From 063f970f0f5e851d72dad0112735692761d6ba36 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Thu, 11 Sep 2025 11:22:12 +0000 Subject: [PATCH 001/556] 8367401: Parallel: Remove unused field in PSKeepAliveClosure Reviewed-by: stefank, fandreuzzi --- src/hotspot/share/gc/parallel/psScavenge.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psScavenge.cpp b/src/hotspot/share/gc/parallel/psScavenge.cpp index 3a47d5864f3..ced84061ec5 100644 --- a/src/hotspot/share/gc/parallel/psScavenge.cpp +++ b/src/hotspot/share/gc/parallel/psScavenge.cpp @@ -148,15 +148,10 @@ public: PSIsAliveClosure PSScavenge::_is_alive_closure; class PSKeepAliveClosure: public OopClosure { -protected: - MutableSpace* _to_space; PSPromotionManager* _promotion_manager; public: PSKeepAliveClosure(PSPromotionManager* pm) : _promotion_manager(pm) { - ParallelScavengeHeap* heap = ParallelScavengeHeap::heap(); - _to_space = heap->young_gen()->to_space(); - assert(_promotion_manager != nullptr, "Sanity"); } From a2d272a02a079e2413d10ad2decb04681ce2f961 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Thu, 11 Sep 2025 11:22:29 +0000 Subject: [PATCH 002/556] 8367339: Parallel: Remove PSScavenge::should_scavenge Reviewed-by: tschatzl, fandreuzzi --- src/hotspot/share/gc/parallel/psCardTable.cpp | 1 - .../share/gc/parallel/psClosure.inline.hpp | 16 ++--- .../share/gc/parallel/psPromotionManager.cpp | 11 +--- .../share/gc/parallel/psPromotionManager.hpp | 3 - .../gc/parallel/psPromotionManager.inline.hpp | 8 +-- src/hotspot/share/gc/parallel/psScavenge.cpp | 2 +- src/hotspot/share/gc/parallel/psScavenge.hpp | 9 --- .../share/gc/parallel/psScavenge.inline.hpp | 63 ------------------- 8 files changed, 13 insertions(+), 100 deletions(-) delete mode 100644 src/hotspot/share/gc/parallel/psScavenge.inline.hpp diff --git a/src/hotspot/share/gc/parallel/psCardTable.cpp b/src/hotspot/share/gc/parallel/psCardTable.cpp index 22a38d816f6..3c40726b721 100644 --- a/src/hotspot/share/gc/parallel/psCardTable.cpp +++ b/src/hotspot/share/gc/parallel/psCardTable.cpp @@ -26,7 +26,6 @@ #include "gc/parallel/parallelScavengeHeap.inline.hpp" #include "gc/parallel/psCardTable.hpp" #include "gc/parallel/psPromotionManager.inline.hpp" -#include "gc/parallel/psScavenge.inline.hpp" #include "gc/parallel/psYoungGen.hpp" #include "memory/iterator.inline.hpp" #include "oops/access.inline.hpp" diff --git a/src/hotspot/share/gc/parallel/psClosure.inline.hpp b/src/hotspot/share/gc/parallel/psClosure.inline.hpp index 914a16e77a6..825612d598a 100644 --- a/src/hotspot/share/gc/parallel/psClosure.inline.hpp +++ b/src/hotspot/share/gc/parallel/psClosure.inline.hpp @@ -28,7 +28,7 @@ // No psClosure.hpp #include "gc/parallel/psPromotionManager.inline.hpp" -#include "gc/parallel/psScavenge.inline.hpp" +#include "gc/parallel/psScavenge.hpp" #include "memory/iterator.hpp" #include "oops/access.inline.hpp" #include "oops/oop.inline.hpp" @@ -39,8 +39,9 @@ public: virtual void do_oop(narrowOop* p) { ShouldNotReachHere(); } virtual void do_oop(oop* p) { - if (PSScavenge::should_scavenge(p)) { - oop o = RawAccess::oop_load(p); + oop o = RawAccess<>::oop_load(p); + if (PSScavenge::is_obj_in_young(o)) { + assert(!PSScavenge::is_obj_in_to_space(o), "Revisiting roots?"); assert(o->is_forwarded(), "Objects are already forwarded before weak processing"); oop new_obj = o->forwardee(); if (log_develop_is_enabled(Trace, gc, scavenge)) { @@ -89,12 +90,11 @@ public: void do_oop(narrowOop* p) { ShouldNotReachHere(); } void do_oop(oop* p) { - ParallelScavengeHeap* psh = ParallelScavengeHeap::heap(); - assert(!psh->is_in_reserved(p), "GC barrier needed"); - if (PSScavenge::should_scavenge(p)) { - assert(PSScavenge::should_scavenge(p, true), "revisiting object?"); + assert(!ParallelScavengeHeap::heap()->is_in_reserved(p), "GC barrier needed"); - oop o = RawAccess::oop_load(p); + oop o = RawAccess<>::oop_load(p); + if (PSScavenge::is_obj_in_young(o)) { + assert(!PSScavenge::is_obj_in_to_space(o), "Revisiting roots?"); oop new_obj = _pm->copy_to_survivor_space(o); RawAccess::oop_store(p, new_obj); diff --git a/src/hotspot/share/gc/parallel/psPromotionManager.cpp b/src/hotspot/share/gc/parallel/psPromotionManager.cpp index 0a463ab7516..90914d87ba4 100644 --- a/src/hotspot/share/gc/parallel/psPromotionManager.cpp +++ b/src/hotspot/share/gc/parallel/psPromotionManager.cpp @@ -27,7 +27,7 @@ #include "gc/parallel/parallelScavengeHeap.hpp" #include "gc/parallel/psOldGen.hpp" #include "gc/parallel/psPromotionManager.inline.hpp" -#include "gc/parallel/psScavenge.inline.hpp" +#include "gc/parallel/psScavenge.hpp" #include "gc/shared/continuationGCSupport.inline.hpp" #include "gc/shared/gcTrace.hpp" #include "gc/shared/partialArraySplitter.inline.hpp" @@ -86,15 +86,6 @@ void PSPromotionManager::initialize() { } } -// Helper functions to get around the circular dependency between -// psScavenge.inline.hpp and psPromotionManager.inline.hpp. -bool PSPromotionManager::should_scavenge(oop* p, bool check_to_space) { - return PSScavenge::should_scavenge(p, check_to_space); -} -bool PSPromotionManager::should_scavenge(narrowOop* p, bool check_to_space) { - return PSScavenge::should_scavenge(p, check_to_space); -} - PSPromotionManager* PSPromotionManager::gc_thread_promotion_manager(uint index) { assert(index < ParallelGCThreads, "index out of range"); assert(_manager_array != nullptr, "Sanity"); diff --git a/src/hotspot/share/gc/parallel/psPromotionManager.hpp b/src/hotspot/share/gc/parallel/psPromotionManager.hpp index f1169c8ad63..20fe3c74a46 100644 --- a/src/hotspot/share/gc/parallel/psPromotionManager.hpp +++ b/src/hotspot/share/gc/parallel/psPromotionManager.hpp @@ -167,9 +167,6 @@ class PSPromotionManager { inline void process_popped_location_depth(ScannerTask task, bool stolen); - static bool should_scavenge(oop* p, bool check_to_space = false); - static bool should_scavenge(narrowOop* p, bool check_to_space = false); - template void copy_and_push_safe_barrier(T* p); diff --git a/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp b/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp index 6154abf1b1c..31c1c445a32 100644 --- a/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp +++ b/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp @@ -31,7 +31,7 @@ #include "gc/parallel/parMarkBitMap.inline.hpp" #include "gc/parallel/psOldGen.hpp" #include "gc/parallel/psPromotionLAB.inline.hpp" -#include "gc/parallel/psScavenge.inline.hpp" +#include "gc/parallel/psScavenge.hpp" #include "gc/parallel/psStringDedup.hpp" #include "gc/shared/continuationGCSupport.inline.hpp" #include "gc/shared/taskqueue.inline.hpp" @@ -139,7 +139,8 @@ inline void PSPromotionManager::push_contents_bounded(oop obj, HeapWord* left, H template inline oop PSPromotionManager::copy_to_survivor_space(oop o) { - assert(should_scavenge(&o), "Sanity"); + assert(PSScavenge::is_obj_in_young(o), "precondition"); + assert(!PSScavenge::is_obj_in_to_space(o), "precondition"); // NOTE! We must be very careful with any methods that access the mark // in o. There may be multiple threads racing on it, and it may be forwarded @@ -235,8 +236,6 @@ inline HeapWord* PSPromotionManager::allocate_in_old_gen(Klass* klass, template inline oop PSPromotionManager::copy_unmarked_to_survivor_space(oop o, markWord test_mark) { - assert(should_scavenge(&o), "Sanity"); - oop new_obj = nullptr; bool new_obj_is_tenured = false; @@ -334,7 +333,6 @@ inline oop PSPromotionManager::copy_unmarked_to_survivor_space(oop o, template inline void PSPromotionManager::copy_and_push_safe_barrier(T* p) { assert(ParallelScavengeHeap::heap()->is_in_reserved(p), "precondition"); - assert(should_scavenge(p, true), "revisiting object?"); oop o = RawAccess::oop_load(p); oop new_obj = copy_to_survivor_space(o); diff --git a/src/hotspot/share/gc/parallel/psScavenge.cpp b/src/hotspot/share/gc/parallel/psScavenge.cpp index ced84061ec5..62d382b40f0 100644 --- a/src/hotspot/share/gc/parallel/psScavenge.cpp +++ b/src/hotspot/share/gc/parallel/psScavenge.cpp @@ -33,7 +33,7 @@ #include "gc/parallel/psParallelCompact.inline.hpp" #include "gc/parallel/psPromotionManager.inline.hpp" #include "gc/parallel/psRootType.hpp" -#include "gc/parallel/psScavenge.inline.hpp" +#include "gc/parallel/psScavenge.hpp" #include "gc/shared/gcCause.hpp" #include "gc/shared/gcHeapSummary.hpp" #include "gc/shared/gcId.hpp" diff --git a/src/hotspot/share/gc/parallel/psScavenge.hpp b/src/hotspot/share/gc/parallel/psScavenge.hpp index 8da555a8bb4..c297a46a46e 100644 --- a/src/hotspot/share/gc/parallel/psScavenge.hpp +++ b/src/hotspot/share/gc/parallel/psScavenge.hpp @@ -100,15 +100,6 @@ class PSScavenge: AllStatic { // Return true iff a young-gc is completed without promotion-failure. static bool invoke(bool clear_soft_refs); - template static inline bool should_scavenge(T* p); - - // These call should_scavenge() above and, if it returns true, also check that - // the object was not newly copied into to_space. The version with the bool - // argument is a convenience wrapper that fetches the to_space pointer from - // the heap and calls the other version (if the arg is true). - template static inline bool should_scavenge(T* p, MutableSpace* to_space); - template static inline bool should_scavenge(T* p, bool check_to_space); - // Is an object in the young generation // This assumes that the 'o' is in the heap, // so it only checks one side of the complete predicate. diff --git a/src/hotspot/share/gc/parallel/psScavenge.inline.hpp b/src/hotspot/share/gc/parallel/psScavenge.inline.hpp deleted file mode 100644 index af3ff4c6165..00000000000 --- a/src/hotspot/share/gc/parallel/psScavenge.inline.hpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2002, 2019, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef SHARE_GC_PARALLEL_PSSCAVENGE_INLINE_HPP -#define SHARE_GC_PARALLEL_PSSCAVENGE_INLINE_HPP - -#include "gc/parallel/psScavenge.hpp" - -#include "gc/parallel/parallelScavengeHeap.hpp" -#include "logging/log.hpp" -#include "memory/iterator.hpp" -#include "memory/resourceArea.hpp" -#include "oops/access.inline.hpp" -#include "oops/oop.inline.hpp" -#include "utilities/globalDefinitions.hpp" - -template inline bool PSScavenge::should_scavenge(T* p) { - T heap_oop = RawAccess<>::oop_load(p); - return PSScavenge::is_obj_in_young(heap_oop); -} - -template -inline bool PSScavenge::should_scavenge(T* p, MutableSpace* to_space) { - if (should_scavenge(p)) { - oop obj = RawAccess::oop_load(p); - // Skip objects copied to to_space since the scavenge started. - HeapWord* const addr = cast_from_oop(obj); - return addr < to_space->bottom() || addr >= to_space->end(); - } - return false; -} - -template -inline bool PSScavenge::should_scavenge(T* p, bool check_to_space) { - if (check_to_space) { - ParallelScavengeHeap* heap = ParallelScavengeHeap::heap(); - return should_scavenge(p, heap->young_gen()->to_space()); - } - return should_scavenge(p); -} - -#endif // SHARE_GC_PARALLEL_PSSCAVENGE_INLINE_HPP From 56f2f7a3af0574357d5d3f99dcd908721ac710e9 Mon Sep 17 00:00:00 2001 From: Roger Riggs Date: Thu, 11 Sep 2025 13:22:20 +0000 Subject: [PATCH 003/556] 8367138: JNI exception pending in os_getCmdlineAndUserInfo of ProcessHandleImpl_macosx.c Reviewed-by: bpb, naoto, jpai, lancea --- src/java.base/macosx/native/libjava/ProcessHandleImpl_macosx.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/java.base/macosx/native/libjava/ProcessHandleImpl_macosx.c b/src/java.base/macosx/native/libjava/ProcessHandleImpl_macosx.c index 9e1d092c57d..2db64ef37b5 100644 --- a/src/java.base/macosx/native/libjava/ProcessHandleImpl_macosx.c +++ b/src/java.base/macosx/native/libjava/ProcessHandleImpl_macosx.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -263,6 +263,7 @@ void os_getCmdlineAndUserInfo(JNIEnv *env, jobject jinfo, pid_t pid) { // on other platforms like Linux/Solaris/AIX where the uid comes from the // same source like the command line info. unix_getUserInfo(env, jinfo, getUID(pid)); + JNU_CHECK_EXCEPTION(env); // Get the maximum size of the arguments mib[0] = CTL_KERN; From 4ea8979b93f80e9ecbc197ee12ceb523ef8da6aa Mon Sep 17 00:00:00 2001 From: Artur Barashev Date: Thu, 11 Sep 2025 13:53:08 +0000 Subject: [PATCH 004/556] 8365953: Key manager returns no certificates when handshakeSession is not an ExtendedSSLSession Reviewed-by: djelinski, wetmore --- .../ssl/X509KeyManagerCertChecking.java | 47 +- .../AlgorithmConstraintsCheck.java | 28 +- ...xtendedSSLSessionAlgorithmConstraints.java | 405 ++++++++++++++++++ 3 files changed, 436 insertions(+), 44 deletions(-) create mode 100644 test/jdk/sun/security/ssl/X509KeyManager/NonExtendedSSLSessionAlgorithmConstraints.java diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java index 00a7ae84352..6f18b80395a 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java @@ -167,25 +167,17 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { return null; } - if (socket != null && socket.isConnected() && - socket instanceof SSLSocket sslSocket) { - + if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) { SSLSession session = sslSocket.getHandshakeSession(); - if (session != null) { - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - String[] peerSupportedSignAlgs = null; - - if (session instanceof ExtendedSSLSession extSession) { - // Peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - peerSupportedSignAlgs = - extSession.getPeerSupportedSignatureAlgorithms(); - } - - return SSLAlgorithmConstraints.forSocket( - sslSocket, peerSupportedSignAlgs, true); - } + if (session instanceof ExtendedSSLSession extSession + && ProtocolVersion.useTLS12PlusSpec( + extSession.getProtocol())) { + // Use peer supported certificate signature algorithms + // sent with "signature_algorithms_cert" TLS extension. + return SSLAlgorithmConstraints.forSocket(sslSocket, + extSession.getPeerSupportedSignatureAlgorithms(), + true); } return SSLAlgorithmConstraints.forSocket(sslSocket, true); @@ -203,20 +195,15 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { if (engine != null) { SSLSession session = engine.getHandshakeSession(); - if (session != null) { - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - String[] peerSupportedSignAlgs = null; - if (session instanceof ExtendedSSLSession extSession) { - // Peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - peerSupportedSignAlgs = - extSession.getPeerSupportedSignatureAlgorithms(); - } - - return SSLAlgorithmConstraints.forEngine( - engine, peerSupportedSignAlgs, true); - } + if (session instanceof ExtendedSSLSession extSession + && ProtocolVersion.useTLS12PlusSpec( + extSession.getProtocol())) { + // Use peer supported certificate signature algorithms + // sent with "signature_algorithms_cert" TLS extension. + return SSLAlgorithmConstraints.forEngine(engine, + extSession.getPeerSupportedSignatureAlgorithms(), + true); } } diff --git a/test/jdk/sun/security/ssl/X509KeyManager/AlgorithmConstraintsCheck.java b/test/jdk/sun/security/ssl/X509KeyManager/AlgorithmConstraintsCheck.java index 997fde5a07a..4caa7b6b944 100644 --- a/test/jdk/sun/security/ssl/X509KeyManager/AlgorithmConstraintsCheck.java +++ b/test/jdk/sun/security/ssl/X509KeyManager/AlgorithmConstraintsCheck.java @@ -57,31 +57,31 @@ import sun.security.x509.X500Name; * @modules java.base/sun.security.x509 * java.base/sun.security.util * @library /test/lib - * @run main/othervm AlgorithmConstraintsCheck false SunX509 SHA256withRSA - * @run main/othervm AlgorithmConstraintsCheck true SunX509 SHA256withRSA - * @run main/othervm AlgorithmConstraintsCheck false PKIX SHA256withRSA - * @run main/othervm AlgorithmConstraintsCheck true PKIX SHA256withRSA + * @run main/othervm AlgorithmConstraintsCheck false SunX509 + * @run main/othervm AlgorithmConstraintsCheck true SunX509 + * @run main/othervm AlgorithmConstraintsCheck false PKIX + * @run main/othervm AlgorithmConstraintsCheck true PKIX */ public class AlgorithmConstraintsCheck { - private static final String CERT_ALIAS = "testalias"; - private static final String KEY_TYPE = "RSA"; + protected static final String CERT_ALIAS = "testalias"; + protected static final String KEY_TYPE = "EC"; + protected static final String CERT_SIG_ALG = "SHA256withECDSA"; public static void main(String[] args) throws Exception { - if (args.length != 3) { + if (args.length != 2) { throw new RuntimeException("Wrong number of arguments"); } String enabled = args[0]; String kmAlg = args[1]; - String certSignatureAlg = args[2]; System.setProperty("jdk.tls.SunX509KeyManager.certChecking", enabled); - SecurityUtils.addToDisabledTlsAlgs(certSignatureAlg); + SecurityUtils.addToDisabledTlsAlgs(CERT_SIG_ALG); X509ExtendedKeyManager km = (X509ExtendedKeyManager) getKeyManager( - kmAlg, certSignatureAlg); + kmAlg, KEY_TYPE, CERT_SIG_ALG); String serverAlias = km.chooseServerAlias(KEY_TYPE, null, null); String engineServerAlias = km.chooseEngineServerAlias( KEY_TYPE, null, null); @@ -108,13 +108,13 @@ public class AlgorithmConstraintsCheck { } // PKIX KeyManager adds a cache prefix to an alias. - private static String normalizeAlias(String alias) { + protected static String normalizeAlias(String alias) { return alias.substring(alias.lastIndexOf(".") + 1); } - private static X509KeyManager getKeyManager(String kmAlg, - String certSignatureAlg) throws Exception { - KeyPairGenerator kpg = KeyPairGenerator.getInstance(KEY_TYPE); + protected static X509KeyManager getKeyManager(String kmAlg, + String keyAlg, String certSignatureAlg) throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlg); KeyPair caKeys = kpg.generateKeyPair(); KeyPair endpointKeys = kpg.generateKeyPair(); diff --git a/test/jdk/sun/security/ssl/X509KeyManager/NonExtendedSSLSessionAlgorithmConstraints.java b/test/jdk/sun/security/ssl/X509KeyManager/NonExtendedSSLSessionAlgorithmConstraints.java new file mode 100644 index 00000000000..30ec655f7b2 --- /dev/null +++ b/test/jdk/sun/security/ssl/X509KeyManager/NonExtendedSSLSessionAlgorithmConstraints.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8365953 + * @summary Key manager returns no certificates when handshakeSession is not + * an ExtendedSSLSession + * @modules java.base/sun.security.x509 + * java.base/sun.security.util + * @library /test/lib + * @run main/othervm NonExtendedSSLSessionAlgorithmConstraints + */ + +import static jdk.test.lib.Asserts.assertEquals; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.Principal; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509ExtendedKeyManager; + +/* + * Make sure Key Managers return the certificates when SSLSocket or SSLEngine + * use an SSLSession which is not extending ExtendedSSLSession. + */ +public class NonExtendedSSLSessionAlgorithmConstraints extends + AlgorithmConstraintsCheck { + + public static void main(String[] args) throws Exception { + new NonExtendedSSLSessionAlgorithmConstraints().runTest(); + } + + private void runTest() throws Exception { + for (String kmAlg : new String[]{"SunX509", "PKIX"}) { + + X509ExtendedKeyManager km = + (X509ExtendedKeyManager) getKeyManager( + kmAlg, KEY_TYPE, CERT_SIG_ALG); + var testSocket = new TestHandshakeSessionSSLSocket(); + var testEngine = new TestHandshakeSessionSSLEngine(); + + // Test SSLSocket + assertEquals(CERT_ALIAS, normalizeAlias(km.chooseServerAlias( + KEY_TYPE, null, testSocket))); + assertEquals(CERT_ALIAS, normalizeAlias(km.chooseClientAlias( + new String[]{KEY_TYPE}, null, testSocket))); + + // Test SSLEngine + assertEquals(CERT_ALIAS, normalizeAlias(km.chooseEngineServerAlias( + KEY_TYPE, null, testEngine))); + assertEquals(CERT_ALIAS, normalizeAlias(km.chooseEngineClientAlias( + new String[]{KEY_TYPE}, null, testEngine))); + } + } + + private static class TestHandshakeSessionSSLSocket extends SSLSocket { + + TestHandshakeSessionSSLSocket() { + } + + @Override + public SSLSession getHandshakeSession() { + return new TestSSLSession(); + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public SSLSession getSession() { + return null; + } + + @Override + public String[] getSupportedCipherSuites() { + return null; + } + + @Override + public String[] getSupportedProtocols() { + return null; + } + + @Override + public String[] getEnabledCipherSuites() { + return null; + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + } + + @Override + public String[] getEnabledProtocols() { + return null; + } + + @Override + public void setEnabledProtocols(String[] protocols) { + } + + @Override + public void addHandshakeCompletedListener + (HandshakeCompletedListener listener) { + } + + @Override + public void removeHandshakeCompletedListener + (HandshakeCompletedListener listener) { + } + + @Override + public void startHandshake() throws IOException { + } + + @Override + public void setUseClientMode(boolean mode) { + } + + @Override + public boolean getUseClientMode() { + return false; + } + + @Override + public void setNeedClientAuth(boolean need) { + } + + @Override + public boolean getNeedClientAuth() { + return false; + } + + @Override + public void setWantClientAuth(boolean want) { + } + + @Override + public boolean getWantClientAuth() { + return false; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + } + + @Override + public boolean getEnableSessionCreation() { + return true; + } + } + + private static class TestHandshakeSessionSSLEngine extends SSLEngine { + + @Override + public SSLSession getHandshakeSession() { + return new TestSSLSession(); + } + + @Override + public String[] getEnabledProtocols() { + return null; + } + + @Override + public SSLEngineResult wrap(ByteBuffer[] src, int off, int len, + ByteBuffer dst) throws SSLException { + return null; + } + + @Override + public SSLEngineResult unwrap(ByteBuffer src, + ByteBuffer[] dst, int off, int len) + throws SSLException { + return null; + } + + @Override + public Runnable getDelegatedTask() { + return null; + } + + @Override + public void closeInbound() { + } + + @Override + public boolean isInboundDone() { + return false; + } + + @Override + public void closeOutbound() { + } + + @Override + public boolean isOutboundDone() { + return false; + } + + @Override + public String[] getEnabledCipherSuites() { + return null; + } + + @Override + public String[] getSupportedCipherSuites() { + return null; + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + } + + @Override + public String[] getSupportedProtocols() { + return null; + } + + @Override + public void setEnabledProtocols(String[] protocols) { + } + + @Override + public SSLSession getSession() { + return null; + } + + @Override + public void beginHandshake() { + } + + @Override + public SSLEngineResult.HandshakeStatus getHandshakeStatus() { + return null; + } + + @Override + public void setUseClientMode(boolean mode) { + } + + @Override + public boolean getUseClientMode() { + return false; + } + + public void setNeedClientAuth(boolean need) { + } + + @Override + public boolean getNeedClientAuth() { + return false; + } + + @Override + public void setWantClientAuth(boolean need) { + } + + @Override + public boolean getWantClientAuth() { + return false; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + } + + @Override + public boolean getEnableSessionCreation() { + return false; + } + } + + public static class TestSSLSession implements SSLSession { + + TestSSLSession() { + } + + @Override + public String getProtocol() { + return "TLSv1.3"; + } + + @Override + public byte[] getId() { + return null; + } + + @Override + public SSLSessionContext getSessionContext() { + return null; + } + + @Override + public long getCreationTime() { + return 0; + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public void invalidate() { + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public void putValue(String name, Object value) { + } + + @Override + public Object getValue(String name) { + return null; + } + + @Override + public void removeValue(String name) { + } + + @Override + public String[] getValueNames() { + return null; + } + + @Override + public java.security.cert.Certificate[] getPeerCertificates() { + return new java.security.cert.Certificate[0]; + } + + @Override + public java.security.cert.Certificate[] getLocalCertificates() { + return new java.security.cert.Certificate[0]; + } + + @Override + public Principal getPeerPrincipal() { + return null; + } + + @Override + public Principal getLocalPrincipal() { + return null; + } + + @Override + public String getCipherSuite() { + return null; + } + + @Override + public String getPeerHost() { + return null; + } + + @Override + public int getPeerPort() { + return 0; + } + + @Override + public int getPacketBufferSize() { + return 0; + } + + @Override + public int getApplicationBufferSize() { + return 0; + } + } +} From 781f2b2f8188c02a6af220ebcc5bc8158fe8423e Mon Sep 17 00:00:00 2001 From: Pasam Soujanya Date: Thu, 11 Sep 2025 13:58:51 +0000 Subject: [PATCH 005/556] 8366278: Form control element + + + """; + Document doc = kit.createDefaultDocument(); + editorPane.setDocument(doc); + editorPane.setText(htmlString); + + frame.add(scrollPane, BorderLayout.CENTER); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(new Dimension(400, 200)); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + private static boolean containsAlt(Container container) { + for (Component c : container.getComponents()) { + if (c instanceof JButton button) { + return "Logo".equals(button.getText()); + } else if (c instanceof Container cont) { + return containsAlt(cont); + } + } + return false; + } +} From 64155dfac068cf01bcab6adb401b360499f33a5f Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Mon, 15 Sep 2025 21:10:26 +0000 Subject: [PATCH 048/556] 8367237: Thread-Safety Usage Warning for java.text.Collator Classes Reviewed-by: iris, naoto --- src/java.base/share/classes/java/text/Collator.java | 9 +++++++-- .../share/classes/java/text/RuleBasedCollator.java | 11 ++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/java.base/share/classes/java/text/Collator.java b/src/java.base/share/classes/java/text/Collator.java index 276a66cdc07..5e576cd800f 100644 --- a/src/java.base/share/classes/java/text/Collator.java +++ b/src/java.base/share/classes/java/text/Collator.java @@ -111,8 +111,13 @@ import sun.util.locale.provider.LocaleServiceProviderPool; *
* @apiNote {@code CollationKey}s from different * {@code Collator}s can not be compared. See the class description - * for {@link CollationKey} - * for an example using {@code CollationKey}s. + * for {@link CollationKey} for an example using {@code CollationKey}s. + * + * @implNote Significant thread contention may occur during concurrent usage + * of the JDK Reference Implementation's {@link RuleBasedCollator}, which is the + * subtype returned by the default provider of the {@link #getInstance()} factory + * methods. As such, users should consider retrieving a separate instance for + * each thread when used in multithreaded environments. * * @see RuleBasedCollator * @see CollationKey diff --git a/src/java.base/share/classes/java/text/RuleBasedCollator.java b/src/java.base/share/classes/java/text/RuleBasedCollator.java index dc45dafb846..af1b6b62bdf 100644 --- a/src/java.base/share/classes/java/text/RuleBasedCollator.java +++ b/src/java.base/share/classes/java/text/RuleBasedCollator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,10 +38,6 @@ package java.text; -import java.text.Normalizer; -import java.util.Vector; -import java.util.Locale; - /** * The {@code RuleBasedCollator} class is a concrete subclass of * {@code Collator} that provides a simple, data-driven, table @@ -239,6 +235,11 @@ import java.util.Locale; * * * + * @implNote For this implementation, concurrent usage of this class may + * lead to significant thread contention since {@code synchronized} is employed + * to ensure thread-safety. As such, users of this class should consider creating + * a separate instance for each thread when used in multithreaded environments. + * * @see Collator * @see CollationElementIterator * @author Helena Shih, Laura Werner, Richard Gillam From 242558484985cb954b0e658776fd59cbca1be1db Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Tue, 16 Sep 2025 01:04:48 +0000 Subject: [PATCH 049/556] 8367142: Avoid InstanceKlass::cast when converting java mirror to InstanceKlass Reviewed-by: dholmes, coleenp --- src/hotspot/share/cds/aotMetaspace.cpp | 4 +- src/hotspot/share/cds/unregisteredClasses.cpp | 2 +- src/hotspot/share/classfile/javaClasses.cpp | 27 +++--- src/hotspot/share/classfile/javaClasses.hpp | 12 +-- .../share/classfile/javaClasses.inline.hpp | 6 ++ .../share/classfile/systemDictionary.cpp | 4 +- .../jfr/leakprofiler/chains/edgeUtils.cpp | 2 +- src/hotspot/share/jvmci/jvmciRuntime.cpp | 2 +- src/hotspot/share/prims/jni.cpp | 2 +- src/hotspot/share/prims/jvm.cpp | 82 +++++++------------ src/hotspot/share/prims/methodHandles.cpp | 2 +- src/hotspot/share/prims/unsafe.cpp | 2 +- src/hotspot/share/prims/whitebox.cpp | 26 +++--- src/hotspot/share/runtime/reflection.cpp | 4 +- src/hotspot/share/runtime/sharedRuntime.cpp | 4 +- 15 files changed, 80 insertions(+), 101 deletions(-) diff --git a/src/hotspot/share/cds/aotMetaspace.cpp b/src/hotspot/share/cds/aotMetaspace.cpp index 01bc4708eb5..b3f859fc4a8 100644 --- a/src/hotspot/share/cds/aotMetaspace.cpp +++ b/src/hotspot/share/cds/aotMetaspace.cpp @@ -785,7 +785,7 @@ void AOTMetaspace::link_all_loaded_classes(JavaThread* current) { const GrowableArray* mirrors = collect_classes.mirrors(); for (int i = 0; i < mirrors->length(); i++) { OopHandle mirror = mirrors->at(i); - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(mirror.resolve())); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror.resolve()); if (may_be_eagerly_linked(ik)) { has_linked |= try_link_class(current, ik); } @@ -812,7 +812,7 @@ void AOTMetaspace::link_shared_classes(TRAPS) { const GrowableArray* mirrors = collect_classes.mirrors(); for (int i = 0; i < mirrors->length(); i++) { OopHandle mirror = mirrors->at(i); - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(mirror.resolve())); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror.resolve()); AOTConstantPoolResolver::preresolve_string_cp_entries(ik, CHECK); } } diff --git a/src/hotspot/share/cds/unregisteredClasses.cpp b/src/hotspot/share/cds/unregisteredClasses.cpp index 31cfbd15d67..51b35899599 100644 --- a/src/hotspot/share/cds/unregisteredClasses.cpp +++ b/src/hotspot/share/cds/unregisteredClasses.cpp @@ -94,7 +94,7 @@ InstanceKlass* UnregisteredClasses::load_class(Symbol* name, const char* path, T CHECK_NULL); assert(result.get_type() == T_OBJECT, "just checking"); - return InstanceKlass::cast(java_lang_Class::as_Klass(result.get_oop())); + return java_lang_Class::as_InstanceKlass(result.get_oop()); } bool UnregisteredClasses::check_for_exclusion(const InstanceKlass* k) { diff --git a/src/hotspot/share/classfile/javaClasses.cpp b/src/hotspot/share/classfile/javaClasses.cpp index 86a3b22fbbb..da093936ce5 100644 --- a/src/hotspot/share/classfile/javaClasses.cpp +++ b/src/hotspot/share/classfile/javaClasses.cpp @@ -990,7 +990,7 @@ void java_lang_Class::fixup_mirror(Klass* k, TRAPS) { create_mirror(k, Handle(), Handle(), Handle(), Handle(), CHECK); } -void java_lang_Class::initialize_mirror_fields(Klass* k, +void java_lang_Class::initialize_mirror_fields(InstanceKlass* ik, Handle mirror, Handle protection_domain, Handle classData, @@ -1005,7 +1005,7 @@ void java_lang_Class::initialize_mirror_fields(Klass* k, set_protection_domain(mirror(), protection_domain()); // Initialize static fields - InstanceKlass::cast(k)->do_local_static_fields(&initialize_static_field, mirror, CHECK); + ik->do_local_static_fields(&initialize_static_field, mirror, CHECK); // Set classData set_class_data(mirror(), classData()); @@ -1111,8 +1111,7 @@ void java_lang_Class::allocate_mirror(Klass* k, bool is_scratch, Handle protecti // and java_mirror in this klass. } else { assert(k->is_instance_klass(), "Must be"); - - initialize_mirror_fields(k, mirror, protection_domain, classData, THREAD); + initialize_mirror_fields(InstanceKlass::cast(k), mirror, protection_domain, classData, THREAD); if (HAS_PENDING_EXCEPTION) { // If any of the fields throws an exception like OOM remove the klass field // from the mirror so GC doesn't follow it after the klass has been deallocated. @@ -2590,7 +2589,7 @@ static void print_stack_element_to_stream(outputStream* st, Handle mirror, int m ResourceMark rm; stringStream ss; - InstanceKlass* holder = InstanceKlass::cast(java_lang_Class::as_Klass(mirror())); + InstanceKlass* holder = java_lang_Class::as_InstanceKlass(mirror()); const char* klass_name = holder->external_name(); char* method_name = name->as_C_string(); ss.print("\tat %s.%s(", klass_name, method_name); @@ -2969,7 +2968,7 @@ void java_lang_Throwable::get_stack_trace_elements(int depth, Handle backtrace, THROW(vmSymbols::java_lang_NullPointerException()); } - InstanceKlass* holder = InstanceKlass::cast(java_lang_Class::as_Klass(bte._mirror())); + InstanceKlass* holder = java_lang_Class::as_InstanceKlass(bte._mirror()); methodHandle method (THREAD, holder->method_with_orig_idnum(bte._method_id, bte._version)); java_lang_StackTraceElement::fill_in(stack_trace_element, holder, @@ -3055,7 +3054,7 @@ bool java_lang_Throwable::get_top_method_and_bci(oop throwable, Method** method, // Get first backtrace element. BacktraceElement bte = iter.next(current); - InstanceKlass* holder = InstanceKlass::cast(java_lang_Class::as_Klass(bte._mirror())); + InstanceKlass* holder = java_lang_Class::as_InstanceKlass(bte._mirror()); assert(holder != nullptr, "first element should be non-null"); Method* m = holder->method_with_orig_idnum(bte._method_id, bte._version); @@ -3441,11 +3440,11 @@ void java_lang_reflect_Method::serialize_offsets(SerializeClosure* f) { Handle java_lang_reflect_Method::create(TRAPS) { assert(Universe::is_fully_initialized(), "Need to find another solution to the reflection problem"); - Klass* klass = vmClasses::reflect_Method_klass(); + InstanceKlass* klass = vmClasses::reflect_Method_klass(); // This class is eagerly initialized during VM initialization, since we keep a reference // to one of the methods - assert(InstanceKlass::cast(klass)->is_initialized(), "must be initialized"); - return InstanceKlass::cast(klass)->allocate_instance_handle(THREAD); + assert(klass->is_initialized(), "must be initialized"); + return klass->allocate_instance_handle(THREAD); } oop java_lang_reflect_Method::clazz(oop reflect) { @@ -3914,17 +3913,15 @@ void reflect_ConstantPool::set_cp(oop reflect, ConstantPool* value) { } ConstantPool* reflect_ConstantPool::get_cp(oop reflect) { - oop mirror = reflect->obj_field(_oop_offset); - Klass* k = java_lang_Class::as_Klass(mirror); - assert(k->is_instance_klass(), "Must be"); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror); // Get the constant pool back from the klass. Since class redefinition // merges the new constant pool into the old, this is essentially the // same constant pool as the original. If constant pool merging is // no longer done in the future, this will have to change to save // the original. - return InstanceKlass::cast(k)->constants(); + return ik->constants(); } @@ -5531,7 +5528,7 @@ void JavaClasses::check_offsets() { #endif // PRODUCT int InjectedField::compute_offset() { - InstanceKlass* ik = InstanceKlass::cast(klass()); + InstanceKlass* ik = klass(); for (AllFieldStream fs(ik); !fs.done(); fs.next()) { if (!may_be_java && !fs.field_flags().is_injected()) { // Only look at injected fields diff --git a/src/hotspot/share/classfile/javaClasses.hpp b/src/hotspot/share/classfile/javaClasses.hpp index 70fa519f0f0..6f82ca10fd6 100644 --- a/src/hotspot/share/classfile/javaClasses.hpp +++ b/src/hotspot/share/classfile/javaClasses.hpp @@ -269,7 +269,7 @@ class java_lang_Class : AllStatic { static void set_protection_domain(oop java_class, oop protection_domain); static void set_class_loader(oop java_class, oop class_loader); static void set_component_mirror(oop java_class, oop comp_mirror); - static void initialize_mirror_fields(Klass* k, Handle mirror, Handle protection_domain, + static void initialize_mirror_fields(InstanceKlass* ik, Handle mirror, Handle protection_domain, Handle classData, TRAPS); static void set_mirror_module_field(JavaThread* current, Klass* K, Handle mirror, Handle module); public: @@ -293,8 +293,10 @@ class java_lang_Class : AllStatic { static void fixup_module_field(Klass* k, Handle module); - // Conversion + // Conversion -- java_class must not be null. The return value is null only if java_class is a primitive type. static Klass* as_Klass(oop java_class); + static InstanceKlass* as_InstanceKlass(oop java_class); + static void set_klass(oop java_class, Klass* klass); static BasicType as_BasicType(oop java_class, Klass** reference_klass = nullptr); static Symbol* as_signature(oop java_class, bool intern_if_not_found); @@ -1895,11 +1897,11 @@ class InjectedField { const vmClassID klass_id; const vmSymbolID name_index; const vmSymbolID signature_index; - const bool may_be_java; + const bool may_be_java; - Klass* klass() const { return vmClasses::klass_at(klass_id); } - Symbol* name() const { return lookup_symbol(name_index); } + InstanceKlass* klass() const { return vmClasses::klass_at(klass_id); } + Symbol* name() const { return lookup_symbol(name_index); } Symbol* signature() const { return lookup_symbol(signature_index); } int compute_offset(); diff --git a/src/hotspot/share/classfile/javaClasses.inline.hpp b/src/hotspot/share/classfile/javaClasses.inline.hpp index 3cbbd2c12f2..21ad62f8408 100644 --- a/src/hotspot/share/classfile/javaClasses.inline.hpp +++ b/src/hotspot/share/classfile/javaClasses.inline.hpp @@ -291,6 +291,12 @@ inline Klass* java_lang_Class::as_Klass(oop java_class) { return k; } +inline InstanceKlass* java_lang_Class::as_InstanceKlass(oop java_class) { + Klass* k = as_Klass(java_class); + assert(k == nullptr || k->is_instance_klass(), "type check"); + return static_cast(k); +} + inline bool java_lang_Class::is_primitive(oop java_class) { // should assert: // assert(java_lang_Class::is_instance(java_class), "must be a Class object"); diff --git a/src/hotspot/share/classfile/systemDictionary.cpp b/src/hotspot/share/classfile/systemDictionary.cpp index 22d4fd1892e..95f86a8950b 100644 --- a/src/hotspot/share/classfile/systemDictionary.cpp +++ b/src/hotspot/share/classfile/systemDictionary.cpp @@ -1277,10 +1277,10 @@ InstanceKlass* SystemDictionary::load_instance_class_impl(Symbol* class_name, Ha assert(result.get_type() == T_OBJECT, "just checking"); oop obj = result.get_oop(); - // Primitive classes return null since forName() can not be + // Primitive classes return null since forName() cannot be // used to obtain any of the Class objects representing primitives or void if ((obj != nullptr) && !(java_lang_Class::is_primitive(obj))) { - InstanceKlass* k = InstanceKlass::cast(java_lang_Class::as_Klass(obj)); + InstanceKlass* k = java_lang_Class::as_InstanceKlass(obj); // For user defined Java class loaders, check that the name returned is // the same as that requested. This check is done for the bootstrap // loader when parsing the class file. diff --git a/src/hotspot/share/jfr/leakprofiler/chains/edgeUtils.cpp b/src/hotspot/share/jfr/leakprofiler/chains/edgeUtils.cpp index d431d98e383..da098634ba6 100644 --- a/src/hotspot/share/jfr/leakprofiler/chains/edgeUtils.cpp +++ b/src/hotspot/share/jfr/leakprofiler/chains/edgeUtils.cpp @@ -65,7 +65,7 @@ const Symbol* EdgeUtils::field_name(const Edge& edge, jshort* modifiers) { if (is_static_field(ref_owner, ik, offset)) { assert(ik->is_mirror_instance_klass(), "invariant"); assert(java_lang_Class::as_Klass(ref_owner)->is_instance_klass(), "invariant"); - ik = InstanceKlass::cast(java_lang_Class::as_Klass(ref_owner)); + ik = java_lang_Class::as_InstanceKlass(ref_owner); } while (ik != nullptr) { JavaFieldStream jfs(ik); diff --git a/src/hotspot/share/jvmci/jvmciRuntime.cpp b/src/hotspot/share/jvmci/jvmciRuntime.cpp index c7c3a00a127..137782f93ef 100644 --- a/src/hotspot/share/jvmci/jvmciRuntime.cpp +++ b/src/hotspot/share/jvmci/jvmciRuntime.cpp @@ -187,7 +187,7 @@ JRT_ENTRY(void, JVMCIRuntime::dynamic_new_array_or_null(JavaThread* current, oop JRT_END JRT_ENTRY(void, JVMCIRuntime::dynamic_new_instance_or_null(JavaThread* current, oopDesc* type_mirror)) - InstanceKlass* klass = InstanceKlass::cast(java_lang_Class::as_Klass(type_mirror)); + InstanceKlass* klass = java_lang_Class::as_InstanceKlass(type_mirror); if (klass == nullptr) { ResourceMark rm(current); diff --git a/src/hotspot/share/prims/jni.cpp b/src/hotspot/share/prims/jni.cpp index 0e469dd7f84..34d2d614d22 100644 --- a/src/hotspot/share/prims/jni.cpp +++ b/src/hotspot/share/prims/jni.cpp @@ -526,7 +526,7 @@ JNI_ENTRY(jint, jni_ThrowNew(JNIEnv *env, jclass clazz, const char *message)) jint ret = JNI_OK; DT_RETURN_MARK(ThrowNew, jint, (const jint&)ret); - InstanceKlass* k = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz))); + InstanceKlass* k = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(clazz)); Symbol* name = k->name(); Handle class_loader (THREAD, k->class_loader()); THROW_MSG_LOADER_(name, (char *)message, class_loader, JNI_OK); diff --git a/src/hotspot/share/prims/jvm.cpp b/src/hotspot/share/prims/jvm.cpp index daacfd4ab7a..2cbe764994d 100644 --- a/src/hotspot/share/prims/jvm.cpp +++ b/src/hotspot/share/prims/jvm.cpp @@ -840,14 +840,9 @@ JVM_ENTRY(jclass, JVM_FindClassFromClass(JNIEnv *env, const char *name, if (log_is_enabled(Debug, class, resolve) && result != nullptr) { // this function is generally only used for class loading during verification. ResourceMark rm; - oop from_mirror = JNIHandles::resolve_non_null(from); - Klass* from_class = java_lang_Class::as_Klass(from_mirror); - const char * from_name = from_class->external_name(); - - oop mirror = JNIHandles::resolve_non_null(result); - Klass* to_class = java_lang_Class::as_Klass(mirror); - const char * to = to_class->external_name(); - log_debug(class, resolve)("%s %s (verification)", from_name, to); + const char* from_name = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(from))->external_name(); + const char* to_name = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(result))->external_name(); + log_debug(class, resolve)("%s %s (verification)", from_name, to_name); } #if INCLUDE_CDS @@ -918,12 +913,12 @@ static jclass jvm_lookup_define_class(jclass lookup, const char *name, jboolean init, int flags, jobject classData, TRAPS) { ResourceMark rm(THREAD); - Klass* lookup_k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(lookup)); - // Lookup class must be a non-null instance + InstanceKlass* lookup_k = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(lookup)); + // Lookup class must not be a primitive class (whose mirror has a null Klass*) if (lookup_k == nullptr) { + // The error message is wrong. We come here only if lookup is a primitive class THROW_MSG_NULL(vmSymbols::java_lang_IllegalArgumentException(), "Lookup class is null"); } - assert(lookup_k->is_instance_klass(), "Lookup class must be an instance klass"); Handle class_loader (THREAD, lookup_k->class_loader()); @@ -934,7 +929,7 @@ static jclass jvm_lookup_define_class(jclass lookup, const char *name, InstanceKlass* host_class = nullptr; if (is_nestmate) { - host_class = InstanceKlass::cast(lookup_k)->nest_host(CHECK_NULL); + host_class = lookup_k->nest_host(CHECK_NULL); } log_info(class, nestmates)("LookupDefineClass: %s - %s%s, %s, %s, %s", @@ -1265,7 +1260,7 @@ JVM_ENTRY(jobjectArray, JVM_GetDeclaredClasses(JNIEnv *env, jclass ofClass)) return (jobjectArray)JNIHandles::make_local(THREAD, result); } - InstanceKlass* k = InstanceKlass::cast(java_lang_Class::as_Klass(ofMirror)); + InstanceKlass* k = java_lang_Class::as_InstanceKlass(ofMirror); InnerClassesIterator iter(k); if (iter.length() == 0) { @@ -1404,11 +1399,10 @@ static bool jvm_get_field_common(jobject field, fieldDescriptor& fd) { oop reflected = JNIHandles::resolve_non_null(field); oop mirror = java_lang_reflect_Field::clazz(reflected); - Klass* k = java_lang_Class::as_Klass(mirror); int slot = java_lang_reflect_Field::slot(reflected); int modifiers = java_lang_reflect_Field::modifiers(reflected); - InstanceKlass* ik = InstanceKlass::cast(k); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror); int offset = ik->field_offset(slot); if (modifiers & JVM_ACC_STATIC) { @@ -1444,9 +1438,9 @@ static Method* jvm_get_method_common(jobject method) { mirror = java_lang_reflect_Method::clazz(reflected); slot = java_lang_reflect_Method::slot(reflected); } - Klass* k = java_lang_Class::as_Klass(mirror); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror); - Method* m = InstanceKlass::cast(k)->method_with_idnum(slot); + Method* m = ik->method_with_idnum(slot); assert(m != nullptr, "cannot find method"); return m; // caller has to deal with null in product mode } @@ -1570,7 +1564,7 @@ JVM_ENTRY(jobjectArray, JVM_GetClassDeclaredFields(JNIEnv *env, jclass ofClass, return (jobjectArray) JNIHandles::make_local(THREAD, res); } - InstanceKlass* k = InstanceKlass::cast(java_lang_Class::as_Klass(ofMirror)); + InstanceKlass* k = java_lang_Class::as_InstanceKlass(ofMirror); constantPoolHandle cp(THREAD, k->constants()); // Ensure class is linked @@ -1627,9 +1621,7 @@ JVM_END // even if the class is not a record. JVM_ENTRY(jobjectArray, JVM_GetRecordComponents(JNIEnv* env, jclass ofClass)) { - Klass* c = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(ofClass)); - assert(c->is_instance_klass(), "must be"); - InstanceKlass* ik = InstanceKlass::cast(c); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(ofClass)); Array* components = ik->record_components(); if (components != nullptr) { @@ -1671,7 +1663,7 @@ static jobjectArray get_class_declared_methods_helper( return (jobjectArray) JNIHandles::make_local(THREAD, res); } - InstanceKlass* k = InstanceKlass::cast(java_lang_Class::as_Klass(ofMirror)); + InstanceKlass* k = java_lang_Class::as_InstanceKlass(ofMirror); // Ensure class is linked k->link_class(CHECK_NULL); @@ -1750,23 +1742,17 @@ JVM_END JVM_ENTRY(jboolean, JVM_AreNestMates(JNIEnv *env, jclass current, jclass member)) { - Klass* c = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(current)); - assert(c->is_instance_klass(), "must be"); - InstanceKlass* ck = InstanceKlass::cast(c); - Klass* m = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(member)); - assert(m->is_instance_klass(), "must be"); - InstanceKlass* mk = InstanceKlass::cast(m); - return ck->has_nestmate_access_to(mk, THREAD); + InstanceKlass* c = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(current)); + InstanceKlass* m = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(member)); + return c->has_nestmate_access_to(m, THREAD); } JVM_END JVM_ENTRY(jclass, JVM_GetNestHost(JNIEnv* env, jclass current)) { // current is not a primitive or array class - Klass* c = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(current)); - assert(c->is_instance_klass(), "must be"); - InstanceKlass* ck = InstanceKlass::cast(c); - InstanceKlass* host = ck->nest_host(THREAD); + InstanceKlass* c = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(current)); + InstanceKlass* host = c->nest_host(THREAD); return (jclass) (host == nullptr ? nullptr : JNIHandles::make_local(THREAD, host->java_mirror())); } @@ -1776,13 +1762,11 @@ JVM_ENTRY(jobjectArray, JVM_GetNestMembers(JNIEnv* env, jclass current)) { // current is not a primitive or array class ResourceMark rm(THREAD); - Klass* c = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(current)); - assert(c->is_instance_klass(), "must be"); - InstanceKlass* ck = InstanceKlass::cast(c); - InstanceKlass* host = ck->nest_host(THREAD); + InstanceKlass* c = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(current)); + InstanceKlass* host = c->nest_host(THREAD); log_trace(class, nestmates)("Calling GetNestMembers for type %s with nest-host %s", - ck->external_name(), host->external_name()); + c->external_name(), host->external_name()); { JvmtiVMObjectAllocEventCollector oam; Array* members = host->nest_members(); @@ -1845,7 +1829,7 @@ JVM_ENTRY(jobjectArray, JVM_GetNestMembers(JNIEnv* env, jclass current)) } } else { - assert(host == ck || ck->is_hidden(), "must be singleton nest or dynamic nestmate"); + assert(host == c || c->is_hidden(), "must be singleton nest or dynamic nestmate"); } return (jobjectArray)JNIHandles::make_local(THREAD, result()); } @@ -1856,9 +1840,8 @@ JVM_ENTRY(jobjectArray, JVM_GetPermittedSubclasses(JNIEnv* env, jclass current)) { oop mirror = JNIHandles::resolve_non_null(current); assert(!java_lang_Class::is_primitive(mirror), "should not be"); - Klass* c = java_lang_Class::as_Klass(mirror); - assert(c->is_instance_klass(), "must be"); - InstanceKlass* ik = InstanceKlass::cast(c); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror); + ResourceMark rm(THREAD); log_trace(class, sealed)("Calling GetPermittedSubclasses for %s type %s", ik->is_sealed() ? "sealed" : "non-sealed", ik->external_name()); @@ -3379,16 +3362,14 @@ JVM_ENTRY(void, JVM_RegisterLambdaProxyClassForArchiving(JNIEnv* env, return; } - Klass* caller_k = java_lang_Class::as_Klass(JNIHandles::resolve(caller)); - InstanceKlass* caller_ik = InstanceKlass::cast(caller_k); + InstanceKlass* caller_ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(caller)); if (caller_ik->is_hidden()) { // Hidden classes not of type lambda proxy classes are currently not being archived. // If the caller_ik is of one of the above types, the corresponding lambda proxy class won't be // registered for archiving. return; } - Klass* lambda_k = java_lang_Class::as_Klass(JNIHandles::resolve(lambdaProxyClass)); - InstanceKlass* lambda_ik = InstanceKlass::cast(lambda_k); + InstanceKlass* lambda_ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(lambdaProxyClass)); assert(lambda_ik->is_hidden(), "must be a hidden class"); assert(!lambda_ik->is_non_strong_hidden(), "expected a strong hidden class"); @@ -3428,8 +3409,7 @@ JVM_ENTRY(jclass, JVM_LookupLambdaProxyClassFromArchive(JNIEnv* env, THROW_(vmSymbols::java_lang_NullPointerException(), nullptr); } - Klass* caller_k = java_lang_Class::as_Klass(JNIHandles::resolve(caller)); - InstanceKlass* caller_ik = InstanceKlass::cast(caller_k); + InstanceKlass* caller_ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(caller)); if (!caller_ik->in_aot_cache()) { // there won't be a shared lambda class if the caller_ik is not in the shared archive. return nullptr; @@ -3825,11 +3805,7 @@ JVM_ENTRY(jint, JVM_GetClassFileVersion(JNIEnv* env, jclass current)) // return latest major version and minor version of 0. return JVM_CLASSFILE_MAJOR_VERSION; } - assert(!java_lang_Class::as_Klass(mirror)->is_array_klass(), "unexpected array class"); - - Klass* c = java_lang_Class::as_Klass(mirror); - assert(c->is_instance_klass(), "must be"); - InstanceKlass* ik = InstanceKlass::cast(c); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror); return (ik->minor_version() << 16) | ik->major_version(); JVM_END diff --git a/src/hotspot/share/prims/methodHandles.cpp b/src/hotspot/share/prims/methodHandles.cpp index c46b46b1af1..b13bd392eaa 100644 --- a/src/hotspot/share/prims/methodHandles.cpp +++ b/src/hotspot/share/prims/methodHandles.cpp @@ -902,7 +902,7 @@ void MethodHandles::expand_MemberName(Handle mname, int suppress, TRAPS) { if (clazz == nullptr) { THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "nothing to expand (as field)"); } - InstanceKlass* defc = InstanceKlass::cast(java_lang_Class::as_Klass(clazz)); + InstanceKlass* defc = java_lang_Class::as_InstanceKlass(clazz); DEBUG_ONLY(clazz = nullptr); // safety intptr_t vmindex = java_lang_invoke_MemberName::vmindex(mname()); bool is_static = ((flags & JVM_ACC_STATIC) != 0); diff --git a/src/hotspot/share/prims/unsafe.cpp b/src/hotspot/share/prims/unsafe.cpp index 4b2ffd57860..c950690e8ab 100644 --- a/src/hotspot/share/prims/unsafe.cpp +++ b/src/hotspot/share/prims/unsafe.cpp @@ -489,7 +489,7 @@ static jlong find_known_instance_field_offset(jclass clazz, jstring name, TRAPS) ResourceMark rm(THREAD); char *utf_name = java_lang_String::as_utf8_string(JNIHandles::resolve_non_null(name)); - InstanceKlass* k = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz))); + InstanceKlass* k = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(clazz)); jint offset = -1; // Not found for (JavaFieldStream fs(k); !fs.done(); fs.next()) { diff --git a/src/hotspot/share/prims/whitebox.cpp b/src/hotspot/share/prims/whitebox.cpp index afaa089e0b2..ce559d47b24 100644 --- a/src/hotspot/share/prims/whitebox.cpp +++ b/src/hotspot/share/prims/whitebox.cpp @@ -1156,7 +1156,7 @@ WB_ENTRY(jboolean, WB_EnqueueMethodForCompilation(JNIEnv* env, jobject o, jobjec WB_END WB_ENTRY(jboolean, WB_EnqueueInitializerForCompilation(JNIEnv* env, jobject o, jclass klass, jint comp_level)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); Method* clinit = ik->class_initializer(); if (clinit == nullptr || clinit->method_holder()->is_not_initialized()) { return false; @@ -1936,18 +1936,18 @@ WB_ENTRY(void, WB_ForceClassLoaderStatsSafepoint(JNIEnv* env, jobject wb)) WB_END WB_ENTRY(jlong, WB_GetConstantPool(JNIEnv* env, jobject wb, jclass klass)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); return (jlong) ik->constants(); WB_END WB_ENTRY(jobjectArray, WB_GetResolvedReferences(JNIEnv* env, jobject wb, jclass klass)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); objArrayOop resolved_refs= ik->constants()->resolved_references(); return (jobjectArray)JNIHandles::make_local(THREAD, resolved_refs); WB_END WB_ENTRY(jint, WB_getFieldEntriesLength(JNIEnv* env, jobject wb, jclass klass)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); ConstantPool* cp = ik->constants(); if (cp->cache() == nullptr) { return -1; @@ -1956,7 +1956,7 @@ WB_ENTRY(jint, WB_getFieldEntriesLength(JNIEnv* env, jobject wb, jclass klass)) WB_END WB_ENTRY(jint, WB_getFieldCPIndex(JNIEnv* env, jobject wb, jclass klass, jint index)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); ConstantPool* cp = ik->constants(); if (cp->cache() == nullptr) { return -1; @@ -1965,7 +1965,7 @@ WB_ENTRY(jint, WB_getFieldCPIndex(JNIEnv* env, jobject wb, jclass klass, jint in WB_END WB_ENTRY(jint, WB_getMethodEntriesLength(JNIEnv* env, jobject wb, jclass klass)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); ConstantPool* cp = ik->constants(); if (cp->cache() == nullptr) { return -1; @@ -1974,7 +1974,7 @@ WB_ENTRY(jint, WB_getMethodEntriesLength(JNIEnv* env, jobject wb, jclass klass)) WB_END WB_ENTRY(jint, WB_getMethodCPIndex(JNIEnv* env, jobject wb, jclass klass, jint index)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); ConstantPool* cp = ik->constants(); if (cp->cache() == nullptr) { return -1; @@ -1983,7 +1983,7 @@ WB_ENTRY(jint, WB_getMethodCPIndex(JNIEnv* env, jobject wb, jclass klass, jint i WB_END WB_ENTRY(jint, WB_getIndyInfoLength(JNIEnv* env, jobject wb, jclass klass)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); ConstantPool* cp = ik->constants(); if (cp->cache() == nullptr) { return -1; @@ -1992,7 +1992,7 @@ WB_ENTRY(jint, WB_getIndyInfoLength(JNIEnv* env, jobject wb, jclass klass)) WB_END WB_ENTRY(jint, WB_getIndyCPIndex(JNIEnv* env, jobject wb, jclass klass, jint index)) - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(klass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(klass)); ConstantPool* cp = ik->constants(); if (cp->cache() == nullptr) { return -1; @@ -2386,10 +2386,8 @@ int WhiteBox::offset_for_field(const char* field_name, oop object, Symbol* signature_symbol) { assert(field_name != nullptr && strlen(field_name) > 0, "Field name not valid"); - //Get the class of our object - Klass* arg_klass = object->klass(); - //Turn it into an instance-klass - InstanceKlass* ik = InstanceKlass::cast(arg_klass); + //Only non-array oops have fields. Don't call this function on arrays! + InstanceKlass* ik = InstanceKlass::cast(object->klass()); //Create symbols to look for in the class TempNewSymbol name_symbol = SymbolTable::new_symbol(field_name); @@ -3065,7 +3063,7 @@ JVM_ENTRY(void, JVM_RegisterWhiteBoxMethods(JNIEnv* env, jclass wbclass)) { if (WhiteBoxAPI) { // Make sure that wbclass is loaded by the null classloader - InstanceKlass* ik = InstanceKlass::cast(java_lang_Class::as_Klass(JNIHandles::resolve(wbclass))); + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve(wbclass)); Handle loader(THREAD, ik->class_loader()); if (loader.is_null()) { WhiteBox::register_methods(env, wbclass, thread, methods, sizeof(methods) / sizeof(methods[0])); diff --git a/src/hotspot/share/runtime/reflection.cpp b/src/hotspot/share/runtime/reflection.cpp index a7b468c57a3..7728643c640 100644 --- a/src/hotspot/share/runtime/reflection.cpp +++ b/src/hotspot/share/runtime/reflection.cpp @@ -1136,7 +1136,7 @@ oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle rtype = T_OBJECT; } - InstanceKlass* klass = InstanceKlass::cast(java_lang_Class::as_Klass(mirror)); + InstanceKlass* klass = java_lang_Class::as_InstanceKlass(mirror); Method* m = klass->method_with_idnum(slot); if (m == nullptr) { THROW_MSG_NULL(vmSymbols::java_lang_InternalError(), "invoke"); @@ -1153,7 +1153,7 @@ oop Reflection::invoke_constructor(oop constructor_mirror, objArrayHandle args, bool override = java_lang_reflect_Constructor::override(constructor_mirror) != 0; objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Constructor::parameter_types(constructor_mirror))); - InstanceKlass* klass = InstanceKlass::cast(java_lang_Class::as_Klass(mirror)); + InstanceKlass* klass = java_lang_Class::as_InstanceKlass(mirror); Method* m = klass->method_with_idnum(slot); if (m == nullptr) { THROW_MSG_NULL(vmSymbols::java_lang_InternalError(), "invoke"); diff --git a/src/hotspot/share/runtime/sharedRuntime.cpp b/src/hotspot/share/runtime/sharedRuntime.cpp index b7ad8081c52..2f7161ff744 100644 --- a/src/hotspot/share/runtime/sharedRuntime.cpp +++ b/src/hotspot/share/runtime/sharedRuntime.cpp @@ -881,8 +881,8 @@ void SharedRuntime::throw_StackOverflowError_common(JavaThread* current, bool de // We avoid using the normal exception construction in this case because // it performs an upcall to Java, and we're already out of stack space. JavaThread* THREAD = current; // For exception macros. - Klass* k = vmClasses::StackOverflowError_klass(); - oop exception_oop = InstanceKlass::cast(k)->allocate_instance(CHECK); + InstanceKlass* k = vmClasses::StackOverflowError_klass(); + oop exception_oop = k->allocate_instance(CHECK); if (delayed) { java_lang_Throwable::set_message(exception_oop, Universe::delayed_stack_overflow_error_message()); From 90e81c2bee86f404250fb9b833d43b18190b5272 Mon Sep 17 00:00:00 2001 From: Dingli Zhang Date: Tue, 16 Sep 2025 01:11:04 +0000 Subject: [PATCH 050/556] 8367616: RISC-V: Auto-enable Zicboz extension for debug builds Reviewed-by: fyang, fjiang --- src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp b/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp index 3d771123f12..3e5fb4610de 100644 --- a/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp +++ b/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp @@ -191,6 +191,9 @@ void RiscvHwprobe::add_features_from_query_result() { VM_Version::ext_Zbs.enable_feature(); } #ifndef PRODUCT + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZICBOZ)) { + VM_Version::ext_Zicboz.enable_feature(); + } if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZBKB)) { VM_Version::ext_Zbkb.enable_feature(); } From 0fbae8050b6f853053c7dee6a43d3ffbcfa69954 Mon Sep 17 00:00:00 2001 From: Kim Barrett Date: Tue, 16 Sep 2025 04:42:50 +0000 Subject: [PATCH 051/556] 8252582: HotSpot Style Guide should permit variable templates Reviewed-by: dholmes, stefank, kvn --- doc/hotspot-style.html | 45 +++++++++++++++++++++++------------------- doc/hotspot-style.md | 33 +++++++++++++++++-------------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/doc/hotspot-style.html b/doc/hotspot-style.html index fb4cffc9d43..7be6867b3ca 100644 --- a/doc/hotspot-style.html +++ b/doc/hotspot-style.html @@ -86,8 +86,9 @@ values
  • thread_local
  • nullptr
  • <atomic>
  • -
  • Inline -Variables
  • +
  • Variable Templates and +Inline Variables
  • Initializing variables with static storage duration
  • @@ -937,12 +938,18 @@ differ from what the Java compilers implement.

    "conservative" memory ordering, which may differ from (may be stronger than) sequentially consistent. There are algorithms in HotSpot that are believed to rely on that ordering.

    -

    Inline Variables

    -

    Variables with static storage duration may be declared -inline (p0386r2). -This has similar effects as for declaring a function inline: it can be -defined, identically, in multiple translation units, must be defined in -every translation unit in which it is Variable Templates and +Inline Variables +

    The use of variable templates (including static data member +templates) (N3651) is permitted. +They provide parameterized variables and constants in a simple and +direct form, instead of requiring the use of various workarounds.

    +

    Variables with static storage duration and variable templates may be +declared inline (p0386r2), and this usage is +permitted. This has similar effects as for declaring a function inline: +it can be defined, identically, in multiple translation units, must be +defined in every translation unit in which it is ODR used, and the behavior of the program is as if there is exactly one variable.

    @@ -955,16 +962,17 @@ initializations can make initialization order problems worse. The few ordering constraints that exist for non-inline variables don't apply, as there isn't a single program-designated translation unit containing the definition.

    -

    A constexpr static data member is implicitly -inline. As a consequence, an A constexpr static data member or static data member +template is implicitly inline. As a consequence, an ODR use of such a variable doesn't -require a definition in some .cpp file. (This is a change from -pre-C++17. Beginning with C++17, such a definition is considered a -duplicate definition, and is deprecated.)

    -

    Declaring a thread_local variable inline is -forbidden for HotSpot code. The use of -thread_local is already heavily restricted.

    +title="One Definition Rule">ODR use of such a member doesn't require +a definition in some .cpp file. (This is a change from pre-C++17. +Beginning with C++17, such a definition is considered a duplicate +definition, and is deprecated.)

    +

    Declaring a thread_local variable template or +inline variable is forbidden in HotSpot code. The use of thread_local is already +heavily restricted.

    Initializing variables with static storage duration

    @@ -1853,9 +1861,6 @@ Features
    • Trailing return type syntax for functions (n2541)

    • -
    • Variable templates (n3651, p0127r2)

    • Member initializers and aggregates (n3653)

    • Rvalue references and move semantics

    • diff --git a/doc/hotspot-style.md b/doc/hotspot-style.md index 3fd5468d531..facdf68462f 100644 --- a/doc/hotspot-style.md +++ b/doc/hotspot-style.md @@ -856,14 +856,19 @@ ordering, which may differ from (may be stronger than) sequentially consistent. There are algorithms in HotSpot that are believed to rely on that ordering. -### Inline Variables +### Variable Templates and Inline Variables -Variables with static storage duration may be declared `inline` -([p0386r2](https://wg21.link/p0386r2)). This has similar effects as for -declaring a function inline: it can be defined, identically, in multiple -translation units, must be defined in every translation unit in which it is -[ODR used][ODR], and the behavior of the program is as if there is exactly one -variable. +The use of variable templates (including static data member templates) +([N3651](https://wg21.link/N3651)) is permitted. They provide parameterized +variables and constants in a simple and direct form, instead of requiring the +use of various workarounds. + +Variables with static storage duration and variable templates may be declared +`inline` ([p0386r2](https://wg21.link/p0386r2)), and this usage is +permitted. This has similar effects as for declaring a function inline: it can +be defined, identically, in multiple translation units, must be defined in +every translation unit in which it is [ODR used][ODR], and the behavior of the +program is as if there is exactly one variable. Declaring a variable inline allows the complete definition to be in a header file, rather than having a declaration in a header and the definition in a @@ -874,13 +879,15 @@ make initialization order problems worse. The few ordering constraints that exist for non-inline variables don't apply, as there isn't a single program-designated translation unit containing the definition. -A `constexpr` static data member is implicitly `inline`. As a consequence, an -[ODR use][ODR] of such a variable doesn't require a definition in some .cpp +A `constexpr` static data member or static data member template +is implicitly `inline`. As a consequence, an +[ODR use][ODR] of such a member doesn't require a definition in some .cpp file. (This is a change from pre-C++17. Beginning with C++17, such a definition is considered a duplicate definition, and is deprecated.) -Declaring a `thread_local` variable `inline` is forbidden for HotSpot code. -[The use of `thread_local`](#thread_local) is already heavily restricted. +Declaring a `thread_local` variable template or `inline` variable is forbidden +in HotSpot code. [The use of `thread_local`](#thread_local) is already +heavily restricted. ### Initializing variables with static storage duration @@ -1849,10 +1856,6 @@ See Object Lifetime: C++17 6.8/8, C++20 6.7.3/8 * Trailing return type syntax for functions ([n2541](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2541.htm)) -* Variable templates -([n3651](https://isocpp.org/files/papers/N3651.pdf), -[p0127r2](http://wg21.link/p0127r2)) - * Member initializers and aggregates ([n3653](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3653.html)) From 76e464bcd56dab6ef0dfd917f87fdedeb9f838b4 Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Tue, 16 Sep 2025 05:06:17 +0000 Subject: [PATCH 052/556] 8367150: Add a header line to improve VMErrorCallback printing Reviewed-by: stefank, ayang --- src/hotspot/share/utilities/vmError.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hotspot/share/utilities/vmError.cpp b/src/hotspot/share/utilities/vmError.cpp index 357bf111804..0fbd8ed4259 100644 --- a/src/hotspot/share/utilities/vmError.cpp +++ b/src/hotspot/share/utilities/vmError.cpp @@ -1143,9 +1143,11 @@ void VMError::report(outputStream* st, bool _verbose) { } STEP_IF("printing registered callbacks", _verbose && _thread != nullptr); + size_t count = 0; for (VMErrorCallback* callback = _thread->_vm_error_callbacks; callback != nullptr; callback = callback->_next) { + st->print_cr("VMErrorCallback %zu:", ++count); callback->call(st); st->cr(); } From 60e9222fe147413f20c140f2c00541b6472dfaa4 Mon Sep 17 00:00:00 2001 From: Prasanta Sadhukhan Date: Tue, 16 Sep 2025 06:30:53 +0000 Subject: [PATCH 053/556] 8015444: java/awt/Focus/KeyStrokeTest.java sometimes fails Reviewed-by: tr --- test/jdk/java/awt/Focus/KeyStrokeTest.java | 79 +++++++++++----------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/test/jdk/java/awt/Focus/KeyStrokeTest.java b/test/jdk/java/awt/Focus/KeyStrokeTest.java index 7c462ce8f22..668bc1216c8 100644 --- a/test/jdk/java/awt/Focus/KeyStrokeTest.java +++ b/test/jdk/java/awt/Focus/KeyStrokeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,9 +29,9 @@ * @run main KeyStrokeTest */ -import java.awt.BorderLayout; import java.awt.Button; import java.awt.Dialog; +import java.awt.EventQueue; import java.awt.Frame; import java.awt.Robot; import java.awt.TextField; @@ -39,25 +39,53 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; public class KeyStrokeTest { static boolean keyTyped; static Frame frame; + static Robot robot; + static final CountDownLatch latch = new CountDownLatch(1); public static void main(String[] args) throws Exception { + robot = new Robot(); try { - KeyStrokeTest test = new KeyStrokeTest(); - test.doTest(); - } finally { - if (frame != null) { - frame.dispose(); + EventQueue.invokeAndWait(() -> { + KeyStrokeTest test = new KeyStrokeTest(); + test.initTest(); + }); + robot.waitForIdle(); + robot.delay(1000); + robot.keyPress(KeyEvent.VK_TAB); + robot.keyRelease(KeyEvent.VK_TAB); + + robot.delay(1000); + robot.keyPress(KeyEvent.VK_SPACE); + robot.keyRelease(KeyEvent.VK_SPACE); + + robot.delay(1000); + robot.keyPress(KeyEvent.VK_A); + robot.keyRelease(KeyEvent.VK_A); + try { + latch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) {} + if (!keyTyped) { + throw new + RuntimeException("First keystroke after JDialog is closed is lost"); } + System.out.println("Test passed"); + } finally { + EventQueue.invokeAndWait(() -> { + if (frame != null) { + frame.dispose(); + } + }); } } - private static void doTest() throws Exception { - final Object monitor = new Object(); - frame = new Frame(); + private static void initTest() { + frame = new Frame("KeyStrokeTest"); TextField textField = new TextField() { public void transferFocus() { System.err.println("transferFocus()"); @@ -71,6 +99,7 @@ public class KeyStrokeTest { }); dialog.add(btn); dialog.setSize(200, 200); + dialog.setLocationRelativeTo(null); dialog.setVisible(true); } }; @@ -81,38 +110,12 @@ public class KeyStrokeTest { if (e.getKeyChar() == 'a') { keyTyped = true; } - - synchronized (monitor) { - monitor.notifyAll(); - } + latch.countDown(); } }); frame.add(textField); frame.setSize(400, 400); + frame.setLocationRelativeTo(null); frame.setVisible(true); - - Robot robot = new Robot(); - robot.waitForIdle(); - robot.delay(1000); - robot.keyPress(KeyEvent.VK_TAB); - robot.keyRelease(KeyEvent.VK_TAB); - - robot.delay(1000); - robot.keyPress(KeyEvent.VK_SPACE); - robot.keyRelease(KeyEvent.VK_SPACE); - - robot.delay(1000); - synchronized (monitor) { - robot.keyPress(KeyEvent.VK_A); - robot.keyRelease(KeyEvent.VK_A); - monitor.wait(3000); - } - - if (!keyTyped) { - throw new RuntimeException("TEST FAILED"); - } - - System.out.println("Test passed"); } - } From 73df06c80c33be584b054a528ecdab4ecbf51d56 Mon Sep 17 00:00:00 2001 From: Andreas Steiner Date: Tue, 16 Sep 2025 07:17:53 +0000 Subject: [PATCH 054/556] 8359104: gc/TestAlwaysPreTouchBehavior.java# fails on Linux Reviewed-by: mbaesken, ayang --- src/hotspot/os/linux/os_linux.cpp | 46 +++++++++++++++++-- src/hotspot/os/linux/os_linux.hpp | 17 +++++++ .../jtreg/gc/TestAlwaysPreTouchBehavior.java | 2 +- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/hotspot/os/linux/os_linux.cpp b/src/hotspot/os/linux/os_linux.cpp index d7bc524e75b..3d44d839735 100644 --- a/src/hotspot/os/linux/os_linux.cpp +++ b/src/hotspot/os/linux/os_linux.cpp @@ -370,11 +370,20 @@ size_t os::physical_memory() { return phys_mem; } +// Returns the resident set size (RSS) of the process. +// Falls back to using VmRSS from /proc/self/status if /proc/self/smaps_rollup is unavailable. +// Note: On kernels with memory cgroups or shared memory, VmRSS may underreport RSS. +// Users requiring accurate RSS values should be aware of this limitation. size_t os::rss() { size_t size = 0; - os::Linux::meminfo_t info; - if (os::Linux::query_process_memory_info(&info)) { - size = info.vmrss * K; + os::Linux::accurate_meminfo_t accurate_info; + if (os::Linux::query_accurate_process_memory_info(&accurate_info) && accurate_info.rss != -1) { + size = accurate_info.rss * K; + } else { + os::Linux::meminfo_t info; + if (os::Linux::query_process_memory_info(&info)) { + size = info.vmrss * K; + } } return size; } @@ -2362,6 +2371,37 @@ bool os::Linux::query_process_memory_info(os::Linux::meminfo_t* info) { return false; } +// Accurate memory information need Linux 4.14 or newer +bool os::Linux::query_accurate_process_memory_info(os::Linux::accurate_meminfo_t* info) { + FILE* f = os::fopen("/proc/self/smaps_rollup", "r"); + if (f == nullptr) { + return false; + } + + const size_t num_values = sizeof(os::Linux::accurate_meminfo_t) / sizeof(size_t); + size_t num_found = 0; + char buf[256]; + info->rss = info->pss = info->pssdirty = info->pssanon = + info->pssfile = info->pssshmem = info->swap = info->swappss = -1; + + while (::fgets(buf, sizeof(buf), f) != nullptr && num_found < num_values) { + if ( (info->rss == -1 && sscanf(buf, "Rss: %zd kB", &info->rss) == 1) || + (info->pss == -1 && sscanf(buf, "Pss: %zd kB", &info->pss) == 1) || + (info->pssdirty == -1 && sscanf(buf, "Pss_Dirty: %zd kB", &info->pssdirty) == 1) || + (info->pssanon == -1 && sscanf(buf, "Pss_Anon: %zd kB", &info->pssanon) == 1) || + (info->pssfile == -1 && sscanf(buf, "Pss_File: %zd kB", &info->pssfile) == 1) || + (info->pssshmem == -1 && sscanf(buf, "Pss_Shmem: %zd kB", &info->pssshmem) == 1) || + (info->swap == -1 && sscanf(buf, "Swap: %zd kB", &info->swap) == 1) || + (info->swappss == -1 && sscanf(buf, "SwapPss: %zd kB", &info->swappss) == 1) + ) + { + num_found ++; + } + } + fclose(f); + return true; +} + #ifdef __GLIBC__ // For Glibc, print a one-liner with the malloc tunables. // Most important and popular is MALLOC_ARENA_MAX, but we are diff --git a/src/hotspot/os/linux/os_linux.hpp b/src/hotspot/os/linux/os_linux.hpp index d3e0d6c5668..497d383200d 100644 --- a/src/hotspot/os/linux/os_linux.hpp +++ b/src/hotspot/os/linux/os_linux.hpp @@ -181,6 +181,23 @@ class os::Linux { // fields will contain -1. static bool query_process_memory_info(meminfo_t* info); + // Output structure for query_accurate_process_memory_info() (all values in KB) + struct accurate_meminfo_t { + ssize_t rss; // current resident set size + ssize_t pss; // current proportional set size + ssize_t pssdirty; // proportional set size (dirty) + ssize_t pssanon; // proportional set size (anonymous mappings) + ssize_t pssfile; // proportional set size (file mappings) + ssize_t pssshmem; // proportional set size (shared mappings) + ssize_t swap; // swapped out + ssize_t swappss; // proportional set size (swapped out) + }; + + // Attempts to query accurate memory information from /proc/self/smaps_rollup and return it in the output structure. + // May fail (returns false) or succeed (returns true) but not all output fields are available; unavailable + // fields will contain -1. + static bool query_accurate_process_memory_info(accurate_meminfo_t* info); + // Tells if the user asked for transparent huge pages. static bool _thp_requested; diff --git a/test/hotspot/jtreg/gc/TestAlwaysPreTouchBehavior.java b/test/hotspot/jtreg/gc/TestAlwaysPreTouchBehavior.java index b8197f70384..141ef7ba197 100644 --- a/test/hotspot/jtreg/gc/TestAlwaysPreTouchBehavior.java +++ b/test/hotspot/jtreg/gc/TestAlwaysPreTouchBehavior.java @@ -155,7 +155,7 @@ public class TestAlwaysPreTouchBehavior { } if (available > requiredAvailable) { Asserts.assertGreaterThan(rss, minRequiredRss, "RSS of this process(" + rss + "b) should be bigger " + - "than or equal to heap size(" + heapSize + "b) (available memory: " + available + ")"); + "than or equal to heap size(" + heapSize + "b) (available memory: " + available + "). On Linux Kernel < 4.14 RSS can be inaccurate"); } } } From 3ba2e748d61a9ed8098093c6d4732973051808b2 Mon Sep 17 00:00:00 2001 From: Guanqiang Han Date: Tue, 16 Sep 2025 08:00:09 +0000 Subject: [PATCH 055/556] 8366925: Improper std::nothrow new expression in NativeHeapTrimmerThread ctor Reviewed-by: ayang, kbarrett, dholmes --- src/hotspot/share/runtime/trimNativeHeap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hotspot/share/runtime/trimNativeHeap.cpp b/src/hotspot/share/runtime/trimNativeHeap.cpp index 875bcb8e0c8..6049f4531c0 100644 --- a/src/hotspot/share/runtime/trimNativeHeap.cpp +++ b/src/hotspot/share/runtime/trimNativeHeap.cpp @@ -163,7 +163,7 @@ class NativeHeapTrimmerThread : public NamedThread { public: NativeHeapTrimmerThread() : - _lock(new (std::nothrow) PaddedMonitor(Mutex::nosafepoint, "NativeHeapTrimmer_lock")), + _lock(new PaddedMonitor(Mutex::nosafepoint, "NativeHeapTrimmer_lock")), _stop(false), _suspend_count(0), _num_trims_performed(0) From eb26865c36f1961ee802c8db812c786d4bdd4944 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Tue, 16 Sep 2025 08:00:32 +0000 Subject: [PATCH 056/556] 8367552: JCmdTestFileSafety.java fails when run by root user Reviewed-by: dcubed, ayang, phubner --- .../jtreg/runtime/cds/appcds/jcmd/JCmdTestFileSafety.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jcmd/JCmdTestFileSafety.java b/test/hotspot/jtreg/runtime/cds/appcds/jcmd/JCmdTestFileSafety.java index 68d77c5455c..74f98d5b777 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/jcmd/JCmdTestFileSafety.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/jcmd/JCmdTestFileSafety.java @@ -141,6 +141,9 @@ public class JCmdTestFileSafety extends JCmdTestDumpBase { // to create archive successfully which is not expected. throw new jtreg.SkippedException("Test skipped on Windows"); } + if (Platform.isRoot()) { + throw new jtreg.SkippedException("Test skipped when executed by root user."); + } runTest(JCmdTestFileSafety::test); } } From ca89cd06d39ed3a6bbe16f60fea4d7382849edbd Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Tue, 16 Sep 2025 08:46:18 +0000 Subject: [PATCH 057/556] 8367410: ZGC: Remove unused ZNmethodTable::wait_until_iteration_done() Reviewed-by: stefank, fandreuzzi --- src/hotspot/share/gc/z/zNMethodTable.cpp | 8 -------- src/hotspot/share/gc/z/zNMethodTable.hpp | 4 +--- src/hotspot/share/gc/z/zNMethodTableIteration.hpp | 4 ++-- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/hotspot/share/gc/z/zNMethodTable.cpp b/src/hotspot/share/gc/z/zNMethodTable.cpp index bbc8f56b654..f73014085f7 100644 --- a/src/hotspot/share/gc/z/zNMethodTable.cpp +++ b/src/hotspot/share/gc/z/zNMethodTable.cpp @@ -194,14 +194,6 @@ void ZNMethodTable::register_nmethod(nmethod* nm) { } } -void ZNMethodTable::wait_until_iteration_done() { - assert(CodeCache_lock->owned_by_self(), "Lock must be held"); - - while (_iteration.in_progress() || _iteration_secondary.in_progress()) { - CodeCache_lock->wait_without_safepoint_check(); - } -} - void ZNMethodTable::unregister_nmethod(nmethod* nm) { MutexLocker mu(CodeCache_lock, Mutex::_no_safepoint_check_flag); diff --git a/src/hotspot/share/gc/z/zNMethodTable.hpp b/src/hotspot/share/gc/z/zNMethodTable.hpp index e160ac1b39a..a8b9029caeb 100644 --- a/src/hotspot/share/gc/z/zNMethodTable.hpp +++ b/src/hotspot/share/gc/z/zNMethodTable.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -64,8 +64,6 @@ public: static void register_nmethod(nmethod* nm); static void unregister_nmethod(nmethod* nm); - static void wait_until_iteration_done(); - static void nmethods_do_begin(bool secondary); static void nmethods_do_end(bool secondary); static void nmethods_do(bool secondary, NMethodClosure* cl); diff --git a/src/hotspot/share/gc/z/zNMethodTableIteration.hpp b/src/hotspot/share/gc/z/zNMethodTableIteration.hpp index fc8acd2589c..34bd7d9b4f8 100644 --- a/src/hotspot/share/gc/z/zNMethodTableIteration.hpp +++ b/src/hotspot/share/gc/z/zNMethodTableIteration.hpp @@ -35,11 +35,11 @@ private: size_t _size; ZCACHE_ALIGNED volatile size_t _claimed; + bool in_progress() const; + public: ZNMethodTableIteration(); - bool in_progress() const; - void nmethods_do_begin(ZNMethodTableEntry* table, size_t size); void nmethods_do_end(); void nmethods_do(NMethodClosure* cl); From c7f014ed494409cdf9fc925fe98de08346606408 Mon Sep 17 00:00:00 2001 From: Hannes Greule Date: Tue, 16 Sep 2025 12:33:32 +0000 Subject: [PATCH 058/556] 8356813: Improve Mod(I|L)Node::Value Reviewed-by: epeter, qamai --- src/hotspot/share/opto/divnode.cpp | 126 ++++---- .../compiler/c2/gvn/ModINodeValueTests.java | 292 ++++++++++++++++++ .../compiler/c2/gvn/ModLNodeValueTests.java | 292 ++++++++++++++++++ 3 files changed, 645 insertions(+), 65 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/c2/gvn/ModINodeValueTests.java create mode 100644 test/hotspot/jtreg/compiler/c2/gvn/ModLNodeValueTests.java diff --git a/src/hotspot/share/opto/divnode.cpp b/src/hotspot/share/opto/divnode.cpp index 0d1337909fb..213f2e1e9a8 100644 --- a/src/hotspot/share/opto/divnode.cpp +++ b/src/hotspot/share/opto/divnode.cpp @@ -1198,44 +1198,76 @@ Node *ModINode::Ideal(PhaseGVN *phase, bool can_reshape) { } //------------------------------Value------------------------------------------ -const Type* ModINode::Value(PhaseGVN* phase) const { +static const Type* mod_value(const PhaseGVN* phase, const Node* in1, const Node* in2, const BasicType bt) { + assert(bt == T_INT || bt == T_LONG, "unexpected basic type"); // Either input is TOP ==> the result is TOP - const Type *t1 = phase->type( in(1) ); - const Type *t2 = phase->type( in(2) ); - if( t1 == Type::TOP ) return Type::TOP; - if( t2 == Type::TOP ) return Type::TOP; + const Type* t1 = phase->type(in1); + const Type* t2 = phase->type(in2); + if (t1 == Type::TOP) { return Type::TOP; } + if (t2 == Type::TOP) { return Type::TOP; } // We always generate the dynamic check for 0. // 0 MOD X is 0 - if( t1 == TypeInt::ZERO ) return TypeInt::ZERO; + if (t1 == TypeInteger::zero(bt)) { return t1; } + // X MOD X is 0 - if (in(1) == in(2)) { - return TypeInt::ZERO; + if (in1 == in2) { + return TypeInteger::zero(bt); } - // Either input is BOTTOM ==> the result is the local BOTTOM - const Type *bot = bottom_type(); - if( (t1 == bot) || (t2 == bot) || - (t1 == Type::BOTTOM) || (t2 == Type::BOTTOM) ) - return bot; - - const TypeInt *i1 = t1->is_int(); - const TypeInt *i2 = t2->is_int(); - if( !i1->is_con() || !i2->is_con() ) { - if( i1->_lo >= 0 && i2->_lo >= 0 ) - return TypeInt::POS; - // If both numbers are not constants, we know little. - return TypeInt::INT; - } // Mod by zero? Throw exception at runtime! - if( !i2->get_con() ) return TypeInt::POS; + if (t2 == TypeInteger::zero(bt)) { + return Type::TOP; + } - // We must be modulo'ing 2 float constants. - // Check for min_jint % '-1', result is defined to be '0'. - if( i1->get_con() == min_jint && i2->get_con() == -1 ) - return TypeInt::ZERO; + const TypeInteger* i1 = t1->is_integer(bt); + const TypeInteger* i2 = t2->is_integer(bt); + if (i1->is_con() && i2->is_con()) { + // We must be modulo'ing 2 int constants. + // Special case: min_jlong % '-1' is UB, and e.g., x86 triggers a division error. + // Any value % -1 is 0, so we can return 0 and avoid that scenario. + if (i2->get_con_as_long(bt) == -1) { + return TypeInteger::zero(bt); + } + return TypeInteger::make(i1->get_con_as_long(bt) % i2->get_con_as_long(bt), bt); + } + // We checked that t2 is not the zero constant. Hence, at least i2->_lo or i2->_hi must be non-zero, + // and hence its absoute value is bigger than zero. Hence, the magnitude of the divisor (i.e. the + // largest absolute value for any value in i2) must be in the range [1, 2^31] or [1, 2^63], depending + // on the BasicType. + julong divisor_magnitude = MAX2(g_uabs(i2->lo_as_long()), g_uabs(i2->hi_as_long())); + // JVMS lrem bytecode: "the magnitude of the result is always less than the magnitude of the divisor" + // "less than" means we can subtract 1 to get an inclusive upper bound in [0, 2^31-1] or [0, 2^63-1], respectively + jlong hi = static_cast(divisor_magnitude - 1); + jlong lo = -hi; + // JVMS lrem bytecode: "the result of the remainder operation can be negative only if the dividend + // is negative and can be positive only if the dividend is positive" + // Note that with a dividend with bounds e.g. lo == -4 and hi == -1 can still result in values + // below lo; i.e., -3 % 3 == 0. + // That means we cannot restrict the bound that is closer to zero beyond knowing its sign (or zero). + if (i1->hi_as_long() <= 0) { + // all dividends are not positive, so the result is not positive + hi = 0; + // if the dividend is known to be closer to zero, use that as a lower limit + lo = MAX2(lo, i1->lo_as_long()); + } else if (i1->lo_as_long() >= 0) { + // all dividends are not negative, so the result is not negative + lo = 0; + // if the dividend is known to be closer to zero, use that as an upper limit + hi = MIN2(hi, i1->hi_as_long()); + } else { + // Mixed signs, so we don't know the sign of the result, but the result is + // either the dividend itself or a value closer to zero than the dividend, + // and it is closer to zero than the divisor. + // As we know i1->_lo < 0 and i1->_hi > 0, we can use these bounds directly. + lo = MAX2(lo, i1->lo_as_long()); + hi = MIN2(hi, i1->hi_as_long()); + } + return TypeInteger::make(lo, hi, MAX2(i1->_widen, i2->_widen), bt); +} - return TypeInt::make( i1->get_con() % i2->get_con() ); +const Type* ModINode::Value(PhaseGVN* phase) const { + return mod_value(phase, in(1), in(2), T_INT); } //============================================================================= @@ -1464,43 +1496,7 @@ Node *ModLNode::Ideal(PhaseGVN *phase, bool can_reshape) { //------------------------------Value------------------------------------------ const Type* ModLNode::Value(PhaseGVN* phase) const { - // Either input is TOP ==> the result is TOP - const Type *t1 = phase->type( in(1) ); - const Type *t2 = phase->type( in(2) ); - if( t1 == Type::TOP ) return Type::TOP; - if( t2 == Type::TOP ) return Type::TOP; - - // We always generate the dynamic check for 0. - // 0 MOD X is 0 - if( t1 == TypeLong::ZERO ) return TypeLong::ZERO; - // X MOD X is 0 - if (in(1) == in(2)) { - return TypeLong::ZERO; - } - - // Either input is BOTTOM ==> the result is the local BOTTOM - const Type *bot = bottom_type(); - if( (t1 == bot) || (t2 == bot) || - (t1 == Type::BOTTOM) || (t2 == Type::BOTTOM) ) - return bot; - - const TypeLong *i1 = t1->is_long(); - const TypeLong *i2 = t2->is_long(); - if( !i1->is_con() || !i2->is_con() ) { - if( i1->_lo >= CONST64(0) && i2->_lo >= CONST64(0) ) - return TypeLong::POS; - // If both numbers are not constants, we know little. - return TypeLong::LONG; - } - // Mod by zero? Throw exception at runtime! - if( !i2->get_con() ) return TypeLong::POS; - - // We must be modulo'ing 2 float constants. - // Check for min_jint % '-1', result is defined to be '0'. - if( i1->get_con() == min_jlong && i2->get_con() == -1 ) - return TypeLong::ZERO; - - return TypeLong::make( i1->get_con() % i2->get_con() ); + return mod_value(phase, in(1), in(2), T_LONG); } Node *UModLNode::Ideal(PhaseGVN *phase, bool can_reshape) { diff --git a/test/hotspot/jtreg/compiler/c2/gvn/ModINodeValueTests.java b/test/hotspot/jtreg/compiler/c2/gvn/ModINodeValueTests.java new file mode 100644 index 00000000000..7870ce10910 --- /dev/null +++ b/test/hotspot/jtreg/compiler/c2/gvn/ModINodeValueTests.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package compiler.c2.gvn; + +import compiler.lib.generators.Generator; +import compiler.lib.generators.Generators; +import compiler.lib.generators.RestrictableGenerator; +import compiler.lib.ir_framework.DontCompile; +import compiler.lib.ir_framework.ForceInline; +import compiler.lib.ir_framework.IR; +import compiler.lib.ir_framework.IRNode; +import compiler.lib.ir_framework.Run; +import compiler.lib.ir_framework.Test; +import compiler.lib.ir_framework.TestFramework; +import jdk.test.lib.Asserts; + +/* + * @test + * @bug 8356813 + * @summary Test that Value method of ModINode is working as expected. + * @key randomness + * @library /test/lib / + * @run driver compiler.c2.gvn.ModINodeValueTests + */ +public class ModINodeValueTests { + private static final RestrictableGenerator INT_GEN = Generators.G.ints(); + private static final int POS_INT = INT_GEN.restricted(1, Integer.MAX_VALUE).next(); + private static final int NEG_INT = INT_GEN.restricted(Integer.MIN_VALUE, -1).next(); + + public static void main(String[] args) { + TestFramework.run(); + } + + @Run(test = { + "nonNegativeDividend", "nonNegativeDividendInRange", + "negativeDividend", "negativeDividendInRange", + "modByKnownBoundsUpper", "modByKnownBoundsUpperInRange", + "modByKnownBoundsLower", "modByKnownBoundsLowerInRange", + "modByKnownBoundsLimitedByDividendUpper", "modByKnownBoundsLimitedByDividendUpperInRange", + "modByKnownBoundsLimitedByDividendLower", "modByKnownBoundsLimitedByDividendLowerInRange", + "testRandomLimits" + }) + public void runMethod() { + int a = INT_GEN.next(); + int b = INT_GEN.next(); + + int min = Integer.MIN_VALUE; + int max = Integer.MAX_VALUE; + + assertResult(0, 0); + assertResult(a, b); + assertResult(min, min); + assertResult(max, max); + } + + @DontCompile + public void assertResult(int x, int y) { + Asserts.assertEQ(x != 0 && POS_INT % x < 0, nonNegativeDividend(x)); + Asserts.assertEQ(x != 0 && POS_INT % x <= 0, nonNegativeDividendInRange(x)); + Asserts.assertEQ(x != 0 && NEG_INT % x > 0, negativeDividend(x)); + Asserts.assertEQ(x != 0 && NEG_INT % x >= 0, negativeDividendInRange(x)); + Asserts.assertEQ(x % (((byte) y) + 129) > 255, modByKnownBoundsUpper(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129) >= 255, modByKnownBoundsUpperInRange(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129) < -255, modByKnownBoundsLower(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129) <= -255, modByKnownBoundsLowerInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) > 127, modByKnownBoundsLimitedByDividendUpper(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) >= 127, modByKnownBoundsLimitedByDividendUpperInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) < -128, modByKnownBoundsLimitedByDividendLower(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1) <= -128, modByKnownBoundsLimitedByDividendLowerInRange(x, y)); + + int res; + try { + res = testRandomLimitsInterpreted(x, y); + } catch (ArithmeticException _) { + try { + testRandomLimits(x, y); + Asserts.fail("Expected ArithmeticException"); + return; // unreachable + } catch (ArithmeticException _) { + return; // test succeeded, no result to assert + } + } + Asserts.assertEQ(res, testRandomLimits(x, y)); + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., POS_INT % x < 0 => false. + public boolean nonNegativeDividend(int x) { + return x != 0 && POS_INT % x < 0; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., POS_INT % x < 0 => false. + // This uses <= to verify the % is not optimized away + public boolean nonNegativeDividendInRange(int x) { + return x != 0 && POS_INT % x <= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., NEG_INT % x > 0 => false. + public boolean negativeDividend(int x) { + return x != 0 && NEG_INT % x > 0; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., NEG_INT % x > 0 => false. + // This uses >= to verify the % is not optimized away + public boolean negativeDividendInRange(int x) { + return x != 0 && NEG_INT % x >= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpper(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129) > 255; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpperInRange(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129) >= 255; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLower(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129) < -255; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLowerInRange(int x, int y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129) <= -255; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpper(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1) > 127; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpperInRange(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1) >= 127; + } + + @Test + @IR(failOn = {IRNode.MOD_I, IRNode.CMP_I}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLower(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1) < -128; + } + + @Test + @IR(counts = {IRNode.MOD_I, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLowerInRange(int x, int y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1) <= -128; + } + + private static final int LIMIT_1 = INT_GEN.next(); + private static final int LIMIT_2 = INT_GEN.next(); + private static final int LIMIT_3 = INT_GEN.next(); + private static final int LIMIT_4 = INT_GEN.next(); + private static final int LIMIT_5 = INT_GEN.next(); + private static final int LIMIT_6 = INT_GEN.next(); + private static final int LIMIT_7 = INT_GEN.next(); + private static final int LIMIT_8 = INT_GEN.next(); + private static final Range RANGE_1 = Range.generate(INT_GEN); + private static final Range RANGE_2 = Range.generate(INT_GEN); + + @Test + public int testRandomLimits(int x, int y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + int z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + @DontCompile + public int testRandomLimitsInterpreted(int x, int y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + int z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + record Range(int lo, int hi) { + Range { + if (lo > hi) { + throw new IllegalArgumentException("lo > hi"); + } + } + + @ForceInline + int clamp(int v) { + return Math.min(hi, Math.max(v, lo)); + } + + static Range generate(Generator g) { + var a = g.next(); + var b = g.next(); + if (a > b) { + var tmp = a; + a = b; + b = tmp; + } + return new Range(a, b); + } + } +} diff --git a/test/hotspot/jtreg/compiler/c2/gvn/ModLNodeValueTests.java b/test/hotspot/jtreg/compiler/c2/gvn/ModLNodeValueTests.java new file mode 100644 index 00000000000..a6eef6d3326 --- /dev/null +++ b/test/hotspot/jtreg/compiler/c2/gvn/ModLNodeValueTests.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package compiler.c2.gvn; + +import compiler.lib.generators.Generator; +import compiler.lib.generators.Generators; +import compiler.lib.ir_framework.DontCompile; +import compiler.lib.ir_framework.ForceInline; +import compiler.lib.ir_framework.IR; +import compiler.lib.ir_framework.IRNode; +import compiler.lib.ir_framework.Run; +import compiler.lib.ir_framework.Test; +import compiler.lib.ir_framework.TestFramework; +import jdk.test.lib.Asserts; + +/* + * @test + * @bug 8356813 + * @summary Test that Value method of ModLNode is working as expected. + * @key randomness + * @library /test/lib / + * @run driver compiler.c2.gvn.ModLNodeValueTests + */ +public class ModLNodeValueTests { + private static final Generator LONG_GEN = Generators.G.longs(); + private static final long POS_LONG = Generators.G.longs().restricted(1L, Long.MAX_VALUE).next(); + private static final long NEG_LONG = Generators.G.longs().restricted(Long.MIN_VALUE, -1L).next(); + + public static void main(String[] args) { + TestFramework.run(); + } + + @Run(test = { + "nonNegativeDividend", "nonNegativeDividendInRange", + "negativeDividend", "negativeDividendInRange", + "modByKnownBoundsUpper", "modByKnownBoundsUpperInRange", + "modByKnownBoundsLower", "modByKnownBoundsLowerInRange", + "modByKnownBoundsLimitedByDividendUpper", "modByKnownBoundsLimitedByDividendUpperInRange", + "modByKnownBoundsLimitedByDividendLower", "modByKnownBoundsLimitedByDividendLowerInRange", + "testRandomLimits" + }) + public void runMethod() { + long a = LONG_GEN.next(); + long b = LONG_GEN.next(); + + long min = Long.MIN_VALUE; + long max = Long.MAX_VALUE; + + assertResult(0, 0); + assertResult(a, b); + assertResult(min, min); + assertResult(max, max); + } + + @DontCompile + public void assertResult(long x, long y) { + Asserts.assertEQ(x != 0 && POS_LONG % x < 0, nonNegativeDividend(x)); + Asserts.assertEQ(x != 0 && POS_LONG % x <= 0, nonNegativeDividendInRange(x)); + Asserts.assertEQ(x != 0 && NEG_LONG % x > 0, negativeDividend(x)); + Asserts.assertEQ(x != 0 && NEG_LONG % x >= 0, negativeDividendInRange(x)); + Asserts.assertEQ(x % (((byte) y) + 129L) > 255, modByKnownBoundsUpper(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129L) >= 255, modByKnownBoundsUpperInRange(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129L) < -255, modByKnownBoundsLower(x, y)); + Asserts.assertEQ(x % (((byte) y) + 129L) <= -255, modByKnownBoundsLowerInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) > 127, modByKnownBoundsLimitedByDividendUpper(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) >= 127, modByKnownBoundsLimitedByDividendUpperInRange(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) < -128, modByKnownBoundsLimitedByDividendLower(x, y)); + Asserts.assertEQ(((byte) x) % (((char) y) + 1L) <= -128, modByKnownBoundsLimitedByDividendLowerInRange(x, y)); + + int res; + try { + res = testRandomLimitsInterpreted(x, y); + } catch (ArithmeticException _) { + try { + testRandomLimits(x, y); + Asserts.fail("Expected ArithmeticException"); + return; // unreachable + } catch (ArithmeticException _) { + return; // test succeeded, no result to assert + } + } + Asserts.assertEQ(res, testRandomLimits(x, y)); + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., posVal % x < 0 => false. + public boolean nonNegativeDividend(long x) { + return x != 0 && POS_LONG % x < 0; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., posVal % x < 0 => false. + // This uses <= to verify the % is not optimized away + public boolean nonNegativeDividendInRange(long x) { + return x != 0 && POS_LONG % x <= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., negValue % x > 0 => false. + public boolean negativeDividend(long x) { + return x != 0 && NEG_LONG % x > 0; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The sign of the result of % is the same as the sign of the dividend, + // i.e., negValue % x > 0 => false. + // This uses >= to verify the % is not optimized away + public boolean negativeDividendInRange(long x) { + return x != 0 && NEG_LONG % x >= 0; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpper(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129L) > 255; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The magnitude of the result is less than the divisor. + public boolean modByKnownBoundsUpperInRange(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129L) >= 255; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLower(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + return x % (((byte) y) + 129L) < -255; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The magnitude of the result is less than the divisor + public boolean modByKnownBoundsLowerInRange(long x, long y) { + // d = ((byte) y) + 129 => [1, 256] divisor + // x % d => [-255, 255] + // in bounds, cannot optimize + return x % (((byte) y) + 129L) <= -255; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpper(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1L) > 127; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendUpperInRange(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1L) >= 127; + } + + @Test + @IR(failOn = {IRNode.MOD_L, IRNode.CMP_L}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLower(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + return ((byte) x) % (((char) y) + 1L) < -128; + } + + @Test + @IR(counts = {IRNode.MOD_L, "1"}) + // The result is closer to zero than or equal to the dividend. + public boolean modByKnownBoundsLimitedByDividendLowerInRange(long x, long y) { + // d = ((char) y) + 1 => [1, 65536] divisor + // e = ((byte) x) => [-128, 127] + // e % d => [-128, 127] + // in bounds, cannot optimize + return ((byte) x) % (((char) y) + 1L) <= -128; + } + + + private static final long LIMIT_1 = LONG_GEN.next(); + private static final long LIMIT_2 = LONG_GEN.next(); + private static final long LIMIT_3 = LONG_GEN.next(); + private static final long LIMIT_4 = LONG_GEN.next(); + private static final long LIMIT_5 = LONG_GEN.next(); + private static final long LIMIT_6 = LONG_GEN.next(); + private static final long LIMIT_7 = LONG_GEN.next(); + private static final long LIMIT_8 = LONG_GEN.next(); + private static final Range RANGE_1 = Range.generate(LONG_GEN); + private static final Range RANGE_2 = Range.generate(LONG_GEN); + + @Test + public int testRandomLimits(long x, long y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + long z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + @DontCompile + public int testRandomLimitsInterpreted(long x, long y) { + x = RANGE_1.clamp(x); + y = RANGE_2.clamp(y); + long z = x % y; + + int sum = 0; + if (z < LIMIT_1) sum += 1; + if (z < LIMIT_2) sum += 2; + if (z < LIMIT_3) sum += 4; + if (z < LIMIT_4) sum += 8; + if (z > LIMIT_5) sum += 16; + if (z > LIMIT_6) sum += 32; + if (z > LIMIT_7) sum += 64; + if (z > LIMIT_8) sum += 128; + + return sum; + } + + record Range(long lo, long hi) { + Range { + if (lo > hi) { + throw new IllegalArgumentException("lo > hi"); + } + } + + @ForceInline + long clamp(long v) { + return Math.min(hi, Math.max(v, lo)); + } + + static Range generate(Generator g) { + var a = g.next(); + var b = g.next(); + if (a > b) { + var tmp = a; + a = b; + b = tmp; + } + return new Range(a, b); + } + } +} From 0bc3705948b1bb8f327dc48c4dbd85d22d66f036 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Tue, 16 Sep 2025 13:16:48 +0000 Subject: [PATCH 059/556] 8367597: Runtime.exit logging failed: Cannot invoke "java.lang.Module.getClassLoader()" because "m" is null Reviewed-by: alanb, rriggs --- src/java.base/share/classes/java/lang/Shutdown.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/java.base/share/classes/java/lang/Shutdown.java b/src/java.base/share/classes/java/lang/Shutdown.java index 36cf471a575..87c4732a5ce 100644 --- a/src/java.base/share/classes/java/lang/Shutdown.java +++ b/src/java.base/share/classes/java/lang/Shutdown.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -157,8 +157,10 @@ class Shutdown { * which should pass a nonzero status code. */ static void exit(int status) { - logRuntimeExit(status); // Log without holding the lock; - + // log only if VM is fully initialized + if (VM.isBooted()) { + logRuntimeExit(status); // Log without holding the lock; + } synchronized (Shutdown.class) { /* Synchronize on the class object, causing any other thread * that attempts to initiate shutdown to stall indefinitely From c82070e6357a1b49f2887ab22267393ba87d9352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20H=C3=A4ssig?= Date: Tue, 16 Sep 2025 13:19:12 +0000 Subject: [PATCH 060/556] 8366775: TestCompileTaskTimeout should use timeoutFactor Reviewed-by: chagedorn, rcastanedalo, mbaesken --- .../jtreg/compiler/arguments/TestCompileTaskTimeout.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java b/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java index cb52e5a24a0..f19223e6dce 100644 --- a/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java +++ b/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java @@ -37,6 +37,11 @@ import jdk.test.lib.process.ProcessTools; public class TestCompileTaskTimeout { public static void main(String[] args) throws Throwable { + double timeoutFactor = 1.0; + try { + timeoutFactor = Double.parseDouble(System.getProperty("test.timeout.factor", "1.0")); + } catch (NumberFormatException ignored) {} + ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=1", "--version") .shouldHaveExitValue(134) .shouldContain("timed out after"); @@ -49,7 +54,8 @@ public class TestCompileTaskTimeout { .shouldHaveExitValue(134) .shouldContain("timed out after"); - ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=2000", "--version") + int timeout = (int)(500.0 * timeoutFactor); + ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=" + timeout, "--version") .shouldHaveExitValue(0); } } From 58007c0bcc03f4609ce202cfb9f89b8438055dac Mon Sep 17 00:00:00 2001 From: Guanqiang Han Date: Tue, 16 Sep 2025 14:57:42 +0000 Subject: [PATCH 061/556] 8367619: String.format in outOfRangeException uses wrong format specifier for String argument Reviewed-by: fandreuzzi, rriggs, liach --- .../share/classes/jdk/internal/classfile/impl/Util.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java b/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java index 7e6384dd1a4..6411c939549 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/Util.java @@ -230,7 +230,7 @@ public final class Util { public static IllegalArgumentException outOfRangeException(int value, String fieldName, String typeName) { return new IllegalArgumentException( - String.format("%s out of range of %d: %d", fieldName, typeName, value)); + String.format("%s out of range of %s: %d", fieldName, typeName, value)); } /// Ensures the given mask won't be truncated when written as an access flag From 15d42c6d772d2c4cca1f21a947407fc0931aee64 Mon Sep 17 00:00:00 2001 From: Koushik Thirupattur Date: Tue, 16 Sep 2025 16:24:19 +0000 Subject: [PATCH 062/556] 8366978: dead code in SunCertPathBuilder Reviewed-by: mullan, hchao --- .../provider/certpath/SunCertPathBuilder.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java b/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java index c4e31cf7947..8b47c437dac 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/SunCertPathBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -637,18 +637,4 @@ public final class SunCertPathBuilder extends CertPathBuilderSpi { return (nextAltNameExt == null); } } - - /** - * Returns true if trust anchor certificate matches specified - * certificate constraints. - */ - private static boolean anchorIsTarget(TrustAnchor anchor, - CertSelector sel) - { - X509Certificate anchorCert = anchor.getTrustedCert(); - if (anchorCert != null) { - return sel.match(anchorCert); - } - return false; - } } From 075ebb4ee592c10879799a68ba79f782ee49b60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20H=C3=BCbner?= Date: Tue, 16 Sep 2025 16:53:21 +0000 Subject: [PATCH 063/556] 8366229: runtime/Thread/TooSmallStackSize.java runs with all collectors Reviewed-by: dholmes, shade --- test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java b/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java index 09db906b151..6fd1da6f2ae 100644 --- a/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java +++ b/test/hotspot/jtreg/runtime/Thread/TooSmallStackSize.java @@ -28,6 +28,7 @@ * VMThreadStackSize values should result in an error message that shows * the minimum stack size value for each thread type. * @library /test/lib + * @requires vm.flagless * @modules java.base/jdk.internal.misc * java.management * @run driver TooSmallStackSize From c41add8d3e24be5f469f18cfbf0f476f2baf63a6 Mon Sep 17 00:00:00 2001 From: Srinivas Vamsi Parasa Date: Tue, 16 Sep 2025 18:13:34 +0000 Subject: [PATCH 064/556] 8354348: Enable Extended EVEX to REX2/REX demotion for commutative operations with same dst and src2 Reviewed-by: jbhateja, epeter, sviswanathan --- src/hotspot/cpu/x86/assembler_x86.cpp | 176 +- src/hotspot/cpu/x86/assembler_x86.hpp | 9 +- test/hotspot/gtest/x86/asmtest.out.h | 5268 +++++++++++++------------ test/hotspot/gtest/x86/x86-asmtest.py | 30 +- 4 files changed, 2916 insertions(+), 2567 deletions(-) diff --git a/src/hotspot/cpu/x86/assembler_x86.cpp b/src/hotspot/cpu/x86/assembler_x86.cpp index d1b6897f287..49d40447f9f 100644 --- a/src/hotspot/cpu/x86/assembler_x86.cpp +++ b/src/hotspot/cpu/x86/assembler_x86.cpp @@ -1398,11 +1398,7 @@ void Assembler::addl(Address dst, Register src) { void Assembler::eaddl(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_32bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x01); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x01, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::addl(Register dst, int32_t imm32) { @@ -1432,11 +1428,7 @@ void Assembler::addl(Register dst, Register src) { } void Assembler::eaddl(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void)emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_arith(0x03, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x03, 0xC0, no_flags, true /* is_commutative */); } void Assembler::addr_nop_4() { @@ -1657,17 +1649,18 @@ void Assembler::eandl(Register dst, Register src1, Address src2, bool no_flags) emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x23, no_flags); } +void Assembler::eandl(Register dst, Address src1, Register src2, bool no_flags) { + InstructionMark im(this); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x21, no_flags, false /* is_map1 */, true /* is_commutative */); +} + void Assembler::andl(Register dst, Register src) { (void) prefix_and_encode(dst->encoding(), src->encoding()); emit_arith(0x23, 0xC0, dst, src); } void Assembler::eandl(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_arith(0x23, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x23, 0xC0, no_flags, true /* is_commutative */); } void Assembler::andnl(Register dst, Register src1, Register src2) { @@ -2519,7 +2512,7 @@ void Assembler::imull(Register dst, Register src) { } void Assembler::eimull(Register dst, Register src1, Register src2, bool no_flags) { - emit_eevex_or_demote(dst->encoding(), src1->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0xAF, no_flags, true /* is_map1 */, true /* swap */); + emit_eevex_or_demote(dst->encoding(), src1->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0xAF, no_flags, true /* is_map1 */, true /* swap */, true /* is_commutative */); } void Assembler::imull(Register dst, Address src, int32_t value) { @@ -4419,11 +4412,7 @@ void Assembler::enotl(Register dst, Register src) { } void Assembler::eorw(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_66, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_arith(0x0B, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_66, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_16bit, 0x0B, 0xC0, no_flags, true /* is_commutative */); } void Assembler::orl(Address dst, int32_t imm32) { @@ -4467,11 +4456,7 @@ void Assembler::orl(Register dst, Register src) { } void Assembler::eorl(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_arith(0x0B, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x0B, 0xC0, no_flags, true /* is_commutative */); } void Assembler::orl(Address dst, Register src) { @@ -4483,11 +4468,7 @@ void Assembler::orl(Address dst, Register src) { void Assembler::eorl(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_32bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x09); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x09, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::orb(Address dst, int imm8) { @@ -4517,11 +4498,7 @@ void Assembler::orb(Address dst, Register src) { void Assembler::eorb(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_8bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x08); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_8bit, 0x08, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::packsswb(XMMRegister dst, XMMRegister src) { @@ -7323,11 +7300,7 @@ void Assembler::xorl(Register dst, Register src) { } void Assembler::exorl(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_arith(0x33, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x33, 0xC0, no_flags, true /* is_commutative */); } void Assembler::xorl(Address dst, Register src) { @@ -7339,11 +7312,7 @@ void Assembler::xorl(Address dst, Register src) { void Assembler::exorl(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_32bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x31); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_32bit, 0x31, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::xorb(Register dst, Address src) { @@ -7367,11 +7336,7 @@ void Assembler::xorb(Address dst, Register src) { void Assembler::exorb(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_8bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x30); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_8bit, 0x30, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::xorw(Register dst, Address src) { @@ -12955,6 +12920,31 @@ void Assembler::eevex_prefix_ndd(Address adr, int ndd_enc, int xreg_enc, VexSimd vex_prefix(adr, ndd_enc, xreg_enc, pre, opc, attributes, /* nds_is_ndd */ true, no_flags); } +void Assembler::emit_eevex_or_demote(Register dst, Address src1, Register src2, VexSimdPrefix pre, VexOpcode opc, + int size, int opcode_byte, bool no_flags, bool is_map1, bool is_commutative) { + if (is_commutative && is_demotable(no_flags, dst->encoding(), src2->encoding())) { + // Opcode byte adjustment due to mismatch between NDD and equivalent demotable variant + opcode_byte += 2; + if (size == EVEX_64bit) { + emit_prefix_and_int8(get_prefixq(src1, dst, is_map1), opcode_byte); + } else { + // For 32-bit, 16-bit and 8-bit + if (size == EVEX_16bit) { + emit_int8(0x66); + } + prefix(src1, dst, false, is_map1); + emit_int8(opcode_byte); + } + } else { + bool vex_w = (size == EVEX_64bit) ? true : false; + InstructionAttr attributes(AVX_128bit, vex_w, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, size); + eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), pre, opc, &attributes, no_flags); + emit_int8(opcode_byte); + } + emit_operand(src2, src1, 0); +} + void Assembler::emit_eevex_or_demote(Register dst, Register src1, Address src2, VexSimdPrefix pre, VexOpcode opc, int size, int opcode_byte, bool no_flags, bool is_map1) { if (is_demotable(no_flags, dst->encoding(), src1->encoding())) { @@ -13055,18 +13045,20 @@ void Assembler::emit_eevex_or_demote(int dst_enc, int nds_enc, int src_enc, int8 } void Assembler::emit_eevex_or_demote(int dst_enc, int nds_enc, int src_enc, VexSimdPrefix pre, VexOpcode opc, - int size, int opcode_byte, bool no_flags, bool is_map1, bool swap) { + int size, int opcode_byte, bool no_flags, bool is_map1, bool swap, bool is_commutative) { int encode; bool is_prefixq = (size == EVEX_64bit) ? true : false; - if (is_demotable(no_flags, dst_enc, nds_enc)) { + bool first_operand_demotable = is_demotable(no_flags, dst_enc, nds_enc); + bool second_operand_demotable = is_commutative && is_demotable(no_flags, dst_enc, src_enc); + if (first_operand_demotable || second_operand_demotable) { if (size == EVEX_16bit) { emit_int8(0x66); } - + int src = first_operand_demotable ? src_enc : nds_enc; if (swap) { - encode = is_prefixq ? prefixq_and_encode(dst_enc, src_enc, is_map1) : prefix_and_encode(dst_enc, src_enc, is_map1); + encode = is_prefixq ? prefixq_and_encode(dst_enc, src, is_map1) : prefix_and_encode(dst_enc, src, is_map1); } else { - encode = is_prefixq ? prefixq_and_encode(src_enc, dst_enc, is_map1) : prefix_and_encode(src_enc, dst_enc, is_map1); + encode = is_prefixq ? prefixq_and_encode(src, dst_enc, is_map1) : prefix_and_encode(src, dst_enc, is_map1); } emit_opcode_prefix_and_encoding((unsigned char)opcode_byte, 0xC0, encode); } else { @@ -13114,6 +13106,26 @@ int Assembler::eevex_prefix_and_encode_nf(int dst_enc, int nds_enc, int src_enc, return vex_prefix_and_encode(dst_enc, nds_enc, src_enc, pre, opc, attributes, /* src_is_gpr */ true, /* nds_is_ndd */ false, no_flags); } +void Assembler::emit_eevex_prefix_or_demote_arith_ndd(Register dst, Register src1, Register src2, VexSimdPrefix pre, VexOpcode opc, + int size, int op1, int op2, bool no_flags, bool is_commutative) { + bool demotable = is_demotable(no_flags, dst->encoding(), src1->encoding()); + if (!demotable && is_commutative) { + if (is_demotable(no_flags, dst->encoding(), src2->encoding())) { + // swap src1 and src2 + Register tmp = src1; + src1 = src2; + src2 = tmp; + } + } + bool vex_w = (size == EVEX_64bit) ? true : false; + bool use_prefixq = vex_w; + InstructionAttr attributes(AVX_128bit, vex_w, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + // NDD shares its encoding bits with NDS bits for regular EVEX instruction. + // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. + (void)emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), pre, opc, &attributes, no_flags, use_prefixq); + emit_arith(op1, op2, src1, src2); +} + void Assembler::emit_eevex_prefix_or_demote_arith_ndd(Register dst, Register nds, int32_t imm32, VexSimdPrefix pre, VexOpcode opc, int size, int op1, int op2, bool no_flags) { int dst_enc = dst->encoding(); @@ -13124,7 +13136,6 @@ void Assembler::emit_eevex_prefix_or_demote_arith_ndd(Register dst, Register nds } else { bool vex_w = (size == EVEX_64bit) ? true : false; InstructionAttr attributes(AVX_128bit, vex_w, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - //attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, size); attributes.set_is_evex_instruction(); vex_prefix_and_encode(0, dst_enc, nds_enc, pre, opc, &attributes, /* src_is_gpr */ true, /* nds_is_ndd */ true, no_flags); @@ -14623,11 +14634,7 @@ void Assembler::addq(Address dst, Register src) { void Assembler::eaddq(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_64bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x01); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x01, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::addq(Register dst, int32_t imm32) { @@ -14656,11 +14663,7 @@ void Assembler::addq(Register dst, Register src) { } void Assembler::eaddq(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags, true /* use_prefixq */); - emit_arith(0x03, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x03, 0xC0, no_flags, true /* is_commutative */); } void Assembler::adcxq(Register dst, Register src) { @@ -14753,11 +14756,7 @@ void Assembler::andq(Register dst, Register src) { } void Assembler::eandq(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags, true /* use_prefixq */); - emit_arith(0x23, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x23, 0xC0, no_flags, true /* is_commutative */); } void Assembler::andq(Address dst, Register src) { @@ -14768,11 +14767,7 @@ void Assembler::andq(Address dst, Register src) { void Assembler::eandq(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_64bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x21); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x21, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::andnq(Register dst, Register src1, Register src2) { @@ -15118,7 +15113,7 @@ void Assembler::eimulq(Register dst, Register src, bool no_flags) { } void Assembler::eimulq(Register dst, Register src1, Register src2, bool no_flags) { - emit_eevex_or_demote(dst->encoding(), src1->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0xAF, no_flags, true /* is_map1 */, true /* swap */); + emit_eevex_or_demote(dst->encoding(), src1->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0xAF, no_flags, true /* is_map1 */, true /* swap */, true /* is_commutative */); } void Assembler::imulq(Register src) { @@ -15580,11 +15575,7 @@ void Assembler::orq(Address dst, Register src) { void Assembler::eorq(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_64bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x09); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x09, no_flags, false /* is_map1 */, true /* is_commutative */); } void Assembler::orq(Register dst, int32_t imm32) { @@ -15624,13 +15615,8 @@ void Assembler::orq(Register dst, Register src) { } void Assembler::eorq(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags, true /* use_prefixq */); - emit_arith(0x0B, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x0B, 0xC0, no_flags, true /* is_commutative */); } - void Assembler::popcntq(Register dst, Address src) { assert(VM_Version::supports_popcnt(), "must support"); InstructionMark im(this); @@ -16372,11 +16358,7 @@ void Assembler::xorq(Register dst, Register src) { } void Assembler::exorq(Register dst, Register src1, Register src2, bool no_flags) { - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - // NDD shares its encoding bits with NDS bits for regular EVEX instruction. - // Therefore, DST is passed as the second argument to minimize changes in the leaf level routine. - (void) emit_eevex_prefix_or_demote_ndd(src1->encoding(), dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags, true /* use_prefixq */); - emit_arith(0x33, 0xC0, src1, src2); + emit_eevex_prefix_or_demote_arith_ndd(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x33, 0xC0, no_flags, true /* is_commutative */); } void Assembler::xorq(Register dst, Address src) { @@ -16430,11 +16412,7 @@ void Assembler::esetzucc(Condition cc, Register dst) { void Assembler::exorq(Register dst, Address src1, Register src2, bool no_flags) { InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); - attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_64bit); - eevex_prefix_ndd(src1, dst->encoding(), src2->encoding(), VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, &attributes, no_flags); - emit_int8(0x31); - emit_operand(src2, src1, 0); + emit_eevex_or_demote(dst, src1, src2, VEX_SIMD_NONE, VEX_OPCODE_0F_3C /* MAP4 */, EVEX_64bit, 0x31, no_flags, false /* is_map1 */, true /* is_commutative */); } void InstructionAttr::set_address_attributes(int tuple_type, int input_size_in_bits) { diff --git a/src/hotspot/cpu/x86/assembler_x86.hpp b/src/hotspot/cpu/x86/assembler_x86.hpp index 45c24f8c832..99dade412b2 100644 --- a/src/hotspot/cpu/x86/assembler_x86.hpp +++ b/src/hotspot/cpu/x86/assembler_x86.hpp @@ -807,14 +807,20 @@ private: int emit_eevex_prefix_or_demote_ndd(int dst_enc, int nds_enc, VexSimdPrefix pre, VexOpcode opc, InstructionAttr *attributes, bool no_flags = false, bool use_prefixq = false); + void emit_eevex_prefix_or_demote_arith_ndd(Register dst, Register src1, Register src2, VexSimdPrefix pre, VexOpcode opc, + int size, int op1, int op2, bool no_flags = false, bool is_commutative = false); + void emit_eevex_prefix_or_demote_arith_ndd(Register dst, Register nds, int32_t imm32, VexSimdPrefix pre, VexOpcode opc, int size, int op1, int op2, bool no_flags); void emit_eevex_or_demote(Register dst, Register src1, Address src2, VexSimdPrefix pre, VexOpcode opc, int size, int opcode_byte, bool no_flags = false, bool is_map1 = false); + void emit_eevex_or_demote(Register dst, Address src1, Register src2, VexSimdPrefix pre, VexOpcode opc, + int size, int opcode_byte, bool no_flags = false, bool is_map1 = false, bool is_commutative = false); + void emit_eevex_or_demote(int dst_enc, int nds_enc, int src_enc, VexSimdPrefix pre, VexOpcode opc, - int size, int opcode_byte, bool no_flags, bool is_map1 = false, bool swap = false); + int size, int opcode_byte, bool no_flags, bool is_map1 = false, bool swap = false, bool is_commutative = false); void emit_eevex_or_demote(int dst_enc, int nds_enc, int src_enc, int8_t imm8, VexSimdPrefix pre, VexOpcode opc, int size, int opcode_byte, bool no_flags, bool is_map1 = false); @@ -1149,6 +1155,7 @@ private: void eandl(Register dst, Register src, int32_t imm32, bool no_flags); void andl(Register dst, Address src); void eandl(Register dst, Register src1, Address src2, bool no_flags); + void eandl(Register dst, Address src1, Register src2, bool no_flags); void andl(Register dst, Register src); void eandl(Register dst, Register src1, Register src2, bool no_flags); void andl(Address dst, Register src); diff --git a/test/hotspot/gtest/x86/asmtest.out.h b/test/hotspot/gtest/x86/asmtest.out.h index a2071bafc20..1f5aae4f636 100644 --- a/test/hotspot/gtest/x86/asmtest.out.h +++ b/test/hotspot/gtest/x86/asmtest.out.h @@ -289,667 +289,751 @@ __ exorl(rdx, Address(r10, r16, (Address::ScaleFactor)2, -0x161e1d47), 16777216, false); // {EVEX}xor edx, dword ptr [r10+r16*4-0x161e1d47], 16777216 IID265 __ exorl(rdx, Address(r29, r23, (Address::ScaleFactor)1, +0x1b34e2f8), 16777216, true); // {NF}xor edx, dword ptr [r29+r23*2+0x1b34e2f8], 16777216 IID266 __ eaddl(r19, Address(r27, r31, (Address::ScaleFactor)0, +0x1f3ce7d8), r29, false); // {EVEX}add r19d, dword ptr [r27+r31*1+0x1f3ce7d8], r29d IID267 - __ eaddl(r28, Address(r24, rcx, (Address::ScaleFactor)3, -0x6053edc2), r28, false); // {EVEX}add r28d, dword ptr [r24+rcx*8-0x6053edc2], r28d IID268 + __ eaddl(r28, Address(r24, rcx, (Address::ScaleFactor)3, -0x6053edc2), r28, false); // add r28d, dword ptr [r24+rcx*8-0x6053edc2] IID268 __ eaddl(r17, Address(r18, r24, (Address::ScaleFactor)3, -0x1bf71f78), r29, true); // {NF}add r17d, dword ptr [r18+r24*8-0x1bf71f78], r29d IID269 __ eaddl(rcx, Address(r15, r28, (Address::ScaleFactor)1, +0x15b8216), rcx, true); // {NF}add ecx, dword ptr [r15+r28*2+0x15b8216], ecx IID270 - __ eorl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false); // {EVEX}or r30d, dword ptr [rbx+rdx*8-0x463540b4], r28d IID271 - __ eorl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false); // {EVEX}or r18d, dword ptr [r28+r10*8+0x3523a73b], r18d IID272 - __ eorl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true); // {NF}or r9d, dword ptr [r15+r15*2-0x2a0bdd56], r21d IID273 - __ eorl(r16, Address(r23, -0x165064ff), r16, true); // {NF}or r16d, dword ptr [r23-0x165064ff], r16d IID274 - __ eorb(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false); // {EVEX}or r28b, byte ptr [r30+r11*1+0x17281e3a], r20b IID275 - __ eorb(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false); // {EVEX}or dl, byte ptr [rbx+r31*4+0x2477b5bb], dl IID276 - __ eorb(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true); // {NF}or r16b, byte ptr [r11+rcx*2-0x3175d1af], r24b IID277 - __ eorb(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true); // {NF}or bl, byte ptr [r11+r20*8-0x22d67bd3], bl IID278 - __ esubl(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false); // {EVEX}sub r26d, dword ptr [r27+r30*2-0x3d9bce2e], edx IID279 - __ esubl(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false); // {EVEX}sub r31d, dword ptr [r22+r29*2+0x14218519], r31d IID280 - __ esubl(r21, Address(r9, -0x1050127a), r13, true); // {NF}sub r21d, dword ptr [r9-0x1050127a], r13d IID281 - __ esubl(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true); // {NF}sub r31d, dword ptr [r9+r8*1-0xae18961], r31d IID282 - __ exorl(r15, Address(r18, +0x5c2bbce5), r12, false); // {EVEX}xor r15d, dword ptr [r18+0x5c2bbce5], r12d IID283 - __ exorl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false); // {EVEX}xor r27d, dword ptr [r25+r23*1+0x5c6078b3], r27d IID284 - __ exorl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true); // {NF}xor r18d, dword ptr [r8+rdx*8-0x9ed3881], r14d IID285 - __ exorl(r9, Address(r15, +0x775acdad), r9, true); // {NF}xor r9d, dword ptr [r15+0x775acdad], r9d IID286 - __ exorb(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false); // {EVEX}xor r21b, byte ptr [r18+r26*2+0x2fe31fd5], r23b IID287 - __ exorb(r10, Address(r27, +0xa3150de), r10, false); // {EVEX}xor r10b, byte ptr [r27+0xa3150de], r10b IID288 - __ exorb(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true); // {NF}xor r18b, byte ptr [r22+r30*8+0x1ad4e897], r24b IID289 - __ exorb(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true); // {NF}xor r8b, byte ptr [r16+r20*1+0x626eae82], r8b IID290 - __ eaddl(r21, r15, 1048576, false); // {EVEX}add r21d, r15d, 1048576 IID291 - __ eaddl(rax, r18, 1048576, false); // {EVEX}add eax, r18d, 1048576 IID292 - __ eaddl(r18, r18, 256, false); // add r18d, 256 IID293 - __ eaddl(r13, r19, 16, true); // {NF}add r13d, r19d, 16 IID294 - __ eaddl(rax, r23, 16, true); // {NF}add eax, r23d, 16 IID295 - __ eaddl(r25, r25, 16777216, true); // {NF}add r25d, r25d, 16777216 IID296 - __ eandl(r29, r18, 1048576, false); // {EVEX}and r29d, r18d, 1048576 IID297 - __ eandl(rax, r14, 1048576, false); // {EVEX}and eax, r14d, 1048576 IID298 - __ eandl(r19, r19, 65536, false); // and r19d, 65536 IID299 - __ eandl(r27, r25, 1048576, true); // {NF}and r27d, r25d, 1048576 IID300 - __ eandl(rax, r20, 1048576, true); // {NF}and eax, r20d, 1048576 IID301 - __ eandl(r28, r28, 16, true); // {NF}and r28d, r28d, 16 IID302 - __ eimull(r31, r22, 4096, false); // {EVEX}imul r31d, r22d, 4096 IID303 + __ eandl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false); // {EVEX}and r30d, dword ptr [rbx+rdx*8-0x463540b4], r28d IID271 + __ eandl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false); // and r18d, dword ptr [r28+r10*8+0x3523a73b] IID272 + __ eandl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true); // {NF}and r9d, dword ptr [r15+r15*2-0x2a0bdd56], r21d IID273 + __ eandl(r16, Address(r23, -0x165064ff), r16, true); // {NF}and r16d, dword ptr [r23-0x165064ff], r16d IID274 + __ eorl(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false); // {EVEX}or r28d, dword ptr [r30+r11*1+0x17281e3a], r20d IID275 + __ eorl(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false); // or edx, dword ptr [rbx+r31*4+0x2477b5bb] IID276 + __ eorl(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true); // {NF}or r16d, dword ptr [r11+rcx*2-0x3175d1af], r24d IID277 + __ eorl(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true); // {NF}or ebx, dword ptr [r11+r20*8-0x22d67bd3], ebx IID278 + __ eorb(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false); // {EVEX}or r26b, byte ptr [r27+r30*2-0x3d9bce2e], dl IID279 + __ eorb(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false); // or r31b, byte ptr [r22+r29*2+0x14218519] IID280 + __ eorb(r21, Address(r9, -0x1050127a), r13, true); // {NF}or r21b, byte ptr [r9-0x1050127a], r13b IID281 + __ eorb(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true); // {NF}or r31b, byte ptr [r9+r8*1-0xae18961], r31b IID282 + __ esubl(r15, Address(r18, +0x5c2bbce5), r12, false); // {EVEX}sub r15d, dword ptr [r18+0x5c2bbce5], r12d IID283 + __ esubl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false); // {EVEX}sub r27d, dword ptr [r25+r23*1+0x5c6078b3], r27d IID284 + __ esubl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true); // {NF}sub r18d, dword ptr [r8+rdx*8-0x9ed3881], r14d IID285 + __ esubl(r9, Address(r15, +0x775acdad), r9, true); // {NF}sub r9d, dword ptr [r15+0x775acdad], r9d IID286 + __ exorl(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false); // {EVEX}xor r21d, dword ptr [r18+r26*2+0x2fe31fd5], r23d IID287 + __ exorl(r10, Address(r27, +0xa3150de), r10, false); // xor r10d, dword ptr [r27+0xa3150de] IID288 + __ exorl(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true); // {NF}xor r18d, dword ptr [r22+r30*8+0x1ad4e897], r24d IID289 + __ exorl(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true); // {NF}xor r8d, dword ptr [r16+r20*1+0x626eae82], r8d IID290 + __ exorb(r16, Address(r21, r15, (Address::ScaleFactor)0, -0x1403b60d), r18, false); // {EVEX}xor r16b, byte ptr [r21+r15*1-0x1403b60d], r18b IID291 + __ exorb(r13, Address(r19, r23, (Address::ScaleFactor)2, +0x237ef1e1), r13, false); // xor r13b, byte ptr [r19+r23*4+0x237ef1e1] IID292 + __ exorb(r29, Address(r18, r14, (Address::ScaleFactor)2, +0x5cc0095b), r14, true); // {NF}xor r29b, byte ptr [r18+r14*4+0x5cc0095b], r14b IID293 + __ exorb(r27, Address(r25, r20, (Address::ScaleFactor)3, +0x1cf7b958), r27, true); // {NF}xor r27b, byte ptr [r25+r20*8+0x1cf7b958], r27b IID294 + __ eaddl(r16, r24, 16, false); // {EVEX}add r16d, r24d, 16 IID295 + __ eaddl(rax, r24, 16, false); // {EVEX}add eax, r24d, 16 IID296 + __ eaddl(r21, r21, 65536, false); // add r21d, 65536 IID297 + __ eaddl(r24, r8, 1048576, true); // {NF}add r24d, r8d, 1048576 IID298 + __ eaddl(rax, r13, 1048576, true); // {NF}add eax, r13d, 1048576 IID299 + __ eaddl(r29, r29, 16777216, true); // {NF}add r29d, r29d, 16777216 IID300 + __ eandl(r12, r12, 16, false); // and r12d, 16 IID301 + __ eandl(rax, r30, 16, false); // {EVEX}and eax, r30d, 16 IID302 + __ eandl(r24, r24, 16, false); // and r24d, 16 IID303 + __ eandl(r8, r12, 1, true); // {NF}and r8d, r12d, 1 IID304 + __ eandl(rax, r13, 1, true); // {NF}and eax, r13d, 1 IID305 + __ eandl(r25, r25, 16, true); // {NF}and r25d, r25d, 16 IID306 + __ eimull(r18, r23, 65536, false); // {EVEX}imul r18d, r23d, 65536 IID307 + __ eimull(rax, r9, 65536, false); // {EVEX}imul eax, r9d, 65536 IID308 + __ eimull(r26, r26, 268435456, false); // {EVEX}imul r26d, r26d, 268435456 IID309 + __ eimull(r25, r21, 1, true); // {NF}imul r25d, r21d, 1 IID310 + __ eimull(rax, r24, 1, true); // {NF}imul eax, r24d, 1 IID311 + __ eimull(r24, r24, 16777216, true); // {NF}imul r24d, r24d, 16777216 IID312 + __ eorl(r30, r26, 1, false); // {EVEX}or r30d, r26d, 1 IID313 + __ eorl(rax, r22, 1, false); // {EVEX}or eax, r22d, 1 IID314 + __ eorl(r17, r17, 1048576, false); // or r17d, 1048576 IID315 + __ eorl(r24, r8, 1, true); // {NF}or r24d, r8d, 1 IID316 + __ eorl(rax, r27, 1, true); // {NF}or eax, r27d, 1 IID317 #endif // _LP64 - __ eimull(rax, rbx, 4096, false); // {EVEX}imul eax, ebx, 4096 IID304 + __ eorl(rdx, rdx, 268435456, true); // {NF}or edx, edx, 268435456 IID318 #ifdef _LP64 - __ eimull(r24, r24, 1048576, false); // {EVEX}imul r24d, r24d, 1048576 IID305 - __ eimull(r21, r16, 65536, true); // {NF}imul r21d, r16d, 65536 IID306 - __ eimull(rax, r24, 65536, true); // {NF}imul eax, r24d, 65536 IID307 - __ eimull(r13, r13, 16, true); // {NF}imul r13d, r13d, 16 IID308 - __ eorl(r29, r8, 16777216, false); // {EVEX}or r29d, r8d, 16777216 IID309 - __ eorl(rax, r12, 16777216, false); // {EVEX}or eax, r12d, 16777216 IID310 - __ eorl(r30, r30, 4096, false); // or r30d, 4096 IID311 - __ eorl(r24, rdx, 16, true); // {NF}or r24d, edx, 16 IID312 - __ eorl(rax, r8, 16, true); // {NF}or eax, r8d, 16 IID313 - __ eorl(r13, r13, 4096, true); // {NF}or r13d, r13d, 4096 IID314 - __ ercll(r25, r13, 1); // {EVEX}rcl r25d, r13d, 1 IID315 - __ ercll(rax, r18, 1); // {EVEX}rcl eax, r18d, 1 IID316 - __ ercll(r9, r9, 16); // rcl r9d, 16 IID317 - __ eroll(r26, r25, 8, false); // {EVEX}rol r26d, r25d, 8 IID318 + __ ercll(r22, r22, 8); // rcl r22d, 8 IID319 + __ ercll(rax, r23, 8); // {EVEX}rcl eax, r23d, 8 IID320 + __ ercll(r19, r19, 4); // rcl r19d, 4 IID321 + __ eroll(r30, r24, 2, false); // {EVEX}rol r30d, r24d, 2 IID322 + __ eroll(rax, r29, 2, false); // {EVEX}rol eax, r29d, 2 IID323 + __ eroll(r8, r8, 2, false); // rol r8d, 2 IID324 + __ eroll(r18, r24, 16, true); // {NF}rol r18d, r24d, 16 IID325 + __ eroll(rax, r13, 16, true); // {NF}rol eax, r13d, 16 IID326 + __ eroll(r24, r24, 1, true); // {NF}rol r24d, r24d, 1 IID327 + __ erorl(r28, r17, 16, false); // {EVEX}ror r28d, r17d, 16 IID328 + __ erorl(rax, r24, 16, false); // {EVEX}ror eax, r24d, 16 IID329 + __ erorl(r17, r17, 4, false); // ror r17d, 4 IID330 + __ erorl(r24, rcx, 4, true); // {NF}ror r24d, ecx, 4 IID331 + __ erorl(rax, r16, 4, true); // {NF}ror eax, r16d, 4 IID332 + __ erorl(r15, r15, 2, true); // {NF}ror r15d, r15d, 2 IID333 + __ esall(r14, r27, 4, false); // {EVEX}sal r14d, r27d, 4 IID334 + __ esall(rax, r23, 4, false); // {EVEX}sal eax, r23d, 4 IID335 + __ esall(r30, r30, 4, false); // sal r30d, 4 IID336 + __ esall(r27, rdx, 2, true); // {NF}sal r27d, edx, 2 IID337 + __ esall(rax, r19, 2, true); // {NF}sal eax, r19d, 2 IID338 + __ esall(r20, r20, 2, true); // {NF}sal r20d, r20d, 2 IID339 + __ esarl(r21, r23, 1, false); // {EVEX}sar r21d, r23d, 1 IID340 + __ esarl(rax, r30, 1, false); // {EVEX}sar eax, r30d, 1 IID341 + __ esarl(r25, r25, 2, false); // sar r25d, 2 IID342 + __ esarl(r24, r19, 4, true); // {NF}sar r24d, r19d, 4 IID343 + __ esarl(rax, r14, 4, true); // {NF}sar eax, r14d, 4 IID344 + __ esarl(r26, r26, 16, true); // {NF}sar r26d, r26d, 16 IID345 + __ eshll(r22, r13, 8, false); // {EVEX}shl r22d, r13d, 8 IID346 + __ eshll(rax, r24, 8, false); // {EVEX}shl eax, r24d, 8 IID347 + __ eshll(r14, r14, 16, false); // shl r14d, 16 IID348 + __ eshll(r28, r25, 8, true); // {NF}shl r28d, r25d, 8 IID349 + __ eshll(rax, r10, 8, true); // {NF}shl eax, r10d, 8 IID350 + __ eshll(r20, r20, 1, true); // {NF}shl r20d, r20d, 1 IID351 + __ eshrl(r12, rbx, 4, false); // {EVEX}shr r12d, ebx, 4 IID352 + __ eshrl(rax, r23, 4, false); // {EVEX}shr eax, r23d, 4 IID353 + __ eshrl(r28, r28, 16, false); // shr r28d, 16 IID354 + __ eshrl(r24, r30, 4, true); // {NF}shr r24d, r30d, 4 IID355 + __ eshrl(rax, r31, 4, true); // {NF}shr eax, r31d, 4 IID356 + __ eshrl(r31, r31, 2, true); // {NF}shr r31d, r31d, 2 IID357 + __ esubl(r20, r10, 256, false); // {EVEX}sub r20d, r10d, 256 IID358 + __ esubl(rax, r13, 256, false); // {EVEX}sub eax, r13d, 256 IID359 + __ esubl(r25, r25, 256, false); // sub r25d, 256 IID360 + __ esubl(r23, r12, 268435456, true); // {NF}sub r23d, r12d, 268435456 IID361 + __ esubl(rax, r16, 268435456, true); // {NF}sub eax, r16d, 268435456 IID362 + __ esubl(r31, r31, 1, true); // {NF}sub r31d, r31d, 1 IID363 + __ exorl(r9, r15, 16777216, false); // {EVEX}xor r9d, r15d, 16777216 IID364 + __ exorl(rax, r13, 16777216, false); // {EVEX}xor eax, r13d, 16777216 IID365 + __ exorl(r28, r28, 16, false); // xor r28d, 16 IID366 + __ exorl(r29, r22, 16, true); // {NF}xor r29d, r22d, 16 IID367 #endif // _LP64 - __ eroll(rax, rdx, 8, false); // {EVEX}rol eax, edx, 8 IID319 + __ exorl(rax, rbx, 16, true); // {NF}xor eax, ebx, 16 IID368 #ifdef _LP64 - __ eroll(r24, r24, 16, false); // rol r24d, 16 IID320 - __ eroll(r24, rcx, 8, true); // {NF}rol r24d, ecx, 8 IID321 - __ eroll(rax, r30, 8, true); // {NF}rol eax, r30d, 8 IID322 - __ eroll(r28, r28, 16, true); // {NF}rol r28d, r28d, 16 IID323 - __ erorl(r17, r28, 4, false); // {EVEX}ror r17d, r28d, 4 IID324 + __ exorl(r8, r8, 16, true); // {NF}xor r8d, r8d, 16 IID369 + __ esubl_imm32(r16, r13, 4194304, false); // {EVEX}sub r16d, r13d, 4194304 IID370 + __ esubl_imm32(rax, r12, 4194304, false); // {EVEX}sub eax, r12d, 4194304 IID371 + __ esubl_imm32(r17, r17, 67108864, false); // sub r17d, 67108864 IID372 + __ esubl_imm32(r22, r26, 1073741824, true); // {NF}sub r22d, r26d, 1073741824 IID373 + __ esubl_imm32(rax, r10, 1073741824, true); // {NF}sub eax, r10d, 1073741824 IID374 + __ esubl_imm32(r11, r11, 1073741824, true); // {NF}sub r11d, r11d, 1073741824 IID375 + __ eaddl(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false); // {EVEX}add r19d, r12d, dword ptr [r30+r8*1+0x6a1a0a73] IID376 + __ eaddl(r30, r30, Address(r18, r19, (Address::ScaleFactor)2, +0x25f990cf), false); // add r30d, dword ptr [r18+r19*4+0x25f990cf] IID377 + __ eaddl(rcx, r25, Address(r19, r16, (Address::ScaleFactor)0, +0x482d5dbc), true); // {NF}add ecx, r25d, dword ptr [r19+r16*1+0x482d5dbc] IID378 + __ eaddl(r9, r9, Address(r11, +0x43d5ee01), true); // {NF}add r9d, r9d, dword ptr [r11+0x43d5ee01] IID379 + __ eandl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false); // {EVEX}and ecx, r23d, dword ptr [r21+r15*4+0x2825c2bc] IID380 + __ eandl(r27, r27, Address(r13, r15, (Address::ScaleFactor)3, -0x1268b895), false); // and r27d, dword ptr [r13+r15*8-0x1268b895] IID381 + __ eandl(r9, r23, Address(r22, r30, (Address::ScaleFactor)0, -0x715acbb), true); // {NF}and r9d, r23d, dword ptr [r22+r30*1-0x715acbb] IID382 + __ eandl(rbx, rbx, Address(r28, r16, (Address::ScaleFactor)2, +0xb0223ee), true); // {NF}and ebx, ebx, dword ptr [r28+r16*4+0xb0223ee] IID383 + __ eimull(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false); // {EVEX}imul r15d, r29d, dword ptr [r15+r28*2-0x1f297a69] IID384 + __ eimull(r17, r17, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), false); // imul r17d, dword ptr [r23+rbx*2+0xadc7545] IID385 + __ eimull(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), true); // {NF}imul r27d, r9d, dword ptr [rdx+r22*4-0x43d90f61] IID386 + __ eimull(rbx, rbx, Address(r28, r22, (Address::ScaleFactor)3, -0x519d9a27), true); // {NF}imul ebx, ebx, dword ptr [r28+r22*8-0x519d9a27] IID387 + __ eorl(r17, rcx, Address(r14, +0x10642223), false); // {EVEX}or r17d, ecx, dword ptr [r14+0x10642223] IID388 + __ eorl(r26, r26, Address(r31, -0x7a9a83ba), false); // or r26d, dword ptr [r31-0x7a9a83ba] IID389 + __ eorl(r15, r22, Address(r12, r12, (Address::ScaleFactor)2, +0x743b6997), true); // {NF}or r15d, r22d, dword ptr [r12+r12*4+0x743b6997] IID390 + __ eorl(r8, r8, Address(rdx, r22, (Address::ScaleFactor)3, -0x588414dc), true); // {NF}or r8d, r8d, dword ptr [rdx+r22*8-0x588414dc] IID391 + __ esubl(rcx, r28, Address(r30, r13, (Address::ScaleFactor)2, +0xe9310e5), false); // {EVEX}sub ecx, r28d, dword ptr [r30+r13*4+0xe9310e5] IID392 + __ esubl(rcx, rcx, Address(r30, r10, (Address::ScaleFactor)1, -0x1b076ed1), false); // sub ecx, dword ptr [r30+r10*2-0x1b076ed1] IID393 + __ esubl(r9, r21, Address(r30, +0x2f79ffd3), true); // {NF}sub r9d, r21d, dword ptr [r30+0x2f79ffd3] IID394 + __ esubl(r16, r16, Address(rdx, r14, (Address::ScaleFactor)2, +0x675d71c1), true); // {NF}sub r16d, r16d, dword ptr [rdx+r14*4+0x675d71c1] IID395 + __ exorl(r27, r28, Address(rbx, r26, (Address::ScaleFactor)2, -0x78c20b81), false); // {EVEX}xor r27d, r28d, dword ptr [rbx+r26*4-0x78c20b81] IID396 + __ exorl(r14, r14, Address(r31, r19, (Address::ScaleFactor)1, -0x4ff251cc), false); // xor r14d, dword ptr [r31+r19*2-0x4ff251cc] IID397 + __ exorl(r20, r18, Address(r13, r16, (Address::ScaleFactor)2, -0x19efc6e2), true); // {NF}xor r20d, r18d, dword ptr [r13+r16*4-0x19efc6e2] IID398 + __ exorl(r19, r19, Address(r13, r23, (Address::ScaleFactor)1, -0x2d1bd8aa), true); // {NF}xor r19d, r19d, dword ptr [r13+r23*2-0x2d1bd8aa] IID399 + __ exorb(r29, r17, Address(rdx, r29, (Address::ScaleFactor)2, +0x66573e84), false); // {EVEX}xor r29b, r17b, byte ptr [rdx+r29*4+0x66573e84] IID400 + __ exorb(r22, r22, Address(r24, r25, (Address::ScaleFactor)3, +0x3a94a93f), false); // xor r22b, byte ptr [r24+r25*8+0x3a94a93f] IID401 + __ exorb(r13, r29, Address(r15, r23, (Address::ScaleFactor)1, +0x76d43532), true); // {NF}xor r13b, r29b, byte ptr [r15+r23*2+0x76d43532] IID402 + __ exorb(r15, r15, Address(r13, r9, (Address::ScaleFactor)0, -0x474e6d1a), true); // {NF}xor r15b, r15b, byte ptr [r13+r9*1-0x474e6d1a] IID403 + __ exorw(r17, r16, Address(r23, rdx, (Address::ScaleFactor)0, +0x562a291), false); // {EVEX}xor r17w, r16w, word ptr [r23+rdx*1+0x562a291] IID404 + __ exorw(r29, r29, Address(r18, r28, (Address::ScaleFactor)3, -0x541967f2), false); // xor r29w, word ptr [r18+r28*8-0x541967f2] IID405 + __ exorw(r27, r11, Address(r10, +0xa911c5a), true); // {NF}xor r27w, r11w, word ptr [r10+0xa911c5a] IID406 + __ exorw(r31, r31, Address(r30, r19, (Address::ScaleFactor)2, -0xf6a3da), true); // {NF}xor r31w, r31w, word ptr [r30+r19*4-0xf6a3da] IID407 + __ eaddl(r12, r13, r23, false); // {load}{EVEX}add r12d, r13d, r23d IID408 + __ eaddl(r28, r28, r20, false); // {load}add r28d, r20d IID409 + __ eaddl(r20, r24, r20, false); // {load}add r20d, r24d IID410 + __ eaddl(r11, r10, r15, true); // {load}{NF}add r11d, r10d, r15d IID411 + __ eaddl(r19, r19, r20, true); // {load}{NF}add r19d, r19d, r20d IID412 + __ eaddl(r23, r15, r23, true); // {load}{NF}add r23d, r15d, r23d IID413 + __ eandl(r26, r19, r24, false); // {load}{EVEX}and r26d, r19d, r24d IID414 + __ eandl(r23, r23, r28, false); // {load}and r23d, r28d IID415 + __ eandl(r11, r13, r11, false); // {load}and r11d, r13d IID416 + __ eandl(r13, rdx, r31, true); // {load}{NF}and r13d, edx, r31d IID417 + __ eandl(r23, r23, r23, true); // {load}{NF}and r23d, r23d, r23d IID418 + __ eandl(r9, r27, r9, true); // {load}{NF}and r9d, r27d, r9d IID419 + __ eimull(r21, r20, r24, false); // {load}{EVEX}imul r21d, r20d, r24d IID420 + __ eimull(r21, r21, r29, false); // {load}imul r21d, r29d IID421 + __ eimull(rbx, r11, rbx, false); // {load}imul ebx, r11d IID422 + __ eimull(r21, rbx, rcx, true); // {load}{NF}imul r21d, ebx, ecx IID423 + __ eimull(r31, r31, r21, true); // {load}{NF}imul r31d, r31d, r21d IID424 + __ eimull(r15, r25, r15, true); // {load}{NF}imul r15d, r25d, r15d IID425 + __ eorw(r30, r23, r25, false); // {load}{EVEX}or r30w, r23w, r25w IID426 + __ eorw(r18, r18, rcx, false); // {load}or r18w, cx IID427 + __ eorw(r10, rcx, r10, false); // {load}or r10w, cx IID428 + __ eorw(r31, r21, r26, true); // {load}{NF}or r31w, r21w, r26w IID429 + __ eorw(r21, r21, r19, true); // {load}{NF}or r21w, r21w, r19w IID430 #endif // _LP64 - __ erorl(rax, rdx, 4, false); // {EVEX}ror eax, edx, 4 IID325 + __ eorw(rdx, rbx, rdx, true); // {load}{NF}or dx, bx, dx IID431 #ifdef _LP64 - __ erorl(r8, r8, 16, false); // ror r8d, 16 IID326 - __ erorl(r19, rdx, 16, true); // {NF}ror r19d, edx, 16 IID327 - __ erorl(rax, r31, 16, true); // {NF}ror eax, r31d, 16 IID328 - __ erorl(r22, r22, 8, true); // {NF}ror r22d, r22d, 8 IID329 - __ esall(r23, r25, 16, false); // {EVEX}sal r23d, r25d, 16 IID330 - __ esall(rax, r14, 16, false); // {EVEX}sal eax, r14d, 16 IID331 - __ esall(r31, r31, 8, false); // sal r31d, 8 IID332 - __ esall(r30, r24, 2, true); // {NF}sal r30d, r24d, 2 IID333 - __ esall(rax, r29, 2, true); // {NF}sal eax, r29d, 2 IID334 - __ esall(r8, r8, 2, true); // {NF}sal r8d, r8d, 2 IID335 - __ esarl(r18, r24, 16, false); // {EVEX}sar r18d, r24d, 16 IID336 - __ esarl(rax, r13, 16, false); // {EVEX}sar eax, r13d, 16 IID337 - __ esarl(r24, r24, 1, false); // sar r24d, 1 IID338 - __ esarl(r28, r17, 16, true); // {NF}sar r28d, r17d, 16 IID339 - __ esarl(rax, r24, 16, true); // {NF}sar eax, r24d, 16 IID340 - __ esarl(r17, r17, 4, true); // {NF}sar r17d, r17d, 4 IID341 - __ eshll(r24, rcx, 4, false); // {EVEX}shl r24d, ecx, 4 IID342 - __ eshll(rax, r16, 4, false); // {EVEX}shl eax, r16d, 4 IID343 - __ eshll(r15, r15, 2, false); // shl r15d, 2 IID344 - __ eshll(r14, r27, 4, true); // {NF}shl r14d, r27d, 4 IID345 - __ eshll(rax, r23, 4, true); // {NF}shl eax, r23d, 4 IID346 - __ eshll(r30, r30, 4, true); // {NF}shl r30d, r30d, 4 IID347 - __ eshrl(r27, rdx, 2, false); // {EVEX}shr r27d, edx, 2 IID348 - __ eshrl(rax, r19, 2, false); // {EVEX}shr eax, r19d, 2 IID349 - __ eshrl(r20, r20, 2, false); // shr r20d, 2 IID350 - __ eshrl(r21, r23, 1, true); // {NF}shr r21d, r23d, 1 IID351 - __ eshrl(rax, r30, 1, true); // {NF}shr eax, r30d, 1 IID352 - __ eshrl(r25, r25, 2, true); // {NF}shr r25d, r25d, 2 IID353 - __ esubl(r24, r19, 1048576, false); // {EVEX}sub r24d, r19d, 1048576 IID354 - __ esubl(rax, r14, 1048576, false); // {EVEX}sub eax, r14d, 1048576 IID355 - __ esubl(r22, r22, 268435456, false); // sub r22d, 268435456 IID356 - __ esubl(r24, r24, 65536, true); // {NF}sub r24d, r24d, 65536 IID357 - __ esubl(rax, r14, 65536, true); // {NF}sub eax, r14d, 65536 IID358 - __ esubl(r28, r28, 268435456, true); // {NF}sub r28d, r28d, 268435456 IID359 - __ exorl(rbx, r20, 256, false); // {EVEX}xor ebx, r20d, 256 IID360 - __ exorl(rax, r15, 256, false); // {EVEX}xor eax, r15d, 256 IID361 -#endif // _LP64 - __ exorl(rbx, rbx, 4096, false); // xor ebx, 4096 IID362 -#ifdef _LP64 - __ exorl(r24, r30, 65536, true); // {NF}xor r24d, r30d, 65536 IID363 - __ exorl(rax, r31, 65536, true); // {NF}xor eax, r31d, 65536 IID364 - __ exorl(r31, r31, 4096, true); // {NF}xor r31d, r31d, 4096 IID365 - __ esubl_imm32(r20, r10, 1048576, false); // {EVEX}sub r20d, r10d, 1048576 IID366 - __ esubl_imm32(rax, r13, 1048576, false); // {EVEX}sub eax, r13d, 1048576 IID367 - __ esubl_imm32(r25, r25, 1048576, false); // sub r25d, 1048576 IID368 - __ esubl_imm32(r23, r12, 1073741824, true); // {NF}sub r23d, r12d, 1073741824 IID369 - __ esubl_imm32(rax, r16, 1073741824, true); // {NF}sub eax, r16d, 1073741824 IID370 - __ esubl_imm32(r31, r31, 65536, true); // {NF}sub r31d, r31d, 65536 IID371 - __ eaddl(r17, r13, Address(r9, +0x7fef2f98), false); // {EVEX}add r17d, r13d, dword ptr [r9+0x7fef2f98] IID372 - __ eaddl(r29, r8, Address(r22, -0x4df70aac), true); // {NF}add r29d, r8d, dword ptr [r22-0x4df70aac] IID373 - __ eandl(r13, r17, Address(r12, r15, (Address::ScaleFactor)3, +0x50a8a902), false); // {EVEX}and r13d, r17d, dword ptr [r12+r15*8+0x50a8a902] IID374 - __ eandl(r22, r25, Address(r26, r10, (Address::ScaleFactor)2, +0x70ea2754), true); // {NF}and r22d, r25d, dword ptr [r26+r10*4+0x70ea2754] IID375 - __ eimull(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false); // {EVEX}imul r19d, r12d, dword ptr [r30+r8*1+0x6a1a0a73] IID376 - __ eimull(r30, r18, Address(r18, r19, (Address::ScaleFactor)2, -0x7fcd28c7), true); // {NF}imul r30d, r18d, dword ptr [r18+r19*4-0x7fcd28c7] IID377 - __ eorl(r16, r31, Address(r25, r11, (Address::ScaleFactor)3, +0x482d5dbc), false); // {EVEX}or r16d, r31d, dword ptr [r25+r11*8+0x482d5dbc] IID378 - __ eorl(r9, r27, Address(r11, +0x43d5ee01), true); // {NF}or r9d, r27d, dword ptr [r11+0x43d5ee01] IID379 - __ esubl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false); // {EVEX}sub ecx, r23d, dword ptr [r21+r15*4+0x2825c2bc] IID380 - __ esubl(r27, r22, Address(r13, r15, (Address::ScaleFactor)1, +0x771f0da7), true); // {NF}sub r27d, r22d, dword ptr [r13+r15*2+0x771f0da7] IID381 - __ exorl(r9, r30, Address(r9, r22, (Address::ScaleFactor)3, -0x4ad6c88e), false); // {EVEX}xor r9d, r30d, dword ptr [r9+r22*8-0x4ad6c88e] IID382 - __ exorl(r11, r16, Address(rbx, r28, (Address::ScaleFactor)2, +0xb0223ee), true); // {NF}xor r11d, r16d, dword ptr [rbx+r28*4+0xb0223ee] IID383 - __ exorb(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false); // {EVEX}xor r15b, r29b, byte ptr [r15+r28*2-0x1f297a69] IID384 - __ exorb(r17, r30, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), true); // {NF}xor r17b, r30b, byte ptr [r23+rbx*2+0xadc7545] IID385 - __ exorw(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), false); // {EVEX}xor r27w, r9w, word ptr [rdx+r22*4-0x43d90f61] IID386 - __ exorw(rbx, r22, Address(r28, r22, (Address::ScaleFactor)0, -0x7d30a0b1), true); // {NF}xor bx, r22w, word ptr [r28+r22*1-0x7d30a0b1] IID387 - __ eaddl(r14, r24, rcx, false); // {load}{EVEX}add r14d, r24d, ecx IID388 - __ eaddl(r8, r8, r17, false); // {load}add r8d, r17d IID389 - __ eaddl(r26, r24, r12, true); // {load}{NF}add r26d, r24d, r12d IID390 - __ eaddl(r24, r24, r23, true); // {load}{NF}add r24d, r24d, r23d IID391 - __ eandl(r13, r26, r31, false); // {load}{EVEX}and r13d, r26d, r31d IID392 - __ eandl(r11, r11, r8, false); // {load}and r11d, r8d IID393 - __ eandl(rcx, r19, r15, true); // {load}{NF}and ecx, r19d, r15d IID394 - __ eandl(r12, r12, r12, true); // {load}{NF}and r12d, r12d, r12d IID395 - __ eimull(r22, r20, r19, false); // {load}{EVEX}imul r22d, r20d, r19d IID396 - __ eimull(r8, r8, rdx, false); // {load}imul r8d, edx IID397 - __ eimull(r22, r27, r23, true); // {load}{NF}imul r22d, r27d, r23d IID398 - __ eimull(r9, r9, r18, true); // {load}{NF}imul r9d, r9d, r18d IID399 - __ eorw(rcx, r30, r13, false); // {load}{EVEX}or cx, r30w, r13w IID400 - __ eorw(r28, r28, r19, false); // {load}or r28w, r19w IID401 - __ eorw(r12, r30, r27, true); // {load}{NF}or r12w, r30w, r27w IID402 - __ eorw(r8, r8, r22, true); // {load}{NF}or r8w, r8w, r22w IID403 - __ eorl(r16, rcx, r30, false); // {load}{EVEX}or r16d, ecx, r30d IID404 - __ eorl(r10, r10, r25, false); // {load}or r10d, r25d IID405 - __ eorl(r15, r17, r17, true); // {load}{NF}or r15d, r17d, r17d IID406 - __ eorl(r9, r9, r30, true); // {load}{NF}or r9d, r9d, r30d IID407 - __ eshldl(r20, r21, r8, false); // {load}{EVEX}shld r20d, r21d, r8d, cl IID408 - __ eshldl(r26, r26, r14, false); // {load}shld r26d, r14d IID409 - __ eshldl(r16, rdx, r14, true); // {load}{NF}shld r16d, edx, r14d, cl IID410 - __ eshldl(r19, r19, r8, true); // {load}{NF}shld r19d, r19d, r8d, cl IID411 - __ eshrdl(r27, rbx, r26, false); // {load}{EVEX}shrd r27d, ebx, r26d, cl IID412 - __ eshrdl(r28, r28, r19, false); // {load}shrd r28d, r19d IID413 - __ eshrdl(rcx, r11, r14, true); // {load}{NF}shrd ecx, r11d, r14d, cl IID414 - __ eshrdl(r31, r31, r19, true); // {load}{NF}shrd r31d, r31d, r19d, cl IID415 - __ esubl(r26, r13, r25, false); // {load}{EVEX}sub r26d, r13d, r25d IID416 - __ esubl(r24, r24, r11, false); // {load}sub r24d, r11d IID417 - __ esubl(r18, r20, r13, true); // {load}{NF}sub r18d, r20d, r13d IID418 - __ esubl(r16, r16, r18, true); // {load}{NF}sub r16d, r16d, r18d IID419 - __ exorl(r19, r17, r8, false); // {load}{EVEX}xor r19d, r17d, r8d IID420 - __ exorl(r19, r19, r13, false); // {load}xor r19d, r13d IID421 - __ exorl(r23, r13, r15, true); // {load}{NF}xor r23d, r13d, r15d IID422 - __ exorl(r11, r11, r29, true); // {load}{NF}xor r11d, r11d, r29d IID423 - __ eshldl(r29, r17, r17, 1, false); // {EVEX}shld r29d, r17d, r17d, 1 IID424 - __ eshldl(r22, r22, r24, 4, false); // shld r22d, r24d, 4 IID425 - __ eshldl(r8, r28, r11, 16, true); // {NF}shld r8d, r28d, r11d, 16 IID426 - __ eshldl(r15, r15, r23, 4, true); // {NF}shld r15d, r15d, r23d, 4 IID427 - __ eshrdl(r29, r22, r16, 4, false); // {EVEX}shrd r29d, r22d, r16d, 4 IID428 - __ eshrdl(r13, r13, r9, 4, false); // shrd r13d, r9d, 4 IID429 - __ eshrdl(r15, r21, r12, 2, true); // {NF}shrd r15d, r21d, r12d, 2 IID430 - __ eshrdl(r17, r17, r23, 2, true); // {NF}shrd r17d, r17d, r23d, 2 IID431 - __ ecmovl (Assembler::Condition::overflow, rdx, r16, r29); // cmovo edx, r16d, r29d IID432 - __ ecmovl (Assembler::Condition::overflow, r10, r10, r21); // cmovo r10d, r21d IID433 - __ ecmovl (Assembler::Condition::noOverflow, r17, r29, r18); // cmovno r17d, r29d, r18d IID434 - __ ecmovl (Assembler::Condition::noOverflow, r28, r28, r24); // cmovno r28d, r24d IID435 - __ ecmovl (Assembler::Condition::below, r10, r20, r27); // cmovb r10d, r20d, r27d IID436 - __ ecmovl (Assembler::Condition::below, r10, r10, r14); // cmovb r10d, r14d IID437 - __ ecmovl (Assembler::Condition::aboveEqual, r11, r27, rcx); // cmovae r11d, r27d, ecx IID438 - __ ecmovl (Assembler::Condition::aboveEqual, r22, r22, r15); // cmovae r22d, r15d IID439 - __ ecmovl (Assembler::Condition::zero, r31, r30, r19); // cmovz r31d, r30d, r19d IID440 - __ ecmovl (Assembler::Condition::zero, r19, r19, r26); // cmovz r19d, r26d IID441 - __ ecmovl (Assembler::Condition::notZero, r21, r14, r26); // cmovnz r21d, r14d, r26d IID442 - __ ecmovl (Assembler::Condition::notZero, r20, r20, r15); // cmovnz r20d, r15d IID443 - __ ecmovl (Assembler::Condition::belowEqual, r12, r13, r23); // cmovbe r12d, r13d, r23d IID444 - __ ecmovl (Assembler::Condition::belowEqual, r28, r28, r20); // cmovbe r28d, r20d IID445 - __ ecmovl (Assembler::Condition::above, r20, r24, r11); // cmova r20d, r24d, r11d IID446 - __ ecmovl (Assembler::Condition::above, r10, r10, r15); // cmova r10d, r15d IID447 - __ ecmovl (Assembler::Condition::negative, r19, r20, r23); // cmovs r19d, r20d, r23d IID448 - __ ecmovl (Assembler::Condition::negative, r15, r15, r26); // cmovs r15d, r26d IID449 - __ ecmovl (Assembler::Condition::positive, r19, r24, r23); // cmovns r19d, r24d, r23d IID450 - __ ecmovl (Assembler::Condition::positive, r28, r28, r11); // cmovns r28d, r11d IID451 - __ ecmovl (Assembler::Condition::parity, r13, r13, rdx); // cmovp r13d, edx IID452 - __ ecmovl (Assembler::Condition::parity, r31, r31, r23); // cmovp r31d, r23d IID453 - __ ecmovl (Assembler::Condition::noParity, r23, r9, r27); // cmovnp r23d, r9d, r27d IID454 - __ ecmovl (Assembler::Condition::noParity, r21, r21, r20); // cmovnp r21d, r20d IID455 - __ ecmovl (Assembler::Condition::less, r24, r21, r29); // cmovl r24d, r21d, r29d IID456 - __ ecmovl (Assembler::Condition::less, rbx, rbx, r11); // cmovl ebx, r11d IID457 - __ ecmovl (Assembler::Condition::greaterEqual, r21, rbx, rcx); // cmovge r21d, ebx, ecx IID458 - __ ecmovl (Assembler::Condition::greaterEqual, r31, r31, r21); // cmovge r31d, r21d IID459 - __ ecmovl (Assembler::Condition::lessEqual, r15, r25, r30); // cmovle r15d, r25d, r30d IID460 - __ ecmovl (Assembler::Condition::lessEqual, r23, r23, r25); // cmovle r23d, r25d IID461 - __ ecmovl (Assembler::Condition::greater, r18, rcx, r10); // cmovg r18d, ecx, r10d IID462 - __ ecmovl (Assembler::Condition::greater, rcx, rcx, r31); // cmovg ecx, r31d IID463 - __ ecmovl (Assembler::Condition::overflow, r21, r19, Address(r26, -0x6e290873)); // cmovo r21d, r19d, dword ptr [r26-0x6e290873] IID464 - __ ecmovl (Assembler::Condition::noOverflow, r24, r19, Address(r22, rcx, (Address::ScaleFactor)0, +0x11f85f9a)); // cmovno r24d, r19d, dword ptr [r22+rcx*1+0x11f85f9a] IID465 - __ ecmovl (Assembler::Condition::below, r17, r24, Address(r20, +0x534d775e)); // cmovb r17d, r24d, dword ptr [r20+0x534d775e] IID466 - __ ecmovl (Assembler::Condition::aboveEqual, r20, r18, Address(r20, -0x47c94ecd)); // cmovae r20d, r18d, dword ptr [r20-0x47c94ecd] IID467 - __ ecmovl (Assembler::Condition::zero, r9, r13, Address(r23, -0x4b83c563)); // cmovz r9d, r13d, dword ptr [r23-0x4b83c563] IID468 - __ ecmovl (Assembler::Condition::notZero, r11, r25, Address(r24, r14, (Address::ScaleFactor)1, -0x446507af)); // cmovnz r11d, r25d, dword ptr [r24+r14*2-0x446507af] IID469 - __ ecmovl (Assembler::Condition::belowEqual, r14, r24, Address(r30, r13, (Address::ScaleFactor)2, +0xd0661d)); // cmovbe r14d, r24d, dword ptr [r30+r13*4+0xd0661d] IID470 - __ ecmovl (Assembler::Condition::above, r13, r25, Address(r14, r27, (Address::ScaleFactor)3, +0x47e1403)); // cmova r13d, r25d, dword ptr [r14+r27*8+0x47e1403] IID471 - __ ecmovl (Assembler::Condition::negative, r24, r19, Address(rcx, rdx, (Address::ScaleFactor)3, -0x644a5318)); // cmovs r24d, r19d, dword ptr [rcx+rdx*8-0x644a5318] IID472 - __ ecmovl (Assembler::Condition::positive, r26, r24, Address(r22, r22, (Address::ScaleFactor)0, +0x70352446)); // cmovns r26d, r24d, dword ptr [r22+r22*1+0x70352446] IID473 - __ ecmovl (Assembler::Condition::parity, r19, r26, Address(r8, r30, (Address::ScaleFactor)2, +0x78a12f5c)); // cmovp r19d, r26d, dword ptr [r8+r30*4+0x78a12f5c] IID474 - __ ecmovl (Assembler::Condition::noParity, r29, r11, Address(r25, r20, (Address::ScaleFactor)0, +0x27a8303a)); // cmovnp r29d, r11d, dword ptr [r25+r20*1+0x27a8303a] IID475 - __ ecmovl (Assembler::Condition::less, r22, r24, Address(r27, r16, (Address::ScaleFactor)1, +0x2541a10)); // cmovl r22d, r24d, dword ptr [r27+r16*2+0x2541a10] IID476 - __ ecmovl (Assembler::Condition::greaterEqual, r31, r15, Address(r8, r16, (Address::ScaleFactor)3, +0x558e3251)); // cmovge r31d, r15d, dword ptr [r8+r16*8+0x558e3251] IID477 - __ ecmovl (Assembler::Condition::lessEqual, r27, r18, Address(r8, r10, (Address::ScaleFactor)0, -0x471987b7)); // cmovle r27d, r18d, dword ptr [r8+r10*1-0x471987b7] IID478 - __ ecmovl (Assembler::Condition::greater, r18, r16, Address(r18, r19, (Address::ScaleFactor)2, -0x120ae81e)); // cmovg r18d, r16d, dword ptr [r18+r19*4-0x120ae81e] IID479 + __ eorl(rcx, r24, r22, false); // {load}{EVEX}or ecx, r24d, r22d IID432 + __ eorl(rcx, rcx, r19, false); // {load}or ecx, r19d IID433 + __ eorl(r27, r27, r27, false); // {load}or r27d, r27d IID434 + __ eorl(r31, r9, r13, true); // {load}{NF}or r31d, r9d, r13d IID435 + __ eorl(r31, r31, r23, true); // {load}{NF}or r31d, r31d, r23d IID436 + __ eorl(r19, r17, r19, true); // {load}{NF}or r19d, r17d, r19d IID437 + __ eshldl(r20, r16, r24, false); // {load}{EVEX}shld r20d, r16d, r24d, cl IID438 + __ eshldl(rdx, rdx, r12, false); // {load}shld edx, r12d IID439 + __ eshldl(r29, r9, r31, true); // {load}{NF}shld r29d, r9d, r31d, cl IID440 + __ eshldl(r17, r17, r20, true); // {load}{NF}shld r17d, r17d, r20d, cl IID441 + __ eshrdl(r20, r15, r18, false); // {load}{EVEX}shrd r20d, r15d, r18d, cl IID442 + __ eshrdl(rcx, rcx, r12, false); // {load}shrd ecx, r12d IID443 + __ eshrdl(r14, r9, r23, true); // {load}{NF}shrd r14d, r9d, r23d, cl IID444 + __ eshrdl(r19, r19, r13, true); // {load}{NF}shrd r19d, r19d, r13d, cl IID445 + __ esubl(r30, r27, r27, false); // {load}{EVEX}sub r30d, r27d, r27d IID446 + __ esubl(rdx, rdx, r11, false); // {load}sub edx, r11d IID447 + __ esubl(r15, r11, r24, true); // {load}{NF}sub r15d, r11d, r24d IID448 + __ esubl(r14, r14, r25, true); // {load}{NF}sub r14d, r14d, r25d IID449 + __ exorl(r31, r16, r12, false); // {load}{EVEX}xor r31d, r16d, r12d IID450 + __ exorl(r20, r20, r14, false); // {load}xor r20d, r14d IID451 + __ exorl(r30, r13, r30, false); // {load}xor r30d, r13d IID452 + __ exorl(r24, r17, r17, true); // {load}{NF}xor r24d, r17d, r17d IID453 + __ exorl(r26, r26, r21, true); // {load}{NF}xor r26d, r26d, r21d IID454 + __ exorl(r11, r13, r11, true); // {load}{NF}xor r11d, r13d, r11d IID455 + __ eshldl(r27, r25, r21, 4, false); // {EVEX}shld r27d, r25d, r21d, 4 IID456 + __ eshldl(r22, r22, r10, 4, false); // shld r22d, r10d, 4 IID457 + __ eshldl(r21, r15, r24, 16, true); // {NF}shld r21d, r15d, r24d, 16 IID458 + __ eshldl(rdx, rdx, r19, 1, true); // {NF}shld edx, edx, r19d, 1 IID459 + __ eshrdl(r23, r13, r8, 16, false); // {EVEX}shrd r23d, r13d, r8d, 16 IID460 + __ eshrdl(r26, r26, r22, 1, false); // shrd r26d, r22d, 1 IID461 + __ eshrdl(r24, r9, r30, 16, true); // {NF}shrd r24d, r9d, r30d, 16 IID462 + __ eshrdl(r19, r19, r8, 4, true); // {NF}shrd r19d, r19d, r8d, 4 IID463 + __ ecmovl (Assembler::Condition::overflow, r30, r26, r17); // cmovo r30d, r26d, r17d IID464 + __ ecmovl (Assembler::Condition::overflow, r14, r14, r26); // cmovo r14d, r26d IID465 + __ ecmovl (Assembler::Condition::noOverflow, r24, r19, r29); // cmovno r24d, r19d, r29d IID466 + __ ecmovl (Assembler::Condition::noOverflow, r25, r25, r20); // cmovno r25d, r20d IID467 + __ ecmovl (Assembler::Condition::below, r11, r10, r14); // cmovb r11d, r10d, r14d IID468 + __ ecmovl (Assembler::Condition::below, r30, r30, r25); // cmovb r30d, r25d IID469 + __ ecmovl (Assembler::Condition::aboveEqual, r13, r22, r27); // cmovae r13d, r22d, r27d IID470 + __ ecmovl (Assembler::Condition::aboveEqual, r16, r16, r24); // cmovae r16d, r24d IID471 + __ ecmovl (Assembler::Condition::zero, r28, r13, r30); // cmovz r28d, r13d, r30d IID472 + __ ecmovl (Assembler::Condition::zero, r30, r30, r24); // cmovz r30d, r24d IID473 + __ ecmovl (Assembler::Condition::notZero, r21, r20, r31); // cmovnz r21d, r20d, r31d IID474 + __ ecmovl (Assembler::Condition::notZero, r8, r8, r16); // cmovnz r8d, r16d IID475 + __ ecmovl (Assembler::Condition::belowEqual, r15, r26, r22); // cmovbe r15d, r26d, r22d IID476 + __ ecmovl (Assembler::Condition::belowEqual, r31, r31, rdx); // cmovbe r31d, edx IID477 + __ ecmovl (Assembler::Condition::above, r27, r8, r10); // cmova r27d, r8d, r10d IID478 + __ ecmovl (Assembler::Condition::above, r18, r18, r11); // cmova r18d, r11d IID479 + __ ecmovl (Assembler::Condition::negative, r27, rbx, r21); // cmovs r27d, ebx, r21d IID480 + __ ecmovl (Assembler::Condition::negative, r12, r12, r31); // cmovs r12d, r31d IID481 + __ ecmovl (Assembler::Condition::positive, r12, rdx, r18); // cmovns r12d, edx, r18d IID482 + __ ecmovl (Assembler::Condition::positive, r18, r18, r19); // cmovns r18d, r19d IID483 + __ ecmovl (Assembler::Condition::parity, r16, r20, r23); // cmovp r16d, r20d, r23d IID484 + __ ecmovl (Assembler::Condition::parity, r18, r18, r16); // cmovp r18d, r16d IID485 + __ ecmovl (Assembler::Condition::noParity, rbx, r31, r30); // cmovnp ebx, r31d, r30d IID486 + __ ecmovl (Assembler::Condition::noParity, r31, r31, r29); // cmovnp r31d, r29d IID487 + __ ecmovl (Assembler::Condition::less, r28, r25, r10); // cmovl r28d, r25d, r10d IID488 + __ ecmovl (Assembler::Condition::less, r24, r24, r20); // cmovl r24d, r20d IID489 + __ ecmovl (Assembler::Condition::greaterEqual, r16, rdx, r26); // cmovge r16d, edx, r26d IID490 + __ ecmovl (Assembler::Condition::greaterEqual, r28, r28, r28); // cmovge r28d, r28d IID491 + __ ecmovl (Assembler::Condition::lessEqual, r9, r20, r24); // cmovle r9d, r20d, r24d IID492 + __ ecmovl (Assembler::Condition::lessEqual, r24, r24, r29); // cmovle r24d, r29d IID493 + __ ecmovl (Assembler::Condition::greater, r23, r27, r15); // cmovg r23d, r27d, r15d IID494 + __ ecmovl (Assembler::Condition::greater, r12, r12, r18); // cmovg r12d, r18d IID495 + __ ecmovl (Assembler::Condition::overflow, r19, r9, Address(r31, rcx, (Address::ScaleFactor)1, -0x2be98bd)); // cmovo r19d, r9d, dword ptr [r31+rcx*2-0x2be98bd] IID496 + __ ecmovl (Assembler::Condition::overflow, r8, r8, Address(r21, r24, (Address::ScaleFactor)1, +0x41e6a0cb)); // cmovo r8d, dword ptr [r21+r24*2+0x41e6a0cb] IID497 + __ ecmovl (Assembler::Condition::noOverflow, r23, r15, Address(r19, r30, (Address::ScaleFactor)3, -0x55adfe2d)); // cmovno r23d, r15d, dword ptr [r19+r30*8-0x55adfe2d] IID498 + __ ecmovl (Assembler::Condition::noOverflow, rdx, rdx, Address(r27, rdx, (Address::ScaleFactor)0, -0x1aa12735)); // cmovno edx, dword ptr [r27+rdx*1-0x1aa12735] IID499 + __ ecmovl (Assembler::Condition::below, rbx, r29, Address(r31, r12, (Address::ScaleFactor)0, +0xbd42246)); // cmovb ebx, r29d, dword ptr [r31+r12*1+0xbd42246] IID500 + __ ecmovl (Assembler::Condition::below, r21, r21, Address(r19, r21, (Address::ScaleFactor)1, -0x41518818)); // cmovb r21d, dword ptr [r19+r21*2-0x41518818] IID501 + __ ecmovl (Assembler::Condition::aboveEqual, r23, r29, Address(r22, r9, (Address::ScaleFactor)2, -0x35addbd8)); // cmovae r23d, r29d, dword ptr [r22+r9*4-0x35addbd8] IID502 + __ ecmovl (Assembler::Condition::aboveEqual, r18, r18, Address(r25, +0x632184c3)); // cmovae r18d, dword ptr [r25+0x632184c3] IID503 + __ ecmovl (Assembler::Condition::zero, r29, r13, Address(r18, r13, (Address::ScaleFactor)0, -0x3972eac6)); // cmovz r29d, r13d, dword ptr [r18+r13*1-0x3972eac6] IID504 + __ ecmovl (Assembler::Condition::zero, r29, r29, Address(r12, r9, (Address::ScaleFactor)3, -0x668cdfd2)); // cmovz r29d, dword ptr [r12+r9*8-0x668cdfd2] IID505 + __ ecmovl (Assembler::Condition::notZero, r25, r18, Address(r9, r22, (Address::ScaleFactor)2, +0x7f6ac91f)); // cmovnz r25d, r18d, dword ptr [r9+r22*4+0x7f6ac91f] IID506 + __ ecmovl (Assembler::Condition::notZero, r28, r28, Address(r30, +0x562e6594)); // cmovnz r28d, dword ptr [r30+0x562e6594] IID507 + __ ecmovl (Assembler::Condition::belowEqual, r27, r24, Address(r15, r20, (Address::ScaleFactor)2, -0x466538b7)); // cmovbe r27d, r24d, dword ptr [r15+r20*4-0x466538b7] IID508 + __ ecmovl (Assembler::Condition::belowEqual, r25, r25, Address(r26, r11, (Address::ScaleFactor)3, -0x593812a9)); // cmovbe r25d, dword ptr [r26+r11*8-0x593812a9] IID509 + __ ecmovl (Assembler::Condition::above, rcx, r20, Address(r16, -0x1389a3eb)); // cmova ecx, r20d, dword ptr [r16-0x1389a3eb] IID510 + __ ecmovl (Assembler::Condition::above, rbx, rbx, Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615)); // cmova ebx, dword ptr [r29+r8*1+0x1d022615] IID511 + __ ecmovl (Assembler::Condition::negative, rdx, r14, Address(r12, r28, (Address::ScaleFactor)1, -0x51725a91)); // cmovs edx, r14d, dword ptr [r12+r28*2-0x51725a91] IID512 + __ ecmovl (Assembler::Condition::negative, r24, r24, Address(r17, r18, (Address::ScaleFactor)1, -0x1725c4e4)); // cmovs r24d, dword ptr [r17+r18*2-0x1725c4e4] IID513 + __ ecmovl (Assembler::Condition::positive, rcx, rcx, Address(r15, r23, (Address::ScaleFactor)2, -0x6bd22ccf)); // cmovns ecx, dword ptr [r15+r23*4-0x6bd22ccf] IID514 + __ ecmovl (Assembler::Condition::positive, r24, r24, Address(r15, r10, (Address::ScaleFactor)1, -0x7ffb3d09)); // cmovns r24d, dword ptr [r15+r10*2-0x7ffb3d09] IID515 + __ ecmovl (Assembler::Condition::parity, r23, rcx, Address(r11, r23, (Address::ScaleFactor)0, +0x3738c585)); // cmovp r23d, ecx, dword ptr [r11+r23*1+0x3738c585] IID516 + __ ecmovl (Assembler::Condition::parity, r24, r24, Address(r30, r10, (Address::ScaleFactor)0, +0xfcc15a8)); // cmovp r24d, dword ptr [r30+r10*1+0xfcc15a8] IID517 + __ ecmovl (Assembler::Condition::noParity, r14, r26, Address(r14, r21, (Address::ScaleFactor)1, -0x4430ce9f)); // cmovnp r14d, r26d, dword ptr [r14+r21*2-0x4430ce9f] IID518 + __ ecmovl (Assembler::Condition::noParity, r10, r10, Address(r28, +0x3d7c59f)); // cmovnp r10d, dword ptr [r28+0x3d7c59f] IID519 + __ ecmovl (Assembler::Condition::less, r10, r21, Address(r8, r8, (Address::ScaleFactor)3, +0x4a6584b4)); // cmovl r10d, r21d, dword ptr [r8+r8*8+0x4a6584b4] IID520 + __ ecmovl (Assembler::Condition::less, r26, r26, Address(r19, r20, (Address::ScaleFactor)3, +0x47c660ef)); // cmovl r26d, dword ptr [r19+r20*8+0x47c660ef] IID521 + __ ecmovl (Assembler::Condition::greaterEqual, r26, r10, Address(rcx, +0x61977a97)); // cmovge r26d, r10d, dword ptr [rcx+0x61977a97] IID522 + __ ecmovl (Assembler::Condition::greaterEqual, r30, r30, Address(r15, r19, (Address::ScaleFactor)3, +0x53c601cb)); // cmovge r30d, dword ptr [r15+r19*8+0x53c601cb] IID523 + __ ecmovl (Assembler::Condition::lessEqual, r14, r9, Address(r17, -0x566ceee2)); // cmovle r14d, r9d, dword ptr [r17-0x566ceee2] IID524 + __ ecmovl (Assembler::Condition::lessEqual, r15, r15, Address(r27, r20, (Address::ScaleFactor)0, +0x76164792)); // cmovle r15d, dword ptr [r27+r20*1+0x76164792] IID525 + __ ecmovl (Assembler::Condition::greater, r27, r14, Address(r9, r13, (Address::ScaleFactor)2, +0xf5752d7)); // cmovg r27d, r14d, dword ptr [r9+r13*4+0xf5752d7] IID526 + __ ecmovl (Assembler::Condition::greater, r12, r12, Address(rbx, rcx, (Address::ScaleFactor)3, -0x5501b4c6)); // cmovg r12d, dword ptr [rbx+rcx*8-0x5501b4c6] IID527 #endif // _LP64 #ifdef _LP64 - __ adcq(rbx, r31); // {load}adc rbx, r31 IID480 - __ cmpq(r30, r31); // {load}cmp r30, r31 IID481 - __ imulq(r29, r28); // {load}imul r29, r28 IID482 - __ popcntq(r25, r10); // {load}popcnt r25, r10 IID483 - __ sbbq(r24, r20); // {load}sbb r24, r20 IID484 - __ subq(r16, rdx); // {load}sub r16, rdx IID485 - __ tzcntq(r26, r28); // {load}tzcnt r26, r28 IID486 - __ lzcntq(r28, r9); // {load}lzcnt r28, r9 IID487 - __ addq(r20, r24); // {load}add r20, r24 IID488 - __ andq(r24, r29); // {load}and r24, r29 IID489 - __ orq(r23, r27); // {load}or r23, r27 IID490 - __ xorq(r15, r12); // {load}xor r15, r12 IID491 - __ movq(r18, r19); // {load}mov r18, r19 IID492 - __ bsfq(r31, rcx); // {load}bsf r31, rcx IID493 - __ bsrq(r9, r13); // {load}bsr r9, r13 IID494 - __ btq(r20, rcx); // {load}bt r20, rcx IID495 - __ xchgq(r8, r21); // {load}xchg r8, r21 IID496 - __ testq(r24, r14); // {load}test r24, r14 IID497 - __ addq(Address(rcx, r23, (Address::ScaleFactor)2, +0x4ff06c4d), r29); // add qword ptr [rcx+r23*4+0x4ff06c4d], r29 IID498 - __ andq(Address(r24, r10, (Address::ScaleFactor)1, -0x75d9a189), r26); // and qword ptr [r24+r10*2-0x75d9a189], r26 IID499 - __ cmpq(Address(rbx, rbx, (Address::ScaleFactor)0, +0x4033d59c), r17); // cmp qword ptr [rbx+rbx*1+0x4033d59c], r17 IID500 - __ orq(Address(r22, r12, (Address::ScaleFactor)3, -0x3893347d), r18); // or qword ptr [r22+r12*8-0x3893347d], r18 IID501 - __ xorq(Address(r20, r23, (Address::ScaleFactor)3, +0x4b311560), r12); // xor qword ptr [r20+r23*8+0x4b311560], r12 IID502 - __ subq(Address(r10, r28, (Address::ScaleFactor)2, +0x5c3a2657), r29); // sub qword ptr [r10+r28*4+0x5c3a2657], r29 IID503 - __ movq(Address(r13, r25, (Address::ScaleFactor)3, +0x1a3d6f3f), r22); // mov qword ptr [r13+r25*8+0x1a3d6f3f], r22 IID504 - __ xaddq(Address(r17, r24, (Address::ScaleFactor)3, -0x35addbd8), r25); // xadd qword ptr [r17+r24*8-0x35addbd8], r25 IID505 - __ andq(Address(r25, +0x632184c3), 16777216); // and qword ptr [r25+0x632184c3], 16777216 IID506 - __ addq(Address(r13, r13, (Address::ScaleFactor)0, -0x3972eac6), 16777216); // add qword ptr [r13+r13*1-0x3972eac6], 16777216 IID507 - __ cmpq(Address(r9, -0x13b4c806), 4096); // cmp qword ptr [r9-0x13b4c806], 4096 IID508 - __ sarq(Address(r31, +0x4fa7f551), 1); // sar qword ptr [r31+0x4fa7f551], 1 IID509 - __ salq(Address(r21, r31, (Address::ScaleFactor)2, +0x31aa8232), 1); // sal qword ptr [r21+r31*4+0x31aa8232], 1 IID510 - __ sbbq(Address(r24, r31, (Address::ScaleFactor)2, -0x466538b7), 268435456); // sbb qword ptr [r24+r31*4-0x466538b7], 268435456 IID511 - __ shrq(Address(r28, r22, (Address::ScaleFactor)0, -0x3efe85b1), 2); // shr qword ptr [r28+r22*1-0x3efe85b1], 2 IID512 - __ subq(Address(r16, -0x1389a3eb), 1048576); // sub qword ptr [r16-0x1389a3eb], 1048576 IID513 - __ xorq(Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615), 16); // xor qword ptr [r29+r8*1+0x1d022615], 16 IID514 - __ orq(Address(r12, r28, (Address::ScaleFactor)1, -0x34c898e2), 1); // or qword ptr [r12+r28*2-0x34c898e2], 1 IID515 - __ movq(Address(rcx, r24, (Address::ScaleFactor)2, -0x1644eb08), 256); // mov qword ptr [rcx+r24*4-0x1644eb08], 256 IID516 - __ testq(Address(r29, -0x7d23890b), -65536); // test qword ptr [r29-0x7d23890b], -65536 IID517 - __ addq(r23, Address(rcx, r19, (Address::ScaleFactor)2, +0x70eac654)); // add r23, qword ptr [rcx+r19*4+0x70eac654] IID518 - __ andq(rdx, Address(r24, r15, (Address::ScaleFactor)0, -0x204ddaa9)); // and rdx, qword ptr [r24+r15*1-0x204ddaa9] IID519 - __ cmpq(rdx, Address(r23, r11, (Address::ScaleFactor)3, +0x32c930bd)); // cmp rdx, qword ptr [r23+r11*8+0x32c930bd] IID520 - __ lzcntq(r28, Address(rdx, -0x5433c28f)); // lzcnt r28, qword ptr [rdx-0x5433c28f] IID521 - __ orq(r22, Address(r19, r14, (Address::ScaleFactor)1, -0x2cc67d38)); // or r22, qword ptr [r19+r14*2-0x2cc67d38] IID522 - __ adcq(r10, Address(r10, +0x3d7c59f)); // adc r10, qword ptr [r10+0x3d7c59f] IID523 - __ imulq(r10, Address(r8, r8, (Address::ScaleFactor)3, -0xe61862d)); // imul r10, qword ptr [r8+r8*8-0xe61862d] IID524 - __ popcntq(r23, Address(r29, -0x777ed96d)); // popcnt r23, qword ptr [r29-0x777ed96d] IID525 - __ sbbq(rcx, Address(rbx, r19, (Address::ScaleFactor)1, +0x53c601cb)); // sbb rcx, qword ptr [rbx+r19*2+0x53c601cb] IID526 - __ subq(r14, Address(r17, rbx, (Address::ScaleFactor)0, -0x768bf073)); // sub r14, qword ptr [r17+rbx*1-0x768bf073] IID527 - __ tzcntq(r29, Address(r10, r19, (Address::ScaleFactor)1, +0x30c98d3c)); // tzcnt r29, qword ptr [r10+r19*2+0x30c98d3c] IID528 - __ xorq(r10, Address(r16, r27, (Address::ScaleFactor)0, -0x3d08d602)); // xor r10, qword ptr [r16+r27*1-0x3d08d602] IID529 - __ movq(r18, Address(r28, r28, (Address::ScaleFactor)3, -0x62fbac91)); // mov r18, qword ptr [r28+r28*8-0x62fbac91] IID530 - __ leaq(rbx, Address(rcx, +0x450602a5)); // lea rbx, qword ptr [rcx+0x450602a5] IID531 - __ cvttsd2siq(r12, Address(r30, r31, (Address::ScaleFactor)0, -0x6798a630)); // cvttsd2si r12, qword ptr [r30+r31*1-0x6798a630] IID532 - __ xchgq(r31, Address(r24, r10, (Address::ScaleFactor)1, -0x706712ed)); // xchg r31, qword ptr [r24+r10*2-0x706712ed] IID533 - __ testq(r14, Address(r13, r20, (Address::ScaleFactor)3, +0x171081f2)); // test r14, qword ptr [r13+r20*8+0x171081f2] IID534 - __ addq(r31, 16); // add r31, 16 IID535 - __ andq(r25, 16); // and r25, 16 IID536 - __ adcq(r23, 256); // adc r23, 256 IID537 - __ cmpq(r19, 268435456); // cmp r19, 268435456 IID538 - __ rclq(r31, 1); // rcl r31, 1 IID539 - __ rcrq(r17, 1); // rcr r17, 1 IID540 - __ rolq(r25, 2); // rol r25, 2 IID541 - __ rorq(r17, 4); // ror r17, 4 IID542 - __ sarq(r28, 1); // sar r28, 1 IID543 - __ salq(r15, 4); // sal r15, 4 IID544 - __ sbbq(rbx, 65536); // sbb rbx, 65536 IID545 - __ shlq(r21, 1); // shl r21, 1 IID546 - __ shrq(r10, 1); // shr r10, 1 IID547 - __ subq(r14, 16); // sub r14, 16 IID548 - __ xorq(r18, 268435456); // xor r18, 268435456 IID549 - __ movq(r23, 16); // mov r23, 16 IID550 - __ mov64(r12, 1099511627776); // mov r12, 1099511627776 IID551 - __ btq(r14, 4); // bt r14, 4 IID552 - __ testq(r24, -4096); // test r24, -4096 IID553 - __ orq_imm32(r19, 1048576); // or r19, 1048576 IID554 - __ subq_imm32(rcx, 268435456); // sub rcx, 268435456 IID555 - __ cmovq(Assembler::Condition::overflow, rdx, Address(r19, rbx, (Address::ScaleFactor)3, +0x211c8c4)); // cmovo rdx, qword ptr [r19+rbx*8+0x211c8c4] IID556 - __ cmovq(Assembler::Condition::noOverflow, rbx, Address(r21, +0x49267743)); // cmovno rbx, qword ptr [r21+0x49267743] IID557 - __ cmovq(Assembler::Condition::below, r21, Address(r8, r28, (Address::ScaleFactor)1, -0x4c8c2946)); // cmovb r21, qword ptr [r8+r28*2-0x4c8c2946] IID558 - __ cmovq(Assembler::Condition::aboveEqual, r12, Address(r26, r20, (Address::ScaleFactor)0, -0x264df89c)); // cmovae r12, qword ptr [r26+r20*1-0x264df89c] IID559 - __ cmovq(Assembler::Condition::zero, r17, Address(r28, r9, (Address::ScaleFactor)2, +0x3497196b)); // cmovz r17, qword ptr [r28+r9*4+0x3497196b] IID560 - __ cmovq(Assembler::Condition::notZero, r13, Address(r15, r23, (Address::ScaleFactor)1, -0x27a30999)); // cmovnz r13, qword ptr [r15+r23*2-0x27a30999] IID561 - __ cmovq(Assembler::Condition::belowEqual, r22, Address(r22, +0xf39ab05)); // cmovbe r22, qword ptr [r22+0xf39ab05] IID562 - __ cmovq(Assembler::Condition::above, rcx, Address(r22, r26, (Address::ScaleFactor)3, -0x48c954c)); // cmova rcx, qword ptr [r22+r26*8-0x48c954c] IID563 - __ cmovq(Assembler::Condition::negative, r25, Address(r19, r21, (Address::ScaleFactor)0, +0xe405b0b)); // cmovs r25, qword ptr [r19+r21*1+0xe405b0b] IID564 - __ cmovq(Assembler::Condition::positive, r12, Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b)); // cmovns r12, qword ptr [r19+r29*8-0x7762044b] IID565 - __ cmovq(Assembler::Condition::parity, rbx, Address(r30, r10, (Address::ScaleFactor)1, -0x19798323)); // cmovp rbx, qword ptr [r30+r10*2-0x19798323] IID566 - __ cmovq(Assembler::Condition::noParity, r21, Address(r24, r31, (Address::ScaleFactor)0, -0x5731652b)); // cmovnp r21, qword ptr [r24+r31*1-0x5731652b] IID567 - __ cmovq(Assembler::Condition::less, r18, Address(r8, r10, (Address::ScaleFactor)1, -0x5613be89)); // cmovl r18, qword ptr [r8+r10*2-0x5613be89] IID568 - __ cmovq(Assembler::Condition::greaterEqual, r28, Address(r21, r21, (Address::ScaleFactor)3, +0x65a0fdc4)); // cmovge r28, qword ptr [r21+r21*8+0x65a0fdc4] IID569 - __ cmovq(Assembler::Condition::lessEqual, r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c)); // cmovle r23, qword ptr [r11+r18*1-0x1d1af10c] IID570 - __ cmovq(Assembler::Condition::greater, r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38)); // cmovg r22, qword ptr [r18+r12*2+0x1a5f1c38] IID571 - __ call(r23); // call r23 IID572 - __ divq(r30); // div r30 IID573 - __ idivq(r19); // idiv r19 IID574 - __ imulq(r9); // imul r9 IID575 - __ mulq(r13); // mul r13 IID576 - __ negq(r16); // neg r16 IID577 - __ notq(r29); // not r29 IID578 - __ rolq(rcx); // rol rcx, cl IID579 - __ rorq(r25); // ror r25, cl IID580 - __ sarq(r8); // sar r8, cl IID581 - __ salq(r27); // sal r27, cl IID582 - __ shlq(r30); // shl r30, cl IID583 - __ shrq(r23); // shr r23, cl IID584 - __ incrementq(rbx); // inc rbx IID585 - __ decrementq(r14); // dec r14 IID586 - __ pushp(r21); // pushp r21 IID587 - __ popp(r21); // popp r21 IID588 - __ call(Address(r20, r21, (Address::ScaleFactor)1, +0x56c6af2f)); // call qword ptr [r20+r21*2+0x56c6af2f] IID589 - __ mulq(Address(r31, r19, (Address::ScaleFactor)3, -0x1b4eb23)); // mul qword ptr [r31+r19*8-0x1b4eb23] IID590 - __ negq(Address(r27, r27, (Address::ScaleFactor)0, -0x58dbfc1f)); // neg qword ptr [r27+r27*1-0x58dbfc1f] IID591 - __ sarq(Address(rbx, r22, (Address::ScaleFactor)2, -0x606349d1)); // sar qword ptr [rbx+r22*4-0x606349d1], cl IID592 - __ salq(Address(r26, r23, (Address::ScaleFactor)3, +0xb95a079)); // sal qword ptr [r26+r23*8+0xb95a079], cl IID593 - __ shrq(Address(r14, r26, (Address::ScaleFactor)0, +0x3544e09)); // shr qword ptr [r14+r26*1+0x3544e09], cl IID594 - __ incrementq(Address(r27, rdx, (Address::ScaleFactor)0, +0x120b3250)); // inc qword ptr [r27+rdx*1+0x120b3250] IID595 - __ decrementq(Address(r9, r25, (Address::ScaleFactor)2, -0x34aaeccb)); // dec qword ptr [r9+r25*4-0x34aaeccb] IID596 - __ imulq(r20, Address(r16, r28, (Address::ScaleFactor)1, -0x59de05a5), 1048576); // imul r20, qword ptr [r16+r28*2-0x59de05a5], 1048576 IID597 - __ imulq(r17, r23, 256); // imul r17, r23, 256 IID598 - __ shldq(r19, r11, 8); // shld r19, r11, 8 IID599 - __ shrdq(r28, r10, 8); // shrd r28, r10, 8 IID600 - __ pop2(r29, r26); // {load}pop2 r26, r29 IID601 - __ pop2p(r22, r10); // {load}pop2p r10, r22 IID602 - __ push2(r25, r30); // {load}push2 r30, r25 IID603 - __ push2p(r28, r15); // {load}push2p r15, r28 IID604 - __ movzbq(r11, Address(r29, r19, (Address::ScaleFactor)2, -0x12368d34)); // movzx r11, byte ptr [r29+r19*4-0x12368d34] IID605 - __ movzwq(r14, Address(r8, r30, (Address::ScaleFactor)2, -0x4a9392de)); // movzx r14, word ptr [r8+r30*4-0x4a9392de] IID606 - __ movsbq(r28, Address(r23, r15, (Address::ScaleFactor)0, +0x6189cb54)); // movsx r28, byte ptr [r23+r15*1+0x6189cb54] IID607 - __ movswq(r28, Address(rbx, r23, (Address::ScaleFactor)3, -0x2de86561)); // movsx r28, word ptr [rbx+r23*8-0x2de86561] IID608 - __ movzbq(r11, rcx); // movzx r11, cl IID609 - __ movzwq(r30, r15); // movzx r30, r15w IID610 - __ movsbq(r14, rcx); // movsx r14, cl IID611 - __ movswq(r23, r9); // movsx r23, r9w IID612 - __ cmpxchgq(r12, Address(r13, r10, (Address::ScaleFactor)1, -0x7c62c3a)); // cmpxchg qword ptr [r13+r10*2-0x7c62c3a], r12 IID613 - __ eidivq(rcx, false); // {EVEX}idiv rcx IID614 - __ eidivq(r15, true); // {NF}idiv r15 IID615 - __ edivq(r23, false); // {EVEX}div r23 IID616 - __ edivq(r24, true); // {NF}div r24 IID617 - __ eimulq(r27, false); // {EVEX}imul r27 IID618 - __ eimulq(r30, true); // {NF}imul r30 IID619 - __ emulq(r12, false); // {EVEX}mul r12 IID620 - __ emulq(rcx, true); // {NF}mul rcx IID621 - __ emulq(Address(r13, r9, (Address::ScaleFactor)3, -0x226aab94), false); // {EVEX}mul qword ptr [r13+r9*8-0x226aab94] IID622 - __ emulq(Address(r13, r24, (Address::ScaleFactor)3, -0x286c7605), true); // {NF}mul qword ptr [r13+r24*8-0x286c7605] IID623 - __ eimulq(r21, r30, false); // {EVEX}imul r21, r30 IID624 - __ eimulq(r17, r17, false); // imul r17 IID625 - __ eimulq(r29, r12, true); // {NF}imul r29, r12 IID626 - __ eimulq(r30, r30, true); // {NF}imul r30, r30 IID627 - __ elzcntq(r24, r15, false); // {EVEX}lzcnt r24, r15 IID628 - __ elzcntq(r25, r25, false); // {EVEX}lzcnt r25, r25 IID629 - __ elzcntq(r25, r21, true); // {NF}lzcnt r25, r21 IID630 - __ elzcntq(r22, r22, true); // {NF}lzcnt r22, r22 IID631 - __ enegq(r17, r30, false); // {EVEX}neg r17, r30 IID632 - __ enegq(r17, r17, false); // neg r17 IID633 - __ enegq(r31, r17, true); // {NF}neg r31, r17 IID634 - __ enegq(r29, r29, true); // {NF}neg r29, r29 IID635 - __ enotq(r10, r9); // {EVEX}not r10, r9 IID636 - __ enotq(r24, r24); // not r24 IID637 - __ epopcntq(r28, r15, false); // {EVEX}popcnt r28, r15 IID638 - __ epopcntq(r10, r10, false); // {EVEX}popcnt r10, r10 IID639 - __ epopcntq(r27, r30, true); // {NF}popcnt r27, r30 IID640 - __ epopcntq(r28, r28, true); // {NF}popcnt r28, r28 IID641 - __ erolq(r28, r14, false); // {EVEX}rol r28, r14, cl IID642 - __ erolq(r23, r23, false); // rol r23, cl IID643 - __ erolq(r23, r24, true); // {NF}rol r23, r24, cl IID644 - __ erolq(r21, r21, true); // {NF}rol r21, r21, cl IID645 - __ erorq(r31, r22, false); // {EVEX}ror r31, r22, cl IID646 - __ erorq(r28, r28, false); // ror r28, cl IID647 - __ erorq(r17, r10, true); // {NF}ror r17, r10, cl IID648 - __ erorq(r9, r9, true); // {NF}ror r9, r9, cl IID649 - __ esalq(r29, r30, false); // {EVEX}sal r29, r30, cl IID650 - __ esalq(r11, r11, false); // sal r11, cl IID651 - __ esalq(r26, r11, true); // {NF}sal r26, r11, cl IID652 - __ esalq(r16, r16, true); // {NF}sal r16, r16, cl IID653 - __ esarq(rbx, r15, false); // {EVEX}sar rbx, r15, cl IID654 - __ esarq(r14, r14, false); // sar r14, cl IID655 - __ esarq(r25, r16, true); // {NF}sar r25, r16, cl IID656 - __ esarq(r8, r8, true); // {NF}sar r8, r8, cl IID657 - __ edecq(r11, r13, false); // {EVEX}dec r11, r13 IID658 - __ edecq(rcx, rcx, false); // dec rcx IID659 - __ edecq(r21, r18, true); // {NF}dec r21, r18 IID660 - __ edecq(r28, r28, true); // {NF}dec r28, r28 IID661 - __ eincq(r16, r16, false); // inc r16 IID662 - __ eincq(r29, r29, false); // inc r29 IID663 - __ eincq(r18, r9, true); // {NF}inc r18, r9 IID664 - __ eincq(r19, r19, true); // {NF}inc r19, r19 IID665 - __ eshlq(r19, r18, false); // {EVEX}shl r19, r18, cl IID666 - __ eshlq(r8, r8, false); // shl r8, cl IID667 - __ eshlq(r12, r15, true); // {NF}shl r12, r15, cl IID668 - __ eshlq(r29, r29, true); // {NF}shl r29, r29, cl IID669 - __ eshrq(r28, r24, false); // {EVEX}shr r28, r24, cl IID670 - __ eshrq(r19, r19, false); // shr r19, cl IID671 - __ eshrq(r8, r28, true); // {NF}shr r8, r28, cl IID672 - __ eshrq(r17, r17, true); // {NF}shr r17, r17, cl IID673 - __ etzcntq(r28, r16, false); // {EVEX}tzcnt r28, r16 IID674 - __ etzcntq(r14, r14, false); // {EVEX}tzcnt r14, r14 IID675 - __ etzcntq(r12, r31, true); // {NF}tzcnt r12, r31 IID676 - __ etzcntq(r14, r14, true); // {NF}tzcnt r14, r14 IID677 - __ eimulq(r31, Address(r13, -0x69c4b352), false); // {EVEX}imul r31, qword ptr [r13-0x69c4b352] IID678 - __ eimulq(r17, Address(r18, -0x60ab1105), true); // {NF}imul r17, qword ptr [r18-0x60ab1105] IID679 - __ elzcntq(r27, Address(r14, r25, (Address::ScaleFactor)2, +0x2798bf83), false); // {EVEX}lzcnt r27, qword ptr [r14+r25*4+0x2798bf83] IID680 - __ elzcntq(r23, Address(r10, r11, (Address::ScaleFactor)0, -0x378e635d), true); // {NF}lzcnt r23, qword ptr [r10+r11*1-0x378e635d] IID681 - __ enegq(rcx, Address(r19, r9, (Address::ScaleFactor)3, -0x6847d440), false); // {EVEX}neg rcx, qword ptr [r19+r9*8-0x6847d440] IID682 - __ enegq(rcx, Address(rbx, rcx, (Address::ScaleFactor)0, +0x6f92d38d), true); // {NF}neg rcx, qword ptr [rbx+rcx*1+0x6f92d38d] IID683 - __ epopcntq(r20, Address(r12, -0x2a8b27d6), false); // {EVEX}popcnt r20, qword ptr [r12-0x2a8b27d6] IID684 - __ epopcntq(r31, Address(r30, +0x4603f6d0), true); // {NF}popcnt r31, qword ptr [r30+0x4603f6d0] IID685 - __ esalq(rbx, Address(r24, +0x567d06f9), false); // {EVEX}sal rbx, qword ptr [r24+0x567d06f9], cl IID686 - __ esalq(r12, Address(r24, r28, (Address::ScaleFactor)0, -0x1c4c584e), true); // {NF}sal r12, qword ptr [r24+r28*1-0x1c4c584e], cl IID687 - __ esarq(r12, Address(r23, r24, (Address::ScaleFactor)2, -0x3157bcba), false); // {EVEX}sar r12, qword ptr [r23+r24*4-0x3157bcba], cl IID688 - __ esarq(r8, Address(r14, r24, (Address::ScaleFactor)2, -0x714290a5), true); // {NF}sar r8, qword ptr [r14+r24*4-0x714290a5], cl IID689 - __ edecq(r23, Address(r8, r15, (Address::ScaleFactor)1, -0x5ae272dd), false); // {EVEX}dec r23, qword ptr [r8+r15*2-0x5ae272dd] IID690 - __ edecq(r13, Address(r29, r9, (Address::ScaleFactor)3, -0x5b5174a9), true); // {NF}dec r13, qword ptr [r29+r9*8-0x5b5174a9] IID691 - __ eincq(r11, Address(r21, r31, (Address::ScaleFactor)3, -0x2176b4dc), false); // {EVEX}inc r11, qword ptr [r21+r31*8-0x2176b4dc] IID692 - __ eincq(r13, Address(rcx, r16, (Address::ScaleFactor)0, -0x36b448c9), true); // {NF}inc r13, qword ptr [rcx+r16*1-0x36b448c9] IID693 - __ eshrq(r26, Address(r25, rcx, (Address::ScaleFactor)2, -0x5f894993), false); // {EVEX}shr r26, qword ptr [r25+rcx*4-0x5f894993], cl IID694 - __ eshrq(r25, Address(r9, +0x51798d21), true); // {NF}shr r25, qword ptr [r9+0x51798d21], cl IID695 - __ etzcntq(r28, Address(r13, r26, (Address::ScaleFactor)2, +0x207196f6), false); // {EVEX}tzcnt r28, qword ptr [r13+r26*4+0x207196f6] IID696 - __ etzcntq(rbx, Address(r19, r13, (Address::ScaleFactor)0, -0x24d937d5), true); // {NF}tzcnt rbx, qword ptr [r19+r13*1-0x24d937d5] IID697 - __ eaddq(r17, Address(r30, +0x3935ccff), r31, false); // {EVEX}add r17, qword ptr [r30+0x3935ccff], r31 IID698 - __ eaddq(r14, Address(r27, r10, (Address::ScaleFactor)2, -0x34ad9bab), r14, false); // {EVEX}add r14, qword ptr [r27+r10*4-0x34ad9bab], r14 IID699 - __ eaddq(r18, Address(r20, r23, (Address::ScaleFactor)0, +0x5ad3ed4b), r30, true); // {NF}add r18, qword ptr [r20+r23*1+0x5ad3ed4b], r30 IID700 - __ eaddq(r20, Address(rdx, -0x322a99e5), r20, true); // {NF}add r20, qword ptr [rdx-0x322a99e5], r20 IID701 - __ eandq(r31, Address(rbx, r27, (Address::ScaleFactor)3, +0x4ce247d2), r17, false); // {EVEX}and r31, qword ptr [rbx+r27*8+0x4ce247d2], r17 IID702 - __ eandq(r30, Address(r18, r19, (Address::ScaleFactor)1, -0x4ee3d14), r30, false); // {EVEX}and r30, qword ptr [r18+r19*2-0x4ee3d14], r30 IID703 - __ eandq(r28, Address(r11, rbx, (Address::ScaleFactor)3, -0x28994bbf), r24, true); // {NF}and r28, qword ptr [r11+rbx*8-0x28994bbf], r24 IID704 - __ eandq(r30, Address(r22, +0x7d21c24), r30, true); // {NF}and r30, qword ptr [r22+0x7d21c24], r30 IID705 - __ eorq(r26, Address(r15, r19, (Address::ScaleFactor)3, +0x58c21792), r20, false); // {EVEX}or r26, qword ptr [r15+r19*8+0x58c21792], r20 IID706 - __ eorq(r13, Address(r10, r27, (Address::ScaleFactor)2, -0x2c70d333), r13, false); // {EVEX}or r13, qword ptr [r10+r27*4-0x2c70d333], r13 IID707 - __ eorq(rbx, Address(r12, rbx, (Address::ScaleFactor)0, -0x1fb0f1bc), r26, true); // {NF}or rbx, qword ptr [r12+rbx*1-0x1fb0f1bc], r26 IID708 - __ eorq(r31, Address(r27, r31, (Address::ScaleFactor)1, +0x28d1756), r31, true); // {NF}or r31, qword ptr [r27+r31*2+0x28d1756], r31 IID709 - __ esubq(r24, Address(r28, r23, (Address::ScaleFactor)1, +0x6980f610), r27, false); // {EVEX}sub r24, qword ptr [r28+r23*2+0x6980f610], r27 IID710 - __ esubq(r15, Address(r11, r30, (Address::ScaleFactor)3, -0x49777e7), r15, false); // {EVEX}sub r15, qword ptr [r11+r30*8-0x49777e7], r15 IID711 - __ esubq(r17, Address(r25, r13, (Address::ScaleFactor)2, +0x31619e46), r31, true); // {NF}sub r17, qword ptr [r25+r13*4+0x31619e46], r31 IID712 - __ esubq(r18, Address(r11, r10, (Address::ScaleFactor)2, +0x1922861a), r18, true); // {NF}sub r18, qword ptr [r11+r10*4+0x1922861a], r18 IID713 - __ exorq(rbx, Address(r11, -0x4716d420), r21, false); // {EVEX}xor rbx, qword ptr [r11-0x4716d420], r21 IID714 - __ exorq(r8, Address(rdx, r9, (Address::ScaleFactor)2, -0x4cfe39c), r8, false); // {EVEX}xor r8, qword ptr [rdx+r9*4-0x4cfe39c], r8 IID715 - __ exorq(r16, Address(r14, r27, (Address::ScaleFactor)0, +0x7c6654d9), r25, true); // {NF}xor r16, qword ptr [r14+r27*1+0x7c6654d9], r25 IID716 - __ exorq(r29, Address(r15, -0x5efab479), r29, true); // {NF}xor r29, qword ptr [r15-0x5efab479], r29 IID717 - __ eaddq(r19, Address(r13, r22, (Address::ScaleFactor)2, +0x68b64559), 16777216, false); // {EVEX}add r19, qword ptr [r13+r22*4+0x68b64559], 16777216 IID718 - __ eaddq(r16, Address(r13, r31, (Address::ScaleFactor)3, -0x65143af5), 1, true); // {NF}add r16, qword ptr [r13+r31*8-0x65143af5], 1 IID719 - __ eandq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), 1, false); // {EVEX}and r31, qword ptr [r24+r13*2-0x25b16a0e], 1 IID720 - __ eandq(r11, Address(r28, -0xf6d4b26), 65536, true); // {NF}and r11, qword ptr [r28-0xf6d4b26], 65536 IID721 - __ eimulq(rcx, Address(r18, r10, (Address::ScaleFactor)0, +0x46ec6da1), 16777216, false); // {EVEX}imul rcx, qword ptr [r18+r10*1+0x46ec6da1], 16777216 IID722 - __ eimulq(r15, Address(r9, r10, (Address::ScaleFactor)3, -0x7fc36af3), 16, true); // {NF}imul r15, qword ptr [r9+r10*8-0x7fc36af3], 16 IID723 - __ eorq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), 1, false); // {EVEX}or r17, qword ptr [r27+r30*1+0x1b4cda2c], 1 IID724 - __ eorq(rdx, Address(r25, r14, (Address::ScaleFactor)2, -0x59aa6b85), 4096, true); // {NF}or rdx, qword ptr [r25+r14*4-0x59aa6b85], 4096 IID725 - __ esalq(r17, Address(r26, r21, (Address::ScaleFactor)1, -0x6ab1f15f), 8, false); // {EVEX}sal r17, qword ptr [r26+r21*2-0x6ab1f15f], 8 IID726 - __ esalq(r12, Address(r22, r17, (Address::ScaleFactor)0, -0x43ac14ab), 2, true); // {NF}sal r12, qword ptr [r22+r17*1-0x43ac14ab], 2 IID727 - __ esarq(r29, Address(r18, r16, (Address::ScaleFactor)0, -0x59dc0c61), 4, false); // {EVEX}sar r29, qword ptr [r18+r16*1-0x59dc0c61], 4 IID728 - __ esarq(r16, Address(r11, -0x7bdd314), 4, true); // {NF}sar r16, qword ptr [r11-0x7bdd314], 4 IID729 - __ eshrq(r26, Address(r23, r27, (Address::ScaleFactor)3, -0x55b92314), 16, false); // {EVEX}shr r26, qword ptr [r23+r27*8-0x55b92314], 16 IID730 - __ eshrq(r23, Address(r16, r29, (Address::ScaleFactor)1, +0x71311a1d), 2, true); // {NF}shr r23, qword ptr [r16+r29*2+0x71311a1d], 2 IID731 - __ esubq(r25, Address(r9, -0x9532bac), 1048576, false); // {EVEX}sub r25, qword ptr [r9-0x9532bac], 1048576 IID732 - __ esubq(r17, Address(r8, r23, (Address::ScaleFactor)0, +0x55d06ca2), 1048576, true); // {NF}sub r17, qword ptr [r8+r23*1+0x55d06ca2], 1048576 IID733 - __ exorq(r29, Address(r9, r24, (Address::ScaleFactor)0, -0x2c141c1), 1048576, false); // {EVEX}xor r29, qword ptr [r9+r24*1-0x2c141c1], 1048576 IID734 - __ exorq(r28, Address(r22, r19, (Address::ScaleFactor)1, -0x2d9d9abd), 16, true); // {NF}xor r28, qword ptr [r22+r19*2-0x2d9d9abd], 16 IID735 - __ eaddq(r22, r14, 16, false); // {EVEX}add r22, r14, 16 IID736 - __ eaddq(rax, r12, 16, false); // {EVEX}add rax, r12, 16 IID737 - __ eaddq(r24, r24, 65536, false); // add r24, 65536 IID738 - __ eaddq(r21, rbx, 65536, true); // {NF}add r21, rbx, 65536 IID739 - __ eaddq(rax, rbx, 65536, true); // {NF}add rax, rbx, 65536 IID740 - __ eaddq(r24, r24, 65536, true); // {NF}add r24, r24, 65536 IID741 - __ eandq(r21, r27, 16777216, false); // {EVEX}and r21, r27, 16777216 IID742 - __ eandq(rax, r27, 16777216, false); // {EVEX}and rax, r27, 16777216 IID743 - __ eandq(r24, r24, 65536, false); // and r24, 65536 IID744 - __ eandq(r13, r31, 1048576, true); // {NF}and r13, r31, 1048576 IID745 - __ eandq(rax, r21, 1048576, true); // {NF}and rax, r21, 1048576 IID746 - __ eandq(r30, r30, 1048576, true); // {NF}and r30, r30, 1048576 IID747 - __ eimulq(r8, r13, 268435456, false); // {EVEX}imul r8, r13, 268435456 IID748 - __ eimulq(rax, r31, 268435456, false); // {EVEX}imul rax, r31, 268435456 IID749 - __ eimulq(r13, r13, 65536, false); // {EVEX}imul r13, r13, 65536 IID750 - __ eimulq(r14, r29, 1048576, true); // {NF}imul r14, r29, 1048576 IID751 - __ eimulq(rax, r22, 1048576, true); // {NF}imul rax, r22, 1048576 IID752 - __ eimulq(r8, r8, 268435456, true); // {NF}imul r8, r8, 268435456 IID753 - __ eorq(r30, r15, 4096, false); // {EVEX}or r30, r15, 4096 IID754 - __ eorq(rax, r28, 4096, false); // {EVEX}or rax, r28, 4096 IID755 - __ eorq(r26, r26, 1048576, false); // or r26, 1048576 IID756 - __ eorq(r16, r12, 268435456, true); // {NF}or r16, r12, 268435456 IID757 - __ eorq(rax, r9, 268435456, true); // {NF}or rax, r9, 268435456 IID758 - __ eorq(r23, r23, 256, true); // {NF}or r23, r23, 256 IID759 - __ erclq(r15, r9, 16); // {EVEX}rcl r15, r9, 16 IID760 - __ erclq(rax, r8, 16); // {EVEX}rcl rax, r8, 16 IID761 - __ erclq(r25, r25, 1); // rcl r25, 1 IID762 - __ erolq(r9, r17, 16, false); // {EVEX}rol r9, r17, 16 IID763 - __ erolq(rax, r20, 16, false); // {EVEX}rol rax, r20, 16 IID764 - __ erolq(r27, r27, 1, false); // rol r27, 1 IID765 - __ erolq(r20, r31, 1, true); // {NF}rol r20, r31, 1 IID766 - __ erolq(rax, r18, 1, true); // {NF}rol rax, r18, 1 IID767 - __ erolq(r28, r28, 16, true); // {NF}rol r28, r28, 16 IID768 - __ erorq(r26, r18, 16, false); // {EVEX}ror r26, r18, 16 IID769 - __ erorq(rax, r24, 16, false); // {EVEX}ror rax, r24, 16 IID770 - __ erorq(r22, r22, 16, false); // ror r22, 16 IID771 - __ erorq(r27, r29, 1, true); // {NF}ror r27, r29, 1 IID772 - __ erorq(rax, r18, 1, true); // {NF}ror rax, r18, 1 IID773 - __ erorq(r21, r21, 1, true); // {NF}ror r21, r21, 1 IID774 - __ esalq(r12, rcx, 2, false); // {EVEX}sal r12, rcx, 2 IID775 - __ esalq(rax, r24, 2, false); // {EVEX}sal rax, r24, 2 IID776 - __ esalq(r22, r22, 8, false); // sal r22, 8 IID777 - __ esalq(r17, r23, 8, true); // {NF}sal r17, r23, 8 IID778 - __ esalq(rax, r27, 8, true); // {NF}sal rax, r27, 8 IID779 - __ esalq(r23, r23, 1, true); // {NF}sal r23, r23, 1 IID780 - __ esarq(r8, r25, 16, false); // {EVEX}sar r8, r25, 16 IID781 - __ esarq(rax, r23, 16, false); // {EVEX}sar rax, r23, 16 IID782 - __ esarq(r9, r9, 4, false); // sar r9, 4 IID783 - __ esarq(r22, r13, 1, true); // {NF}sar r22, r13, 1 IID784 - __ esarq(rax, r11, 1, true); // {NF}sar rax, r11, 1 IID785 - __ esarq(r12, r12, 2, true); // {NF}sar r12, r12, 2 IID786 - __ eshlq(rcx, r30, 8, false); // {EVEX}shl rcx, r30, 8 IID787 - __ eshlq(rax, r19, 8, false); // {EVEX}shl rax, r19, 8 IID788 - __ eshlq(r13, r13, 2, false); // shl r13, 2 IID789 - __ eshlq(r18, r11, 8, true); // {NF}shl r18, r11, 8 IID790 - __ eshlq(rax, r9, 8, true); // {NF}shl rax, r9, 8 IID791 - __ eshlq(rcx, rcx, 16, true); // {NF}shl rcx, rcx, 16 IID792 - __ eshrq(r10, r22, 4, false); // {EVEX}shr r10, r22, 4 IID793 - __ eshrq(rax, r9, 4, false); // {EVEX}shr rax, r9, 4 IID794 - __ eshrq(r12, r12, 2, false); // shr r12, 2 IID795 - __ eshrq(r26, r31, 8, true); // {NF}shr r26, r31, 8 IID796 - __ eshrq(rax, r12, 8, true); // {NF}shr rax, r12, 8 IID797 - __ eshrq(r28, r28, 1, true); // {NF}shr r28, r28, 1 IID798 - __ esubq(r15, r30, 65536, false); // {EVEX}sub r15, r30, 65536 IID799 - __ esubq(rax, rcx, 65536, false); // {EVEX}sub rax, rcx, 65536 IID800 - __ esubq(r26, r26, 16, false); // sub r26, 16 IID801 - __ esubq(r12, r14, 1, true); // {NF}sub r12, r14, 1 IID802 - __ esubq(rax, r21, 1, true); // {NF}sub rax, r21, 1 IID803 - __ esubq(r20, r20, 1048576, true); // {NF}sub r20, r20, 1048576 IID804 - __ exorq(r11, rbx, 16777216, false); // {EVEX}xor r11, rbx, 16777216 IID805 - __ exorq(rax, r23, 16777216, false); // {EVEX}xor rax, r23, 16777216 IID806 - __ exorq(r31, r31, 268435456, false); // xor r31, 268435456 IID807 - __ exorq(r29, r28, 4096, true); // {NF}xor r29, r28, 4096 IID808 - __ exorq(rax, r19, 4096, true); // {NF}xor rax, r19, 4096 IID809 - __ exorq(rdx, rdx, 268435456, true); // {NF}xor rdx, rdx, 268435456 IID810 - __ eorq_imm32(rdx, rdx, 1048576, false); // or rdx, 1048576 IID811 - __ eorq_imm32(rax, r22, 1048576, false); // {EVEX}or rax, r22, 1048576 IID812 - __ eorq_imm32(r29, r29, 1048576, false); // or r29, 1048576 IID813 - __ eorq_imm32(r17, rcx, 4194304, false); // {EVEX}or r17, rcx, 4194304 IID814 - __ eorq_imm32(rax, r25, 4194304, false); // {EVEX}or rax, r25, 4194304 IID815 - __ eorq_imm32(r27, r27, 1073741824, false); // or r27, 1073741824 IID816 - __ esubq_imm32(r16, r19, 4194304, false); // {EVEX}sub r16, r19, 4194304 IID817 - __ esubq_imm32(rax, r31, 4194304, false); // {EVEX}sub rax, r31, 4194304 IID818 - __ esubq_imm32(r26, r26, 262144, false); // sub r26, 262144 IID819 - __ esubq_imm32(r17, r22, 1073741824, true); // {NF}sub r17, r22, 1073741824 IID820 - __ esubq_imm32(rax, r18, 1073741824, true); // {NF}sub rax, r18, 1073741824 IID821 - __ esubq_imm32(r23, r23, 268435456, true); // {NF}sub r23, r23, 268435456 IID822 - __ eaddq(r13, r30, Address(r24, r19, (Address::ScaleFactor)1, +0x56ea3a3b), false); // {EVEX}add r13, r30, qword ptr [r24+r19*2+0x56ea3a3b] IID823 - __ eaddq(r29, r15, Address(r26, r27, (Address::ScaleFactor)3, -0x4b113958), true); // {NF}add r29, r15, qword ptr [r26+r27*8-0x4b113958] IID824 - __ eandq(r12, r30, Address(r31, -0x46103c74), false); // {EVEX}and r12, r30, qword ptr [r31-0x46103c74] IID825 - __ eandq(r27, r10, Address(r22, r25, (Address::ScaleFactor)1, +0x6a1ebee5), true); // {NF}and r27, r10, qword ptr [r22+r25*2+0x6a1ebee5] IID826 - __ eorq(r30, r26, Address(r11, r18, (Address::ScaleFactor)2, -0x2b9fff29), false); // {EVEX}or r30, r26, qword ptr [r11+r18*4-0x2b9fff29] IID827 - __ eorq(r9, r12, Address(r18, r17, (Address::ScaleFactor)0, +0xb4859f6), true); // {NF}or r9, r12, qword ptr [r18+r17*1+0xb4859f6] IID828 - __ eimulq(rdx, r17, Address(r24, rdx, (Address::ScaleFactor)2, +0x3d284cd8), false); // {EVEX}imul rdx, r17, qword ptr [r24+rdx*4+0x3d284cd8] IID829 - __ eimulq(r29, r26, Address(r30, r12, (Address::ScaleFactor)1, +0x6e813124), true); // {NF}imul r29, r26, qword ptr [r30+r12*2+0x6e813124] IID830 - __ esubq(rbx, r13, Address(r22, -0x702a289e), false); // {EVEX}sub rbx, r13, qword ptr [r22-0x702a289e] IID831 - __ esubq(r23, r29, Address(r25, rdx, (Address::ScaleFactor)0, -0x6252a7ed), true); // {NF}sub r23, r29, qword ptr [r25+rdx*1-0x6252a7ed] IID832 - __ exorq(r8, r18, Address(r19, r14, (Address::ScaleFactor)2, -0xebfa697), false); // {EVEX}xor r8, r18, qword ptr [r19+r14*4-0xebfa697] IID833 - __ exorq(r10, r28, Address(r26, +0x168381ca), true); // {NF}xor r10, r28, qword ptr [r26+0x168381ca] IID834 - __ eaddq(rcx, r18, r8, false); // {load}{EVEX}add rcx, r18, r8 IID835 - __ eaddq(rcx, rcx, r14, false); // {load}add rcx, r14 IID836 - __ eaddq(r23, r10, r16, true); // {load}{NF}add r23, r10, r16 IID837 - __ eaddq(r11, r11, r24, true); // {load}{NF}add r11, r11, r24 IID838 - __ eadcxq(r9, r18, rdx); // {load}{EVEX}adcx r9, r18, rdx IID839 - __ eadcxq(r8, r8, r15); // {load}adcx r8, r15 IID840 - __ eadoxq(r15, r22, r26); // {load}{EVEX}adox r15, r22, r26 IID841 - __ eadoxq(r11, r11, rdx); // {load}adox r11, rdx IID842 - __ eandq(r19, rdx, r22, false); // {load}{EVEX}and r19, rdx, r22 IID843 - __ eandq(r29, r29, r17, false); // {load}and r29, r17 IID844 - __ eandq(r23, r27, r15, true); // {load}{NF}and r23, r27, r15 IID845 - __ eandq(r9, r9, r13, true); // {load}{NF}and r9, r9, r13 IID846 - __ eimulq(r18, r15, r16, false); // {load}{EVEX}imul r18, r15, r16 IID847 - __ eimulq(rcx, rcx, r17, false); // {load}imul rcx, r17 IID848 - __ eimulq(r23, r12, r20, true); // {load}{NF}imul r23, r12, r20 IID849 - __ eimulq(r10, r10, r9, true); // {load}{NF}imul r10, r10, r9 IID850 - __ eorq(rdx, r19, r14, false); // {load}{EVEX}or rdx, r19, r14 IID851 - __ eorq(rcx, rcx, r13, false); // {load}or rcx, r13 IID852 - __ eorq(r9, r25, r29, true); // {load}{NF}or r9, r25, r29 IID853 - __ eorq(rdx, rdx, r25, true); // {load}{NF}or rdx, rdx, r25 IID854 - __ esubq(r23, r8, r16, false); // {load}{EVEX}sub r23, r8, r16 IID855 - __ esubq(r13, r13, r13, false); // {load}sub r13, r13 IID856 - __ esubq(r19, r12, r15, true); // {load}{NF}sub r19, r12, r15 IID857 - __ esubq(r9, r9, rdx, true); // {load}{NF}sub r9, r9, rdx IID858 - __ exorq(r13, r16, r31, false); // {load}{EVEX}xor r13, r16, r31 IID859 - __ exorq(r17, r17, r30, false); // {load}xor r17, r30 IID860 - __ exorq(r19, r30, r20, true); // {load}{NF}xor r19, r30, r20 IID861 - __ exorq(r31, r31, r13, true); // {load}{NF}xor r31, r31, r13 IID862 - __ eshldq(r22, r10, r13, 4, false); // {EVEX}shld r22, r10, r13, 4 IID863 - __ eshldq(r24, r24, r21, 16, false); // shld r24, r21, 16 IID864 - __ eshldq(r20, r13, r27, 16, true); // {NF}shld r20, r13, r27, 16 IID865 - __ eshldq(r31, r31, r19, 2, true); // {NF}shld r31, r31, r19, 2 IID866 - __ eshrdq(r30, r20, r11, 8, false); // {EVEX}shrd r30, r20, r11, 8 IID867 - __ eshrdq(rdx, rdx, r15, 1, false); // shrd rdx, r15, 1 IID868 - __ eshrdq(r28, r30, r14, 2, true); // {NF}shrd r28, r30, r14, 2 IID869 - __ eshrdq(r20, r20, r16, 1, true); // {NF}shrd r20, r20, r16, 1 IID870 - __ ecmovq (Assembler::Condition::overflow, r21, r17, r28); // cmovo r21, r17, r28 IID871 - __ ecmovq (Assembler::Condition::overflow, r15, r15, r30); // cmovo r15, r30 IID872 - __ ecmovq (Assembler::Condition::noOverflow, rcx, r15, r15); // cmovno rcx, r15, r15 IID873 - __ ecmovq (Assembler::Condition::noOverflow, rcx, rcx, r13); // cmovno rcx, r13 IID874 - __ ecmovq (Assembler::Condition::below, rdx, r26, r26); // cmovb rdx, r26, r26 IID875 - __ ecmovq (Assembler::Condition::below, r28, r28, r15); // cmovb r28, r15 IID876 - __ ecmovq (Assembler::Condition::aboveEqual, r8, rdx, rcx); // cmovae r8, rdx, rcx IID877 - __ ecmovq (Assembler::Condition::aboveEqual, rcx, rcx, rcx); // cmovae rcx, rcx IID878 - __ ecmovq (Assembler::Condition::zero, r10, r13, r9); // cmovz r10, r13, r9 IID879 - __ ecmovq (Assembler::Condition::zero, r14, r14, r27); // cmovz r14, r27 IID880 - __ ecmovq (Assembler::Condition::notZero, r11, r23, r9); // cmovnz r11, r23, r9 IID881 - __ ecmovq (Assembler::Condition::notZero, r11, r11, rdx); // cmovnz r11, rdx IID882 - __ ecmovq (Assembler::Condition::belowEqual, r31, r14, r25); // cmovbe r31, r14, r25 IID883 - __ ecmovq (Assembler::Condition::belowEqual, r20, r20, r12); // cmovbe r20, r12 IID884 - __ ecmovq (Assembler::Condition::above, rdx, r10, r28); // cmova rdx, r10, r28 IID885 - __ ecmovq (Assembler::Condition::above, r8, r8, r17); // cmova r8, r17 IID886 - __ ecmovq (Assembler::Condition::negative, rcx, r30, r23); // cmovs rcx, r30, r23 IID887 - __ ecmovq (Assembler::Condition::negative, r26, r26, r18); // cmovs r26, r18 IID888 - __ ecmovq (Assembler::Condition::positive, rdx, rbx, r18); // cmovns rdx, rbx, r18 IID889 - __ ecmovq (Assembler::Condition::positive, r21, r21, r13); // cmovns r21, r13 IID890 - __ ecmovq (Assembler::Condition::parity, r27, r28, r27); // cmovp r27, r28, r27 IID891 - __ ecmovq (Assembler::Condition::parity, r11, r11, r30); // cmovp r11, r30 IID892 - __ ecmovq (Assembler::Condition::noParity, rcx, r21, r18); // cmovnp rcx, r21, r18 IID893 - __ ecmovq (Assembler::Condition::noParity, rcx, rcx, r29); // cmovnp rcx, r29 IID894 - __ ecmovq (Assembler::Condition::less, rdx, r21, r12); // cmovl rdx, r21, r12 IID895 - __ ecmovq (Assembler::Condition::less, rdx, rdx, r26); // cmovl rdx, r26 IID896 - __ ecmovq (Assembler::Condition::greaterEqual, r17, rbx, r22); // cmovge r17, rbx, r22 IID897 - __ ecmovq (Assembler::Condition::greaterEqual, rdx, rdx, r11); // cmovge rdx, r11 IID898 - __ ecmovq (Assembler::Condition::lessEqual, rdx, r14, r8); // cmovle rdx, r14, r8 IID899 - __ ecmovq (Assembler::Condition::lessEqual, r14, r14, r8); // cmovle r14, r8 IID900 - __ ecmovq (Assembler::Condition::greater, r25, r29, r21); // cmovg r25, r29, r21 IID901 - __ ecmovq (Assembler::Condition::greater, r26, r26, r30); // cmovg r26, r30 IID902 - __ ecmovq (Assembler::Condition::overflow, r24, r21, Address(r13, r11, (Address::ScaleFactor)1, +0x439c521e)); // cmovo r24, r21, qword ptr [r13+r11*2+0x439c521e] IID903 - __ ecmovq (Assembler::Condition::noOverflow, r11, r18, Address(r29, r16, (Address::ScaleFactor)0, +0x632127f)); // cmovno r11, r18, qword ptr [r29+r16*1+0x632127f] IID904 - __ ecmovq (Assembler::Condition::below, r16, r8, Address(r8, r26, (Address::ScaleFactor)1, +0x10633def)); // cmovb r16, r8, qword ptr [r8+r26*2+0x10633def] IID905 - __ ecmovq (Assembler::Condition::aboveEqual, r13, r14, Address(r18, -0x54f69e38)); // cmovae r13, r14, qword ptr [r18-0x54f69e38] IID906 - __ ecmovq (Assembler::Condition::zero, r12, r8, Address(r31, r26, (Address::ScaleFactor)1, -0x7a1e447a)); // cmovz r12, r8, qword ptr [r31+r26*2-0x7a1e447a] IID907 - __ ecmovq (Assembler::Condition::notZero, r29, r29, Address(r19, r11, (Address::ScaleFactor)2, -0x35d82dd2)); // cmovnz r29, qword ptr [r19+r11*4-0x35d82dd2] IID908 - __ ecmovq (Assembler::Condition::belowEqual, rcx, r18, Address(r25, r28, (Address::ScaleFactor)0, +0x30be64a0)); // cmovbe rcx, r18, qword ptr [r25+r28*1+0x30be64a0] IID909 - __ ecmovq (Assembler::Condition::above, r28, r12, Address(r10, r16, (Address::ScaleFactor)1, -0x22b8fefa)); // cmova r28, r12, qword ptr [r10+r16*2-0x22b8fefa] IID910 - __ ecmovq (Assembler::Condition::negative, r11, r8, Address(rbx, r11, (Address::ScaleFactor)3, +0x25cc9e96)); // cmovs r11, r8, qword ptr [rbx+r11*8+0x25cc9e96] IID911 - __ ecmovq (Assembler::Condition::positive, r12, r27, Address(r11, -0xc2d70fe)); // cmovns r12, r27, qword ptr [r11-0xc2d70fe] IID912 - __ ecmovq (Assembler::Condition::parity, r8, r26, Address(r19, rbx, (Address::ScaleFactor)1, -0x486db7ea)); // cmovp r8, r26, qword ptr [r19+rbx*2-0x486db7ea] IID913 - __ ecmovq (Assembler::Condition::noParity, r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884)); // cmovnp r30, r10, qword ptr [r14+r18*8+0x14884884] IID914 - __ ecmovq (Assembler::Condition::less, r27, r8, Address(r29, r14, (Address::ScaleFactor)2, +0x92b7a8)); // cmovl r27, r8, qword ptr [r29+r14*4+0x92b7a8] IID915 - __ ecmovq (Assembler::Condition::greaterEqual, r14, r28, Address(r19, rdx, (Address::ScaleFactor)0, +0x9c2d45)); // cmovge r14, r28, qword ptr [r19+rdx*1+0x9c2d45] IID916 - __ ecmovq (Assembler::Condition::lessEqual, r25, r8, Address(rcx, r18, (Address::ScaleFactor)2, +0x6655c86b)); // cmovle r25, r8, qword ptr [rcx+r18*4+0x6655c86b] IID917 - __ ecmovq (Assembler::Condition::greater, r19, r21, Address(r10, r25, (Address::ScaleFactor)0, -0x1005430b)); // cmovg r19, r21, qword ptr [r10+r25*1-0x1005430b] IID918 + __ adcq(r30, r31); // {load}adc r30, r31 IID528 + __ cmpq(r12, rdx); // {load}cmp r12, rdx IID529 + __ imulq(r21, r24); // {load}imul r21, r24 IID530 + __ popcntq(r9, r25); // {load}popcnt r9, r25 IID531 + __ sbbq(r8, r12); // {load}sbb r8, r12 IID532 + __ subq(r31, r24); // {load}sub r31, r24 IID533 + __ tzcntq(r10, r16); // {load}tzcnt r10, r16 IID534 + __ lzcntq(r20, r21); // {load}lzcnt r20, r21 IID535 + __ addq(rdx, r17); // {load}add rdx, r17 IID536 + __ andq(r14, r13); // {load}and r14, r13 IID537 + __ orq(r20, r24); // {load}or r20, r24 IID538 + __ xorq(r21, r22); // {load}xor r21, r22 IID539 + __ movq(r12, r27); // {load}mov r12, r27 IID540 + __ bsfq(r23, rdx); // {load}bsf r23, rdx IID541 + __ bsrq(r31, r28); // {load}bsr r31, r28 IID542 + __ btq(r8, r25); // {load}bt r8, r25 IID543 + __ xchgq(r21, rbx); // {load}xchg r21, rbx IID544 + __ testq(r23, r23); // {load}test r23, r23 IID545 + __ addq(Address(r19, -0x180d3ea1), r10); // add qword ptr [r19-0x180d3ea1], r10 IID546 + __ andq(Address(r11, r17, (Address::ScaleFactor)1, -0x78976be8), r25); // and qword ptr [r11+r17*2-0x78976be8], r25 IID547 + __ cmpq(Address(rbx, r28, (Address::ScaleFactor)3, +0x35f72102), r13); // cmp qword ptr [rbx+r28*8+0x35f72102], r13 IID548 + __ orq(Address(r8, -0x34465011), r21); // or qword ptr [r8-0x34465011], r21 IID549 + __ xorq(Address(r19, -0x404b22dd), r18); // xor qword ptr [r19-0x404b22dd], r18 IID550 + __ subq(Address(r23, r27, (Address::ScaleFactor)3, -0x428d2646), r14); // sub qword ptr [r23+r27*8-0x428d2646], r14 IID551 + __ movq(Address(r9, rcx, (Address::ScaleFactor)2, -0x72611661), r28); // mov qword ptr [r9+rcx*4-0x72611661], r28 IID552 + __ xaddq(Address(r24, r21, (Address::ScaleFactor)2, +0x3a6be990), rbx); // xadd qword ptr [r24+r21*4+0x3a6be990], rbx IID553 + __ andq(Address(r22, r10, (Address::ScaleFactor)0, +0x7ef8bdd), 1048576); // and qword ptr [r22+r10*1+0x7ef8bdd], 1048576 IID554 + __ addq(Address(r13, r28, (Address::ScaleFactor)0, -0x754789b1), 65536); // add qword ptr [r13+r28*1-0x754789b1], 65536 IID555 + __ cmpq(Address(r10, -0xbd2a8da), 268435456); // cmp qword ptr [r10-0xbd2a8da], 268435456 IID556 + __ sarq(Address(r23, r14, (Address::ScaleFactor)1, +0x6a16d9f5), 4); // sar qword ptr [r23+r14*2+0x6a16d9f5], 4 IID557 + __ salq(Address(rcx, r21, (Address::ScaleFactor)1, +0x5f66ac1e), 8); // sal qword ptr [rcx+r21*2+0x5f66ac1e], 8 IID558 + __ sbbq(Address(rcx, r22, (Address::ScaleFactor)3, -0x48c954c), 268435456); // sbb qword ptr [rcx+r22*8-0x48c954c], 268435456 IID559 + __ shrq(Address(r21, r30, (Address::ScaleFactor)0, +0xe405b0b), 8); // shr qword ptr [r21+r30*1+0xe405b0b], 8 IID560 + __ subq(Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b), 4096); // sub qword ptr [r19+r29*8-0x7762044b], 4096 IID561 + __ xorq(Address(r30, r10, (Address::ScaleFactor)1, -0x19798323), 16); // xor qword ptr [r30+r10*2-0x19798323], 16 IID562 + __ orq(Address(rdx, r24, (Address::ScaleFactor)3, +0x18d9b316), 4096); // or qword ptr [rdx+r24*8+0x18d9b316], 4096 IID563 + __ movq(Address(rbx, -0x3058074d), 256); // mov qword ptr [rbx-0x3058074d], 256 IID564 + __ testq(Address(r28, r21, (Address::ScaleFactor)3, +0x65a0fdc4), -268435456); // test qword ptr [r28+r21*8+0x65a0fdc4], -268435456 IID565 + __ addq(r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c)); // add r23, qword ptr [r11+r18*1-0x1d1af10c] IID566 + __ andq(r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38)); // and r22, qword ptr [r18+r12*2+0x1a5f1c38] IID567 + __ cmpq(r23, Address(r30, r19, (Address::ScaleFactor)0, -0x3e912f7f)); // cmp r23, qword ptr [r30+r19*1-0x3e912f7f] IID568 + __ lzcntq(r29, Address(rcx, +0x12e3fbe4)); // lzcnt r29, qword ptr [rcx+0x12e3fbe4] IID569 + __ orq(r14, Address(r21, r21, (Address::ScaleFactor)2, +0xd73042)); // or r14, qword ptr [r21+r21*4+0xd73042] IID570 + __ adcq(r31, Address(r17, r31, (Address::ScaleFactor)2, +0xabde912)); // adc r31, qword ptr [r17+r31*4+0xabde912] IID571 + __ imulq(r20, Address(r13, r27, (Address::ScaleFactor)0, -0x58dbfc1f)); // imul r20, qword ptr [r13+r27*1-0x58dbfc1f] IID572 + __ popcntq(rbx, Address(r22, -0x72c66c23)); // popcnt rbx, qword ptr [r22-0x72c66c23] IID573 + __ sbbq(r26, Address(r9, +0x334aba09)); // sbb r26, qword ptr [r9+0x334aba09] IID574 + __ subq(r9, Address(r9, r30, (Address::ScaleFactor)3, -0x219a6102)); // sub r9, qword ptr [r9+r30*8-0x219a6102] IID575 + __ tzcntq(r25, Address(r20, -0x2131bab1)); // tzcnt r25, qword ptr [r20-0x2131bab1] IID576 + __ xorq(r16, Address(r28, r16, (Address::ScaleFactor)1, +0x48c483b9)); // xor r16, qword ptr [r28+r16*2+0x48c483b9] IID577 + __ movq(r30, Address(r9, r16, (Address::ScaleFactor)0, -0x88ce84f)); // mov r30, qword ptr [r9+r16*1-0x88ce84f] IID578 + __ leaq(r11, Address(r30, r29, (Address::ScaleFactor)2, +0x3eeb8fd0)); // lea r11, qword ptr [r30+r29*4+0x3eeb8fd0] IID579 + __ cvttsd2siq(r26, Address(r29, r10, (Address::ScaleFactor)3, +0x3ef4822e)); // cvttsd2si r26, qword ptr [r29+r10*8+0x3ef4822e] IID580 + __ xchgq(r29, Address(r19, r20, (Address::ScaleFactor)2, -0x3f0f3db9)); // xchg r29, qword ptr [r19+r20*4-0x3f0f3db9] IID581 + __ testq(r8, Address(r30, r20, (Address::ScaleFactor)0, +0x15b56a17)); // test r8, qword ptr [r30+r20*1+0x15b56a17] IID582 + __ addq(r26, 4096); // add r26, 4096 IID583 + __ andq(r20, 16); // and r20, 16 IID584 + __ adcq(r23, 1048576); // adc r23, 1048576 IID585 + __ cmpq(r12, 4096); // cmp r12, 4096 IID586 + __ rclq(rcx, 4); // rcl rcx, 4 IID587 + __ rcrq(r14, 1); // rcr r14, 1 IID588 + __ rolq(r23, 2); // rol r23, 2 IID589 + __ rorq(r12, 4); // ror r12, 4 IID590 + __ sarq(r10, 4); // sar r10, 4 IID591 + __ salq(r20, 4); // sal r20, 4 IID592 + __ sbbq(rcx, 1048576); // sbb rcx, 1048576 IID593 + __ shlq(r23, 16); // shl r23, 16 IID594 + __ shrq(r27, 2); // shr r27, 2 IID595 + __ subq(rcx, 65536); // sub rcx, 65536 IID596 + __ xorq(r9, 1048576); // xor r9, 1048576 IID597 + __ movq(r16, 65536); // mov r16, 65536 IID598 + __ mov64(r24, 4503599627370496); // mov r24, 4503599627370496 IID599 + __ btq(r18, 64); // bt r18, 64 IID600 + __ testq(r29, -4096); // test r29, -4096 IID601 + __ orq_imm32(r30, 67108864); // or r30, 67108864 IID602 + __ subq_imm32(r25, 268435456); // sub r25, 268435456 IID603 + __ cmovq(Assembler::Condition::overflow, r30, Address(r17, r31, (Address::ScaleFactor)2, +0x47ff92f0)); // cmovo r30, qword ptr [r17+r31*4+0x47ff92f0] IID604 + __ cmovq(Assembler::Condition::noOverflow, r9, Address(r24, r28, (Address::ScaleFactor)1, +0x384904c0)); // cmovno r9, qword ptr [r24+r28*2+0x384904c0] IID605 + __ cmovq(Assembler::Condition::below, r23, Address(r23, r24, (Address::ScaleFactor)3, -0x197f1266)); // cmovb r23, qword ptr [r23+r24*8-0x197f1266] IID606 + __ cmovq(Assembler::Condition::aboveEqual, r9, Address(r29, r30, (Address::ScaleFactor)0, +0x2b5d49c8)); // cmovae r9, qword ptr [r29+r30*1+0x2b5d49c8] IID607 + __ cmovq(Assembler::Condition::zero, r16, Address(rbx, r15, (Address::ScaleFactor)1, +0x22379381)); // cmovz r16, qword ptr [rbx+r15*2+0x22379381] IID608 + __ cmovq(Assembler::Condition::notZero, r8, Address(r11, +0x49d67a0)); // cmovnz r8, qword ptr [r11+0x49d67a0] IID609 + __ cmovq(Assembler::Condition::belowEqual, r28, Address(r16, r16, (Address::ScaleFactor)2, -0x5e941da9)); // cmovbe r28, qword ptr [r16+r16*4-0x5e941da9] IID610 + __ cmovq(Assembler::Condition::above, r19, Address(r18, r8, (Address::ScaleFactor)0, -0xa5e55ec)); // cmova r19, qword ptr [r18+r8*1-0xa5e55ec] IID611 + __ cmovq(Assembler::Condition::negative, r28, Address(r17, r28, (Address::ScaleFactor)1, -0x3264220c)); // cmovs r28, qword ptr [r17+r28*2-0x3264220c] IID612 + __ cmovq(Assembler::Condition::positive, r31, Address(r14, r31, (Address::ScaleFactor)1, +0x5001bc5a)); // cmovns r31, qword ptr [r14+r31*2+0x5001bc5a] IID613 + __ cmovq(Assembler::Condition::parity, rbx, Address(r18, r17, (Address::ScaleFactor)2, -0x286f2379)); // cmovp rbx, qword ptr [r18+r17*4-0x286f2379] IID614 + __ cmovq(Assembler::Condition::noParity, r17, Address(r20, -0x5549f838)); // cmovnp r17, qword ptr [r20-0x5549f838] IID615 + __ cmovq(Assembler::Condition::less, r30, Address(r9, r28, (Address::ScaleFactor)1, -0x25b00cf3)); // cmovl r30, qword ptr [r9+r28*2-0x25b00cf3] IID616 + __ cmovq(Assembler::Condition::greaterEqual, r19, Address(r9, -0x2aabf22c)); // cmovge r19, qword ptr [r9-0x2aabf22c] IID617 + __ cmovq(Assembler::Condition::lessEqual, rbx, Address(rcx, r12, (Address::ScaleFactor)1, -0x432d68cc)); // cmovle rbx, qword ptr [rcx+r12*2-0x432d68cc] IID618 + __ cmovq(Assembler::Condition::greater, rbx, Address(r15, r17, (Address::ScaleFactor)3, -0x2b97565e)); // cmovg rbx, qword ptr [r15+r17*8-0x2b97565e] IID619 + __ call(r24); // call r24 IID620 + __ divq(r9); // div r9 IID621 + __ idivq(r28); // idiv r28 IID622 + __ imulq(rdx); // imul rdx IID623 + __ mulq(r31); // mul r31 IID624 + __ negq(r12); // neg r12 IID625 + __ notq(r12); // not r12 IID626 + __ rolq(r24); // rol r24, cl IID627 + __ rorq(r28); // ror r28, cl IID628 + __ sarq(r11); // sar r11, cl IID629 + __ salq(r27); // sal r27, cl IID630 + __ shlq(r23); // shl r23, cl IID631 + __ shrq(r17); // shr r17, cl IID632 + __ incrementq(r16); // inc r16 IID633 + __ decrementq(r12); // dec r12 IID634 + __ pushp(r23); // pushp r23 IID635 + __ popp(r24); // popp r24 IID636 + __ call(Address(r18, r14, (Address::ScaleFactor)0, -0x66639d32)); // call qword ptr [r18+r14*1-0x66639d32] IID637 + __ mulq(Address(r24, -0x660a2421)); // mul qword ptr [r24-0x660a2421] IID638 + __ negq(Address(r14, r18, (Address::ScaleFactor)0, +0x40f3936e)); // neg qword ptr [r14+r18*1+0x40f3936e] IID639 + __ sarq(Address(r10, r13, (Address::ScaleFactor)0, +0x7d04cb72)); // sar qword ptr [r10+r13*1+0x7d04cb72], cl IID640 + __ salq(Address(r18, r11, (Address::ScaleFactor)3, -0x2176b4dc)); // sal qword ptr [r18+r11*8-0x2176b4dc], cl IID641 + __ shrq(Address(r13, rcx, (Address::ScaleFactor)1, +0x7996aa80)); // shr qword ptr [r13+rcx*2+0x7996aa80], cl IID642 + __ incrementq(Address(r14, +0x67c2d02a)); // inc qword ptr [r14+0x67c2d02a] IID643 + __ decrementq(Address(r22, r26, (Address::ScaleFactor)0, +0x224f62c0)); // dec qword ptr [r22+r26*1+0x224f62c0] IID644 + __ imulq(rdx, Address(r31, rbx, (Address::ScaleFactor)1, +0x2b00bb10), 16777216); // imul rdx, qword ptr [r31+rbx*2+0x2b00bb10], 16777216 IID645 + __ imulq(r21, r31, 4096); // imul r21, r31, 4096 IID646 + __ shldq(rbx, r19, 1); // shld rbx, r19, 1 IID647 + __ shrdq(r11, r23, 4); // shrd r11, r23, 4 IID648 + __ pop2(r16, r30); // {load}pop2 r30, r16 IID649 + __ pop2p(r17, rbx); // {load}pop2p rbx, r17 IID650 + __ push2(r20, r30); // {load}push2 r30, r20 IID651 + __ push2p(r8, r31); // {load}push2p r31, r8 IID652 + __ movzbq(r28, Address(r8, r14, (Address::ScaleFactor)0, +0x469ae67a)); // movzx r28, byte ptr [r8+r14*1+0x469ae67a] IID653 + __ movzwq(r14, Address(r8, r18, (Address::ScaleFactor)2, -0x48699e02)); // movzx r14, word ptr [r8+r18*4-0x48699e02] IID654 + __ movsbq(r21, Address(rbx, -0x64dae06b)); // movsx r21, byte ptr [rbx-0x64dae06b] IID655 + __ movswq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x60318819)); // movsx r19, word ptr [r31+rbx*4+0x60318819] IID656 + __ movzbq(r30, r13); // movzx r30, r13b IID657 + __ movzwq(r30, r18); // movzx r30, r18w IID658 + __ movsbq(r19, r15); // movsx r19, r15b IID659 + __ movswq(r20, r16); // movsx r20, r16w IID660 + __ cmpxchgq(r28, Address(r11, rbx, (Address::ScaleFactor)3, +0xfc3479d)); // cmpxchg qword ptr [r11+rbx*8+0xfc3479d], r28 IID661 + __ eidivq(r20, false); // {EVEX}idiv r20 IID662 + __ eidivq(r30, true); // {NF}idiv r30 IID663 + __ edivq(r22, false); // {EVEX}div r22 IID664 + __ edivq(r11, true); // {NF}div r11 IID665 + __ eimulq(rcx, false); // {EVEX}imul rcx IID666 + __ eimulq(r28, true); // {NF}imul r28 IID667 + __ emulq(r21, false); // {EVEX}mul r21 IID668 + __ emulq(r13, true); // {NF}mul r13 IID669 + __ emulq(Address(r26, r15, (Address::ScaleFactor)2, +0x70a1ce6e), false); // {EVEX}mul qword ptr [r26+r15*4+0x70a1ce6e] IID670 + __ emulq(Address(r24, r19, (Address::ScaleFactor)1, -0x1670855c), true); // {NF}mul qword ptr [r24+r19*2-0x1670855c] IID671 + __ eimulq(r10, r27, false); // {EVEX}imul r10, r27 IID672 + __ eimulq(r17, r17, false); // imul r17 IID673 + __ eimulq(rdx, r22, true); // {NF}imul rdx, r22 IID674 + __ eimulq(rbx, rbx, true); // {NF}imul rbx, rbx IID675 + __ elzcntq(r28, r15, false); // {EVEX}lzcnt r28, r15 IID676 + __ elzcntq(r15, r15, false); // {EVEX}lzcnt r15, r15 IID677 + __ elzcntq(rbx, r12, true); // {NF}lzcnt rbx, r12 IID678 + __ elzcntq(rbx, rbx, true); // {NF}lzcnt rbx, rbx IID679 + __ enegq(r26, r11, false); // {EVEX}neg r26, r11 IID680 + __ enegq(r17, r17, false); // neg r17 IID681 + __ enegq(rdx, r31, true); // {NF}neg rdx, r31 IID682 + __ enegq(r27, r27, true); // {NF}neg r27, r27 IID683 + __ enotq(r31, r15); // {EVEX}not r31, r15 IID684 + __ enotq(r21, r21); // not r21 IID685 + __ epopcntq(rbx, r24, false); // {EVEX}popcnt rbx, r24 IID686 + __ epopcntq(r28, r28, false); // {EVEX}popcnt r28, r28 IID687 + __ epopcntq(r23, r27, true); // {NF}popcnt r23, r27 IID688 + __ epopcntq(r13, r13, true); // {NF}popcnt r13, r13 IID689 + __ erolq(r25, r28, false); // {EVEX}rol r25, r28, cl IID690 + __ erolq(r31, r31, false); // rol r31, cl IID691 + __ erolq(r25, r23, true); // {NF}rol r25, r23, cl IID692 + __ erolq(rcx, rcx, true); // {NF}rol rcx, rcx, cl IID693 + __ erorq(r22, r14, false); // {EVEX}ror r22, r14, cl IID694 + __ erorq(r15, r15, false); // ror r15, cl IID695 + __ erorq(r11, r30, true); // {NF}ror r11, r30, cl IID696 + __ erorq(r24, r24, true); // {NF}ror r24, r24, cl IID697 + __ esalq(r10, r20, false); // {EVEX}sal r10, r20, cl IID698 + __ esalq(r19, r19, false); // sal r19, cl IID699 + __ esalq(r17, r25, true); // {NF}sal r17, r25, cl IID700 + __ esalq(r13, r13, true); // {NF}sal r13, r13, cl IID701 + __ esarq(r31, r30, false); // {EVEX}sar r31, r30, cl IID702 + __ esarq(r18, r18, false); // sar r18, cl IID703 + __ esarq(r25, r25, true); // {NF}sar r25, r25, cl IID704 + __ esarq(r28, r28, true); // {NF}sar r28, r28, cl IID705 + __ edecq(r22, r27, false); // {EVEX}dec r22, r27 IID706 + __ edecq(r12, r12, false); // dec r12 IID707 + __ edecq(r18, r11, true); // {NF}dec r18, r11 IID708 + __ edecq(r10, r10, true); // {NF}dec r10, r10 IID709 + __ eincq(r20, r24, false); // {EVEX}inc r20, r24 IID710 + __ eincq(r18, r18, false); // inc r18 IID711 + __ eincq(rbx, r11, true); // {NF}inc rbx, r11 IID712 + __ eincq(r26, r26, true); // {NF}inc r26, r26 IID713 + __ eshlq(r21, r8, false); // {EVEX}shl r21, r8, cl IID714 + __ eshlq(rbx, rbx, false); // shl rbx, cl IID715 + __ eshlq(r22, r21, true); // {NF}shl r22, r21, cl IID716 + __ eshlq(r27, r27, true); // {NF}shl r27, r27, cl IID717 + __ eshrq(r12, r16, false); // {EVEX}shr r12, r16, cl IID718 + __ eshrq(r8, r8, false); // shr r8, cl IID719 + __ eshrq(rdx, r9, true); // {NF}shr rdx, r9, cl IID720 + __ eshrq(r20, r20, true); // {NF}shr r20, r20, cl IID721 + __ etzcntq(r31, r21, false); // {EVEX}tzcnt r31, r21 IID722 + __ etzcntq(r20, r20, false); // {EVEX}tzcnt r20, r20 IID723 + __ etzcntq(rcx, r16, true); // {NF}tzcnt rcx, r16 IID724 + __ etzcntq(r14, r14, true); // {NF}tzcnt r14, r14 IID725 + __ eimulq(r27, Address(r25, r9, (Address::ScaleFactor)1, +0x445a2393), false); // {EVEX}imul r27, qword ptr [r25+r9*2+0x445a2393] IID726 + __ eimulq(r23, Address(rcx, r9, (Address::ScaleFactor)1, -0x1480ef0c), true); // {NF}imul r23, qword ptr [rcx+r9*2-0x1480ef0c] IID727 + __ elzcntq(r13, Address(r22, r17, (Address::ScaleFactor)1, -0x750c1996), false); // {EVEX}lzcnt r13, qword ptr [r22+r17*2-0x750c1996] IID728 + __ elzcntq(r13, Address(r31, -0x342b6259), true); // {NF}lzcnt r13, qword ptr [r31-0x342b6259] IID729 + __ enegq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), false); // {EVEX}neg r31, qword ptr [r24+r13*2-0x25b16a0e] IID730 + __ enegq(r13, Address(r11, r28, (Address::ScaleFactor)3, +0x5c0013ab), true); // {NF}neg r13, qword ptr [r11+r28*8+0x5c0013ab] IID731 + __ epopcntq(rdx, Address(r18, rcx, (Address::ScaleFactor)2, -0x6113eaaf), false); // {EVEX}popcnt rdx, qword ptr [r18+rcx*4-0x6113eaaf] IID732 + __ epopcntq(r9, Address(r10, -0x5ca7d588), true); // {NF}popcnt r9, qword ptr [r10-0x5ca7d588] IID733 + __ esalq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), false); // {EVEX}sal r17, qword ptr [r27+r30*1+0x1b4cda2c], cl IID734 + __ esalq(r25, Address(r12, rdx, (Address::ScaleFactor)1, +0x62823bce), true); // {NF}sal r25, qword ptr [r12+rdx*2+0x62823bce], cl IID735 + __ esarq(r9, Address(r10, r18, (Address::ScaleFactor)2, -0x264a7a48), false); // {EVEX}sar r9, qword ptr [r10+r18*4-0x264a7a48], cl IID736 + __ esarq(rbx, Address(r14, r27, (Address::ScaleFactor)0, +0x20291e00), true); // {NF}sar rbx, qword ptr [r14+r27*1+0x20291e00], cl IID737 + __ edecq(r12, Address(r15, r14, (Address::ScaleFactor)2, -0x20f7dabb), false); // {EVEX}dec r12, qword ptr [r15+r14*4-0x20f7dabb] IID738 + __ edecq(r9, Address(r10, r25, (Address::ScaleFactor)1, +0x21411d84), true); // {NF}dec r9, qword ptr [r10+r25*2+0x21411d84] IID739 + __ eincq(r20, Address(rbx, r25, (Address::ScaleFactor)3, +0x2f0329e), false); // {EVEX}inc r20, qword ptr [rbx+r25*8+0x2f0329e] IID740 + __ eincq(r10, Address(r12, r31, (Address::ScaleFactor)0, -0x37505c8c), true); // {NF}inc r10, qword ptr [r12+r31*1-0x37505c8c] IID741 + __ eshrq(r24, Address(r23, r14, (Address::ScaleFactor)3, -0x71e75ab0), false); // {EVEX}shr r24, qword ptr [r23+r14*8-0x71e75ab0], cl IID742 + __ eshrq(r25, Address(r19, r10, (Address::ScaleFactor)1, +0x507b0a88), true); // {NF}shr r25, qword ptr [r19+r10*2+0x507b0a88], cl IID743 + __ etzcntq(r31, Address(rbx, r16, (Address::ScaleFactor)0, +0x19d5192a), false); // {EVEX}tzcnt r31, qword ptr [rbx+r16*1+0x19d5192a] IID744 + __ etzcntq(r9, Address(r22, r28, (Address::ScaleFactor)2, +0x211007cd), true); // {NF}tzcnt r9, qword ptr [r22+r28*4+0x211007cd] IID745 + __ eaddq(r16, Address(r21, rbx, (Address::ScaleFactor)3, -0x823fa1e), r28, false); // {EVEX}add r16, qword ptr [r21+rbx*8-0x823fa1e], r28 IID746 + __ eaddq(r15, Address(rdx, r8, (Address::ScaleFactor)3, -0x34b9a058), r15, false); // add r15, qword ptr [rdx+r8*8-0x34b9a058] IID747 + __ eaddq(r24, Address(r14, r24, (Address::ScaleFactor)3, +0x6cdc59d2), r13, true); // {NF}add r24, qword ptr [r14+r24*8+0x6cdc59d2], r13 IID748 + __ eaddq(rbx, Address(r27, r14, (Address::ScaleFactor)3, +0x36c5e8de), rbx, true); // {NF}add rbx, qword ptr [r27+r14*8+0x36c5e8de], rbx IID749 + __ eandq(r21, Address(r27, r27, (Address::ScaleFactor)1, -0x2c023b13), r27, false); // {EVEX}and r21, qword ptr [r27+r27*2-0x2c023b13], r27 IID750 + __ eandq(r31, Address(r21, r15, (Address::ScaleFactor)2, +0x6ef2c74a), r31, false); // and r31, qword ptr [r21+r15*4+0x6ef2c74a] IID751 + __ eandq(r13, Address(r31, r25, (Address::ScaleFactor)1, +0x734fe9ab), r27, true); // {NF}and r13, qword ptr [r31+r25*2+0x734fe9ab], r27 IID752 + __ eandq(r15, Address(r14, r29, (Address::ScaleFactor)3, -0x6e68556), r15, true); // {NF}and r15, qword ptr [r14+r29*8-0x6e68556], r15 IID753 + __ eorq(r12, Address(r30, r15, (Address::ScaleFactor)3, +0x3ba33f9e), r28, false); // {EVEX}or r12, qword ptr [r30+r15*8+0x3ba33f9e], r28 IID754 + __ eorq(r16, Address(r12, r9, (Address::ScaleFactor)0, -0x28e03b33), r16, false); // or r16, qword ptr [r12+r9*1-0x28e03b33] IID755 + __ eorq(r8, Address(r8, r25, (Address::ScaleFactor)3, -0x1e42bd95), r27, true); // {NF}or r8, qword ptr [r8+r25*8-0x1e42bd95], r27 IID756 + __ eorq(rcx, Address(r27, rbx, (Address::ScaleFactor)2, +0x7be4bcad), rcx, true); // {NF}or rcx, qword ptr [r27+rbx*4+0x7be4bcad], rcx IID757 + __ esubq(r24, Address(r23, r22, (Address::ScaleFactor)2, +0x6f8827d7), rdx, false); // {EVEX}sub r24, qword ptr [r23+r22*4+0x6f8827d7], rdx IID758 + __ esubq(r21, Address(r10, -0x635b8c8), r21, false); // {EVEX}sub r21, qword ptr [r10-0x635b8c8], r21 IID759 + __ esubq(r23, Address(r27, r26, (Address::ScaleFactor)3, +0x922bcc0), rbx, true); // {NF}sub r23, qword ptr [r27+r26*8+0x922bcc0], rbx IID760 + __ esubq(r25, Address(r23, r15, (Address::ScaleFactor)0, -0x38f494ac), r25, true); // {NF}sub r25, qword ptr [r23+r15*1-0x38f494ac], r25 IID761 + __ exorq(r11, Address(r12, r19, (Address::ScaleFactor)2, -0x5b71ec17), rcx, false); // {EVEX}xor r11, qword ptr [r12+r19*4-0x5b71ec17], rcx IID762 + __ exorq(r28, Address(r19, r18, (Address::ScaleFactor)0, +0x716b9b7e), r28, false); // xor r28, qword ptr [r19+r18*1+0x716b9b7e] IID763 + __ exorq(r21, Address(rcx, r29, (Address::ScaleFactor)0, -0x5af0441e), r16, true); // {NF}xor r21, qword ptr [rcx+r29*1-0x5af0441e], r16 IID764 + __ exorq(r12, Address(r20, r26, (Address::ScaleFactor)0, +0xe0b7fb1), r12, true); // {NF}xor r12, qword ptr [r20+r26*1+0xe0b7fb1], r12 IID765 + __ eaddq(r30, Address(rcx, +0x2d3b7b4f), 1048576, false); // {EVEX}add r30, qword ptr [rcx+0x2d3b7b4f], 1048576 IID766 + __ eaddq(r14, Address(r21, r15, (Address::ScaleFactor)2, -0x1222aee8), 4096, true); // {NF}add r14, qword ptr [r21+r15*4-0x1222aee8], 4096 IID767 + __ eandq(r23, Address(r20, r31, (Address::ScaleFactor)0, -0x96e4d6a), 16, false); // {EVEX}and r23, qword ptr [r20+r31*1-0x96e4d6a], 16 IID768 + __ eandq(r10, Address(rdx, rdx, (Address::ScaleFactor)3, +0x3875f17c), 1, true); // {NF}and r10, qword ptr [rdx+rdx*8+0x3875f17c], 1 IID769 + __ eimulq(r17, Address(rcx, r25, (Address::ScaleFactor)2, +0x32c71076), 4096, false); // {EVEX}imul r17, qword ptr [rcx+r25*4+0x32c71076], 4096 IID770 + __ eimulq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x7bada60d), 1048576, true); // {NF}imul r19, qword ptr [r31+rbx*4+0x7bada60d], 1048576 IID771 + __ eorq(r25, Address(r18, r23, (Address::ScaleFactor)1, +0x48147444), 16777216, false); // {EVEX}or r25, qword ptr [r18+r23*2+0x48147444], 16777216 IID772 + __ eorq(r29, Address(r26, r27, (Address::ScaleFactor)1, -0x4b113958), 1048576, true); // {NF}or r29, qword ptr [r26+r27*2-0x4b113958], 1048576 IID773 + __ esalq(r31, Address(r18, -0x46103c74), 2, false); // {EVEX}sal r31, qword ptr [r18-0x46103c74], 2 IID774 + __ esalq(r25, Address(r10, r15, (Address::ScaleFactor)0, +0x48925da4), 16, true); // {NF}sal r25, qword ptr [r10+r15*1+0x48925da4], 16 IID775 + __ esarq(r26, Address(r18, -0x5ea1c542), 8, false); // {EVEX}sar r26, qword ptr [r18-0x5ea1c542], 8 IID776 + __ esarq(r12, Address(r10, r22, (Address::ScaleFactor)2, +0x5d958264), 8, true); // {NF}sar r12, qword ptr [r10+r22*4+0x5d958264], 8 IID777 + __ eshrq(rdx, Address(r17, r20, (Address::ScaleFactor)2, +0x295add23), 16, false); // {EVEX}shr rdx, qword ptr [r17+r20*4+0x295add23], 16 IID778 + __ eshrq(rbx, Address(r22, r28, (Address::ScaleFactor)1, +0x782929cb), 2, true); // {NF}shr rbx, qword ptr [r22+r28*2+0x782929cb], 2 IID779 + __ esubq(r19, Address(r23, -0x49811d72), 1, false); // {EVEX}sub r19, qword ptr [r23-0x49811d72], 1 IID780 + __ esubq(r8, Address(r19, r14, (Address::ScaleFactor)2, -0x1b2bae9a), 1048576, true); // {NF}sub r8, qword ptr [r19+r14*4-0x1b2bae9a], 1048576 IID781 + __ exorq(r19, Address(rcx, r10, (Address::ScaleFactor)0, +0x45a66ee9), 1048576, false); // {EVEX}xor r19, qword ptr [rcx+r10*1+0x45a66ee9], 1048576 IID782 + __ exorq(r28, Address(r9, r29, (Address::ScaleFactor)0, -0x28a19314), 16, true); // {NF}xor r28, qword ptr [r9+r29*1-0x28a19314], 16 IID783 + __ eaddq(r8, rcx, 16777216, false); // {EVEX}add r8, rcx, 16777216 IID784 + __ eaddq(rax, r14, 16777216, false); // {EVEX}add rax, r14, 16777216 IID785 + __ eaddq(r16, r16, 256, false); // add r16, 256 IID786 + __ eaddq(r24, r9, 4096, true); // {NF}add r24, r9, 4096 IID787 + __ eaddq(rax, r18, 4096, true); // {NF}add rax, r18, 4096 IID788 + __ eaddq(r8, r8, 1, true); // {NF}add r8, r8, 1 IID789 + __ eandq(r15, r22, 1048576, false); // {EVEX}and r15, r22, 1048576 IID790 + __ eandq(rax, r26, 1048576, false); // {EVEX}and rax, r26, 1048576 IID791 + __ eandq(rdx, rdx, 4096, false); // and rdx, 4096 IID792 + __ eandq(rdx, r22, 268435456, true); // {NF}and rdx, r22, 268435456 IID793 + __ eandq(rax, r29, 268435456, true); // {NF}and rax, r29, 268435456 IID794 + __ eandq(r23, r23, 16777216, true); // {NF}and r23, r23, 16777216 IID795 + __ eimulq(r9, r13, 1048576, false); // {EVEX}imul r9, r13, 1048576 IID796 + __ eimulq(rax, r18, 1048576, false); // {EVEX}imul rax, r18, 1048576 IID797 + __ eimulq(r16, r16, 1048576, false); // {EVEX}imul r16, r16, 1048576 IID798 + __ eimulq(r17, r23, 1, true); // {NF}imul r17, r23, 1 IID799 + __ eimulq(rax, r12, 1, true); // {NF}imul rax, r12, 1 IID800 + __ eimulq(r10, r10, 268435456, true); // {NF}imul r10, r10, 268435456 IID801 + __ eorq(rdx, r19, 256, false); // {EVEX}or rdx, r19, 256 IID802 + __ eorq(rax, r14, 256, false); // {EVEX}or rax, r14, 256 IID803 + __ eorq(r13, r13, 1, false); // or r13, 1 IID804 + __ eorq(r25, r29, 256, true); // {NF}or r25, r29, 256 IID805 + __ eorq(rax, rdx, 256, true); // {NF}or rax, rdx, 256 IID806 + __ eorq(r16, r16, 16, true); // {NF}or r16, r16, 16 IID807 + __ erclq(r13, r19, 4); // {EVEX}rcl r13, r19, 4 IID808 + __ erclq(rax, r12, 4); // {EVEX}rcl rax, r12, 4 IID809 + __ erclq(r9, r9, 4); // rcl r9, 4 IID810 + __ erolq(r13, r16, 1, false); // {EVEX}rol r13, r16, 1 IID811 + __ erolq(rax, r31, 1, false); // {EVEX}rol rax, r31, 1 IID812 + __ erolq(r30, r30, 8, false); // rol r30, 8 IID813 + __ erolq(r30, r20, 8, true); // {NF}rol r30, r20, 8 IID814 + __ erolq(rax, r31, 8, true); // {NF}rol rax, r31, 8 IID815 + __ erolq(r31, r31, 4, true); // {NF}rol r31, r31, 4 IID816 + __ erorq(r22, r10, 4, false); // {EVEX}ror r22, r10, 4 IID817 + __ erorq(rax, r13, 4, false); // {EVEX}ror rax, r13, 4 IID818 + __ erorq(r24, r24, 16, false); // ror r24, 16 IID819 + __ erorq(r29, r22, 16, true); // {NF}ror r29, r22, 16 IID820 + __ erorq(rax, r20, 16, true); // {NF}ror rax, r20, 16 IID821 + __ erorq(r27, r27, 4, true); // {NF}ror r27, r27, 4 IID822 + __ esalq(r31, r19, 2, false); // {EVEX}sal r31, r19, 2 IID823 + __ esalq(rax, r20, 2, false); // {EVEX}sal rax, r20, 2 IID824 + __ esalq(r11, r11, 8, false); // sal r11, 8 IID825 + __ esalq(rdx, r15, 1, true); // {NF}sal rdx, r15, 1 IID826 + __ esalq(rax, r10, 1, true); // {NF}sal rax, r10, 1 IID827 + __ esalq(r29, r29, 4, true); // {NF}sal r29, r29, 4 IID828 + __ esarq(r20, r16, 1, false); // {EVEX}sar r20, r16, 1 IID829 + __ esarq(rax, r21, 1, false); // {EVEX}sar rax, r21, 1 IID830 + __ esarq(r28, r28, 8, false); // sar r28, 8 IID831 + __ esarq(r30, rcx, 4, true); // {NF}sar r30, rcx, 4 IID832 + __ esarq(rax, r15, 4, true); // {NF}sar rax, r15, 4 IID833 + __ esarq(rcx, rcx, 4, true); // {NF}sar rcx, rcx, 4 IID834 + __ eshlq(rdx, r26, 4, false); // {EVEX}shl rdx, r26, 4 IID835 + __ eshlq(rax, r26, 4, false); // {EVEX}shl rax, r26, 4 IID836 + __ eshlq(r8, r8, 4, false); // shl r8, 4 IID837 + __ eshlq(rcx, rcx, 1, true); // {NF}shl rcx, rcx, 1 IID838 + __ eshlq(rax, rcx, 1, true); // {NF}shl rax, rcx, 1 IID839 + __ eshlq(r13, r13, 2, true); // {NF}shl r13, r13, 2 IID840 + __ eshrq(r14, r27, 2, false); // {EVEX}shr r14, r27, 2 IID841 + __ eshrq(rax, r11, 2, false); // {EVEX}shr rax, r11, 2 IID842 + __ eshrq(r9, r9, 16, false); // shr r9, 16 IID843 + __ eshrq(rdx, r31, 2, true); // {NF}shr rdx, r31, 2 IID844 + __ eshrq(rax, r14, 2, true); // {NF}shr rax, r14, 2 IID845 + __ eshrq(r12, r12, 8, true); // {NF}shr r12, r12, 8 IID846 + __ esubq(r10, r28, 1, false); // {EVEX}sub r10, r28, 1 IID847 + __ esubq(rax, r8, 1, false); // {EVEX}sub rax, r8, 1 IID848 + __ esubq(rcx, rcx, 16777216, false); // sub rcx, 16777216 IID849 + __ esubq(rdx, rbx, 16777216, true); // {NF}sub rdx, rbx, 16777216 IID850 + __ esubq(rax, r18, 16777216, true); // {NF}sub rax, r18, 16777216 IID851 + __ esubq(r27, r27, 65536, true); // {NF}sub r27, r27, 65536 IID852 + __ exorq(r30, rcx, 4096, false); // {EVEX}xor r30, rcx, 4096 IID853 + __ exorq(rax, r21, 4096, false); // {EVEX}xor rax, r21, 4096 IID854 + __ exorq(rcx, rcx, 16777216, false); // xor rcx, 16777216 IID855 + __ exorq(r21, r12, 1, true); // {NF}xor r21, r12, 1 IID856 + __ exorq(rax, rdx, 1, true); // {NF}xor rax, rdx, 1 IID857 + __ exorq(rbx, rbx, 16777216, true); // {NF}xor rbx, rbx, 16777216 IID858 + __ eorq_imm32(r11, rdx, 65536, false); // {EVEX}or r11, rdx, 65536 IID859 + __ eorq_imm32(rax, r14, 65536, false); // {EVEX}or rax, r14, 65536 IID860 + __ eorq_imm32(r14, r14, 262144, false); // or r14, 262144 IID861 + __ eorq_imm32(r25, r29, 262144, false); // {EVEX}or r25, r29, 262144 IID862 + __ eorq_imm32(rax, r21, 262144, false); // {EVEX}or rax, r21, 262144 IID863 + __ eorq_imm32(r11, r11, 16777216, false); // or r11, 16777216 IID864 + __ esubq_imm32(r29, r19, 67108864, false); // {EVEX}sub r29, r19, 67108864 IID865 + __ esubq_imm32(rax, r11, 67108864, false); // {EVEX}sub rax, r11, 67108864 IID866 + __ esubq_imm32(r18, r18, 67108864, false); // sub r18, 67108864 IID867 + __ esubq_imm32(r28, r23, 4194304, true); // {NF}sub r28, r23, 4194304 IID868 + __ esubq_imm32(rax, r21, 4194304, true); // {NF}sub rax, r21, 4194304 IID869 + __ esubq_imm32(r16, r16, 16777216, true); // {NF}sub r16, r16, 16777216 IID870 + __ eaddq(r8, r25, Address(r26, r8, (Address::ScaleFactor)1, +0x10633def), false); // {EVEX}add r8, r25, qword ptr [r26+r8*2+0x10633def] IID871 + __ eaddq(r13, r13, Address(r18, r16, (Address::ScaleFactor)1, -0x74204508), false); // add r13, qword ptr [r18+r16*2-0x74204508] IID872 + __ eaddq(r17, r26, Address(r12, +0x23a80abf), true); // {NF}add r17, r26, qword ptr [r12+0x23a80abf] IID873 + __ eaddq(r9, r9, Address(r29, r19, (Address::ScaleFactor)0, -0x29e9e52), true); // {NF}add r9, r9, qword ptr [r29+r19*1-0x29e9e52] IID874 + __ eandq(r9, r28, Address(rcx, r25, (Address::ScaleFactor)2, +0x4261ffaa), false); // {EVEX}and r9, r28, qword ptr [rcx+r25*4+0x4261ffaa] IID875 + __ eandq(r27, r27, Address(rdx, r28, (Address::ScaleFactor)0, -0x26bdc9c1), false); // and r27, qword ptr [rdx+r28*1-0x26bdc9c1] IID876 + __ eandq(r14, r11, Address(r16, +0x63ba0ddf), true); // {NF}and r14, r11, qword ptr [r16+0x63ba0ddf] IID877 + __ eandq(r8, r8, Address(r22, r25, (Address::ScaleFactor)1, -0x43b6ab44), true); // {NF}and r8, r8, qword ptr [r22+r25*2-0x43b6ab44] IID878 + __ eorq(r19, rcx, Address(r27, rcx, (Address::ScaleFactor)2, -0x7f687fc6), false); // {EVEX}or r19, rcx, qword ptr [r27+rcx*4-0x7f687fc6] IID879 + __ eorq(r19, r19, Address(rbx, r26, (Address::ScaleFactor)1, -0x486db7ea), false); // or r19, qword ptr [rbx+r26*2-0x486db7ea] IID880 + __ eorq(r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884), true); // {NF}or r30, r10, qword ptr [r14+r18*8+0x14884884] IID881 + __ eorq(r27, r27, Address(r29, +0x20337180), true); // {NF}or r27, r27, qword ptr [r29+0x20337180] IID882 + __ eimulq(rcx, r21, Address(r21, rbx, (Address::ScaleFactor)0, -0x3303888e), false); // {EVEX}imul rcx, r21, qword ptr [r21+rbx*1-0x3303888e] IID883 + __ eimulq(rdx, rdx, Address(r28, r9, (Address::ScaleFactor)3, -0x7ad8f741), false); // imul rdx, qword ptr [r28+r9*8-0x7ad8f741] IID884 + __ eimulq(r8, r29, Address(r17, r12, (Address::ScaleFactor)0, +0x6e85396a), true); // {NF}imul r8, r29, qword ptr [r17+r12*1+0x6e85396a] IID885 + __ eimulq(r16, r16, Address(r19, r10, (Address::ScaleFactor)3, -0x49599300), true); // {NF}imul r16, r16, qword ptr [r19+r10*8-0x49599300] IID886 + __ esubq(r20, r17, Address(r13, r22, (Address::ScaleFactor)0, +0x1d219a4f), false); // {EVEX}sub r20, r17, qword ptr [r13+r22*1+0x1d219a4f] IID887 + __ esubq(r25, r25, Address(r21, r21, (Address::ScaleFactor)3, -0x6868a8c7), false); // sub r25, qword ptr [r21+r21*8-0x6868a8c7] IID888 + __ esubq(r20, r24, Address(rbx, r20, (Address::ScaleFactor)2, +0x32c59da6), true); // {NF}sub r20, r24, qword ptr [rbx+r20*4+0x32c59da6] IID889 + __ esubq(r8, r8, Address(r12, r17, (Address::ScaleFactor)0, -0x26be2dcf), true); // {NF}sub r8, r8, qword ptr [r12+r17*1-0x26be2dcf] IID890 + __ exorq(rdx, r19, Address(r9, +0x7d903b91), false); // {EVEX}xor rdx, r19, qword ptr [r9+0x7d903b91] IID891 + __ exorq(r28, r28, Address(r29, r27, (Address::ScaleFactor)2, +0x53091f6f), false); // xor r28, qword ptr [r29+r27*4+0x53091f6f] IID892 + __ exorq(r17, r16, Address(r27, +0x7c6e9207), true); // {NF}xor r17, r16, qword ptr [r27+0x7c6e9207] IID893 + __ exorq(r15, r15, Address(r13, r24, (Address::ScaleFactor)3, -0x75c87960), true); // {NF}xor r15, r15, qword ptr [r13+r24*8-0x75c87960] IID894 + __ eaddq(r16, rbx, r18, false); // {load}{EVEX}add r16, rbx, r18 IID895 + __ eaddq(r24, r24, r18, false); // {load}add r24, r18 IID896 + __ eaddq(r9, r15, r9, false); // {load}add r9, r15 IID897 + __ eaddq(r19, r26, r13, true); // {load}{NF}add r19, r26, r13 IID898 + __ eaddq(r28, r28, r22, true); // {load}{NF}add r28, r28, r22 IID899 + __ eaddq(r22, r11, r22, true); // {load}{NF}add r22, r11, r22 IID900 + __ eadcxq(rcx, r12, r13); // {load}{EVEX}adcx rcx, r12, r13 IID901 + __ eadcxq(r30, r30, r12); // {load}adcx r30, r12 IID902 + __ eadoxq(r28, r14, r18); // {load}{EVEX}adox r28, r14, r18 IID903 + __ eadoxq(r30, r30, r19); // {load}adox r30, r19 IID904 + __ eandq(r20, r14, r14, false); // {load}{EVEX}and r20, r14, r14 IID905 + __ eandq(r17, r17, r23, false); // {load}and r17, r23 IID906 + __ eandq(r17, r14, r17, false); // {load}and r17, r14 IID907 + __ eandq(r19, r20, r15, true); // {load}{NF}and r19, r20, r15 IID908 + __ eandq(rbx, rbx, r13, true); // {load}{NF}and rbx, rbx, r13 IID909 + __ eandq(r22, r30, r22, true); // {load}{NF}and r22, r30, r22 IID910 + __ eimulq(r17, r24, rcx, false); // {load}{EVEX}imul r17, r24, rcx IID911 + __ eimulq(r21, r21, r8, false); // {load}imul r21, r8 IID912 + __ eimulq(r29, r21, r29, false); // {load}imul r29, r21 IID913 + __ eimulq(r27, r13, r23, true); // {load}{NF}imul r27, r13, r23 IID914 + __ eimulq(r26, r26, r8, true); // {load}{NF}imul r26, r26, r8 IID915 + __ eimulq(r22, r13, r22, true); // {load}{NF}imul r22, r13, r22 IID916 + __ eorq(r11, rdx, r29, false); // {load}{EVEX}or r11, rdx, r29 IID917 + __ eorq(rdx, rdx, r31, false); // {load}or rdx, r31 IID918 + __ eorq(r10, r29, r10, false); // {load}or r10, r29 IID919 + __ eorq(r27, r28, rcx, true); // {load}{NF}or r27, r28, rcx IID920 + __ eorq(r25, r25, r9, true); // {load}{NF}or r25, r25, r9 IID921 + __ eorq(rcx, r8, rcx, true); // {load}{NF}or rcx, r8, rcx IID922 + __ esubq(rcx, r10, r16, false); // {load}{EVEX}sub rcx, r10, r16 IID923 + __ esubq(r17, r17, rcx, false); // {load}sub r17, rcx IID924 + __ esubq(r13, r21, r24, true); // {load}{NF}sub r13, r21, r24 IID925 + __ esubq(r31, r31, r28, true); // {load}{NF}sub r31, r31, r28 IID926 + __ exorq(r23, r28, r23, false); // {load}xor r23, r28 IID927 + __ exorq(r10, r10, r11, false); // {load}xor r10, r11 IID928 + __ exorq(r19, r18, r19, false); // {load}xor r19, r18 IID929 + __ exorq(r31, r9, rdx, true); // {load}{NF}xor r31, r9, rdx IID930 + __ exorq(r13, r13, r9, true); // {load}{NF}xor r13, r13, r9 IID931 + __ exorq(rcx, r10, rcx, true); // {load}{NF}xor rcx, r10, rcx IID932 + __ eshldq(r12, r24, r22, 8, false); // {EVEX}shld r12, r24, r22, 8 IID933 + __ eshldq(r25, r25, r25, 8, false); // shld r25, r25, 8 IID934 + __ eshldq(r21, r20, r15, 8, true); // {NF}shld r21, r20, r15, 8 IID935 + __ eshldq(r21, r21, r10, 8, true); // {NF}shld r21, r21, r10, 8 IID936 + __ eshrdq(r18, r18, r8, 2, false); // shrd r18, r8, 2 IID937 + __ eshrdq(r26, r26, r29, 8, false); // shrd r26, r29, 8 IID938 + __ eshrdq(r29, r26, r19, 2, true); // {NF}shrd r29, r26, r19, 2 IID939 + __ eshrdq(r12, r12, rcx, 4, true); // {NF}shrd r12, r12, rcx, 4 IID940 + __ ecmovq (Assembler::Condition::overflow, r21, r22, r23); // cmovo r21, r22, r23 IID941 + __ ecmovq (Assembler::Condition::overflow, r9, r9, r13); // cmovo r9, r13 IID942 + __ ecmovq (Assembler::Condition::noOverflow, rcx, r23, r24); // cmovno rcx, r23, r24 IID943 + __ ecmovq (Assembler::Condition::noOverflow, r28, r28, rdx); // cmovno r28, rdx IID944 + __ ecmovq (Assembler::Condition::below, r14, r31, r23); // cmovb r14, r31, r23 IID945 + __ ecmovq (Assembler::Condition::below, r30, r30, r23); // cmovb r30, r23 IID946 + __ ecmovq (Assembler::Condition::aboveEqual, r10, r29, r22); // cmovae r10, r29, r22 IID947 + __ ecmovq (Assembler::Condition::aboveEqual, rbx, rbx, r26); // cmovae rbx, r26 IID948 + __ ecmovq (Assembler::Condition::zero, r23, r21, r13); // cmovz r23, r21, r13 IID949 + __ ecmovq (Assembler::Condition::zero, r10, r10, r20); // cmovz r10, r20 IID950 + __ ecmovq (Assembler::Condition::notZero, rbx, r9, r29); // cmovnz rbx, r9, r29 IID951 + __ ecmovq (Assembler::Condition::notZero, r16, r16, r30); // cmovnz r16, r30 IID952 + __ ecmovq (Assembler::Condition::belowEqual, r13, rcx, r29); // cmovbe r13, rcx, r29 IID953 + __ ecmovq (Assembler::Condition::belowEqual, r31, r31, r13); // cmovbe r31, r13 IID954 + __ ecmovq (Assembler::Condition::above, r27, r9, r30); // cmova r27, r9, r30 IID955 + __ ecmovq (Assembler::Condition::above, r26, r26, r20); // cmova r26, r20 IID956 + __ ecmovq (Assembler::Condition::negative, r8, r12, r22); // cmovs r8, r12, r22 IID957 + __ ecmovq (Assembler::Condition::negative, r31, r31, r17); // cmovs r31, r17 IID958 + __ ecmovq (Assembler::Condition::positive, r29, rcx, r25); // cmovns r29, rcx, r25 IID959 + __ ecmovq (Assembler::Condition::positive, r22, r22, r14); // cmovns r22, r14 IID960 + __ ecmovq (Assembler::Condition::parity, rcx, r27, r9); // cmovp rcx, r27, r9 IID961 + __ ecmovq (Assembler::Condition::parity, r22, r22, r11); // cmovp r22, r11 IID962 + __ ecmovq (Assembler::Condition::noParity, r14, r19, r24); // cmovnp r14, r19, r24 IID963 + __ ecmovq (Assembler::Condition::noParity, r24, r24, r17); // cmovnp r24, r17 IID964 + __ ecmovq (Assembler::Condition::less, r17, r19, r30); // cmovl r17, r19, r30 IID965 + __ ecmovq (Assembler::Condition::less, r19, r19, r14); // cmovl r19, r14 IID966 + __ ecmovq (Assembler::Condition::greaterEqual, r25, r11, r29); // cmovge r25, r11, r29 IID967 + __ ecmovq (Assembler::Condition::greaterEqual, r12, r12, r26); // cmovge r12, r26 IID968 + __ ecmovq (Assembler::Condition::lessEqual, r11, rbx, r10); // cmovle r11, rbx, r10 IID969 + __ ecmovq (Assembler::Condition::lessEqual, rdx, rdx, r22); // cmovle rdx, r22 IID970 + __ ecmovq (Assembler::Condition::greater, r14, r15, r23); // cmovg r14, r15, r23 IID971 + __ ecmovq (Assembler::Condition::greater, r8, r8, r24); // cmovg r8, r24 IID972 + __ ecmovq (Assembler::Condition::overflow, rbx, r31, Address(r10, r8, (Address::ScaleFactor)3, -0x313f60e0)); // cmovo rbx, r31, qword ptr [r10+r8*8-0x313f60e0] IID973 + __ ecmovq (Assembler::Condition::overflow, r23, r23, Address(rcx, r24, (Address::ScaleFactor)2, +0x17f41d9c)); // cmovo r23, qword ptr [rcx+r24*4+0x17f41d9c] IID974 + __ ecmovq (Assembler::Condition::noOverflow, r31, r11, Address(r16, +0x2c018942)); // cmovno r31, r11, qword ptr [r16+0x2c018942] IID975 + __ ecmovq (Assembler::Condition::noOverflow, r11, r11, Address(r16, r20, (Address::ScaleFactor)3, +0x674b6a55)); // cmovno r11, qword ptr [r16+r20*8+0x674b6a55] IID976 + __ ecmovq (Assembler::Condition::below, r9, r13, Address(r9, rcx, (Address::ScaleFactor)0, +0x394a11df)); // cmovb r9, r13, qword ptr [r9+rcx*1+0x394a11df] IID977 + __ ecmovq (Assembler::Condition::below, r30, r30, Address(rdx, r22, (Address::ScaleFactor)1, -0x6c362b88)); // cmovb r30, qword ptr [rdx+r22*2-0x6c362b88] IID978 + __ ecmovq (Assembler::Condition::aboveEqual, r13, rcx, Address(r24, rcx, (Address::ScaleFactor)3, +0x46500b66)); // cmovae r13, rcx, qword ptr [r24+rcx*8+0x46500b66] IID979 + __ ecmovq (Assembler::Condition::aboveEqual, r24, r24, Address(r18, r25, (Address::ScaleFactor)1, +0x53283b7c)); // cmovae r24, qword ptr [r18+r25*2+0x53283b7c] IID980 + __ ecmovq (Assembler::Condition::zero, r23, r25, Address(r15, r9, (Address::ScaleFactor)0, -0x5f03031e)); // cmovz r23, r25, qword ptr [r15+r9*1-0x5f03031e] IID981 + __ ecmovq (Assembler::Condition::zero, r25, r25, Address(r28, r16, (Address::ScaleFactor)1, -0x53cef514)); // cmovz r25, qword ptr [r28+r16*2-0x53cef514] IID982 + __ ecmovq (Assembler::Condition::notZero, rbx, r25, Address(r24, r25, (Address::ScaleFactor)2, -0x66caac87)); // cmovnz rbx, r25, qword ptr [r24+r25*4-0x66caac87] IID983 + __ ecmovq (Assembler::Condition::notZero, r16, r16, Address(r27, r30, (Address::ScaleFactor)3, +0x797f455d)); // cmovnz r16, qword ptr [r27+r30*8+0x797f455d] IID984 + __ ecmovq (Assembler::Condition::belowEqual, r25, r30, Address(r18, r18, (Address::ScaleFactor)1, +0x1c9daacd)); // cmovbe r25, r30, qword ptr [r18+r18*2+0x1c9daacd] IID985 + __ ecmovq (Assembler::Condition::belowEqual, r22, r22, Address(rcx, r25, (Address::ScaleFactor)1, -0x3dcbfaa9)); // cmovbe r22, qword ptr [rcx+r25*2-0x3dcbfaa9] IID986 + __ ecmovq (Assembler::Condition::above, r24, r26, Address(r25, +0x747060b5)); // cmova r24, r26, qword ptr [r25+0x747060b5] IID987 + __ ecmovq (Assembler::Condition::above, r8, r8, Address(r24, r20, (Address::ScaleFactor)3, +0x47d285f6)); // cmova r8, qword ptr [r24+r20*8+0x47d285f6] IID988 + __ ecmovq (Assembler::Condition::negative, r12, r16, Address(r13, r10, (Address::ScaleFactor)2, +0x34e5b214)); // cmovs r12, r16, qword ptr [r13+r10*4+0x34e5b214] IID989 + __ ecmovq (Assembler::Condition::negative, rdx, rdx, Address(r15, r19, (Address::ScaleFactor)0, -0x405138b1)); // cmovs rdx, qword ptr [r15+r19*1-0x405138b1] IID990 + __ ecmovq (Assembler::Condition::positive, r18, r21, Address(rbx, r13, (Address::ScaleFactor)2, +0x51b19197)); // cmovns r18, r21, qword ptr [rbx+r13*4+0x51b19197] IID991 + __ ecmovq (Assembler::Condition::positive, r24, r24, Address(r11, r31, (Address::ScaleFactor)3, +0x3e01520a)); // cmovns r24, qword ptr [r11+r31*8+0x3e01520a] IID992 + __ ecmovq (Assembler::Condition::parity, r29, r26, Address(r10, r25, (Address::ScaleFactor)3, -0x5f7c3872)); // cmovp r29, r26, qword ptr [r10+r25*8-0x5f7c3872] IID993 + __ ecmovq (Assembler::Condition::parity, r11, r11, Address(r22, r10, (Address::ScaleFactor)3, -0x68731453)); // cmovp r11, qword ptr [r22+r10*8-0x68731453] IID994 + __ ecmovq (Assembler::Condition::noParity, r20, r15, Address(r9, r25, (Address::ScaleFactor)0, +0x4a37edaa)); // cmovnp r20, r15, qword ptr [r9+r25*1+0x4a37edaa] IID995 + __ ecmovq (Assembler::Condition::noParity, r31, r31, Address(r9, r20, (Address::ScaleFactor)0, +0x4f999f86)); // cmovnp r31, qword ptr [r9+r20*1+0x4f999f86] IID996 + __ ecmovq (Assembler::Condition::less, r18, r23, Address(r9, r27, (Address::ScaleFactor)0, -0x3410441d)); // cmovl r18, r23, qword ptr [r9+r27*1-0x3410441d] IID997 + __ ecmovq (Assembler::Condition::less, r16, r16, Address(r24, r10, (Address::ScaleFactor)3, +0x52ed66ee)); // cmovl r16, qword ptr [r24+r10*8+0x52ed66ee] IID998 + __ ecmovq (Assembler::Condition::greaterEqual, r11, r18, Address(rcx, +0x1de09163)); // cmovge r11, r18, qword ptr [rcx+0x1de09163] IID999 + __ ecmovq (Assembler::Condition::greaterEqual, r14, r14, Address(r24, r23, (Address::ScaleFactor)1, +0x5df3b4da)); // cmovge r14, qword ptr [r24+r23*2+0x5df3b4da] IID1000 + __ ecmovq (Assembler::Condition::lessEqual, r15, r14, Address(r30, r20, (Address::ScaleFactor)1, +0x5c9ab976)); // cmovle r15, r14, qword ptr [r30+r20*2+0x5c9ab976] IID1001 + __ ecmovq (Assembler::Condition::lessEqual, r26, r26, Address(r18, r27, (Address::ScaleFactor)2, -0xd8c329)); // cmovle r26, qword ptr [r18+r27*4-0xd8c329] IID1002 + __ ecmovq (Assembler::Condition::greater, r29, r9, Address(r30, r20, (Address::ScaleFactor)3, -0x37a9cf8d)); // cmovg r29, r9, qword ptr [r30+r20*8-0x37a9cf8d] IID1003 + __ ecmovq (Assembler::Condition::greater, r20, r20, Address(r8, rbx, (Address::ScaleFactor)1, +0x1bdc7def)); // cmovg r20, qword ptr [r8+rbx*2+0x1bdc7def] IID1004 #endif // _LP64 static const uint8_t insns[] = @@ -1243,665 +1327,749 @@ 0x62, 0xd4, 0x68, 0x18, 0x81, 0xb4, 0x82, 0xb9, 0xe2, 0xe1, 0xe9, 0x00, 0x00, 0x00, 0x01, // IID265 0x62, 0xdc, 0x68, 0x1c, 0x81, 0xb4, 0x7d, 0xf8, 0xe2, 0x34, 0x1b, 0x00, 0x00, 0x00, 0x01, // IID266 0x62, 0x0c, 0x60, 0x10, 0x01, 0xac, 0x3b, 0xd8, 0xe7, 0x3c, 0x1f, // IID267 - 0x62, 0x4c, 0x1c, 0x10, 0x01, 0xa4, 0xc8, 0x3e, 0x12, 0xac, 0x9f, // IID268 + 0xd5, 0x55, 0x03, 0xa4, 0xc8, 0x3e, 0x12, 0xac, 0x9f, // IID268 0x62, 0x2c, 0x70, 0x14, 0x01, 0xac, 0xc2, 0x88, 0xe0, 0x08, 0xe4, // IID269 0x62, 0x94, 0x70, 0x1c, 0x01, 0x8c, 0x67, 0x16, 0x82, 0x5b, 0x01, // IID270 - 0x62, 0x64, 0x0c, 0x10, 0x09, 0xa4, 0xd3, 0x4c, 0xbf, 0xca, 0xb9, // IID271 - 0x62, 0x8c, 0x6c, 0x10, 0x09, 0x94, 0xd4, 0x3b, 0xa7, 0x23, 0x35, // IID272 - 0x62, 0x84, 0x34, 0x1c, 0x09, 0xac, 0x7f, 0xaa, 0x22, 0xf4, 0xd5, // IID273 - 0x62, 0xec, 0x7c, 0x14, 0x09, 0x87, 0x01, 0x9b, 0xaf, 0xe9, // IID274 - 0x62, 0x8c, 0x1c, 0x10, 0x08, 0xa4, 0x1e, 0x3a, 0x1e, 0x28, 0x17, // IID275 - 0x62, 0xb4, 0x68, 0x18, 0x08, 0x94, 0xbb, 0xbb, 0xb5, 0x77, 0x24, // IID276 - 0x62, 0x44, 0x7c, 0x14, 0x08, 0x84, 0x4b, 0x51, 0x2e, 0x8a, 0xce, // IID277 - 0x62, 0xd4, 0x60, 0x1c, 0x08, 0x9c, 0xe3, 0x2d, 0x84, 0x29, 0xdd, // IID278 - 0x62, 0x9c, 0x28, 0x10, 0x29, 0x94, 0x73, 0xd2, 0x31, 0x64, 0xc2, // IID279 - 0x62, 0x2c, 0x00, 0x10, 0x29, 0xbc, 0x6e, 0x19, 0x85, 0x21, 0x14, // IID280 - 0x62, 0x54, 0x54, 0x14, 0x29, 0xa9, 0x86, 0xed, 0xaf, 0xef, // IID281 - 0x62, 0x04, 0x04, 0x14, 0x29, 0xbc, 0x01, 0x9f, 0x76, 0x1e, 0xf5, // IID282 - 0x62, 0x7c, 0x04, 0x18, 0x31, 0xa2, 0xe5, 0xbc, 0x2b, 0x5c, // IID283 - 0x62, 0x4c, 0x20, 0x10, 0x31, 0x9c, 0x39, 0xb3, 0x78, 0x60, 0x5c, // IID284 - 0x62, 0x54, 0x6c, 0x14, 0x31, 0xb4, 0xd0, 0x7f, 0xc7, 0x12, 0xf6, // IID285 - 0x62, 0x54, 0x34, 0x1c, 0x31, 0x8f, 0xad, 0xcd, 0x5a, 0x77, // IID286 - 0x62, 0xac, 0x50, 0x10, 0x30, 0xbc, 0x52, 0xd5, 0x1f, 0xe3, 0x2f, // IID287 - 0x62, 0x5c, 0x2c, 0x18, 0x30, 0x93, 0xde, 0x50, 0x31, 0x0a, // IID288 - 0x62, 0x2c, 0x68, 0x14, 0x30, 0x84, 0xf6, 0x97, 0xe8, 0xd4, 0x1a, // IID289 - 0x62, 0x7c, 0x38, 0x1c, 0x30, 0x84, 0x20, 0x82, 0xae, 0x6e, 0x62, // IID290 - 0x62, 0xd4, 0x54, 0x10, 0x81, 0xc7, 0x00, 0x00, 0x10, 0x00, // IID291 - 0x62, 0xfc, 0x7c, 0x18, 0x81, 0xc2, 0x00, 0x00, 0x10, 0x00, // IID292 - 0xd5, 0x10, 0x81, 0xc2, 0x00, 0x01, 0x00, 0x00, // IID293 - 0x62, 0xfc, 0x14, 0x1c, 0x83, 0xc3, 0x10, // IID294 - 0x62, 0xfc, 0x7c, 0x1c, 0x83, 0xc7, 0x10, // IID295 - 0x62, 0xdc, 0x34, 0x14, 0x81, 0xc1, 0x00, 0x00, 0x00, 0x01, // IID296 - 0x62, 0xfc, 0x14, 0x10, 0x81, 0xe2, 0x00, 0x00, 0x10, 0x00, // IID297 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xe6, 0x00, 0x00, 0x10, 0x00, // IID298 - 0xd5, 0x10, 0x81, 0xe3, 0x00, 0x00, 0x01, 0x00, // IID299 - 0x62, 0xdc, 0x24, 0x14, 0x81, 0xe1, 0x00, 0x00, 0x10, 0x00, // IID300 - 0x62, 0xfc, 0x7c, 0x1c, 0x81, 0xe4, 0x00, 0x00, 0x10, 0x00, // IID301 - 0x62, 0xdc, 0x1c, 0x14, 0x83, 0xe4, 0x10, // IID302 - 0x62, 0x6c, 0x7c, 0x08, 0x69, 0xfe, 0x00, 0x10, 0x00, 0x00, // IID303 + 0x62, 0x64, 0x0c, 0x10, 0x21, 0xa4, 0xd3, 0x4c, 0xbf, 0xca, 0xb9, // IID271 + 0xd5, 0x53, 0x23, 0x94, 0xd4, 0x3b, 0xa7, 0x23, 0x35, // IID272 + 0x62, 0x84, 0x34, 0x1c, 0x21, 0xac, 0x7f, 0xaa, 0x22, 0xf4, 0xd5, // IID273 + 0x62, 0xec, 0x7c, 0x14, 0x21, 0x87, 0x01, 0x9b, 0xaf, 0xe9, // IID274 + 0x62, 0x8c, 0x1c, 0x10, 0x09, 0xa4, 0x1e, 0x3a, 0x1e, 0x28, 0x17, // IID275 + 0xd5, 0x22, 0x0b, 0x94, 0xbb, 0xbb, 0xb5, 0x77, 0x24, // IID276 + 0x62, 0x44, 0x7c, 0x14, 0x09, 0x84, 0x4b, 0x51, 0x2e, 0x8a, 0xce, // IID277 + 0x62, 0xd4, 0x60, 0x1c, 0x09, 0x9c, 0xe3, 0x2d, 0x84, 0x29, 0xdd, // IID278 + 0x62, 0x9c, 0x28, 0x10, 0x08, 0x94, 0x73, 0xd2, 0x31, 0x64, 0xc2, // IID279 + 0xd5, 0x76, 0x0a, 0xbc, 0x6e, 0x19, 0x85, 0x21, 0x14, // IID280 + 0x62, 0x54, 0x54, 0x14, 0x08, 0xa9, 0x86, 0xed, 0xaf, 0xef, // IID281 + 0x62, 0x04, 0x04, 0x14, 0x08, 0xbc, 0x01, 0x9f, 0x76, 0x1e, 0xf5, // IID282 + 0x62, 0x7c, 0x04, 0x18, 0x29, 0xa2, 0xe5, 0xbc, 0x2b, 0x5c, // IID283 + 0x62, 0x4c, 0x20, 0x10, 0x29, 0x9c, 0x39, 0xb3, 0x78, 0x60, 0x5c, // IID284 + 0x62, 0x54, 0x6c, 0x14, 0x29, 0xb4, 0xd0, 0x7f, 0xc7, 0x12, 0xf6, // IID285 + 0x62, 0x54, 0x34, 0x1c, 0x29, 0x8f, 0xad, 0xcd, 0x5a, 0x77, // IID286 + 0x62, 0xac, 0x50, 0x10, 0x31, 0xbc, 0x52, 0xd5, 0x1f, 0xe3, 0x2f, // IID287 + 0xd5, 0x15, 0x33, 0x93, 0xde, 0x50, 0x31, 0x0a, // IID288 + 0x62, 0x2c, 0x68, 0x14, 0x31, 0x84, 0xf6, 0x97, 0xe8, 0xd4, 0x1a, // IID289 + 0x62, 0x7c, 0x38, 0x1c, 0x31, 0x84, 0x20, 0x82, 0xae, 0x6e, 0x62, // IID290 + 0x62, 0xac, 0x7c, 0x10, 0x30, 0x94, 0x3d, 0xf3, 0x49, 0xfc, 0xeb, // IID291 + 0xd5, 0x34, 0x32, 0xac, 0xbb, 0xe1, 0xf1, 0x7e, 0x23, // IID292 + 0x62, 0x3c, 0x14, 0x14, 0x30, 0xb4, 0xb2, 0x5b, 0x09, 0xc0, 0x5c, // IID293 + 0x62, 0x4c, 0x20, 0x14, 0x30, 0x9c, 0xe1, 0x58, 0xb9, 0xf7, 0x1c, // IID294 + 0x62, 0xdc, 0x7c, 0x10, 0x83, 0xc0, 0x10, // IID295 + 0x62, 0xdc, 0x7c, 0x18, 0x83, 0xc0, 0x10, // IID296 + 0xd5, 0x10, 0x81, 0xc5, 0x00, 0x00, 0x01, 0x00, // IID297 + 0x62, 0xd4, 0x3c, 0x14, 0x81, 0xc0, 0x00, 0x00, 0x10, 0x00, // IID298 + 0x62, 0xd4, 0x7c, 0x1c, 0x81, 0xc5, 0x00, 0x00, 0x10, 0x00, // IID299 + 0x62, 0xdc, 0x14, 0x14, 0x81, 0xc5, 0x00, 0x00, 0x00, 0x01, // IID300 + 0x41, 0x83, 0xe4, 0x10, // IID301 + 0x62, 0xdc, 0x7c, 0x18, 0x83, 0xe6, 0x10, // IID302 + 0xd5, 0x11, 0x83, 0xe0, 0x10, // IID303 + 0x62, 0xd4, 0x3c, 0x1c, 0x83, 0xe4, 0x01, // IID304 + 0x62, 0xd4, 0x7c, 0x1c, 0x83, 0xe5, 0x01, // IID305 + 0x62, 0xdc, 0x34, 0x14, 0x83, 0xe1, 0x10, // IID306 + 0x62, 0xec, 0x7c, 0x08, 0x69, 0xd7, 0x00, 0x00, 0x01, 0x00, // IID307 + 0x62, 0xd4, 0x7c, 0x08, 0x69, 0xc1, 0x00, 0x00, 0x01, 0x00, // IID308 + 0x62, 0x4c, 0x7c, 0x08, 0x69, 0xd2, 0x00, 0x00, 0x00, 0x10, // IID309 + 0x62, 0x6c, 0x7c, 0x0c, 0x6b, 0xcd, 0x01, // IID310 + 0x62, 0xdc, 0x7c, 0x0c, 0x6b, 0xc0, 0x01, // IID311 + 0x62, 0x4c, 0x7c, 0x0c, 0x69, 0xc0, 0x00, 0x00, 0x00, 0x01, // IID312 + 0x62, 0xdc, 0x0c, 0x10, 0x83, 0xca, 0x01, // IID313 + 0x62, 0xfc, 0x7c, 0x18, 0x83, 0xce, 0x01, // IID314 + 0xd5, 0x10, 0x81, 0xc9, 0x00, 0x00, 0x10, 0x00, // IID315 + 0x62, 0xd4, 0x3c, 0x14, 0x83, 0xc8, 0x01, // IID316 + 0x62, 0xdc, 0x7c, 0x1c, 0x83, 0xcb, 0x01, // IID317 #endif // _LP64 - 0x62, 0xf4, 0x7c, 0x08, 0x69, 0xc3, 0x00, 0x10, 0x00, 0x00, // IID304 + 0x62, 0xf4, 0x6c, 0x1c, 0x81, 0xca, 0x00, 0x00, 0x00, 0x10, // IID318 #ifdef _LP64 - 0x62, 0x4c, 0x7c, 0x08, 0x69, 0xc0, 0x00, 0x00, 0x10, 0x00, // IID305 - 0x62, 0xec, 0x7c, 0x0c, 0x69, 0xe8, 0x00, 0x00, 0x01, 0x00, // IID306 - 0x62, 0xdc, 0x7c, 0x0c, 0x69, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID307 - 0x62, 0x54, 0x7c, 0x0c, 0x6b, 0xed, 0x10, // IID308 - 0x62, 0xd4, 0x14, 0x10, 0x81, 0xc8, 0x00, 0x00, 0x00, 0x01, // IID309 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xcc, 0x00, 0x00, 0x00, 0x01, // IID310 - 0xd5, 0x11, 0x81, 0xce, 0x00, 0x10, 0x00, 0x00, // IID311 - 0x62, 0xf4, 0x3c, 0x14, 0x83, 0xca, 0x10, // IID312 - 0x62, 0xd4, 0x7c, 0x1c, 0x83, 0xc8, 0x10, // IID313 - 0x62, 0xd4, 0x14, 0x1c, 0x81, 0xcd, 0x00, 0x10, 0x00, 0x00, // IID314 - 0x62, 0xd4, 0x34, 0x10, 0xd1, 0xd5, // IID315 - 0x62, 0xfc, 0x7c, 0x18, 0xd1, 0xd2, // IID316 - 0x41, 0xc1, 0xd1, 0x10, // IID317 - 0x62, 0xdc, 0x2c, 0x10, 0xc1, 0xc1, 0x08, // IID318 + 0xd5, 0x10, 0xc1, 0xd6, 0x08, // IID319 + 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xd7, 0x08, // IID320 + 0xd5, 0x10, 0xc1, 0xd3, 0x04, // IID321 + 0x62, 0xdc, 0x0c, 0x10, 0xc1, 0xc0, 0x02, // IID322 + 0x62, 0xdc, 0x7c, 0x18, 0xc1, 0xc5, 0x02, // IID323 + 0x41, 0xc1, 0xc0, 0x02, // IID324 + 0x62, 0xdc, 0x6c, 0x14, 0xc1, 0xc0, 0x10, // IID325 + 0x62, 0xd4, 0x7c, 0x1c, 0xc1, 0xc5, 0x10, // IID326 + 0x62, 0xdc, 0x3c, 0x14, 0xd1, 0xc0, // IID327 + 0x62, 0xfc, 0x1c, 0x10, 0xc1, 0xc9, 0x10, // IID328 + 0x62, 0xdc, 0x7c, 0x18, 0xc1, 0xc8, 0x10, // IID329 + 0xd5, 0x10, 0xc1, 0xc9, 0x04, // IID330 + 0x62, 0xf4, 0x3c, 0x14, 0xc1, 0xc9, 0x04, // IID331 + 0x62, 0xfc, 0x7c, 0x1c, 0xc1, 0xc8, 0x04, // IID332 + 0x62, 0xd4, 0x04, 0x1c, 0xc1, 0xcf, 0x02, // IID333 + 0x62, 0xdc, 0x0c, 0x18, 0xc1, 0xe3, 0x04, // IID334 + 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xe7, 0x04, // IID335 + 0xd5, 0x11, 0xc1, 0xe6, 0x04, // IID336 + 0x62, 0xf4, 0x24, 0x14, 0xc1, 0xe2, 0x02, // IID337 + 0x62, 0xfc, 0x7c, 0x1c, 0xc1, 0xe3, 0x02, // IID338 + 0x62, 0xfc, 0x5c, 0x14, 0xc1, 0xe4, 0x02, // IID339 + 0x62, 0xfc, 0x54, 0x10, 0xd1, 0xff, // IID340 + 0x62, 0xdc, 0x7c, 0x18, 0xd1, 0xfe, // IID341 + 0xd5, 0x11, 0xc1, 0xf9, 0x02, // IID342 + 0x62, 0xfc, 0x3c, 0x14, 0xc1, 0xfb, 0x04, // IID343 + 0x62, 0xd4, 0x7c, 0x1c, 0xc1, 0xfe, 0x04, // IID344 + 0x62, 0xdc, 0x2c, 0x14, 0xc1, 0xfa, 0x10, // IID345 + 0x62, 0xd4, 0x4c, 0x10, 0xc1, 0xe5, 0x08, // IID346 + 0x62, 0xdc, 0x7c, 0x18, 0xc1, 0xe0, 0x08, // IID347 + 0x41, 0xc1, 0xe6, 0x10, // IID348 + 0x62, 0xdc, 0x1c, 0x14, 0xc1, 0xe1, 0x08, // IID349 + 0x62, 0xd4, 0x7c, 0x1c, 0xc1, 0xe2, 0x08, // IID350 + 0x62, 0xfc, 0x5c, 0x14, 0xd1, 0xe4, // IID351 + 0x62, 0xf4, 0x1c, 0x18, 0xc1, 0xeb, 0x04, // IID352 + 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xef, 0x04, // IID353 + 0xd5, 0x11, 0xc1, 0xec, 0x10, // IID354 + 0x62, 0xdc, 0x3c, 0x14, 0xc1, 0xee, 0x04, // IID355 + 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xef, 0x04, // IID356 + 0x62, 0xdc, 0x04, 0x14, 0xc1, 0xef, 0x02, // IID357 + 0x62, 0xd4, 0x5c, 0x10, 0x81, 0xea, 0x00, 0x01, 0x00, 0x00, // IID358 + 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xed, 0x00, 0x01, 0x00, 0x00, // IID359 + 0xd5, 0x11, 0x81, 0xe9, 0x00, 0x01, 0x00, 0x00, // IID360 + 0x62, 0xd4, 0x44, 0x14, 0x81, 0xec, 0x00, 0x00, 0x00, 0x10, // IID361 + 0x62, 0xfc, 0x7c, 0x1c, 0x81, 0xe8, 0x00, 0x00, 0x00, 0x10, // IID362 + 0x62, 0xdc, 0x04, 0x14, 0x83, 0xef, 0x01, // IID363 + 0x62, 0xd4, 0x34, 0x18, 0x81, 0xf7, 0x00, 0x00, 0x00, 0x01, // IID364 + 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xf5, 0x00, 0x00, 0x00, 0x01, // IID365 + 0xd5, 0x11, 0x83, 0xf4, 0x10, // IID366 + 0x62, 0xfc, 0x14, 0x14, 0x83, 0xf6, 0x10, // IID367 #endif // _LP64 - 0x62, 0xf4, 0x7c, 0x18, 0xc1, 0xc2, 0x08, // IID319 + 0x62, 0xf4, 0x7c, 0x1c, 0x83, 0xf3, 0x10, // IID368 #ifdef _LP64 - 0xd5, 0x11, 0xc1, 0xc0, 0x10, // IID320 - 0x62, 0xf4, 0x3c, 0x14, 0xc1, 0xc1, 0x08, // IID321 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xc6, 0x08, // IID322 - 0x62, 0xdc, 0x1c, 0x14, 0xc1, 0xc4, 0x10, // IID323 - 0x62, 0xdc, 0x74, 0x10, 0xc1, 0xcc, 0x04, // IID324 + 0x62, 0xd4, 0x3c, 0x1c, 0x83, 0xf0, 0x10, // IID369 + 0x62, 0xd4, 0x7c, 0x10, 0x81, 0xed, 0x00, 0x00, 0x40, 0x00, // IID370 + 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xec, 0x00, 0x00, 0x40, 0x00, // IID371 + 0xd5, 0x10, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x04, // IID372 + 0x62, 0xdc, 0x4c, 0x14, 0x81, 0xea, 0x00, 0x00, 0x00, 0x40, // IID373 + 0x62, 0xd4, 0x7c, 0x1c, 0x81, 0xea, 0x00, 0x00, 0x00, 0x40, // IID374 + 0x62, 0xd4, 0x24, 0x1c, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x40, // IID375 + 0x62, 0x1c, 0x64, 0x10, 0x03, 0xa4, 0x06, 0x73, 0x0a, 0x1a, 0x6a, // IID376 + 0xd5, 0x74, 0x03, 0xb4, 0x9a, 0xcf, 0x90, 0xf9, 0x25, // IID377 + 0x62, 0x6c, 0x70, 0x1c, 0x03, 0x8c, 0x03, 0xbc, 0x5d, 0x2d, 0x48, // IID378 + 0x62, 0x54, 0x34, 0x1c, 0x03, 0x8b, 0x01, 0xee, 0xd5, 0x43, // IID379 + 0x62, 0xac, 0x74, 0x18, 0x23, 0xbc, 0xbd, 0xbc, 0xc2, 0x25, 0x28, // IID380 + 0xd5, 0x47, 0x23, 0x9c, 0xfd, 0x6b, 0x47, 0x97, 0xed, // IID381 + 0x62, 0xac, 0x30, 0x1c, 0x23, 0xbc, 0x36, 0x45, 0x53, 0xea, 0xf8, // IID382 + 0x62, 0xdc, 0x60, 0x1c, 0x23, 0x9c, 0x84, 0xee, 0x23, 0x02, 0x0b, // IID383 + 0x62, 0x04, 0x00, 0x18, 0xaf, 0xac, 0x67, 0x97, 0x85, 0xd6, 0xe0, // IID384 + 0xd5, 0xd0, 0xaf, 0x8c, 0x5f, 0x45, 0x75, 0xdc, 0x0a, // IID385 + 0x62, 0x74, 0x20, 0x14, 0xaf, 0x8c, 0xb2, 0x9f, 0xf0, 0x26, 0xbc, // IID386 + 0x62, 0xdc, 0x60, 0x1c, 0xaf, 0x9c, 0xf4, 0xd9, 0x65, 0x62, 0xae, // IID387 + 0x62, 0xd4, 0x74, 0x10, 0x0b, 0x8e, 0x23, 0x22, 0x64, 0x10, // IID388 + 0xd5, 0x55, 0x0b, 0x97, 0x46, 0x7c, 0x65, 0x85, // IID389 + 0x62, 0x84, 0x04, 0x1c, 0x0b, 0xb4, 0xa4, 0x97, 0x69, 0x3b, 0x74, // IID390 + 0x62, 0x74, 0x38, 0x1c, 0x0b, 0x84, 0xf2, 0x24, 0xeb, 0x7b, 0xa7, // IID391 + 0x62, 0x0c, 0x74, 0x18, 0x2b, 0xa4, 0xae, 0xe5, 0x10, 0x93, 0x0e, // IID392 + 0xd5, 0x13, 0x2b, 0x8c, 0x56, 0x2f, 0x91, 0xf8, 0xe4, // IID393 + 0x62, 0xcc, 0x34, 0x1c, 0x2b, 0xae, 0xd3, 0xff, 0x79, 0x2f, // IID394 + 0x62, 0xa4, 0x7c, 0x14, 0x2b, 0x84, 0xb2, 0xc1, 0x71, 0x5d, 0x67, // IID395 + 0x62, 0x24, 0x20, 0x10, 0x33, 0xa4, 0x93, 0x7f, 0xf4, 0x3d, 0x87, // IID396 + 0xd5, 0x35, 0x33, 0xb4, 0x5f, 0x34, 0xae, 0x0d, 0xb0, // IID397 + 0x62, 0xc4, 0x58, 0x14, 0x33, 0x94, 0x85, 0x1e, 0x39, 0x10, 0xe6, // IID398 + 0x62, 0xc4, 0x60, 0x14, 0x33, 0x9c, 0x7d, 0x56, 0x27, 0xe4, 0xd2, // IID399 + 0x62, 0xa4, 0x10, 0x10, 0x32, 0x8c, 0xaa, 0x84, 0x3e, 0x57, 0x66, // IID400 + 0xd5, 0x73, 0x32, 0xb4, 0xc8, 0x3f, 0xa9, 0x94, 0x3a, // IID401 + 0x62, 0x44, 0x10, 0x1c, 0x32, 0xac, 0x7f, 0x32, 0x35, 0xd4, 0x76, // IID402 + 0x62, 0x14, 0x04, 0x1c, 0x32, 0xbc, 0x0d, 0xe6, 0x92, 0xb1, 0xb8, // IID403 + 0x62, 0xec, 0x75, 0x10, 0x33, 0x84, 0x17, 0x91, 0xa2, 0x62, 0x05, // IID404 + 0x66, 0xd5, 0x76, 0x33, 0xac, 0xe2, 0x0e, 0x98, 0xe6, 0xab, // IID405 + 0x62, 0x54, 0x25, 0x14, 0x33, 0x9a, 0x5a, 0x1c, 0x91, 0x0a, // IID406 + 0x62, 0x4c, 0x01, 0x14, 0x33, 0xbc, 0x9e, 0x26, 0x5c, 0x09, 0xff, // IID407 + 0x62, 0x7c, 0x1c, 0x18, 0x03, 0xef, // IID408 + 0xd5, 0x54, 0x03, 0xe4, // IID409 + 0xd5, 0x51, 0x03, 0xe0, // IID410 + 0x62, 0x54, 0x24, 0x1c, 0x03, 0xd7, // IID411 + 0x62, 0xec, 0x64, 0x14, 0x03, 0xdc, // IID412 + 0x62, 0x7c, 0x44, 0x14, 0x03, 0xff, // IID413 + 0x62, 0xcc, 0x2c, 0x10, 0x23, 0xd8, // IID414 + 0xd5, 0x51, 0x23, 0xfc, // IID415 + 0x45, 0x23, 0xdd, // IID416 + 0x62, 0xdc, 0x14, 0x1c, 0x23, 0xd7, // IID417 + 0x62, 0xec, 0x44, 0x14, 0x23, 0xff, // IID418 + 0x62, 0x44, 0x34, 0x1c, 0x23, 0xd9, // IID419 + 0x62, 0xcc, 0x54, 0x10, 0xaf, 0xe0, // IID420 + 0xd5, 0xd1, 0xaf, 0xed, // IID421 + 0x41, 0x0f, 0xaf, 0xdb, // IID422 + 0x62, 0xf4, 0x54, 0x14, 0xaf, 0xd9, // IID423 + 0x62, 0x6c, 0x04, 0x14, 0xaf, 0xfd, // IID424 + 0x62, 0x44, 0x04, 0x1c, 0xaf, 0xcf, // IID425 + 0x62, 0xcc, 0x0d, 0x10, 0x0b, 0xf9, // IID426 + 0x66, 0xd5, 0x40, 0x0b, 0xd1, // IID427 + 0x66, 0x44, 0x0b, 0xd1, // IID428 + 0x62, 0xcc, 0x05, 0x14, 0x0b, 0xea, // IID429 + 0x62, 0xec, 0x55, 0x14, 0x0b, 0xeb, // IID430 #endif // _LP64 - 0x62, 0xf4, 0x7c, 0x18, 0xc1, 0xca, 0x04, // IID325 + 0x62, 0xf4, 0x6d, 0x1c, 0x0b, 0xda, // IID431 #ifdef _LP64 - 0x41, 0xc1, 0xc8, 0x10, // IID326 - 0x62, 0xf4, 0x64, 0x14, 0xc1, 0xca, 0x10, // IID327 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xcf, 0x10, // IID328 - 0x62, 0xfc, 0x4c, 0x14, 0xc1, 0xce, 0x08, // IID329 - 0x62, 0xdc, 0x44, 0x10, 0xc1, 0xe1, 0x10, // IID330 - 0x62, 0xd4, 0x7c, 0x18, 0xc1, 0xe6, 0x10, // IID331 - 0xd5, 0x11, 0xc1, 0xe7, 0x08, // IID332 - 0x62, 0xdc, 0x0c, 0x14, 0xc1, 0xe0, 0x02, // IID333 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xe5, 0x02, // IID334 - 0x62, 0xd4, 0x3c, 0x1c, 0xc1, 0xe0, 0x02, // IID335 - 0x62, 0xdc, 0x6c, 0x10, 0xc1, 0xf8, 0x10, // IID336 - 0x62, 0xd4, 0x7c, 0x18, 0xc1, 0xfd, 0x10, // IID337 - 0xd5, 0x11, 0xd1, 0xf8, // IID338 - 0x62, 0xfc, 0x1c, 0x14, 0xc1, 0xf9, 0x10, // IID339 - 0x62, 0xdc, 0x7c, 0x1c, 0xc1, 0xf8, 0x10, // IID340 - 0x62, 0xfc, 0x74, 0x14, 0xc1, 0xf9, 0x04, // IID341 - 0x62, 0xf4, 0x3c, 0x10, 0xc1, 0xe1, 0x04, // IID342 - 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xe0, 0x04, // IID343 - 0x41, 0xc1, 0xe7, 0x02, // IID344 - 0x62, 0xdc, 0x0c, 0x1c, 0xc1, 0xe3, 0x04, // IID345 - 0x62, 0xfc, 0x7c, 0x1c, 0xc1, 0xe7, 0x04, // IID346 - 0x62, 0xdc, 0x0c, 0x14, 0xc1, 0xe6, 0x04, // IID347 - 0x62, 0xf4, 0x24, 0x10, 0xc1, 0xea, 0x02, // IID348 - 0x62, 0xfc, 0x7c, 0x18, 0xc1, 0xeb, 0x02, // IID349 - 0xd5, 0x10, 0xc1, 0xec, 0x02, // IID350 - 0x62, 0xfc, 0x54, 0x14, 0xd1, 0xef, // IID351 - 0x62, 0xdc, 0x7c, 0x1c, 0xd1, 0xee, // IID352 - 0x62, 0xdc, 0x34, 0x14, 0xc1, 0xe9, 0x02, // IID353 - 0x62, 0xfc, 0x3c, 0x10, 0x81, 0xeb, 0x00, 0x00, 0x10, 0x00, // IID354 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xee, 0x00, 0x00, 0x10, 0x00, // IID355 - 0xd5, 0x10, 0x81, 0xee, 0x00, 0x00, 0x00, 0x10, // IID356 - 0x62, 0xdc, 0x3c, 0x14, 0x81, 0xe8, 0x00, 0x00, 0x01, 0x00, // IID357 - 0x62, 0xd4, 0x7c, 0x1c, 0x81, 0xee, 0x00, 0x00, 0x01, 0x00, // IID358 - 0x62, 0xdc, 0x1c, 0x14, 0x81, 0xec, 0x00, 0x00, 0x00, 0x10, // IID359 - 0x62, 0xfc, 0x64, 0x18, 0x81, 0xf4, 0x00, 0x01, 0x00, 0x00, // IID360 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xf7, 0x00, 0x01, 0x00, 0x00, // IID361 -#endif // _LP64 - 0x81, 0xf3, 0x00, 0x10, 0x00, 0x00, // IID362 -#ifdef _LP64 - 0x62, 0xdc, 0x3c, 0x14, 0x81, 0xf6, 0x00, 0x00, 0x01, 0x00, // IID363 - 0x62, 0xdc, 0x7c, 0x1c, 0x81, 0xf7, 0x00, 0x00, 0x01, 0x00, // IID364 - 0x62, 0xdc, 0x04, 0x14, 0x81, 0xf7, 0x00, 0x10, 0x00, 0x00, // IID365 - 0x62, 0xd4, 0x5c, 0x10, 0x81, 0xea, 0x00, 0x00, 0x10, 0x00, // IID366 - 0x62, 0xd4, 0x7c, 0x18, 0x81, 0xed, 0x00, 0x00, 0x10, 0x00, // IID367 - 0xd5, 0x11, 0x81, 0xe9, 0x00, 0x00, 0x10, 0x00, // IID368 - 0x62, 0xd4, 0x44, 0x14, 0x81, 0xec, 0x00, 0x00, 0x00, 0x40, // IID369 - 0x62, 0xfc, 0x7c, 0x1c, 0x81, 0xe8, 0x00, 0x00, 0x00, 0x40, // IID370 - 0x62, 0xdc, 0x04, 0x14, 0x81, 0xef, 0x00, 0x00, 0x01, 0x00, // IID371 - 0x62, 0x54, 0x74, 0x10, 0x03, 0xa9, 0x98, 0x2f, 0xef, 0x7f, // IID372 - 0x62, 0x7c, 0x14, 0x14, 0x03, 0x86, 0x54, 0xf5, 0x08, 0xb2, // IID373 - 0x62, 0x84, 0x14, 0x18, 0x23, 0x8c, 0xfc, 0x02, 0xa9, 0xa8, 0x50, // IID374 - 0x62, 0x0c, 0x4c, 0x14, 0x23, 0x8c, 0x92, 0x54, 0x27, 0xea, 0x70, // IID375 - 0x62, 0x1c, 0x64, 0x10, 0xaf, 0xa4, 0x06, 0x73, 0x0a, 0x1a, 0x6a, // IID376 - 0x62, 0xec, 0x08, 0x14, 0xaf, 0x94, 0x9a, 0x39, 0xd7, 0x32, 0x80, // IID377 - 0x62, 0x0c, 0x7c, 0x10, 0x0b, 0xbc, 0xd9, 0xbc, 0x5d, 0x2d, 0x48, // IID378 - 0x62, 0x44, 0x34, 0x1c, 0x0b, 0x9b, 0x01, 0xee, 0xd5, 0x43, // IID379 - 0x62, 0xac, 0x74, 0x18, 0x2b, 0xbc, 0xbd, 0xbc, 0xc2, 0x25, 0x28, // IID380 - 0x62, 0x84, 0x24, 0x14, 0x2b, 0xb4, 0x7d, 0xa7, 0x0d, 0x1f, 0x77, // IID381 - 0x62, 0x44, 0x30, 0x18, 0x33, 0xb4, 0xf1, 0x72, 0x37, 0x29, 0xb5, // IID382 - 0x62, 0xa4, 0x20, 0x1c, 0x33, 0x84, 0xa3, 0xee, 0x23, 0x02, 0x0b, // IID383 - 0x62, 0x04, 0x00, 0x18, 0x32, 0xac, 0x67, 0x97, 0x85, 0xd6, 0xe0, // IID384 - 0x62, 0x6c, 0x74, 0x14, 0x32, 0xb4, 0x5f, 0x45, 0x75, 0xdc, 0x0a, // IID385 - 0x62, 0x74, 0x21, 0x10, 0x33, 0x8c, 0xb2, 0x9f, 0xf0, 0x26, 0xbc, // IID386 - 0x62, 0xcc, 0x61, 0x1c, 0x33, 0xb4, 0x34, 0x4f, 0x5f, 0xcf, 0x82, // IID387 - 0x62, 0x64, 0x0c, 0x18, 0x03, 0xc1, // IID388 - 0xd5, 0x14, 0x03, 0xc1, // IID389 - 0x62, 0x44, 0x2c, 0x14, 0x03, 0xc4, // IID390 - 0x62, 0x6c, 0x3c, 0x14, 0x03, 0xc7, // IID391 - 0x62, 0x4c, 0x14, 0x18, 0x23, 0xd7, // IID392 - 0x45, 0x23, 0xd8, // IID393 - 0x62, 0xc4, 0x74, 0x1c, 0x23, 0xdf, // IID394 - 0x62, 0x54, 0x1c, 0x1c, 0x23, 0xe4, // IID395 - 0x62, 0xec, 0x4c, 0x10, 0xaf, 0xe3, // IID396 - 0x44, 0x0f, 0xaf, 0xc2, // IID397 - 0x62, 0x6c, 0x4c, 0x14, 0xaf, 0xdf, // IID398 - 0x62, 0x7c, 0x34, 0x1c, 0xaf, 0xca, // IID399 - 0x62, 0x44, 0x75, 0x18, 0x0b, 0xf5, // IID400 - 0x66, 0xd5, 0x54, 0x0b, 0xe3, // IID401 - 0x62, 0x4c, 0x1d, 0x1c, 0x0b, 0xf3, // IID402 - 0x62, 0x7c, 0x3d, 0x1c, 0x0b, 0xc6, // IID403 - 0x62, 0xdc, 0x7c, 0x10, 0x0b, 0xce, // IID404 - 0xd5, 0x15, 0x0b, 0xd1, // IID405 - 0x62, 0xec, 0x04, 0x1c, 0x0b, 0xc9, // IID406 - 0x62, 0x5c, 0x34, 0x1c, 0x0b, 0xce, // IID407 - 0x62, 0x7c, 0x5c, 0x10, 0xa5, 0xc5, // IID408 - 0xd5, 0x95, 0xa5, 0xf2, // IID409 - 0x62, 0x74, 0x7c, 0x14, 0xa5, 0xf2, // IID410 - 0x62, 0x7c, 0x64, 0x14, 0xa5, 0xc3, // IID411 - 0x62, 0x64, 0x24, 0x10, 0xad, 0xd3, // IID412 - 0xd5, 0xd1, 0xad, 0xdc, // IID413 - 0x62, 0x54, 0x74, 0x1c, 0xad, 0xf3, // IID414 - 0x62, 0xcc, 0x04, 0x14, 0xad, 0xdf, // IID415 - 0x62, 0x5c, 0x2c, 0x10, 0x2b, 0xe9, // IID416 - 0xd5, 0x45, 0x2b, 0xc3, // IID417 - 0x62, 0xc4, 0x6c, 0x14, 0x2b, 0xe5, // IID418 - 0x62, 0xec, 0x7c, 0x14, 0x2b, 0xc2, // IID419 - 0x62, 0xc4, 0x64, 0x10, 0x33, 0xc8, // IID420 - 0xd5, 0x41, 0x33, 0xdd, // IID421 - 0x62, 0x54, 0x44, 0x14, 0x33, 0xef, // IID422 - 0x62, 0x5c, 0x24, 0x1c, 0x33, 0xdd, // IID423 - 0x62, 0xec, 0x14, 0x10, 0x24, 0xc9, 0x01, // IID424 - 0xd5, 0xd4, 0xa4, 0xc6, 0x04, // IID425 - 0x62, 0x5c, 0x3c, 0x1c, 0x24, 0xdc, 0x10, // IID426 - 0x62, 0xc4, 0x04, 0x1c, 0x24, 0xff, 0x04, // IID427 - 0x62, 0xec, 0x14, 0x10, 0x2c, 0xc6, 0x04, // IID428 - 0x45, 0x0f, 0xac, 0xcd, 0x04, // IID429 - 0x62, 0x7c, 0x04, 0x1c, 0x2c, 0xe5, 0x02, // IID430 - 0x62, 0xec, 0x74, 0x14, 0x2c, 0xf9, 0x02, // IID431 - 0x62, 0xcc, 0x6c, 0x18, 0x40, 0xc5, // IID432 - 0xd5, 0x94, 0x40, 0xd5, // IID433 - 0x62, 0x6c, 0x74, 0x10, 0x41, 0xea, // IID434 - 0xd5, 0xd5, 0x41, 0xe0, // IID435 - 0x62, 0xcc, 0x2c, 0x18, 0x42, 0xe3, // IID436 - 0x45, 0x0f, 0x42, 0xd6, // IID437 - 0x62, 0x64, 0x24, 0x18, 0x43, 0xd9, // IID438 - 0xd5, 0xc1, 0x43, 0xf7, // IID439 - 0x62, 0x6c, 0x04, 0x10, 0x44, 0xf3, // IID440 - 0xd5, 0xd1, 0x44, 0xda, // IID441 - 0x62, 0x5c, 0x54, 0x10, 0x45, 0xf2, // IID442 - 0xd5, 0xc1, 0x45, 0xe7, // IID443 - 0x62, 0x7c, 0x1c, 0x18, 0x46, 0xef, // IID444 - 0xd5, 0xd4, 0x46, 0xe4, // IID445 - 0x62, 0x44, 0x5c, 0x10, 0x47, 0xc3, // IID446 - 0x45, 0x0f, 0x47, 0xd7, // IID447 - 0x62, 0xec, 0x64, 0x10, 0x48, 0xe7, // IID448 - 0xd5, 0x95, 0x48, 0xfa, // IID449 - 0x62, 0x6c, 0x64, 0x10, 0x49, 0xc7, // IID450 - 0xd5, 0xc5, 0x49, 0xe3, // IID451 - 0x44, 0x0f, 0x4a, 0xea, // IID452 - 0xd5, 0xd4, 0x4a, 0xff, // IID453 - 0x62, 0x5c, 0x44, 0x10, 0x4b, 0xcb, // IID454 - 0xd5, 0xd0, 0x4b, 0xec, // IID455 - 0x62, 0xcc, 0x3c, 0x10, 0x4c, 0xed, // IID456 - 0x41, 0x0f, 0x4c, 0xdb, // IID457 - 0x62, 0xf4, 0x54, 0x10, 0x4d, 0xd9, // IID458 - 0xd5, 0xd4, 0x4d, 0xfd, // IID459 - 0x62, 0x4c, 0x04, 0x18, 0x4e, 0xce, // IID460 - 0xd5, 0xd1, 0x4e, 0xf9, // IID461 - 0x62, 0xd4, 0x6c, 0x10, 0x4f, 0xca, // IID462 - 0xd5, 0x91, 0x4f, 0xcf, // IID463 - 0x62, 0xcc, 0x54, 0x10, 0x40, 0x9a, 0x8d, 0xf7, 0xd6, 0x91, // IID464 - 0x62, 0xec, 0x3c, 0x10, 0x41, 0x9c, 0x0e, 0x9a, 0x5f, 0xf8, 0x11, // IID465 - 0x62, 0x6c, 0x74, 0x10, 0x42, 0x84, 0x24, 0x5e, 0x77, 0x4d, 0x53, // IID466 - 0x62, 0xec, 0x5c, 0x10, 0x43, 0x94, 0x24, 0x33, 0xb1, 0x36, 0xb8, // IID467 - 0x62, 0x7c, 0x34, 0x18, 0x44, 0xaf, 0x9d, 0x3a, 0x7c, 0xb4, // IID468 - 0x62, 0x0c, 0x24, 0x18, 0x45, 0x8c, 0x70, 0x51, 0xf8, 0x9a, 0xbb, // IID469 - 0x62, 0x0c, 0x0c, 0x18, 0x46, 0x84, 0xae, 0x1d, 0x66, 0xd0, 0x00, // IID470 - 0x62, 0x04, 0x10, 0x18, 0x47, 0x8c, 0xde, 0x03, 0x14, 0x7e, 0x04, // IID471 - 0x62, 0xe4, 0x3c, 0x10, 0x48, 0x9c, 0xd1, 0xe8, 0xac, 0xb5, 0x9b, // IID472 - 0x62, 0x6c, 0x28, 0x10, 0x49, 0x84, 0x36, 0x46, 0x24, 0x35, 0x70, // IID473 - 0x62, 0x04, 0x60, 0x10, 0x4a, 0x94, 0xb0, 0x5c, 0x2f, 0xa1, 0x78, // IID474 - 0x62, 0x5c, 0x10, 0x10, 0x4b, 0x9c, 0x21, 0x3a, 0x30, 0xa8, 0x27, // IID475 - 0x62, 0x4c, 0x48, 0x10, 0x4c, 0x84, 0x43, 0x10, 0x1a, 0x54, 0x02, // IID476 - 0x62, 0x54, 0x00, 0x10, 0x4d, 0xbc, 0xc0, 0x51, 0x32, 0x8e, 0x55, // IID477 - 0x62, 0x84, 0x24, 0x10, 0x4e, 0x94, 0x10, 0x49, 0x78, 0xe6, 0xb8, // IID478 - 0x62, 0xec, 0x68, 0x10, 0x4f, 0x84, 0x9a, 0xe2, 0x17, 0xf5, 0xed, // IID479 - 0xd5, 0x19, 0x13, 0xdf, // IID480 - 0xd5, 0x5d, 0x3b, 0xf7, // IID481 - 0xd5, 0xdd, 0xaf, 0xec, // IID482 - 0xf3, 0xd5, 0xcd, 0xb8, 0xca, // IID483 - 0xd5, 0x5c, 0x1b, 0xc4, // IID484 - 0xd5, 0x48, 0x2b, 0xc2, // IID485 - 0xf3, 0xd5, 0xdd, 0xbc, 0xd4, // IID486 - 0xf3, 0xd5, 0xcd, 0xbd, 0xe1, // IID487 - 0xd5, 0x59, 0x03, 0xe0, // IID488 - 0xd5, 0x5d, 0x23, 0xc5, // IID489 - 0xd5, 0x59, 0x0b, 0xfb, // IID490 - 0x4d, 0x33, 0xfc, // IID491 - 0xd5, 0x58, 0x8b, 0xd3, // IID492 - 0xd5, 0xcc, 0xbc, 0xf9, // IID493 - 0x4d, 0x0f, 0xbd, 0xcd, // IID494 - 0xd5, 0x98, 0xa3, 0xcc, // IID495 - 0xd5, 0x1c, 0x87, 0xc5, // IID496 - 0xd5, 0x4d, 0x85, 0xc6, // IID497 - 0xd5, 0x6c, 0x01, 0xac, 0xb9, 0x4d, 0x6c, 0xf0, 0x4f, // IID498 - 0xd5, 0x5f, 0x21, 0x94, 0x50, 0x77, 0x5e, 0x26, 0x8a, // IID499 - 0xd5, 0x48, 0x39, 0x8c, 0x1b, 0x9c, 0xd5, 0x33, 0x40, // IID500 - 0xd5, 0x5a, 0x09, 0x94, 0xe6, 0x83, 0xcb, 0x6c, 0xc7, // IID501 - 0xd5, 0x3c, 0x31, 0xa4, 0xfc, 0x60, 0x15, 0x31, 0x4b, // IID502 - 0xd5, 0x6f, 0x29, 0xac, 0xa2, 0x57, 0x26, 0x3a, 0x5c, // IID503 - 0xd5, 0x6b, 0x89, 0xb4, 0xcd, 0x3f, 0x6f, 0x3d, 0x1a, // IID504 - 0xd5, 0xfe, 0xc1, 0x8c, 0xc1, 0x28, 0x24, 0x52, 0xca, // IID505 - 0xd5, 0x19, 0x81, 0xa1, 0xc3, 0x84, 0x21, 0x63, 0x00, 0x00, 0x00, 0x01, // IID506 - 0x4b, 0x81, 0x84, 0x2d, 0x3a, 0x15, 0x8d, 0xc6, 0x00, 0x00, 0x00, 0x01, // IID507 - 0x49, 0x81, 0xb9, 0xfa, 0x37, 0x4b, 0xec, 0x00, 0x10, 0x00, 0x00, // IID508 - 0xd5, 0x19, 0xd1, 0xbf, 0x51, 0xf5, 0xa7, 0x4f, // IID509 - 0xd5, 0x3a, 0xd1, 0xa4, 0xbd, 0x32, 0x82, 0xaa, 0x31, // IID510 - 0xd5, 0x3b, 0x81, 0x9c, 0xb8, 0x49, 0xc7, 0x9a, 0xb9, 0x00, 0x00, 0x00, 0x10, // IID511 - 0xd5, 0x39, 0xc1, 0xac, 0x34, 0x4f, 0x7a, 0x01, 0xc1, 0x02, // IID512 - 0xd5, 0x18, 0x81, 0xa8, 0x15, 0x5c, 0x76, 0xec, 0x00, 0x00, 0x10, 0x00, // IID513 - 0xd5, 0x1b, 0x83, 0xb4, 0x05, 0x15, 0x26, 0x02, 0x1d, 0x10, // IID514 - 0xd5, 0x2b, 0x83, 0x8c, 0x64, 0x1e, 0x67, 0x37, 0xcb, 0x01, // IID515 - 0xd5, 0x2a, 0xc7, 0x84, 0x81, 0xf8, 0x14, 0xbb, 0xe9, 0x00, 0x01, 0x00, 0x00, // IID516 - 0xd5, 0x19, 0xf7, 0x85, 0xf5, 0x76, 0xdc, 0x82, 0x00, 0x00, 0xff, 0xff, // IID517 - 0xd5, 0x68, 0x03, 0xbc, 0x99, 0x54, 0xc6, 0xea, 0x70, // IID518 - 0xd5, 0x1b, 0x23, 0x94, 0x38, 0x57, 0x25, 0xb2, 0xdf, // IID519 - 0xd5, 0x1a, 0x3b, 0x94, 0xdf, 0xbd, 0x30, 0xc9, 0x32, // IID520 - 0xf3, 0xd5, 0xcc, 0xbd, 0xa2, 0x71, 0x3d, 0xcc, 0xab, // IID521 - 0xd5, 0x5a, 0x0b, 0xb4, 0x73, 0xc8, 0x82, 0x39, 0xd3, // IID522 - 0x4d, 0x13, 0x92, 0x9f, 0xc5, 0xd7, 0x03, // IID523 - 0x4f, 0x0f, 0xaf, 0x94, 0xc0, 0xd3, 0x79, 0x9e, 0xf1, // IID524 - 0xf3, 0xd5, 0xd9, 0xb8, 0xbd, 0x93, 0x26, 0x81, 0x88, // IID525 - 0xd5, 0x28, 0x1b, 0x8c, 0x5b, 0xcb, 0x01, 0xc6, 0x53, // IID526 - 0xd5, 0x1c, 0x2b, 0xb4, 0x19, 0x8d, 0x0f, 0x74, 0x89, // IID527 - 0xf3, 0xd5, 0xed, 0xbc, 0xac, 0x5a, 0x3c, 0x8d, 0xc9, 0x30, // IID528 - 0xd5, 0x3e, 0x33, 0x94, 0x18, 0xfe, 0x29, 0xf7, 0xc2, // IID529 - 0xd5, 0x7b, 0x8b, 0x94, 0xe4, 0x6f, 0x53, 0x04, 0x9d, // IID530 - 0x48, 0x8d, 0x99, 0xa5, 0x02, 0x06, 0x45, // IID531 - 0xf2, 0xd5, 0xbf, 0x2c, 0xa4, 0x3e, 0xd0, 0x59, 0x67, 0x98, // IID532 - 0xd5, 0x5f, 0x87, 0xbc, 0x50, 0x13, 0xed, 0x98, 0x8f, // IID533 - 0xd5, 0x2d, 0x85, 0xb4, 0xe5, 0xf2, 0x81, 0x10, 0x17, // IID534 - 0xd5, 0x19, 0x83, 0xc7, 0x10, // IID535 - 0xd5, 0x19, 0x83, 0xe1, 0x10, // IID536 - 0xd5, 0x18, 0x81, 0xd7, 0x00, 0x01, 0x00, 0x00, // IID537 - 0xd5, 0x18, 0x81, 0xfb, 0x00, 0x00, 0x00, 0x10, // IID538 - 0xd5, 0x19, 0xd1, 0xd7, // IID539 - 0xd5, 0x18, 0xd1, 0xd9, // IID540 - 0xd5, 0x19, 0xc1, 0xc1, 0x02, // IID541 - 0xd5, 0x18, 0xc1, 0xc9, 0x04, // IID542 - 0xd5, 0x19, 0xd1, 0xfc, // IID543 - 0x49, 0xc1, 0xe7, 0x04, // IID544 - 0x48, 0x81, 0xdb, 0x00, 0x00, 0x01, 0x00, // IID545 - 0xd5, 0x18, 0xd1, 0xe5, // IID546 - 0x49, 0xd1, 0xea, // IID547 - 0x49, 0x83, 0xee, 0x10, // IID548 - 0xd5, 0x18, 0x81, 0xf2, 0x00, 0x00, 0x00, 0x10, // IID549 - 0xd5, 0x18, 0xc7, 0xc7, 0x10, 0x00, 0x00, 0x00, // IID550 - 0x49, 0xbc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // IID551 - 0x49, 0x0f, 0xba, 0xe6, 0x04, // IID552 - 0xd5, 0x19, 0xf7, 0xc0, 0x00, 0xf0, 0xff, 0xff, // IID553 - 0xd5, 0x18, 0x81, 0xcb, 0x00, 0x00, 0x10, 0x00, // IID554 - 0x48, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x10, // IID555 - 0xd5, 0x98, 0x40, 0x94, 0xdb, 0xc4, 0xc8, 0x11, 0x02, // IID556 - 0xd5, 0x98, 0x41, 0x9d, 0x43, 0x77, 0x26, 0x49, // IID557 - 0xd5, 0xeb, 0x42, 0xac, 0x60, 0xba, 0xd6, 0x73, 0xb3, // IID558 - 0xd5, 0xbd, 0x43, 0xa4, 0x22, 0x64, 0x07, 0xb2, 0xd9, // IID559 - 0xd5, 0xdb, 0x44, 0x8c, 0x8c, 0x6b, 0x19, 0x97, 0x34, // IID560 - 0xd5, 0xad, 0x45, 0xac, 0x7f, 0x67, 0xf6, 0x5c, 0xd8, // IID561 - 0xd5, 0xd8, 0x46, 0xb6, 0x05, 0xab, 0x39, 0x0f, // IID562 - 0xd5, 0xba, 0x47, 0x8c, 0xd6, 0xb4, 0x6a, 0x73, 0xfb, // IID563 - 0xd5, 0xfc, 0x48, 0x8c, 0x2b, 0x0b, 0x5b, 0x40, 0x0e, // IID564 - 0xd5, 0xbe, 0x49, 0xa4, 0xeb, 0xb5, 0xfb, 0x9d, 0x88, // IID565 - 0xd5, 0x9b, 0x4a, 0x9c, 0x56, 0xdd, 0x7c, 0x86, 0xe6, // IID566 - 0xd5, 0xfb, 0x4b, 0xac, 0x38, 0xd5, 0x9a, 0xce, 0xa8, // IID567 - 0xd5, 0xcb, 0x4c, 0x94, 0x50, 0x77, 0x41, 0xec, 0xa9, // IID568 - 0xd5, 0xfc, 0x4d, 0xa4, 0xed, 0xc4, 0xfd, 0xa0, 0x65, // IID569 - 0xd5, 0xe9, 0x4e, 0xbc, 0x13, 0xf4, 0x0e, 0xe5, 0xe2, // IID570 - 0xd5, 0xda, 0x4f, 0xb4, 0x62, 0x38, 0x1c, 0x5f, 0x1a, // IID571 - 0xd5, 0x10, 0xff, 0xd7, // IID572 - 0xd5, 0x19, 0xf7, 0xf6, // IID573 - 0xd5, 0x18, 0xf7, 0xfb, // IID574 - 0x49, 0xf7, 0xe9, // IID575 - 0x49, 0xf7, 0xe5, // IID576 - 0xd5, 0x18, 0xf7, 0xd8, // IID577 - 0xd5, 0x19, 0xf7, 0xd5, // IID578 - 0x48, 0xd3, 0xc1, // IID579 - 0xd5, 0x19, 0xd3, 0xc9, // IID580 - 0x49, 0xd3, 0xf8, // IID581 - 0xd5, 0x19, 0xd3, 0xe3, // IID582 - 0xd5, 0x19, 0xd3, 0xe6, // IID583 - 0xd5, 0x18, 0xd3, 0xef, // IID584 - 0x48, 0xff, 0xc3, // IID585 - 0x49, 0xff, 0xce, // IID586 - 0xd5, 0x18, 0x55, // IID587 - 0xd5, 0x18, 0x5d, // IID588 - 0xd5, 0x30, 0xff, 0x94, 0x6c, 0x2f, 0xaf, 0xc6, 0x56, // IID589 - 0xd5, 0x39, 0xf7, 0xa4, 0xdf, 0xdd, 0x14, 0x4b, 0xfe, // IID590 - 0xd5, 0x3b, 0xf7, 0x9c, 0x1b, 0xe1, 0x03, 0x24, 0xa7, // IID591 - 0xd5, 0x28, 0xd3, 0xbc, 0xb3, 0x2f, 0xb6, 0x9c, 0x9f, // IID592 - 0xd5, 0x39, 0xd3, 0xa4, 0xfa, 0x79, 0xa0, 0x95, 0x0b, // IID593 - 0xd5, 0x2b, 0xd3, 0xac, 0x16, 0x09, 0x4e, 0x54, 0x03, // IID594 - 0xd5, 0x19, 0xff, 0x84, 0x13, 0x50, 0x32, 0x0b, 0x12, // IID595 - 0xd5, 0x2b, 0xff, 0x8c, 0x89, 0x35, 0x13, 0x55, 0xcb, // IID596 - 0xd5, 0x7a, 0x69, 0xa4, 0x60, 0x5b, 0xfa, 0x21, 0xa6, 0x00, 0x00, 0x10, 0x00, // IID597 - 0xd5, 0x58, 0x69, 0xcf, 0x00, 0x01, 0x00, 0x00, // IID598 - 0xd5, 0x9c, 0xa4, 0xdb, 0x08, // IID599 - 0xd5, 0x9d, 0xac, 0xd4, 0x08, // IID600 - 0x62, 0xdc, 0x2c, 0x10, 0x8f, 0xc5, // IID601 - 0x62, 0xfc, 0xac, 0x18, 0x8f, 0xc6, // IID602 - 0x62, 0xdc, 0x0c, 0x10, 0xff, 0xf1, // IID603 - 0x62, 0xdc, 0x84, 0x18, 0xff, 0xf4, // IID604 - 0xd5, 0xbd, 0xb6, 0x9c, 0x9d, 0xcc, 0x72, 0xc9, 0xed, // IID605 - 0xd5, 0xaf, 0xb7, 0xb4, 0xb0, 0x22, 0x6d, 0x6c, 0xb5, // IID606 - 0xd5, 0xde, 0xbe, 0xa4, 0x3f, 0x54, 0xcb, 0x89, 0x61, // IID607 - 0xd5, 0xec, 0xbf, 0xa4, 0xfb, 0x9f, 0x9a, 0x17, 0xd2, // IID608 - 0x4c, 0x0f, 0xb6, 0xd9, // IID609 - 0xd5, 0xcd, 0xb7, 0xf7, // IID610 - 0x4c, 0x0f, 0xbe, 0xf1, // IID611 - 0xd5, 0xc9, 0xbf, 0xf9, // IID612 - 0x4f, 0x0f, 0xb1, 0xa4, 0x55, 0xc6, 0xd3, 0x39, 0xf8, // IID613 - 0x62, 0xf4, 0xfc, 0x08, 0xf7, 0xf9, // IID614 - 0x62, 0xd4, 0xfc, 0x0c, 0xf7, 0xff, // IID615 - 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xf7, // IID616 - 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xf0, // IID617 - 0x62, 0xdc, 0xfc, 0x08, 0xf7, 0xeb, // IID618 - 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xee, // IID619 - 0x62, 0xd4, 0xfc, 0x08, 0xf7, 0xe4, // IID620 - 0x62, 0xf4, 0xfc, 0x0c, 0xf7, 0xe1, // IID621 - 0x62, 0x94, 0xfc, 0x08, 0xf7, 0xa4, 0xcd, 0x6c, 0x54, 0x95, 0xdd, // IID622 - 0x62, 0x94, 0xf8, 0x0c, 0xf7, 0xa4, 0xc5, 0xfb, 0x89, 0x93, 0xd7, // IID623 - 0x62, 0xcc, 0xfc, 0x08, 0xaf, 0xee, // IID624 - 0xd5, 0x18, 0xf7, 0xe9, // IID625 - 0x62, 0x44, 0xfc, 0x0c, 0xaf, 0xec, // IID626 - 0x62, 0x4c, 0xfc, 0x0c, 0xaf, 0xf6, // IID627 - 0x62, 0x44, 0xfc, 0x08, 0xf5, 0xc7, // IID628 - 0x62, 0x4c, 0xfc, 0x08, 0xf5, 0xc9, // IID629 - 0x62, 0x6c, 0xfc, 0x0c, 0xf5, 0xcd, // IID630 - 0x62, 0xec, 0xfc, 0x0c, 0xf5, 0xf6, // IID631 - 0x62, 0xdc, 0xf4, 0x10, 0xf7, 0xde, // IID632 - 0xd5, 0x18, 0xf7, 0xd9, // IID633 - 0x62, 0xfc, 0x84, 0x14, 0xf7, 0xd9, // IID634 - 0x62, 0xdc, 0x94, 0x14, 0xf7, 0xdd, // IID635 - 0x62, 0xd4, 0xac, 0x18, 0xf7, 0xd1, // IID636 - 0xd5, 0x19, 0xf7, 0xd0, // IID637 - 0x62, 0x44, 0xfc, 0x08, 0x88, 0xe7, // IID638 - 0x62, 0x54, 0xfc, 0x08, 0x88, 0xd2, // IID639 - 0x62, 0x4c, 0xfc, 0x0c, 0x88, 0xde, // IID640 - 0x62, 0x4c, 0xfc, 0x0c, 0x88, 0xe4, // IID641 - 0x62, 0xd4, 0x9c, 0x10, 0xd3, 0xc6, // IID642 - 0xd5, 0x18, 0xd3, 0xc7, // IID643 - 0x62, 0xdc, 0xc4, 0x14, 0xd3, 0xc0, // IID644 - 0x62, 0xfc, 0xd4, 0x14, 0xd3, 0xc5, // IID645 - 0x62, 0xfc, 0x84, 0x10, 0xd3, 0xce, // IID646 - 0xd5, 0x19, 0xd3, 0xcc, // IID647 - 0x62, 0xd4, 0xf4, 0x14, 0xd3, 0xca, // IID648 - 0x62, 0xd4, 0xb4, 0x1c, 0xd3, 0xc9, // IID649 - 0x62, 0xdc, 0x94, 0x10, 0xd3, 0xe6, // IID650 - 0x49, 0xd3, 0xe3, // IID651 - 0x62, 0xd4, 0xac, 0x14, 0xd3, 0xe3, // IID652 - 0x62, 0xfc, 0xfc, 0x14, 0xd3, 0xe0, // IID653 - 0x62, 0xd4, 0xe4, 0x18, 0xd3, 0xff, // IID654 - 0x49, 0xd3, 0xfe, // IID655 - 0x62, 0xfc, 0xb4, 0x14, 0xd3, 0xf8, // IID656 - 0x62, 0xd4, 0xbc, 0x1c, 0xd3, 0xf8, // IID657 - 0x62, 0xd4, 0xa4, 0x18, 0xff, 0xcd, // IID658 - 0x48, 0xff, 0xc9, // IID659 - 0x62, 0xfc, 0xd4, 0x14, 0xff, 0xca, // IID660 - 0x62, 0xdc, 0x9c, 0x14, 0xff, 0xcc, // IID661 - 0xd5, 0x18, 0xff, 0xc0, // IID662 - 0xd5, 0x19, 0xff, 0xc5, // IID663 - 0x62, 0xd4, 0xec, 0x14, 0xff, 0xc1, // IID664 - 0x62, 0xfc, 0xe4, 0x14, 0xff, 0xc3, // IID665 - 0x62, 0xfc, 0xe4, 0x10, 0xd3, 0xe2, // IID666 - 0x49, 0xd3, 0xe0, // IID667 - 0x62, 0xd4, 0x9c, 0x1c, 0xd3, 0xe7, // IID668 - 0x62, 0xdc, 0x94, 0x14, 0xd3, 0xe5, // IID669 - 0x62, 0xdc, 0x9c, 0x10, 0xd3, 0xe8, // IID670 - 0xd5, 0x18, 0xd3, 0xeb, // IID671 - 0x62, 0xdc, 0xbc, 0x1c, 0xd3, 0xec, // IID672 - 0x62, 0xfc, 0xf4, 0x14, 0xd3, 0xe9, // IID673 - 0x62, 0x6c, 0xfc, 0x08, 0xf4, 0xe0, // IID674 - 0x62, 0x54, 0xfc, 0x08, 0xf4, 0xf6, // IID675 - 0x62, 0x5c, 0xfc, 0x0c, 0xf4, 0xe7, // IID676 - 0x62, 0x54, 0xfc, 0x0c, 0xf4, 0xf6, // IID677 - 0x62, 0x44, 0xfc, 0x08, 0xaf, 0xbd, 0xae, 0x4c, 0x3b, 0x96, // IID678 - 0x62, 0xec, 0xfc, 0x0c, 0xaf, 0x8a, 0xfb, 0xee, 0x54, 0x9f, // IID679 - 0x62, 0x04, 0xf8, 0x08, 0xf5, 0x9c, 0x8e, 0x83, 0xbf, 0x98, 0x27, // IID680 - 0x62, 0x84, 0xfc, 0x0c, 0xf5, 0xbc, 0x1a, 0xa3, 0x9c, 0x71, 0xc8, // IID681 - 0x62, 0xbc, 0xf4, 0x18, 0xf7, 0x9c, 0xcb, 0xc0, 0x2b, 0xb8, 0x97, // IID682 - 0x62, 0xf4, 0xf4, 0x1c, 0xf7, 0x9c, 0x0b, 0x8d, 0xd3, 0x92, 0x6f, // IID683 - 0x62, 0xc4, 0xfc, 0x08, 0x88, 0xa4, 0x24, 0x2a, 0xd8, 0x74, 0xd5, // IID684 - 0x62, 0x4c, 0xfc, 0x0c, 0x88, 0xbe, 0xd0, 0xf6, 0x03, 0x46, // IID685 - 0x62, 0xdc, 0xe4, 0x18, 0xd3, 0xa0, 0xf9, 0x06, 0x7d, 0x56, // IID686 - 0x62, 0x9c, 0x98, 0x1c, 0xd3, 0xa4, 0x20, 0xb2, 0xa7, 0xb3, 0xe3, // IID687 - 0x62, 0xbc, 0x98, 0x18, 0xd3, 0xbc, 0x87, 0x46, 0x43, 0xa8, 0xce, // IID688 - 0x62, 0x94, 0xb8, 0x1c, 0xd3, 0xbc, 0x86, 0x5b, 0x6f, 0xbd, 0x8e, // IID689 - 0x62, 0x94, 0xc4, 0x10, 0xff, 0x8c, 0x78, 0x23, 0x8d, 0x1d, 0xa5, // IID690 - 0x62, 0x9c, 0x94, 0x1c, 0xff, 0x8c, 0xcd, 0x57, 0x8b, 0xae, 0xa4, // IID691 - 0x62, 0xbc, 0xa0, 0x18, 0xff, 0x84, 0xfd, 0x24, 0x4b, 0x89, 0xde, // IID692 - 0x62, 0xf4, 0x90, 0x1c, 0xff, 0x84, 0x01, 0x37, 0xb7, 0x4b, 0xc9, // IID693 - 0x62, 0xdc, 0xac, 0x10, 0xd3, 0xac, 0x89, 0x6d, 0xb6, 0x76, 0xa0, // IID694 - 0x62, 0xd4, 0xb4, 0x14, 0xd3, 0xa9, 0x21, 0x8d, 0x79, 0x51, // IID695 - 0x62, 0x04, 0xf8, 0x08, 0xf4, 0xa4, 0x95, 0xf6, 0x96, 0x71, 0x20, // IID696 - 0x62, 0xbc, 0xfc, 0x0c, 0xf4, 0x9c, 0x2b, 0x2b, 0xc8, 0x26, 0xdb, // IID697 - 0x62, 0x4c, 0xf4, 0x10, 0x01, 0xbe, 0xff, 0xcc, 0x35, 0x39, // IID698 - 0x62, 0x1c, 0x8c, 0x18, 0x01, 0xb4, 0x93, 0x55, 0x64, 0x52, 0xcb, // IID699 - 0x62, 0x6c, 0xe8, 0x14, 0x01, 0xb4, 0x3c, 0x4b, 0xed, 0xd3, 0x5a, // IID700 - 0x62, 0xe4, 0xdc, 0x14, 0x01, 0xa2, 0x1b, 0x66, 0xd5, 0xcd, // IID701 - 0x62, 0xa4, 0x80, 0x10, 0x21, 0x8c, 0xdb, 0xd2, 0x47, 0xe2, 0x4c, // IID702 - 0x62, 0x6c, 0x88, 0x10, 0x21, 0xb4, 0x5a, 0xec, 0xc2, 0x11, 0xfb, // IID703 - 0x62, 0x44, 0x9c, 0x14, 0x21, 0x84, 0xdb, 0x41, 0xb4, 0x66, 0xd7, // IID704 - 0x62, 0x6c, 0x8c, 0x14, 0x21, 0xb6, 0x24, 0x1c, 0xd2, 0x07, // IID705 - 0x62, 0xc4, 0xa8, 0x10, 0x09, 0xa4, 0xdf, 0x92, 0x17, 0xc2, 0x58, // IID706 - 0x62, 0x14, 0x90, 0x18, 0x09, 0xac, 0x9a, 0xcd, 0x2c, 0x8f, 0xd3, // IID707 - 0x62, 0x44, 0xe4, 0x1c, 0x09, 0x94, 0x1c, 0x44, 0x0e, 0x4f, 0xe0, // IID708 - 0x62, 0x0c, 0x80, 0x14, 0x09, 0xbc, 0x7b, 0x56, 0x17, 0x8d, 0x02, // IID709 - 0x62, 0x4c, 0xb8, 0x10, 0x29, 0x9c, 0x7c, 0x10, 0xf6, 0x80, 0x69, // IID710 - 0x62, 0x14, 0x80, 0x18, 0x29, 0xbc, 0xf3, 0x19, 0x88, 0x68, 0xfb, // IID711 - 0x62, 0x0c, 0xf4, 0x14, 0x29, 0xbc, 0xa9, 0x46, 0x9e, 0x61, 0x31, // IID712 - 0x62, 0x84, 0xec, 0x14, 0x29, 0x94, 0x93, 0x1a, 0x86, 0x22, 0x19, // IID713 - 0x62, 0xc4, 0xe4, 0x18, 0x31, 0xab, 0xe0, 0x2b, 0xe9, 0xb8, // IID714 - 0x62, 0x34, 0xbc, 0x18, 0x31, 0x84, 0x8a, 0x64, 0x1c, 0x30, 0xfb, // IID715 - 0x62, 0x04, 0xf8, 0x14, 0x31, 0x8c, 0x1e, 0xd9, 0x54, 0x66, 0x7c, // IID716 - 0x62, 0x44, 0x94, 0x14, 0x31, 0xaf, 0x87, 0x4b, 0x05, 0xa1, // IID717 - 0x62, 0xd4, 0xe0, 0x10, 0x81, 0x84, 0xb5, 0x59, 0x45, 0xb6, 0x68, 0x00, 0x00, 0x00, 0x01, // IID718 - 0x62, 0x94, 0xf8, 0x14, 0x83, 0x84, 0xfd, 0x0b, 0xc5, 0xeb, 0x9a, 0x01, // IID719 - 0x62, 0x9c, 0x84, 0x10, 0x83, 0xa4, 0x68, 0xf2, 0x95, 0x4e, 0xda, 0x01, // IID720 - 0x62, 0xdc, 0xa4, 0x1c, 0x81, 0xa4, 0x24, 0xda, 0xb4, 0x92, 0xf0, 0x00, 0x00, 0x01, 0x00, // IID721 - 0x62, 0xbc, 0xfc, 0x08, 0x69, 0x8c, 0x12, 0xa1, 0x6d, 0xec, 0x46, 0x00, 0x00, 0x00, 0x01, // IID722 - 0x62, 0x14, 0xfc, 0x0c, 0x6b, 0xbc, 0xd1, 0x0d, 0x95, 0x3c, 0x80, 0x10, // IID723 - 0x62, 0x9c, 0xf0, 0x10, 0x83, 0x8c, 0x33, 0x2c, 0xda, 0x4c, 0x1b, 0x01, // IID724 - 0x62, 0x9c, 0xec, 0x1c, 0x81, 0x8c, 0xb1, 0x7b, 0x94, 0x55, 0xa6, 0x00, 0x10, 0x00, 0x00, // IID725 - 0x62, 0xdc, 0xf0, 0x10, 0xc1, 0xa4, 0x6a, 0xa1, 0x0e, 0x4e, 0x95, 0x08, // IID726 - 0x62, 0xfc, 0x98, 0x1c, 0xc1, 0xa4, 0x0e, 0x55, 0xeb, 0x53, 0xbc, 0x02, // IID727 - 0x62, 0xfc, 0x90, 0x10, 0xc1, 0xbc, 0x02, 0x9f, 0xf3, 0x23, 0xa6, 0x04, // IID728 - 0x62, 0xd4, 0xfc, 0x14, 0xc1, 0xbb, 0xec, 0x2c, 0x42, 0xf8, 0x04, // IID729 - 0x62, 0xbc, 0xa8, 0x10, 0xc1, 0xac, 0xdf, 0xec, 0xdc, 0x46, 0xaa, 0x10, // IID730 - 0x62, 0xbc, 0xc0, 0x14, 0xc1, 0xac, 0x68, 0x1d, 0x1a, 0x31, 0x71, 0x02, // IID731 - 0x62, 0xd4, 0xb4, 0x10, 0x81, 0xa9, 0x54, 0xd4, 0xac, 0xf6, 0x00, 0x00, 0x10, 0x00, // IID732 - 0x62, 0xd4, 0xf0, 0x14, 0x81, 0xac, 0x38, 0xa2, 0x6c, 0xd0, 0x55, 0x00, 0x00, 0x10, 0x00, // IID733 - 0x62, 0x94, 0x90, 0x10, 0x81, 0xb4, 0x01, 0x3f, 0xbe, 0x3e, 0xfd, 0x00, 0x00, 0x10, 0x00, // IID734 - 0x62, 0xfc, 0x98, 0x14, 0x83, 0xb4, 0x5e, 0x43, 0x65, 0x62, 0xd2, 0x10, // IID735 - 0x62, 0xd4, 0xcc, 0x10, 0x83, 0xc6, 0x10, // IID736 - 0x62, 0xd4, 0xfc, 0x18, 0x83, 0xc4, 0x10, // IID737 - 0xd5, 0x19, 0x81, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID738 - 0x62, 0xf4, 0xd4, 0x14, 0x81, 0xc3, 0x00, 0x00, 0x01, 0x00, // IID739 - 0x62, 0xf4, 0xfc, 0x1c, 0x81, 0xc3, 0x00, 0x00, 0x01, 0x00, // IID740 - 0x62, 0xdc, 0xbc, 0x14, 0x81, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID741 - 0x62, 0xdc, 0xd4, 0x10, 0x81, 0xe3, 0x00, 0x00, 0x00, 0x01, // IID742 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xe3, 0x00, 0x00, 0x00, 0x01, // IID743 - 0xd5, 0x19, 0x81, 0xe0, 0x00, 0x00, 0x01, 0x00, // IID744 - 0x62, 0xdc, 0x94, 0x1c, 0x81, 0xe7, 0x00, 0x00, 0x10, 0x00, // IID745 - 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xe5, 0x00, 0x00, 0x10, 0x00, // IID746 - 0x62, 0xdc, 0x8c, 0x14, 0x81, 0xe6, 0x00, 0x00, 0x10, 0x00, // IID747 - 0x62, 0x54, 0xfc, 0x08, 0x69, 0xc5, 0x00, 0x00, 0x00, 0x10, // IID748 - 0x62, 0xdc, 0xfc, 0x08, 0x69, 0xc7, 0x00, 0x00, 0x00, 0x10, // IID749 - 0x62, 0x54, 0xfc, 0x08, 0x69, 0xed, 0x00, 0x00, 0x01, 0x00, // IID750 - 0x62, 0x5c, 0xfc, 0x0c, 0x69, 0xf5, 0x00, 0x00, 0x10, 0x00, // IID751 - 0x62, 0xfc, 0xfc, 0x0c, 0x69, 0xc6, 0x00, 0x00, 0x10, 0x00, // IID752 - 0x62, 0x54, 0xfc, 0x0c, 0x69, 0xc0, 0x00, 0x00, 0x00, 0x10, // IID753 - 0x62, 0xd4, 0x8c, 0x10, 0x81, 0xcf, 0x00, 0x10, 0x00, 0x00, // IID754 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xcc, 0x00, 0x10, 0x00, 0x00, // IID755 - 0xd5, 0x19, 0x81, 0xca, 0x00, 0x00, 0x10, 0x00, // IID756 - 0x62, 0xd4, 0xfc, 0x14, 0x81, 0xcc, 0x00, 0x00, 0x00, 0x10, // IID757 - 0x62, 0xd4, 0xfc, 0x1c, 0x81, 0xc9, 0x00, 0x00, 0x00, 0x10, // IID758 - 0x62, 0xfc, 0xc4, 0x14, 0x81, 0xcf, 0x00, 0x01, 0x00, 0x00, // IID759 - 0x62, 0xd4, 0x84, 0x18, 0xc1, 0xd1, 0x10, // IID760 - 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xd0, 0x10, // IID761 - 0xd5, 0x19, 0xd1, 0xd1, // IID762 - 0x62, 0xfc, 0xb4, 0x18, 0xc1, 0xc1, 0x10, // IID763 - 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xc4, 0x10, // IID764 - 0xd5, 0x19, 0xd1, 0xc3, // IID765 - 0x62, 0xdc, 0xdc, 0x14, 0xd1, 0xc7, // IID766 - 0x62, 0xfc, 0xfc, 0x1c, 0xd1, 0xc2, // IID767 - 0x62, 0xdc, 0x9c, 0x14, 0xc1, 0xc4, 0x10, // IID768 - 0x62, 0xfc, 0xac, 0x10, 0xc1, 0xca, 0x10, // IID769 - 0x62, 0xdc, 0xfc, 0x18, 0xc1, 0xc8, 0x10, // IID770 - 0xd5, 0x18, 0xc1, 0xce, 0x10, // IID771 - 0x62, 0xdc, 0xa4, 0x14, 0xd1, 0xcd, // IID772 - 0x62, 0xfc, 0xfc, 0x1c, 0xd1, 0xca, // IID773 - 0x62, 0xfc, 0xd4, 0x14, 0xd1, 0xcd, // IID774 - 0x62, 0xf4, 0x9c, 0x18, 0xc1, 0xe1, 0x02, // IID775 - 0x62, 0xdc, 0xfc, 0x18, 0xc1, 0xe0, 0x02, // IID776 - 0xd5, 0x18, 0xc1, 0xe6, 0x08, // IID777 - 0x62, 0xfc, 0xf4, 0x14, 0xc1, 0xe7, 0x08, // IID778 - 0x62, 0xdc, 0xfc, 0x1c, 0xc1, 0xe3, 0x08, // IID779 - 0x62, 0xfc, 0xc4, 0x14, 0xd1, 0xe7, // IID780 - 0x62, 0xdc, 0xbc, 0x18, 0xc1, 0xf9, 0x10, // IID781 - 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xff, 0x10, // IID782 - 0x49, 0xc1, 0xf9, 0x04, // IID783 - 0x62, 0xd4, 0xcc, 0x14, 0xd1, 0xfd, // IID784 - 0x62, 0xd4, 0xfc, 0x1c, 0xd1, 0xfb, // IID785 - 0x62, 0xd4, 0x9c, 0x1c, 0xc1, 0xfc, 0x02, // IID786 - 0x62, 0xdc, 0xf4, 0x18, 0xc1, 0xe6, 0x08, // IID787 - 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xe3, 0x08, // IID788 - 0x49, 0xc1, 0xe5, 0x02, // IID789 - 0x62, 0xd4, 0xec, 0x14, 0xc1, 0xe3, 0x08, // IID790 - 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xe1, 0x08, // IID791 - 0x62, 0xf4, 0xf4, 0x1c, 0xc1, 0xe1, 0x10, // IID792 - 0x62, 0xfc, 0xac, 0x18, 0xc1, 0xee, 0x04, // IID793 - 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xe9, 0x04, // IID794 - 0x49, 0xc1, 0xec, 0x02, // IID795 - 0x62, 0xdc, 0xac, 0x14, 0xc1, 0xef, 0x08, // IID796 - 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xec, 0x08, // IID797 - 0x62, 0xdc, 0x9c, 0x14, 0xd1, 0xec, // IID798 - 0x62, 0xdc, 0x84, 0x18, 0x81, 0xee, 0x00, 0x00, 0x01, 0x00, // IID799 - 0x62, 0xf4, 0xfc, 0x18, 0x81, 0xe9, 0x00, 0x00, 0x01, 0x00, // IID800 - 0xd5, 0x19, 0x83, 0xea, 0x10, // IID801 - 0x62, 0xd4, 0x9c, 0x1c, 0x83, 0xee, 0x01, // IID802 - 0x62, 0xfc, 0xfc, 0x1c, 0x83, 0xed, 0x01, // IID803 - 0x62, 0xfc, 0xdc, 0x14, 0x81, 0xec, 0x00, 0x00, 0x10, 0x00, // IID804 - 0x62, 0xf4, 0xa4, 0x18, 0x81, 0xf3, 0x00, 0x00, 0x00, 0x01, // IID805 - 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xf7, 0x00, 0x00, 0x00, 0x01, // IID806 - 0xd5, 0x19, 0x81, 0xf7, 0x00, 0x00, 0x00, 0x10, // IID807 - 0x62, 0xdc, 0x94, 0x14, 0x81, 0xf4, 0x00, 0x10, 0x00, 0x00, // IID808 - 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xf3, 0x00, 0x10, 0x00, 0x00, // IID809 - 0x62, 0xf4, 0xec, 0x1c, 0x81, 0xf2, 0x00, 0x00, 0x00, 0x10, // IID810 - 0x48, 0x81, 0xca, 0x00, 0x00, 0x10, 0x00, // IID811 - 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xce, 0x00, 0x00, 0x10, 0x00, // IID812 - 0xd5, 0x19, 0x81, 0xcd, 0x00, 0x00, 0x10, 0x00, // IID813 - 0x62, 0xf4, 0xf4, 0x10, 0x81, 0xc9, 0x00, 0x00, 0x40, 0x00, // IID814 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xc9, 0x00, 0x00, 0x40, 0x00, // IID815 - 0xd5, 0x19, 0x81, 0xcb, 0x00, 0x00, 0x00, 0x40, // IID816 - 0x62, 0xfc, 0xfc, 0x10, 0x81, 0xeb, 0x00, 0x00, 0x40, 0x00, // IID817 - 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xef, 0x00, 0x00, 0x40, 0x00, // IID818 - 0xd5, 0x19, 0x81, 0xea, 0x00, 0x00, 0x04, 0x00, // IID819 - 0x62, 0xfc, 0xf4, 0x14, 0x81, 0xee, 0x00, 0x00, 0x00, 0x40, // IID820 - 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xea, 0x00, 0x00, 0x00, 0x40, // IID821 - 0x62, 0xfc, 0xc4, 0x14, 0x81, 0xef, 0x00, 0x00, 0x00, 0x10, // IID822 - 0x62, 0x4c, 0x90, 0x18, 0x03, 0xb4, 0x58, 0x3b, 0x3a, 0xea, 0x56, // IID823 - 0x62, 0x1c, 0x90, 0x14, 0x03, 0xbc, 0xda, 0xa8, 0xc6, 0xee, 0xb4, // IID824 - 0x62, 0x4c, 0x9c, 0x18, 0x23, 0xb7, 0x8c, 0xc3, 0xef, 0xb9, // IID825 - 0x62, 0x3c, 0xa0, 0x14, 0x23, 0x94, 0x4e, 0xe5, 0xbe, 0x1e, 0x6a, // IID826 - 0x62, 0x44, 0x88, 0x10, 0x0b, 0x94, 0x93, 0xd7, 0x00, 0x60, 0xd4, // IID827 - 0x62, 0x7c, 0xb0, 0x1c, 0x0b, 0xa4, 0x0a, 0xf6, 0x59, 0x48, 0x0b, // IID828 - 0x62, 0xcc, 0xec, 0x18, 0xaf, 0x8c, 0x90, 0xd8, 0x4c, 0x28, 0x3d, // IID829 - 0x62, 0x0c, 0x94, 0x14, 0xaf, 0x94, 0x66, 0x24, 0x31, 0x81, 0x6e, // IID830 - 0x62, 0x7c, 0xe4, 0x18, 0x2b, 0xae, 0x62, 0xd7, 0xd5, 0x8f, // IID831 - 0x62, 0x4c, 0xc4, 0x14, 0x2b, 0xac, 0x11, 0x13, 0x58, 0xad, 0x9d, // IID832 - 0x62, 0xac, 0xbc, 0x18, 0x33, 0x94, 0xb3, 0x69, 0x59, 0x40, 0xf1, // IID833 - 0x62, 0x4c, 0xac, 0x1c, 0x33, 0xa2, 0xca, 0x81, 0x83, 0x16, // IID834 - 0x62, 0xc4, 0xf4, 0x18, 0x03, 0xd0, // IID835 - 0x49, 0x03, 0xce, // IID836 - 0x62, 0x7c, 0xc4, 0x14, 0x03, 0xd0, // IID837 - 0x62, 0x5c, 0xa4, 0x1c, 0x03, 0xd8, // IID838 - 0x62, 0xe4, 0xb5, 0x18, 0x66, 0xd2, // IID839 - 0x66, 0x4d, 0x0f, 0x38, 0xf6, 0xc7, // IID840 - 0x62, 0xcc, 0x86, 0x18, 0x66, 0xf2, // IID841 - 0xf3, 0x4c, 0x0f, 0x38, 0xf6, 0xda, // IID842 - 0x62, 0xfc, 0xe4, 0x10, 0x23, 0xd6, // IID843 - 0xd5, 0x5c, 0x23, 0xe9, // IID844 - 0x62, 0x44, 0xc4, 0x14, 0x23, 0xdf, // IID845 - 0x62, 0x54, 0xb4, 0x1c, 0x23, 0xcd, // IID846 - 0x62, 0x7c, 0xec, 0x10, 0xaf, 0xf8, // IID847 - 0xd5, 0x98, 0xaf, 0xc9, // IID848 - 0x62, 0x7c, 0xc4, 0x14, 0xaf, 0xe4, // IID849 - 0x62, 0x54, 0xac, 0x1c, 0xaf, 0xd1, // IID850 - 0x62, 0xc4, 0xec, 0x18, 0x0b, 0xde, // IID851 - 0x49, 0x0b, 0xcd, // IID852 - 0x62, 0x4c, 0xb4, 0x1c, 0x0b, 0xcd, // IID853 - 0x62, 0xdc, 0xec, 0x1c, 0x0b, 0xd1, // IID854 - 0x62, 0x7c, 0xc4, 0x10, 0x2b, 0xc0, // IID855 - 0x4d, 0x2b, 0xed, // IID856 - 0x62, 0x54, 0xe4, 0x14, 0x2b, 0xe7, // IID857 - 0x62, 0x74, 0xb4, 0x1c, 0x2b, 0xca, // IID858 - 0x62, 0xcc, 0x94, 0x18, 0x33, 0xc7, // IID859 - 0xd5, 0x59, 0x33, 0xce, // IID860 - 0x62, 0x6c, 0xe4, 0x14, 0x33, 0xf4, // IID861 - 0x62, 0x44, 0x84, 0x14, 0x33, 0xfd, // IID862 - 0x62, 0x54, 0xcc, 0x10, 0x24, 0xea, 0x04, // IID863 - 0xd5, 0xd9, 0xa4, 0xe8, 0x10, // IID864 - 0x62, 0x44, 0xdc, 0x14, 0x24, 0xdd, 0x10, // IID865 - 0x62, 0xcc, 0x84, 0x14, 0x24, 0xdf, 0x02, // IID866 - 0x62, 0x7c, 0x8c, 0x10, 0x2c, 0xdc, 0x08, // IID867 - 0x4c, 0x0f, 0xac, 0xfa, 0x01, // IID868 - 0x62, 0x5c, 0x9c, 0x14, 0x2c, 0xf6, 0x02, // IID869 - 0x62, 0xec, 0xdc, 0x14, 0x2c, 0xc4, 0x01, // IID870 - 0x62, 0xcc, 0xd4, 0x10, 0x40, 0xcc, // IID871 - 0xd5, 0x9d, 0x40, 0xfe, // IID872 - 0x62, 0x54, 0xf4, 0x18, 0x41, 0xff, // IID873 - 0x49, 0x0f, 0x41, 0xcd, // IID874 - 0x62, 0x4c, 0xec, 0x18, 0x42, 0xd2, // IID875 - 0xd5, 0xcd, 0x42, 0xe7, // IID876 - 0x62, 0xf4, 0xbc, 0x18, 0x43, 0xd1, // IID877 - 0x48, 0x0f, 0x43, 0xc9, // IID878 - 0x62, 0x54, 0xac, 0x18, 0x44, 0xe9, // IID879 - 0xd5, 0x9d, 0x44, 0xf3, // IID880 - 0x62, 0xc4, 0xa4, 0x18, 0x45, 0xf9, // IID881 - 0x4c, 0x0f, 0x45, 0xda, // IID882 - 0x62, 0x5c, 0x84, 0x10, 0x46, 0xf1, // IID883 - 0xd5, 0xc9, 0x46, 0xe4, // IID884 - 0x62, 0x5c, 0xec, 0x18, 0x47, 0xd4, // IID885 - 0xd5, 0x9c, 0x47, 0xc1, // IID886 - 0x62, 0x6c, 0xf4, 0x18, 0x48, 0xf7, // IID887 - 0xd5, 0xdc, 0x48, 0xd2, // IID888 - 0x62, 0xfc, 0xec, 0x18, 0x49, 0xda, // IID889 - 0xd5, 0xc9, 0x49, 0xed, // IID890 - 0x62, 0x4c, 0xa4, 0x10, 0x4a, 0xe3, // IID891 - 0xd5, 0x9d, 0x4a, 0xde, // IID892 - 0x62, 0xec, 0xf4, 0x18, 0x4b, 0xea, // IID893 - 0xd5, 0x99, 0x4b, 0xcd, // IID894 - 0x62, 0xc4, 0xec, 0x18, 0x4c, 0xec, // IID895 - 0xd5, 0x99, 0x4c, 0xd2, // IID896 - 0x62, 0xfc, 0xf4, 0x10, 0x4d, 0xde, // IID897 - 0x49, 0x0f, 0x4d, 0xd3, // IID898 - 0x62, 0x54, 0xec, 0x18, 0x4e, 0xf0, // IID899 - 0x4d, 0x0f, 0x4e, 0xf0, // IID900 - 0x62, 0x6c, 0xb4, 0x10, 0x4f, 0xed, // IID901 - 0xd5, 0xdd, 0x4f, 0xd6, // IID902 - 0x62, 0x84, 0xbc, 0x10, 0x40, 0xac, 0x5d, 0x1e, 0x52, 0x9c, 0x43, // IID903 - 0x62, 0xcc, 0xa0, 0x18, 0x41, 0x94, 0x05, 0x7f, 0x12, 0x32, 0x06, // IID904 - 0x62, 0x14, 0xf8, 0x10, 0x42, 0x84, 0x50, 0xef, 0x3d, 0x63, 0x10, // IID905 - 0x62, 0x7c, 0x94, 0x18, 0x43, 0xb2, 0xc8, 0x61, 0x09, 0xab, // IID906 - 0x62, 0x1c, 0x98, 0x18, 0x44, 0x84, 0x57, 0x86, 0xbb, 0xe1, 0x85, // IID907 - 0xd5, 0xde, 0x45, 0xac, 0x9b, 0x2e, 0xd2, 0x27, 0xca, // IID908 - 0x62, 0x8c, 0xf0, 0x18, 0x46, 0x94, 0x21, 0xa0, 0x64, 0xbe, 0x30, // IID909 - 0x62, 0x54, 0x98, 0x10, 0x47, 0xa4, 0x42, 0x06, 0x01, 0x47, 0xdd, // IID910 - 0x62, 0x34, 0xa4, 0x18, 0x48, 0x84, 0xdb, 0x96, 0x9e, 0xcc, 0x25, // IID911 - 0x62, 0x44, 0x9c, 0x18, 0x49, 0x9b, 0x02, 0x8f, 0xd2, 0xf3, // IID912 - 0x62, 0x6c, 0xbc, 0x18, 0x4a, 0x94, 0x5b, 0x16, 0x48, 0x92, 0xb7, // IID913 - 0x62, 0x54, 0x88, 0x10, 0x4b, 0x94, 0xd6, 0x84, 0x48, 0x88, 0x14, // IID914 - 0x62, 0x1c, 0xa4, 0x10, 0x4c, 0x84, 0xb5, 0xa8, 0xb7, 0x92, 0x00, // IID915 - 0x62, 0x6c, 0x8c, 0x18, 0x4d, 0xa4, 0x13, 0x45, 0x2d, 0x9c, 0x00, // IID916 - 0x62, 0x74, 0xb0, 0x10, 0x4e, 0x84, 0x91, 0x6b, 0xc8, 0x55, 0x66, // IID917 - 0x62, 0x84, 0xe0, 0x10, 0x4f, 0xac, 0x0a, 0xf5, 0xbc, 0xfa, 0xef, // IID918 + 0x62, 0x6c, 0x74, 0x18, 0x0b, 0xc6, // IID432 + 0xd5, 0x10, 0x0b, 0xcb, // IID433 + 0xd5, 0x55, 0x0b, 0xdb, // IID434 + 0x62, 0x54, 0x04, 0x14, 0x0b, 0xcd, // IID435 + 0x62, 0x6c, 0x04, 0x14, 0x0b, 0xff, // IID436 + 0x62, 0xec, 0x64, 0x14, 0x0b, 0xcb, // IID437 + 0x62, 0x6c, 0x5c, 0x10, 0xa5, 0xc0, // IID438 + 0x44, 0x0f, 0xa5, 0xe2, // IID439 + 0x62, 0x44, 0x14, 0x14, 0xa5, 0xf9, // IID440 + 0x62, 0xec, 0x74, 0x14, 0xa5, 0xe1, // IID441 + 0x62, 0xc4, 0x5c, 0x10, 0xad, 0xd7, // IID442 + 0x44, 0x0f, 0xad, 0xe1, // IID443 + 0x62, 0xc4, 0x0c, 0x1c, 0xad, 0xf9, // IID444 + 0x62, 0x7c, 0x64, 0x14, 0xad, 0xeb, // IID445 + 0x62, 0x4c, 0x0c, 0x10, 0x2b, 0xdb, // IID446 + 0x41, 0x2b, 0xd3, // IID447 + 0x62, 0x5c, 0x04, 0x1c, 0x2b, 0xd8, // IID448 + 0x62, 0x5c, 0x0c, 0x1c, 0x2b, 0xf1, // IID449 + 0x62, 0xc4, 0x04, 0x10, 0x33, 0xc4, // IID450 + 0xd5, 0x41, 0x33, 0xe6, // IID451 + 0xd5, 0x45, 0x33, 0xf5, // IID452 + 0x62, 0xec, 0x3c, 0x14, 0x33, 0xc9, // IID453 + 0x62, 0x6c, 0x2c, 0x14, 0x33, 0xd5, // IID454 + 0x62, 0x54, 0x24, 0x1c, 0x33, 0xeb, // IID455 + 0x62, 0xcc, 0x24, 0x10, 0x24, 0xe9, 0x04, // IID456 + 0xd5, 0x94, 0xa4, 0xd6, 0x04, // IID457 + 0x62, 0x44, 0x54, 0x14, 0x24, 0xc7, 0x10, // IID458 + 0x62, 0xe4, 0x6c, 0x1c, 0x24, 0xda, 0x01, // IID459 + 0x62, 0x54, 0x44, 0x10, 0x2c, 0xc5, 0x10, // IID460 + 0xd5, 0xd1, 0xac, 0xf2, 0x01, // IID461 + 0x62, 0x44, 0x3c, 0x14, 0x2c, 0xf1, 0x10, // IID462 + 0x62, 0x7c, 0x64, 0x14, 0x2c, 0xc3, 0x04, // IID463 + 0x62, 0x6c, 0x0c, 0x10, 0x40, 0xd1, // IID464 + 0xd5, 0x95, 0x40, 0xf2, // IID465 + 0x62, 0xcc, 0x3c, 0x10, 0x41, 0xdd, // IID466 + 0xd5, 0xd4, 0x41, 0xcc, // IID467 + 0x62, 0x54, 0x24, 0x18, 0x42, 0xd6, // IID468 + 0xd5, 0xd5, 0x42, 0xf1, // IID469 + 0x62, 0xcc, 0x14, 0x18, 0x43, 0xf3, // IID470 + 0xd5, 0xd1, 0x43, 0xc0, // IID471 + 0x62, 0x5c, 0x1c, 0x10, 0x44, 0xee, // IID472 + 0xd5, 0xd5, 0x44, 0xf0, // IID473 + 0x62, 0xcc, 0x54, 0x10, 0x45, 0xe7, // IID474 + 0xd5, 0x94, 0x45, 0xc0, // IID475 + 0x62, 0x6c, 0x04, 0x18, 0x46, 0xd6, // IID476 + 0xd5, 0xc4, 0x46, 0xfa, // IID477 + 0x62, 0x54, 0x24, 0x10, 0x47, 0xc2, // IID478 + 0xd5, 0xc1, 0x47, 0xd3, // IID479 + 0x62, 0xfc, 0x24, 0x10, 0x48, 0xdd, // IID480 + 0xd5, 0x95, 0x48, 0xe7, // IID481 + 0x62, 0xfc, 0x1c, 0x18, 0x49, 0xd2, // IID482 + 0xd5, 0xd0, 0x49, 0xd3, // IID483 + 0x62, 0xec, 0x7c, 0x10, 0x4a, 0xe7, // IID484 + 0xd5, 0xd0, 0x4a, 0xd0, // IID485 + 0x62, 0x4c, 0x64, 0x18, 0x4b, 0xfe, // IID486 + 0xd5, 0xd5, 0x4b, 0xfd, // IID487 + 0x62, 0x44, 0x1c, 0x10, 0x4c, 0xca, // IID488 + 0xd5, 0xd4, 0x4c, 0xc4, // IID489 + 0x62, 0xdc, 0x7c, 0x10, 0x4d, 0xd2, // IID490 + 0xd5, 0xd5, 0x4d, 0xe4, // IID491 + 0x62, 0xcc, 0x34, 0x18, 0x4e, 0xe0, // IID492 + 0xd5, 0xd5, 0x4e, 0xc5, // IID493 + 0x62, 0x44, 0x44, 0x10, 0x4f, 0xdf, // IID494 + 0xd5, 0x94, 0x4f, 0xe2, // IID495 + 0x62, 0x5c, 0x64, 0x10, 0x40, 0x8c, 0x4f, 0x43, 0x67, 0x41, 0xfd, // IID496 + 0xd5, 0xb6, 0x40, 0x84, 0x45, 0xcb, 0xa0, 0xe6, 0x41, // IID497 + 0x62, 0x3c, 0x40, 0x10, 0x41, 0xbc, 0xf3, 0xd3, 0x01, 0x52, 0xaa, // IID498 + 0xd5, 0x91, 0x41, 0x94, 0x13, 0xcb, 0xd8, 0x5e, 0xe5, // IID499 + 0x62, 0x0c, 0x64, 0x18, 0x42, 0xac, 0x27, 0x46, 0x22, 0xd4, 0x0b, // IID500 + 0xd5, 0xf0, 0x42, 0xac, 0x6b, 0xe8, 0x77, 0xae, 0xbe, // IID501 + 0x62, 0x2c, 0x44, 0x10, 0x43, 0xac, 0x8e, 0x28, 0x24, 0x52, 0xca, // IID502 + 0xd5, 0xd1, 0x43, 0x91, 0xc3, 0x84, 0x21, 0x63, // IID503 + 0x62, 0x3c, 0x14, 0x10, 0x44, 0xac, 0x2a, 0x3a, 0x15, 0x8d, 0xc6, // IID504 + 0xd5, 0xc7, 0x44, 0xac, 0xcc, 0x2e, 0x20, 0x73, 0x99, // IID505 + 0x62, 0xc4, 0x30, 0x10, 0x45, 0x94, 0xb1, 0x1f, 0xc9, 0x6a, 0x7f, // IID506 + 0xd5, 0xd5, 0x45, 0xa6, 0x94, 0x65, 0x2e, 0x56, // IID507 + 0x62, 0x44, 0x20, 0x10, 0x46, 0x84, 0xa7, 0x49, 0xc7, 0x9a, 0xb9, // IID508 + 0xd5, 0xd7, 0x46, 0x8c, 0xda, 0x57, 0xed, 0xc7, 0xa6, // IID509 + 0x62, 0xec, 0x74, 0x18, 0x47, 0xa0, 0x15, 0x5c, 0x76, 0xec, // IID510 + 0xd5, 0x93, 0x47, 0x9c, 0x05, 0x15, 0x26, 0x02, 0x1d, // IID511 + 0x62, 0x14, 0x68, 0x18, 0x48, 0xb4, 0x64, 0x6f, 0xa5, 0x8d, 0xae, // IID512 + 0xd5, 0xf4, 0x48, 0x84, 0x51, 0x1c, 0x3b, 0xda, 0xe8, // IID513 + 0xd5, 0xa1, 0x49, 0x8c, 0xbf, 0x31, 0xd3, 0x2d, 0x94, // IID514 + 0xd5, 0xc7, 0x49, 0x84, 0x57, 0xf7, 0xc2, 0x04, 0x80, // IID515 + 0x62, 0xd4, 0x40, 0x10, 0x4a, 0x8c, 0x3b, 0x85, 0xc5, 0x38, 0x37, // IID516 + 0xd5, 0xd7, 0x4a, 0x84, 0x16, 0xa8, 0x15, 0xcc, 0x0f, // IID517 + 0x62, 0x44, 0x08, 0x18, 0x4b, 0x94, 0x6e, 0x61, 0x31, 0xcf, 0xbb, // IID518 + 0xd5, 0x95, 0x4b, 0x94, 0x24, 0x9f, 0xc5, 0xd7, 0x03, // IID519 + 0x62, 0x84, 0x2c, 0x18, 0x4c, 0xac, 0xc0, 0xb4, 0x84, 0x65, 0x4a, // IID520 + 0xd5, 0xf4, 0x4c, 0x94, 0xe3, 0xef, 0x60, 0xc6, 0x47, // IID521 + 0x62, 0x74, 0x2c, 0x10, 0x4d, 0x91, 0x97, 0x7a, 0x97, 0x61, // IID522 + 0xd5, 0xe5, 0x4d, 0xb4, 0xdf, 0xcb, 0x01, 0xc6, 0x53, // IID523 + 0x62, 0x7c, 0x0c, 0x18, 0x4e, 0x89, 0x1e, 0x11, 0x93, 0xa9, // IID524 + 0xd5, 0xb5, 0x4e, 0xbc, 0x23, 0x92, 0x47, 0x16, 0x76, // IID525 + 0x62, 0x14, 0x24, 0x10, 0x4f, 0xb4, 0xa9, 0xd7, 0x52, 0x57, 0x0f, // IID526 + 0x44, 0x0f, 0x4f, 0xa4, 0xcb, 0x3a, 0x4b, 0xfe, 0xaa, // IID527 + 0xd5, 0x5d, 0x13, 0xf7, // IID528 + 0x4c, 0x3b, 0xe2, // IID529 + 0xd5, 0xd9, 0xaf, 0xe8, // IID530 + 0xf3, 0xd5, 0x9d, 0xb8, 0xc9, // IID531 + 0x4d, 0x1b, 0xc4, // IID532 + 0xd5, 0x5d, 0x2b, 0xf8, // IID533 + 0xf3, 0xd5, 0x9c, 0xbc, 0xd0, // IID534 + 0xf3, 0xd5, 0xd8, 0xbd, 0xe5, // IID535 + 0xd5, 0x18, 0x03, 0xd1, // IID536 + 0x4d, 0x23, 0xf5, // IID537 + 0xd5, 0x59, 0x0b, 0xe0, // IID538 + 0xd5, 0x58, 0x33, 0xee, // IID539 + 0xd5, 0x1d, 0x8b, 0xe3, // IID540 + 0xd5, 0xc8, 0xbc, 0xfa, // IID541 + 0xd5, 0xdd, 0xbd, 0xfc, // IID542 + 0xd5, 0xcd, 0xa3, 0xc8, // IID543 + 0xd5, 0x48, 0x87, 0xeb, // IID544 + 0xd5, 0x58, 0x85, 0xff, // IID545 + 0xd5, 0x1c, 0x01, 0x93, 0x5f, 0xc1, 0xf2, 0xe7, // IID546 + 0xd5, 0x6d, 0x21, 0x8c, 0x4b, 0x18, 0x94, 0x68, 0x87, // IID547 + 0xd5, 0x2e, 0x39, 0xac, 0xe3, 0x02, 0x21, 0xf7, 0x35, // IID548 + 0xd5, 0x49, 0x09, 0xa8, 0xef, 0xaf, 0xb9, 0xcb, // IID549 + 0xd5, 0x58, 0x31, 0x93, 0x23, 0xdd, 0xb4, 0xbf, // IID550 + 0xd5, 0x3e, 0x29, 0xb4, 0xdf, 0xba, 0xd9, 0x72, 0xbd, // IID551 + 0xd5, 0x4d, 0x89, 0xa4, 0x89, 0x9f, 0xe9, 0x9e, 0x8d, // IID552 + 0xd5, 0xb9, 0xc1, 0x9c, 0xa8, 0x90, 0xe9, 0x6b, 0x3a, // IID553 + 0xd5, 0x1a, 0x81, 0xa4, 0x16, 0xdd, 0x8b, 0xef, 0x07, 0x00, 0x00, 0x10, 0x00, // IID554 + 0xd5, 0x2b, 0x81, 0x84, 0x25, 0x4f, 0x76, 0xb8, 0x8a, 0x00, 0x00, 0x01, 0x00, // IID555 + 0x49, 0x81, 0xba, 0x26, 0x57, 0x2d, 0xf4, 0x00, 0x00, 0x00, 0x10, // IID556 + 0xd5, 0x1a, 0xc1, 0xbc, 0x77, 0xf5, 0xd9, 0x16, 0x6a, 0x04, // IID557 + 0xd5, 0x28, 0xc1, 0xa4, 0x69, 0x1e, 0xac, 0x66, 0x5f, 0x08, // IID558 + 0xd5, 0x28, 0x81, 0x9c, 0xf1, 0xb4, 0x6a, 0x73, 0xfb, 0x00, 0x00, 0x00, 0x10, // IID559 + 0xd5, 0x3a, 0xc1, 0xac, 0x35, 0x0b, 0x5b, 0x40, 0x0e, 0x08, // IID560 + 0xd5, 0x3a, 0x81, 0xac, 0xeb, 0xb5, 0xfb, 0x9d, 0x88, 0x00, 0x10, 0x00, 0x00, // IID561 + 0xd5, 0x1b, 0x83, 0xb4, 0x56, 0xdd, 0x7c, 0x86, 0xe6, 0x10, // IID562 + 0xd5, 0x2a, 0x81, 0x8c, 0xc2, 0x16, 0xb3, 0xd9, 0x18, 0x00, 0x10, 0x00, 0x00, // IID563 + 0x48, 0xc7, 0x83, 0xb3, 0xf8, 0xa7, 0xcf, 0x00, 0x01, 0x00, 0x00, // IID564 + 0xd5, 0x39, 0xf7, 0x84, 0xec, 0xc4, 0xfd, 0xa0, 0x65, 0x00, 0x00, 0x00, 0xf0, // IID565 + 0xd5, 0x69, 0x03, 0xbc, 0x13, 0xf4, 0x0e, 0xe5, 0xe2, // IID566 + 0xd5, 0x5a, 0x23, 0xb4, 0x62, 0x38, 0x1c, 0x5f, 0x1a, // IID567 + 0xd5, 0x79, 0x3b, 0xbc, 0x1e, 0x81, 0xd0, 0x6e, 0xc1, // IID568 + 0xf3, 0xd5, 0xcc, 0xbd, 0xa9, 0xe4, 0xfb, 0xe3, 0x12, // IID569 + 0xd5, 0x3c, 0x0b, 0xb4, 0xad, 0x42, 0x30, 0xd7, 0x00, // IID570 + 0xd5, 0x7e, 0x13, 0xbc, 0xb9, 0x12, 0xe9, 0xbd, 0x0a, // IID571 + 0xd5, 0xeb, 0xaf, 0xa4, 0x1d, 0xe1, 0x03, 0x24, 0xa7, // IID572 + 0xf3, 0xd5, 0x98, 0xb8, 0x9e, 0xdd, 0x93, 0x39, 0x8d, // IID573 + 0xd5, 0x4d, 0x1b, 0x91, 0x09, 0xba, 0x4a, 0x33, // IID574 + 0xd5, 0x2f, 0x2b, 0x8c, 0xf1, 0xfe, 0x9e, 0x65, 0xde, // IID575 + 0xf3, 0xd5, 0xdc, 0xbc, 0x8c, 0x24, 0x4f, 0x45, 0xce, 0xde, // IID576 + 0xd5, 0x79, 0x33, 0x84, 0x44, 0xb9, 0x83, 0xc4, 0x48, // IID577 + 0xd5, 0x6d, 0x8b, 0xb4, 0x01, 0xb1, 0x17, 0x73, 0xf7, // IID578 + 0xd5, 0x3f, 0x8d, 0x9c, 0xae, 0xd0, 0x8f, 0xeb, 0x3e, // IID579 + 0xf2, 0xd5, 0xdf, 0x2c, 0x94, 0xd5, 0x2e, 0x82, 0xf4, 0x3e, // IID580 + 0xd5, 0x7c, 0x87, 0xac, 0xa3, 0x47, 0xc2, 0xf0, 0xc0, // IID581 + 0xd5, 0x3d, 0x85, 0x84, 0x26, 0x17, 0x6a, 0xb5, 0x15, // IID582 + 0xd5, 0x19, 0x81, 0xc2, 0x00, 0x10, 0x00, 0x00, // IID583 + 0xd5, 0x18, 0x83, 0xe4, 0x10, // IID584 + 0xd5, 0x18, 0x81, 0xd7, 0x00, 0x00, 0x10, 0x00, // IID585 + 0x49, 0x81, 0xfc, 0x00, 0x10, 0x00, 0x00, // IID586 + 0x48, 0xc1, 0xd1, 0x04, // IID587 + 0x49, 0xd1, 0xde, // IID588 + 0xd5, 0x18, 0xc1, 0xc7, 0x02, // IID589 + 0x49, 0xc1, 0xcc, 0x04, // IID590 + 0x49, 0xc1, 0xfa, 0x04, // IID591 + 0xd5, 0x18, 0xc1, 0xe4, 0x04, // IID592 + 0x48, 0x81, 0xd9, 0x00, 0x00, 0x10, 0x00, // IID593 + 0xd5, 0x18, 0xc1, 0xe7, 0x10, // IID594 + 0xd5, 0x19, 0xc1, 0xeb, 0x02, // IID595 + 0x48, 0x81, 0xe9, 0x00, 0x00, 0x01, 0x00, // IID596 + 0x49, 0x81, 0xf1, 0x00, 0x00, 0x10, 0x00, // IID597 + 0xd5, 0x18, 0xc7, 0xc0, 0x00, 0x00, 0x01, 0x00, // IID598 + 0xd5, 0x19, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, // IID599 + 0xd5, 0x98, 0xba, 0xe2, 0x40, // IID600 + 0xd5, 0x19, 0xf7, 0xc5, 0x00, 0xf0, 0xff, 0xff, // IID601 + 0xd5, 0x19, 0x81, 0xce, 0x00, 0x00, 0x00, 0x04, // IID602 + 0xd5, 0x19, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x10, // IID603 + 0xd5, 0xfe, 0x40, 0xb4, 0xb9, 0xf0, 0x92, 0xff, 0x47, // IID604 + 0xd5, 0xbf, 0x41, 0x8c, 0x60, 0xc0, 0x04, 0x49, 0x38, // IID605 + 0xd5, 0xfa, 0x42, 0xbc, 0xc7, 0x9a, 0xed, 0x80, 0xe6, // IID606 + 0xd5, 0xbf, 0x43, 0x8c, 0x35, 0xc8, 0x49, 0x5d, 0x2b, // IID607 + 0xd5, 0xca, 0x44, 0x84, 0x7b, 0x81, 0x93, 0x37, 0x22, // IID608 + 0x4d, 0x0f, 0x45, 0x83, 0xa0, 0x67, 0x9d, 0x04, // IID609 + 0xd5, 0xfc, 0x46, 0xa4, 0x80, 0x57, 0xe2, 0x6b, 0xa1, // IID610 + 0xd5, 0xda, 0x47, 0x9c, 0x02, 0x14, 0xaa, 0xa1, 0xf5, // IID611 + 0xd5, 0xfe, 0x48, 0xa4, 0x61, 0xf4, 0xdd, 0x9b, 0xcd, // IID612 + 0xd5, 0xef, 0x49, 0xbc, 0x7e, 0x5a, 0xbc, 0x01, 0x50, // IID613 + 0xd5, 0xb8, 0x4a, 0x9c, 0x8a, 0x87, 0xdc, 0x90, 0xd7, // IID614 + 0xd5, 0xd8, 0x4b, 0x8c, 0x24, 0xc8, 0x07, 0xb6, 0xaa, // IID615 + 0xd5, 0xef, 0x4c, 0xb4, 0x61, 0x0d, 0xf3, 0x4f, 0xda, // IID616 + 0xd5, 0xc9, 0x4d, 0x99, 0xd4, 0x0d, 0x54, 0xd5, // IID617 + 0x4a, 0x0f, 0x4e, 0x9c, 0x61, 0x34, 0x97, 0xd2, 0xbc, // IID618 + 0xd5, 0xa9, 0x4f, 0x9c, 0xcf, 0xa2, 0xa9, 0x68, 0xd4, // IID619 + 0xd5, 0x11, 0xff, 0xd0, // IID620 + 0x49, 0xf7, 0xf1, // IID621 + 0xd5, 0x19, 0xf7, 0xfc, // IID622 + 0x48, 0xf7, 0xea, // IID623 + 0xd5, 0x19, 0xf7, 0xe7, // IID624 + 0x49, 0xf7, 0xdc, // IID625 + 0x49, 0xf7, 0xd4, // IID626 + 0xd5, 0x19, 0xd3, 0xc0, // IID627 + 0xd5, 0x19, 0xd3, 0xcc, // IID628 + 0x49, 0xd3, 0xfb, // IID629 + 0xd5, 0x19, 0xd3, 0xe3, // IID630 + 0xd5, 0x18, 0xd3, 0xe7, // IID631 + 0xd5, 0x18, 0xd3, 0xe9, // IID632 + 0xd5, 0x18, 0xff, 0xc0, // IID633 + 0x49, 0xff, 0xcc, // IID634 + 0xd5, 0x18, 0x57, // IID635 + 0xd5, 0x19, 0x58, // IID636 + 0xd5, 0x12, 0xff, 0x94, 0x32, 0xce, 0x62, 0x9c, 0x99, // IID637 + 0xd5, 0x19, 0xf7, 0xa0, 0xdf, 0xdb, 0xf5, 0x99, // IID638 + 0xd5, 0x29, 0xf7, 0x9c, 0x16, 0x6e, 0x93, 0xf3, 0x40, // IID639 + 0x4b, 0xd3, 0xbc, 0x2a, 0x72, 0xcb, 0x04, 0x7d, // IID640 + 0xd5, 0x1a, 0xd3, 0xa4, 0xda, 0x24, 0x4b, 0x89, 0xde, // IID641 + 0x49, 0xd3, 0xac, 0x4d, 0x80, 0xaa, 0x96, 0x79, // IID642 + 0x49, 0xff, 0x86, 0x2a, 0xd0, 0xc2, 0x67, // IID643 + 0xd5, 0x3a, 0xff, 0x8c, 0x16, 0xc0, 0x62, 0x4f, 0x22, // IID644 + 0xd5, 0x19, 0x69, 0x94, 0x5f, 0x10, 0xbb, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x01, // IID645 + 0xd5, 0x59, 0x69, 0xef, 0x00, 0x10, 0x00, 0x00, // IID646 + 0xd5, 0xc8, 0xa4, 0xdb, 0x01, // IID647 + 0xd5, 0xc9, 0xac, 0xfb, 0x04, // IID648 + 0x62, 0xfc, 0x0c, 0x10, 0x8f, 0xc0, // IID649 + 0x62, 0xfc, 0xe4, 0x18, 0x8f, 0xc1, // IID650 + 0x62, 0xfc, 0x0c, 0x10, 0xff, 0xf4, // IID651 + 0x62, 0xd4, 0x84, 0x10, 0xff, 0xf0, // IID652 + 0xd5, 0xcf, 0xb6, 0xa4, 0x30, 0x7a, 0xe6, 0x9a, 0x46, // IID653 + 0xd5, 0xad, 0xb7, 0xb4, 0x90, 0xfe, 0x61, 0x96, 0xb7, // IID654 + 0xd5, 0xc8, 0xbe, 0xab, 0x95, 0x1f, 0x25, 0x9b, // IID655 + 0xd5, 0xd9, 0xbf, 0x9c, 0x9f, 0x19, 0x88, 0x31, 0x60, // IID656 + 0xd5, 0xcd, 0xb6, 0xf5, // IID657 + 0xd5, 0xdc, 0xb7, 0xf2, // IID658 + 0xd5, 0xc9, 0xbe, 0xdf, // IID659 + 0xd5, 0xd8, 0xbf, 0xe0, // IID660 + 0xd5, 0xcd, 0xb1, 0xa4, 0xdb, 0x9d, 0x47, 0xc3, 0x0f, // IID661 + 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xfc, // IID662 + 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xfe, // IID663 + 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xf6, // IID664 + 0x62, 0xd4, 0xfc, 0x0c, 0xf7, 0xf3, // IID665 + 0x62, 0xf4, 0xfc, 0x08, 0xf7, 0xe9, // IID666 + 0x62, 0xdc, 0xfc, 0x0c, 0xf7, 0xec, // IID667 + 0x62, 0xfc, 0xfc, 0x08, 0xf7, 0xe5, // IID668 + 0x62, 0xd4, 0xfc, 0x0c, 0xf7, 0xe5, // IID669 + 0x62, 0x9c, 0xfc, 0x08, 0xf7, 0xa4, 0xba, 0x6e, 0xce, 0xa1, 0x70, // IID670 + 0x62, 0xdc, 0xf8, 0x0c, 0xf7, 0xa4, 0x58, 0xa4, 0x7a, 0x8f, 0xe9, // IID671 + 0x62, 0x5c, 0xfc, 0x08, 0xaf, 0xd3, // IID672 + 0xd5, 0x18, 0xf7, 0xe9, // IID673 + 0x62, 0xfc, 0xfc, 0x0c, 0xaf, 0xd6, // IID674 + 0x62, 0xf4, 0xfc, 0x0c, 0xaf, 0xdb, // IID675 + 0x62, 0x44, 0xfc, 0x08, 0xf5, 0xe7, // IID676 + 0x62, 0x54, 0xfc, 0x08, 0xf5, 0xff, // IID677 + 0x62, 0xd4, 0xfc, 0x0c, 0xf5, 0xdc, // IID678 + 0x62, 0xf4, 0xfc, 0x0c, 0xf5, 0xdb, // IID679 + 0x62, 0xd4, 0xac, 0x10, 0xf7, 0xdb, // IID680 + 0xd5, 0x18, 0xf7, 0xd9, // IID681 + 0x62, 0xdc, 0xec, 0x1c, 0xf7, 0xdf, // IID682 + 0x62, 0xdc, 0xa4, 0x14, 0xf7, 0xdb, // IID683 + 0x62, 0xd4, 0x84, 0x10, 0xf7, 0xd7, // IID684 + 0xd5, 0x18, 0xf7, 0xd5, // IID685 + 0x62, 0xdc, 0xfc, 0x08, 0x88, 0xd8, // IID686 + 0x62, 0x4c, 0xfc, 0x08, 0x88, 0xe4, // IID687 + 0x62, 0xcc, 0xfc, 0x0c, 0x88, 0xfb, // IID688 + 0x62, 0x54, 0xfc, 0x0c, 0x88, 0xed, // IID689 + 0x62, 0xdc, 0xb4, 0x10, 0xd3, 0xc4, // IID690 + 0xd5, 0x19, 0xd3, 0xc7, // IID691 + 0x62, 0xfc, 0xb4, 0x14, 0xd3, 0xc7, // IID692 + 0x62, 0xf4, 0xf4, 0x1c, 0xd3, 0xc1, // IID693 + 0x62, 0xd4, 0xcc, 0x10, 0xd3, 0xce, // IID694 + 0x49, 0xd3, 0xcf, // IID695 + 0x62, 0xdc, 0xa4, 0x1c, 0xd3, 0xce, // IID696 + 0x62, 0xdc, 0xbc, 0x14, 0xd3, 0xc8, // IID697 + 0x62, 0xfc, 0xac, 0x18, 0xd3, 0xe4, // IID698 + 0xd5, 0x18, 0xd3, 0xe3, // IID699 + 0x62, 0xdc, 0xf4, 0x14, 0xd3, 0xe1, // IID700 + 0x62, 0xd4, 0x94, 0x1c, 0xd3, 0xe5, // IID701 + 0x62, 0xdc, 0x84, 0x10, 0xd3, 0xfe, // IID702 + 0xd5, 0x18, 0xd3, 0xfa, // IID703 + 0x62, 0xdc, 0xb4, 0x14, 0xd3, 0xf9, // IID704 + 0x62, 0xdc, 0x9c, 0x14, 0xd3, 0xfc, // IID705 + 0x62, 0xdc, 0xcc, 0x10, 0xff, 0xcb, // IID706 + 0x49, 0xff, 0xcc, // IID707 + 0x62, 0xd4, 0xec, 0x14, 0xff, 0xcb, // IID708 + 0x62, 0xd4, 0xac, 0x1c, 0xff, 0xca, // IID709 + 0x62, 0xdc, 0xdc, 0x10, 0xff, 0xc0, // IID710 + 0xd5, 0x18, 0xff, 0xc2, // IID711 + 0x62, 0xd4, 0xe4, 0x1c, 0xff, 0xc3, // IID712 + 0x62, 0xdc, 0xac, 0x14, 0xff, 0xc2, // IID713 + 0x62, 0xd4, 0xd4, 0x10, 0xd3, 0xe0, // IID714 + 0x48, 0xd3, 0xe3, // IID715 + 0x62, 0xfc, 0xcc, 0x14, 0xd3, 0xe5, // IID716 + 0x62, 0xdc, 0xa4, 0x14, 0xd3, 0xe3, // IID717 + 0x62, 0xfc, 0x9c, 0x18, 0xd3, 0xe8, // IID718 + 0x49, 0xd3, 0xe8, // IID719 + 0x62, 0xd4, 0xec, 0x1c, 0xd3, 0xe9, // IID720 + 0x62, 0xfc, 0xdc, 0x14, 0xd3, 0xec, // IID721 + 0x62, 0x6c, 0xfc, 0x08, 0xf4, 0xfd, // IID722 + 0x62, 0xec, 0xfc, 0x08, 0xf4, 0xe4, // IID723 + 0x62, 0xfc, 0xfc, 0x0c, 0xf4, 0xc8, // IID724 + 0x62, 0x54, 0xfc, 0x0c, 0xf4, 0xf6, // IID725 + 0x62, 0x0c, 0xfc, 0x08, 0xaf, 0x9c, 0x49, 0x93, 0x23, 0x5a, 0x44, // IID726 + 0x62, 0xa4, 0xfc, 0x0c, 0xaf, 0xbc, 0x49, 0xf4, 0x10, 0x7f, 0xeb, // IID727 + 0x62, 0x7c, 0xf8, 0x08, 0xf5, 0xac, 0x4e, 0x6a, 0xe6, 0xf3, 0x8a, // IID728 + 0x62, 0x5c, 0xfc, 0x0c, 0xf5, 0xaf, 0xa7, 0x9d, 0xd4, 0xcb, // IID729 + 0x62, 0x9c, 0x84, 0x10, 0xf7, 0x9c, 0x68, 0xf2, 0x95, 0x4e, 0xda, // IID730 + 0x62, 0x94, 0x90, 0x1c, 0xf7, 0x9c, 0xe3, 0xab, 0x13, 0x00, 0x5c, // IID731 + 0x62, 0xfc, 0xfc, 0x08, 0x88, 0x94, 0x8a, 0x51, 0x15, 0xec, 0x9e, // IID732 + 0x62, 0x54, 0xfc, 0x0c, 0x88, 0x8a, 0x78, 0x2a, 0x58, 0xa3, // IID733 + 0x62, 0x9c, 0xf0, 0x10, 0xd3, 0xa4, 0x33, 0x2c, 0xda, 0x4c, 0x1b, // IID734 + 0x62, 0xd4, 0xb4, 0x14, 0xd3, 0xa4, 0x54, 0xce, 0x3b, 0x82, 0x62, // IID735 + 0x62, 0xd4, 0xb0, 0x18, 0xd3, 0xbc, 0x92, 0xb8, 0x85, 0xb5, 0xd9, // IID736 + 0x62, 0x94, 0xe0, 0x1c, 0xd3, 0xbc, 0x1e, 0x00, 0x1e, 0x29, 0x20, // IID737 + 0x62, 0x94, 0x9c, 0x18, 0xff, 0x8c, 0xb7, 0x45, 0x25, 0x08, 0xdf, // IID738 + 0x62, 0x94, 0xb0, 0x1c, 0xff, 0x8c, 0x4a, 0x84, 0x1d, 0x41, 0x21, // IID739 + 0x62, 0xb4, 0xd8, 0x10, 0xff, 0x84, 0xcb, 0x9e, 0x32, 0xf0, 0x02, // IID740 + 0x62, 0x94, 0xa8, 0x1c, 0xff, 0x84, 0x3c, 0x74, 0xa3, 0xaf, 0xc8, // IID741 + 0x62, 0xbc, 0xbc, 0x10, 0xd3, 0xac, 0xf7, 0x50, 0xa5, 0x18, 0x8e, // IID742 + 0x62, 0xbc, 0xb4, 0x14, 0xd3, 0xac, 0x53, 0x88, 0x0a, 0x7b, 0x50, // IID743 + 0x62, 0x64, 0xf8, 0x08, 0xf4, 0xbc, 0x03, 0x2a, 0x19, 0xd5, 0x19, // IID744 + 0x62, 0x3c, 0xf8, 0x0c, 0xf4, 0x8c, 0xa6, 0xcd, 0x07, 0x10, 0x21, // IID745 + 0x62, 0x6c, 0xfc, 0x10, 0x01, 0xa4, 0xdd, 0xe2, 0x05, 0xdc, 0xf7, // IID746 + 0x4e, 0x03, 0xbc, 0xc2, 0xa8, 0x5f, 0x46, 0xcb, // IID747 + 0x62, 0x14, 0xb8, 0x14, 0x01, 0xac, 0xc6, 0xd2, 0x59, 0xdc, 0x6c, // IID748 + 0x62, 0x9c, 0xe4, 0x1c, 0x01, 0x9c, 0xf3, 0xde, 0xe8, 0xc5, 0x36, // IID749 + 0x62, 0x0c, 0xd0, 0x10, 0x21, 0x9c, 0x5b, 0xed, 0xc4, 0xfd, 0xd3, // IID750 + 0xd5, 0x5e, 0x23, 0xbc, 0xbd, 0x4a, 0xc7, 0xf2, 0x6e, // IID751 + 0x62, 0x0c, 0x90, 0x1c, 0x21, 0x9c, 0x4f, 0xab, 0xe9, 0x4f, 0x73, // IID752 + 0x62, 0x14, 0x80, 0x1c, 0x21, 0xbc, 0xee, 0xaa, 0x7a, 0x19, 0xf9, // IID753 + 0x62, 0x0c, 0x9c, 0x18, 0x09, 0xa4, 0xfe, 0x9e, 0x3f, 0xa3, 0x3b, // IID754 + 0xd5, 0x4b, 0x0b, 0x84, 0x0c, 0xcd, 0xc4, 0x1f, 0xd7, // IID755 + 0x62, 0x04, 0xb8, 0x1c, 0x09, 0x9c, 0xc8, 0x6b, 0x42, 0xbd, 0xe1, // IID756 + 0x62, 0xdc, 0xf4, 0x1c, 0x09, 0x8c, 0x9b, 0xad, 0xbc, 0xe4, 0x7b, // IID757 + 0x62, 0xfc, 0xb8, 0x10, 0x29, 0x94, 0xb7, 0xd7, 0x27, 0x88, 0x6f, // IID758 + 0x62, 0xc4, 0xd4, 0x10, 0x29, 0xaa, 0x38, 0x47, 0xca, 0xf9, // IID759 + 0x62, 0x9c, 0xc0, 0x14, 0x29, 0x9c, 0xd3, 0xc0, 0xbc, 0x22, 0x09, // IID760 + 0x62, 0x2c, 0xb4, 0x14, 0x29, 0x8c, 0x3f, 0x54, 0x6b, 0x0b, 0xc7, // IID761 + 0x62, 0xd4, 0xa0, 0x18, 0x31, 0x8c, 0x9c, 0xe9, 0x13, 0x8e, 0xa4, // IID762 + 0xd5, 0x7c, 0x33, 0xa4, 0x13, 0x7e, 0x9b, 0x6b, 0x71, // IID763 + 0x62, 0xa4, 0xd0, 0x14, 0x31, 0x84, 0x29, 0xe2, 0xbb, 0x0f, 0xa5, // IID764 + 0x62, 0x3c, 0x98, 0x1c, 0x31, 0xa4, 0x14, 0xb1, 0x7f, 0x0b, 0x0e, // IID765 + 0x62, 0xf4, 0x8c, 0x10, 0x81, 0x81, 0x4f, 0x7b, 0x3b, 0x2d, 0x00, 0x00, 0x10, 0x00, // IID766 + 0x62, 0xbc, 0x8c, 0x1c, 0x81, 0x84, 0xbd, 0x18, 0x51, 0xdd, 0xed, 0x00, 0x10, 0x00, 0x00, // IID767 + 0x62, 0xbc, 0xc0, 0x10, 0x83, 0xa4, 0x3c, 0x96, 0xb2, 0x91, 0xf6, 0x10, // IID768 + 0x62, 0xf4, 0xac, 0x1c, 0x83, 0xa4, 0xd2, 0x7c, 0xf1, 0x75, 0x38, 0x01, // IID769 + 0x62, 0xa4, 0xf8, 0x08, 0x69, 0x8c, 0x89, 0x76, 0x10, 0xc7, 0x32, 0x00, 0x10, 0x00, 0x00, // IID770 + 0x62, 0xcc, 0xfc, 0x0c, 0x69, 0x9c, 0x9f, 0x0d, 0xa6, 0xad, 0x7b, 0x00, 0x00, 0x10, 0x00, // IID771 + 0x62, 0xfc, 0xb0, 0x10, 0x81, 0x8c, 0x7a, 0x44, 0x74, 0x14, 0x48, 0x00, 0x00, 0x00, 0x01, // IID772 + 0x62, 0x9c, 0x90, 0x14, 0x81, 0x8c, 0x5a, 0xa8, 0xc6, 0xee, 0xb4, 0x00, 0x00, 0x10, 0x00, // IID773 + 0x62, 0xfc, 0x84, 0x10, 0xc1, 0xa2, 0x8c, 0xc3, 0xef, 0xb9, 0x02, // IID774 + 0x62, 0x94, 0xb4, 0x14, 0xc1, 0xa4, 0x3a, 0xa4, 0x5d, 0x92, 0x48, 0x10, // IID775 + 0x62, 0xfc, 0xac, 0x10, 0xc1, 0xba, 0xbe, 0x3a, 0x5e, 0xa1, 0x08, // IID776 + 0x62, 0xd4, 0x98, 0x1c, 0xc1, 0xbc, 0xb2, 0x64, 0x82, 0x95, 0x5d, 0x08, // IID777 + 0x62, 0xfc, 0xe8, 0x18, 0xc1, 0xac, 0xa1, 0x23, 0xdd, 0x5a, 0x29, 0x10, // IID778 + 0x62, 0xbc, 0xe0, 0x1c, 0xc1, 0xac, 0x66, 0xcb, 0x29, 0x29, 0x78, 0x02, // IID779 + 0x62, 0xfc, 0xe4, 0x10, 0x83, 0xaf, 0x8e, 0xe2, 0x7e, 0xb6, 0x01, // IID780 + 0x62, 0xbc, 0xbc, 0x1c, 0x81, 0xac, 0xb3, 0x66, 0x51, 0xd4, 0xe4, 0x00, 0x00, 0x10, 0x00, // IID781 + 0x62, 0xb4, 0xe4, 0x10, 0x81, 0xb4, 0x11, 0xe9, 0x6e, 0xa6, 0x45, 0x00, 0x00, 0x10, 0x00, // IID782 + 0x62, 0x94, 0x98, 0x14, 0x83, 0xb4, 0x29, 0xec, 0x6c, 0x5e, 0xd7, 0x10, // IID783 + 0x62, 0xf4, 0xbc, 0x18, 0x81, 0xc1, 0x00, 0x00, 0x00, 0x01, // IID784 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xc6, 0x00, 0x00, 0x00, 0x01, // IID785 + 0xd5, 0x18, 0x81, 0xc0, 0x00, 0x01, 0x00, 0x00, // IID786 + 0x62, 0xd4, 0xbc, 0x14, 0x81, 0xc1, 0x00, 0x10, 0x00, 0x00, // IID787 + 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xc2, 0x00, 0x10, 0x00, 0x00, // IID788 + 0x62, 0xd4, 0xbc, 0x1c, 0x83, 0xc0, 0x01, // IID789 + 0x62, 0xfc, 0x84, 0x18, 0x81, 0xe6, 0x00, 0x00, 0x10, 0x00, // IID790 + 0x62, 0xdc, 0xfc, 0x18, 0x81, 0xe2, 0x00, 0x00, 0x10, 0x00, // IID791 + 0x48, 0x81, 0xe2, 0x00, 0x10, 0x00, 0x00, // IID792 + 0x62, 0xfc, 0xec, 0x1c, 0x81, 0xe6, 0x00, 0x00, 0x00, 0x10, // IID793 + 0x62, 0xdc, 0xfc, 0x1c, 0x81, 0xe5, 0x00, 0x00, 0x00, 0x10, // IID794 + 0x62, 0xfc, 0xc4, 0x14, 0x81, 0xe7, 0x00, 0x00, 0x00, 0x01, // IID795 + 0x62, 0x54, 0xfc, 0x08, 0x69, 0xcd, 0x00, 0x00, 0x10, 0x00, // IID796 + 0x62, 0xfc, 0xfc, 0x08, 0x69, 0xc2, 0x00, 0x00, 0x10, 0x00, // IID797 + 0x62, 0xec, 0xfc, 0x08, 0x69, 0xc0, 0x00, 0x00, 0x10, 0x00, // IID798 + 0x62, 0xec, 0xfc, 0x0c, 0x6b, 0xcf, 0x01, // IID799 + 0x62, 0xd4, 0xfc, 0x0c, 0x6b, 0xc4, 0x01, // IID800 + 0x62, 0x54, 0xfc, 0x0c, 0x69, 0xd2, 0x00, 0x00, 0x00, 0x10, // IID801 + 0x62, 0xfc, 0xec, 0x18, 0x81, 0xcb, 0x00, 0x01, 0x00, 0x00, // IID802 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xce, 0x00, 0x01, 0x00, 0x00, // IID803 + 0x49, 0x83, 0xcd, 0x01, // IID804 + 0x62, 0xdc, 0xb4, 0x14, 0x81, 0xcd, 0x00, 0x01, 0x00, 0x00, // IID805 + 0x62, 0xf4, 0xfc, 0x1c, 0x81, 0xca, 0x00, 0x01, 0x00, 0x00, // IID806 + 0x62, 0xfc, 0xfc, 0x14, 0x83, 0xc8, 0x10, // IID807 + 0x62, 0xfc, 0x94, 0x18, 0xc1, 0xd3, 0x04, // IID808 + 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xd4, 0x04, // IID809 + 0x49, 0xc1, 0xd1, 0x04, // IID810 + 0x62, 0xfc, 0x94, 0x18, 0xd1, 0xc0, // IID811 + 0x62, 0xdc, 0xfc, 0x18, 0xd1, 0xc7, // IID812 + 0xd5, 0x19, 0xc1, 0xc6, 0x08, // IID813 + 0x62, 0xfc, 0x8c, 0x14, 0xc1, 0xc4, 0x08, // IID814 + 0x62, 0xdc, 0xfc, 0x1c, 0xc1, 0xc7, 0x08, // IID815 + 0x62, 0xdc, 0x84, 0x14, 0xc1, 0xc7, 0x04, // IID816 + 0x62, 0xd4, 0xcc, 0x10, 0xc1, 0xca, 0x04, // IID817 + 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xcd, 0x04, // IID818 + 0xd5, 0x19, 0xc1, 0xc8, 0x10, // IID819 + 0x62, 0xfc, 0x94, 0x14, 0xc1, 0xce, 0x10, // IID820 + 0x62, 0xfc, 0xfc, 0x1c, 0xc1, 0xcc, 0x10, // IID821 + 0x62, 0xdc, 0xa4, 0x14, 0xc1, 0xcb, 0x04, // IID822 + 0x62, 0xfc, 0x84, 0x10, 0xc1, 0xe3, 0x02, // IID823 + 0x62, 0xfc, 0xfc, 0x18, 0xc1, 0xe4, 0x02, // IID824 + 0x49, 0xc1, 0xe3, 0x08, // IID825 + 0x62, 0xd4, 0xec, 0x1c, 0xd1, 0xe7, // IID826 + 0x62, 0xd4, 0xfc, 0x1c, 0xd1, 0xe2, // IID827 + 0x62, 0xdc, 0x94, 0x14, 0xc1, 0xe5, 0x04, // IID828 + 0x62, 0xfc, 0xdc, 0x10, 0xd1, 0xf8, // IID829 + 0x62, 0xfc, 0xfc, 0x18, 0xd1, 0xfd, // IID830 + 0xd5, 0x19, 0xc1, 0xfc, 0x08, // IID831 + 0x62, 0xf4, 0x8c, 0x14, 0xc1, 0xf9, 0x04, // IID832 + 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xff, 0x04, // IID833 + 0x62, 0xf4, 0xf4, 0x1c, 0xc1, 0xf9, 0x04, // IID834 + 0x62, 0xdc, 0xec, 0x18, 0xc1, 0xe2, 0x04, // IID835 + 0x62, 0xdc, 0xfc, 0x18, 0xc1, 0xe2, 0x04, // IID836 + 0x49, 0xc1, 0xe0, 0x04, // IID837 + 0x62, 0xf4, 0xf4, 0x1c, 0xd1, 0xe1, // IID838 + 0x62, 0xf4, 0xfc, 0x1c, 0xd1, 0xe1, // IID839 + 0x62, 0xd4, 0x94, 0x1c, 0xc1, 0xe5, 0x02, // IID840 + 0x62, 0xdc, 0x8c, 0x18, 0xc1, 0xeb, 0x02, // IID841 + 0x62, 0xd4, 0xfc, 0x18, 0xc1, 0xeb, 0x02, // IID842 + 0x49, 0xc1, 0xe9, 0x10, // IID843 + 0x62, 0xdc, 0xec, 0x1c, 0xc1, 0xef, 0x02, // IID844 + 0x62, 0xd4, 0xfc, 0x1c, 0xc1, 0xee, 0x02, // IID845 + 0x62, 0xd4, 0x9c, 0x1c, 0xc1, 0xec, 0x08, // IID846 + 0x62, 0xdc, 0xac, 0x18, 0x83, 0xec, 0x01, // IID847 + 0x62, 0xd4, 0xfc, 0x18, 0x83, 0xe8, 0x01, // IID848 + 0x48, 0x81, 0xe9, 0x00, 0x00, 0x00, 0x01, // IID849 + 0x62, 0xf4, 0xec, 0x1c, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x01, // IID850 + 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xea, 0x00, 0x00, 0x00, 0x01, // IID851 + 0x62, 0xdc, 0xa4, 0x14, 0x81, 0xeb, 0x00, 0x00, 0x01, 0x00, // IID852 + 0x62, 0xf4, 0x8c, 0x10, 0x81, 0xf1, 0x00, 0x10, 0x00, 0x00, // IID853 + 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xf5, 0x00, 0x10, 0x00, 0x00, // IID854 + 0x48, 0x81, 0xf1, 0x00, 0x00, 0x00, 0x01, // IID855 + 0x62, 0xd4, 0xd4, 0x14, 0x83, 0xf4, 0x01, // IID856 + 0x62, 0xf4, 0xfc, 0x1c, 0x83, 0xf2, 0x01, // IID857 + 0x62, 0xf4, 0xe4, 0x1c, 0x81, 0xf3, 0x00, 0x00, 0x00, 0x01, // IID858 + 0x62, 0xf4, 0xa4, 0x18, 0x81, 0xca, 0x00, 0x00, 0x01, 0x00, // IID859 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xce, 0x00, 0x00, 0x01, 0x00, // IID860 + 0x49, 0x81, 0xce, 0x00, 0x00, 0x04, 0x00, // IID861 + 0x62, 0xdc, 0xb4, 0x10, 0x81, 0xcd, 0x00, 0x00, 0x04, 0x00, // IID862 + 0x62, 0xfc, 0xfc, 0x18, 0x81, 0xcd, 0x00, 0x00, 0x04, 0x00, // IID863 + 0x49, 0x81, 0xcb, 0x00, 0x00, 0x00, 0x01, // IID864 + 0x62, 0xfc, 0x94, 0x10, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x04, // IID865 + 0x62, 0xd4, 0xfc, 0x18, 0x81, 0xeb, 0x00, 0x00, 0x00, 0x04, // IID866 + 0xd5, 0x18, 0x81, 0xea, 0x00, 0x00, 0x00, 0x04, // IID867 + 0x62, 0xfc, 0x9c, 0x14, 0x81, 0xef, 0x00, 0x00, 0x40, 0x00, // IID868 + 0x62, 0xfc, 0xfc, 0x1c, 0x81, 0xed, 0x00, 0x00, 0x40, 0x00, // IID869 + 0x62, 0xfc, 0xfc, 0x14, 0x81, 0xe8, 0x00, 0x00, 0x00, 0x01, // IID870 + 0x62, 0x0c, 0xbc, 0x18, 0x03, 0x8c, 0x42, 0xef, 0x3d, 0x63, 0x10, // IID871 + 0xd5, 0x3c, 0x03, 0xac, 0x42, 0xf8, 0xba, 0xdf, 0x8b, // IID872 + 0x62, 0x44, 0xf4, 0x14, 0x03, 0x94, 0x24, 0xbf, 0x0a, 0xa8, 0x23, // IID873 + 0x62, 0x5c, 0xb0, 0x1c, 0x03, 0x8c, 0x1d, 0xae, 0x61, 0x61, 0xfd, // IID874 + 0x62, 0x24, 0xb0, 0x18, 0x23, 0xa4, 0x89, 0xaa, 0xff, 0x61, 0x42, // IID875 + 0xd5, 0x6e, 0x23, 0x9c, 0x22, 0x3f, 0x36, 0x42, 0xd9, // IID876 + 0x62, 0x7c, 0x8c, 0x1c, 0x23, 0x98, 0xdf, 0x0d, 0xba, 0x63, // IID877 + 0x62, 0x3c, 0xb8, 0x1c, 0x23, 0x84, 0x4e, 0xbc, 0x54, 0x49, 0xbc, // IID878 + 0x62, 0xdc, 0xe4, 0x10, 0x0b, 0x8c, 0x8b, 0x3a, 0x80, 0x97, 0x80, // IID879 + 0xd5, 0x6a, 0x0b, 0x9c, 0x53, 0x16, 0x48, 0x92, 0xb7, // IID880 + 0x62, 0x54, 0x88, 0x14, 0x0b, 0x94, 0xd6, 0x84, 0x48, 0x88, 0x14, // IID881 + 0x62, 0x4c, 0xa4, 0x14, 0x0b, 0x9d, 0x80, 0x71, 0x33, 0x20, // IID882 + 0x62, 0xec, 0xf4, 0x18, 0xaf, 0xac, 0x1d, 0x72, 0x77, 0xfc, 0xcc, // IID883 + 0xd5, 0x9b, 0xaf, 0x94, 0xcc, 0xbf, 0x08, 0x27, 0x85, // IID884 + 0x62, 0x2c, 0xbc, 0x1c, 0xaf, 0xac, 0x21, 0x6a, 0x39, 0x85, 0x6e, // IID885 + 0x62, 0xac, 0xfc, 0x14, 0xaf, 0x84, 0xd3, 0x00, 0x6d, 0xa6, 0xb6, // IID886 + 0x62, 0xc4, 0xd8, 0x10, 0x2b, 0x8c, 0x35, 0x4f, 0x9a, 0x21, 0x1d, // IID887 + 0xd5, 0x7c, 0x2b, 0x8c, 0xed, 0x39, 0x57, 0x97, 0x97, // IID888 + 0x62, 0x64, 0xd8, 0x14, 0x2b, 0x84, 0xa3, 0xa6, 0x9d, 0xc5, 0x32, // IID889 + 0x62, 0x54, 0xb8, 0x1c, 0x2b, 0x84, 0x0c, 0x31, 0xd2, 0x41, 0xd9, // IID890 + 0x62, 0xc4, 0xec, 0x18, 0x33, 0x99, 0x91, 0x3b, 0x90, 0x7d, // IID891 + 0xd5, 0x7f, 0x33, 0xa4, 0x9d, 0x6f, 0x1f, 0x09, 0x53, // IID892 + 0x62, 0xcc, 0xf4, 0x14, 0x33, 0x83, 0x07, 0x92, 0x6e, 0x7c, // IID893 + 0x62, 0x14, 0x80, 0x1c, 0x33, 0xbc, 0xc5, 0xa0, 0x86, 0x37, 0x8a, // IID894 + 0x62, 0xfc, 0xfc, 0x10, 0x03, 0xda, // IID895 + 0xd5, 0x5c, 0x03, 0xc2, // IID896 + 0x4d, 0x03, 0xcf, // IID897 + 0x62, 0x44, 0xe4, 0x14, 0x03, 0xd5, // IID898 + 0x62, 0x6c, 0x9c, 0x14, 0x03, 0xe6, // IID899 + 0x62, 0x7c, 0xcc, 0x14, 0x03, 0xde, // IID900 + 0x62, 0x54, 0xf5, 0x18, 0x66, 0xe5, // IID901 + 0x62, 0x44, 0xfd, 0x08, 0x66, 0xf4, // IID902 + 0x62, 0x7c, 0x9e, 0x10, 0x66, 0xf2, // IID903 + 0x62, 0x6c, 0xfe, 0x08, 0x66, 0xf3, // IID904 + 0x62, 0x54, 0xdc, 0x10, 0x23, 0xf6, // IID905 + 0xd5, 0x58, 0x23, 0xcf, // IID906 + 0xd5, 0x49, 0x23, 0xce, // IID907 + 0x62, 0xc4, 0xe4, 0x14, 0x23, 0xe7, // IID908 + 0x62, 0xd4, 0xe4, 0x1c, 0x23, 0xdd, // IID909 + 0x62, 0x6c, 0xcc, 0x14, 0x23, 0xf6, // IID910 + 0x62, 0x64, 0xf4, 0x10, 0xaf, 0xc1, // IID911 + 0xd5, 0xc9, 0xaf, 0xe8, // IID912 + 0xd5, 0xdc, 0xaf, 0xed, // IID913 + 0x62, 0x7c, 0xa4, 0x14, 0xaf, 0xef, // IID914 + 0x62, 0x44, 0xac, 0x14, 0xaf, 0xd0, // IID915 + 0x62, 0x7c, 0xcc, 0x14, 0xaf, 0xee, // IID916 + 0x62, 0xdc, 0xa4, 0x18, 0x0b, 0xd5, // IID917 + 0xd5, 0x19, 0x0b, 0xd7, // IID918 + 0xd5, 0x1d, 0x0b, 0xd5, // IID919 + 0x62, 0x64, 0xa4, 0x14, 0x0b, 0xe1, // IID920 + 0x62, 0x44, 0xb4, 0x14, 0x0b, 0xc9, // IID921 + 0x62, 0x74, 0xf4, 0x1c, 0x0b, 0xc1, // IID922 + 0x62, 0x7c, 0xf4, 0x18, 0x2b, 0xd0, // IID923 + 0xd5, 0x48, 0x2b, 0xc9, // IID924 + 0x62, 0xcc, 0x94, 0x1c, 0x2b, 0xe8, // IID925 + 0x62, 0x4c, 0x84, 0x14, 0x2b, 0xfc, // IID926 + 0xd5, 0x59, 0x33, 0xfc, // IID927 + 0x4d, 0x33, 0xd3, // IID928 + 0xd5, 0x58, 0x33, 0xda, // IID929 + 0x62, 0x74, 0x84, 0x14, 0x33, 0xca, // IID930 + 0x62, 0x54, 0x94, 0x1c, 0x33, 0xe9, // IID931 + 0x62, 0x74, 0xf4, 0x1c, 0x33, 0xd1, // IID932 + 0x62, 0xcc, 0x9c, 0x18, 0x24, 0xf0, 0x08, // IID933 + 0xd5, 0xdd, 0xa4, 0xc9, 0x08, // IID934 + 0x62, 0x7c, 0xd4, 0x14, 0x24, 0xfc, 0x08, // IID935 + 0x62, 0x7c, 0xd4, 0x14, 0x24, 0xd5, 0x08, // IID936 + 0xd5, 0x9c, 0xac, 0xc2, 0x02, // IID937 + 0xd5, 0xdd, 0xac, 0xea, 0x08, // IID938 + 0x62, 0xcc, 0x94, 0x14, 0x2c, 0xda, 0x02, // IID939 + 0x62, 0xd4, 0x9c, 0x1c, 0x2c, 0xcc, 0x04, // IID940 + 0x62, 0xec, 0xd4, 0x10, 0x40, 0xf7, // IID941 + 0x4d, 0x0f, 0x40, 0xcd, // IID942 + 0x62, 0xcc, 0xf4, 0x18, 0x41, 0xf8, // IID943 + 0xd5, 0xcc, 0x41, 0xe2, // IID944 + 0x62, 0x6c, 0x8c, 0x18, 0x42, 0xff, // IID945 + 0xd5, 0xdc, 0x42, 0xf7, // IID946 + 0x62, 0x6c, 0xac, 0x18, 0x43, 0xee, // IID947 + 0xd5, 0x99, 0x43, 0xda, // IID948 + 0x62, 0xc4, 0xc4, 0x10, 0x44, 0xed, // IID949 + 0xd5, 0x9c, 0x44, 0xd4, // IID950 + 0x62, 0x5c, 0xe4, 0x18, 0x45, 0xcd, // IID951 + 0xd5, 0xd9, 0x45, 0xc6, // IID952 + 0x62, 0xdc, 0x94, 0x18, 0x46, 0xcd, // IID953 + 0xd5, 0xcd, 0x46, 0xfd, // IID954 + 0x62, 0x5c, 0xa4, 0x10, 0x47, 0xce, // IID955 + 0xd5, 0xdc, 0x47, 0xd4, // IID956 + 0x62, 0x7c, 0xbc, 0x18, 0x48, 0xe6, // IID957 + 0xd5, 0xdc, 0x48, 0xf9, // IID958 + 0x62, 0xdc, 0x94, 0x10, 0x49, 0xc9, // IID959 + 0xd5, 0xc9, 0x49, 0xf6, // IID960 + 0x62, 0x44, 0xf4, 0x18, 0x4a, 0xd9, // IID961 + 0xd5, 0xc9, 0x4a, 0xf3, // IID962 + 0x62, 0xcc, 0x8c, 0x18, 0x4b, 0xd8, // IID963 + 0xd5, 0xdc, 0x4b, 0xc1, // IID964 + 0x62, 0xcc, 0xf4, 0x10, 0x4c, 0xde, // IID965 + 0xd5, 0xc9, 0x4c, 0xde, // IID966 + 0x62, 0x5c, 0xb4, 0x10, 0x4d, 0xdd, // IID967 + 0xd5, 0x9d, 0x4d, 0xe2, // IID968 + 0x62, 0xd4, 0xa4, 0x18, 0x4e, 0xda, // IID969 + 0xd5, 0x98, 0x4e, 0xd6, // IID970 + 0x62, 0x7c, 0x8c, 0x18, 0x4f, 0xff, // IID971 + 0xd5, 0x9d, 0x4f, 0xc0, // IID972 + 0x62, 0x04, 0xe4, 0x18, 0x40, 0xbc, 0xc2, 0x20, 0x9f, 0xc0, 0xce, // IID973 + 0xd5, 0xea, 0x40, 0xbc, 0x81, 0x9c, 0x1d, 0xf4, 0x17, // IID974 + 0x62, 0x7c, 0x84, 0x10, 0x41, 0x98, 0x42, 0x89, 0x01, 0x2c, // IID975 + 0xd5, 0xbc, 0x41, 0x9c, 0xe0, 0x55, 0x6a, 0x4b, 0x67, // IID976 + 0x62, 0x54, 0xb4, 0x18, 0x42, 0xac, 0x09, 0xdf, 0x11, 0x4a, 0x39, // IID977 + 0xd5, 0xec, 0x42, 0xb4, 0x72, 0x78, 0xd4, 0xc9, 0x93, // IID978 + 0x62, 0xdc, 0x94, 0x18, 0x43, 0x8c, 0xc8, 0x66, 0x0b, 0x50, 0x46, // IID979 + 0xd5, 0xfe, 0x43, 0x84, 0x4a, 0x7c, 0x3b, 0x28, 0x53, // IID980 + 0x62, 0x04, 0xc4, 0x10, 0x44, 0x8c, 0x0f, 0xe2, 0xfc, 0xfc, 0xa0, // IID981 + 0xd5, 0xfd, 0x44, 0x8c, 0x44, 0xec, 0x0a, 0x31, 0xac, // IID982 + 0x62, 0x0c, 0xe0, 0x18, 0x45, 0x8c, 0x88, 0x79, 0x53, 0x35, 0x99, // IID983 + 0xd5, 0xfb, 0x45, 0x84, 0xf3, 0x5d, 0x45, 0x7f, 0x79, // IID984 + 0x62, 0x6c, 0xb0, 0x10, 0x46, 0xb4, 0x52, 0xcd, 0xaa, 0x9d, 0x1c, // IID985 + 0xd5, 0xea, 0x46, 0xb4, 0x49, 0x57, 0x05, 0x34, 0xc2, // IID986 + 0x62, 0x4c, 0xbc, 0x10, 0x47, 0x91, 0xb5, 0x60, 0x70, 0x74, // IID987 + 0xd5, 0xbd, 0x47, 0x84, 0xe0, 0xf6, 0x85, 0xd2, 0x47, // IID988 + 0x62, 0x84, 0x9c, 0x18, 0x48, 0x84, 0x95, 0x14, 0xb2, 0xe5, 0x34, // IID989 + 0xd5, 0xa9, 0x48, 0x94, 0x1f, 0x4f, 0xc7, 0xae, 0xbf, // IID990 + 0x62, 0xa4, 0xec, 0x10, 0x49, 0xac, 0xab, 0x97, 0x91, 0xb1, 0x51, // IID991 + 0xd5, 0xef, 0x49, 0x84, 0xfb, 0x0a, 0x52, 0x01, 0x3e, // IID992 + 0x62, 0x04, 0x90, 0x10, 0x4a, 0x94, 0xca, 0x8e, 0xc7, 0x83, 0xa0, // IID993 + 0xd5, 0x9e, 0x4a, 0x9c, 0xd6, 0xad, 0xeb, 0x8c, 0x97, // IID994 + 0x62, 0x14, 0xd8, 0x10, 0x4b, 0xbc, 0x09, 0xaa, 0xed, 0x37, 0x4a, // IID995 + 0xd5, 0xed, 0x4b, 0xbc, 0x21, 0x86, 0x9f, 0x99, 0x4f, // IID996 + 0x62, 0x84, 0xe8, 0x10, 0x4c, 0xbc, 0x19, 0xe3, 0xbb, 0xef, 0xcb, // IID997 + 0xd5, 0xdb, 0x4c, 0x84, 0xd0, 0xee, 0x66, 0xed, 0x52, // IID998 + 0x62, 0xe4, 0xa4, 0x18, 0x4d, 0x91, 0x63, 0x91, 0xe0, 0x1d, // IID999 + 0xd5, 0xbd, 0x4d, 0xb4, 0x78, 0xda, 0xb4, 0xf3, 0x5d, // IID1000 + 0x62, 0x5c, 0x80, 0x18, 0x4e, 0xb4, 0x66, 0x76, 0xb9, 0x9a, 0x5c, // IID1001 + 0xd5, 0xfe, 0x4e, 0x94, 0x9a, 0xd7, 0x3c, 0x27, 0xff, // IID1002 + 0x62, 0x5c, 0x90, 0x10, 0x4f, 0x8c, 0xe6, 0x73, 0x30, 0x56, 0xc8, // IID1003 + 0xd5, 0xc9, 0x4f, 0xa4, 0x58, 0xef, 0x7d, 0xdc, 0x1b, // IID1004 #endif // _LP64 }; @@ -2196,19 +2364,19 @@ 15, // IID265 15, // IID266 11, // IID267 - 11, // IID268 + 9, // IID268 11, // IID269 11, // IID270 11, // IID271 - 11, // IID272 + 9, // IID272 11, // IID273 10, // IID274 11, // IID275 - 11, // IID276 + 9, // IID276 11, // IID277 11, // IID278 11, // IID279 - 11, // IID280 + 9, // IID280 10, // IID281 11, // IID282 10, // IID283 @@ -2216,645 +2384,729 @@ 11, // IID285 10, // IID286 11, // IID287 - 10, // IID288 + 8, // IID288 11, // IID289 11, // IID290 - 10, // IID291 - 10, // IID292 - 8, // IID293 - 7, // IID294 + 11, // IID291 + 9, // IID292 + 11, // IID293 + 11, // IID294 7, // IID295 - 10, // IID296 - 10, // IID297 + 7, // IID296 + 8, // IID297 10, // IID298 - 8, // IID299 + 10, // IID299 10, // IID300 - 10, // IID301 + 4, // IID301 7, // IID302 - 10, // IID303 -#endif // _LP64 - 10, // IID304 -#ifdef _LP64 - 10, // IID305 - 10, // IID306 + 5, // IID303 + 7, // IID304 + 7, // IID305 + 7, // IID306 10, // IID307 - 7, // IID308 + 10, // IID308 10, // IID309 - 10, // IID310 - 8, // IID311 - 7, // IID312 + 7, // IID310 + 7, // IID311 + 10, // IID312 7, // IID313 - 10, // IID314 - 6, // IID315 - 6, // IID316 - 4, // IID317 - 7, // IID318 + 7, // IID314 + 8, // IID315 + 7, // IID316 + 7, // IID317 #endif // _LP64 - 7, // IID319 + 10, // IID318 #ifdef _LP64 - 5, // IID320 - 7, // IID321 + 5, // IID319 + 7, // IID320 + 5, // IID321 7, // IID322 7, // IID323 - 7, // IID324 -#endif // _LP64 + 4, // IID324 7, // IID325 -#ifdef _LP64 - 4, // IID326 - 7, // IID327 + 7, // IID326 + 6, // IID327 7, // IID328 7, // IID329 - 7, // IID330 + 5, // IID330 7, // IID331 - 5, // IID332 + 7, // IID332 7, // IID333 7, // IID334 7, // IID335 - 7, // IID336 + 5, // IID336 7, // IID337 - 4, // IID338 + 7, // IID338 7, // IID339 - 7, // IID340 - 7, // IID341 - 7, // IID342 + 6, // IID340 + 6, // IID341 + 5, // IID342 7, // IID343 - 4, // IID344 + 7, // IID344 7, // IID345 7, // IID346 7, // IID347 - 7, // IID348 + 4, // IID348 7, // IID349 - 5, // IID350 + 7, // IID350 6, // IID351 - 6, // IID352 + 7, // IID352 7, // IID353 - 10, // IID354 - 10, // IID355 - 8, // IID356 - 10, // IID357 + 5, // IID354 + 7, // IID355 + 7, // IID356 + 7, // IID357 10, // IID358 10, // IID359 - 10, // IID360 + 8, // IID360 10, // IID361 -#endif // _LP64 - 6, // IID362 -#ifdef _LP64 - 10, // IID363 + 10, // IID362 + 7, // IID363 10, // IID364 10, // IID365 - 10, // IID366 - 10, // IID367 - 8, // IID368 - 10, // IID369 + 5, // IID366 + 7, // IID367 +#endif // _LP64 + 7, // IID368 +#ifdef _LP64 + 7, // IID369 10, // IID370 10, // IID371 - 10, // IID372 + 8, // IID372 10, // IID373 - 11, // IID374 - 11, // IID375 + 10, // IID374 + 10, // IID375 11, // IID376 - 11, // IID377 + 9, // IID377 11, // IID378 10, // IID379 11, // IID380 - 11, // IID381 + 9, // IID381 11, // IID382 11, // IID383 11, // IID384 - 11, // IID385 + 9, // IID385 11, // IID386 11, // IID387 - 6, // IID388 - 4, // IID389 - 6, // IID390 - 6, // IID391 - 6, // IID392 - 3, // IID393 - 6, // IID394 - 6, // IID395 - 6, // IID396 - 4, // IID397 - 6, // IID398 - 6, // IID399 - 6, // IID400 - 5, // IID401 - 6, // IID402 - 6, // IID403 - 6, // IID404 - 4, // IID405 - 6, // IID406 - 6, // IID407 + 10, // IID388 + 8, // IID389 + 11, // IID390 + 11, // IID391 + 11, // IID392 + 9, // IID393 + 10, // IID394 + 11, // IID395 + 11, // IID396 + 9, // IID397 + 11, // IID398 + 11, // IID399 + 11, // IID400 + 9, // IID401 + 11, // IID402 + 11, // IID403 + 11, // IID404 + 10, // IID405 + 10, // IID406 + 11, // IID407 6, // IID408 4, // IID409 - 6, // IID410 + 4, // IID410 6, // IID411 6, // IID412 - 4, // IID413 + 6, // IID413 6, // IID414 - 6, // IID415 - 6, // IID416 - 4, // IID417 + 4, // IID415 + 3, // IID416 + 6, // IID417 6, // IID418 6, // IID419 6, // IID420 4, // IID421 - 6, // IID422 + 4, // IID422 6, // IID423 - 7, // IID424 - 5, // IID425 - 7, // IID426 - 7, // IID427 - 7, // IID428 - 5, // IID429 - 7, // IID430 - 7, // IID431 + 6, // IID424 + 6, // IID425 + 6, // IID426 + 5, // IID427 + 4, // IID428 + 6, // IID429 + 6, // IID430 +#endif // _LP64 + 6, // IID431 +#ifdef _LP64 6, // IID432 4, // IID433 - 6, // IID434 - 4, // IID435 + 4, // IID434 + 6, // IID435 6, // IID436 - 4, // IID437 + 6, // IID437 6, // IID438 4, // IID439 6, // IID440 - 4, // IID441 + 6, // IID441 6, // IID442 4, // IID443 6, // IID444 - 4, // IID445 + 6, // IID445 6, // IID446 - 4, // IID447 + 3, // IID447 6, // IID448 - 4, // IID449 + 6, // IID449 6, // IID450 4, // IID451 4, // IID452 - 4, // IID453 + 6, // IID453 6, // IID454 - 4, // IID455 - 6, // IID456 - 4, // IID457 - 6, // IID458 - 4, // IID459 - 6, // IID460 - 4, // IID461 - 6, // IID462 - 4, // IID463 - 10, // IID464 - 11, // IID465 - 11, // IID466 - 11, // IID467 - 10, // IID468 - 11, // IID469 - 11, // IID470 - 11, // IID471 - 11, // IID472 - 11, // IID473 - 11, // IID474 - 11, // IID475 - 11, // IID476 - 11, // IID477 - 11, // IID478 - 11, // IID479 - 4, // IID480 + 6, // IID455 + 7, // IID456 + 5, // IID457 + 7, // IID458 + 7, // IID459 + 7, // IID460 + 5, // IID461 + 7, // IID462 + 7, // IID463 + 6, // IID464 + 4, // IID465 + 6, // IID466 + 4, // IID467 + 6, // IID468 + 4, // IID469 + 6, // IID470 + 4, // IID471 + 6, // IID472 + 4, // IID473 + 6, // IID474 + 4, // IID475 + 6, // IID476 + 4, // IID477 + 6, // IID478 + 4, // IID479 + 6, // IID480 4, // IID481 - 4, // IID482 - 5, // IID483 - 4, // IID484 + 6, // IID482 + 4, // IID483 + 6, // IID484 4, // IID485 - 5, // IID486 - 5, // IID487 - 4, // IID488 + 6, // IID486 + 4, // IID487 + 6, // IID488 4, // IID489 - 4, // IID490 - 3, // IID491 - 4, // IID492 + 6, // IID490 + 4, // IID491 + 6, // IID492 4, // IID493 - 4, // IID494 + 6, // IID494 4, // IID495 - 4, // IID496 - 4, // IID497 - 9, // IID498 + 11, // IID496 + 9, // IID497 + 11, // IID498 9, // IID499 - 9, // IID500 + 11, // IID500 9, // IID501 - 9, // IID502 - 9, // IID503 - 9, // IID504 + 11, // IID502 + 8, // IID503 + 11, // IID504 9, // IID505 - 12, // IID506 - 12, // IID507 + 11, // IID506 + 8, // IID507 11, // IID508 - 8, // IID509 - 9, // IID510 - 13, // IID511 - 10, // IID512 - 12, // IID513 - 10, // IID514 - 10, // IID515 - 13, // IID516 - 12, // IID517 - 9, // IID518 + 9, // IID509 + 10, // IID510 + 9, // IID511 + 11, // IID512 + 9, // IID513 + 9, // IID514 + 9, // IID515 + 11, // IID516 + 9, // IID517 + 11, // IID518 9, // IID519 - 9, // IID520 + 11, // IID520 9, // IID521 - 9, // IID522 - 7, // IID523 - 9, // IID524 + 10, // IID522 + 9, // IID523 + 10, // IID524 9, // IID525 - 9, // IID526 + 11, // IID526 9, // IID527 - 10, // IID528 - 9, // IID529 - 9, // IID530 - 7, // IID531 - 10, // IID532 - 9, // IID533 - 9, // IID534 + 4, // IID528 + 3, // IID529 + 4, // IID530 + 5, // IID531 + 3, // IID532 + 4, // IID533 + 5, // IID534 5, // IID535 - 5, // IID536 - 8, // IID537 - 8, // IID538 + 4, // IID536 + 3, // IID537 + 4, // IID538 4, // IID539 4, // IID540 - 5, // IID541 - 5, // IID542 + 4, // IID541 + 4, // IID542 4, // IID543 4, // IID544 - 7, // IID545 - 4, // IID546 - 3, // IID547 - 4, // IID548 + 4, // IID545 + 8, // IID546 + 9, // IID547 + 9, // IID548 8, // IID549 8, // IID550 - 10, // IID551 - 5, // IID552 - 8, // IID553 - 8, // IID554 - 7, // IID555 - 9, // IID556 - 8, // IID557 - 9, // IID558 - 9, // IID559 - 9, // IID560 - 9, // IID561 - 8, // IID562 - 9, // IID563 - 9, // IID564 - 9, // IID565 + 9, // IID551 + 9, // IID552 + 9, // IID553 + 13, // IID554 + 13, // IID555 + 11, // IID556 + 10, // IID557 + 10, // IID558 + 13, // IID559 + 10, // IID560 + 13, // IID561 + 10, // IID562 + 13, // IID563 + 11, // IID564 + 13, // IID565 9, // IID566 9, // IID567 9, // IID568 9, // IID569 9, // IID570 9, // IID571 - 4, // IID572 - 4, // IID573 - 4, // IID574 - 3, // IID575 - 3, // IID576 - 4, // IID577 - 4, // IID578 - 3, // IID579 - 4, // IID580 - 3, // IID581 - 4, // IID582 - 4, // IID583 - 4, // IID584 - 3, // IID585 - 3, // IID586 - 3, // IID587 + 9, // IID572 + 9, // IID573 + 8, // IID574 + 9, // IID575 + 10, // IID576 + 9, // IID577 + 9, // IID578 + 9, // IID579 + 10, // IID580 + 9, // IID581 + 9, // IID582 + 8, // IID583 + 5, // IID584 + 8, // IID585 + 7, // IID586 + 4, // IID587 3, // IID588 - 9, // IID589 - 9, // IID590 - 9, // IID591 - 9, // IID592 - 9, // IID593 - 9, // IID594 - 9, // IID595 - 9, // IID596 - 13, // IID597 + 5, // IID589 + 4, // IID590 + 4, // IID591 + 5, // IID592 + 7, // IID593 + 5, // IID594 + 5, // IID595 + 7, // IID596 + 7, // IID597 8, // IID598 - 5, // IID599 + 11, // IID599 5, // IID600 - 6, // IID601 - 6, // IID602 - 6, // IID603 - 6, // IID604 + 8, // IID601 + 8, // IID602 + 8, // IID603 + 9, // IID604 9, // IID605 9, // IID606 9, // IID607 9, // IID608 - 4, // IID609 - 4, // IID610 - 4, // IID611 - 4, // IID612 + 8, // IID609 + 9, // IID610 + 9, // IID611 + 9, // IID612 9, // IID613 - 6, // IID614 - 6, // IID615 - 6, // IID616 - 6, // IID617 - 6, // IID618 - 6, // IID619 - 6, // IID620 - 6, // IID621 - 11, // IID622 - 11, // IID623 - 6, // IID624 - 4, // IID625 - 6, // IID626 - 6, // IID627 - 6, // IID628 - 6, // IID629 - 6, // IID630 - 6, // IID631 - 6, // IID632 + 9, // IID614 + 9, // IID615 + 9, // IID616 + 8, // IID617 + 9, // IID618 + 9, // IID619 + 4, // IID620 + 3, // IID621 + 4, // IID622 + 3, // IID623 + 4, // IID624 + 3, // IID625 + 3, // IID626 + 4, // IID627 + 4, // IID628 + 3, // IID629 + 4, // IID630 + 4, // IID631 + 4, // IID632 4, // IID633 - 6, // IID634 - 6, // IID635 - 6, // IID636 - 4, // IID637 - 6, // IID638 - 6, // IID639 - 6, // IID640 - 6, // IID641 - 6, // IID642 - 4, // IID643 - 6, // IID644 - 6, // IID645 - 6, // IID646 - 4, // IID647 - 6, // IID648 + 3, // IID634 + 3, // IID635 + 3, // IID636 + 9, // IID637 + 8, // IID638 + 9, // IID639 + 8, // IID640 + 9, // IID641 + 8, // IID642 + 7, // IID643 + 9, // IID644 + 13, // IID645 + 8, // IID646 + 5, // IID647 + 5, // IID648 6, // IID649 6, // IID650 - 3, // IID651 + 6, // IID651 6, // IID652 - 6, // IID653 - 6, // IID654 - 3, // IID655 - 6, // IID656 - 6, // IID657 - 6, // IID658 - 3, // IID659 - 6, // IID660 - 6, // IID661 - 4, // IID662 - 4, // IID663 + 9, // IID653 + 9, // IID654 + 8, // IID655 + 9, // IID656 + 4, // IID657 + 4, // IID658 + 4, // IID659 + 4, // IID660 + 9, // IID661 + 6, // IID662 + 6, // IID663 6, // IID664 6, // IID665 6, // IID666 - 3, // IID667 + 6, // IID667 6, // IID668 6, // IID669 - 6, // IID670 - 4, // IID671 + 11, // IID670 + 11, // IID671 6, // IID672 - 6, // IID673 + 4, // IID673 6, // IID674 6, // IID675 6, // IID676 6, // IID677 - 10, // IID678 - 10, // IID679 - 11, // IID680 - 11, // IID681 - 11, // IID682 - 11, // IID683 - 11, // IID684 - 10, // IID685 - 10, // IID686 - 11, // IID687 - 11, // IID688 - 11, // IID689 - 11, // IID690 - 11, // IID691 - 11, // IID692 - 11, // IID693 - 11, // IID694 - 10, // IID695 - 11, // IID696 - 11, // IID697 - 10, // IID698 - 11, // IID699 - 11, // IID700 - 10, // IID701 - 11, // IID702 - 11, // IID703 - 11, // IID704 - 10, // IID705 - 11, // IID706 - 11, // IID707 - 11, // IID708 - 11, // IID709 - 11, // IID710 - 11, // IID711 - 11, // IID712 - 11, // IID713 - 10, // IID714 - 11, // IID715 - 11, // IID716 - 10, // IID717 - 15, // IID718 - 12, // IID719 - 12, // IID720 - 15, // IID721 - 15, // IID722 - 12, // IID723 - 12, // IID724 - 15, // IID725 - 12, // IID726 - 12, // IID727 - 12, // IID728 - 11, // IID729 - 12, // IID730 - 12, // IID731 - 14, // IID732 - 15, // IID733 - 15, // IID734 - 12, // IID735 - 7, // IID736 - 7, // IID737 - 8, // IID738 - 10, // IID739 - 10, // IID740 - 10, // IID741 - 10, // IID742 - 10, // IID743 - 8, // IID744 - 10, // IID745 - 10, // IID746 - 10, // IID747 - 10, // IID748 - 10, // IID749 - 10, // IID750 - 10, // IID751 - 10, // IID752 - 10, // IID753 - 10, // IID754 - 10, // IID755 - 8, // IID756 - 10, // IID757 - 10, // IID758 + 6, // IID678 + 6, // IID679 + 6, // IID680 + 4, // IID681 + 6, // IID682 + 6, // IID683 + 6, // IID684 + 4, // IID685 + 6, // IID686 + 6, // IID687 + 6, // IID688 + 6, // IID689 + 6, // IID690 + 4, // IID691 + 6, // IID692 + 6, // IID693 + 6, // IID694 + 3, // IID695 + 6, // IID696 + 6, // IID697 + 6, // IID698 + 4, // IID699 + 6, // IID700 + 6, // IID701 + 6, // IID702 + 4, // IID703 + 6, // IID704 + 6, // IID705 + 6, // IID706 + 3, // IID707 + 6, // IID708 + 6, // IID709 + 6, // IID710 + 4, // IID711 + 6, // IID712 + 6, // IID713 + 6, // IID714 + 3, // IID715 + 6, // IID716 + 6, // IID717 + 6, // IID718 + 3, // IID719 + 6, // IID720 + 6, // IID721 + 6, // IID722 + 6, // IID723 + 6, // IID724 + 6, // IID725 + 11, // IID726 + 11, // IID727 + 11, // IID728 + 10, // IID729 + 11, // IID730 + 11, // IID731 + 11, // IID732 + 10, // IID733 + 11, // IID734 + 11, // IID735 + 11, // IID736 + 11, // IID737 + 11, // IID738 + 11, // IID739 + 11, // IID740 + 11, // IID741 + 11, // IID742 + 11, // IID743 + 11, // IID744 + 11, // IID745 + 11, // IID746 + 8, // IID747 + 11, // IID748 + 11, // IID749 + 11, // IID750 + 9, // IID751 + 11, // IID752 + 11, // IID753 + 11, // IID754 + 9, // IID755 + 11, // IID756 + 11, // IID757 + 11, // IID758 10, // IID759 - 7, // IID760 - 7, // IID761 - 4, // IID762 - 7, // IID763 - 7, // IID764 - 4, // IID765 - 6, // IID766 - 6, // IID767 - 7, // IID768 - 7, // IID769 - 7, // IID770 - 5, // IID771 - 6, // IID772 - 6, // IID773 - 6, // IID774 - 7, // IID775 - 7, // IID776 - 5, // IID777 - 7, // IID778 - 7, // IID779 - 6, // IID780 - 7, // IID781 - 7, // IID782 - 4, // IID783 - 6, // IID784 - 6, // IID785 - 7, // IID786 - 7, // IID787 - 7, // IID788 - 4, // IID789 - 7, // IID790 - 7, // IID791 + 11, // IID760 + 11, // IID761 + 11, // IID762 + 9, // IID763 + 11, // IID764 + 11, // IID765 + 14, // IID766 + 15, // IID767 + 12, // IID768 + 12, // IID769 + 15, // IID770 + 15, // IID771 + 15, // IID772 + 15, // IID773 + 11, // IID774 + 12, // IID775 + 11, // IID776 + 12, // IID777 + 12, // IID778 + 12, // IID779 + 11, // IID780 + 15, // IID781 + 15, // IID782 + 12, // IID783 + 10, // IID784 + 10, // IID785 + 8, // IID786 + 10, // IID787 + 10, // IID788 + 7, // IID789 + 10, // IID790 + 10, // IID791 7, // IID792 - 7, // IID793 - 7, // IID794 - 4, // IID795 - 7, // IID796 - 7, // IID797 - 6, // IID798 - 10, // IID799 - 10, // IID800 - 5, // IID801 - 7, // IID802 - 7, // IID803 - 10, // IID804 + 10, // IID793 + 10, // IID794 + 10, // IID795 + 10, // IID796 + 10, // IID797 + 10, // IID798 + 7, // IID799 + 7, // IID800 + 10, // IID801 + 10, // IID802 + 10, // IID803 + 4, // IID804 10, // IID805 10, // IID806 - 8, // IID807 - 10, // IID808 - 10, // IID809 - 10, // IID810 - 7, // IID811 - 10, // IID812 - 8, // IID813 - 10, // IID814 - 10, // IID815 - 8, // IID816 - 10, // IID817 - 10, // IID818 - 8, // IID819 - 10, // IID820 - 10, // IID821 - 10, // IID822 - 11, // IID823 - 11, // IID824 - 10, // IID825 - 11, // IID826 - 11, // IID827 - 11, // IID828 - 11, // IID829 - 11, // IID830 - 10, // IID831 - 11, // IID832 - 11, // IID833 - 10, // IID834 - 6, // IID835 - 3, // IID836 - 6, // IID837 + 7, // IID807 + 7, // IID808 + 7, // IID809 + 4, // IID810 + 6, // IID811 + 6, // IID812 + 5, // IID813 + 7, // IID814 + 7, // IID815 + 7, // IID816 + 7, // IID817 + 7, // IID818 + 5, // IID819 + 7, // IID820 + 7, // IID821 + 7, // IID822 + 7, // IID823 + 7, // IID824 + 4, // IID825 + 6, // IID826 + 6, // IID827 + 7, // IID828 + 6, // IID829 + 6, // IID830 + 5, // IID831 + 7, // IID832 + 7, // IID833 + 7, // IID834 + 7, // IID835 + 7, // IID836 + 4, // IID837 6, // IID838 6, // IID839 - 6, // IID840 - 6, // IID841 - 6, // IID842 - 6, // IID843 - 4, // IID844 - 6, // IID845 - 6, // IID846 - 6, // IID847 - 4, // IID848 - 6, // IID849 - 6, // IID850 - 6, // IID851 - 3, // IID852 - 6, // IID853 - 6, // IID854 - 6, // IID855 - 3, // IID856 - 6, // IID857 - 6, // IID858 - 6, // IID859 - 4, // IID860 - 6, // IID861 - 6, // IID862 - 7, // IID863 - 5, // IID864 - 7, // IID865 - 7, // IID866 - 7, // IID867 - 5, // IID868 - 7, // IID869 - 7, // IID870 - 6, // IID871 - 4, // IID872 - 6, // IID873 - 4, // IID874 - 6, // IID875 - 4, // IID876 - 6, // IID877 - 4, // IID878 - 6, // IID879 - 4, // IID880 - 6, // IID881 - 4, // IID882 - 6, // IID883 - 4, // IID884 - 6, // IID885 - 4, // IID886 - 6, // IID887 - 4, // IID888 - 6, // IID889 - 4, // IID890 - 6, // IID891 - 4, // IID892 - 6, // IID893 - 4, // IID894 + 7, // IID840 + 7, // IID841 + 7, // IID842 + 4, // IID843 + 7, // IID844 + 7, // IID845 + 7, // IID846 + 7, // IID847 + 7, // IID848 + 7, // IID849 + 10, // IID850 + 10, // IID851 + 10, // IID852 + 10, // IID853 + 10, // IID854 + 7, // IID855 + 7, // IID856 + 7, // IID857 + 10, // IID858 + 10, // IID859 + 10, // IID860 + 7, // IID861 + 10, // IID862 + 10, // IID863 + 7, // IID864 + 10, // IID865 + 10, // IID866 + 8, // IID867 + 10, // IID868 + 10, // IID869 + 10, // IID870 + 11, // IID871 + 9, // IID872 + 11, // IID873 + 11, // IID874 + 11, // IID875 + 9, // IID876 + 10, // IID877 + 11, // IID878 + 11, // IID879 + 9, // IID880 + 11, // IID881 + 10, // IID882 + 11, // IID883 + 9, // IID884 + 11, // IID885 + 11, // IID886 + 11, // IID887 + 9, // IID888 + 11, // IID889 + 11, // IID890 + 10, // IID891 + 9, // IID892 + 10, // IID893 + 11, // IID894 6, // IID895 4, // IID896 - 6, // IID897 - 4, // IID898 + 3, // IID897 + 6, // IID898 6, // IID899 - 4, // IID900 + 6, // IID900 6, // IID901 - 4, // IID902 - 11, // IID903 - 11, // IID904 - 11, // IID905 - 10, // IID906 - 11, // IID907 - 9, // IID908 - 11, // IID909 - 11, // IID910 - 11, // IID911 - 10, // IID912 - 11, // IID913 - 11, // IID914 - 11, // IID915 - 11, // IID916 - 11, // IID917 - 11, // IID918 + 6, // IID902 + 6, // IID903 + 6, // IID904 + 6, // IID905 + 4, // IID906 + 4, // IID907 + 6, // IID908 + 6, // IID909 + 6, // IID910 + 6, // IID911 + 4, // IID912 + 4, // IID913 + 6, // IID914 + 6, // IID915 + 6, // IID916 + 6, // IID917 + 4, // IID918 + 4, // IID919 + 6, // IID920 + 6, // IID921 + 6, // IID922 + 6, // IID923 + 4, // IID924 + 6, // IID925 + 6, // IID926 + 4, // IID927 + 3, // IID928 + 4, // IID929 + 6, // IID930 + 6, // IID931 + 6, // IID932 + 7, // IID933 + 5, // IID934 + 7, // IID935 + 7, // IID936 + 5, // IID937 + 5, // IID938 + 7, // IID939 + 7, // IID940 + 6, // IID941 + 4, // IID942 + 6, // IID943 + 4, // IID944 + 6, // IID945 + 4, // IID946 + 6, // IID947 + 4, // IID948 + 6, // IID949 + 4, // IID950 + 6, // IID951 + 4, // IID952 + 6, // IID953 + 4, // IID954 + 6, // IID955 + 4, // IID956 + 6, // IID957 + 4, // IID958 + 6, // IID959 + 4, // IID960 + 6, // IID961 + 4, // IID962 + 6, // IID963 + 4, // IID964 + 6, // IID965 + 4, // IID966 + 6, // IID967 + 4, // IID968 + 6, // IID969 + 4, // IID970 + 6, // IID971 + 4, // IID972 + 11, // IID973 + 9, // IID974 + 10, // IID975 + 9, // IID976 + 11, // IID977 + 9, // IID978 + 11, // IID979 + 9, // IID980 + 11, // IID981 + 9, // IID982 + 11, // IID983 + 9, // IID984 + 11, // IID985 + 9, // IID986 + 10, // IID987 + 9, // IID988 + 11, // IID989 + 9, // IID990 + 11, // IID991 + 9, // IID992 + 11, // IID993 + 9, // IID994 + 11, // IID995 + 9, // IID996 + 11, // IID997 + 9, // IID998 + 10, // IID999 + 9, // IID1000 + 11, // IID1001 + 9, // IID1002 + 11, // IID1003 + 9, // IID1004 #endif // _LP64 }; @@ -3152,662 +3404,746 @@ "__ eaddl(r28, Address(r24, rcx, (Address::ScaleFactor)3, -0x6053edc2), r28, false);", // IID268 "__ eaddl(r17, Address(r18, r24, (Address::ScaleFactor)3, -0x1bf71f78), r29, true);", // IID269 "__ eaddl(rcx, Address(r15, r28, (Address::ScaleFactor)1, +0x15b8216), rcx, true);", // IID270 - "__ eorl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false);", // IID271 - "__ eorl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false);", // IID272 - "__ eorl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true);", // IID273 - "__ eorl(r16, Address(r23, -0x165064ff), r16, true);", // IID274 - "__ eorb(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false);", // IID275 - "__ eorb(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false);", // IID276 - "__ eorb(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true);", // IID277 - "__ eorb(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true);", // IID278 - "__ esubl(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false);", // IID279 - "__ esubl(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false);", // IID280 - "__ esubl(r21, Address(r9, -0x1050127a), r13, true);", // IID281 - "__ esubl(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true);", // IID282 - "__ exorl(r15, Address(r18, +0x5c2bbce5), r12, false);", // IID283 - "__ exorl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false);", // IID284 - "__ exorl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true);", // IID285 - "__ exorl(r9, Address(r15, +0x775acdad), r9, true);", // IID286 - "__ exorb(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false);", // IID287 - "__ exorb(r10, Address(r27, +0xa3150de), r10, false);", // IID288 - "__ exorb(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true);", // IID289 - "__ exorb(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true);", // IID290 - "__ eaddl(r21, r15, 1048576, false);", // IID291 - "__ eaddl(rax, r18, 1048576, false);", // IID292 - "__ eaddl(r18, r18, 256, false);", // IID293 - "__ eaddl(r13, r19, 16, true);", // IID294 - "__ eaddl(rax, r23, 16, true);", // IID295 - "__ eaddl(r25, r25, 16777216, true);", // IID296 - "__ eandl(r29, r18, 1048576, false);", // IID297 - "__ eandl(rax, r14, 1048576, false);", // IID298 - "__ eandl(r19, r19, 65536, false);", // IID299 - "__ eandl(r27, r25, 1048576, true);", // IID300 - "__ eandl(rax, r20, 1048576, true);", // IID301 - "__ eandl(r28, r28, 16, true);", // IID302 - "__ eimull(r31, r22, 4096, false);", // IID303 + "__ eandl(r30, Address(rbx, rdx, (Address::ScaleFactor)3, -0x463540b4), r28, false);", // IID271 + "__ eandl(r18, Address(r28, r10, (Address::ScaleFactor)3, +0x3523a73b), r18, false);", // IID272 + "__ eandl(r9, Address(r15, r15, (Address::ScaleFactor)1, -0x2a0bdd56), r21, true);", // IID273 + "__ eandl(r16, Address(r23, -0x165064ff), r16, true);", // IID274 + "__ eorl(r28, Address(r30, r11, (Address::ScaleFactor)0, +0x17281e3a), r20, false);", // IID275 + "__ eorl(rdx, Address(rbx, r31, (Address::ScaleFactor)2, +0x2477b5bb), rdx, false);", // IID276 + "__ eorl(r16, Address(r11, rcx, (Address::ScaleFactor)1, -0x3175d1af), r24, true);", // IID277 + "__ eorl(rbx, Address(r11, r20, (Address::ScaleFactor)3, -0x22d67bd3), rbx, true);", // IID278 + "__ eorb(r26, Address(r27, r30, (Address::ScaleFactor)1, -0x3d9bce2e), rdx, false);", // IID279 + "__ eorb(r31, Address(r22, r29, (Address::ScaleFactor)1, +0x14218519), r31, false);", // IID280 + "__ eorb(r21, Address(r9, -0x1050127a), r13, true);", // IID281 + "__ eorb(r31, Address(r9, r8, (Address::ScaleFactor)0, -0xae18961), r31, true);", // IID282 + "__ esubl(r15, Address(r18, +0x5c2bbce5), r12, false);", // IID283 + "__ esubl(r27, Address(r25, r23, (Address::ScaleFactor)0, +0x5c6078b3), r27, false);", // IID284 + "__ esubl(r18, Address(r8, rdx, (Address::ScaleFactor)3, -0x9ed3881), r14, true);", // IID285 + "__ esubl(r9, Address(r15, +0x775acdad), r9, true);", // IID286 + "__ exorl(r21, Address(r18, r26, (Address::ScaleFactor)1, +0x2fe31fd5), r23, false);", // IID287 + "__ exorl(r10, Address(r27, +0xa3150de), r10, false);", // IID288 + "__ exorl(r18, Address(r22, r30, (Address::ScaleFactor)3, +0x1ad4e897), r24, true);", // IID289 + "__ exorl(r8, Address(r16, r20, (Address::ScaleFactor)0, +0x626eae82), r8, true);", // IID290 + "__ exorb(r16, Address(r21, r15, (Address::ScaleFactor)0, -0x1403b60d), r18, false);", // IID291 + "__ exorb(r13, Address(r19, r23, (Address::ScaleFactor)2, +0x237ef1e1), r13, false);", // IID292 + "__ exorb(r29, Address(r18, r14, (Address::ScaleFactor)2, +0x5cc0095b), r14, true);", // IID293 + "__ exorb(r27, Address(r25, r20, (Address::ScaleFactor)3, +0x1cf7b958), r27, true);", // IID294 + "__ eaddl(r16, r24, 16, false);", // IID295 + "__ eaddl(rax, r24, 16, false);", // IID296 + "__ eaddl(r21, r21, 65536, false);", // IID297 + "__ eaddl(r24, r8, 1048576, true);", // IID298 + "__ eaddl(rax, r13, 1048576, true);", // IID299 + "__ eaddl(r29, r29, 16777216, true);", // IID300 + "__ eandl(r12, r12, 16, false);", // IID301 + "__ eandl(rax, r30, 16, false);", // IID302 + "__ eandl(r24, r24, 16, false);", // IID303 + "__ eandl(r8, r12, 1, true);", // IID304 + "__ eandl(rax, r13, 1, true);", // IID305 + "__ eandl(r25, r25, 16, true);", // IID306 + "__ eimull(r18, r23, 65536, false);", // IID307 + "__ eimull(rax, r9, 65536, false);", // IID308 + "__ eimull(r26, r26, 268435456, false);", // IID309 + "__ eimull(r25, r21, 1, true);", // IID310 + "__ eimull(rax, r24, 1, true);", // IID311 + "__ eimull(r24, r24, 16777216, true);", // IID312 + "__ eorl(r30, r26, 1, false);", // IID313 + "__ eorl(rax, r22, 1, false);", // IID314 + "__ eorl(r17, r17, 1048576, false);", // IID315 + "__ eorl(r24, r8, 1, true);", // IID316 + "__ eorl(rax, r27, 1, true);", // IID317 #endif // _LP64 - "__ eimull(rax, rbx, 4096, false);", // IID304 + "__ eorl(rdx, rdx, 268435456, true);", // IID318 #ifdef _LP64 - "__ eimull(r24, r24, 1048576, false);", // IID305 - "__ eimull(r21, r16, 65536, true);", // IID306 - "__ eimull(rax, r24, 65536, true);", // IID307 - "__ eimull(r13, r13, 16, true);", // IID308 - "__ eorl(r29, r8, 16777216, false);", // IID309 - "__ eorl(rax, r12, 16777216, false);", // IID310 - "__ eorl(r30, r30, 4096, false);", // IID311 - "__ eorl(r24, rdx, 16, true);", // IID312 - "__ eorl(rax, r8, 16, true);", // IID313 - "__ eorl(r13, r13, 4096, true);", // IID314 - "__ ercll(r25, r13, 1);", // IID315 - "__ ercll(rax, r18, 1);", // IID316 - "__ ercll(r9, r9, 16);", // IID317 - "__ eroll(r26, r25, 8, false);", // IID318 + "__ ercll(r22, r22, 8);", // IID319 + "__ ercll(rax, r23, 8);", // IID320 + "__ ercll(r19, r19, 4);", // IID321 + "__ eroll(r30, r24, 2, false);", // IID322 + "__ eroll(rax, r29, 2, false);", // IID323 + "__ eroll(r8, r8, 2, false);", // IID324 + "__ eroll(r18, r24, 16, true);", // IID325 + "__ eroll(rax, r13, 16, true);", // IID326 + "__ eroll(r24, r24, 1, true);", // IID327 + "__ erorl(r28, r17, 16, false);", // IID328 + "__ erorl(rax, r24, 16, false);", // IID329 + "__ erorl(r17, r17, 4, false);", // IID330 + "__ erorl(r24, rcx, 4, true);", // IID331 + "__ erorl(rax, r16, 4, true);", // IID332 + "__ erorl(r15, r15, 2, true);", // IID333 + "__ esall(r14, r27, 4, false);", // IID334 + "__ esall(rax, r23, 4, false);", // IID335 + "__ esall(r30, r30, 4, false);", // IID336 + "__ esall(r27, rdx, 2, true);", // IID337 + "__ esall(rax, r19, 2, true);", // IID338 + "__ esall(r20, r20, 2, true);", // IID339 + "__ esarl(r21, r23, 1, false);", // IID340 + "__ esarl(rax, r30, 1, false);", // IID341 + "__ esarl(r25, r25, 2, false);", // IID342 + "__ esarl(r24, r19, 4, true);", // IID343 + "__ esarl(rax, r14, 4, true);", // IID344 + "__ esarl(r26, r26, 16, true);", // IID345 + "__ eshll(r22, r13, 8, false);", // IID346 + "__ eshll(rax, r24, 8, false);", // IID347 + "__ eshll(r14, r14, 16, false);", // IID348 + "__ eshll(r28, r25, 8, true);", // IID349 + "__ eshll(rax, r10, 8, true);", // IID350 + "__ eshll(r20, r20, 1, true);", // IID351 + "__ eshrl(r12, rbx, 4, false);", // IID352 + "__ eshrl(rax, r23, 4, false);", // IID353 + "__ eshrl(r28, r28, 16, false);", // IID354 + "__ eshrl(r24, r30, 4, true);", // IID355 + "__ eshrl(rax, r31, 4, true);", // IID356 + "__ eshrl(r31, r31, 2, true);", // IID357 + "__ esubl(r20, r10, 256, false);", // IID358 + "__ esubl(rax, r13, 256, false);", // IID359 + "__ esubl(r25, r25, 256, false);", // IID360 + "__ esubl(r23, r12, 268435456, true);", // IID361 + "__ esubl(rax, r16, 268435456, true);", // IID362 + "__ esubl(r31, r31, 1, true);", // IID363 + "__ exorl(r9, r15, 16777216, false);", // IID364 + "__ exorl(rax, r13, 16777216, false);", // IID365 + "__ exorl(r28, r28, 16, false);", // IID366 + "__ exorl(r29, r22, 16, true);", // IID367 #endif // _LP64 - "__ eroll(rax, rdx, 8, false);", // IID319 + "__ exorl(rax, rbx, 16, true);", // IID368 #ifdef _LP64 - "__ eroll(r24, r24, 16, false);", // IID320 - "__ eroll(r24, rcx, 8, true);", // IID321 - "__ eroll(rax, r30, 8, true);", // IID322 - "__ eroll(r28, r28, 16, true);", // IID323 - "__ erorl(r17, r28, 4, false);", // IID324 + "__ exorl(r8, r8, 16, true);", // IID369 + "__ esubl_imm32(r16, r13, 4194304, false);", // IID370 + "__ esubl_imm32(rax, r12, 4194304, false);", // IID371 + "__ esubl_imm32(r17, r17, 67108864, false);", // IID372 + "__ esubl_imm32(r22, r26, 1073741824, true);", // IID373 + "__ esubl_imm32(rax, r10, 1073741824, true);", // IID374 + "__ esubl_imm32(r11, r11, 1073741824, true);", // IID375 + "__ eaddl(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false);", // IID376 + "__ eaddl(r30, r30, Address(r18, r19, (Address::ScaleFactor)2, +0x25f990cf), false);", // IID377 + "__ eaddl(rcx, r25, Address(r19, r16, (Address::ScaleFactor)0, +0x482d5dbc), true);", // IID378 + "__ eaddl(r9, r9, Address(r11, +0x43d5ee01), true);", // IID379 + "__ eandl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false);", // IID380 + "__ eandl(r27, r27, Address(r13, r15, (Address::ScaleFactor)3, -0x1268b895), false);", // IID381 + "__ eandl(r9, r23, Address(r22, r30, (Address::ScaleFactor)0, -0x715acbb), true);", // IID382 + "__ eandl(rbx, rbx, Address(r28, r16, (Address::ScaleFactor)2, +0xb0223ee), true);", // IID383 + "__ eimull(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false);", // IID384 + "__ eimull(r17, r17, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), false);", // IID385 + "__ eimull(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), true);", // IID386 + "__ eimull(rbx, rbx, Address(r28, r22, (Address::ScaleFactor)3, -0x519d9a27), true);", // IID387 + "__ eorl(r17, rcx, Address(r14, +0x10642223), false);", // IID388 + "__ eorl(r26, r26, Address(r31, -0x7a9a83ba), false);", // IID389 + "__ eorl(r15, r22, Address(r12, r12, (Address::ScaleFactor)2, +0x743b6997), true);", // IID390 + "__ eorl(r8, r8, Address(rdx, r22, (Address::ScaleFactor)3, -0x588414dc), true);", // IID391 + "__ esubl(rcx, r28, Address(r30, r13, (Address::ScaleFactor)2, +0xe9310e5), false);", // IID392 + "__ esubl(rcx, rcx, Address(r30, r10, (Address::ScaleFactor)1, -0x1b076ed1), false);", // IID393 + "__ esubl(r9, r21, Address(r30, +0x2f79ffd3), true);", // IID394 + "__ esubl(r16, r16, Address(rdx, r14, (Address::ScaleFactor)2, +0x675d71c1), true);", // IID395 + "__ exorl(r27, r28, Address(rbx, r26, (Address::ScaleFactor)2, -0x78c20b81), false);", // IID396 + "__ exorl(r14, r14, Address(r31, r19, (Address::ScaleFactor)1, -0x4ff251cc), false);", // IID397 + "__ exorl(r20, r18, Address(r13, r16, (Address::ScaleFactor)2, -0x19efc6e2), true);", // IID398 + "__ exorl(r19, r19, Address(r13, r23, (Address::ScaleFactor)1, -0x2d1bd8aa), true);", // IID399 + "__ exorb(r29, r17, Address(rdx, r29, (Address::ScaleFactor)2, +0x66573e84), false);", // IID400 + "__ exorb(r22, r22, Address(r24, r25, (Address::ScaleFactor)3, +0x3a94a93f), false);", // IID401 + "__ exorb(r13, r29, Address(r15, r23, (Address::ScaleFactor)1, +0x76d43532), true);", // IID402 + "__ exorb(r15, r15, Address(r13, r9, (Address::ScaleFactor)0, -0x474e6d1a), true);", // IID403 + "__ exorw(r17, r16, Address(r23, rdx, (Address::ScaleFactor)0, +0x562a291), false);", // IID404 + "__ exorw(r29, r29, Address(r18, r28, (Address::ScaleFactor)3, -0x541967f2), false);", // IID405 + "__ exorw(r27, r11, Address(r10, +0xa911c5a), true);", // IID406 + "__ exorw(r31, r31, Address(r30, r19, (Address::ScaleFactor)2, -0xf6a3da), true);", // IID407 + "__ eaddl(r12, r13, r23, false);", // IID408 + "__ eaddl(r28, r28, r20, false);", // IID409 + "__ eaddl(r20, r24, r20, false);", // IID410 + "__ eaddl(r11, r10, r15, true);", // IID411 + "__ eaddl(r19, r19, r20, true);", // IID412 + "__ eaddl(r23, r15, r23, true);", // IID413 + "__ eandl(r26, r19, r24, false);", // IID414 + "__ eandl(r23, r23, r28, false);", // IID415 + "__ eandl(r11, r13, r11, false);", // IID416 + "__ eandl(r13, rdx, r31, true);", // IID417 + "__ eandl(r23, r23, r23, true);", // IID418 + "__ eandl(r9, r27, r9, true);", // IID419 + "__ eimull(r21, r20, r24, false);", // IID420 + "__ eimull(r21, r21, r29, false);", // IID421 + "__ eimull(rbx, r11, rbx, false);", // IID422 + "__ eimull(r21, rbx, rcx, true);", // IID423 + "__ eimull(r31, r31, r21, true);", // IID424 + "__ eimull(r15, r25, r15, true);", // IID425 + "__ eorw(r30, r23, r25, false);", // IID426 + "__ eorw(r18, r18, rcx, false);", // IID427 + "__ eorw(r10, rcx, r10, false);", // IID428 + "__ eorw(r31, r21, r26, true);", // IID429 + "__ eorw(r21, r21, r19, true);", // IID430 #endif // _LP64 - "__ erorl(rax, rdx, 4, false);", // IID325 + "__ eorw(rdx, rbx, rdx, true);", // IID431 #ifdef _LP64 - "__ erorl(r8, r8, 16, false);", // IID326 - "__ erorl(r19, rdx, 16, true);", // IID327 - "__ erorl(rax, r31, 16, true);", // IID328 - "__ erorl(r22, r22, 8, true);", // IID329 - "__ esall(r23, r25, 16, false);", // IID330 - "__ esall(rax, r14, 16, false);", // IID331 - "__ esall(r31, r31, 8, false);", // IID332 - "__ esall(r30, r24, 2, true);", // IID333 - "__ esall(rax, r29, 2, true);", // IID334 - "__ esall(r8, r8, 2, true);", // IID335 - "__ esarl(r18, r24, 16, false);", // IID336 - "__ esarl(rax, r13, 16, false);", // IID337 - "__ esarl(r24, r24, 1, false);", // IID338 - "__ esarl(r28, r17, 16, true);", // IID339 - "__ esarl(rax, r24, 16, true);", // IID340 - "__ esarl(r17, r17, 4, true);", // IID341 - "__ eshll(r24, rcx, 4, false);", // IID342 - "__ eshll(rax, r16, 4, false);", // IID343 - "__ eshll(r15, r15, 2, false);", // IID344 - "__ eshll(r14, r27, 4, true);", // IID345 - "__ eshll(rax, r23, 4, true);", // IID346 - "__ eshll(r30, r30, 4, true);", // IID347 - "__ eshrl(r27, rdx, 2, false);", // IID348 - "__ eshrl(rax, r19, 2, false);", // IID349 - "__ eshrl(r20, r20, 2, false);", // IID350 - "__ eshrl(r21, r23, 1, true);", // IID351 - "__ eshrl(rax, r30, 1, true);", // IID352 - "__ eshrl(r25, r25, 2, true);", // IID353 - "__ esubl(r24, r19, 1048576, false);", // IID354 - "__ esubl(rax, r14, 1048576, false);", // IID355 - "__ esubl(r22, r22, 268435456, false);", // IID356 - "__ esubl(r24, r24, 65536, true);", // IID357 - "__ esubl(rax, r14, 65536, true);", // IID358 - "__ esubl(r28, r28, 268435456, true);", // IID359 - "__ exorl(rbx, r20, 256, false);", // IID360 - "__ exorl(rax, r15, 256, false);", // IID361 -#endif // _LP64 - "__ exorl(rbx, rbx, 4096, false);", // IID362 -#ifdef _LP64 - "__ exorl(r24, r30, 65536, true);", // IID363 - "__ exorl(rax, r31, 65536, true);", // IID364 - "__ exorl(r31, r31, 4096, true);", // IID365 - "__ esubl_imm32(r20, r10, 1048576, false);", // IID366 - "__ esubl_imm32(rax, r13, 1048576, false);", // IID367 - "__ esubl_imm32(r25, r25, 1048576, false);", // IID368 - "__ esubl_imm32(r23, r12, 1073741824, true);", // IID369 - "__ esubl_imm32(rax, r16, 1073741824, true);", // IID370 - "__ esubl_imm32(r31, r31, 65536, true);", // IID371 - "__ eaddl(r17, r13, Address(r9, +0x7fef2f98), false);", // IID372 - "__ eaddl(r29, r8, Address(r22, -0x4df70aac), true);", // IID373 - "__ eandl(r13, r17, Address(r12, r15, (Address::ScaleFactor)3, +0x50a8a902), false);", // IID374 - "__ eandl(r22, r25, Address(r26, r10, (Address::ScaleFactor)2, +0x70ea2754), true);", // IID375 - "__ eimull(r19, r12, Address(r30, r8, (Address::ScaleFactor)0, +0x6a1a0a73), false);", // IID376 - "__ eimull(r30, r18, Address(r18, r19, (Address::ScaleFactor)2, -0x7fcd28c7), true);", // IID377 - "__ eorl(r16, r31, Address(r25, r11, (Address::ScaleFactor)3, +0x482d5dbc), false);", // IID378 - "__ eorl(r9, r27, Address(r11, +0x43d5ee01), true);", // IID379 - "__ esubl(rcx, r23, Address(r21, r15, (Address::ScaleFactor)2, +0x2825c2bc), false);", // IID380 - "__ esubl(r27, r22, Address(r13, r15, (Address::ScaleFactor)1, +0x771f0da7), true);", // IID381 - "__ exorl(r9, r30, Address(r9, r22, (Address::ScaleFactor)3, -0x4ad6c88e), false);", // IID382 - "__ exorl(r11, r16, Address(rbx, r28, (Address::ScaleFactor)2, +0xb0223ee), true);", // IID383 - "__ exorb(r15, r29, Address(r15, r28, (Address::ScaleFactor)1, -0x1f297a69), false);", // IID384 - "__ exorb(r17, r30, Address(r23, rbx, (Address::ScaleFactor)1, +0xadc7545), true);", // IID385 - "__ exorw(r27, r9, Address(rdx, r22, (Address::ScaleFactor)2, -0x43d90f61), false);", // IID386 - "__ exorw(rbx, r22, Address(r28, r22, (Address::ScaleFactor)0, -0x7d30a0b1), true);", // IID387 - "__ eaddl(r14, r24, rcx, false);", // IID388 - "__ eaddl(r8, r8, r17, false);", // IID389 - "__ eaddl(r26, r24, r12, true);", // IID390 - "__ eaddl(r24, r24, r23, true);", // IID391 - "__ eandl(r13, r26, r31, false);", // IID392 - "__ eandl(r11, r11, r8, false);", // IID393 - "__ eandl(rcx, r19, r15, true);", // IID394 - "__ eandl(r12, r12, r12, true);", // IID395 - "__ eimull(r22, r20, r19, false);", // IID396 - "__ eimull(r8, r8, rdx, false);", // IID397 - "__ eimull(r22, r27, r23, true);", // IID398 - "__ eimull(r9, r9, r18, true);", // IID399 - "__ eorw(rcx, r30, r13, false);", // IID400 - "__ eorw(r28, r28, r19, false);", // IID401 - "__ eorw(r12, r30, r27, true);", // IID402 - "__ eorw(r8, r8, r22, true);", // IID403 - "__ eorl(r16, rcx, r30, false);", // IID404 - "__ eorl(r10, r10, r25, false);", // IID405 - "__ eorl(r15, r17, r17, true);", // IID406 - "__ eorl(r9, r9, r30, true);", // IID407 - "__ eshldl(r20, r21, r8, false);", // IID408 - "__ eshldl(r26, r26, r14, false);", // IID409 - "__ eshldl(r16, rdx, r14, true);", // IID410 - "__ eshldl(r19, r19, r8, true);", // IID411 - "__ eshrdl(r27, rbx, r26, false);", // IID412 - "__ eshrdl(r28, r28, r19, false);", // IID413 - "__ eshrdl(rcx, r11, r14, true);", // IID414 - "__ eshrdl(r31, r31, r19, true);", // IID415 - "__ esubl(r26, r13, r25, false);", // IID416 - "__ esubl(r24, r24, r11, false);", // IID417 - "__ esubl(r18, r20, r13, true);", // IID418 - "__ esubl(r16, r16, r18, true);", // IID419 - "__ exorl(r19, r17, r8, false);", // IID420 - "__ exorl(r19, r19, r13, false);", // IID421 - "__ exorl(r23, r13, r15, true);", // IID422 - "__ exorl(r11, r11, r29, true);", // IID423 - "__ eshldl(r29, r17, r17, 1, false);", // IID424 - "__ eshldl(r22, r22, r24, 4, false);", // IID425 - "__ eshldl(r8, r28, r11, 16, true);", // IID426 - "__ eshldl(r15, r15, r23, 4, true);", // IID427 - "__ eshrdl(r29, r22, r16, 4, false);", // IID428 - "__ eshrdl(r13, r13, r9, 4, false);", // IID429 - "__ eshrdl(r15, r21, r12, 2, true);", // IID430 - "__ eshrdl(r17, r17, r23, 2, true);", // IID431 - "__ ecmovl (Assembler::Condition::overflow, rdx, r16, r29);", // IID432 - "__ ecmovl (Assembler::Condition::overflow, r10, r10, r21);", // IID433 - "__ ecmovl (Assembler::Condition::noOverflow, r17, r29, r18);", // IID434 - "__ ecmovl (Assembler::Condition::noOverflow, r28, r28, r24);", // IID435 - "__ ecmovl (Assembler::Condition::below, r10, r20, r27);", // IID436 - "__ ecmovl (Assembler::Condition::below, r10, r10, r14);", // IID437 - "__ ecmovl (Assembler::Condition::aboveEqual, r11, r27, rcx);", // IID438 - "__ ecmovl (Assembler::Condition::aboveEqual, r22, r22, r15);", // IID439 - "__ ecmovl (Assembler::Condition::zero, r31, r30, r19);", // IID440 - "__ ecmovl (Assembler::Condition::zero, r19, r19, r26);", // IID441 - "__ ecmovl (Assembler::Condition::notZero, r21, r14, r26);", // IID442 - "__ ecmovl (Assembler::Condition::notZero, r20, r20, r15);", // IID443 - "__ ecmovl (Assembler::Condition::belowEqual, r12, r13, r23);", // IID444 - "__ ecmovl (Assembler::Condition::belowEqual, r28, r28, r20);", // IID445 - "__ ecmovl (Assembler::Condition::above, r20, r24, r11);", // IID446 - "__ ecmovl (Assembler::Condition::above, r10, r10, r15);", // IID447 - "__ ecmovl (Assembler::Condition::negative, r19, r20, r23);", // IID448 - "__ ecmovl (Assembler::Condition::negative, r15, r15, r26);", // IID449 - "__ ecmovl (Assembler::Condition::positive, r19, r24, r23);", // IID450 - "__ ecmovl (Assembler::Condition::positive, r28, r28, r11);", // IID451 - "__ ecmovl (Assembler::Condition::parity, r13, r13, rdx);", // IID452 - "__ ecmovl (Assembler::Condition::parity, r31, r31, r23);", // IID453 - "__ ecmovl (Assembler::Condition::noParity, r23, r9, r27);", // IID454 - "__ ecmovl (Assembler::Condition::noParity, r21, r21, r20);", // IID455 - "__ ecmovl (Assembler::Condition::less, r24, r21, r29);", // IID456 - "__ ecmovl (Assembler::Condition::less, rbx, rbx, r11);", // IID457 - "__ ecmovl (Assembler::Condition::greaterEqual, r21, rbx, rcx);", // IID458 - "__ ecmovl (Assembler::Condition::greaterEqual, r31, r31, r21);", // IID459 - "__ ecmovl (Assembler::Condition::lessEqual, r15, r25, r30);", // IID460 - "__ ecmovl (Assembler::Condition::lessEqual, r23, r23, r25);", // IID461 - "__ ecmovl (Assembler::Condition::greater, r18, rcx, r10);", // IID462 - "__ ecmovl (Assembler::Condition::greater, rcx, rcx, r31);", // IID463 - "__ ecmovl (Assembler::Condition::overflow, r21, r19, Address(r26, -0x6e290873));", // IID464 - "__ ecmovl (Assembler::Condition::noOverflow, r24, r19, Address(r22, rcx, (Address::ScaleFactor)0, +0x11f85f9a));", // IID465 - "__ ecmovl (Assembler::Condition::below, r17, r24, Address(r20, +0x534d775e));", // IID466 - "__ ecmovl (Assembler::Condition::aboveEqual, r20, r18, Address(r20, -0x47c94ecd));", // IID467 - "__ ecmovl (Assembler::Condition::zero, r9, r13, Address(r23, -0x4b83c563));", // IID468 - "__ ecmovl (Assembler::Condition::notZero, r11, r25, Address(r24, r14, (Address::ScaleFactor)1, -0x446507af));", // IID469 - "__ ecmovl (Assembler::Condition::belowEqual, r14, r24, Address(r30, r13, (Address::ScaleFactor)2, +0xd0661d));", // IID470 - "__ ecmovl (Assembler::Condition::above, r13, r25, Address(r14, r27, (Address::ScaleFactor)3, +0x47e1403));", // IID471 - "__ ecmovl (Assembler::Condition::negative, r24, r19, Address(rcx, rdx, (Address::ScaleFactor)3, -0x644a5318));", // IID472 - "__ ecmovl (Assembler::Condition::positive, r26, r24, Address(r22, r22, (Address::ScaleFactor)0, +0x70352446));", // IID473 - "__ ecmovl (Assembler::Condition::parity, r19, r26, Address(r8, r30, (Address::ScaleFactor)2, +0x78a12f5c));", // IID474 - "__ ecmovl (Assembler::Condition::noParity, r29, r11, Address(r25, r20, (Address::ScaleFactor)0, +0x27a8303a));", // IID475 - "__ ecmovl (Assembler::Condition::less, r22, r24, Address(r27, r16, (Address::ScaleFactor)1, +0x2541a10));", // IID476 - "__ ecmovl (Assembler::Condition::greaterEqual, r31, r15, Address(r8, r16, (Address::ScaleFactor)3, +0x558e3251));", // IID477 - "__ ecmovl (Assembler::Condition::lessEqual, r27, r18, Address(r8, r10, (Address::ScaleFactor)0, -0x471987b7));", // IID478 - "__ ecmovl (Assembler::Condition::greater, r18, r16, Address(r18, r19, (Address::ScaleFactor)2, -0x120ae81e));", // IID479 - "__ adcq(rbx, r31);", // IID480 - "__ cmpq(r30, r31);", // IID481 - "__ imulq(r29, r28);", // IID482 - "__ popcntq(r25, r10);", // IID483 - "__ sbbq(r24, r20);", // IID484 - "__ subq(r16, rdx);", // IID485 - "__ tzcntq(r26, r28);", // IID486 - "__ lzcntq(r28, r9);", // IID487 - "__ addq(r20, r24);", // IID488 - "__ andq(r24, r29);", // IID489 - "__ orq(r23, r27);", // IID490 - "__ xorq(r15, r12);", // IID491 - "__ movq(r18, r19);", // IID492 - "__ bsfq(r31, rcx);", // IID493 - "__ bsrq(r9, r13);", // IID494 - "__ btq(r20, rcx);", // IID495 - "__ xchgq(r8, r21);", // IID496 - "__ testq(r24, r14);", // IID497 - "__ addq(Address(rcx, r23, (Address::ScaleFactor)2, +0x4ff06c4d), r29);", // IID498 - "__ andq(Address(r24, r10, (Address::ScaleFactor)1, -0x75d9a189), r26);", // IID499 - "__ cmpq(Address(rbx, rbx, (Address::ScaleFactor)0, +0x4033d59c), r17);", // IID500 - "__ orq(Address(r22, r12, (Address::ScaleFactor)3, -0x3893347d), r18);", // IID501 - "__ xorq(Address(r20, r23, (Address::ScaleFactor)3, +0x4b311560), r12);", // IID502 - "__ subq(Address(r10, r28, (Address::ScaleFactor)2, +0x5c3a2657), r29);", // IID503 - "__ movq(Address(r13, r25, (Address::ScaleFactor)3, +0x1a3d6f3f), r22);", // IID504 - "__ xaddq(Address(r17, r24, (Address::ScaleFactor)3, -0x35addbd8), r25);", // IID505 - "__ andq(Address(r25, +0x632184c3), 16777216);", // IID506 - "__ addq(Address(r13, r13, (Address::ScaleFactor)0, -0x3972eac6), 16777216);", // IID507 - "__ cmpq(Address(r9, -0x13b4c806), 4096);", // IID508 - "__ sarq(Address(r31, +0x4fa7f551), 1);", // IID509 - "__ salq(Address(r21, r31, (Address::ScaleFactor)2, +0x31aa8232), 1);", // IID510 - "__ sbbq(Address(r24, r31, (Address::ScaleFactor)2, -0x466538b7), 268435456);", // IID511 - "__ shrq(Address(r28, r22, (Address::ScaleFactor)0, -0x3efe85b1), 2);", // IID512 - "__ subq(Address(r16, -0x1389a3eb), 1048576);", // IID513 - "__ xorq(Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615), 16);", // IID514 - "__ orq(Address(r12, r28, (Address::ScaleFactor)1, -0x34c898e2), 1);", // IID515 - "__ movq(Address(rcx, r24, (Address::ScaleFactor)2, -0x1644eb08), 256);", // IID516 - "__ testq(Address(r29, -0x7d23890b), -65536);", // IID517 - "__ addq(r23, Address(rcx, r19, (Address::ScaleFactor)2, +0x70eac654));", // IID518 - "__ andq(rdx, Address(r24, r15, (Address::ScaleFactor)0, -0x204ddaa9));", // IID519 - "__ cmpq(rdx, Address(r23, r11, (Address::ScaleFactor)3, +0x32c930bd));", // IID520 - "__ lzcntq(r28, Address(rdx, -0x5433c28f));", // IID521 - "__ orq(r22, Address(r19, r14, (Address::ScaleFactor)1, -0x2cc67d38));", // IID522 - "__ adcq(r10, Address(r10, +0x3d7c59f));", // IID523 - "__ imulq(r10, Address(r8, r8, (Address::ScaleFactor)3, -0xe61862d));", // IID524 - "__ popcntq(r23, Address(r29, -0x777ed96d));", // IID525 - "__ sbbq(rcx, Address(rbx, r19, (Address::ScaleFactor)1, +0x53c601cb));", // IID526 - "__ subq(r14, Address(r17, rbx, (Address::ScaleFactor)0, -0x768bf073));", // IID527 - "__ tzcntq(r29, Address(r10, r19, (Address::ScaleFactor)1, +0x30c98d3c));", // IID528 - "__ xorq(r10, Address(r16, r27, (Address::ScaleFactor)0, -0x3d08d602));", // IID529 - "__ movq(r18, Address(r28, r28, (Address::ScaleFactor)3, -0x62fbac91));", // IID530 - "__ leaq(rbx, Address(rcx, +0x450602a5));", // IID531 - "__ cvttsd2siq(r12, Address(r30, r31, (Address::ScaleFactor)0, -0x6798a630));", // IID532 - "__ xchgq(r31, Address(r24, r10, (Address::ScaleFactor)1, -0x706712ed));", // IID533 - "__ testq(r14, Address(r13, r20, (Address::ScaleFactor)3, +0x171081f2));", // IID534 - "__ addq(r31, 16);", // IID535 - "__ andq(r25, 16);", // IID536 - "__ adcq(r23, 256);", // IID537 - "__ cmpq(r19, 268435456);", // IID538 - "__ rclq(r31, 1);", // IID539 - "__ rcrq(r17, 1);", // IID540 - "__ rolq(r25, 2);", // IID541 - "__ rorq(r17, 4);", // IID542 - "__ sarq(r28, 1);", // IID543 - "__ salq(r15, 4);", // IID544 - "__ sbbq(rbx, 65536);", // IID545 - "__ shlq(r21, 1);", // IID546 - "__ shrq(r10, 1);", // IID547 - "__ subq(r14, 16);", // IID548 - "__ xorq(r18, 268435456);", // IID549 - "__ movq(r23, 16);", // IID550 - "__ mov64(r12, 1099511627776);", // IID551 - "__ btq(r14, 4);", // IID552 - "__ testq(r24, -4096);", // IID553 - "__ orq_imm32(r19, 1048576);", // IID554 - "__ subq_imm32(rcx, 268435456);", // IID555 - "__ cmovq(Assembler::Condition::overflow, rdx, Address(r19, rbx, (Address::ScaleFactor)3, +0x211c8c4));", // IID556 - "__ cmovq(Assembler::Condition::noOverflow, rbx, Address(r21, +0x49267743));", // IID557 - "__ cmovq(Assembler::Condition::below, r21, Address(r8, r28, (Address::ScaleFactor)1, -0x4c8c2946));", // IID558 - "__ cmovq(Assembler::Condition::aboveEqual, r12, Address(r26, r20, (Address::ScaleFactor)0, -0x264df89c));", // IID559 - "__ cmovq(Assembler::Condition::zero, r17, Address(r28, r9, (Address::ScaleFactor)2, +0x3497196b));", // IID560 - "__ cmovq(Assembler::Condition::notZero, r13, Address(r15, r23, (Address::ScaleFactor)1, -0x27a30999));", // IID561 - "__ cmovq(Assembler::Condition::belowEqual, r22, Address(r22, +0xf39ab05));", // IID562 - "__ cmovq(Assembler::Condition::above, rcx, Address(r22, r26, (Address::ScaleFactor)3, -0x48c954c));", // IID563 - "__ cmovq(Assembler::Condition::negative, r25, Address(r19, r21, (Address::ScaleFactor)0, +0xe405b0b));", // IID564 - "__ cmovq(Assembler::Condition::positive, r12, Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b));", // IID565 - "__ cmovq(Assembler::Condition::parity, rbx, Address(r30, r10, (Address::ScaleFactor)1, -0x19798323));", // IID566 - "__ cmovq(Assembler::Condition::noParity, r21, Address(r24, r31, (Address::ScaleFactor)0, -0x5731652b));", // IID567 - "__ cmovq(Assembler::Condition::less, r18, Address(r8, r10, (Address::ScaleFactor)1, -0x5613be89));", // IID568 - "__ cmovq(Assembler::Condition::greaterEqual, r28, Address(r21, r21, (Address::ScaleFactor)3, +0x65a0fdc4));", // IID569 - "__ cmovq(Assembler::Condition::lessEqual, r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c));", // IID570 - "__ cmovq(Assembler::Condition::greater, r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38));", // IID571 - "__ call(r23);", // IID572 - "__ divq(r30);", // IID573 - "__ idivq(r19);", // IID574 - "__ imulq(r9);", // IID575 - "__ mulq(r13);", // IID576 - "__ negq(r16);", // IID577 - "__ notq(r29);", // IID578 - "__ rolq(rcx);", // IID579 - "__ rorq(r25);", // IID580 - "__ sarq(r8);", // IID581 - "__ salq(r27);", // IID582 - "__ shlq(r30);", // IID583 - "__ shrq(r23);", // IID584 - "__ incrementq(rbx);", // IID585 - "__ decrementq(r14);", // IID586 - "__ pushp(r21);", // IID587 - "__ popp(r21);", // IID588 - "__ call(Address(r20, r21, (Address::ScaleFactor)1, +0x56c6af2f));", // IID589 - "__ mulq(Address(r31, r19, (Address::ScaleFactor)3, -0x1b4eb23));", // IID590 - "__ negq(Address(r27, r27, (Address::ScaleFactor)0, -0x58dbfc1f));", // IID591 - "__ sarq(Address(rbx, r22, (Address::ScaleFactor)2, -0x606349d1));", // IID592 - "__ salq(Address(r26, r23, (Address::ScaleFactor)3, +0xb95a079));", // IID593 - "__ shrq(Address(r14, r26, (Address::ScaleFactor)0, +0x3544e09));", // IID594 - "__ incrementq(Address(r27, rdx, (Address::ScaleFactor)0, +0x120b3250));", // IID595 - "__ decrementq(Address(r9, r25, (Address::ScaleFactor)2, -0x34aaeccb));", // IID596 - "__ imulq(r20, Address(r16, r28, (Address::ScaleFactor)1, -0x59de05a5), 1048576);", // IID597 - "__ imulq(r17, r23, 256);", // IID598 - "__ shldq(r19, r11, 8);", // IID599 - "__ shrdq(r28, r10, 8);", // IID600 - "__ pop2(r29, r26);", // IID601 - "__ pop2p(r22, r10);", // IID602 - "__ push2(r25, r30);", // IID603 - "__ push2p(r28, r15);", // IID604 - "__ movzbq(r11, Address(r29, r19, (Address::ScaleFactor)2, -0x12368d34));", // IID605 - "__ movzwq(r14, Address(r8, r30, (Address::ScaleFactor)2, -0x4a9392de));", // IID606 - "__ movsbq(r28, Address(r23, r15, (Address::ScaleFactor)0, +0x6189cb54));", // IID607 - "__ movswq(r28, Address(rbx, r23, (Address::ScaleFactor)3, -0x2de86561));", // IID608 - "__ movzbq(r11, rcx);", // IID609 - "__ movzwq(r30, r15);", // IID610 - "__ movsbq(r14, rcx);", // IID611 - "__ movswq(r23, r9);", // IID612 - "__ cmpxchgq(r12, Address(r13, r10, (Address::ScaleFactor)1, -0x7c62c3a));", // IID613 - "__ eidivq(rcx, false);", // IID614 - "__ eidivq(r15, true);", // IID615 - "__ edivq(r23, false);", // IID616 - "__ edivq(r24, true);", // IID617 - "__ eimulq(r27, false);", // IID618 - "__ eimulq(r30, true);", // IID619 - "__ emulq(r12, false);", // IID620 - "__ emulq(rcx, true);", // IID621 - "__ emulq(Address(r13, r9, (Address::ScaleFactor)3, -0x226aab94), false);", // IID622 - "__ emulq(Address(r13, r24, (Address::ScaleFactor)3, -0x286c7605), true);", // IID623 - "__ eimulq(r21, r30, false);", // IID624 - "__ eimulq(r17, r17, false);", // IID625 - "__ eimulq(r29, r12, true);", // IID626 - "__ eimulq(r30, r30, true);", // IID627 - "__ elzcntq(r24, r15, false);", // IID628 - "__ elzcntq(r25, r25, false);", // IID629 - "__ elzcntq(r25, r21, true);", // IID630 - "__ elzcntq(r22, r22, true);", // IID631 - "__ enegq(r17, r30, false);", // IID632 - "__ enegq(r17, r17, false);", // IID633 - "__ enegq(r31, r17, true);", // IID634 - "__ enegq(r29, r29, true);", // IID635 - "__ enotq(r10, r9);", // IID636 - "__ enotq(r24, r24);", // IID637 - "__ epopcntq(r28, r15, false);", // IID638 - "__ epopcntq(r10, r10, false);", // IID639 - "__ epopcntq(r27, r30, true);", // IID640 - "__ epopcntq(r28, r28, true);", // IID641 - "__ erolq(r28, r14, false);", // IID642 - "__ erolq(r23, r23, false);", // IID643 - "__ erolq(r23, r24, true);", // IID644 - "__ erolq(r21, r21, true);", // IID645 - "__ erorq(r31, r22, false);", // IID646 - "__ erorq(r28, r28, false);", // IID647 - "__ erorq(r17, r10, true);", // IID648 - "__ erorq(r9, r9, true);", // IID649 - "__ esalq(r29, r30, false);", // IID650 - "__ esalq(r11, r11, false);", // IID651 - "__ esalq(r26, r11, true);", // IID652 - "__ esalq(r16, r16, true);", // IID653 - "__ esarq(rbx, r15, false);", // IID654 - "__ esarq(r14, r14, false);", // IID655 - "__ esarq(r25, r16, true);", // IID656 - "__ esarq(r8, r8, true);", // IID657 - "__ edecq(r11, r13, false);", // IID658 - "__ edecq(rcx, rcx, false);", // IID659 - "__ edecq(r21, r18, true);", // IID660 - "__ edecq(r28, r28, true);", // IID661 - "__ eincq(r16, r16, false);", // IID662 - "__ eincq(r29, r29, false);", // IID663 - "__ eincq(r18, r9, true);", // IID664 - "__ eincq(r19, r19, true);", // IID665 - "__ eshlq(r19, r18, false);", // IID666 - "__ eshlq(r8, r8, false);", // IID667 - "__ eshlq(r12, r15, true);", // IID668 - "__ eshlq(r29, r29, true);", // IID669 - "__ eshrq(r28, r24, false);", // IID670 - "__ eshrq(r19, r19, false);", // IID671 - "__ eshrq(r8, r28, true);", // IID672 - "__ eshrq(r17, r17, true);", // IID673 - "__ etzcntq(r28, r16, false);", // IID674 - "__ etzcntq(r14, r14, false);", // IID675 - "__ etzcntq(r12, r31, true);", // IID676 - "__ etzcntq(r14, r14, true);", // IID677 - "__ eimulq(r31, Address(r13, -0x69c4b352), false);", // IID678 - "__ eimulq(r17, Address(r18, -0x60ab1105), true);", // IID679 - "__ elzcntq(r27, Address(r14, r25, (Address::ScaleFactor)2, +0x2798bf83), false);", // IID680 - "__ elzcntq(r23, Address(r10, r11, (Address::ScaleFactor)0, -0x378e635d), true);", // IID681 - "__ enegq(rcx, Address(r19, r9, (Address::ScaleFactor)3, -0x6847d440), false);", // IID682 - "__ enegq(rcx, Address(rbx, rcx, (Address::ScaleFactor)0, +0x6f92d38d), true);", // IID683 - "__ epopcntq(r20, Address(r12, -0x2a8b27d6), false);", // IID684 - "__ epopcntq(r31, Address(r30, +0x4603f6d0), true);", // IID685 - "__ esalq(rbx, Address(r24, +0x567d06f9), false);", // IID686 - "__ esalq(r12, Address(r24, r28, (Address::ScaleFactor)0, -0x1c4c584e), true);", // IID687 - "__ esarq(r12, Address(r23, r24, (Address::ScaleFactor)2, -0x3157bcba), false);", // IID688 - "__ esarq(r8, Address(r14, r24, (Address::ScaleFactor)2, -0x714290a5), true);", // IID689 - "__ edecq(r23, Address(r8, r15, (Address::ScaleFactor)1, -0x5ae272dd), false);", // IID690 - "__ edecq(r13, Address(r29, r9, (Address::ScaleFactor)3, -0x5b5174a9), true);", // IID691 - "__ eincq(r11, Address(r21, r31, (Address::ScaleFactor)3, -0x2176b4dc), false);", // IID692 - "__ eincq(r13, Address(rcx, r16, (Address::ScaleFactor)0, -0x36b448c9), true);", // IID693 - "__ eshrq(r26, Address(r25, rcx, (Address::ScaleFactor)2, -0x5f894993), false);", // IID694 - "__ eshrq(r25, Address(r9, +0x51798d21), true);", // IID695 - "__ etzcntq(r28, Address(r13, r26, (Address::ScaleFactor)2, +0x207196f6), false);", // IID696 - "__ etzcntq(rbx, Address(r19, r13, (Address::ScaleFactor)0, -0x24d937d5), true);", // IID697 - "__ eaddq(r17, Address(r30, +0x3935ccff), r31, false);", // IID698 - "__ eaddq(r14, Address(r27, r10, (Address::ScaleFactor)2, -0x34ad9bab), r14, false);", // IID699 - "__ eaddq(r18, Address(r20, r23, (Address::ScaleFactor)0, +0x5ad3ed4b), r30, true);", // IID700 - "__ eaddq(r20, Address(rdx, -0x322a99e5), r20, true);", // IID701 - "__ eandq(r31, Address(rbx, r27, (Address::ScaleFactor)3, +0x4ce247d2), r17, false);", // IID702 - "__ eandq(r30, Address(r18, r19, (Address::ScaleFactor)1, -0x4ee3d14), r30, false);", // IID703 - "__ eandq(r28, Address(r11, rbx, (Address::ScaleFactor)3, -0x28994bbf), r24, true);", // IID704 - "__ eandq(r30, Address(r22, +0x7d21c24), r30, true);", // IID705 - "__ eorq(r26, Address(r15, r19, (Address::ScaleFactor)3, +0x58c21792), r20, false);", // IID706 - "__ eorq(r13, Address(r10, r27, (Address::ScaleFactor)2, -0x2c70d333), r13, false);", // IID707 - "__ eorq(rbx, Address(r12, rbx, (Address::ScaleFactor)0, -0x1fb0f1bc), r26, true);", // IID708 - "__ eorq(r31, Address(r27, r31, (Address::ScaleFactor)1, +0x28d1756), r31, true);", // IID709 - "__ esubq(r24, Address(r28, r23, (Address::ScaleFactor)1, +0x6980f610), r27, false);", // IID710 - "__ esubq(r15, Address(r11, r30, (Address::ScaleFactor)3, -0x49777e7), r15, false);", // IID711 - "__ esubq(r17, Address(r25, r13, (Address::ScaleFactor)2, +0x31619e46), r31, true);", // IID712 - "__ esubq(r18, Address(r11, r10, (Address::ScaleFactor)2, +0x1922861a), r18, true);", // IID713 - "__ exorq(rbx, Address(r11, -0x4716d420), r21, false);", // IID714 - "__ exorq(r8, Address(rdx, r9, (Address::ScaleFactor)2, -0x4cfe39c), r8, false);", // IID715 - "__ exorq(r16, Address(r14, r27, (Address::ScaleFactor)0, +0x7c6654d9), r25, true);", // IID716 - "__ exorq(r29, Address(r15, -0x5efab479), r29, true);", // IID717 - "__ eaddq(r19, Address(r13, r22, (Address::ScaleFactor)2, +0x68b64559), 16777216, false);", // IID718 - "__ eaddq(r16, Address(r13, r31, (Address::ScaleFactor)3, -0x65143af5), 1, true);", // IID719 - "__ eandq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), 1, false);", // IID720 - "__ eandq(r11, Address(r28, -0xf6d4b26), 65536, true);", // IID721 - "__ eimulq(rcx, Address(r18, r10, (Address::ScaleFactor)0, +0x46ec6da1), 16777216, false);", // IID722 - "__ eimulq(r15, Address(r9, r10, (Address::ScaleFactor)3, -0x7fc36af3), 16, true);", // IID723 - "__ eorq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), 1, false);", // IID724 - "__ eorq(rdx, Address(r25, r14, (Address::ScaleFactor)2, -0x59aa6b85), 4096, true);", // IID725 - "__ esalq(r17, Address(r26, r21, (Address::ScaleFactor)1, -0x6ab1f15f), 8, false);", // IID726 - "__ esalq(r12, Address(r22, r17, (Address::ScaleFactor)0, -0x43ac14ab), 2, true);", // IID727 - "__ esarq(r29, Address(r18, r16, (Address::ScaleFactor)0, -0x59dc0c61), 4, false);", // IID728 - "__ esarq(r16, Address(r11, -0x7bdd314), 4, true);", // IID729 - "__ eshrq(r26, Address(r23, r27, (Address::ScaleFactor)3, -0x55b92314), 16, false);", // IID730 - "__ eshrq(r23, Address(r16, r29, (Address::ScaleFactor)1, +0x71311a1d), 2, true);", // IID731 - "__ esubq(r25, Address(r9, -0x9532bac), 1048576, false);", // IID732 - "__ esubq(r17, Address(r8, r23, (Address::ScaleFactor)0, +0x55d06ca2), 1048576, true);", // IID733 - "__ exorq(r29, Address(r9, r24, (Address::ScaleFactor)0, -0x2c141c1), 1048576, false);", // IID734 - "__ exorq(r28, Address(r22, r19, (Address::ScaleFactor)1, -0x2d9d9abd), 16, true);", // IID735 - "__ eaddq(r22, r14, 16, false);", // IID736 - "__ eaddq(rax, r12, 16, false);", // IID737 - "__ eaddq(r24, r24, 65536, false);", // IID738 - "__ eaddq(r21, rbx, 65536, true);", // IID739 - "__ eaddq(rax, rbx, 65536, true);", // IID740 - "__ eaddq(r24, r24, 65536, true);", // IID741 - "__ eandq(r21, r27, 16777216, false);", // IID742 - "__ eandq(rax, r27, 16777216, false);", // IID743 - "__ eandq(r24, r24, 65536, false);", // IID744 - "__ eandq(r13, r31, 1048576, true);", // IID745 - "__ eandq(rax, r21, 1048576, true);", // IID746 - "__ eandq(r30, r30, 1048576, true);", // IID747 - "__ eimulq(r8, r13, 268435456, false);", // IID748 - "__ eimulq(rax, r31, 268435456, false);", // IID749 - "__ eimulq(r13, r13, 65536, false);", // IID750 - "__ eimulq(r14, r29, 1048576, true);", // IID751 - "__ eimulq(rax, r22, 1048576, true);", // IID752 - "__ eimulq(r8, r8, 268435456, true);", // IID753 - "__ eorq(r30, r15, 4096, false);", // IID754 - "__ eorq(rax, r28, 4096, false);", // IID755 - "__ eorq(r26, r26, 1048576, false);", // IID756 - "__ eorq(r16, r12, 268435456, true);", // IID757 - "__ eorq(rax, r9, 268435456, true);", // IID758 - "__ eorq(r23, r23, 256, true);", // IID759 - "__ erclq(r15, r9, 16);", // IID760 - "__ erclq(rax, r8, 16);", // IID761 - "__ erclq(r25, r25, 1);", // IID762 - "__ erolq(r9, r17, 16, false);", // IID763 - "__ erolq(rax, r20, 16, false);", // IID764 - "__ erolq(r27, r27, 1, false);", // IID765 - "__ erolq(r20, r31, 1, true);", // IID766 - "__ erolq(rax, r18, 1, true);", // IID767 - "__ erolq(r28, r28, 16, true);", // IID768 - "__ erorq(r26, r18, 16, false);", // IID769 - "__ erorq(rax, r24, 16, false);", // IID770 - "__ erorq(r22, r22, 16, false);", // IID771 - "__ erorq(r27, r29, 1, true);", // IID772 - "__ erorq(rax, r18, 1, true);", // IID773 - "__ erorq(r21, r21, 1, true);", // IID774 - "__ esalq(r12, rcx, 2, false);", // IID775 - "__ esalq(rax, r24, 2, false);", // IID776 - "__ esalq(r22, r22, 8, false);", // IID777 - "__ esalq(r17, r23, 8, true);", // IID778 - "__ esalq(rax, r27, 8, true);", // IID779 - "__ esalq(r23, r23, 1, true);", // IID780 - "__ esarq(r8, r25, 16, false);", // IID781 - "__ esarq(rax, r23, 16, false);", // IID782 - "__ esarq(r9, r9, 4, false);", // IID783 - "__ esarq(r22, r13, 1, true);", // IID784 - "__ esarq(rax, r11, 1, true);", // IID785 - "__ esarq(r12, r12, 2, true);", // IID786 - "__ eshlq(rcx, r30, 8, false);", // IID787 - "__ eshlq(rax, r19, 8, false);", // IID788 - "__ eshlq(r13, r13, 2, false);", // IID789 - "__ eshlq(r18, r11, 8, true);", // IID790 - "__ eshlq(rax, r9, 8, true);", // IID791 - "__ eshlq(rcx, rcx, 16, true);", // IID792 - "__ eshrq(r10, r22, 4, false);", // IID793 - "__ eshrq(rax, r9, 4, false);", // IID794 - "__ eshrq(r12, r12, 2, false);", // IID795 - "__ eshrq(r26, r31, 8, true);", // IID796 - "__ eshrq(rax, r12, 8, true);", // IID797 - "__ eshrq(r28, r28, 1, true);", // IID798 - "__ esubq(r15, r30, 65536, false);", // IID799 - "__ esubq(rax, rcx, 65536, false);", // IID800 - "__ esubq(r26, r26, 16, false);", // IID801 - "__ esubq(r12, r14, 1, true);", // IID802 - "__ esubq(rax, r21, 1, true);", // IID803 - "__ esubq(r20, r20, 1048576, true);", // IID804 - "__ exorq(r11, rbx, 16777216, false);", // IID805 - "__ exorq(rax, r23, 16777216, false);", // IID806 - "__ exorq(r31, r31, 268435456, false);", // IID807 - "__ exorq(r29, r28, 4096, true);", // IID808 - "__ exorq(rax, r19, 4096, true);", // IID809 - "__ exorq(rdx, rdx, 268435456, true);", // IID810 - "__ eorq_imm32(rdx, rdx, 1048576, false);", // IID811 - "__ eorq_imm32(rax, r22, 1048576, false);", // IID812 - "__ eorq_imm32(r29, r29, 1048576, false);", // IID813 - "__ eorq_imm32(r17, rcx, 4194304, false);", // IID814 - "__ eorq_imm32(rax, r25, 4194304, false);", // IID815 - "__ eorq_imm32(r27, r27, 1073741824, false);", // IID816 - "__ esubq_imm32(r16, r19, 4194304, false);", // IID817 - "__ esubq_imm32(rax, r31, 4194304, false);", // IID818 - "__ esubq_imm32(r26, r26, 262144, false);", // IID819 - "__ esubq_imm32(r17, r22, 1073741824, true);", // IID820 - "__ esubq_imm32(rax, r18, 1073741824, true);", // IID821 - "__ esubq_imm32(r23, r23, 268435456, true);", // IID822 - "__ eaddq(r13, r30, Address(r24, r19, (Address::ScaleFactor)1, +0x56ea3a3b), false);", // IID823 - "__ eaddq(r29, r15, Address(r26, r27, (Address::ScaleFactor)3, -0x4b113958), true);", // IID824 - "__ eandq(r12, r30, Address(r31, -0x46103c74), false);", // IID825 - "__ eandq(r27, r10, Address(r22, r25, (Address::ScaleFactor)1, +0x6a1ebee5), true);", // IID826 - "__ eorq(r30, r26, Address(r11, r18, (Address::ScaleFactor)2, -0x2b9fff29), false);", // IID827 - "__ eorq(r9, r12, Address(r18, r17, (Address::ScaleFactor)0, +0xb4859f6), true);", // IID828 - "__ eimulq(rdx, r17, Address(r24, rdx, (Address::ScaleFactor)2, +0x3d284cd8), false);", // IID829 - "__ eimulq(r29, r26, Address(r30, r12, (Address::ScaleFactor)1, +0x6e813124), true);", // IID830 - "__ esubq(rbx, r13, Address(r22, -0x702a289e), false);", // IID831 - "__ esubq(r23, r29, Address(r25, rdx, (Address::ScaleFactor)0, -0x6252a7ed), true);", // IID832 - "__ exorq(r8, r18, Address(r19, r14, (Address::ScaleFactor)2, -0xebfa697), false);", // IID833 - "__ exorq(r10, r28, Address(r26, +0x168381ca), true);", // IID834 - "__ eaddq(rcx, r18, r8, false);", // IID835 - "__ eaddq(rcx, rcx, r14, false);", // IID836 - "__ eaddq(r23, r10, r16, true);", // IID837 - "__ eaddq(r11, r11, r24, true);", // IID838 - "__ eadcxq(r9, r18, rdx);", // IID839 - "__ eadcxq(r8, r8, r15);", // IID840 - "__ eadoxq(r15, r22, r26);", // IID841 - "__ eadoxq(r11, r11, rdx);", // IID842 - "__ eandq(r19, rdx, r22, false);", // IID843 - "__ eandq(r29, r29, r17, false);", // IID844 - "__ eandq(r23, r27, r15, true);", // IID845 - "__ eandq(r9, r9, r13, true);", // IID846 - "__ eimulq(r18, r15, r16, false);", // IID847 - "__ eimulq(rcx, rcx, r17, false);", // IID848 - "__ eimulq(r23, r12, r20, true);", // IID849 - "__ eimulq(r10, r10, r9, true);", // IID850 - "__ eorq(rdx, r19, r14, false);", // IID851 - "__ eorq(rcx, rcx, r13, false);", // IID852 - "__ eorq(r9, r25, r29, true);", // IID853 - "__ eorq(rdx, rdx, r25, true);", // IID854 - "__ esubq(r23, r8, r16, false);", // IID855 - "__ esubq(r13, r13, r13, false);", // IID856 - "__ esubq(r19, r12, r15, true);", // IID857 - "__ esubq(r9, r9, rdx, true);", // IID858 - "__ exorq(r13, r16, r31, false);", // IID859 - "__ exorq(r17, r17, r30, false);", // IID860 - "__ exorq(r19, r30, r20, true);", // IID861 - "__ exorq(r31, r31, r13, true);", // IID862 - "__ eshldq(r22, r10, r13, 4, false);", // IID863 - "__ eshldq(r24, r24, r21, 16, false);", // IID864 - "__ eshldq(r20, r13, r27, 16, true);", // IID865 - "__ eshldq(r31, r31, r19, 2, true);", // IID866 - "__ eshrdq(r30, r20, r11, 8, false);", // IID867 - "__ eshrdq(rdx, rdx, r15, 1, false);", // IID868 - "__ eshrdq(r28, r30, r14, 2, true);", // IID869 - "__ eshrdq(r20, r20, r16, 1, true);", // IID870 - "__ ecmovq (Assembler::Condition::overflow, r21, r17, r28);", // IID871 - "__ ecmovq (Assembler::Condition::overflow, r15, r15, r30);", // IID872 - "__ ecmovq (Assembler::Condition::noOverflow, rcx, r15, r15);", // IID873 - "__ ecmovq (Assembler::Condition::noOverflow, rcx, rcx, r13);", // IID874 - "__ ecmovq (Assembler::Condition::below, rdx, r26, r26);", // IID875 - "__ ecmovq (Assembler::Condition::below, r28, r28, r15);", // IID876 - "__ ecmovq (Assembler::Condition::aboveEqual, r8, rdx, rcx);", // IID877 - "__ ecmovq (Assembler::Condition::aboveEqual, rcx, rcx, rcx);", // IID878 - "__ ecmovq (Assembler::Condition::zero, r10, r13, r9);", // IID879 - "__ ecmovq (Assembler::Condition::zero, r14, r14, r27);", // IID880 - "__ ecmovq (Assembler::Condition::notZero, r11, r23, r9);", // IID881 - "__ ecmovq (Assembler::Condition::notZero, r11, r11, rdx);", // IID882 - "__ ecmovq (Assembler::Condition::belowEqual, r31, r14, r25);", // IID883 - "__ ecmovq (Assembler::Condition::belowEqual, r20, r20, r12);", // IID884 - "__ ecmovq (Assembler::Condition::above, rdx, r10, r28);", // IID885 - "__ ecmovq (Assembler::Condition::above, r8, r8, r17);", // IID886 - "__ ecmovq (Assembler::Condition::negative, rcx, r30, r23);", // IID887 - "__ ecmovq (Assembler::Condition::negative, r26, r26, r18);", // IID888 - "__ ecmovq (Assembler::Condition::positive, rdx, rbx, r18);", // IID889 - "__ ecmovq (Assembler::Condition::positive, r21, r21, r13);", // IID890 - "__ ecmovq (Assembler::Condition::parity, r27, r28, r27);", // IID891 - "__ ecmovq (Assembler::Condition::parity, r11, r11, r30);", // IID892 - "__ ecmovq (Assembler::Condition::noParity, rcx, r21, r18);", // IID893 - "__ ecmovq (Assembler::Condition::noParity, rcx, rcx, r29);", // IID894 - "__ ecmovq (Assembler::Condition::less, rdx, r21, r12);", // IID895 - "__ ecmovq (Assembler::Condition::less, rdx, rdx, r26);", // IID896 - "__ ecmovq (Assembler::Condition::greaterEqual, r17, rbx, r22);", // IID897 - "__ ecmovq (Assembler::Condition::greaterEqual, rdx, rdx, r11);", // IID898 - "__ ecmovq (Assembler::Condition::lessEqual, rdx, r14, r8);", // IID899 - "__ ecmovq (Assembler::Condition::lessEqual, r14, r14, r8);", // IID900 - "__ ecmovq (Assembler::Condition::greater, r25, r29, r21);", // IID901 - "__ ecmovq (Assembler::Condition::greater, r26, r26, r30);", // IID902 - "__ ecmovq (Assembler::Condition::overflow, r24, r21, Address(r13, r11, (Address::ScaleFactor)1, +0x439c521e));", // IID903 - "__ ecmovq (Assembler::Condition::noOverflow, r11, r18, Address(r29, r16, (Address::ScaleFactor)0, +0x632127f));", // IID904 - "__ ecmovq (Assembler::Condition::below, r16, r8, Address(r8, r26, (Address::ScaleFactor)1, +0x10633def));", // IID905 - "__ ecmovq (Assembler::Condition::aboveEqual, r13, r14, Address(r18, -0x54f69e38));", // IID906 - "__ ecmovq (Assembler::Condition::zero, r12, r8, Address(r31, r26, (Address::ScaleFactor)1, -0x7a1e447a));", // IID907 - "__ ecmovq (Assembler::Condition::notZero, r29, r29, Address(r19, r11, (Address::ScaleFactor)2, -0x35d82dd2));", // IID908 - "__ ecmovq (Assembler::Condition::belowEqual, rcx, r18, Address(r25, r28, (Address::ScaleFactor)0, +0x30be64a0));", // IID909 - "__ ecmovq (Assembler::Condition::above, r28, r12, Address(r10, r16, (Address::ScaleFactor)1, -0x22b8fefa));", // IID910 - "__ ecmovq (Assembler::Condition::negative, r11, r8, Address(rbx, r11, (Address::ScaleFactor)3, +0x25cc9e96));", // IID911 - "__ ecmovq (Assembler::Condition::positive, r12, r27, Address(r11, -0xc2d70fe));", // IID912 - "__ ecmovq (Assembler::Condition::parity, r8, r26, Address(r19, rbx, (Address::ScaleFactor)1, -0x486db7ea));", // IID913 - "__ ecmovq (Assembler::Condition::noParity, r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884));", // IID914 - "__ ecmovq (Assembler::Condition::less, r27, r8, Address(r29, r14, (Address::ScaleFactor)2, +0x92b7a8));", // IID915 - "__ ecmovq (Assembler::Condition::greaterEqual, r14, r28, Address(r19, rdx, (Address::ScaleFactor)0, +0x9c2d45));", // IID916 - "__ ecmovq (Assembler::Condition::lessEqual, r25, r8, Address(rcx, r18, (Address::ScaleFactor)2, +0x6655c86b));", // IID917 - "__ ecmovq (Assembler::Condition::greater, r19, r21, Address(r10, r25, (Address::ScaleFactor)0, -0x1005430b));", // IID918 + "__ eorl(rcx, r24, r22, false);", // IID432 + "__ eorl(rcx, rcx, r19, false);", // IID433 + "__ eorl(r27, r27, r27, false);", // IID434 + "__ eorl(r31, r9, r13, true);", // IID435 + "__ eorl(r31, r31, r23, true);", // IID436 + "__ eorl(r19, r17, r19, true);", // IID437 + "__ eshldl(r20, r16, r24, false);", // IID438 + "__ eshldl(rdx, rdx, r12, false);", // IID439 + "__ eshldl(r29, r9, r31, true);", // IID440 + "__ eshldl(r17, r17, r20, true);", // IID441 + "__ eshrdl(r20, r15, r18, false);", // IID442 + "__ eshrdl(rcx, rcx, r12, false);", // IID443 + "__ eshrdl(r14, r9, r23, true);", // IID444 + "__ eshrdl(r19, r19, r13, true);", // IID445 + "__ esubl(r30, r27, r27, false);", // IID446 + "__ esubl(rdx, rdx, r11, false);", // IID447 + "__ esubl(r15, r11, r24, true);", // IID448 + "__ esubl(r14, r14, r25, true);", // IID449 + "__ exorl(r31, r16, r12, false);", // IID450 + "__ exorl(r20, r20, r14, false);", // IID451 + "__ exorl(r30, r13, r30, false);", // IID452 + "__ exorl(r24, r17, r17, true);", // IID453 + "__ exorl(r26, r26, r21, true);", // IID454 + "__ exorl(r11, r13, r11, true);", // IID455 + "__ eshldl(r27, r25, r21, 4, false);", // IID456 + "__ eshldl(r22, r22, r10, 4, false);", // IID457 + "__ eshldl(r21, r15, r24, 16, true);", // IID458 + "__ eshldl(rdx, rdx, r19, 1, true);", // IID459 + "__ eshrdl(r23, r13, r8, 16, false);", // IID460 + "__ eshrdl(r26, r26, r22, 1, false);", // IID461 + "__ eshrdl(r24, r9, r30, 16, true);", // IID462 + "__ eshrdl(r19, r19, r8, 4, true);", // IID463 + "__ ecmovl (Assembler::Condition::overflow, r30, r26, r17);", // IID464 + "__ ecmovl (Assembler::Condition::overflow, r14, r14, r26);", // IID465 + "__ ecmovl (Assembler::Condition::noOverflow, r24, r19, r29);", // IID466 + "__ ecmovl (Assembler::Condition::noOverflow, r25, r25, r20);", // IID467 + "__ ecmovl (Assembler::Condition::below, r11, r10, r14);", // IID468 + "__ ecmovl (Assembler::Condition::below, r30, r30, r25);", // IID469 + "__ ecmovl (Assembler::Condition::aboveEqual, r13, r22, r27);", // IID470 + "__ ecmovl (Assembler::Condition::aboveEqual, r16, r16, r24);", // IID471 + "__ ecmovl (Assembler::Condition::zero, r28, r13, r30);", // IID472 + "__ ecmovl (Assembler::Condition::zero, r30, r30, r24);", // IID473 + "__ ecmovl (Assembler::Condition::notZero, r21, r20, r31);", // IID474 + "__ ecmovl (Assembler::Condition::notZero, r8, r8, r16);", // IID475 + "__ ecmovl (Assembler::Condition::belowEqual, r15, r26, r22);", // IID476 + "__ ecmovl (Assembler::Condition::belowEqual, r31, r31, rdx);", // IID477 + "__ ecmovl (Assembler::Condition::above, r27, r8, r10);", // IID478 + "__ ecmovl (Assembler::Condition::above, r18, r18, r11);", // IID479 + "__ ecmovl (Assembler::Condition::negative, r27, rbx, r21);", // IID480 + "__ ecmovl (Assembler::Condition::negative, r12, r12, r31);", // IID481 + "__ ecmovl (Assembler::Condition::positive, r12, rdx, r18);", // IID482 + "__ ecmovl (Assembler::Condition::positive, r18, r18, r19);", // IID483 + "__ ecmovl (Assembler::Condition::parity, r16, r20, r23);", // IID484 + "__ ecmovl (Assembler::Condition::parity, r18, r18, r16);", // IID485 + "__ ecmovl (Assembler::Condition::noParity, rbx, r31, r30);", // IID486 + "__ ecmovl (Assembler::Condition::noParity, r31, r31, r29);", // IID487 + "__ ecmovl (Assembler::Condition::less, r28, r25, r10);", // IID488 + "__ ecmovl (Assembler::Condition::less, r24, r24, r20);", // IID489 + "__ ecmovl (Assembler::Condition::greaterEqual, r16, rdx, r26);", // IID490 + "__ ecmovl (Assembler::Condition::greaterEqual, r28, r28, r28);", // IID491 + "__ ecmovl (Assembler::Condition::lessEqual, r9, r20, r24);", // IID492 + "__ ecmovl (Assembler::Condition::lessEqual, r24, r24, r29);", // IID493 + "__ ecmovl (Assembler::Condition::greater, r23, r27, r15);", // IID494 + "__ ecmovl (Assembler::Condition::greater, r12, r12, r18);", // IID495 + "__ ecmovl (Assembler::Condition::overflow, r19, r9, Address(r31, rcx, (Address::ScaleFactor)1, -0x2be98bd));", // IID496 + "__ ecmovl (Assembler::Condition::overflow, r8, r8, Address(r21, r24, (Address::ScaleFactor)1, +0x41e6a0cb));", // IID497 + "__ ecmovl (Assembler::Condition::noOverflow, r23, r15, Address(r19, r30, (Address::ScaleFactor)3, -0x55adfe2d));", // IID498 + "__ ecmovl (Assembler::Condition::noOverflow, rdx, rdx, Address(r27, rdx, (Address::ScaleFactor)0, -0x1aa12735));", // IID499 + "__ ecmovl (Assembler::Condition::below, rbx, r29, Address(r31, r12, (Address::ScaleFactor)0, +0xbd42246));", // IID500 + "__ ecmovl (Assembler::Condition::below, r21, r21, Address(r19, r21, (Address::ScaleFactor)1, -0x41518818));", // IID501 + "__ ecmovl (Assembler::Condition::aboveEqual, r23, r29, Address(r22, r9, (Address::ScaleFactor)2, -0x35addbd8));", // IID502 + "__ ecmovl (Assembler::Condition::aboveEqual, r18, r18, Address(r25, +0x632184c3));", // IID503 + "__ ecmovl (Assembler::Condition::zero, r29, r13, Address(r18, r13, (Address::ScaleFactor)0, -0x3972eac6));", // IID504 + "__ ecmovl (Assembler::Condition::zero, r29, r29, Address(r12, r9, (Address::ScaleFactor)3, -0x668cdfd2));", // IID505 + "__ ecmovl (Assembler::Condition::notZero, r25, r18, Address(r9, r22, (Address::ScaleFactor)2, +0x7f6ac91f));", // IID506 + "__ ecmovl (Assembler::Condition::notZero, r28, r28, Address(r30, +0x562e6594));", // IID507 + "__ ecmovl (Assembler::Condition::belowEqual, r27, r24, Address(r15, r20, (Address::ScaleFactor)2, -0x466538b7));", // IID508 + "__ ecmovl (Assembler::Condition::belowEqual, r25, r25, Address(r26, r11, (Address::ScaleFactor)3, -0x593812a9));", // IID509 + "__ ecmovl (Assembler::Condition::above, rcx, r20, Address(r16, -0x1389a3eb));", // IID510 + "__ ecmovl (Assembler::Condition::above, rbx, rbx, Address(r29, r8, (Address::ScaleFactor)0, +0x1d022615));", // IID511 + "__ ecmovl (Assembler::Condition::negative, rdx, r14, Address(r12, r28, (Address::ScaleFactor)1, -0x51725a91));", // IID512 + "__ ecmovl (Assembler::Condition::negative, r24, r24, Address(r17, r18, (Address::ScaleFactor)1, -0x1725c4e4));", // IID513 + "__ ecmovl (Assembler::Condition::positive, rcx, rcx, Address(r15, r23, (Address::ScaleFactor)2, -0x6bd22ccf));", // IID514 + "__ ecmovl (Assembler::Condition::positive, r24, r24, Address(r15, r10, (Address::ScaleFactor)1, -0x7ffb3d09));", // IID515 + "__ ecmovl (Assembler::Condition::parity, r23, rcx, Address(r11, r23, (Address::ScaleFactor)0, +0x3738c585));", // IID516 + "__ ecmovl (Assembler::Condition::parity, r24, r24, Address(r30, r10, (Address::ScaleFactor)0, +0xfcc15a8));", // IID517 + "__ ecmovl (Assembler::Condition::noParity, r14, r26, Address(r14, r21, (Address::ScaleFactor)1, -0x4430ce9f));", // IID518 + "__ ecmovl (Assembler::Condition::noParity, r10, r10, Address(r28, +0x3d7c59f));", // IID519 + "__ ecmovl (Assembler::Condition::less, r10, r21, Address(r8, r8, (Address::ScaleFactor)3, +0x4a6584b4));", // IID520 + "__ ecmovl (Assembler::Condition::less, r26, r26, Address(r19, r20, (Address::ScaleFactor)3, +0x47c660ef));", // IID521 + "__ ecmovl (Assembler::Condition::greaterEqual, r26, r10, Address(rcx, +0x61977a97));", // IID522 + "__ ecmovl (Assembler::Condition::greaterEqual, r30, r30, Address(r15, r19, (Address::ScaleFactor)3, +0x53c601cb));", // IID523 + "__ ecmovl (Assembler::Condition::lessEqual, r14, r9, Address(r17, -0x566ceee2));", // IID524 + "__ ecmovl (Assembler::Condition::lessEqual, r15, r15, Address(r27, r20, (Address::ScaleFactor)0, +0x76164792));", // IID525 + "__ ecmovl (Assembler::Condition::greater, r27, r14, Address(r9, r13, (Address::ScaleFactor)2, +0xf5752d7));", // IID526 + "__ ecmovl (Assembler::Condition::greater, r12, r12, Address(rbx, rcx, (Address::ScaleFactor)3, -0x5501b4c6));", // IID527 + "__ adcq(r30, r31);", // IID528 + "__ cmpq(r12, rdx);", // IID529 + "__ imulq(r21, r24);", // IID530 + "__ popcntq(r9, r25);", // IID531 + "__ sbbq(r8, r12);", // IID532 + "__ subq(r31, r24);", // IID533 + "__ tzcntq(r10, r16);", // IID534 + "__ lzcntq(r20, r21);", // IID535 + "__ addq(rdx, r17);", // IID536 + "__ andq(r14, r13);", // IID537 + "__ orq(r20, r24);", // IID538 + "__ xorq(r21, r22);", // IID539 + "__ movq(r12, r27);", // IID540 + "__ bsfq(r23, rdx);", // IID541 + "__ bsrq(r31, r28);", // IID542 + "__ btq(r8, r25);", // IID543 + "__ xchgq(r21, rbx);", // IID544 + "__ testq(r23, r23);", // IID545 + "__ addq(Address(r19, -0x180d3ea1), r10);", // IID546 + "__ andq(Address(r11, r17, (Address::ScaleFactor)1, -0x78976be8), r25);", // IID547 + "__ cmpq(Address(rbx, r28, (Address::ScaleFactor)3, +0x35f72102), r13);", // IID548 + "__ orq(Address(r8, -0x34465011), r21);", // IID549 + "__ xorq(Address(r19, -0x404b22dd), r18);", // IID550 + "__ subq(Address(r23, r27, (Address::ScaleFactor)3, -0x428d2646), r14);", // IID551 + "__ movq(Address(r9, rcx, (Address::ScaleFactor)2, -0x72611661), r28);", // IID552 + "__ xaddq(Address(r24, r21, (Address::ScaleFactor)2, +0x3a6be990), rbx);", // IID553 + "__ andq(Address(r22, r10, (Address::ScaleFactor)0, +0x7ef8bdd), 1048576);", // IID554 + "__ addq(Address(r13, r28, (Address::ScaleFactor)0, -0x754789b1), 65536);", // IID555 + "__ cmpq(Address(r10, -0xbd2a8da), 268435456);", // IID556 + "__ sarq(Address(r23, r14, (Address::ScaleFactor)1, +0x6a16d9f5), 4);", // IID557 + "__ salq(Address(rcx, r21, (Address::ScaleFactor)1, +0x5f66ac1e), 8);", // IID558 + "__ sbbq(Address(rcx, r22, (Address::ScaleFactor)3, -0x48c954c), 268435456);", // IID559 + "__ shrq(Address(r21, r30, (Address::ScaleFactor)0, +0xe405b0b), 8);", // IID560 + "__ subq(Address(r19, r29, (Address::ScaleFactor)3, -0x7762044b), 4096);", // IID561 + "__ xorq(Address(r30, r10, (Address::ScaleFactor)1, -0x19798323), 16);", // IID562 + "__ orq(Address(rdx, r24, (Address::ScaleFactor)3, +0x18d9b316), 4096);", // IID563 + "__ movq(Address(rbx, -0x3058074d), 256);", // IID564 + "__ testq(Address(r28, r21, (Address::ScaleFactor)3, +0x65a0fdc4), -268435456);", // IID565 + "__ addq(r23, Address(r11, r18, (Address::ScaleFactor)0, -0x1d1af10c));", // IID566 + "__ andq(r22, Address(r18, r12, (Address::ScaleFactor)1, +0x1a5f1c38));", // IID567 + "__ cmpq(r23, Address(r30, r19, (Address::ScaleFactor)0, -0x3e912f7f));", // IID568 + "__ lzcntq(r29, Address(rcx, +0x12e3fbe4));", // IID569 + "__ orq(r14, Address(r21, r21, (Address::ScaleFactor)2, +0xd73042));", // IID570 + "__ adcq(r31, Address(r17, r31, (Address::ScaleFactor)2, +0xabde912));", // IID571 + "__ imulq(r20, Address(r13, r27, (Address::ScaleFactor)0, -0x58dbfc1f));", // IID572 + "__ popcntq(rbx, Address(r22, -0x72c66c23));", // IID573 + "__ sbbq(r26, Address(r9, +0x334aba09));", // IID574 + "__ subq(r9, Address(r9, r30, (Address::ScaleFactor)3, -0x219a6102));", // IID575 + "__ tzcntq(r25, Address(r20, -0x2131bab1));", // IID576 + "__ xorq(r16, Address(r28, r16, (Address::ScaleFactor)1, +0x48c483b9));", // IID577 + "__ movq(r30, Address(r9, r16, (Address::ScaleFactor)0, -0x88ce84f));", // IID578 + "__ leaq(r11, Address(r30, r29, (Address::ScaleFactor)2, +0x3eeb8fd0));", // IID579 + "__ cvttsd2siq(r26, Address(r29, r10, (Address::ScaleFactor)3, +0x3ef4822e));", // IID580 + "__ xchgq(r29, Address(r19, r20, (Address::ScaleFactor)2, -0x3f0f3db9));", // IID581 + "__ testq(r8, Address(r30, r20, (Address::ScaleFactor)0, +0x15b56a17));", // IID582 + "__ addq(r26, 4096);", // IID583 + "__ andq(r20, 16);", // IID584 + "__ adcq(r23, 1048576);", // IID585 + "__ cmpq(r12, 4096);", // IID586 + "__ rclq(rcx, 4);", // IID587 + "__ rcrq(r14, 1);", // IID588 + "__ rolq(r23, 2);", // IID589 + "__ rorq(r12, 4);", // IID590 + "__ sarq(r10, 4);", // IID591 + "__ salq(r20, 4);", // IID592 + "__ sbbq(rcx, 1048576);", // IID593 + "__ shlq(r23, 16);", // IID594 + "__ shrq(r27, 2);", // IID595 + "__ subq(rcx, 65536);", // IID596 + "__ xorq(r9, 1048576);", // IID597 + "__ movq(r16, 65536);", // IID598 + "__ mov64(r24, 4503599627370496);", // IID599 + "__ btq(r18, 64);", // IID600 + "__ testq(r29, -4096);", // IID601 + "__ orq_imm32(r30, 67108864);", // IID602 + "__ subq_imm32(r25, 268435456);", // IID603 + "__ cmovq(Assembler::Condition::overflow, r30, Address(r17, r31, (Address::ScaleFactor)2, +0x47ff92f0));", // IID604 + "__ cmovq(Assembler::Condition::noOverflow, r9, Address(r24, r28, (Address::ScaleFactor)1, +0x384904c0));", // IID605 + "__ cmovq(Assembler::Condition::below, r23, Address(r23, r24, (Address::ScaleFactor)3, -0x197f1266));", // IID606 + "__ cmovq(Assembler::Condition::aboveEqual, r9, Address(r29, r30, (Address::ScaleFactor)0, +0x2b5d49c8));", // IID607 + "__ cmovq(Assembler::Condition::zero, r16, Address(rbx, r15, (Address::ScaleFactor)1, +0x22379381));", // IID608 + "__ cmovq(Assembler::Condition::notZero, r8, Address(r11, +0x49d67a0));", // IID609 + "__ cmovq(Assembler::Condition::belowEqual, r28, Address(r16, r16, (Address::ScaleFactor)2, -0x5e941da9));", // IID610 + "__ cmovq(Assembler::Condition::above, r19, Address(r18, r8, (Address::ScaleFactor)0, -0xa5e55ec));", // IID611 + "__ cmovq(Assembler::Condition::negative, r28, Address(r17, r28, (Address::ScaleFactor)1, -0x3264220c));", // IID612 + "__ cmovq(Assembler::Condition::positive, r31, Address(r14, r31, (Address::ScaleFactor)1, +0x5001bc5a));", // IID613 + "__ cmovq(Assembler::Condition::parity, rbx, Address(r18, r17, (Address::ScaleFactor)2, -0x286f2379));", // IID614 + "__ cmovq(Assembler::Condition::noParity, r17, Address(r20, -0x5549f838));", // IID615 + "__ cmovq(Assembler::Condition::less, r30, Address(r9, r28, (Address::ScaleFactor)1, -0x25b00cf3));", // IID616 + "__ cmovq(Assembler::Condition::greaterEqual, r19, Address(r9, -0x2aabf22c));", // IID617 + "__ cmovq(Assembler::Condition::lessEqual, rbx, Address(rcx, r12, (Address::ScaleFactor)1, -0x432d68cc));", // IID618 + "__ cmovq(Assembler::Condition::greater, rbx, Address(r15, r17, (Address::ScaleFactor)3, -0x2b97565e));", // IID619 + "__ call(r24);", // IID620 + "__ divq(r9);", // IID621 + "__ idivq(r28);", // IID622 + "__ imulq(rdx);", // IID623 + "__ mulq(r31);", // IID624 + "__ negq(r12);", // IID625 + "__ notq(r12);", // IID626 + "__ rolq(r24);", // IID627 + "__ rorq(r28);", // IID628 + "__ sarq(r11);", // IID629 + "__ salq(r27);", // IID630 + "__ shlq(r23);", // IID631 + "__ shrq(r17);", // IID632 + "__ incrementq(r16);", // IID633 + "__ decrementq(r12);", // IID634 + "__ pushp(r23);", // IID635 + "__ popp(r24);", // IID636 + "__ call(Address(r18, r14, (Address::ScaleFactor)0, -0x66639d32));", // IID637 + "__ mulq(Address(r24, -0x660a2421));", // IID638 + "__ negq(Address(r14, r18, (Address::ScaleFactor)0, +0x40f3936e));", // IID639 + "__ sarq(Address(r10, r13, (Address::ScaleFactor)0, +0x7d04cb72));", // IID640 + "__ salq(Address(r18, r11, (Address::ScaleFactor)3, -0x2176b4dc));", // IID641 + "__ shrq(Address(r13, rcx, (Address::ScaleFactor)1, +0x7996aa80));", // IID642 + "__ incrementq(Address(r14, +0x67c2d02a));", // IID643 + "__ decrementq(Address(r22, r26, (Address::ScaleFactor)0, +0x224f62c0));", // IID644 + "__ imulq(rdx, Address(r31, rbx, (Address::ScaleFactor)1, +0x2b00bb10), 16777216);", // IID645 + "__ imulq(r21, r31, 4096);", // IID646 + "__ shldq(rbx, r19, 1);", // IID647 + "__ shrdq(r11, r23, 4);", // IID648 + "__ pop2(r16, r30);", // IID649 + "__ pop2p(r17, rbx);", // IID650 + "__ push2(r20, r30);", // IID651 + "__ push2p(r8, r31);", // IID652 + "__ movzbq(r28, Address(r8, r14, (Address::ScaleFactor)0, +0x469ae67a));", // IID653 + "__ movzwq(r14, Address(r8, r18, (Address::ScaleFactor)2, -0x48699e02));", // IID654 + "__ movsbq(r21, Address(rbx, -0x64dae06b));", // IID655 + "__ movswq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x60318819));", // IID656 + "__ movzbq(r30, r13);", // IID657 + "__ movzwq(r30, r18);", // IID658 + "__ movsbq(r19, r15);", // IID659 + "__ movswq(r20, r16);", // IID660 + "__ cmpxchgq(r28, Address(r11, rbx, (Address::ScaleFactor)3, +0xfc3479d));", // IID661 + "__ eidivq(r20, false);", // IID662 + "__ eidivq(r30, true);", // IID663 + "__ edivq(r22, false);", // IID664 + "__ edivq(r11, true);", // IID665 + "__ eimulq(rcx, false);", // IID666 + "__ eimulq(r28, true);", // IID667 + "__ emulq(r21, false);", // IID668 + "__ emulq(r13, true);", // IID669 + "__ emulq(Address(r26, r15, (Address::ScaleFactor)2, +0x70a1ce6e), false);", // IID670 + "__ emulq(Address(r24, r19, (Address::ScaleFactor)1, -0x1670855c), true);", // IID671 + "__ eimulq(r10, r27, false);", // IID672 + "__ eimulq(r17, r17, false);", // IID673 + "__ eimulq(rdx, r22, true);", // IID674 + "__ eimulq(rbx, rbx, true);", // IID675 + "__ elzcntq(r28, r15, false);", // IID676 + "__ elzcntq(r15, r15, false);", // IID677 + "__ elzcntq(rbx, r12, true);", // IID678 + "__ elzcntq(rbx, rbx, true);", // IID679 + "__ enegq(r26, r11, false);", // IID680 + "__ enegq(r17, r17, false);", // IID681 + "__ enegq(rdx, r31, true);", // IID682 + "__ enegq(r27, r27, true);", // IID683 + "__ enotq(r31, r15);", // IID684 + "__ enotq(r21, r21);", // IID685 + "__ epopcntq(rbx, r24, false);", // IID686 + "__ epopcntq(r28, r28, false);", // IID687 + "__ epopcntq(r23, r27, true);", // IID688 + "__ epopcntq(r13, r13, true);", // IID689 + "__ erolq(r25, r28, false);", // IID690 + "__ erolq(r31, r31, false);", // IID691 + "__ erolq(r25, r23, true);", // IID692 + "__ erolq(rcx, rcx, true);", // IID693 + "__ erorq(r22, r14, false);", // IID694 + "__ erorq(r15, r15, false);", // IID695 + "__ erorq(r11, r30, true);", // IID696 + "__ erorq(r24, r24, true);", // IID697 + "__ esalq(r10, r20, false);", // IID698 + "__ esalq(r19, r19, false);", // IID699 + "__ esalq(r17, r25, true);", // IID700 + "__ esalq(r13, r13, true);", // IID701 + "__ esarq(r31, r30, false);", // IID702 + "__ esarq(r18, r18, false);", // IID703 + "__ esarq(r25, r25, true);", // IID704 + "__ esarq(r28, r28, true);", // IID705 + "__ edecq(r22, r27, false);", // IID706 + "__ edecq(r12, r12, false);", // IID707 + "__ edecq(r18, r11, true);", // IID708 + "__ edecq(r10, r10, true);", // IID709 + "__ eincq(r20, r24, false);", // IID710 + "__ eincq(r18, r18, false);", // IID711 + "__ eincq(rbx, r11, true);", // IID712 + "__ eincq(r26, r26, true);", // IID713 + "__ eshlq(r21, r8, false);", // IID714 + "__ eshlq(rbx, rbx, false);", // IID715 + "__ eshlq(r22, r21, true);", // IID716 + "__ eshlq(r27, r27, true);", // IID717 + "__ eshrq(r12, r16, false);", // IID718 + "__ eshrq(r8, r8, false);", // IID719 + "__ eshrq(rdx, r9, true);", // IID720 + "__ eshrq(r20, r20, true);", // IID721 + "__ etzcntq(r31, r21, false);", // IID722 + "__ etzcntq(r20, r20, false);", // IID723 + "__ etzcntq(rcx, r16, true);", // IID724 + "__ etzcntq(r14, r14, true);", // IID725 + "__ eimulq(r27, Address(r25, r9, (Address::ScaleFactor)1, +0x445a2393), false);", // IID726 + "__ eimulq(r23, Address(rcx, r9, (Address::ScaleFactor)1, -0x1480ef0c), true);", // IID727 + "__ elzcntq(r13, Address(r22, r17, (Address::ScaleFactor)1, -0x750c1996), false);", // IID728 + "__ elzcntq(r13, Address(r31, -0x342b6259), true);", // IID729 + "__ enegq(r31, Address(r24, r13, (Address::ScaleFactor)1, -0x25b16a0e), false);", // IID730 + "__ enegq(r13, Address(r11, r28, (Address::ScaleFactor)3, +0x5c0013ab), true);", // IID731 + "__ epopcntq(rdx, Address(r18, rcx, (Address::ScaleFactor)2, -0x6113eaaf), false);", // IID732 + "__ epopcntq(r9, Address(r10, -0x5ca7d588), true);", // IID733 + "__ esalq(r17, Address(r27, r30, (Address::ScaleFactor)0, +0x1b4cda2c), false);", // IID734 + "__ esalq(r25, Address(r12, rdx, (Address::ScaleFactor)1, +0x62823bce), true);", // IID735 + "__ esarq(r9, Address(r10, r18, (Address::ScaleFactor)2, -0x264a7a48), false);", // IID736 + "__ esarq(rbx, Address(r14, r27, (Address::ScaleFactor)0, +0x20291e00), true);", // IID737 + "__ edecq(r12, Address(r15, r14, (Address::ScaleFactor)2, -0x20f7dabb), false);", // IID738 + "__ edecq(r9, Address(r10, r25, (Address::ScaleFactor)1, +0x21411d84), true);", // IID739 + "__ eincq(r20, Address(rbx, r25, (Address::ScaleFactor)3, +0x2f0329e), false);", // IID740 + "__ eincq(r10, Address(r12, r31, (Address::ScaleFactor)0, -0x37505c8c), true);", // IID741 + "__ eshrq(r24, Address(r23, r14, (Address::ScaleFactor)3, -0x71e75ab0), false);", // IID742 + "__ eshrq(r25, Address(r19, r10, (Address::ScaleFactor)1, +0x507b0a88), true);", // IID743 + "__ etzcntq(r31, Address(rbx, r16, (Address::ScaleFactor)0, +0x19d5192a), false);", // IID744 + "__ etzcntq(r9, Address(r22, r28, (Address::ScaleFactor)2, +0x211007cd), true);", // IID745 + "__ eaddq(r16, Address(r21, rbx, (Address::ScaleFactor)3, -0x823fa1e), r28, false);", // IID746 + "__ eaddq(r15, Address(rdx, r8, (Address::ScaleFactor)3, -0x34b9a058), r15, false);", // IID747 + "__ eaddq(r24, Address(r14, r24, (Address::ScaleFactor)3, +0x6cdc59d2), r13, true);", // IID748 + "__ eaddq(rbx, Address(r27, r14, (Address::ScaleFactor)3, +0x36c5e8de), rbx, true);", // IID749 + "__ eandq(r21, Address(r27, r27, (Address::ScaleFactor)1, -0x2c023b13), r27, false);", // IID750 + "__ eandq(r31, Address(r21, r15, (Address::ScaleFactor)2, +0x6ef2c74a), r31, false);", // IID751 + "__ eandq(r13, Address(r31, r25, (Address::ScaleFactor)1, +0x734fe9ab), r27, true);", // IID752 + "__ eandq(r15, Address(r14, r29, (Address::ScaleFactor)3, -0x6e68556), r15, true);", // IID753 + "__ eorq(r12, Address(r30, r15, (Address::ScaleFactor)3, +0x3ba33f9e), r28, false);", // IID754 + "__ eorq(r16, Address(r12, r9, (Address::ScaleFactor)0, -0x28e03b33), r16, false);", // IID755 + "__ eorq(r8, Address(r8, r25, (Address::ScaleFactor)3, -0x1e42bd95), r27, true);", // IID756 + "__ eorq(rcx, Address(r27, rbx, (Address::ScaleFactor)2, +0x7be4bcad), rcx, true);", // IID757 + "__ esubq(r24, Address(r23, r22, (Address::ScaleFactor)2, +0x6f8827d7), rdx, false);", // IID758 + "__ esubq(r21, Address(r10, -0x635b8c8), r21, false);", // IID759 + "__ esubq(r23, Address(r27, r26, (Address::ScaleFactor)3, +0x922bcc0), rbx, true);", // IID760 + "__ esubq(r25, Address(r23, r15, (Address::ScaleFactor)0, -0x38f494ac), r25, true);", // IID761 + "__ exorq(r11, Address(r12, r19, (Address::ScaleFactor)2, -0x5b71ec17), rcx, false);", // IID762 + "__ exorq(r28, Address(r19, r18, (Address::ScaleFactor)0, +0x716b9b7e), r28, false);", // IID763 + "__ exorq(r21, Address(rcx, r29, (Address::ScaleFactor)0, -0x5af0441e), r16, true);", // IID764 + "__ exorq(r12, Address(r20, r26, (Address::ScaleFactor)0, +0xe0b7fb1), r12, true);", // IID765 + "__ eaddq(r30, Address(rcx, +0x2d3b7b4f), 1048576, false);", // IID766 + "__ eaddq(r14, Address(r21, r15, (Address::ScaleFactor)2, -0x1222aee8), 4096, true);", // IID767 + "__ eandq(r23, Address(r20, r31, (Address::ScaleFactor)0, -0x96e4d6a), 16, false);", // IID768 + "__ eandq(r10, Address(rdx, rdx, (Address::ScaleFactor)3, +0x3875f17c), 1, true);", // IID769 + "__ eimulq(r17, Address(rcx, r25, (Address::ScaleFactor)2, +0x32c71076), 4096, false);", // IID770 + "__ eimulq(r19, Address(r31, rbx, (Address::ScaleFactor)2, +0x7bada60d), 1048576, true);", // IID771 + "__ eorq(r25, Address(r18, r23, (Address::ScaleFactor)1, +0x48147444), 16777216, false);", // IID772 + "__ eorq(r29, Address(r26, r27, (Address::ScaleFactor)1, -0x4b113958), 1048576, true);", // IID773 + "__ esalq(r31, Address(r18, -0x46103c74), 2, false);", // IID774 + "__ esalq(r25, Address(r10, r15, (Address::ScaleFactor)0, +0x48925da4), 16, true);", // IID775 + "__ esarq(r26, Address(r18, -0x5ea1c542), 8, false);", // IID776 + "__ esarq(r12, Address(r10, r22, (Address::ScaleFactor)2, +0x5d958264), 8, true);", // IID777 + "__ eshrq(rdx, Address(r17, r20, (Address::ScaleFactor)2, +0x295add23), 16, false);", // IID778 + "__ eshrq(rbx, Address(r22, r28, (Address::ScaleFactor)1, +0x782929cb), 2, true);", // IID779 + "__ esubq(r19, Address(r23, -0x49811d72), 1, false);", // IID780 + "__ esubq(r8, Address(r19, r14, (Address::ScaleFactor)2, -0x1b2bae9a), 1048576, true);", // IID781 + "__ exorq(r19, Address(rcx, r10, (Address::ScaleFactor)0, +0x45a66ee9), 1048576, false);", // IID782 + "__ exorq(r28, Address(r9, r29, (Address::ScaleFactor)0, -0x28a19314), 16, true);", // IID783 + "__ eaddq(r8, rcx, 16777216, false);", // IID784 + "__ eaddq(rax, r14, 16777216, false);", // IID785 + "__ eaddq(r16, r16, 256, false);", // IID786 + "__ eaddq(r24, r9, 4096, true);", // IID787 + "__ eaddq(rax, r18, 4096, true);", // IID788 + "__ eaddq(r8, r8, 1, true);", // IID789 + "__ eandq(r15, r22, 1048576, false);", // IID790 + "__ eandq(rax, r26, 1048576, false);", // IID791 + "__ eandq(rdx, rdx, 4096, false);", // IID792 + "__ eandq(rdx, r22, 268435456, true);", // IID793 + "__ eandq(rax, r29, 268435456, true);", // IID794 + "__ eandq(r23, r23, 16777216, true);", // IID795 + "__ eimulq(r9, r13, 1048576, false);", // IID796 + "__ eimulq(rax, r18, 1048576, false);", // IID797 + "__ eimulq(r16, r16, 1048576, false);", // IID798 + "__ eimulq(r17, r23, 1, true);", // IID799 + "__ eimulq(rax, r12, 1, true);", // IID800 + "__ eimulq(r10, r10, 268435456, true);", // IID801 + "__ eorq(rdx, r19, 256, false);", // IID802 + "__ eorq(rax, r14, 256, false);", // IID803 + "__ eorq(r13, r13, 1, false);", // IID804 + "__ eorq(r25, r29, 256, true);", // IID805 + "__ eorq(rax, rdx, 256, true);", // IID806 + "__ eorq(r16, r16, 16, true);", // IID807 + "__ erclq(r13, r19, 4);", // IID808 + "__ erclq(rax, r12, 4);", // IID809 + "__ erclq(r9, r9, 4);", // IID810 + "__ erolq(r13, r16, 1, false);", // IID811 + "__ erolq(rax, r31, 1, false);", // IID812 + "__ erolq(r30, r30, 8, false);", // IID813 + "__ erolq(r30, r20, 8, true);", // IID814 + "__ erolq(rax, r31, 8, true);", // IID815 + "__ erolq(r31, r31, 4, true);", // IID816 + "__ erorq(r22, r10, 4, false);", // IID817 + "__ erorq(rax, r13, 4, false);", // IID818 + "__ erorq(r24, r24, 16, false);", // IID819 + "__ erorq(r29, r22, 16, true);", // IID820 + "__ erorq(rax, r20, 16, true);", // IID821 + "__ erorq(r27, r27, 4, true);", // IID822 + "__ esalq(r31, r19, 2, false);", // IID823 + "__ esalq(rax, r20, 2, false);", // IID824 + "__ esalq(r11, r11, 8, false);", // IID825 + "__ esalq(rdx, r15, 1, true);", // IID826 + "__ esalq(rax, r10, 1, true);", // IID827 + "__ esalq(r29, r29, 4, true);", // IID828 + "__ esarq(r20, r16, 1, false);", // IID829 + "__ esarq(rax, r21, 1, false);", // IID830 + "__ esarq(r28, r28, 8, false);", // IID831 + "__ esarq(r30, rcx, 4, true);", // IID832 + "__ esarq(rax, r15, 4, true);", // IID833 + "__ esarq(rcx, rcx, 4, true);", // IID834 + "__ eshlq(rdx, r26, 4, false);", // IID835 + "__ eshlq(rax, r26, 4, false);", // IID836 + "__ eshlq(r8, r8, 4, false);", // IID837 + "__ eshlq(rcx, rcx, 1, true);", // IID838 + "__ eshlq(rax, rcx, 1, true);", // IID839 + "__ eshlq(r13, r13, 2, true);", // IID840 + "__ eshrq(r14, r27, 2, false);", // IID841 + "__ eshrq(rax, r11, 2, false);", // IID842 + "__ eshrq(r9, r9, 16, false);", // IID843 + "__ eshrq(rdx, r31, 2, true);", // IID844 + "__ eshrq(rax, r14, 2, true);", // IID845 + "__ eshrq(r12, r12, 8, true);", // IID846 + "__ esubq(r10, r28, 1, false);", // IID847 + "__ esubq(rax, r8, 1, false);", // IID848 + "__ esubq(rcx, rcx, 16777216, false);", // IID849 + "__ esubq(rdx, rbx, 16777216, true);", // IID850 + "__ esubq(rax, r18, 16777216, true);", // IID851 + "__ esubq(r27, r27, 65536, true);", // IID852 + "__ exorq(r30, rcx, 4096, false);", // IID853 + "__ exorq(rax, r21, 4096, false);", // IID854 + "__ exorq(rcx, rcx, 16777216, false);", // IID855 + "__ exorq(r21, r12, 1, true);", // IID856 + "__ exorq(rax, rdx, 1, true);", // IID857 + "__ exorq(rbx, rbx, 16777216, true);", // IID858 + "__ eorq_imm32(r11, rdx, 65536, false);", // IID859 + "__ eorq_imm32(rax, r14, 65536, false);", // IID860 + "__ eorq_imm32(r14, r14, 262144, false);", // IID861 + "__ eorq_imm32(r25, r29, 262144, false);", // IID862 + "__ eorq_imm32(rax, r21, 262144, false);", // IID863 + "__ eorq_imm32(r11, r11, 16777216, false);", // IID864 + "__ esubq_imm32(r29, r19, 67108864, false);", // IID865 + "__ esubq_imm32(rax, r11, 67108864, false);", // IID866 + "__ esubq_imm32(r18, r18, 67108864, false);", // IID867 + "__ esubq_imm32(r28, r23, 4194304, true);", // IID868 + "__ esubq_imm32(rax, r21, 4194304, true);", // IID869 + "__ esubq_imm32(r16, r16, 16777216, true);", // IID870 + "__ eaddq(r8, r25, Address(r26, r8, (Address::ScaleFactor)1, +0x10633def), false);", // IID871 + "__ eaddq(r13, r13, Address(r18, r16, (Address::ScaleFactor)1, -0x74204508), false);", // IID872 + "__ eaddq(r17, r26, Address(r12, +0x23a80abf), true);", // IID873 + "__ eaddq(r9, r9, Address(r29, r19, (Address::ScaleFactor)0, -0x29e9e52), true);", // IID874 + "__ eandq(r9, r28, Address(rcx, r25, (Address::ScaleFactor)2, +0x4261ffaa), false);", // IID875 + "__ eandq(r27, r27, Address(rdx, r28, (Address::ScaleFactor)0, -0x26bdc9c1), false);", // IID876 + "__ eandq(r14, r11, Address(r16, +0x63ba0ddf), true);", // IID877 + "__ eandq(r8, r8, Address(r22, r25, (Address::ScaleFactor)1, -0x43b6ab44), true);", // IID878 + "__ eorq(r19, rcx, Address(r27, rcx, (Address::ScaleFactor)2, -0x7f687fc6), false);", // IID879 + "__ eorq(r19, r19, Address(rbx, r26, (Address::ScaleFactor)1, -0x486db7ea), false);", // IID880 + "__ eorq(r30, r10, Address(r14, r18, (Address::ScaleFactor)3, +0x14884884), true);", // IID881 + "__ eorq(r27, r27, Address(r29, +0x20337180), true);", // IID882 + "__ eimulq(rcx, r21, Address(r21, rbx, (Address::ScaleFactor)0, -0x3303888e), false);", // IID883 + "__ eimulq(rdx, rdx, Address(r28, r9, (Address::ScaleFactor)3, -0x7ad8f741), false);", // IID884 + "__ eimulq(r8, r29, Address(r17, r12, (Address::ScaleFactor)0, +0x6e85396a), true);", // IID885 + "__ eimulq(r16, r16, Address(r19, r10, (Address::ScaleFactor)3, -0x49599300), true);", // IID886 + "__ esubq(r20, r17, Address(r13, r22, (Address::ScaleFactor)0, +0x1d219a4f), false);", // IID887 + "__ esubq(r25, r25, Address(r21, r21, (Address::ScaleFactor)3, -0x6868a8c7), false);", // IID888 + "__ esubq(r20, r24, Address(rbx, r20, (Address::ScaleFactor)2, +0x32c59da6), true);", // IID889 + "__ esubq(r8, r8, Address(r12, r17, (Address::ScaleFactor)0, -0x26be2dcf), true);", // IID890 + "__ exorq(rdx, r19, Address(r9, +0x7d903b91), false);", // IID891 + "__ exorq(r28, r28, Address(r29, r27, (Address::ScaleFactor)2, +0x53091f6f), false);", // IID892 + "__ exorq(r17, r16, Address(r27, +0x7c6e9207), true);", // IID893 + "__ exorq(r15, r15, Address(r13, r24, (Address::ScaleFactor)3, -0x75c87960), true);", // IID894 + "__ eaddq(r16, rbx, r18, false);", // IID895 + "__ eaddq(r24, r24, r18, false);", // IID896 + "__ eaddq(r9, r15, r9, false);", // IID897 + "__ eaddq(r19, r26, r13, true);", // IID898 + "__ eaddq(r28, r28, r22, true);", // IID899 + "__ eaddq(r22, r11, r22, true);", // IID900 + "__ eadcxq(rcx, r12, r13);", // IID901 + "__ eadcxq(r30, r30, r12);", // IID902 + "__ eadoxq(r28, r14, r18);", // IID903 + "__ eadoxq(r30, r30, r19);", // IID904 + "__ eandq(r20, r14, r14, false);", // IID905 + "__ eandq(r17, r17, r23, false);", // IID906 + "__ eandq(r17, r14, r17, false);", // IID907 + "__ eandq(r19, r20, r15, true);", // IID908 + "__ eandq(rbx, rbx, r13, true);", // IID909 + "__ eandq(r22, r30, r22, true);", // IID910 + "__ eimulq(r17, r24, rcx, false);", // IID911 + "__ eimulq(r21, r21, r8, false);", // IID912 + "__ eimulq(r29, r21, r29, false);", // IID913 + "__ eimulq(r27, r13, r23, true);", // IID914 + "__ eimulq(r26, r26, r8, true);", // IID915 + "__ eimulq(r22, r13, r22, true);", // IID916 + "__ eorq(r11, rdx, r29, false);", // IID917 + "__ eorq(rdx, rdx, r31, false);", // IID918 + "__ eorq(r10, r29, r10, false);", // IID919 + "__ eorq(r27, r28, rcx, true);", // IID920 + "__ eorq(r25, r25, r9, true);", // IID921 + "__ eorq(rcx, r8, rcx, true);", // IID922 + "__ esubq(rcx, r10, r16, false);", // IID923 + "__ esubq(r17, r17, rcx, false);", // IID924 + "__ esubq(r13, r21, r24, true);", // IID925 + "__ esubq(r31, r31, r28, true);", // IID926 + "__ exorq(r23, r28, r23, false);", // IID927 + "__ exorq(r10, r10, r11, false);", // IID928 + "__ exorq(r19, r18, r19, false);", // IID929 + "__ exorq(r31, r9, rdx, true);", // IID930 + "__ exorq(r13, r13, r9, true);", // IID931 + "__ exorq(rcx, r10, rcx, true);", // IID932 + "__ eshldq(r12, r24, r22, 8, false);", // IID933 + "__ eshldq(r25, r25, r25, 8, false);", // IID934 + "__ eshldq(r21, r20, r15, 8, true);", // IID935 + "__ eshldq(r21, r21, r10, 8, true);", // IID936 + "__ eshrdq(r18, r18, r8, 2, false);", // IID937 + "__ eshrdq(r26, r26, r29, 8, false);", // IID938 + "__ eshrdq(r29, r26, r19, 2, true);", // IID939 + "__ eshrdq(r12, r12, rcx, 4, true);", // IID940 + "__ ecmovq (Assembler::Condition::overflow, r21, r22, r23);", // IID941 + "__ ecmovq (Assembler::Condition::overflow, r9, r9, r13);", // IID942 + "__ ecmovq (Assembler::Condition::noOverflow, rcx, r23, r24);", // IID943 + "__ ecmovq (Assembler::Condition::noOverflow, r28, r28, rdx);", // IID944 + "__ ecmovq (Assembler::Condition::below, r14, r31, r23);", // IID945 + "__ ecmovq (Assembler::Condition::below, r30, r30, r23);", // IID946 + "__ ecmovq (Assembler::Condition::aboveEqual, r10, r29, r22);", // IID947 + "__ ecmovq (Assembler::Condition::aboveEqual, rbx, rbx, r26);", // IID948 + "__ ecmovq (Assembler::Condition::zero, r23, r21, r13);", // IID949 + "__ ecmovq (Assembler::Condition::zero, r10, r10, r20);", // IID950 + "__ ecmovq (Assembler::Condition::notZero, rbx, r9, r29);", // IID951 + "__ ecmovq (Assembler::Condition::notZero, r16, r16, r30);", // IID952 + "__ ecmovq (Assembler::Condition::belowEqual, r13, rcx, r29);", // IID953 + "__ ecmovq (Assembler::Condition::belowEqual, r31, r31, r13);", // IID954 + "__ ecmovq (Assembler::Condition::above, r27, r9, r30);", // IID955 + "__ ecmovq (Assembler::Condition::above, r26, r26, r20);", // IID956 + "__ ecmovq (Assembler::Condition::negative, r8, r12, r22);", // IID957 + "__ ecmovq (Assembler::Condition::negative, r31, r31, r17);", // IID958 + "__ ecmovq (Assembler::Condition::positive, r29, rcx, r25);", // IID959 + "__ ecmovq (Assembler::Condition::positive, r22, r22, r14);", // IID960 + "__ ecmovq (Assembler::Condition::parity, rcx, r27, r9);", // IID961 + "__ ecmovq (Assembler::Condition::parity, r22, r22, r11);", // IID962 + "__ ecmovq (Assembler::Condition::noParity, r14, r19, r24);", // IID963 + "__ ecmovq (Assembler::Condition::noParity, r24, r24, r17);", // IID964 + "__ ecmovq (Assembler::Condition::less, r17, r19, r30);", // IID965 + "__ ecmovq (Assembler::Condition::less, r19, r19, r14);", // IID966 + "__ ecmovq (Assembler::Condition::greaterEqual, r25, r11, r29);", // IID967 + "__ ecmovq (Assembler::Condition::greaterEqual, r12, r12, r26);", // IID968 + "__ ecmovq (Assembler::Condition::lessEqual, r11, rbx, r10);", // IID969 + "__ ecmovq (Assembler::Condition::lessEqual, rdx, rdx, r22);", // IID970 + "__ ecmovq (Assembler::Condition::greater, r14, r15, r23);", // IID971 + "__ ecmovq (Assembler::Condition::greater, r8, r8, r24);", // IID972 + "__ ecmovq (Assembler::Condition::overflow, rbx, r31, Address(r10, r8, (Address::ScaleFactor)3, -0x313f60e0));", // IID973 + "__ ecmovq (Assembler::Condition::overflow, r23, r23, Address(rcx, r24, (Address::ScaleFactor)2, +0x17f41d9c));", // IID974 + "__ ecmovq (Assembler::Condition::noOverflow, r31, r11, Address(r16, +0x2c018942));", // IID975 + "__ ecmovq (Assembler::Condition::noOverflow, r11, r11, Address(r16, r20, (Address::ScaleFactor)3, +0x674b6a55));", // IID976 + "__ ecmovq (Assembler::Condition::below, r9, r13, Address(r9, rcx, (Address::ScaleFactor)0, +0x394a11df));", // IID977 + "__ ecmovq (Assembler::Condition::below, r30, r30, Address(rdx, r22, (Address::ScaleFactor)1, -0x6c362b88));", // IID978 + "__ ecmovq (Assembler::Condition::aboveEqual, r13, rcx, Address(r24, rcx, (Address::ScaleFactor)3, +0x46500b66));", // IID979 + "__ ecmovq (Assembler::Condition::aboveEqual, r24, r24, Address(r18, r25, (Address::ScaleFactor)1, +0x53283b7c));", // IID980 + "__ ecmovq (Assembler::Condition::zero, r23, r25, Address(r15, r9, (Address::ScaleFactor)0, -0x5f03031e));", // IID981 + "__ ecmovq (Assembler::Condition::zero, r25, r25, Address(r28, r16, (Address::ScaleFactor)1, -0x53cef514));", // IID982 + "__ ecmovq (Assembler::Condition::notZero, rbx, r25, Address(r24, r25, (Address::ScaleFactor)2, -0x66caac87));", // IID983 + "__ ecmovq (Assembler::Condition::notZero, r16, r16, Address(r27, r30, (Address::ScaleFactor)3, +0x797f455d));", // IID984 + "__ ecmovq (Assembler::Condition::belowEqual, r25, r30, Address(r18, r18, (Address::ScaleFactor)1, +0x1c9daacd));", // IID985 + "__ ecmovq (Assembler::Condition::belowEqual, r22, r22, Address(rcx, r25, (Address::ScaleFactor)1, -0x3dcbfaa9));", // IID986 + "__ ecmovq (Assembler::Condition::above, r24, r26, Address(r25, +0x747060b5));", // IID987 + "__ ecmovq (Assembler::Condition::above, r8, r8, Address(r24, r20, (Address::ScaleFactor)3, +0x47d285f6));", // IID988 + "__ ecmovq (Assembler::Condition::negative, r12, r16, Address(r13, r10, (Address::ScaleFactor)2, +0x34e5b214));", // IID989 + "__ ecmovq (Assembler::Condition::negative, rdx, rdx, Address(r15, r19, (Address::ScaleFactor)0, -0x405138b1));", // IID990 + "__ ecmovq (Assembler::Condition::positive, r18, r21, Address(rbx, r13, (Address::ScaleFactor)2, +0x51b19197));", // IID991 + "__ ecmovq (Assembler::Condition::positive, r24, r24, Address(r11, r31, (Address::ScaleFactor)3, +0x3e01520a));", // IID992 + "__ ecmovq (Assembler::Condition::parity, r29, r26, Address(r10, r25, (Address::ScaleFactor)3, -0x5f7c3872));", // IID993 + "__ ecmovq (Assembler::Condition::parity, r11, r11, Address(r22, r10, (Address::ScaleFactor)3, -0x68731453));", // IID994 + "__ ecmovq (Assembler::Condition::noParity, r20, r15, Address(r9, r25, (Address::ScaleFactor)0, +0x4a37edaa));", // IID995 + "__ ecmovq (Assembler::Condition::noParity, r31, r31, Address(r9, r20, (Address::ScaleFactor)0, +0x4f999f86));", // IID996 + "__ ecmovq (Assembler::Condition::less, r18, r23, Address(r9, r27, (Address::ScaleFactor)0, -0x3410441d));", // IID997 + "__ ecmovq (Assembler::Condition::less, r16, r16, Address(r24, r10, (Address::ScaleFactor)3, +0x52ed66ee));", // IID998 + "__ ecmovq (Assembler::Condition::greaterEqual, r11, r18, Address(rcx, +0x1de09163));", // IID999 + "__ ecmovq (Assembler::Condition::greaterEqual, r14, r14, Address(r24, r23, (Address::ScaleFactor)1, +0x5df3b4da));", // IID1000 + "__ ecmovq (Assembler::Condition::lessEqual, r15, r14, Address(r30, r20, (Address::ScaleFactor)1, +0x5c9ab976));", // IID1001 + "__ ecmovq (Assembler::Condition::lessEqual, r26, r26, Address(r18, r27, (Address::ScaleFactor)2, -0xd8c329));", // IID1002 + "__ ecmovq (Assembler::Condition::greater, r29, r9, Address(r30, r20, (Address::ScaleFactor)3, -0x37a9cf8d));", // IID1003 + "__ ecmovq (Assembler::Condition::greater, r20, r20, Address(r8, rbx, (Address::ScaleFactor)1, +0x1bdc7def));", // IID1004 #endif // _LP64 }; // END Generated code -- do not edit diff --git a/test/hotspot/gtest/x86/x86-asmtest.py b/test/hotspot/gtest/x86/x86-asmtest.py index 7081c64e604..714b7aaea40 100644 --- a/test/hotspot/gtest/x86/x86-asmtest.py +++ b/test/hotspot/gtest/x86/x86-asmtest.py @@ -92,6 +92,8 @@ registers_mapping = { 'r31': {64: 'r31', 32: 'r31d', 16: 'r31w', 8: 'r31b'}, } +commutative_instrs = ['imul', 'add', 'and', 'xor', 'or'] + class Operand(object): def generate(self): return self @@ -400,6 +402,15 @@ class RegMemRegNddInstruction(NFInstruction): self.mem = Address().generate(mem_base, mem_idx, width) self.reg2 = Register().generate(reg2, width) self.generate_operands(self.reg1, self.mem, self.reg2) + self.demote = True + + def astr(self): + if self.demote: + ops = [op.cstr() for op in self.operands] + # imul does not support RegMemReg + if self._aname in commutative_instrs[1:] and ops[0] == ops[2] and (not self.no_flag): + return f'{self._aname} ' + ', '.join([op.astr() for op in self.operands[:2]]) + return super().astr() class RegRegImmNddInstruction(NFInstruction): def __init__(self, name, aname, width, no_flag, reg1, reg2, imm): @@ -448,6 +459,9 @@ class RegRegRegNddInstruction(NFInstruction): ops = [op.cstr() for op in self.operands] if ops[0] == ops[1] and (not self.no_flag): return hdr + f'{self._aname} ' + ', '.join([op.astr() for op in self.operands[1:]]) + if self._aname in commutative_instrs and ops[0] == ops[2] and (not self.no_flag): + return hdr + f'{self._aname} ' + ', '.join([op.astr() for op in self.operands[:2]]) + return hdr + super().astr() class RegRegRegImmNddInstruction(NFInstruction): @@ -574,6 +588,18 @@ def generate(RegOp, ops, print_lp64_flag=True, full_set=False): lp64_flag = handle_lp64_flag(lp64_flag, print_lp64_flag, test_reg1, test_reg2, test_reg3) instr = RegOp(*op, reg1=test_reg1, reg2=test_reg2, reg3=test_reg3) print_instruction(instr, lp64_flag, print_lp64_flag) + + demote = True if TEST_DEMOTION else False + commute = True if op[1] in commutative_instrs else False + if RegOp in [RegRegRegNddInstruction] and demote and commute : + for i in range(len(test_regs) if full_set else 1): + test_reg1 = test_regs[i] if full_set else random.choice(test_regs) + test_reg2 = test_regs[(i + 2) % len(test_regs)] if full_set else random.choice(test_regs) + test_reg3 = test_reg1 + + lp64_flag = handle_lp64_flag(lp64_flag, print_lp64_flag, test_reg1, test_reg2, test_reg3) + instr = RegOp(*op, reg1=test_reg1, reg2=test_reg2, reg3=test_reg3) + print_instruction(instr, lp64_flag, print_lp64_flag) elif RegOp in [MemRegInstruction, RegMemInstruction, MoveRegMemInstruction, CmpxchgInstruction, CondRegMemInstruction, RegMemNddInstruction]: if full_set: @@ -699,7 +725,7 @@ def generate(RegOp, ops, print_lp64_flag=True, full_set=False): print_instruction(instr, lp64_flag, print_lp64_flag) elif RegOp in [RegMemRegNddInstruction, RegRegMemNddInstruction, CondRegRegMemInstruction]: - demote_options = [False] if TEST_DEMOTION and RegOp not in [RegMemRegNddInstruction] else [False, True] + demote_options = [False, True] for demote in demote_options: for i in range(len(test_regs) if full_set else 1): test_reg1 = test_regs[i] if full_set else random.choice(test_regs) @@ -1023,6 +1049,8 @@ instruction_set = { RegMemRegNddInstruction: [ ('eaddl', 'add', 32, False), ('eaddl', 'add', 32, True), + ('eandl', 'and', 32, False), + ('eandl', 'and', 32, True), ('eorl', 'or', 32, False), ('eorl', 'or', 32, True), ('eorb', 'or', 8, False), From e883dec6be8cb2fc44e45a6b4677cca2f4df58ef Mon Sep 17 00:00:00 2001 From: Srinivas Vamsi Parasa Date: Tue, 16 Sep 2025 18:14:07 +0000 Subject: [PATCH 065/556] 8367694: Fix jtreg test failure when Intel APX is enabled for KNL platforms Reviewed-by: sviswanathan, epeter --- src/hotspot/cpu/x86/assembler_x86.cpp | 24 ++++++++++++------------ src/hotspot/cpu/x86/vm_version_x86.cpp | 22 ++++++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/hotspot/cpu/x86/assembler_x86.cpp b/src/hotspot/cpu/x86/assembler_x86.cpp index 49d40447f9f..3f1140c937b 100644 --- a/src/hotspot/cpu/x86/assembler_x86.cpp +++ b/src/hotspot/cpu/x86/assembler_x86.cpp @@ -13780,7 +13780,7 @@ void Assembler::pdepq(Register dst, Register src1, Address src2) { void Assembler::sarxl(Register dst, Register src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); int encode = vex_prefix_and_encode(dst->encoding(), src2->encoding(), src1->encoding(), VEX_SIMD_F3, VEX_OPCODE_0F_38, &attributes, true); emit_int16((unsigned char)0xF7, (0xC0 | encode)); } @@ -13788,7 +13788,7 @@ void Assembler::sarxl(Register dst, Register src1, Register src2) { void Assembler::sarxl(Register dst, Address src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_32bit); vex_prefix(src1, src2->encoding(), dst->encoding(), VEX_SIMD_F3, VEX_OPCODE_0F_38, &attributes); emit_int8((unsigned char)0xF7); @@ -13797,7 +13797,7 @@ void Assembler::sarxl(Register dst, Address src1, Register src2) { void Assembler::sarxq(Register dst, Register src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); int encode = vex_prefix_and_encode(dst->encoding(), src2->encoding(), src1->encoding(), VEX_SIMD_F3, VEX_OPCODE_0F_38, &attributes, true); emit_int16((unsigned char)0xF7, (0xC0 | encode)); } @@ -13805,7 +13805,7 @@ void Assembler::sarxq(Register dst, Register src1, Register src2) { void Assembler::sarxq(Register dst, Address src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_64bit); vex_prefix(src1, src2->encoding(), dst->encoding(), VEX_SIMD_F3, VEX_OPCODE_0F_38, &attributes); emit_int8((unsigned char)0xF7); @@ -13814,7 +13814,7 @@ void Assembler::sarxq(Register dst, Address src1, Register src2) { void Assembler::shlxl(Register dst, Register src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); int encode = vex_prefix_and_encode(dst->encoding(), src2->encoding(), src1->encoding(), VEX_SIMD_66, VEX_OPCODE_0F_38, &attributes, true); emit_int16((unsigned char)0xF7, (0xC0 | encode)); } @@ -13822,7 +13822,7 @@ void Assembler::shlxl(Register dst, Register src1, Register src2) { void Assembler::shlxl(Register dst, Address src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_32bit); vex_prefix(src1, src2->encoding(), dst->encoding(), VEX_SIMD_66, VEX_OPCODE_0F_38, &attributes); emit_int8((unsigned char)0xF7); @@ -13831,7 +13831,7 @@ void Assembler::shlxl(Register dst, Address src1, Register src2) { void Assembler::shlxq(Register dst, Register src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); int encode = vex_prefix_and_encode(dst->encoding(), src2->encoding(), src1->encoding(), VEX_SIMD_66, VEX_OPCODE_0F_38, &attributes, true); emit_int16((unsigned char)0xF7, (0xC0 | encode)); } @@ -13839,7 +13839,7 @@ void Assembler::shlxq(Register dst, Register src1, Register src2) { void Assembler::shlxq(Register dst, Address src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_64bit); vex_prefix(src1, src2->encoding(), dst->encoding(), VEX_SIMD_66, VEX_OPCODE_0F_38, &attributes); emit_int8((unsigned char)0xF7); @@ -13848,7 +13848,7 @@ void Assembler::shlxq(Register dst, Address src1, Register src2) { void Assembler::shrxl(Register dst, Register src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); int encode = vex_prefix_and_encode(dst->encoding(), src2->encoding(), src1->encoding(), VEX_SIMD_F2, VEX_OPCODE_0F_38, &attributes, true); emit_int16((unsigned char)0xF7, (0xC0 | encode)); } @@ -13856,7 +13856,7 @@ void Assembler::shrxl(Register dst, Register src1, Register src2) { void Assembler::shrxl(Register dst, Address src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_32bit); vex_prefix(src1, src2->encoding(), dst->encoding(), VEX_SIMD_F2, VEX_OPCODE_0F_38, &attributes); emit_int8((unsigned char)0xF7); @@ -13865,7 +13865,7 @@ void Assembler::shrxl(Register dst, Address src1, Register src2) { void Assembler::shrxq(Register dst, Register src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); int encode = vex_prefix_and_encode(dst->encoding(), src2->encoding(), src1->encoding(), VEX_SIMD_F2, VEX_OPCODE_0F_38, &attributes, true); emit_int16((unsigned char)0xF7, (0xC0 | encode)); } @@ -13873,7 +13873,7 @@ void Assembler::shrxq(Register dst, Register src1, Register src2) { void Assembler::shrxq(Register dst, Address src1, Register src2) { assert(VM_Version::supports_bmi2(), ""); InstructionMark im(this); - InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); attributes.set_address_attributes(/* tuple_type */ EVEX_NOSCALE, /* input_size_in_bits */ EVEX_64bit); vex_prefix(src1, src2->encoding(), dst->encoding(), VEX_SIMD_F2, VEX_OPCODE_0F_38, &attributes); emit_int8((unsigned char)0xF7); diff --git a/src/hotspot/cpu/x86/vm_version_x86.cpp b/src/hotspot/cpu/x86/vm_version_x86.cpp index 094ab370190..f380f2ad271 100644 --- a/src/hotspot/cpu/x86/vm_version_x86.cpp +++ b/src/hotspot/cpu/x86/vm_version_x86.cpp @@ -1016,16 +1016,6 @@ void VM_Version::get_processor_features() { _features.clear_feature(CPU_AVX10_2); } - // Currently APX support is only enabled for targets supporting AVX512VL feature. - bool apx_supported = os_supports_apx_egprs() && supports_apx_f() && supports_avx512vl(); - if (UseAPX && !apx_supported) { - warning("UseAPX is not supported on this CPU, setting it to false"); - FLAG_SET_DEFAULT(UseAPX, false); - } - - if (!UseAPX) { - _features.clear_feature(CPU_APX_F); - } if (UseAVX < 2) { _features.clear_feature(CPU_AVX2); @@ -1049,6 +1039,7 @@ void VM_Version::get_processor_features() { _features.clear_feature(CPU_VZEROUPPER); _features.clear_feature(CPU_AVX512BW); _features.clear_feature(CPU_AVX512VL); + _features.clear_feature(CPU_APX_F); _features.clear_feature(CPU_AVX512DQ); _features.clear_feature(CPU_AVX512_VNNI); _features.clear_feature(CPU_AVX512_VAES); @@ -1068,6 +1059,17 @@ void VM_Version::get_processor_features() { } } + // Currently APX support is only enabled for targets supporting AVX512VL feature. + bool apx_supported = os_supports_apx_egprs() && supports_apx_f() && supports_avx512vl(); + if (UseAPX && !apx_supported) { + warning("UseAPX is not supported on this CPU, setting it to false"); + FLAG_SET_DEFAULT(UseAPX, false); + } + + if (!UseAPX) { + _features.clear_feature(CPU_APX_F); + } + if (FLAG_IS_DEFAULT(IntelJccErratumMitigation)) { _has_intel_jcc_erratum = compute_has_intel_jcc_erratum(); FLAG_SET_ERGO(IntelJccErratumMitigation, _has_intel_jcc_erratum); From b75e35cb94d17a742d88f23dfd1b016c26a5e63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20H=C3=BCbner?= Date: Tue, 16 Sep 2025 19:17:53 +0000 Subject: [PATCH 066/556] 8365858: FilteredJavaFieldStream is unnecessary Reviewed-by: liach, jsjolen, coleenp, amenkov --- src/hotspot/share/classfile/javaClasses.cpp | 20 ++-- src/hotspot/share/classfile/javaClasses.hpp | 9 +- .../share/classfile/javaClassesImpl.hpp | 3 +- src/hotspot/share/prims/jvmtiEnv.cpp | 6 +- src/hotspot/share/prims/jvmtiTagMap.cpp | 18 ++- src/hotspot/share/runtime/reflectionUtils.cpp | 38 ------ src/hotspot/share/runtime/reflectionUtils.hpp | 109 ------------------ .../jdk/internal/reflect/ConstantPool.java | 7 -- .../ci/runtime/test/TestResolvedJavaType.java | 3 - 9 files changed, 28 insertions(+), 185 deletions(-) delete mode 100644 src/hotspot/share/runtime/reflectionUtils.cpp delete mode 100644 src/hotspot/share/runtime/reflectionUtils.hpp diff --git a/src/hotspot/share/classfile/javaClasses.cpp b/src/hotspot/share/classfile/javaClasses.cpp index da093936ce5..60a63892518 100644 --- a/src/hotspot/share/classfile/javaClasses.cpp +++ b/src/hotspot/share/classfile/javaClasses.cpp @@ -79,7 +79,7 @@ #include "runtime/javaCalls.hpp" #include "runtime/javaThread.hpp" #include "runtime/jniHandles.inline.hpp" -#include "runtime/reflectionUtils.hpp" +#include "runtime/reflection.hpp" #include "runtime/safepoint.hpp" #include "runtime/safepointVerifiers.hpp" #include "runtime/threadSMR.hpp" @@ -3741,20 +3741,17 @@ oop java_lang_reflect_RecordComponent::create(InstanceKlass* holder, RecordCompo return element(); } -int reflect_ConstantPool::_oop_offset; - -#define CONSTANTPOOL_FIELDS_DO(macro) \ - macro(_oop_offset, k, "constantPoolOop", object_signature, false) +int reflect_ConstantPool::_vmholder_offset; void reflect_ConstantPool::compute_offsets() { InstanceKlass* k = vmClasses::reflect_ConstantPool_klass(); - // The field is called ConstantPool* in the sun.reflect.ConstantPool class. - CONSTANTPOOL_FIELDS_DO(FIELD_COMPUTE_OFFSET); + // The field is injected and called Object vmholder in the jdk.internal.reflect.ConstantPool class. + CONSTANTPOOL_INJECTED_FIELDS(INJECTED_FIELD_COMPUTE_OFFSET); } #if INCLUDE_CDS void reflect_ConstantPool::serialize_offsets(SerializeClosure* f) { - CONSTANTPOOL_FIELDS_DO(FIELD_SERIALIZE_OFFSET); + CONSTANTPOOL_INJECTED_FIELDS(INJECTED_FIELD_SERIALIZE_OFFSET); } #endif @@ -3907,13 +3904,15 @@ Handle reflect_ConstantPool::create(TRAPS) { void reflect_ConstantPool::set_cp(oop reflect, ConstantPool* value) { + assert(_vmholder_offset != 0, "Uninitialized vmholder"); oop mirror = value->pool_holder()->java_mirror(); // Save the mirror to get back the constant pool. - reflect->obj_field_put(_oop_offset, mirror); + reflect->obj_field_put(_vmholder_offset, mirror); } ConstantPool* reflect_ConstantPool::get_cp(oop reflect) { - oop mirror = reflect->obj_field(_oop_offset); + assert(_vmholder_offset != 0, "Uninitialized vmholder"); + oop mirror = reflect->obj_field(_vmholder_offset); InstanceKlass* ik = java_lang_Class::as_InstanceKlass(mirror); // Get the constant pool back from the klass. Since class redefinition @@ -5554,5 +5553,4 @@ int InjectedField::compute_offset() { void javaClasses_init() { JavaClasses::compute_offsets(); JavaClasses::check_offsets(); - FilteredFieldsMap::initialize(); // must be done after computing offsets. } diff --git a/src/hotspot/share/classfile/javaClasses.hpp b/src/hotspot/share/classfile/javaClasses.hpp index 6f82ca10fd6..b137f1a8035 100644 --- a/src/hotspot/share/classfile/javaClasses.hpp +++ b/src/hotspot/share/classfile/javaClasses.hpp @@ -936,12 +936,16 @@ class java_lang_Module { friend class JavaClasses; }; +#define CONSTANTPOOL_INJECTED_FIELDS(macro) \ + macro(reflect_ConstantPool, vmholder, object_signature, false) + // Interface to jdk.internal.reflect.ConstantPool objects class reflect_ConstantPool { private: // Note that to reduce dependencies on the JDK we compute these - // offsets at run-time. - static int _oop_offset; + // offsets at run-time. This field is the oop offset for the + // actual constant pool, previously called constantPoolOop. + static int _vmholder_offset; static void compute_offsets(); @@ -953,7 +957,6 @@ class reflect_ConstantPool { // Accessors static void set_cp(oop reflect, ConstantPool* value); - static int oop_offset() { CHECK_INIT(_oop_offset); } static ConstantPool* get_cp(oop reflect); diff --git a/src/hotspot/share/classfile/javaClassesImpl.hpp b/src/hotspot/share/classfile/javaClassesImpl.hpp index b450a4e3cc4..5f88f708523 100644 --- a/src/hotspot/share/classfile/javaClassesImpl.hpp +++ b/src/hotspot/share/classfile/javaClassesImpl.hpp @@ -42,7 +42,8 @@ THREAD_INJECTED_FIELDS(macro) \ VTHREAD_INJECTED_FIELDS(macro) \ INTERNALERROR_INJECTED_FIELDS(macro) \ - STACKCHUNK_INJECTED_FIELDS(macro) + STACKCHUNK_INJECTED_FIELDS(macro) \ + CONSTANTPOOL_INJECTED_FIELDS(macro) #define INJECTED_FIELD_COMPUTE_OFFSET(klass, name, signature, may_be_java) \ klass::_##name##_offset = JavaClasses::compute_injected_offset(InjectedFieldID::klass##_##name##_enum); diff --git a/src/hotspot/share/prims/jvmtiEnv.cpp b/src/hotspot/share/prims/jvmtiEnv.cpp index 3eb507ba5e3..5642cd9ff8f 100644 --- a/src/hotspot/share/prims/jvmtiEnv.cpp +++ b/src/hotspot/share/prims/jvmtiEnv.cpp @@ -38,6 +38,7 @@ #include "memory/allocation.hpp" #include "memory/resourceArea.hpp" #include "memory/universe.hpp" +#include "oops/fieldStreams.inline.hpp" #include "oops/instanceKlass.hpp" #include "oops/klass.inline.hpp" #include "oops/objArrayOop.inline.hpp" @@ -68,7 +69,6 @@ #include "runtime/objectMonitor.inline.hpp" #include "runtime/os.hpp" #include "runtime/osThread.hpp" -#include "runtime/reflectionUtils.hpp" #include "runtime/signature.hpp" #include "runtime/threadHeapSampler.hpp" #include "runtime/threads.hpp" @@ -2841,9 +2841,9 @@ JvmtiEnv::GetClassFields(oop k_mirror, jint* field_count_ptr, jfieldID** fields_ InstanceKlass* ik = InstanceKlass::cast(k); - FilteredJavaFieldStream flds(ik); + JavaFieldStream flds(ik); - int result_count = flds.field_count(); + int result_count = ik->java_fields_count(); // Allocate the result and fill it in. jfieldID* result_list = (jfieldID*)jvmtiMalloc(result_count * sizeof(jfieldID)); diff --git a/src/hotspot/share/prims/jvmtiTagMap.cpp b/src/hotspot/share/prims/jvmtiTagMap.cpp index 4febb4f3125..a69c7cb7142 100644 --- a/src/hotspot/share/prims/jvmtiTagMap.cpp +++ b/src/hotspot/share/prims/jvmtiTagMap.cpp @@ -36,6 +36,7 @@ #include "oops/access.inline.hpp" #include "oops/arrayOop.hpp" #include "oops/constantPool.inline.hpp" +#include "oops/fieldStreams.inline.hpp" #include "oops/instanceMirrorKlass.hpp" #include "oops/klass.inline.hpp" #include "oops/objArrayKlass.hpp" @@ -58,7 +59,6 @@ #include "runtime/jniHandles.inline.hpp" #include "runtime/mutex.hpp" #include "runtime/mutexLocker.hpp" -#include "runtime/reflectionUtils.hpp" #include "runtime/safepoint.hpp" #include "runtime/threadSMR.hpp" #include "runtime/timerTrace.hpp" @@ -429,8 +429,8 @@ int ClassFieldMap::interfaces_field_count(InstanceKlass* ik) { const Array* interfaces = ik->transitive_interfaces(); int count = 0; for (int i = 0; i < interfaces->length(); i++) { - FilteredJavaFieldStream fld(interfaces->at(i)); - count += fld.field_count(); + count += interfaces->at(i)->java_fields_count(); + } return count; } @@ -452,11 +452,10 @@ ClassFieldMap* ClassFieldMap::create_map_of_static_fields(Klass* k) { // Need to calculate start index of this class fields: number of fields in all interfaces and superclasses. int index = interfaces_field_count(ik); for (InstanceKlass* super_klass = ik->super(); super_klass != nullptr; super_klass = super_klass->super()) { - FilteredJavaFieldStream super_fld(super_klass); - index += super_fld.field_count(); + index += super_klass->java_fields_count(); } - for (FilteredJavaFieldStream fld(ik); !fld.done(); fld.next(), index++) { + for (JavaFieldStream fld(ik); !fld.done(); fld.next(), index++) { // ignore instance fields if (!fld.access_flags().is_static()) { continue; @@ -479,13 +478,12 @@ ClassFieldMap* ClassFieldMap::create_map_of_instance_fields(oop obj) { // fields of the superclasses are reported first, so need to know total field number to calculate field indices int total_field_number = interfaces_field_count(ik); for (InstanceKlass* klass = ik; klass != nullptr; klass = klass->super()) { - FilteredJavaFieldStream fld(klass); - total_field_number += fld.field_count(); + total_field_number += klass->java_fields_count(); } for (InstanceKlass* klass = ik; klass != nullptr; klass = klass->super()) { - FilteredJavaFieldStream fld(klass); - int start_index = total_field_number - fld.field_count(); + JavaFieldStream fld(klass); + int start_index = total_field_number - klass->java_fields_count(); for (int index = 0; !fld.done(); fld.next(), index++) { // ignore static fields if (fld.access_flags().is_static()) { diff --git a/src/hotspot/share/runtime/reflectionUtils.cpp b/src/hotspot/share/runtime/reflectionUtils.cpp deleted file mode 100644 index ba1e167f1d6..00000000000 --- a/src/hotspot/share/runtime/reflectionUtils.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#include "classfile/javaClasses.hpp" -#include "classfile/vmClasses.hpp" -#include "oops/instanceKlass.inline.hpp" -#include "runtime/reflectionUtils.hpp" - - -GrowableArray *FilteredFieldsMap::_filtered_fields = - new (mtServiceability) GrowableArray(3, mtServiceability); - - -void FilteredFieldsMap::initialize() { - int offset = reflect_ConstantPool::oop_offset(); - _filtered_fields->append(new FilteredField(vmClasses::reflect_ConstantPool_klass(), offset)); -} diff --git a/src/hotspot/share/runtime/reflectionUtils.hpp b/src/hotspot/share/runtime/reflectionUtils.hpp deleted file mode 100644 index 5753f46055e..00000000000 --- a/src/hotspot/share/runtime/reflectionUtils.hpp +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef SHARE_RUNTIME_REFLECTIONUTILS_HPP -#define SHARE_RUNTIME_REFLECTIONUTILS_HPP - -#include "memory/allStatic.hpp" -#include "oops/fieldStreams.inline.hpp" -#include "oops/instanceKlass.hpp" -#include "oops/oopsHierarchy.hpp" -#include "runtime/reflection.hpp" -#include "utilities/globalDefinitions.hpp" -#include "utilities/growableArray.hpp" - -class FilteredField : public CHeapObj { - private: - Klass* _klass; - int _field_offset; - - public: - FilteredField(Klass* klass, int field_offset) { - _klass = klass; - _field_offset = field_offset; - } - Klass* klass() { return _klass; } - int field_offset() { return _field_offset; } -}; - -class FilteredFieldsMap : AllStatic { - private: - static GrowableArray *_filtered_fields; - public: - static void initialize(); - static bool is_filtered_field(Klass* klass, int field_offset) { - for (int i=0; i < _filtered_fields->length(); i++) { - if (klass == _filtered_fields->at(i)->klass() && - field_offset == _filtered_fields->at(i)->field_offset()) { - return true; - } - } - return false; - } - static int filtered_fields_count(Klass* klass, bool local_only) { - int nflds = 0; - for (int i=0; i < _filtered_fields->length(); i++) { - if (local_only && klass == _filtered_fields->at(i)->klass()) { - nflds++; - } else if (klass->is_subtype_of(_filtered_fields->at(i)->klass())) { - nflds++; - } - } - return nflds; - } -}; - -// Iterate over Java fields filtering fields like reflection does. -class FilteredJavaFieldStream : public JavaFieldStream { -private: - InstanceKlass* _klass; - int _filtered_fields_count; - bool has_filtered_field() const { return (_filtered_fields_count > 0); } - void skip_filtered_fields() { - if (has_filtered_field()) { - while (!done() && FilteredFieldsMap::is_filtered_field((Klass*)_klass, offset())) { - JavaFieldStream::next(); - } - } - } - -public: - FilteredJavaFieldStream(InstanceKlass* klass) - : JavaFieldStream(klass), - _klass(klass), - _filtered_fields_count(FilteredFieldsMap::filtered_fields_count(klass, true)) - { - // skip filtered fields at the beginning - skip_filtered_fields(); - } - int field_count() const { - return _klass->java_fields_count() - _filtered_fields_count; - } - void next() { - JavaFieldStream::next(); - skip_filtered_fields(); - } -}; - -#endif // SHARE_RUNTIME_REFLECTIONUTILS_HPP diff --git a/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java b/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java index d56ecff5e36..be241a1eca5 100644 --- a/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java +++ b/src/java.base/share/classes/jdk/internal/reflect/ConstantPool.java @@ -106,13 +106,6 @@ public class ConstantPool { // Internals only below this point // - static { - Reflection.registerFieldsToFilter(ConstantPool.class, Set.of("constantPoolOop")); - } - - // HotSpot-internal constant pool object (set by the VM, name known to the VM) - private Object constantPoolOop; - private native int getSize0 (); private native Class getClassAt0 (int index); private native Class getClassAtIfLoaded0 (int index); diff --git a/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java b/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java index 6501d082075..2ea54f2b4ef 100644 --- a/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java +++ b/test/hotspot/jtreg/compiler/jvmci/jdk.vm.ci.runtime.test/src/jdk/vm/ci/runtime/test/TestResolvedJavaType.java @@ -949,9 +949,6 @@ public class TestResolvedJavaType extends TypeUniverse { * Replicates the semantics of jdk.internal.reflect.Reflection#fieldFilterMap. */ private static boolean isHiddenFromReflection(ResolvedJavaField f) { - if (f.getDeclaringClass().equals(metaAccess.lookupJavaType(ConstantPool.class)) && f.getName().equals("constantPoolOop")) { - return true; - } if (f.getDeclaringClass().equals(metaAccess.lookupJavaType(Class.class))) { String name = f.getName(); return name.equals("classLoader") || From e1071797a4f0ab1a6af29824a777a7800d729b0e Mon Sep 17 00:00:00 2001 From: Sergey Bylokhov Date: Tue, 16 Sep 2025 21:51:47 +0000 Subject: [PATCH 067/556] 8367017: Remove legacy checks from WrappedToolkitTest and convert from bash Reviewed-by: prr --- .../WrappedToolkitTest/TestWrapped.java | 71 +++--- .../WrappedToolkitTest/WrappedToolkitTest.sh | 221 ------------------ 2 files changed, 34 insertions(+), 258 deletions(-) delete mode 100644 test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/WrappedToolkitTest.sh diff --git a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.java b/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.java index e2ded58e7f2..4b4041dbcfd 100644 --- a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.java +++ b/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/TestWrapped.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,55 +22,52 @@ */ /* - * test + * @test * @bug 6282388 - * @summary Tests that AWT use correct toolkit to be wrapped into HeadlessToolkit - * @author artem.ananiev@sun.com: area=awt.headless - * @run shell WrappedToolkitTest.sh + * @summary Tests that AWT uses correct toolkit wrapped into HeadlessToolkit + * @modules java.desktop/sun.awt:open + * @library /test/lib + * @run main/othervm -Djava.awt.headless=true TestWrapped */ -import java.awt.*; +import java.awt.Toolkit; +import java.lang.Class; +import java.lang.reflect.Field; -import java.lang.reflect.*; +import jdk.test.lib.Platform; -import sun.awt.*; +public final class TestWrapped { -public class TestWrapped -{ - public static void main(String[] args) - { - try - { - if (args.length != 1) { - System.err.println("No correct toolkit class name is specified, test is not run"); - System.exit(0); + private static final String HEADLESS_TOOLKIT = "sun.awt.HeadlessToolkit"; + private static final String MACOSX_TOOLKIT = "sun.lwawt.macosx.LWCToolkit"; + private static final String UNIX_TOOLKIT = "sun.awt.X11.XToolkit"; + private static final String WINDOWS_TOOLKIT = "sun.awt.windows.WToolkit"; + + public static void main(String[] args) throws Exception { + String expectedToolkitClassName; + if (Platform.isWindows()) { + expectedToolkitClassName = WINDOWS_TOOLKIT; + } else if (Platform.isOSX()) { + expectedToolkitClassName = MACOSX_TOOLKIT; + } else { + expectedToolkitClassName = UNIX_TOOLKIT; } - String correctToolkitClassName = args[0]; Toolkit tk = Toolkit.getDefaultToolkit(); - Class tkClass = tk.getClass(); - if (!tkClass.getName().equals("sun.awt.HeadlessToolkit")) - { - System.err.println(tkClass.getName()); - System.err.println("Error: default toolkit is not an instance of HeadlessToolkit"); - System.exit(-1); + Class tkClass = tk.getClass(); + if (!tkClass.getName().equals(HEADLESS_TOOLKIT)) { + System.err.println("Expected: " + HEADLESS_TOOLKIT); + System.err.println("Actual: " + tkClass.getName()); + throw new RuntimeException("Wrong default toolkit"); } Field f = tkClass.getDeclaredField("tk"); f.setAccessible(true); - Class wrappedClass = f.get(tk).getClass(); - if (!wrappedClass.getName().equals(correctToolkitClassName)) { - System.err.println(wrappedClass.getName()); - System.err.println("Error: wrapped toolkit is not an instance of " + correctToolkitClassName); - System.exit(-1); + Class wrappedClass = f.get(tk).getClass(); + if (!wrappedClass.getName().equals(expectedToolkitClassName)) { + System.err.println("Expected: " + expectedToolkitClassName); + System.err.println("Actual: " + wrappedClass.getName()); + throw new RuntimeException("Wrong wrapped toolkit"); } - } - catch (Exception z) - { - z.printStackTrace(System.err); - System.exit(-1); - } - - System.exit(0); } } diff --git a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/WrappedToolkitTest.sh b/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/WrappedToolkitTest.sh deleted file mode 100644 index ac0cf58942c..00000000000 --- a/test/jdk/java/awt/Toolkit/Headless/WrappedToolkitTest/WrappedToolkitTest.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/bin/ksh -p - -# -# Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# -# @test -# @bug 6282388 8030640 -# @summary Tests that AWT use correct toolkit to be wrapped into HeadlessToolkit -# @author artem.ananiev@sun.com: area=awt.headless -# compile TestWrapped.java -# @run shell WrappedToolkitTest.sh - -# Beginning of subroutines: -status=1 - -#Call this from anywhere to fail the test with an error message -# usage: fail "reason why the test failed" -fail() - { echo "The test failed :-(" - echo "$*" 1>&2 - echo "exit status was $status" - exit $status - } #end of fail() - -#Call this from anywhere to pass the test with a message -# usage: pass "reason why the test passed if applicable" -pass() - { echo "The test passed!!!" - echo "$*" 1>&2 - exit 0 - } #end of pass() - -# end of subroutines - - -# The beginning of the script proper - -# Checking for proper OS -OS=`uname -s` -case "$OS" in - AIX | CYGWIN* | Darwin | Linux ) - FILESEP="/" - ;; - - Windows* ) - FILESEP="\\" - ;; - - # catch all other OSs - * ) - echo "Unrecognized system! $OS" - fail "Unrecognized system! $OS" - ;; -esac - -# check that some executable or other file you need is available, abort if not -# note that the name of the executable is in the fail string as well. -# this is how to check for presence of the compiler, etc. -#RESOURCE=`whence SomeProgramOrFileNeeded` -#if [ "${RESOURCE}" = "" ] ; -# then fail "Need SomeProgramOrFileNeeded to perform the test" ; -#fi - -# Want this test to run standalone as well as in the harness, so do the -# following to copy the test's directory into the harness's scratch directory -# and set all appropriate variables: - -if [ -z "${TESTJAVA}" ] ; then - # TESTJAVA is not set, so the test is running stand-alone. - # TESTJAVA holds the path to the root directory of the build of the JDK - # to be tested. That is, any java files run explicitly in this shell - # should use TESTJAVA in the path to the java interpreter. - # So, we'll set this to the JDK spec'd on the command line. If none - # is given on the command line, tell the user that and use a cheesy - # default. - # THIS IS THE JDK BEING TESTED. - if [ -n "$1" ] ; - then TESTJAVA=$1 - else fail "no JDK specified on command line!" - fi - TESTSRC=. - TESTCLASSES=. - STANDALONE=1; -fi -echo "JDK under test is: $TESTJAVA" - -#if in test harness, then copy the entire directory that the test is in over -# to the scratch directory. This catches any support files needed by the test. -if [ -z "${STANDALONE}" ] ; - then cp ${TESTSRC}/* . -fi -case "$OS" in - Windows* | CYGWIN* ) - ${COMPILEJAVA}/bin/javac ${TESTJAVACOPTS} \ - --add-exports java.desktop/sun.awt=ALL-UNNAMED \ - --add-exports java.desktop/sun.awt.windows=ALL-UNNAMED ${CP} \ - *.java - status=$? - if [ ! $status -eq "0" ]; then - fail "Compilation failed"; - fi - ;; - - AIX | Linux ) - ${COMPILEJAVA}/bin/javac ${TESTJAVACOPTS} \ - --add-exports java.desktop/sun.awt=ALL-UNNAMED \ - --add-exports java.desktop/sun.awt.X11=ALL-UNNAMED ${CP} \ - *.java - status=$? - if [ ! $status -eq "0" ]; then - fail "Compilation failed"; - fi - ;; - - Darwin) - ${COMPILEJAVA}/bin/javac ${TESTJAVACOPTS} \ - --add-exports java.desktop/sun.awt=ALL-UNNAMED \ - --add-exports java.desktop/sun.lwawt.macosx=ALL-UNNAMED ${CP} \ - *.java - status=$? - if [ ! $status -eq "0" ]; then - fail "Compilation failed"; - fi - ;; - -esac - -#Just before executing anything, make sure it has executable permission! -chmod 777 ./* - -############### YOUR TEST CODE HERE!!!!!!! ############# - -case "$OS" in - Windows* | CYGWIN* ) - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.windows=ALL-UNNAMED ${CP} \ - TestWrapped sun.awt.windows.WToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.windows.WToolkit"; - fi - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.windows=ALL-UNNAMED ${CP} \ - -Dawt.toolkit=sun.awt.windows.WToolkit \ - TestWrapped sun.awt.windows.WToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.windows.WToolkit"; - fi - ;; - - AIX | Linux ) - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.X11=ALL-UNNAMED ${CP} \ - -Dawt.toolkit=sun.awt.X11.XToolkit \ - TestWrapped sun.awt.X11.XToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.xawt.XToolkit"; - fi - AWT_TOOLKIT=XToolkit ${TESTJAVA}/bin/java ${TESTVMOPTS} \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.awt.X11=ALL-UNNAMED ${CP} \ - -Djava.awt.headless=true \ - TestWrapped sun.awt.X11.XToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.awt.xawt.XToolkit"; - fi - ;; - - Darwin) - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED ${CP} \ - TestWrapped sun.lwawt.macosx.LWCToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.lwawt.macosx.LWCToolkit"; - fi - ${TESTJAVA}/bin/java ${TESTVMOPTS} -Djava.awt.headless=true \ - --add-opens java.desktop/sun.awt=ALL-UNNAMED \ - --add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED ${CP} \ - -Dawt.toolkit=sun.lwawt.macosx.LWCToolkit \ - TestWrapped sun.lwawt.macosx.LWCToolkit - status=$? - if [ ! $status -eq "0" ]; then - fail "Test FAILED: toolkit wrapped into HeadlessToolkit is not an instance of sun.lwawt.macosx.LWCToolkit"; - fi - ;; - -esac - -pass "All the tests are PASSED"; - -#For additional examples of how to write platform independent KSH scripts, -# see the jtreg file itself. It is a KSH script for both Solaris and Win32 From c2c44a061a6ba392b4e93eca2c85bd96ab7dcffe Mon Sep 17 00:00:00 2001 From: Kim Barrett Date: Wed, 17 Sep 2025 05:51:51 +0000 Subject: [PATCH 068/556] 8367724: Remove Trailing Return Types from undecided list Reviewed-by: stefank, phubner --- doc/hotspot-style.html | 2 -- doc/hotspot-style.md | 3 --- 2 files changed, 5 deletions(-) diff --git a/doc/hotspot-style.html b/doc/hotspot-style.html index 7be6867b3ca..f1c25dab7f4 100644 --- a/doc/hotspot-style.html +++ b/doc/hotspot-style.html @@ -1859,8 +1859,6 @@ difference.

      Additional Undecided Features

        -
      • Trailing return type syntax for functions (n2541)

      • Member initializers and aggregates (n3653)

      • Rvalue references and move semantics

      • diff --git a/doc/hotspot-style.md b/doc/hotspot-style.md index facdf68462f..e49f49ec1c9 100644 --- a/doc/hotspot-style.md +++ b/doc/hotspot-style.md @@ -1853,9 +1853,6 @@ See Object Lifetime: C++17 6.8/8, C++20 6.7.3/8 ### Additional Undecided Features -* Trailing return type syntax for functions -([n2541](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2541.htm)) - * Member initializers and aggregates ([n3653](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3653.html)) From 45cc515f451accfd1a0a36d17ccb38d428a5d035 Mon Sep 17 00:00:00 2001 From: erifan Date: Wed, 17 Sep 2025 07:32:19 +0000 Subject: [PATCH 069/556] 8354242: VectorAPI: combine vector not operation with compare Reviewed-by: epeter, jbhateja, xgong --- src/hotspot/share/opto/node.cpp | 3 + src/hotspot/share/opto/subnode.hpp | 4 +- src/hotspot/share/opto/vectornode.cpp | 98 ++ src/hotspot/share/opto/vectornode.hpp | 2 + .../compiler/lib/ir_framework/IRNode.java | 10 + .../vectorapi/VectorMaskCompareNotTest.java | 1299 +++++++++++++++++ .../vector/MaskCompareNotBenchmark.java | 220 +++ 7 files changed, 1635 insertions(+), 1 deletion(-) create mode 100644 test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java create mode 100644 test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java diff --git a/src/hotspot/share/opto/node.cpp b/src/hotspot/share/opto/node.cpp index 5ecc038954d..f5f7a4231f2 100644 --- a/src/hotspot/share/opto/node.cpp +++ b/src/hotspot/share/opto/node.cpp @@ -1216,6 +1216,9 @@ bool Node::has_special_unique_user() const { } else if ((is_IfFalse() || is_IfTrue()) && n->is_If()) { // See IfNode::fold_compares return true; + } else if (n->Opcode() == Op_XorV || n->Opcode() == Op_XorVMask) { + // Condition for XorVMask(VectorMaskCmp(x,y,cond), MaskAll(true)) ==> VectorMaskCmp(x,y,ncond) + return true; } else { return false; } diff --git a/src/hotspot/share/opto/subnode.hpp b/src/hotspot/share/opto/subnode.hpp index 57a501ecbc3..5acf31b45c4 100644 --- a/src/hotspot/share/opto/subnode.hpp +++ b/src/hotspot/share/opto/subnode.hpp @@ -328,7 +328,9 @@ struct BoolTest { // a simple char array where each element is the ASCII version of a 'mask' // enum from above. mask commute( ) const { return mask("032147658"[_test]-'0'); } - mask negate( ) const { return mask(_test^4); } + mask negate( ) const { return negate_mask(_test); } + // Return the negative mask for the given mask, for both signed and unsigned comparison. + static mask negate_mask(mask btm) { return mask(btm ^ 4); } bool is_canonical( ) const { return (_test == BoolTest::ne || _test == BoolTest::lt || _test == BoolTest::le || _test == BoolTest::overflow); } bool is_less( ) const { return _test == BoolTest::lt || _test == BoolTest::le; } bool is_greater( ) const { return _test == BoolTest::gt || _test == BoolTest::ge; } diff --git a/src/hotspot/share/opto/vectornode.cpp b/src/hotspot/share/opto/vectornode.cpp index 2153a12c402..ae9ef552df4 100644 --- a/src/hotspot/share/opto/vectornode.cpp +++ b/src/hotspot/share/opto/vectornode.cpp @@ -2268,6 +2268,99 @@ Node* OrVNode::Identity(PhaseGVN* phase) { return redundant_logical_identity(this); } +// Returns whether (XorV (VectorMaskCmp) -1) can be optimized by negating the +// comparison operation. +bool VectorMaskCmpNode::predicate_can_be_negated() { + switch (_predicate) { + case BoolTest::eq: + case BoolTest::ne: + // eq and ne also apply to floating-point special values like NaN and infinities. + return true; + case BoolTest::le: + case BoolTest::ge: + case BoolTest::lt: + case BoolTest::gt: + case BoolTest::ule: + case BoolTest::uge: + case BoolTest::ult: + case BoolTest::ugt: { + BasicType bt = vect_type()->element_basic_type(); + // For float and double, we don't know if either comparison operand is a + // NaN, NaN {le|ge|lt|gt} anything is false, resulting in inconsistent + // results before and after negation. + return is_integral_type(bt); + } + default: + return false; + } +} + +// This function transforms the following patterns: +// +// For integer types: +// (XorV (VectorMaskCmp src1 src2 cond) (Replicate -1)) +// => (VectorMaskCmp src1 src2 ncond) +// (XorVMask (VectorMaskCmp src1 src2 cond) (MaskAll m1)) +// => (VectorMaskCmp src1 src2 ncond) +// (XorV (VectorMaskCast (VectorMaskCmp src1 src2 cond)) (Replicate -1)) +// => (VectorMaskCast (VectorMaskCmp src1 src2 ncond)) +// (XorVMask (VectorMaskCast (VectorMaskCmp src1 src2 cond)) (MaskAll m1)) +// => (VectorMaskCast (VectorMaskCmp src1 src2 ncond)) +// cond can be eq, ne, le, ge, lt, gt, ule, uge, ult and ugt. +// ncond is the negative comparison of cond. +// +// For float and double types: +// (XorV (VectorMaskCast (VectorMaskCmp src1 src2 cond)) (Replicate -1)) +// => (VectorMaskCast (VectorMaskCmp src1 src2 ncond)) +// (XorVMask (VectorMaskCast (VectorMaskCmp src1 src2 cond)) (MaskAll m1)) +// => (VectorMaskCast (VectorMaskCmp src1 src2 ncond)) +// cond can be eq or ne. +Node* XorVNode::Ideal_XorV_VectorMaskCmp(PhaseGVN* phase, bool can_reshape) { + Node* in1 = in(1); + Node* in2 = in(2); + // Transformations for predicated vectors are not supported for now. + if (is_predicated_vector() || + in1->is_predicated_vector() || + in2->is_predicated_vector()) { + return nullptr; + } + + // XorV/XorVMask is commutative, swap VectorMaskCmp/VectorMaskCast to in1. + if (VectorNode::is_all_ones_vector(in1)) { + swap(in1, in2); + } + + bool with_vector_mask_cast = false; + // Required conditions: + // 1. VectorMaskCast and VectorMaskCmp should only have a single use, + // otherwise the optimization may be unprofitable. + // 2. The predicate of VectorMaskCmp should be negatable. + // 3. The second input should be an all true vector mask. + if (in1->Opcode() == Op_VectorMaskCast) { + if (in1->outcnt() != 1) { + return nullptr; + } + with_vector_mask_cast = true; + in1 = in1->in(1); + } + if (in1->Opcode() != Op_VectorMaskCmp || + in1->outcnt() != 1 || + !in1->as_VectorMaskCmp()->predicate_can_be_negated() || + !VectorNode::is_all_ones_vector(in2)) { + return nullptr; + } + + BoolTest::mask neg_cond = BoolTest::negate_mask((in1->as_VectorMaskCmp())->get_predicate()); + ConINode* predicate_node = phase->intcon(neg_cond); + const TypeVect* vt = in1->as_Vector()->vect_type(); + Node* res = new VectorMaskCmpNode(neg_cond, in1->in(1), in1->in(2), predicate_node, vt); + if (with_vector_mask_cast) { + // We optimized out a VectorMaskCast, regenerate one to ensure type correctness. + res = new VectorMaskCastNode(phase->transform(res), vect_type()); + } + return res; +} + Node* XorVNode::Ideal(PhaseGVN* phase, bool can_reshape) { // (XorV src src) => (Replicate zero) // (XorVMask src src) => (MaskAll zero) @@ -2281,6 +2374,11 @@ Node* XorVNode::Ideal(PhaseGVN* phase, bool can_reshape) { Node* zero = phase->transform(phase->zerocon(bt)); return VectorNode::scalar2vector(zero, length(), bt, bottom_type()->isa_vectmask() != nullptr); } + + Node* res = Ideal_XorV_VectorMaskCmp(phase, can_reshape); + if (res != nullptr) { + return res; + } return VectorNode::Ideal(phase, can_reshape); } diff --git a/src/hotspot/share/opto/vectornode.hpp b/src/hotspot/share/opto/vectornode.hpp index 463680d0a52..53778b61d0e 100644 --- a/src/hotspot/share/opto/vectornode.hpp +++ b/src/hotspot/share/opto/vectornode.hpp @@ -1013,6 +1013,7 @@ class XorVNode : public VectorNode { XorVNode(Node* in1, Node* in2, const TypeVect* vt) : VectorNode(in1,in2,vt) {} virtual int Opcode() const; virtual Node* Ideal(PhaseGVN* phase, bool can_reshape); + Node* Ideal_XorV_VectorMaskCmp(PhaseGVN* phase, bool can_reshape); }; //------------------------------XorReductionVNode-------------------------------------- @@ -1676,6 +1677,7 @@ class VectorMaskCmpNode : public VectorNode { virtual bool cmp( const Node &n ) const { return VectorNode::cmp(n) && _predicate == ((VectorMaskCmpNode&)n)._predicate; } + bool predicate_can_be_negated(); BoolTest::mask get_predicate() { return _predicate; } #ifndef PRODUCT virtual void dump_spec(outputStream *st) const; diff --git a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java index 52fc9a05f98..bb69e5bfe80 100644 --- a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java +++ b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java @@ -2295,6 +2295,11 @@ public class IRNode { vectorNode(VECTOR_MASK_CMP_D, "VectorMaskCmp", TYPE_DOUBLE); } + public static final String VECTOR_MASK_CMP = PREFIX + "VECTOR_MASK_CMP" + POSTFIX; + static { + beforeMatchingNameRegex(VECTOR_MASK_CMP, "VectorMaskCmp"); + } + public static final String VECTOR_CAST_B2S = VECTOR_PREFIX + "VECTOR_CAST_B2S" + POSTFIX; static { vectorNode(VECTOR_CAST_B2S, "VectorCastB2X", TYPE_SHORT); @@ -2705,6 +2710,11 @@ public class IRNode { vectorNode(XOR_VL, "XorV", TYPE_LONG); } + public static final String XOR_V = PREFIX + "XOR_V" + POSTFIX; + static { + beforeMatchingNameRegex(XOR_V, "XorV"); + } + public static final String XOR_V_MASK = PREFIX + "XOR_V_MASK" + POSTFIX; static { beforeMatchingNameRegex(XOR_V_MASK, "XorVMask"); diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java new file mode 100644 index 00000000000..851113ea4de --- /dev/null +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java @@ -0,0 +1,1299 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package compiler.vectorapi; + +import compiler.lib.generators.*; +import compiler.lib.ir_framework.*; +import jdk.incubator.vector.*; +import jdk.test.lib.Asserts; + +/* + * @test + * @bug 8354242 + * @key randomness + * @library /test/lib / + * @summary test combining vector not operation with compare + * @modules jdk.incubator.vector + * + * @run driver compiler.vectorapi.VectorMaskCompareNotTest + */ + +public class VectorMaskCompareNotTest { + private static int LENGTH = 128; + + private static final VectorSpecies B_SPECIES = VectorSpecies.ofLargestShape(byte.class); + private static final VectorSpecies S_SPECIES = VectorSpecies.ofLargestShape(short.class); + private static final VectorSpecies I_SPECIES = VectorSpecies.ofLargestShape(int.class); + private static final VectorSpecies L_SPECIES = VectorSpecies.ofLargestShape(long.class); + private static final VectorSpecies F_SPECIES = VectorSpecies.ofLargestShape(float.class); + private static final VectorSpecies D_SPECIES = VectorSpecies.ofLargestShape(double.class); + + // Vector species for vector mask cast operation between int and long types, + // they must have the same number of elements. + // For other types, use a vector species of the specified width. + private static final VectorSpecies L_SPECIES_FOR_CAST = VectorSpecies.ofLargestShape(long.class); + private static final VectorSpecies I_SPECIES_FOR_CAST = VectorSpecies.of(int.class, VectorShape.forBitSize(L_SPECIES_FOR_CAST.vectorBitSize() / 2)); + + private static final Generators RD = Generators.G; + + private static byte[] ba; + private static byte[] bb; + private static short[] sa; + private static short[] sb; + private static int[] ia; + private static int[] ib; + private static int[] ic; + private static long[] la; + private static long[] lb; + private static float[] fa; + private static float[] fb; + private static float[] fnan; + private static float[] fpinf; + private static float[] fninf; + private static double[] da; + private static double[] db; + private static double[] dnan; + private static double[] dpinf; + private static double[] dninf; + private static boolean[] mr; + + static { + ba = new byte[LENGTH]; + bb = new byte[LENGTH]; + sa = new short[LENGTH]; + sb = new short[LENGTH]; + ia = new int[LENGTH]; + ib = new int[LENGTH]; + ic = new int[LENGTH]; + la = new long[LENGTH]; + lb = new long[LENGTH]; + fa = new float[LENGTH]; + fb = new float[LENGTH]; + fnan = new float[LENGTH]; + fpinf = new float[LENGTH]; + fninf = new float[LENGTH]; + da = new double[LENGTH]; + db = new double[LENGTH]; + dnan = new double[LENGTH]; + dpinf = new double[LENGTH]; + dninf = new double[LENGTH]; + mr = new boolean[LENGTH]; + + Generator iGen = RD.ints(); + Generator lGen = RD.longs(); + // Use uniform generators for floating point numbers not to generate NaN values. + Generator fGen = RD.uniformFloats(Float.MIN_VALUE, Float.MAX_VALUE); + Generator dGen = RD.uniformDoubles(Double.MIN_VALUE, Double.MAX_VALUE); + for (int i = 0; i < LENGTH; i++) { + ba[i] = iGen.next().byteValue(); + bb[i] = iGen.next().byteValue(); + sa[i] = iGen.next().shortValue(); + sb[i] = iGen.next().shortValue(); + ia[i] = iGen.next(); + ib[i] = iGen.next(); + la[i] = lGen.next(); + lb[i] = lGen.next(); + fa[i] = fGen.next(); + fb[i] = fGen.next(); + fnan[i] = Float.NaN; + fpinf[i] = Float.POSITIVE_INFINITY; + fninf[i] = Float.NEGATIVE_INFINITY; + da[i] = dGen.next(); + db[i] = dGen.next(); + dnan[i] = Double.NaN; + dpinf[i] = Double.POSITIVE_INFINITY; + dninf[i] = Double.NEGATIVE_INFINITY; + } + } + + public static int compareUnsigned(Number a, Number b) { + if (a instanceof Byte) { + return Integer.compareUnsigned(Byte.toUnsignedInt(a.byteValue()), Byte.toUnsignedInt(b.byteValue())); + } else if (a instanceof Short) { + return Integer.compareUnsigned(Short.toUnsignedInt(a.shortValue()), Short.toUnsignedInt(b.shortValue())); + } else if (a instanceof Integer) { + return Integer.compareUnsigned(a.intValue(), b.intValue()); + } else if (a instanceof Long) { + return Long.compareUnsigned(a.longValue(), b.longValue()); + } else { + throw new IllegalArgumentException("Unsupported type for unsigned comparison: " + a.getClass() + ", " + b.getClass()); + } + } + + public static > void compareResults(T a, T b, boolean r, VectorOperators.Comparison op) { + if (op == VectorOperators.EQ) { + // For floating point numbers, a is not NaN, b may be NaN. If b is NaN, + // a.compareTo(b) will return 1, 1 != 0 is true, r is expected to be true. + Asserts.assertEquals(a.compareTo(b) != 0, r); + } else if (op == VectorOperators.NE) { + // For floating point numbers, a is not NaN, b may be NaN. If b is NaN, + // a.compareTo(b) will return 1, 1 == 0 is false, r is expected to be false. + Asserts.assertEquals(a.compareTo(b) == 0, r); + } else if (op == VectorOperators.LE) { + Asserts.assertEquals(a.compareTo(b) > 0, r); + } else if (op == VectorOperators.GE) { + Asserts.assertEquals(a.compareTo(b) < 0, r); + } else if (op == VectorOperators.LT) { + Asserts.assertEquals(a.compareTo(b) >= 0, r); + } else if (op == VectorOperators.GT) { + Asserts.assertEquals(a.compareTo(b) <= 0, r); + } else if (op == VectorOperators.ULE) { + Asserts.assertEquals(compareUnsigned(a, b) > 0, r); + } else if (op == VectorOperators.UGE) { + Asserts.assertEquals(compareUnsigned(a, b) < 0, r); + } else if (op == VectorOperators.ULT) { + Asserts.assertEquals(compareUnsigned(a, b) >= 0, r); + } else if (op == VectorOperators.UGT) { + Asserts.assertEquals(compareUnsigned(a, b) <= 0, r); + } else { + throw new IllegalArgumentException("Unknown comparison operator: " + op); + } + } + + @DontInline + public static void verifyResultsByte(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(ba[i], bb[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsShort(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(sa[i], sb[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsInt(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(ia[i], ib[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsLong(VectorSpecies vs, VectorOperators.Comparison op) { + for (int i = 0; i < vs.length(); i++) { + compareResults(la[i], lb[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsFloat(VectorSpecies vs, VectorOperators.Comparison op, float[] a, float[] b) { + for (int i = 0; i < vs.length(); i++) { + compareResults(a[i], b[i], mr[i], op); + } + } + + @DontInline + public static void verifyResultsDouble(VectorSpecies vs, VectorOperators.Comparison op, double[] a, double[] b) { + for (int i = 0; i < vs.length(); i++) { + compareResults(a[i], b[i], mr[i], op); + } + } + + interface VectorMaskOperator { + public VectorMask apply(VectorMask m); + } + + @ForceInline + public static void testCompareMaskNotByte(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + ByteVector av = ByteVector.fromArray(vs, ba, 0); + ByteVector bv = ByteVector.fromArray(vs, bb, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotShort(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + ShortVector av = ShortVector.fromArray(vs, sa, 0); + ShortVector bv = ShortVector.fromArray(vs, sb, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotInt(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + IntVector av = IntVector.fromArray(vs, ia, 0); + IntVector bv = IntVector.fromArray(vs, ib, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotLong(VectorSpecies vs, VectorOperators.Comparison op, VectorMaskOperator func) { + LongVector av = LongVector.fromArray(vs, la, 0); + LongVector bv = LongVector.fromArray(vs, lb, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotFloat(VectorSpecies vs, VectorOperators.Comparison op, float[] a, float[] b, VectorMaskOperator func) { + FloatVector av = FloatVector.fromArray(vs, a, 0); + FloatVector bv = FloatVector.fromArray(vs, b, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + @ForceInline + public static void testCompareMaskNotDouble(VectorSpecies vs, VectorOperators.Comparison op, double[] a, double[] b, VectorMaskOperator func) { + DoubleVector av = DoubleVector.fromArray(vs, a, 0); + DoubleVector bv = DoubleVector.fromArray(vs, b, 0); + VectorMask m = av.compare(op, bv); + func.apply(m).intoArray(mr, 0); + } + + // Byte tests + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.EQ); + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.EQ); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.EQ, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.NE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.NE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.NE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.NE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.LT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.LT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.LT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.LT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.GT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.GT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.GT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.GT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.LE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.LE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.LE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.LE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.GE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.GE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.GE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.GE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.ULT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGT); + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGT, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGT); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.UGT, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.ULE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.ULE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.ULE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotByte() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGE); + testCompareMaskNotByte(B_SPECIES, VectorOperators.UGE, (m) -> { return B_SPECIES.maskAll(true).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.UGE); + + testCompareMaskNotByte(ByteVector.SPECIES_64, VectorOperators.UGE, (m) -> { return m.cast(ShortVector.SPECIES_128).not(); }); + verifyResultsByte(ByteVector.SPECIES_64, VectorOperators.UGE); + } + + // Short tests + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.EQ); + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.EQ); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.EQ, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.EQ); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.EQ, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.NE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.NE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.NE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.NE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.NE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.NE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.LT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.LT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.LT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.LT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.LT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.LT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.GT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.GT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.GT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.GT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.GT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.GT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareLEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.LE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.LE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.LE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.LE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.LE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.LE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareGEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.GE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.GE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.GE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.GE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.GE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.GE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.ULT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.ULT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.ULT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGT); + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGT, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGT); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.UGT, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.UGT); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.UGT, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareULEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.ULE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.ULE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.ULE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.ULE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.ULE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotShort() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGE); + testCompareMaskNotShort(S_SPECIES, VectorOperators.UGE, (m) -> { return S_SPECIES.maskAll(true).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.UGE); + + testCompareMaskNotShort(ShortVector.SPECIES_64, VectorOperators.UGE, (m) -> { return IntVector.SPECIES_128.maskAll(true).xor(m.cast(IntVector.SPECIES_128)); }); + verifyResultsShort(ShortVector.SPECIES_64, VectorOperators.UGE); + testCompareMaskNotShort(ShortVector.SPECIES_128, VectorOperators.UGE, (m) -> { return m.cast(ByteVector.SPECIES_64).not(); }); + verifyResultsShort(ShortVector.SPECIES_128, VectorOperators.UGE); + } + + // Int tests + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.EQ); + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.EQ); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.EQ, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.EQ); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.EQ, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.NE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.NE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.NE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.NE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.NE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.NE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.LT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.LT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.LT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.LT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.LT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.LT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.GT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.GT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.GT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.GT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.GT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.GT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.LE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.LE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.LE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.LE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.LE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.LE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.GE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.GE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.GE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.GE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.GE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.GE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.ULT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.ULT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.ULT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGT); + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGT, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGT); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.UGT, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.UGT); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.UGT, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.ULE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.ULE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.ULE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.ULE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.ULE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 2", + IRNode.VECTOR_MASK_CMP, "= 4" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotInt() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGE); + testCompareMaskNotInt(I_SPECIES, VectorOperators.UGE, (m) -> { return I_SPECIES.maskAll(true).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.UGE); + + testCompareMaskNotInt(I_SPECIES_FOR_CAST, VectorOperators.UGE, (m) -> { return L_SPECIES_FOR_CAST.maskAll(true).xor(m.cast(L_SPECIES_FOR_CAST)); }); + verifyResultsInt(I_SPECIES_FOR_CAST, VectorOperators.UGE); + testCompareMaskNotInt(IntVector.SPECIES_128, VectorOperators.UGE, (m) -> { return m.cast(ShortVector.SPECIES_64).not(); }); + verifyResultsInt(IntVector.SPECIES_128, VectorOperators.UGE); + } + + // Long tests + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.EQ); + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.EQ); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.EQ, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.EQ); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.NE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.NE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.NE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.NE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.NE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.LT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.LT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.LT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.LT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.LT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.LT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.GT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.GT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.GT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.GT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.GT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.GT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareLEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.LE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.LE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.LE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.LE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.LE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.LE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareGEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.GE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.GE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.GE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.GE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.GE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.GE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.ULT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.ULT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGTMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGT, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGT); + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGT, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGT); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.UGT, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.UGT); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareULEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.ULE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.ULE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.ULE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.ULE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CAST, "= 1", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareUGEMaskNotLong() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGE, (m) -> { return m.not(); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGE); + testCompareMaskNotLong(L_SPECIES, VectorOperators.UGE, (m) -> { return L_SPECIES.maskAll(true).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.UGE); + + testCompareMaskNotLong(L_SPECIES_FOR_CAST, VectorOperators.UGE, (m) -> { return m.cast(I_SPECIES_FOR_CAST).not(); }); + verifyResultsLong(L_SPECIES_FOR_CAST, VectorOperators.UGE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloat() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fb); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fb); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloat() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fb, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fb); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fb, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fb); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloatNaN() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fnan, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fnan); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fnan, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloatNaN() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fnan, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fnan); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fnan, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloatPositiveInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloatPositiveInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fpinf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fpinf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fpinf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareEQMaskNotFloatNegativeInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fninf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fninf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fninf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fninf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx", "true", "rvv", "true" }) + public static void testCompareNEMaskNotFloatNegativeInfinity() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fninf, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fninf); + testCompareMaskNotFloat(F_SPECIES, VectorOperators.NE, fa, fninf, (m) -> { return F_SPECIES.maskAll(true).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fninf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDouble() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, db); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, db); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDouble() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, db, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, db); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, db, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, db); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDoubleNaN() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dnan, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dnan); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dnan, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDoubleNaN() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dnan, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dnan); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dnan, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dnan); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDoublePositiveInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dpinf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dpinf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dpinf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDoublePositiveInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dpinf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dpinf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dpinf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dpinf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareEQMaskNotDoubleNegativeInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dninf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dninf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, dninf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, dninf); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 0", + IRNode.XOR_V, "= 0", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "asimd", "true", "avx2", "true", "rvv", "true" }) + public static void testCompareNEMaskNotDoubleNegativeInfinity() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dninf, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dninf); + testCompareMaskNotDouble(D_SPECIES, VectorOperators.NE, da, dninf, (m) -> { return D_SPECIES.maskAll(true).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, dninf); + } + + // negative tests + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotByteNegative() { + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsByte(B_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotByte(B_SPECIES, VectorOperators.EQ, (m) -> { return B_SPECIES.maskAll(false).xor(m); }); + verifyResultsByte(B_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotShortNegative() { + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsShort(S_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotShort(S_SPECIES, VectorOperators.EQ, (m) -> { return S_SPECIES.maskAll(false).xor(m); }); + verifyResultsShort(S_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotIntNegative() { + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsInt(I_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotInt(I_SPECIES, VectorOperators.EQ, (m) -> { return I_SPECIES.maskAll(false).xor(m); }); + verifyResultsInt(I_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 2", + IRNode.VECTOR_MASK_CMP, "= 2" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotLongNegative() { + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsLong(L_SPECIES, VectorOperators.EQ); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotLong(L_SPECIES, VectorOperators.EQ, (m) -> { return L_SPECIES.maskAll(false).xor(m); }); + verifyResultsLong(L_SPECIES, VectorOperators.NE); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotFloatNegative() { + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsFloat(F_SPECIES, VectorOperators.EQ, fa, fb); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotFloat(F_SPECIES, VectorOperators.EQ, fa, fb, (m) -> { return F_SPECIES.maskAll(false).xor(m); }); + verifyResultsFloat(F_SPECIES, VectorOperators.NE, fa, fb); + + // Float vectors use the LT comparison. + testCompareMaskNotFloat(F_SPECIES, VectorOperators.LT, fa, fb, (m) -> { return m.not(); }); + verifyResultsFloat(F_SPECIES, VectorOperators.LT, fa, fb); + } + + @Test + @IR(counts = { IRNode.XOR_V_MASK, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureOr = { "sve", "true", "avx512", "true", "rvv", "true" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) + @IR(counts = { IRNode.XOR_V, "= 3", + IRNode.VECTOR_MASK_CMP, "= 3" }, + applyIfCPUFeatureAnd = { "avx2", "true" }) + public static void testCompareMaskNotDoubleNegative() { + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { + // The vector mask is used multiple times. + ic[0] = m.trueCount(); + return m.not(); + }); + verifyResultsDouble(D_SPECIES, VectorOperators.EQ, da, db); + + // One of the operands of XOR is not all ones vector. + testCompareMaskNotDouble(D_SPECIES, VectorOperators.EQ, da, db, (m) -> { return D_SPECIES.maskAll(false).xor(m); }); + verifyResultsDouble(D_SPECIES, VectorOperators.NE, da, db); + + // Double vectors use the LT comparison. + testCompareMaskNotDouble(D_SPECIES, VectorOperators.LT, da, db, (m) -> { return m.not(); }); + verifyResultsDouble(D_SPECIES, VectorOperators.LT, da, db); + } + + public static void main(String[] args) { + TestFramework testFramework = new TestFramework(); + testFramework.setDefaultWarmup(5000) + .addFlags("--add-modules=jdk.incubator.vector") + .start(); + } +} diff --git a/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java b/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java new file mode 100644 index 00000000000..d83bc126a1d --- /dev/null +++ b/test/micro/org/openjdk/bench/jdk/incubator/vector/MaskCompareNotBenchmark.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.openjdk.bench.jdk.incubator.vector; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.*; + +import jdk.incubator.vector.*; +import java.lang.invoke.*; +import java.util.concurrent.TimeUnit; +import java.util.Random; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 2, jvmArgs = { "--add-modules=jdk.incubator.vector" }) +public abstract class MaskCompareNotBenchmark { + @Param({"4096"}) + protected int ARRAYLEN; + + // Abstract method to get comparison operator from subclasses + protected abstract String getComparisonOperatorName(); + + // To get compile-time constants for comparison operation + static final MutableCallSite MUTABLE_COMPARISON_CONSTANT = new MutableCallSite(MethodType.methodType(VectorOperators.Comparison.class)); + static final MethodHandle MUTABLE_COMPARISON_CONSTANT_HANDLE = MUTABLE_COMPARISON_CONSTANT.dynamicInvoker(); + + private static Random r = new Random(); + + protected static final VectorSpecies B_SPECIES = ByteVector.SPECIES_MAX; + protected static final VectorSpecies S_SPECIES = ShortVector.SPECIES_MAX; + protected static final VectorSpecies I_SPECIES = IntVector.SPECIES_MAX; + protected static final VectorSpecies L_SPECIES = LongVector.SPECIES_MAX; + protected static final VectorSpecies F_SPECIES = FloatVector.SPECIES_MAX; + protected static final VectorSpecies D_SPECIES = DoubleVector.SPECIES_MAX; + + protected boolean[] mr; + protected byte[] ba; + protected byte[] bb; + protected short[] sa; + protected short[] sb; + protected int[] ia; + protected int[] ib; + protected long[] la; + protected long[] lb; + protected float[] fa; + protected float[] fb; + protected double[] da; + protected double[] db; + + @Setup + public void init() throws Throwable { + mr = new boolean[ARRAYLEN]; + ba = new byte[ARRAYLEN]; + bb = new byte[ARRAYLEN]; + sa = new short[ARRAYLEN]; + sb = new short[ARRAYLEN]; + ia = new int[ARRAYLEN]; + ib = new int[ARRAYLEN]; + la = new long[ARRAYLEN]; + lb = new long[ARRAYLEN]; + fa = new float[ARRAYLEN]; + fb = new float[ARRAYLEN]; + da = new double[ARRAYLEN]; + db = new double[ARRAYLEN]; + + for (int i = 0; i < ARRAYLEN; i++) { + mr[i] = r.nextBoolean(); + ba[i] = (byte) r.nextInt(); + bb[i] = (byte) r.nextInt(); + sa[i] = (short) r.nextInt(); + sb[i] = (short) r.nextInt(); + ia[i] = r.nextInt(); + ib[i] = r.nextInt(); + la[i] = r.nextLong(); + lb[i] = r.nextLong(); + fa[i] = r.nextFloat(); + fb[i] = r.nextFloat(); + da[i] = r.nextDouble(); + db[i] = r.nextDouble(); + } + + VectorOperators.Comparison comparisonOp = getComparisonOperator(getComparisonOperatorName()); + MethodHandle constant = MethodHandles.constant(VectorOperators.Comparison.class, comparisonOp); + MUTABLE_COMPARISON_CONSTANT.setTarget(constant); + } + + @CompilerControl(CompilerControl.Mode.INLINE) + private static VectorOperators.Comparison getComparisonOperator(String op) { + switch (op) { + case "EQ": return VectorOperators.EQ; + case "NE": return VectorOperators.NE; + case "LT": return VectorOperators.LT; + case "LE": return VectorOperators.LE; + case "GT": return VectorOperators.GT; + case "GE": return VectorOperators.GE; + case "ULT": return VectorOperators.ULT; + case "ULE": return VectorOperators.ULE; + case "UGT": return VectorOperators.UGT; + case "UGE": return VectorOperators.UGE; + default: throw new IllegalArgumentException("Unknown comparison operator: " + op); + } + } + + @CompilerControl(CompilerControl.Mode.INLINE) + protected VectorOperators.Comparison comparison_con() throws Throwable { + return (VectorOperators.Comparison) MUTABLE_COMPARISON_CONSTANT_HANDLE.invokeExact(); + } + + // Subclasses with different comparison operators + public static class IntegerComparisons extends MaskCompareNotBenchmark { + @Param({"EQ", "NE", "LT", "LE", "GT", "GE", "ULT", "ULE", "UGT", "UGE"}) + public String COMPARISON_OP; + + @Override + protected String getComparisonOperatorName() { + return COMPARISON_OP; + } + + @Benchmark + public void testCompareMaskNotByte() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + ByteVector bv = ByteVector.fromArray(B_SPECIES, bb, 0); + for (int j = 0; j < ARRAYLEN; j += B_SPECIES.length()) { + ByteVector av = ByteVector.fromArray(B_SPECIES, ba, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotShort() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + ShortVector bv = ShortVector.fromArray(S_SPECIES, sb, 0); + for (int j = 0; j < ARRAYLEN; j += S_SPECIES.length()) { + ShortVector av = ShortVector.fromArray(S_SPECIES, sa, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotInt() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + IntVector bv = IntVector.fromArray(I_SPECIES, ib, 0); + for (int j = 0; j < ARRAYLEN; j += I_SPECIES.length()) { + IntVector av = IntVector.fromArray(I_SPECIES, ia, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotLong() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + LongVector bv = LongVector.fromArray(L_SPECIES, lb, 0); + for (int j = 0; j < ARRAYLEN; j += L_SPECIES.length()) { + LongVector av = LongVector.fromArray(L_SPECIES, la, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + } + + public static class FloatingPointComparisons extends MaskCompareNotBenchmark { + // "ULT", "ULE", "UGT", "UGE" are not supported for floating point types + @Param({"EQ", "NE", "LT", "LE", "GT", "GE"}) + public String COMPARISON_OP; + + @Override + protected String getComparisonOperatorName() { + return COMPARISON_OP; + } + + @Benchmark + public void testCompareMaskNotFloat() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + FloatVector bv = FloatVector.fromArray(F_SPECIES, fb, 0); + for (int j = 0; j < ARRAYLEN; j += F_SPECIES.length()) { + FloatVector av = FloatVector.fromArray(F_SPECIES, fa, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + + @Benchmark + public void testCompareMaskNotDouble() throws Throwable { + VectorOperators.Comparison op = comparison_con(); + DoubleVector bv = DoubleVector.fromArray(D_SPECIES, db, 0); + for (int j = 0; j < ARRAYLEN; j += D_SPECIES.length()) { + DoubleVector av = DoubleVector.fromArray(D_SPECIES, da, j); + VectorMask m = av.compare(op, bv).not(); + m.intoArray(mr, j); + } + } + } +} From 9c0f41e9973726df0544bf0c7f06a7eb214b849f Mon Sep 17 00:00:00 2001 From: Alexander Zvegintsev Date: Wed, 17 Sep 2025 08:07:50 +0000 Subject: [PATCH 070/556] 8225787: java/awt/Window/GetScreenLocation/GetScreenLocationTest.java fails on Ubuntu 8203004: UnixMultiResolutionSplashTest.java fails on Ubuntu16.04 Reviewed-by: psadhukhan, serb --- test/jdk/ProblemList.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 2628d18ae54..528a33c1bd5 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -453,7 +453,6 @@ java/awt/Focus/NonFocusableBlockedOwnerTest/NonFocusableBlockedOwnerTest.java 71 java/awt/Focus/TranserFocusToWindow/TranserFocusToWindow.java 6848810 macosx-all,linux-all java/awt/FileDialog/ModalFocus/FileDialogModalFocusTest.java 8194751 linux-all java/awt/image/VolatileImage/BitmaskVolatileImage.java 8133102 linux-all -java/awt/SplashScreen/MultiResolutionSplash/unix/UnixMultiResolutionSplashTest.java 8203004 linux-all java/awt/ScrollPane/ScrollPaneEventType.java 8296516 macosx-all java/awt/Robot/AcceptExtraMouseButtons/AcceptExtraMouseButtons.java 7107528 linux-all,macosx-all java/awt/Mouse/MouseDragEvent/MouseDraggedTest.java 8080676 linux-all @@ -491,7 +490,6 @@ java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeForModalDia java/awt/KeyboardFocusmanager/TypeAhead/MenuItemActivatedTest/MenuItemActivatedTest.java 8302787 windows-all java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java 8321303 linux-all -java/awt/Window/GetScreenLocation/GetScreenLocationTest.java 8225787 linux-x64 java/awt/Dialog/MakeWindowAlwaysOnTop/MakeWindowAlwaysOnTop.java 8266243 macosx-aarch64 java/awt/Dialog/ChoiceModalDialogTest.java 8161475 macosx-all java/awt/Dialog/FileDialogUserFilterTest.java 8001142 generic-all From d0ea6686ebe2baff28f5368f5bbf9dc7f34dd6d8 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Wed, 17 Sep 2025 08:21:20 +0000 Subject: [PATCH 071/556] 8367417: Serial: Use NMethodToOopClosure during Young GC Reviewed-by: fandreuzzi, stefank, tschatzl --- src/hotspot/share/gc/serial/defNewGeneration.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/hotspot/share/gc/serial/defNewGeneration.cpp b/src/hotspot/share/gc/serial/defNewGeneration.cpp index b5d65793b90..bcd131a5fa2 100644 --- a/src/hotspot/share/gc/serial/defNewGeneration.cpp +++ b/src/hotspot/share/gc/serial/defNewGeneration.cpp @@ -600,13 +600,11 @@ bool DefNewGeneration::collect(bool clear_all_soft_refs) { &old_gen_cl); { - StrongRootsScope srs(0); RootScanClosure oop_closure{this}; CLDScanClosure cld_closure{this}; - MarkingNMethodClosure nmethod_closure(&oop_closure, - NMethodToOopClosure::FixRelocations, - false /* keepalive_nmethods */); + NMethodToOopClosure nmethod_closure(&oop_closure, + NMethodToOopClosure::FixRelocations); // Starting tracing from roots, there are 4 kinds of roots in young-gc. // From 976207df1fcebf76a5f732b26424d6a4896b359e Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Wed, 17 Sep 2025 08:26:31 +0000 Subject: [PATCH 072/556] 8367476: Shenandoah: Remove use of CollectedHeap::_soft_ref_policy Reviewed-by: ayang, wkemper --- src/hotspot/share/gc/epsilon/epsilonHeap.hpp | 1 - src/hotspot/share/gc/g1/g1CollectedHeap.hpp | 1 - .../gc/parallel/parallelScavengeHeap.cpp | 7 +--- .../gc/parallel/parallelScavengeHeap.hpp | 2 - src/hotspot/share/gc/serial/serialHeap.cpp | 7 +--- src/hotspot/share/gc/serial/serialHeap.hpp | 5 --- src/hotspot/share/gc/shared/collectedHeap.cpp | 1 - src/hotspot/share/gc/shared/collectedHeap.hpp | 6 --- src/hotspot/share/gc/shared/gcCause.hpp | 7 ++++ .../share/gc/shared/gcVMOperations.cpp | 1 - src/hotspot/share/gc/shared/softRefPolicy.hpp | 42 ------------------- .../gc/shenandoah/shenandoahConcurrentGC.cpp | 3 +- .../gc/shenandoah/shenandoahControlThread.cpp | 5 ++- .../shenandoahGenerationalControlThread.cpp | 8 ++-- .../share/gc/shenandoah/shenandoahHeap.hpp | 1 - .../shenandoahReferenceProcessor.cpp | 9 ++-- .../shenandoahReferenceProcessor.hpp | 2 + .../share/gc/shenandoah/shenandoahSTWMark.cpp | 1 - src/hotspot/share/gc/z/zCollectedHeap.hpp | 1 - src/hotspot/share/prims/whitebox.cpp | 1 - 20 files changed, 26 insertions(+), 85 deletions(-) delete mode 100644 src/hotspot/share/gc/shared/softRefPolicy.hpp diff --git a/src/hotspot/share/gc/epsilon/epsilonHeap.hpp b/src/hotspot/share/gc/epsilon/epsilonHeap.hpp index f8aa9d7dbf1..e23e24a5afc 100644 --- a/src/hotspot/share/gc/epsilon/epsilonHeap.hpp +++ b/src/hotspot/share/gc/epsilon/epsilonHeap.hpp @@ -29,7 +29,6 @@ #include "gc/epsilon/epsilonBarrierSet.hpp" #include "gc/epsilon/epsilonMonitoringSupport.hpp" #include "gc/shared/collectedHeap.hpp" -#include "gc/shared/softRefPolicy.hpp" #include "gc/shared/space.hpp" #include "memory/virtualspace.hpp" #include "services/memoryManager.hpp" diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp index 0bb16edaf78..8d26bcb1c0b 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp @@ -50,7 +50,6 @@ #include "gc/shared/collectedHeap.hpp" #include "gc/shared/gcHeapSummary.hpp" #include "gc/shared/plab.hpp" -#include "gc/shared/softRefPolicy.hpp" #include "gc/shared/taskqueue.hpp" #include "memory/allocation.hpp" #include "memory/iterator.hpp" diff --git a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp index 21841330fa7..81412e2f614 100644 --- a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp +++ b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp @@ -469,14 +469,9 @@ void ParallelScavengeHeap::collect(GCCause::Cause cause) { VMThread::execute(&op); } -bool ParallelScavengeHeap::must_clear_all_soft_refs() { - return _gc_cause == GCCause::_metadata_GC_clear_soft_refs || - _gc_cause == GCCause::_wb_full_gc; -} - void ParallelScavengeHeap::collect_at_safepoint(bool full) { assert(!GCLocker::is_active(), "precondition"); - bool clear_soft_refs = must_clear_all_soft_refs(); + bool clear_soft_refs = GCCause::should_clear_all_soft_refs(_gc_cause); if (!full) { bool success = PSScavenge::invoke(clear_soft_refs); diff --git a/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp b/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp index bd701ae8be3..b1176a1637b 100644 --- a/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp +++ b/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp @@ -96,8 +96,6 @@ class ParallelScavengeHeap : public CollectedHeap { void update_parallel_worker_threads_cpu_time(); - bool must_clear_all_soft_refs(); - HeapWord* allocate_new_tlab(size_t min_size, size_t requested_size, size_t* actual_size) override; inline bool should_alloc_in_eden(size_t size) const; diff --git a/src/hotspot/share/gc/serial/serialHeap.cpp b/src/hotspot/share/gc/serial/serialHeap.cpp index 662a6be695b..e019144d628 100644 --- a/src/hotspot/share/gc/serial/serialHeap.cpp +++ b/src/hotspot/share/gc/serial/serialHeap.cpp @@ -340,11 +340,6 @@ HeapWord* SerialHeap::mem_allocate(size_t size) { false /* is_tlab */); } -bool SerialHeap::must_clear_all_soft_refs() { - return _gc_cause == GCCause::_metadata_GC_clear_soft_refs || - _gc_cause == GCCause::_wb_full_gc; -} - bool SerialHeap::is_young_gc_safe() const { if (!_young_gen->to()->is_empty()) { return false; @@ -497,7 +492,7 @@ void SerialHeap::scan_evacuated_objs(YoungGenScanClosure* young_cl, void SerialHeap::collect_at_safepoint(bool full) { assert(!GCLocker::is_active(), "precondition"); - bool clear_soft_refs = must_clear_all_soft_refs(); + bool clear_soft_refs = GCCause::should_clear_all_soft_refs(_gc_cause); if (!full) { bool success = do_young_collection(clear_soft_refs); diff --git a/src/hotspot/share/gc/serial/serialHeap.hpp b/src/hotspot/share/gc/serial/serialHeap.hpp index 72778981eee..86fb286f33f 100644 --- a/src/hotspot/share/gc/serial/serialHeap.hpp +++ b/src/hotspot/share/gc/serial/serialHeap.hpp @@ -31,7 +31,6 @@ #include "gc/shared/collectedHeap.hpp" #include "gc/shared/oopStorageParState.hpp" #include "gc/shared/preGCValues.hpp" -#include "gc/shared/softRefPolicy.hpp" #include "utilities/growableArray.hpp" class CardTableRS; @@ -104,10 +103,6 @@ private: void do_full_collection(bool clear_all_soft_refs) override; - // Does the "cause" of GC indicate that - // we absolutely __must__ clear soft refs? - bool must_clear_all_soft_refs(); - bool is_young_gc_safe() const; void gc_prologue(); diff --git a/src/hotspot/share/gc/shared/collectedHeap.cpp b/src/hotspot/share/gc/shared/collectedHeap.cpp index 71017817d14..9b6956ca75a 100644 --- a/src/hotspot/share/gc/shared/collectedHeap.cpp +++ b/src/hotspot/share/gc/shared/collectedHeap.cpp @@ -276,7 +276,6 @@ bool CollectedHeap::is_oop(oop object) const { CollectedHeap::CollectedHeap() : _capacity_at_last_gc(0), _used_at_last_gc(0), - _soft_ref_policy(), _is_stw_gc_active(false), _last_whole_heap_examined_time_ns(os::javaTimeNanos()), _total_collections(0), diff --git a/src/hotspot/share/gc/shared/collectedHeap.hpp b/src/hotspot/share/gc/shared/collectedHeap.hpp index 57bd9316731..f4f5ce79074 100644 --- a/src/hotspot/share/gc/shared/collectedHeap.hpp +++ b/src/hotspot/share/gc/shared/collectedHeap.hpp @@ -27,7 +27,6 @@ #include "gc/shared/gcCause.hpp" #include "gc/shared/gcWhen.hpp" -#include "gc/shared/softRefPolicy.hpp" #include "gc/shared/verifyOption.hpp" #include "memory/allocation.hpp" #include "memory/metaspace.hpp" @@ -104,8 +103,6 @@ class CollectedHeap : public CHeapObj { size_t _capacity_at_last_gc; size_t _used_at_last_gc; - SoftRefPolicy _soft_ref_policy; - // First, set it to java_lang_Object. // Then, set it to FillerObject after the FillerObject_klass loading is complete. static Klass* _filler_object_klass; @@ -395,9 +392,6 @@ protected: } } - // Return the SoftRefPolicy for the heap; - SoftRefPolicy* soft_ref_policy() { return &_soft_ref_policy; } - virtual MemoryUsage memory_usage(); virtual GrowableArray memory_managers() = 0; virtual GrowableArray memory_pools() = 0; diff --git a/src/hotspot/share/gc/shared/gcCause.hpp b/src/hotspot/share/gc/shared/gcCause.hpp index f775d41340d..aac4b801b74 100644 --- a/src/hotspot/share/gc/shared/gcCause.hpp +++ b/src/hotspot/share/gc/shared/gcCause.hpp @@ -103,6 +103,13 @@ class GCCause : public AllStatic { cause == _codecache_GC_aggressive); } + // Does the "cause" of GC indicate that + // we absolutely __must__ clear soft refs? + inline static bool should_clear_all_soft_refs(GCCause::Cause cause) { + return cause == GCCause::_metadata_GC_clear_soft_refs || + cause == GCCause::_wb_full_gc; + } + // Return a string describing the GCCause. static const char* to_string(GCCause::Cause cause); }; diff --git a/src/hotspot/share/gc/shared/gcVMOperations.cpp b/src/hotspot/share/gc/shared/gcVMOperations.cpp index 1299f64995f..97bae4f6d44 100644 --- a/src/hotspot/share/gc/shared/gcVMOperations.cpp +++ b/src/hotspot/share/gc/shared/gcVMOperations.cpp @@ -29,7 +29,6 @@ #include "gc/shared/gcId.hpp" #include "gc/shared/gcLocker.hpp" #include "gc/shared/gcVMOperations.hpp" -#include "gc/shared/softRefPolicy.hpp" #include "interpreter/oopMapCache.hpp" #include "logging/log.hpp" #include "memory/classLoaderMetaspace.hpp" diff --git a/src/hotspot/share/gc/shared/softRefPolicy.hpp b/src/hotspot/share/gc/shared/softRefPolicy.hpp deleted file mode 100644 index 1e8ec356c40..00000000000 --- a/src/hotspot/share/gc/shared/softRefPolicy.hpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef SHARE_GC_SHARED_SOFTREFPOLICY_HPP -#define SHARE_GC_SHARED_SOFTREFPOLICY_HPP - -class SoftRefPolicy { - private: - // Set to true when policy wants soft refs cleared. - // Reset to false by gc after it clears all soft refs. - bool _should_clear_all_soft_refs; - - public: - SoftRefPolicy() : - _should_clear_all_soft_refs(false) {} - - bool should_clear_all_soft_refs() { return _should_clear_all_soft_refs; } - void set_should_clear_all_soft_refs(bool v) { _should_clear_all_soft_refs = v; } -}; - -#endif // SHARE_GC_SHARED_SOFTREFPOLICY_HPP diff --git a/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp b/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp index 111e0073470..6d3b93ac406 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahConcurrentGC.cpp @@ -114,6 +114,8 @@ void ShenandoahConcurrentGC::entry_concurrent_update_refs_prepare(ShenandoahHeap bool ShenandoahConcurrentGC::collect(GCCause::Cause cause) { ShenandoahHeap* const heap = ShenandoahHeap::heap(); + _generation->ref_processor()->set_soft_reference_policy( + GCCause::should_clear_all_soft_refs(cause)); ShenandoahBreakpointGCScope breakpoint_gc_scope(cause); @@ -732,7 +734,6 @@ void ShenandoahConcurrentGC::op_init_mark() { // Weak reference processing ShenandoahReferenceProcessor* rp = _generation->ref_processor(); rp->reset_thread_locals(); - rp->set_soft_reference_policy(heap->soft_ref_policy()->should_clear_all_soft_refs()); // Make above changes visible to worker threads OrderAccess::fence(); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp index 6290101bc49..80e5b709ada 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp @@ -34,6 +34,7 @@ #include "gc/shenandoah/shenandoahGeneration.hpp" #include "gc/shenandoah/shenandoahHeap.inline.hpp" #include "gc/shenandoah/shenandoahMonitoringSupport.hpp" +#include "gc/shenandoah/shenandoahReferenceProcessor.hpp" #include "gc/shenandoah/shenandoahUtils.hpp" #include "logging/log.hpp" #include "memory/metaspaceStats.hpp" @@ -118,7 +119,7 @@ void ShenandoahControlThread::run_service() { // Blow all soft references on this cycle, if handling allocation failure, // either implicit or explicit GC request, or we are requested to do so unconditionally. if (alloc_failure_pending || is_gc_requested || ShenandoahAlwaysClearSoftRefs) { - heap->soft_ref_policy()->set_should_clear_all_soft_refs(true); + heap->global_generation()->ref_processor()->set_soft_reference_policy(true); } const bool gc_requested = (mode != none); @@ -193,7 +194,7 @@ void ShenandoahControlThread::run_service() { heap->set_forced_counters_update(false); // Retract forceful part of soft refs policy - heap->soft_ref_policy()->set_should_clear_all_soft_refs(false); + heap->global_generation()->ref_processor()->set_soft_reference_policy(false); // Clear metaspace oom flag, if current cycle unloaded classes if (heap->unload_classes()) { diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp index ce8d96308ba..761ba02d569 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp @@ -37,6 +37,7 @@ #include "gc/shenandoah/shenandoahMonitoringSupport.hpp" #include "gc/shenandoah/shenandoahOldGC.hpp" #include "gc/shenandoah/shenandoahOldGeneration.hpp" +#include "gc/shenandoah/shenandoahReferenceProcessor.hpp" #include "gc/shenandoah/shenandoahUtils.hpp" #include "gc/shenandoah/shenandoahYoungGeneration.hpp" #include "logging/log.hpp" @@ -216,8 +217,9 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest // Blow away all soft references on this cycle, if handling allocation failure, // either implicit or explicit GC request, or we are requested to do so unconditionally. - if (request.generation->is_global() && (ShenandoahCollectorPolicy::is_allocation_failure(request.cause) || ShenandoahCollectorPolicy::is_explicit_gc(request.cause) || ShenandoahAlwaysClearSoftRefs)) { - _heap->soft_ref_policy()->set_should_clear_all_soft_refs(true); + if (GCCause::should_clear_all_soft_refs(request.cause) || (request.generation->is_global() && + (ShenandoahCollectorPolicy::is_allocation_failure(request.cause) || ShenandoahCollectorPolicy::is_explicit_gc(request.cause) || ShenandoahAlwaysClearSoftRefs))) { + request.generation->ref_processor()->set_soft_reference_policy(true); } // GC is starting, bump the internal ID @@ -289,7 +291,7 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest _heap->set_forced_counters_update(false); // Retract forceful part of soft refs policy - _heap->soft_ref_policy()->set_should_clear_all_soft_refs(false); + request.generation->ref_processor()->set_soft_reference_policy(false); // Clear metaspace oom flag, if current cycle unloaded classes if (_heap->unload_classes()) { diff --git a/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp b/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp index eafd1b28b3a..322ac26e254 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp @@ -29,7 +29,6 @@ #include "gc/shared/collectedHeap.hpp" #include "gc/shared/markBitMap.hpp" -#include "gc/shared/softRefPolicy.hpp" #include "gc/shenandoah/mode/shenandoahMode.hpp" #include "gc/shenandoah/shenandoahAllocRequest.hpp" #include "gc/shenandoah/shenandoahAsserts.hpp" diff --git a/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.cpp b/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.cpp index f8726386b5d..4ca6f2fdf49 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.cpp @@ -221,8 +221,10 @@ void ShenandoahRefProcThreadLocal::set_discovered_list_head(oop head) { *discovered_list_addr() = head; } +AlwaysClearPolicy ShenandoahReferenceProcessor::_always_clear_policy; + ShenandoahReferenceProcessor::ShenandoahReferenceProcessor(uint max_workers) : - _soft_reference_policy(nullptr), + _soft_reference_policy(&_always_clear_policy), _ref_proc_thread_locals(NEW_C_HEAP_ARRAY(ShenandoahRefProcThreadLocal, max_workers, mtGC)), _pending_list(nullptr), _pending_list_tail(&_pending_list), @@ -245,12 +247,11 @@ void ShenandoahReferenceProcessor::set_mark_closure(uint worker_id, ShenandoahMa } void ShenandoahReferenceProcessor::set_soft_reference_policy(bool clear) { - static AlwaysClearPolicy always_clear_policy; static LRUMaxHeapPolicy lru_max_heap_policy; if (clear) { log_info(gc, ref)("Clearing All SoftReferences"); - _soft_reference_policy = &always_clear_policy; + _soft_reference_policy = &_always_clear_policy; } else { _soft_reference_policy = &lru_max_heap_policy; } @@ -284,7 +285,7 @@ bool ShenandoahReferenceProcessor::is_softly_live(oop reference, ReferenceType t // Ask SoftReference policy const jlong clock = java_lang_ref_SoftReference::clock(); assert(clock != 0, "Clock not initialized"); - assert(_soft_reference_policy != nullptr, "Policy not initialized"); + assert(_soft_reference_policy != nullptr, "Should never be null"); return !_soft_reference_policy->should_clear_reference(reference, clock); } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.hpp b/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.hpp index 682c4268754..11099f1303d 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahReferenceProcessor.hpp @@ -127,6 +127,8 @@ public: class ShenandoahReferenceProcessor : public ReferenceDiscoverer { private: + static AlwaysClearPolicy _always_clear_policy; + ReferencePolicy* _soft_reference_policy; ShenandoahRefProcThreadLocal* _ref_proc_thread_locals; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp b/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp index c2bfea664fd..260c1e0276f 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp @@ -77,7 +77,6 @@ void ShenandoahSTWMark::mark() { ShenandoahReferenceProcessor* rp = _generation->ref_processor(); shenandoah_assert_generations_reconciled(); rp->reset_thread_locals(); - rp->set_soft_reference_policy(heap->soft_ref_policy()->should_clear_all_soft_refs()); // Init mark, do not expect forwarded pointers in roots if (ShenandoahVerify) { diff --git a/src/hotspot/share/gc/z/zCollectedHeap.hpp b/src/hotspot/share/gc/z/zCollectedHeap.hpp index c124976c80f..bbcddec917f 100644 --- a/src/hotspot/share/gc/z/zCollectedHeap.hpp +++ b/src/hotspot/share/gc/z/zCollectedHeap.hpp @@ -25,7 +25,6 @@ #define SHARE_GC_Z_ZCOLLECTEDHEAP_HPP #include "gc/shared/collectedHeap.hpp" -#include "gc/shared/softRefPolicy.hpp" #include "gc/z/zBarrierSet.hpp" #include "gc/z/zHeap.hpp" #include "gc/z/zInitialize.hpp" diff --git a/src/hotspot/share/prims/whitebox.cpp b/src/hotspot/share/prims/whitebox.cpp index ce559d47b24..e8e873fcb66 100644 --- a/src/hotspot/share/prims/whitebox.cpp +++ b/src/hotspot/share/prims/whitebox.cpp @@ -1501,7 +1501,6 @@ WB_ENTRY(jboolean, WB_IsInStringTable(JNIEnv* env, jobject o, jstring javaString WB_END WB_ENTRY(void, WB_FullGC(JNIEnv* env, jobject o)) - Universe::heap()->soft_ref_policy()->set_should_clear_all_soft_refs(true); Universe::heap()->collect(GCCause::_wb_full_gc); WB_END From 5730e908c636ad57e6bbc5a1b64ce88245c38788 Mon Sep 17 00:00:00 2001 From: Daniel Gredler Date: Wed, 17 Sep 2025 09:16:41 +0000 Subject: [PATCH 073/556] 4138921: TextLayout handling of empty strings Reviewed-by: prr, serb --- .../classes/java/awt/font/TextLayout.java | 52 +-- .../share/classes/java/awt/font/TextLine.java | 18 +- .../classes/sun/font/TextLabelFactory.java | 6 +- .../TextLayout/TextLayoutConstructorTest.java | 316 ++++++++++++++++++ 4 files changed, 362 insertions(+), 30 deletions(-) create mode 100644 test/jdk/java/awt/font/TextLayout/TextLayoutConstructorTest.java diff --git a/src/java.desktop/share/classes/java/awt/font/TextLayout.java b/src/java.desktop/share/classes/java/awt/font/TextLayout.java index 74382b4bd83..5e8f3040055 100644 --- a/src/java.desktop/share/classes/java/awt/font/TextLayout.java +++ b/src/java.desktop/share/classes/java/awt/font/TextLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -256,7 +256,6 @@ public final class TextLayout implements Cloneable { */ private boolean cacheIsValid = false; - // This value is obtained from an attribute, and constrained to the // interval [0,1]. If 0, the layout cannot be justified. private float justifyRatio; @@ -367,6 +366,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(String string, Font font, FontRenderContext frc) { @@ -378,8 +378,8 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null string passed to TextLayout constructor."); } - if (string.length() == 0) { - throw new IllegalArgumentException("Zero length string passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } Map attributes = null; @@ -415,6 +415,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(String string, Map attributes, FontRenderContext frc) @@ -427,8 +428,8 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null map passed to TextLayout constructor."); } - if (string.length() == 0) { - throw new IllegalArgumentException("Zero length string passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } char[] text = string.toCharArray(); @@ -499,6 +500,7 @@ public final class TextLayout implements Cloneable { * device resolution, and attributes such as antialiasing. This * parameter does not specify a translation between the * {@code TextLayout} and user space. + * @throws IllegalArgumentException if any of the parameters are null. */ public TextLayout(AttributedCharacterIterator text, FontRenderContext frc) { @@ -506,14 +508,13 @@ public final class TextLayout implements Cloneable { throw new IllegalArgumentException("Null iterator passed to TextLayout constructor."); } - int start = text.getBeginIndex(); - int limit = text.getEndIndex(); - if (start == limit) { - throw new IllegalArgumentException("Zero length iterator passed to TextLayout constructor."); + if (frc == null) { + throw new IllegalArgumentException("Null font render context passed to TextLayout constructor."); } + int start = text.getBeginIndex(); + int limit = text.getEndIndex(); int len = limit - start; - text.first(); char[] chars = new char[len]; int n = 0; for (char c = text.first(); @@ -1125,7 +1126,12 @@ public final class TextLayout implements Cloneable { float top1X, top2X; float bottom1X, bottom2X; - if (caret == 0 || caret == characterCount) { + if (caret == 0 && characterCount == 0) { + + top1X = top2X = 0; + bottom1X = bottom2X = 0; + + } else if (caret == 0 || caret == characterCount) { float pos; int logIndex; @@ -1143,8 +1149,8 @@ public final class TextLayout implements Cloneable { pos += angle * shift; top1X = top2X = pos + angle*textLine.getCharAscent(logIndex); bottom1X = bottom2X = pos - angle*textLine.getCharDescent(logIndex); - } - else { + + } else { { int logIndex = textLine.visualToLogical(caret-1); @@ -1884,7 +1890,6 @@ public final class TextLayout implements Cloneable { Shape[] result = new Shape[2]; TextHitInfo hit = TextHitInfo.afterOffset(offset); - int hitCaret = hitToCaret(hit); LayoutPathImpl lp = textLine.getLayoutPath(); @@ -2180,12 +2185,16 @@ public final class TextLayout implements Cloneable { checkTextHit(firstEndpoint); checkTextHit(secondEndpoint); - if(bounds == null) { - throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()"); + if (bounds == null) { + throw new IllegalArgumentException("Null Rectangle2D passed to TextLayout.getVisualHighlightShape()"); } GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + if (characterCount == 0) { + return result; + } + int firstCaret = hitToCaret(firstEndpoint); int secondCaret = hitToCaret(secondEndpoint); @@ -2194,8 +2203,9 @@ public final class TextLayout implements Cloneable { if (firstCaret == 0 || secondCaret == 0) { GeneralPath ls = leftShape(bounds); - if (!ls.getBounds().isEmpty()) + if (!ls.getBounds().isEmpty()) { result.append(ls, false); + } } if (firstCaret == characterCount || secondCaret == characterCount) { @@ -2282,12 +2292,16 @@ public final class TextLayout implements Cloneable { secondEndpoint = t; } - if(firstEndpoint < 0 || secondEndpoint > characterCount) { + if (firstEndpoint < 0 || secondEndpoint > characterCount) { throw new IllegalArgumentException("Range is invalid in TextLayout.getLogicalHighlightShape()"); } GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + if (characterCount == 0) { + return result; + } + int[] carets = new int[10]; // would this ever not handle all cases? int count = 0; diff --git a/src/java.desktop/share/classes/java/awt/font/TextLine.java b/src/java.desktop/share/classes/java/awt/font/TextLine.java index 681fcd90083..3464c1626c6 100644 --- a/src/java.desktop/share/classes/java/awt/font/TextLine.java +++ b/src/java.desktop/share/classes/java/awt/font/TextLine.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -836,7 +836,7 @@ final class TextLine { } if (result == null) { - result = new Rectangle2D.Float(Float.MAX_VALUE, Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE); + result = new Rectangle2D.Float(0, 0, 0, 0); } return result; @@ -844,6 +844,10 @@ final class TextLine { public Rectangle2D getItalicBounds() { + if (fComponents.length == 0) { + return new Rectangle2D.Float(0, 0, 0, 0); + } + float left = Float.MAX_VALUE, right = -Float.MAX_VALUE; float top = Float.MAX_VALUE, bottom = -Float.MAX_VALUE; @@ -927,7 +931,7 @@ final class TextLine { // dlf: get baseRot from font for now??? if (!requiresBidi) { - requiresBidi = Bidi.requiresBidi(chars, 0, chars.length); + requiresBidi = Bidi.requiresBidi(chars, 0, characterCount); } if (requiresBidi) { @@ -935,7 +939,7 @@ final class TextLine { ? Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT : values.getRunDirection(); - bidi = new Bidi(chars, 0, embs, 0, chars.length, bidiflags); + bidi = new Bidi(chars, 0, embs, 0, characterCount, bidiflags); if (!bidi.isLeftToRight()) { levels = BidiUtils.getLevels(bidi); int[] charsVtoL = BidiUtils.createVisualToLogicalMap(levels); @@ -945,13 +949,11 @@ final class TextLine { } Decoration decorator = Decoration.getDecoration(values); - int layoutFlags = 0; // no extra info yet, bidi determines run and line direction TextLabelFactory factory = new TextLabelFactory(frc, chars, bidi, layoutFlags); TextLineComponent[] components = new TextLineComponent[1]; - - components = createComponentsOnRun(0, chars.length, + components = createComponentsOnRun(0, characterCount, chars, charsLtoV, levels, factory, font, lm, @@ -972,7 +974,7 @@ final class TextLine { } return new TextLine(frc, components, lm.baselineOffsets, - chars, 0, chars.length, charsLtoV, levels, isDirectionLTR); + chars, 0, characterCount, charsLtoV, levels, isDirectionLTR); } private static TextLineComponent[] expandArray(TextLineComponent[] orig) { diff --git a/src/java.desktop/share/classes/sun/font/TextLabelFactory.java b/src/java.desktop/share/classes/sun/font/TextLabelFactory.java index d245f33b8af..7a41bd2e7f5 100644 --- a/src/java.desktop/share/classes/sun/font/TextLabelFactory.java +++ b/src/java.desktop/share/classes/sun/font/TextLabelFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -119,7 +119,7 @@ public final class TextLabelFactory { int start, int limit) { - if (start >= limit || start < lineStart || limit > lineLimit) { + if (start > limit || start < lineStart || limit > lineLimit) { throw new IllegalArgumentException("bad start: " + start + " or limit: " + limit); } @@ -145,7 +145,7 @@ public final class TextLabelFactory { int start, int limit) { - if (start >= limit || start < lineStart || limit > lineLimit) { + if (start > limit || start < lineStart || limit > lineLimit) { throw new IllegalArgumentException("bad start: " + start + " or limit: " + limit); } diff --git a/test/jdk/java/awt/font/TextLayout/TextLayoutConstructorTest.java b/test/jdk/java/awt/font/TextLayout/TextLayoutConstructorTest.java new file mode 100644 index 00000000000..31117e495bc --- /dev/null +++ b/test/jdk/java/awt/font/TextLayout/TextLayoutConstructorTest.java @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 4138921 + * @summary Confirm constructor behavior for various edge cases. + */ + +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.font.TextAttribute; +import java.awt.font.TextHitInfo; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class TextLayoutConstructorTest { + + public static void main(String[] args) throws Exception { + testFontConstructor(); + testMapConstructor(); + testIteratorConstructor(); + } + + private static void testFontConstructor() { + + // new TextLayout(String, Font, FontRenderContext) + + Font font = new Font(Font.DIALOG, Font.PLAIN, 20); + FontRenderContext frc = new FontRenderContext(null, true, true); + + assertThrows(() -> new TextLayout(null, font, frc), + IllegalArgumentException.class, + "Null string passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", (Font) null, frc), + IllegalArgumentException.class, + "Null font passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", font, null), + IllegalArgumentException.class, + "Null font render context passed to TextLayout constructor."); + + Function< String, TextLayout > creator = (s) -> new TextLayout(s, font, frc); + assertEmptyTextLayoutBehavior(creator); + } + + private static void testMapConstructor() { + + // new TextLayout(String, Map, FontRenderContext) + + Map< TextAttribute, Object > attributes = Map.of(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); + FontRenderContext frc = new FontRenderContext(null, true, true); + + assertThrows(() -> new TextLayout(null, attributes, frc), + IllegalArgumentException.class, + "Null string passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", (Map) null, frc), + IllegalArgumentException.class, + "Null map passed to TextLayout constructor."); + + assertThrows(() -> new TextLayout("test", attributes, null), + IllegalArgumentException.class, + "Null font render context passed to TextLayout constructor."); + + Function< String, TextLayout > creator = (s) -> new TextLayout(s, attributes, frc); + assertEmptyTextLayoutBehavior(creator); + } + + private static void testIteratorConstructor() { + + // new TextLayout(AttributedCharacterIterator, FontRenderContext) + + Map< TextAttribute, Object > attributes = Map.of(); + FontRenderContext frc = new FontRenderContext(null, true, true); + + assertThrows(() -> new TextLayout(null, frc), + IllegalArgumentException.class, + "Null iterator passed to TextLayout constructor."); + + AttributedCharacterIterator it1 = new AttributedString("test", attributes).getIterator(); + assertThrows(() -> new TextLayout(it1, null), + IllegalArgumentException.class, + "Null font render context passed to TextLayout constructor."); + + Function< String, TextLayout > creator = (s) -> { + AttributedCharacterIterator it2 = new AttributedString(s, attributes).getIterator(); + return new TextLayout(it2, frc); + }; + assertEmptyTextLayoutBehavior(creator); + } + + private static void assertEmptyTextLayoutBehavior(Function< String, TextLayout > creator) { + + TextLayout tl = creator.apply(""); + TextLayout ref = creator.apply(" "); // space + FontRenderContext frc = new FontRenderContext(null, true, true); + Rectangle zero = new Rectangle(0, 0, 0, 0); + Rectangle2D.Float zero2D = new Rectangle2D.Float(0, 0, 0, 0); + Rectangle2D.Float oneTwo = new Rectangle2D.Float(1, 2, 0, 0); + Rectangle2D.Float kilo = new Rectangle2D.Float(0, 0, 1000, 1000); + AffineTransform identity = new AffineTransform(); + TextLayout.CaretPolicy policy = new TextLayout.CaretPolicy(); + TextHitInfo start = TextHitInfo.trailing(-1); + TextHitInfo end = TextHitInfo.leading(0); + + assertEqual(0, tl.getJustifiedLayout(100).getAdvance(), "justified advance"); + assertEqual(0, tl.getBaseline(), "baseline"); + + float[] offsets = tl.getBaselineOffsets(); + float[] refOffsets = ref.getBaselineOffsets(); + assertEqual(3, offsets.length, "baseline offsets"); + assertEqual(refOffsets[0], offsets[0], "baseline offset 1"); + assertEqual(refOffsets[1], offsets[1], "baseline offset 2"); + assertEqual(refOffsets[2], offsets[2], "baseline offset 3"); + + assertEqual(0, tl.getAdvance(), "advance"); + assertEqual(0, tl.getVisibleAdvance(), "visible advance"); + assertEqual(ref.getAscent(), tl.getAscent(), "ascent"); + assertEqual(ref.getDescent(), tl.getDescent(), "descent"); + assertEqual(ref.getLeading(), tl.getLeading(), "leading"); + assertEqual(zero2D, tl.getBounds(), "bounds"); + assertEqual(zero2D, tl.getPixelBounds(frc, 0, 0), "pixel bounds 1"); + assertEqual(oneTwo, tl.getPixelBounds(frc, 1, 2), "pixel bounds 2"); + assertEqual(true, tl.isLeftToRight(), "left to right"); + assertEqual(false, tl.isVertical(), "is vertical"); + assertEqual(0, tl.getCharacterCount(), "character count"); + + float[] caretInfo = tl.getCaretInfo(start, kilo); + float[] refCaretInfo = ref.getCaretInfo(start, kilo); + assertEqual(6, caretInfo.length, "caret info length 1"); + assertEqual(refCaretInfo[0], caretInfo[0], "first caret info 1"); + assertEqual(refCaretInfo[1], caretInfo[1], "second caret info 1"); + assertEqual(refCaretInfo[2], caretInfo[2], "third caret info 1"); + assertEqual(refCaretInfo[3], caretInfo[3], "fourth caret info 1"); + assertEqual(refCaretInfo[4], caretInfo[4], "fifth caret info 1"); + assertEqual(refCaretInfo[5], caretInfo[5], "sixth caret info 1"); + + float[] caretInfo2 = tl.getCaretInfo(start); + float[] refCaretInfo2 = ref.getCaretInfo(start); + assertEqual(6, caretInfo2.length, "caret info length 2"); + assertEqual(refCaretInfo2[0], caretInfo2[0], "first caret info 2"); + assertEqual(refCaretInfo2[1], caretInfo2[1], "second caret info 2"); + assertEqual(refCaretInfo2[2], caretInfo2[2], "third caret info 2"); + assertEqual(refCaretInfo2[3], caretInfo2[3], "fourth caret info 2"); + assertEqual(refCaretInfo2[4], caretInfo2[4], "fifth caret info 2"); + assertEqual(refCaretInfo2[5], caretInfo2[5], "sixth caret info 2"); + + assertEqual(null, tl.getNextRightHit(start), "next right hit 1"); + assertEqual(null, tl.getNextRightHit(end), "next right hit 2"); + assertEqual(null, tl.getNextRightHit(0, policy), "next right hit 3"); + assertEqual(null, tl.getNextRightHit(0), "next right hit 4"); + assertEqual(null, tl.getNextLeftHit(start), "next left hit 1"); + assertEqual(null, tl.getNextLeftHit(end), "next left hit 2"); + assertEqual(null, tl.getNextLeftHit(0, policy), "next left hit 3"); + assertEqual(null, tl.getNextLeftHit(0), "next left hit 4"); + assertEqual(end, tl.getVisualOtherHit(start), "visual other hit"); + + Shape caretShape = tl.getCaretShape(start, kilo); + Shape refCaretShape = ref.getCaretShape(start, kilo); + assertEqual(refCaretShape.getBounds(), caretShape.getBounds(), "caret shape 1"); + + Shape caretShape2 = tl.getCaretShape(start); + Shape refCaretShape2 = ref.getCaretShape(start); + assertEqual(refCaretShape2.getBounds(), caretShape2.getBounds(), "caret shape 2"); + + assertEqual(0, tl.getCharacterLevel(0), "character level"); + + Shape[] caretShapes = tl.getCaretShapes(0, kilo, policy); + Shape[] refCaretShapes = ref.getCaretShapes(0, kilo, policy); + assertEqual(2, caretShapes.length, "caret shapes length 1"); + assertEqual(refCaretShapes[0].getBounds(), caretShapes[0].getBounds(), "caret shapes strong 1"); + assertEqual(refCaretShapes[1], caretShapes[1], "caret shapes weak 1"); + assertEqual(null, caretShapes[1], "caret shapes weak 1"); + + Shape[] caretShapes2 = tl.getCaretShapes(0, kilo); + Shape[] refCaretShapes2 = ref.getCaretShapes(0, kilo); + assertEqual(2, caretShapes2.length, "caret shapes length 2"); + assertEqual(refCaretShapes2[0].getBounds(), caretShapes2[0].getBounds(), "caret shapes strong 2"); + assertEqual(refCaretShapes2[1], caretShapes2[1], "caret shapes weak 2"); + assertEqual(null, caretShapes2[1], "caret shapes weak 2"); + + Shape[] caretShapes3 = tl.getCaretShapes(0); + Shape[] refCaretShapes3 = ref.getCaretShapes(0); + assertEqual(2, caretShapes3.length, "caret shapes length 3"); + assertEqual(refCaretShapes3[0].getBounds(), caretShapes3[0].getBounds(), "caret shapes strong 3"); + assertEqual(refCaretShapes3[1], caretShapes3[1], "caret shapes weak 3"); + assertEqual(null, caretShapes3[1], "caret shapes weak 3"); + + assertEqual(0, tl.getLogicalRangesForVisualSelection(start, start).length, "logical ranges for visual selection"); + assertEqual(zero2D, tl.getVisualHighlightShape(start, start, kilo).getBounds(), "visual highlight shape 1"); + assertEqual(zero2D, tl.getVisualHighlightShape(start, start).getBounds(), "visual highlight shape 2"); + assertEqual(zero, tl.getLogicalHighlightShape(0, 0, kilo).getBounds(), "logical highlight shape 1"); + assertEqual(zero, tl.getLogicalHighlightShape(0, 0).getBounds(), "logical highlight shape 2"); + assertEqual(zero, tl.getBlackBoxBounds(0, 0).getBounds(), "black box bounds"); + + TextHitInfo hit = tl.hitTestChar(0, 0); + assertEqual(-1, hit.getCharIndex(), "hit test char index 1"); + assertEqual(false, hit.isLeadingEdge(), "hit test leading edge 1"); + + TextHitInfo hit2 = tl.hitTestChar(0, 0, kilo); + assertEqual(-1, hit2.getCharIndex(), "hit test char index 2"); + assertEqual(false, hit2.isLeadingEdge(), "hit test leading edge 2"); + + assertEqual(false, tl.equals(creator.apply("")), "equals"); + assertEqual(false, tl.toString().isEmpty(), "to string"); + assertDoesNotDraw(tl); + assertEqual(zero2D, tl.getOutline(identity).getBounds(), "outline"); + assertEqual(null, tl.getLayoutPath(), "layout path"); + + Point2D.Float point = new Point2D.Float(7, 7); + tl.hitToPoint(start, point); + assertEqual(0, point.x, "hit to point x"); + assertEqual(0, point.y, "hit to point y"); + } + + private static void assertEqual(int expected, int actual, String name) { + if (expected != actual) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertEqual(float expected, float actual, String name) { + if (expected != actual) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertEqual(boolean expected, boolean actual, String name) { + if (expected != actual) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertEqual(Object expected, Object actual, String name) { + if (!Objects.equals(expected, actual)) { + throw new RuntimeException("Expected " + name + " = " + expected + ", but got " + actual); + } + } + + private static void assertThrows(Runnable r, Class< ? > type, String message) { + Class< ? > actualType; + String actualMessage; + Exception actualException; + try { + r.run(); + actualType = null; + actualMessage = null; + actualException = null; + } catch (Exception e) { + actualType = e.getClass(); + actualMessage = e.getMessage(); + actualException = e; + } + if (!Objects.equals(type, actualType)) { + throw new RuntimeException(type + " != " + actualType, actualException); + } + if (!Objects.equals(message, actualMessage)) { + throw new RuntimeException(message + " != " + actualMessage, actualException); + } + } + + private static void assertDoesNotDraw(TextLayout layout) { + + int w = 200; + int h = 200; + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g2d = image.createGraphics(); + int expected = image.getRGB(0, 0); + + layout.draw(g2d, w / 2f, h / 2f); // should not actually draw anything + + int[] rowPixels = new int[w]; + for (int y = 0; y < h; y++) { + image.getRGB(0, y, w, 1, rowPixels, 0, w); + for (int x = 0; x < w; x++) { + if (rowPixels[x] != expected) { + throw new RuntimeException( + "pixel (" + x + ", " + y +"): " + expected + " != " + rowPixels[x]); + } + } + } + } +} From 005f3a392f20ea2fbe2d7d699448e65d3443a073 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Wed, 17 Sep 2025 09:41:44 +0000 Subject: [PATCH 074/556] 8367743: G1: Use named constants for G1CSetCandidateGroup group ids Reviewed-by: ayang, iwalulya --- src/hotspot/share/gc/g1/g1CollectedHeap.cpp | 2 +- .../share/gc/g1/g1CollectionSetCandidates.cpp | 2 +- .../share/gc/g1/g1CollectionSetCandidates.hpp | 25 ++++++++++++------- src/hotspot/share/gc/g1/g1ConcurrentMark.cpp | 8 +++--- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index 4f7eaa36c2d..4a257265931 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -1200,7 +1200,7 @@ G1CollectedHeap::G1CollectedHeap() : _rem_set(nullptr), _card_set_config(), _card_set_freelist_pool(G1CardSetConfiguration::num_mem_object_types()), - _young_regions_cset_group(card_set_config(), &_card_set_freelist_pool, 1u /* group_id */), + _young_regions_cset_group(card_set_config(), &_card_set_freelist_pool, G1CSetCandidateGroup::YoungRegionId), _cm(nullptr), _cm_thread(nullptr), _cr(nullptr), diff --git a/src/hotspot/share/gc/g1/g1CollectionSetCandidates.cpp b/src/hotspot/share/gc/g1/g1CollectionSetCandidates.cpp index ccb52922c09..47340fad768 100644 --- a/src/hotspot/share/gc/g1/g1CollectionSetCandidates.cpp +++ b/src/hotspot/share/gc/g1/g1CollectionSetCandidates.cpp @@ -27,7 +27,7 @@ #include "gc/g1/g1HeapRegion.inline.hpp" #include "utilities/growableArray.hpp" -uint G1CSetCandidateGroup::_next_group_id = 2; +uint G1CSetCandidateGroup::_next_group_id = G1CSetCandidateGroup::InitialId; G1CSetCandidateGroup::G1CSetCandidateGroup(G1CardSetConfiguration* config, G1MonotonicArenaFreePool* card_set_freelist_pool, uint group_id) : _candidates(4, mtGCCardSet), diff --git a/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp b/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp index 02a4d5f6d76..0f4e92968fa 100644 --- a/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp +++ b/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp @@ -73,14 +73,21 @@ class G1CSetCandidateGroup : public CHeapObj{ size_t _reclaimable_bytes; double _gc_efficiency; - // The _group_id is primarily used when printing out per-region liveness information, - // making it easier to associate regions with their assigned G1CSetCandidateGroup, if any. - // Note: - // * _group_id 0 is reserved for special G1CSetCandidateGroups that hold only a single region, - // such as G1CSetCandidateGroups for retained regions. - // * _group_id 1 is reserved for the G1CSetCandidateGroup that contains all young regions. +public: + // The _group_id uniquely identifies a candidate group when printing, making it + // easier to associate regions with their assigned G1CSetCandidateGroup, if any. + // Special values for the id: + // * id 0 is reserved for regions that do not have a remembered set. + // * id 1 is reserved for the G1CollectionSetCandidate that contains all young regions. + // * other ids are handed out incrementally, starting from InitialId. + static const uint NoRemSetId = 0; + static const uint YoungRegionId = 1; + static const uint InitialId = 2; + +private: const uint _group_id; static uint _next_group_id; + public: G1CSetCandidateGroup(); G1CSetCandidateGroup(G1CardSetConfiguration* config, G1MonotonicArenaFreePool* card_set_freelist_pool, uint group_id); @@ -95,8 +102,6 @@ public: G1CardSet* card_set() { return &_card_set; } const G1CardSet* card_set() const { return &_card_set; } - uint group_id() const { return _group_id; } - void calculate_efficiency(); double liveness_percent() const; @@ -127,8 +132,10 @@ public: return _candidates.end(); } + uint group_id() const { return _group_id; } + static void reset_next_group_id() { - _next_group_id = 2; + _next_group_id = InitialId; } }; diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp index 6d30a93dafb..3d95541ae3c 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp @@ -3052,11 +3052,9 @@ bool G1PrintRegionLivenessInfoClosure::do_heap_region(G1HeapRegion* r) { size_t remset_bytes = r->rem_set()->mem_size(); size_t code_roots_bytes = r->rem_set()->code_roots_mem_size(); const char* remset_type = r->rem_set()->get_short_state_str(); - uint cset_group_id = 0; - - if (r->rem_set()->has_cset_group()) { - cset_group_id = r->rem_set()->cset_group_id(); - } + uint cset_group_id = r->rem_set()->has_cset_group() + ? r->rem_set()->cset_group_id() + : G1CSetCandidateGroup::NoRemSetId; _total_used_bytes += used_bytes; _total_capacity_bytes += capacity_bytes; From faebec63a94bb532b9d0ca0736c73ddbf1392ac2 Mon Sep 17 00:00:00 2001 From: Andrew Dinn Date: Wed, 17 Sep 2025 09:42:01 +0000 Subject: [PATCH 075/556] 8367532: Declare all stubgen stub entries including internal cross-stub entries Reviewed-by: fyang, asmehra --- .../cpu/aarch64/macroAssembler_aarch64.hpp | 2 +- .../aarch64/macroAssembler_aarch64_aes.cpp | 7 +- .../cpu/aarch64/stubGenerator_aarch64.cpp | 374 ++++++++++-------- src/hotspot/cpu/arm/stubGenerator_arm.cpp | 5 + src/hotspot/cpu/ppc/stubGenerator_ppc.cpp | 8 +- src/hotspot/cpu/riscv/stubGenerator_riscv.cpp | 178 ++++++--- .../x86/stubGenerator_x86_64_arraycopy.cpp | 113 ++++-- .../share/runtime/stubDeclarations.hpp | 48 +++ 8 files changed, 477 insertions(+), 258 deletions(-) diff --git a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp index fe2440fd3fd..0570fad5b8d 100644 --- a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp @@ -1623,7 +1623,7 @@ public: FloatRegister p, FloatRegister z, FloatRegister t1); void ghash_reduce_wide(int index, FloatRegister result, FloatRegister lo, FloatRegister hi, FloatRegister p, FloatRegister z, FloatRegister t1); - void ghash_processBlocks_wide(address p, Register state, Register subkeyH, + void ghash_processBlocks_wide(Label& p, Register state, Register subkeyH, Register data, Register blocks, int unrolls); diff --git a/src/hotspot/cpu/aarch64/macroAssembler_aarch64_aes.cpp b/src/hotspot/cpu/aarch64/macroAssembler_aarch64_aes.cpp index 84b85b7b445..25ec9cf9bdd 100644 --- a/src/hotspot/cpu/aarch64/macroAssembler_aarch64_aes.cpp +++ b/src/hotspot/cpu/aarch64/macroAssembler_aarch64_aes.cpp @@ -507,7 +507,7 @@ void MacroAssembler::ghash_modmul(FloatRegister result, // // Clobbers all vector registers. // -void MacroAssembler::ghash_processBlocks_wide(address field_polynomial, Register state, +void MacroAssembler::ghash_processBlocks_wide(Label& field_polynomial, Register state, Register subkeyH, Register data, Register blocks, int unrolls) { int register_stride = 7; @@ -531,7 +531,10 @@ void MacroAssembler::ghash_processBlocks_wide(address field_polynomial, Register FloatRegister p = v31; eor(vzr, T16B, vzr, vzr); // zero register - ldrq(p, field_polynomial); // The field polynomial + // load polynomial via label which must identify local data in the + // same code stub + adr(rscratch1, field_polynomial); + ldrq(p, rscratch1); // The field polynomial ldrq(v0, Address(state)); ldrq(Hprime, Address(subkeyH)); diff --git a/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp b/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp index 3f1a9f7daaa..ffe5afd93cb 100644 --- a/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp @@ -802,7 +802,7 @@ class StubGenerator: public StubCodeGenerator { // // s and d are adjusted to point to the remaining words to copy // - void generate_copy_longs(StubId stub_id, DecoratorSet decorators, Label &start, Register s, Register d, Register count) { + address generate_copy_longs(StubId stub_id, DecoratorSet decorators, Register s, Register d, Register count) { BasicType type; copy_direction direction; @@ -854,7 +854,7 @@ class StubGenerator: public StubCodeGenerator { StubCodeMark mark(this, stub_id); - __ bind(start); + address start = __ pc(); Label unaligned_copy_long; if (AvoidUnalignedAccesses) { @@ -894,9 +894,9 @@ class StubGenerator: public StubCodeGenerator { int prefetch = PrefetchCopyIntervalInBytes; bool use_stride = false; if (direction == copy_backwards) { - use_stride = prefetch > 256; - prefetch = -prefetch; - if (use_stride) __ mov(stride, prefetch); + use_stride = prefetch > 256; + prefetch = -prefetch; + if (use_stride) __ mov(stride, prefetch); } __ bind(again); @@ -1026,9 +1026,9 @@ class StubGenerator: public StubCodeGenerator { int prefetch = PrefetchCopyIntervalInBytes; bool use_stride = false; if (direction == copy_backwards) { - use_stride = prefetch > 256; - prefetch = -prefetch; - if (use_stride) __ mov(stride, prefetch); + use_stride = prefetch > 256; + prefetch = -prefetch; + if (use_stride) __ mov(stride, prefetch); } __ bind(again); @@ -1037,15 +1037,15 @@ class StubGenerator: public StubCodeGenerator { __ prfm(use_stride ? Address(s, stride) : Address(s, prefetch), PLDL1KEEP); if (direction == copy_forwards) { - // allowing for the offset of -8 the store instructions place - // registers into the target 64 bit block at the following - // offsets - // - // t0 at offset 0 - // t1 at offset 8, t2 at offset 16 - // t3 at offset 24, t4 at offset 32 - // t5 at offset 40, t6 at offset 48 - // t7 at offset 56 + // allowing for the offset of -8 the store instructions place + // registers into the target 64 bit block at the following + // offsets + // + // t0 at offset 0 + // t1 at offset 8, t2 at offset 16 + // t3 at offset 24, t4 at offset 32 + // t5 at offset 40, t6 at offset 48 + // t7 at offset 56 bs.copy_store_at_8(Address(d, 1 * unit), t0); bs.copy_store_at_16(Address(d, 2 * unit), t1, t2); @@ -1057,18 +1057,18 @@ class StubGenerator: public StubCodeGenerator { bs.copy_store_at_8(Address(__ pre(d, 8 * unit)), t7); bs.copy_load_at_16(t6, t7, Address(__ pre(s, 8 * unit))); } else { - // d was not offset when we started so the registers are - // written into the 64 bit block preceding d with the following - // offsets - // - // t1 at offset -8 - // t3 at offset -24, t0 at offset -16 - // t5 at offset -48, t2 at offset -32 - // t7 at offset -56, t4 at offset -48 - // t6 at offset -64 - // - // note that this matches the offsets previously noted for the - // loads + // d was not offset when we started so the registers are + // written into the 64 bit block preceding d with the following + // offsets + // + // t1 at offset -8 + // t3 at offset -24, t0 at offset -16 + // t5 at offset -48, t2 at offset -32 + // t7 at offset -56, t4 at offset -48 + // t6 at offset -64 + // + // note that this matches the offsets previously noted for the + // loads bs.copy_store_at_8(Address(d, 1 * unit), t1); bs.copy_store_at_16(Address(d, 3 * unit), t3, t0); @@ -1109,10 +1109,10 @@ class StubGenerator: public StubCodeGenerator { { Label L1, L2; __ tbz(count, exact_log2(4), L1); - // this is the same as above but copying only 4 longs hence - // with only one intervening stp between the str instructions - // but note that the offsets and registers still follow the - // same pattern + // this is the same as above but copying only 4 longs hence + // with only one intervening stp between the str instructions + // but note that the offsets and registers still follow the + // same pattern bs.copy_load_at_16(t0, t1, Address(s, 2 * unit)); bs.copy_load_at_16(t2, t3, Address(__ pre(s, 4 * unit))); if (direction == copy_forwards) { @@ -1127,10 +1127,10 @@ class StubGenerator: public StubCodeGenerator { __ bind(L1); __ tbz(count, 1, L2); - // this is the same as above but copying only 2 longs hence - // there is no intervening stp between the str instructions - // but note that the offset and register patterns are still - // the same + // this is the same as above but copying only 2 longs hence + // there is no intervening stp between the str instructions + // but note that the offset and register patterns are still + // the same bs.copy_load_at_16(t0, t1, Address(__ pre(s, 2 * unit))); if (direction == copy_forwards) { bs.copy_store_at_8(Address(d, 1 * unit), t0); @@ -1141,18 +1141,20 @@ class StubGenerator: public StubCodeGenerator { } __ bind(L2); - // for forwards copy we need to re-adjust the offsets we - // applied so that s and d are follow the last words written + // for forwards copy we need to re-adjust the offsets we + // applied so that s and d are follow the last words written - if (direction == copy_forwards) { - __ add(s, s, 16); - __ add(d, d, 8); - } + if (direction == copy_forwards) { + __ add(s, s, 16); + __ add(d, d, 8); + } } __ ret(lr); - } + } + + return start; } // Small copy: less than 16 bytes. @@ -1206,10 +1208,6 @@ class StubGenerator: public StubCodeGenerator { } } - Label copy_f, copy_b; - Label copy_obj_f, copy_obj_b; - Label copy_obj_uninit_f, copy_obj_uninit_b; - // All-singing all-dancing memory copy. // // Copy count units of memory from s to d. The size of a unit is @@ -1447,19 +1445,19 @@ class StubGenerator: public StubCodeGenerator { } if (direction == copy_forwards) { if (type != T_OBJECT) { - __ bl(copy_f); + __ bl(StubRoutines::aarch64::copy_byte_f()); } else if ((decorators & IS_DEST_UNINITIALIZED) != 0) { - __ bl(copy_obj_uninit_f); + __ bl(StubRoutines::aarch64::copy_oop_uninit_f()); } else { - __ bl(copy_obj_f); + __ bl(StubRoutines::aarch64::copy_oop_f()); } } else { if (type != T_OBJECT) { - __ bl(copy_b); + __ bl(StubRoutines::aarch64::copy_byte_b()); } else if ((decorators & IS_DEST_UNINITIALIZED) != 0) { - __ bl(copy_obj_uninit_b); + __ bl(StubRoutines::aarch64::copy_oop_uninit_b()); } else { - __ bl(copy_obj_b); + __ bl(StubRoutines::aarch64::copy_oop_b()); } } @@ -1522,11 +1520,11 @@ class StubGenerator: public StubCodeGenerator { // the hardware handle it. The two dwords within qwords that span // cache line boundaries will still be loaded and stored atomically. // - // Side Effects: entry is set to the (post push) entry point so it - // can be used by the corresponding conjoint copy - // method + // Side Effects: nopush_entry is set to the (post push) entry point + // so it can be used by the corresponding conjoint + // copy method // - address generate_disjoint_copy(StubId stub_id, address *entry) { + address generate_disjoint_copy(StubId stub_id, address *nopush_entry) { Register s = c_rarg0, d = c_rarg1, count = c_rarg2; RegSet saved_reg = RegSet::of(s, d, count); int size; @@ -1615,8 +1613,8 @@ class StubGenerator: public StubCodeGenerator { address start = __ pc(); __ enter(); - if (entry != nullptr) { - *entry = __ pc(); + if (nopush_entry != nullptr) { + *nopush_entry = __ pc(); // caller can pass a 64-bit byte count here (from Unsafe.copyMemory) BLOCK_COMMENT("Entry:"); } @@ -1679,10 +1677,10 @@ class StubGenerator: public StubCodeGenerator { // cache line boundaries will still be loaded and stored atomically. // // Side Effects: - // entry is set to the no-overlap entry point so it can be used by - // some other conjoint copy method + // nopush_entry is set to the no-overlap entry point so it can be + // used by some other conjoint copy method // - address generate_conjoint_copy(StubId stub_id, address nooverlap_target, address *entry) { + address generate_conjoint_copy(StubId stub_id, address nooverlap_target, address *nopush_entry) { Register s = c_rarg0, d = c_rarg1, count = c_rarg2; RegSet saved_regs = RegSet::of(s, d, count); int size; @@ -1769,16 +1767,19 @@ class StubGenerator: public StubCodeGenerator { address start = __ pc(); __ enter(); - if (entry != nullptr) { - *entry = __ pc(); + if (nopush_entry != nullptr) { + *nopush_entry = __ pc(); // caller can pass a 64-bit byte count here (from Unsafe.copyMemory) BLOCK_COMMENT("Entry:"); } // use fwd copy when (d-s) above_equal (count*size) + Label L_overlapping; __ sub(rscratch1, d, s); __ cmp(rscratch1, count, Assembler::LSL, exact_log2(size)); - __ br(Assembler::HS, nooverlap_target); + __ br(Assembler::LO, L_overlapping); + __ b(RuntimeAddress(nooverlap_target)); + __ bind(L_overlapping); DecoratorSet decorators = IN_HEAP | IS_ARRAY; if (dest_uninitialized) { @@ -1850,7 +1851,7 @@ class StubGenerator: public StubCodeGenerator { // r0 == 0 - success // r0 == -1^K - failure, where K is partial transfer count // - address generate_checkcast_copy(StubId stub_id, address *entry) { + address generate_checkcast_copy(StubId stub_id, address *nopush_entry) { bool dest_uninitialized; switch (stub_id) { case StubId::stubgen_checkcast_arraycopy_id: @@ -1911,8 +1912,8 @@ class StubGenerator: public StubCodeGenerator { #endif //ASSERT // Caller of this entry point must set up the argument registers. - if (entry != nullptr) { - *entry = __ pc(); + if (nopush_entry != nullptr) { + *nopush_entry = __ pc(); BLOCK_COMMENT("Entry:"); } @@ -2724,13 +2725,21 @@ class StubGenerator: public StubCodeGenerator { } void generate_arraycopy_stubs() { - address entry; - address entry_jbyte_arraycopy; - address entry_jshort_arraycopy; - address entry_jint_arraycopy; - address entry_oop_arraycopy; - address entry_jlong_arraycopy; - address entry_checkcast_arraycopy; + // Some copy stubs publish a normal entry and then a 2nd 'fallback' + // entry immediately following their stack push. This can be used + // as a post-push branch target for compatible stubs when they + // identify a special case that can be handled by the fallback + // stub e.g a disjoint copy stub may be use as a special case + // fallback for its compatible conjoint copy stub. + // + // A no push entry is always returned in the following local and + // then published by assigning to the appropriate entry field in + // class StubRoutines. The entry value is then passed to the + // generator for the compatible stub. That means the entry must be + // listed when saving to/restoring from the AOT cache, ensuring + // that the inter-stub jumps are noted at AOT-cache save and + // relocated at AOT cache load. + address nopush_entry; // generate the common exit first so later stubs can rely on it if // they want an UnsafeMemoryAccess exit non-local to the stub @@ -2738,83 +2747,123 @@ class StubGenerator: public StubCodeGenerator { // register the stub as the default exit with class UnsafeMemoryAccess UnsafeMemoryAccess::set_common_exit_stub_pc(StubRoutines::_unsafecopy_common_exit); - generate_copy_longs(StubId::stubgen_copy_byte_f_id, IN_HEAP | IS_ARRAY, copy_f, r0, r1, r15); - generate_copy_longs(StubId::stubgen_copy_byte_b_id, IN_HEAP | IS_ARRAY, copy_b, r0, r1, r15); + // generate and publish arch64-specific bulk copy routines first + // so we can call them from other copy stubs + StubRoutines::aarch64::_copy_byte_f = generate_copy_longs(StubId::stubgen_copy_byte_f_id, IN_HEAP | IS_ARRAY, r0, r1, r15); + StubRoutines::aarch64::_copy_byte_b = generate_copy_longs(StubId::stubgen_copy_byte_b_id, IN_HEAP | IS_ARRAY, r0, r1, r15); - generate_copy_longs(StubId::stubgen_copy_oop_f_id, IN_HEAP | IS_ARRAY, copy_obj_f, r0, r1, r15); - generate_copy_longs(StubId::stubgen_copy_oop_b_id, IN_HEAP | IS_ARRAY, copy_obj_b, r0, r1, r15); + StubRoutines::aarch64::_copy_oop_f = generate_copy_longs(StubId::stubgen_copy_oop_f_id, IN_HEAP | IS_ARRAY, r0, r1, r15); + StubRoutines::aarch64::_copy_oop_b = generate_copy_longs(StubId::stubgen_copy_oop_b_id, IN_HEAP | IS_ARRAY, r0, r1, r15); - generate_copy_longs(StubId::stubgen_copy_oop_uninit_f_id, IN_HEAP | IS_ARRAY | IS_DEST_UNINITIALIZED, copy_obj_uninit_f, r0, r1, r15); - generate_copy_longs(StubId::stubgen_copy_oop_uninit_b_id, IN_HEAP | IS_ARRAY | IS_DEST_UNINITIALIZED, copy_obj_uninit_b, r0, r1, r15); + StubRoutines::aarch64::_copy_oop_uninit_f = generate_copy_longs(StubId::stubgen_copy_oop_uninit_f_id, IN_HEAP | IS_ARRAY | IS_DEST_UNINITIALIZED, r0, r1, r15); + StubRoutines::aarch64::_copy_oop_uninit_b = generate_copy_longs(StubId::stubgen_copy_oop_uninit_b_id, IN_HEAP | IS_ARRAY | IS_DEST_UNINITIALIZED, r0, r1, r15); StubRoutines::aarch64::_zero_blocks = generate_zero_blocks(); //*** jbyte // Always need aligned and unaligned versions - StubRoutines::_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jbyte_disjoint_arraycopy_id, &entry); - StubRoutines::_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_jbyte_arraycopy_id, entry, &entry_jbyte_arraycopy); - StubRoutines::_arrayof_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jbyte_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jbyte_arraycopy_id, entry, nullptr); + StubRoutines::_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jbyte_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jbyte_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_jbyte_arraycopy_id, StubRoutines::_jbyte_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jbyte_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jbyte_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jbyte_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jbyte_arraycopy_id, StubRoutines::_arrayof_jbyte_disjoint_arraycopy_nopush, nullptr); //*** jshort // Always need aligned and unaligned versions - StubRoutines::_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jshort_disjoint_arraycopy_id, &entry); - StubRoutines::_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_jshort_arraycopy_id, entry, &entry_jshort_arraycopy); - StubRoutines::_arrayof_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jshort_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jshort_arraycopy_id, entry, nullptr); + StubRoutines::_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jshort_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jshort_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_jshort_arraycopy_id, StubRoutines::_jshort_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is used by generic/unsafe copy + StubRoutines::_jshort_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jshort_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jshort_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jshort_arraycopy_id, StubRoutines::_arrayof_jshort_disjoint_arraycopy_nopush, nullptr); //*** jint // Aligned versions - StubRoutines::_arrayof_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jint_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jint_arraycopy_id, entry, &entry_jint_arraycopy); + StubRoutines::_arrayof_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jint_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jint_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jint_arraycopy_id, StubRoutines::_arrayof_jint_disjoint_arraycopy_nopush, nullptr); // In 64 bit we need both aligned and unaligned versions of jint arraycopy. - // entry_jint_arraycopy always points to the unaligned version - StubRoutines::_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jint_disjoint_arraycopy_id, &entry); - StubRoutines::_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_jint_arraycopy_id, entry, &entry_jint_arraycopy); + // jint_arraycopy_nopush always points to the unaligned version + StubRoutines::_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jint_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jint_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_jint_arraycopy_id, StubRoutines::_jint_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jint_arraycopy_nopush = nopush_entry; //*** jlong // It is always aligned - StubRoutines::_arrayof_jlong_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jlong_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jlong_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jlong_arraycopy_id, entry, &entry_jlong_arraycopy); + StubRoutines::_arrayof_jlong_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jlong_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jlong_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jlong_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jlong_arraycopy_id, StubRoutines::_arrayof_jlong_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jlong_arraycopy_nopush = nopush_entry; + // disjoint normal/nopush and conjoint normal entries are not + // generated since the arrayof versions are the same StubRoutines::_jlong_disjoint_arraycopy = StubRoutines::_arrayof_jlong_disjoint_arraycopy; + StubRoutines::_jlong_disjoint_arraycopy_nopush = StubRoutines::_arrayof_jlong_disjoint_arraycopy_nopush; StubRoutines::_jlong_arraycopy = StubRoutines::_arrayof_jlong_arraycopy; //*** oops { - // With compressed oops we need unaligned versions; notice that - // we overwrite entry_oop_arraycopy. - bool aligned = !UseCompressedOops; - StubRoutines::_arrayof_oop_disjoint_arraycopy - = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_id, &entry); + = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_oop_disjoint_arraycopy_nopush = nopush_entry; StubRoutines::_arrayof_oop_arraycopy - = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_id, entry, &entry_oop_arraycopy); + = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_id, StubRoutines::_arrayof_oop_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint arrayof nopush entry is needed by generic/unsafe copy + StubRoutines::_oop_arraycopy_nopush = nopush_entry; // Aligned versions without pre-barriers StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit - = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_uninit_id, &entry); + = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_uninit_id, &nopush_entry); + // disjoint arrayof+uninit nopush entry is needed by conjoint copy + StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit_nopush = nopush_entry; + // note that we don't need a returned nopush entry because the + // generic/unsafe copy does not cater for uninit arrays. StubRoutines::_arrayof_oop_arraycopy_uninit - = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_uninit_id, entry, nullptr); + = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_uninit_id, StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit_nopush, nullptr); } + // for oop copies reuse arrayof entries for non-arrayof cases StubRoutines::_oop_disjoint_arraycopy = StubRoutines::_arrayof_oop_disjoint_arraycopy; + StubRoutines::_oop_disjoint_arraycopy_nopush = StubRoutines::_arrayof_oop_disjoint_arraycopy_nopush; StubRoutines::_oop_arraycopy = StubRoutines::_arrayof_oop_arraycopy; StubRoutines::_oop_disjoint_arraycopy_uninit = StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit; + StubRoutines::_oop_disjoint_arraycopy_uninit_nopush = StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit_nopush; StubRoutines::_oop_arraycopy_uninit = StubRoutines::_arrayof_oop_arraycopy_uninit; - StubRoutines::_checkcast_arraycopy = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_id, &entry_checkcast_arraycopy); + StubRoutines::_checkcast_arraycopy = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_id, &nopush_entry); + // checkcast nopush entry is needed by generic copy + StubRoutines::_checkcast_arraycopy_nopush = nopush_entry; + // note that we don't need a returned nopush entry because the + // generic copy does not cater for uninit arrays. StubRoutines::_checkcast_arraycopy_uninit = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_uninit_id, nullptr); - StubRoutines::_unsafe_arraycopy = generate_unsafe_copy(entry_jbyte_arraycopy, - entry_jshort_arraycopy, - entry_jint_arraycopy, - entry_jlong_arraycopy); + // unsafe arraycopy may fallback on conjoint stubs + StubRoutines::_unsafe_arraycopy = generate_unsafe_copy(StubRoutines::_jbyte_arraycopy_nopush, + StubRoutines::_jshort_arraycopy_nopush, + StubRoutines::_jint_arraycopy_nopush, + StubRoutines::_jlong_arraycopy_nopush); - StubRoutines::_generic_arraycopy = generate_generic_copy(entry_jbyte_arraycopy, - entry_jshort_arraycopy, - entry_jint_arraycopy, - entry_oop_arraycopy, - entry_jlong_arraycopy, - entry_checkcast_arraycopy); + // generic arraycopy may fallback on conjoint stubs + StubRoutines::_generic_arraycopy = generate_generic_copy(StubRoutines::_jbyte_arraycopy_nopush, + StubRoutines::_jshort_arraycopy_nopush, + StubRoutines::_jint_arraycopy_nopush, + StubRoutines::_oop_arraycopy_nopush, + StubRoutines::_jlong_arraycopy_nopush, + StubRoutines::_checkcast_arraycopy_nopush); StubRoutines::_jbyte_fill = generate_fill(StubId::stubgen_jbyte_fill_id); StubRoutines::_jshort_fill = generate_fill(StubId::stubgen_jshort_fill_id); @@ -3402,14 +3451,9 @@ class StubGenerator: public StubCodeGenerator { // counter = c_rarg7 - 16 bytes of CTR // return - number of processed bytes address generate_galoisCounterMode_AESCrypt() { - address ghash_polynomial = __ pc(); - __ emit_int64(0x87); // The low-order bits of the field - // polynomial (i.e. p = z^7+z^2+z+1) - // repeated in the low and high parts of a - // 128-bit vector - __ emit_int64(0x87); + Label ghash_polynomial; // local data generated after code - __ align(CodeEntryAlignment); + __ align(CodeEntryAlignment); StubId stub_id = StubId::stubgen_galoisCounterMode_AESCrypt_id; StubCodeMark mark(this, stub_id); address start = __ pc(); @@ -3514,7 +3558,17 @@ class StubGenerator: public StubCodeGenerator { __ leave(); // required for proper stackwalking of RuntimeStub frame __ ret(lr); - return start; + + // bind label and generate polynomial data + __ align(wordSize * 2); + __ bind(ghash_polynomial); + __ emit_int64(0x87); // The low-order bits of the field + // polynomial (i.e. p = z^7+z^2+z+1) + // repeated in the low and high parts of a + // 128-bit vector + __ emit_int64(0x87); + + return start; } class Cached64Bytes { @@ -4559,16 +4613,6 @@ class StubGenerator: public StubCodeGenerator { // by the second lane from all vectors and so on. address generate_chacha20Block_blockpar() { Label L_twoRounds, L_cc20_const; - // The constant data is broken into two 128-bit segments to be loaded - // onto FloatRegisters. The first 128 bits are a counter add overlay - // that adds +0/+1/+2/+3 to the vector holding replicated state[12]. - // The second 128-bits is a table constant used for 8-bit left rotations. - __ BIND(L_cc20_const); - __ emit_int64(0x0000000100000000UL); - __ emit_int64(0x0000000300000002UL); - __ emit_int64(0x0605040702010003UL); - __ emit_int64(0x0E0D0C0F0A09080BUL); - __ align(CodeEntryAlignment); StubId stub_id = StubId::stubgen_chacha20Block_id; StubCodeMark mark(this, stub_id); @@ -4716,6 +4760,17 @@ class StubGenerator: public StubCodeGenerator { __ leave(); __ ret(lr); + // bind label and generate local constant data used by this stub + // The constant data is broken into two 128-bit segments to be loaded + // onto FloatRegisters. The first 128 bits are a counter add overlay + // that adds +0/+1/+2/+3 to the vector holding replicated state[12]. + // The second 128-bits is a table constant used for 8-bit left rotations. + __ BIND(L_cc20_const); + __ emit_int64(0x0000000100000000UL); + __ emit_int64(0x0000000300000002UL); + __ emit_int64(0x0605040702010003UL); + __ emit_int64(0x0E0D0C0F0A09080BUL); + return start; } @@ -6036,10 +6091,6 @@ class StubGenerator: public StubCodeGenerator { address generate_kyber12To16() { Label L_F00, L_loop, L_end; - __ BIND(L_F00); - __ emit_int64(0x0f000f000f000f00); - __ emit_int64(0x0f000f000f000f00); - __ align(CodeEntryAlignment); StubId stub_id = StubId::stubgen_kyber12To16_id; StubCodeMark mark(this, stub_id); @@ -6233,6 +6284,11 @@ class StubGenerator: public StubCodeGenerator { __ mov(r0, zr); // return 0 __ ret(lr); + // bind label and generate constant data used by this stub + __ BIND(L_F00); + __ emit_int64(0x0f000f000f000f00); + __ emit_int64(0x0f000f000f000f00); + return start; } @@ -9642,14 +9698,7 @@ class StubGenerator: public StubCodeGenerator { StubId stub_id = StubId::stubgen_ghash_processBlocks_id; StubCodeMark mark(this, stub_id); - __ align(wordSize * 2); - address p = __ pc(); - __ emit_int64(0x87); // The low-order bits of the field - // polynomial (i.e. p = z^7+z^2+z+1) - // repeated in the low and high parts of a - // 128-bit vector - __ emit_int64(0x87); - + Label polynomial; // local data generated at end of stub __ align(CodeEntryAlignment); address start = __ pc(); @@ -9661,7 +9710,8 @@ class StubGenerator: public StubCodeGenerator { FloatRegister vzr = v30; __ eor(vzr, __ T16B, vzr, vzr); // zero register - __ ldrq(v24, p); // The field polynomial + __ adr(rscratch1, polynomial); + __ ldrq(v24, rscratch1); // The field polynomial __ ldrq(v0, Address(state)); __ ldrq(v1, Address(subkeyH)); @@ -9701,6 +9751,15 @@ class StubGenerator: public StubCodeGenerator { __ st1(v0, __ T16B, state); __ ret(lr); + // bind label and generate local polynomial data + __ align(wordSize * 2); + __ bind(polynomial); + __ emit_int64(0x87); // The low-order bits of the field + // polynomial (i.e. p = z^7+z^2+z+1) + // repeated in the low and high parts of a + // 128-bit vector + __ emit_int64(0x87); + return start; } @@ -9709,14 +9768,7 @@ class StubGenerator: public StubCodeGenerator { StubId stub_id = StubId::stubgen_ghash_processBlocks_wide_id; StubCodeMark mark(this, stub_id); - __ align(wordSize * 2); - address p = __ pc(); - __ emit_int64(0x87); // The low-order bits of the field - // polynomial (i.e. p = z^7+z^2+z+1) - // repeated in the low and high parts of a - // 128-bit vector - __ emit_int64(0x87); - + Label polynomial; // local data generated after stub __ align(CodeEntryAlignment); address start = __ pc(); @@ -9738,7 +9790,7 @@ class StubGenerator: public StubCodeGenerator { __ st1(v8, v9, v10, v11, __ T16B, Address(sp)); } - __ ghash_processBlocks_wide(p, state, subkeyH, data, blocks, unroll); + __ ghash_processBlocks_wide(polynomial, state, subkeyH, data, blocks, unroll); if (unroll > 1) { // And restore state @@ -9751,7 +9803,17 @@ class StubGenerator: public StubCodeGenerator { __ ret(lr); + // bind label and generate polynomial data + __ align(wordSize * 2); + __ bind(polynomial); + __ emit_int64(0x87); // The low-order bits of the field + // polynomial (i.e. p = z^7+z^2+z+1) + // repeated in the low and high parts of a + // 128-bit vector + __ emit_int64(0x87); + return start; + } void generate_base64_encode_simdround(Register src, Register dst, diff --git a/src/hotspot/cpu/arm/stubGenerator_arm.cpp b/src/hotspot/cpu/arm/stubGenerator_arm.cpp index 2e2e0f7a4b9..a36ad3a0c47 100644 --- a/src/hotspot/cpu/arm/stubGenerator_arm.cpp +++ b/src/hotspot/cpu/arm/stubGenerator_arm.cpp @@ -3011,6 +3011,10 @@ class StubGenerator: public StubCodeGenerator { // Note: the disjoint stubs must be generated first, some of // the conjoint stubs use them. + // Note: chaining of stubs does not rely on branching to an + // auxiliary post-push entry because none of the stubs + // push/pop a frame. + // these need always status in case they are called from generic_arraycopy StubRoutines::_jbyte_disjoint_arraycopy = generate_primitive_copy(StubId::stubgen_jbyte_disjoint_arraycopy_id); StubRoutines::_jshort_disjoint_arraycopy = generate_primitive_copy(StubId::stubgen_jshort_disjoint_arraycopy_id); @@ -3024,6 +3028,7 @@ class StubGenerator: public StubCodeGenerator { StubRoutines::_arrayof_jlong_disjoint_arraycopy = generate_primitive_copy(StubId::stubgen_arrayof_jlong_disjoint_arraycopy_id); StubRoutines::_arrayof_oop_disjoint_arraycopy = generate_oop_copy (StubId::stubgen_arrayof_oop_disjoint_arraycopy_id); + // disjoint copy entry is needed by conjoint copy // these need always status in case they are called from generic_arraycopy StubRoutines::_jbyte_arraycopy = generate_primitive_copy(StubId::stubgen_jbyte_arraycopy_id, StubRoutines::_jbyte_disjoint_arraycopy); StubRoutines::_jshort_arraycopy = generate_primitive_copy(StubId::stubgen_jshort_arraycopy_id, StubRoutines::_jshort_disjoint_arraycopy); diff --git a/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp b/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp index f9f43ade501..948092bbb9a 100644 --- a/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp +++ b/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp @@ -3277,8 +3277,12 @@ class StubGenerator: public StubCodeGenerator { // register the stub as the default exit with class UnsafeMemoryAccess UnsafeMemoryAccess::set_common_exit_stub_pc(StubRoutines::_unsafecopy_common_exit); - // Note: the disjoint stubs must be generated first, some of - // the conjoint stubs use them. + // Note: the disjoint stubs must be generated first, some of the + // conjoint stubs use them. + + // Note: chaining of stubs does not rely on branching to an + // auxiliary post-push entry because none of the stubs + // push/pop a frame. // non-aligned disjoint versions StubRoutines::_jbyte_disjoint_arraycopy = generate_disjoint_byte_copy(StubId::stubgen_jbyte_disjoint_arraycopy_id); diff --git a/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp b/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp index 385c839879c..88961ccd5a4 100644 --- a/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp +++ b/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp @@ -732,8 +732,7 @@ class StubGenerator: public StubCodeGenerator { // // s and d are adjusted to point to the remaining words to copy // - void generate_copy_longs(StubId stub_id, Label &start, - Register s, Register d, Register count) { + address generate_copy_longs(StubId stub_id, Register s, Register d, Register count) { BasicType type; copy_direction direction; switch (stub_id) { @@ -763,7 +762,7 @@ class StubGenerator: public StubCodeGenerator { Label again, drain; StubCodeMark mark(this, stub_id); __ align(CodeEntryAlignment); - __ bind(start); + address start = __ pc(); if (direction == copy_forwards) { __ sub(s, s, bias); @@ -879,9 +878,9 @@ class StubGenerator: public StubCodeGenerator { } __ ret(); - } - Label copy_f, copy_b; + return start; + } typedef void (MacroAssembler::*copy_insn)(Register Rd, const Address &adr, Register temp); @@ -1099,8 +1098,8 @@ class StubGenerator: public StubCodeGenerator { // stub_id - is used to name the stub and identify all details of // how to perform the copy. // - // entry - is assigned to the stub's post push entry point unless - // it is null + // nopush_entry - is assigned to the stub's post push entry point + // unless it is null // // Inputs: // c_rarg0 - source array address @@ -1111,11 +1110,11 @@ class StubGenerator: public StubCodeGenerator { // the hardware handle it. The two dwords within qwords that span // cache line boundaries will still be loaded and stored atomically. // - // Side Effects: entry is set to the (post push) entry point so it - // can be used by the corresponding conjoint copy - // method + // Side Effects: nopush_entry is set to the (post push) entry point + // so it can be used by the corresponding conjoint + // copy method // - address generate_disjoint_copy(StubId stub_id, address* entry) { + address generate_disjoint_copy(StubId stub_id, address* nopush_entry) { size_t size; bool aligned; bool is_oop; @@ -1204,8 +1203,8 @@ class StubGenerator: public StubCodeGenerator { address start = __ pc(); __ enter(); - if (entry != nullptr) { - *entry = __ pc(); + if (nopush_entry != nullptr) { + *nopush_entry = __ pc(); // caller can pass a 64-bit byte count here (from Unsafe.copyMemory) BLOCK_COMMENT("Entry:"); } @@ -1256,8 +1255,8 @@ class StubGenerator: public StubCodeGenerator { // corresponding disjoint copy routine which can be // jumped to if the ranges do not actually overlap // - // entry - is assigned to the stub's post push entry point unless - // it is null + // nopush_entry - is assigned to the stub's post push entry point + // unless it is null // // Inputs: // c_rarg0 - source array address @@ -1269,10 +1268,10 @@ class StubGenerator: public StubCodeGenerator { // cache line boundaries will still be loaded and stored atomically. // // Side Effects: - // entry is set to the no-overlap entry point so it can be used by - // some other conjoint copy method + // nopush_entry is set to the no-overlap entry point so it can be + // used by some other conjoint copy method // - address generate_conjoint_copy(StubId stub_id, address nooverlap_target, address *entry) { + address generate_conjoint_copy(StubId stub_id, address nooverlap_target, address *nopush_entry) { const Register s = c_rarg0, d = c_rarg1, count = c_rarg2; RegSet saved_regs = RegSet::of(s, d, count); int size; @@ -1359,8 +1358,8 @@ class StubGenerator: public StubCodeGenerator { address start = __ pc(); __ enter(); - if (entry != nullptr) { - *entry = __ pc(); + if (nopush_entry != nullptr) { + *nopush_entry = __ pc(); // caller can pass a 64-bit byte count here (from Unsafe.copyMemory) BLOCK_COMMENT("Entry:"); } @@ -1370,7 +1369,7 @@ class StubGenerator: public StubCodeGenerator { __ slli(t1, count, exact_log2(size)); Label L_continue; __ bltu(t0, t1, L_continue); - __ j(nooverlap_target); + __ j(RuntimeAddress(nooverlap_target)); __ bind(L_continue); DecoratorSet decorators = IN_HEAP | IS_ARRAY; @@ -1445,7 +1444,7 @@ class StubGenerator: public StubCodeGenerator { // x10 == 0 - success // x10 == -1^K - failure, where K is partial transfer count // - address generate_checkcast_copy(StubId stub_id, address* entry) { + address generate_checkcast_copy(StubId stub_id, address* nopush_entry) { bool dest_uninitialized; switch (stub_id) { case StubId::stubgen_checkcast_arraycopy_id: @@ -1496,8 +1495,8 @@ class StubGenerator: public StubCodeGenerator { __ enter(); // required for proper stackwalking of RuntimeStub frame // Caller of this entry point must set up the argument registers. - if (entry != nullptr) { - *entry = __ pc(); + if (nopush_entry != nullptr) { + *nopush_entry = __ pc(); BLOCK_COMMENT("Entry:"); } @@ -2294,13 +2293,21 @@ class StubGenerator: public StubCodeGenerator { } void generate_arraycopy_stubs() { - address entry = nullptr; - address entry_jbyte_arraycopy = nullptr; - address entry_jshort_arraycopy = nullptr; - address entry_jint_arraycopy = nullptr; - address entry_oop_arraycopy = nullptr; - address entry_jlong_arraycopy = nullptr; - address entry_checkcast_arraycopy = nullptr; + // Some copy stubs publish a normal entry and then a 2nd 'fallback' + // entry immediately following their stack push. This can be used + // as a post-push branch target for compatible stubs when they + // identify a special case that can be handled by the fallback + // stub e.g a disjoint copy stub may be use as a special case + // fallback for its compatible conjoint copy stub. + // + // A no push entry is always returned in the following local and + // then published by assigning to the appropriate entry field in + // class StubRoutines. The entry value is then passed to the + // generator for the compatible stub. That means the entry must be + // listed when saving to/restoring from the AOT cache, ensuring + // that the inter-stub jumps are noted at AOT-cache save and + // relocated at AOT cache load. + address nopush_entry = nullptr; // generate the common exit first so later stubs can rely on it if // they want an UnsafeMemoryAccess exit non-local to the stub @@ -2308,72 +2315,117 @@ class StubGenerator: public StubCodeGenerator { // register the stub as the default exit with class UnsafeMemoryAccess UnsafeMemoryAccess::set_common_exit_stub_pc(StubRoutines::_unsafecopy_common_exit); - generate_copy_longs(StubId::stubgen_copy_byte_f_id, copy_f, c_rarg0, c_rarg1, t1); - generate_copy_longs(StubId::stubgen_copy_byte_b_id, copy_b, c_rarg0, c_rarg1, t1); + // generate and publish riscv-specific bulk copy routines first + // so we can call them from other copy stubs + StubRoutines::riscv::_copy_byte_f = generate_copy_longs(StubId::stubgen_copy_byte_f_id, c_rarg0, c_rarg1, t1); + StubRoutines::riscv::_copy_byte_b = generate_copy_longs(StubId::stubgen_copy_byte_b_id, c_rarg0, c_rarg1, t1); StubRoutines::riscv::_zero_blocks = generate_zero_blocks(); //*** jbyte // Always need aligned and unaligned versions - StubRoutines::_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jbyte_disjoint_arraycopy_id, &entry); - StubRoutines::_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_jbyte_arraycopy_id, entry, &entry_jbyte_arraycopy); - StubRoutines::_arrayof_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jbyte_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jbyte_arraycopy_id, entry, nullptr); + StubRoutines::_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jbyte_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jbyte_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_jbyte_arraycopy_id, StubRoutines::_jbyte_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jbyte_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jbyte_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jbyte_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jbyte_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jbyte_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jbyte_arraycopy_id, StubRoutines::_arrayof_jbyte_disjoint_arraycopy_nopush, nullptr); //*** jshort // Always need aligned and unaligned versions - StubRoutines::_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jshort_disjoint_arraycopy_id, &entry); - StubRoutines::_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_jshort_arraycopy_id, entry, &entry_jshort_arraycopy); - StubRoutines::_arrayof_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jshort_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jshort_arraycopy_id, entry, nullptr); + StubRoutines::_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jshort_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jshort_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_jshort_arraycopy_id, StubRoutines::_jshort_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is used by generic/unsafe copy + StubRoutines::_jshort_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jshort_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jshort_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jshort_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jshort_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jshort_arraycopy_id, StubRoutines::_arrayof_jshort_disjoint_arraycopy_nopush, nullptr); //*** jint // Aligned versions - StubRoutines::_arrayof_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jint_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jint_arraycopy_id, entry, &entry_jint_arraycopy); + StubRoutines::_arrayof_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jint_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jint_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jint_arraycopy_id, StubRoutines::_arrayof_jint_disjoint_arraycopy_nopush, nullptr); // In 64 bit we need both aligned and unaligned versions of jint arraycopy. // entry_jint_arraycopy always points to the unaligned version - StubRoutines::_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jint_disjoint_arraycopy_id, &entry); - StubRoutines::_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_jint_arraycopy_id, entry, &entry_jint_arraycopy); + StubRoutines::_jint_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_jint_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jint_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jint_arraycopy = generate_conjoint_copy(StubId::stubgen_jint_arraycopy_id, StubRoutines::_jint_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jint_arraycopy_nopush = nopush_entry; //*** jlong // It is always aligned - StubRoutines::_arrayof_jlong_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jlong_disjoint_arraycopy_id, &entry); - StubRoutines::_arrayof_jlong_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jlong_arraycopy_id, entry, &entry_jlong_arraycopy); + StubRoutines::_arrayof_jlong_disjoint_arraycopy = generate_disjoint_copy(StubId::stubgen_arrayof_jlong_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_jlong_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_arrayof_jlong_arraycopy = generate_conjoint_copy(StubId::stubgen_arrayof_jlong_arraycopy_id, StubRoutines::_arrayof_jlong_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jlong_arraycopy_nopush = nopush_entry; + // disjoint normal/nopush and conjoint normal entries are not + // generated since the arrayof versions are the same StubRoutines::_jlong_disjoint_arraycopy = StubRoutines::_arrayof_jlong_disjoint_arraycopy; + StubRoutines::_jlong_disjoint_arraycopy_nopush = StubRoutines::_arrayof_jlong_disjoint_arraycopy_nopush; StubRoutines::_jlong_arraycopy = StubRoutines::_arrayof_jlong_arraycopy; //*** oops StubRoutines::_arrayof_oop_disjoint_arraycopy - = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_id, &entry); + = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_id, &nopush_entry); + // disjoint arrayof nopush entry is needed by conjoint copy + StubRoutines::_arrayof_oop_disjoint_arraycopy_nopush = nopush_entry; StubRoutines::_arrayof_oop_arraycopy - = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_id, entry, &entry_oop_arraycopy); + = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_id, StubRoutines::_arrayof_oop_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint arrayof nopush entry is needed by generic/unsafe copy + StubRoutines::_oop_arraycopy_nopush = nopush_entry; // Aligned versions without pre-barriers StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit - = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_uninit_id, &entry); - StubRoutines::_arrayof_oop_arraycopy_uninit - = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_uninit_id, entry, nullptr); + = generate_disjoint_copy(StubId::stubgen_arrayof_oop_disjoint_arraycopy_uninit_id, &nopush_entry); + // disjoint arrayof+uninit nopush entry is needed by conjoint copy + StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit_nopush = nopush_entry; + // note that we don't need a returned nopush entry because the + // generic/unsafe copy does not cater for uninit arrays. + StubRoutines::_arrayof_oop_arraycopy_uninit + = generate_conjoint_copy(StubId::stubgen_arrayof_oop_arraycopy_uninit_id, StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit_nopush, nullptr); + + // for oop copies reuse arrayof entries for non-arrayof cases StubRoutines::_oop_disjoint_arraycopy = StubRoutines::_arrayof_oop_disjoint_arraycopy; + StubRoutines::_oop_disjoint_arraycopy_nopush = StubRoutines::_arrayof_oop_disjoint_arraycopy_nopush; StubRoutines::_oop_arraycopy = StubRoutines::_arrayof_oop_arraycopy; StubRoutines::_oop_disjoint_arraycopy_uninit = StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit; + StubRoutines::_oop_disjoint_arraycopy_uninit_nopush = StubRoutines::_arrayof_oop_disjoint_arraycopy_uninit_nopush; StubRoutines::_oop_arraycopy_uninit = StubRoutines::_arrayof_oop_arraycopy_uninit; - StubRoutines::_checkcast_arraycopy = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_id, &entry_checkcast_arraycopy); + StubRoutines::_checkcast_arraycopy = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_id, &nopush_entry); + // checkcast nopush entry is needed by generic copy + StubRoutines::_checkcast_arraycopy_nopush = nopush_entry; + // note that we don't need a returned nopush entry because the + // generic copy does not cater for uninit arrays. StubRoutines::_checkcast_arraycopy_uninit = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_uninit_id, nullptr); - StubRoutines::_unsafe_arraycopy = generate_unsafe_copy(entry_jbyte_arraycopy, - entry_jshort_arraycopy, - entry_jint_arraycopy, - entry_jlong_arraycopy); + // unsafe arraycopy may fallback on conjoint stubs + StubRoutines::_unsafe_arraycopy = generate_unsafe_copy(StubRoutines::_jbyte_arraycopy_nopush, + StubRoutines::_jshort_arraycopy_nopush, + StubRoutines::_jint_arraycopy_nopush, + StubRoutines::_jlong_arraycopy_nopush); - StubRoutines::_generic_arraycopy = generate_generic_copy(entry_jbyte_arraycopy, - entry_jshort_arraycopy, - entry_jint_arraycopy, - entry_oop_arraycopy, - entry_jlong_arraycopy, - entry_checkcast_arraycopy); + // generic arraycopy may fallback on conjoint stubs + StubRoutines::_generic_arraycopy = generate_generic_copy(StubRoutines::_jbyte_arraycopy_nopush, + StubRoutines::_jshort_arraycopy_nopush, + StubRoutines::_jint_arraycopy_nopush, + StubRoutines::_oop_arraycopy_nopush, + StubRoutines::_jlong_arraycopy_nopush, + StubRoutines::_checkcast_arraycopy_nopush); StubRoutines::_jbyte_fill = generate_fill(StubId::stubgen_jbyte_fill_id); StubRoutines::_jshort_fill = generate_fill(StubId::stubgen_jshort_fill_id); diff --git a/src/hotspot/cpu/x86/stubGenerator_x86_64_arraycopy.cpp b/src/hotspot/cpu/x86/stubGenerator_x86_64_arraycopy.cpp index 743457f87af..d53fafafdb4 100644 --- a/src/hotspot/cpu/x86/stubGenerator_x86_64_arraycopy.cpp +++ b/src/hotspot/cpu/x86/stubGenerator_x86_64_arraycopy.cpp @@ -76,50 +76,95 @@ static uint& get_profile_ctr(int shift) { #endif // !PRODUCT void StubGenerator::generate_arraycopy_stubs() { - address entry; - address entry_jbyte_arraycopy; - address entry_jshort_arraycopy; - address entry_jint_arraycopy; - address entry_oop_arraycopy; - address entry_jlong_arraycopy; - address entry_checkcast_arraycopy; + // Some copy stubs publish a normal entry and then a 2nd 'fallback' + // entry immediately following their stack push. This can be used + // as a post-push branch target for compatible stubs when they + // identify a special case that can be handled by the fallback + // stub e.g a disjoint copy stub may be use as a special case + // fallback for its compatible conjoint copy stub. + // + // A no push entry is always returned in the following local and + // then published by assigning to the appropriate entry field in + // class StubRoutines. The entry value is then passed to the + // generator for the compatible stub. That means the entry must be + // listed when saving to/restoring from the AOT cache, ensuring + // that the inter-stub jumps are noted at AOT-cache save and + // relocated at AOT cache load. + address nopush_entry; - StubRoutines::_jbyte_disjoint_arraycopy = generate_disjoint_byte_copy(&entry); - StubRoutines::_jbyte_arraycopy = generate_conjoint_byte_copy(entry, &entry_jbyte_arraycopy); + StubRoutines::_jbyte_disjoint_arraycopy = generate_disjoint_byte_copy(&nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jbyte_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jbyte_arraycopy = generate_conjoint_byte_copy(StubRoutines::_jbyte_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jbyte_arraycopy_nopush = nopush_entry; - StubRoutines::_jshort_disjoint_arraycopy = generate_disjoint_short_copy(&entry); - StubRoutines::_jshort_arraycopy = generate_conjoint_short_copy(entry, &entry_jshort_arraycopy); + StubRoutines::_jshort_disjoint_arraycopy = generate_disjoint_short_copy(&nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jshort_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jshort_arraycopy = generate_conjoint_short_copy(StubRoutines::_jshort_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jshort_arraycopy_nopush = nopush_entry; - StubRoutines::_jint_disjoint_arraycopy = generate_disjoint_int_oop_copy(StubId::stubgen_jint_disjoint_arraycopy_id, &entry); - StubRoutines::_jint_arraycopy = generate_conjoint_int_oop_copy(StubId::stubgen_jint_arraycopy_id, entry, &entry_jint_arraycopy); + StubRoutines::_jint_disjoint_arraycopy = generate_disjoint_int_oop_copy(StubId::stubgen_jint_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jint_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jint_arraycopy = generate_conjoint_int_oop_copy(StubId::stubgen_jint_arraycopy_id, StubRoutines::_jint_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jint_arraycopy_nopush = nopush_entry; + + StubRoutines::_jlong_disjoint_arraycopy = generate_disjoint_long_oop_copy(StubId::stubgen_jlong_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_jlong_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_jlong_arraycopy = generate_conjoint_long_oop_copy(StubId::stubgen_jlong_arraycopy_id, StubRoutines::_jlong_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_jlong_arraycopy_nopush = nopush_entry; - StubRoutines::_jlong_disjoint_arraycopy = generate_disjoint_long_oop_copy(StubId::stubgen_jlong_disjoint_arraycopy_id, &entry); - StubRoutines::_jlong_arraycopy = generate_conjoint_long_oop_copy(StubId::stubgen_jlong_arraycopy_id, entry, &entry_jlong_arraycopy); if (UseCompressedOops) { - StubRoutines::_oop_disjoint_arraycopy = generate_disjoint_int_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_id, &entry); - StubRoutines::_oop_arraycopy = generate_conjoint_int_oop_copy(StubId::stubgen_oop_arraycopy_id, entry, &entry_oop_arraycopy); - StubRoutines::_oop_disjoint_arraycopy_uninit = generate_disjoint_int_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_uninit_id, &entry); - StubRoutines::_oop_arraycopy_uninit = generate_conjoint_int_oop_copy(StubId::stubgen_oop_arraycopy_uninit_id, entry, nullptr); + StubRoutines::_oop_disjoint_arraycopy = generate_disjoint_int_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_oop_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_oop_arraycopy = generate_conjoint_int_oop_copy(StubId::stubgen_oop_arraycopy_id, StubRoutines::_oop_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_oop_arraycopy_nopush = nopush_entry; + StubRoutines::_oop_disjoint_arraycopy_uninit = generate_disjoint_int_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_uninit_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_oop_disjoint_arraycopy_uninit_nopush = nopush_entry; + // note that we don't need a returned nopush entry because the + // generic/unsafe copy does not cater for uninit arrays. + StubRoutines::_oop_arraycopy_uninit = generate_conjoint_int_oop_copy(StubId::stubgen_oop_arraycopy_uninit_id, StubRoutines::_oop_disjoint_arraycopy_uninit_nopush, nullptr); } else { - StubRoutines::_oop_disjoint_arraycopy = generate_disjoint_long_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_id, &entry); - StubRoutines::_oop_arraycopy = generate_conjoint_long_oop_copy(StubId::stubgen_oop_arraycopy_id, entry, &entry_oop_arraycopy); - StubRoutines::_oop_disjoint_arraycopy_uninit = generate_disjoint_long_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_uninit_id, &entry); - StubRoutines::_oop_arraycopy_uninit = generate_conjoint_long_oop_copy(StubId::stubgen_oop_arraycopy_uninit_id, entry, nullptr); + StubRoutines::_oop_disjoint_arraycopy = generate_disjoint_long_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_oop_disjoint_arraycopy_nopush = nopush_entry; + StubRoutines::_oop_arraycopy = generate_conjoint_long_oop_copy(StubId::stubgen_oop_arraycopy_id, StubRoutines::_oop_disjoint_arraycopy_nopush, &nopush_entry); + // conjoint nopush entry is needed by generic/unsafe copy + StubRoutines::_oop_arraycopy_nopush = nopush_entry; + StubRoutines::_oop_disjoint_arraycopy_uninit = generate_disjoint_long_oop_copy(StubId::stubgen_oop_disjoint_arraycopy_uninit_id, &nopush_entry); + // disjoint nopush entry is needed by conjoint copy + StubRoutines::_oop_disjoint_arraycopy_uninit_nopush = nopush_entry; + // note that we don't need a returned nopush entry because the + // generic/unsafe copy does not cater for uninit arrays. + StubRoutines::_oop_arraycopy_uninit = generate_conjoint_long_oop_copy(StubId::stubgen_oop_arraycopy_uninit_id, StubRoutines::_oop_disjoint_arraycopy_uninit_nopush, nullptr); } - StubRoutines::_checkcast_arraycopy = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_id, &entry_checkcast_arraycopy); + StubRoutines::_checkcast_arraycopy = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_id, &nopush_entry); + // checkcast nopush entry is needed by generic copy + StubRoutines::_checkcast_arraycopy_nopush = nopush_entry; + // note that we don't need a returned nopush entry because the + // generic copy does not cater for uninit arrays. StubRoutines::_checkcast_arraycopy_uninit = generate_checkcast_copy(StubId::stubgen_checkcast_arraycopy_uninit_id, nullptr); - StubRoutines::_unsafe_arraycopy = generate_unsafe_copy(entry_jbyte_arraycopy, - entry_jshort_arraycopy, - entry_jint_arraycopy, - entry_jlong_arraycopy); - StubRoutines::_generic_arraycopy = generate_generic_copy(entry_jbyte_arraycopy, - entry_jshort_arraycopy, - entry_jint_arraycopy, - entry_oop_arraycopy, - entry_jlong_arraycopy, - entry_checkcast_arraycopy); + StubRoutines::_unsafe_arraycopy = generate_unsafe_copy(StubRoutines::_jbyte_arraycopy_nopush, + StubRoutines::_jshort_arraycopy_nopush, + StubRoutines::_jint_arraycopy_nopush, + StubRoutines::_jlong_arraycopy_nopush); + StubRoutines::_generic_arraycopy = generate_generic_copy(StubRoutines::_jbyte_arraycopy_nopush, + StubRoutines::_jshort_arraycopy_nopush, + StubRoutines::_jint_arraycopy_nopush, + StubRoutines::_oop_arraycopy_nopush, + StubRoutines::_jlong_arraycopy_nopush, + StubRoutines::_checkcast_arraycopy_nopush); StubRoutines::_jbyte_fill = generate_fill(StubId::stubgen_jbyte_fill_id); StubRoutines::_jshort_fill = generate_fill(StubId::stubgen_jshort_fill_id); diff --git a/src/hotspot/share/runtime/stubDeclarations.hpp b/src/hotspot/share/runtime/stubDeclarations.hpp index c79caa12a6c..b051c6d0e18 100644 --- a/src/hotspot/share/runtime/stubDeclarations.hpp +++ b/src/hotspot/share/runtime/stubDeclarations.hpp @@ -891,18 +891,28 @@ do_stub(final, jbyte_arraycopy) \ do_entry_init(final, jbyte_arraycopy, jbyte_arraycopy, \ jbyte_arraycopy, StubRoutines::jbyte_copy) \ + do_entry(final, jbyte_arraycopy, jbyte_arraycopy_nopush, \ + jbyte_arraycopy_nopush) \ do_stub(final, jshort_arraycopy) \ do_entry_init(final, jshort_arraycopy, jshort_arraycopy, \ jshort_arraycopy, StubRoutines::jshort_copy) \ + do_entry(final, jshort_arraycopy, jshort_arraycopy_nopush, \ + jshort_arraycopy_nopush) \ do_stub(final, jint_arraycopy) \ do_entry_init(final, jint_arraycopy, jint_arraycopy, \ jint_arraycopy, StubRoutines::jint_copy) \ + do_entry(final, jint_arraycopy, jint_arraycopy_nopush, \ + jint_arraycopy_nopush) \ do_stub(final, jlong_arraycopy) \ do_entry_init(final, jlong_arraycopy, jlong_arraycopy, \ jlong_arraycopy, StubRoutines::jlong_copy) \ + do_entry(final, jlong_arraycopy, jlong_arraycopy_nopush, \ + jlong_arraycopy_nopush) \ do_stub(final, oop_arraycopy) \ do_entry_init(final, oop_arraycopy, oop_arraycopy, \ oop_arraycopy_entry, StubRoutines::oop_copy) \ + do_entry(final, oop_arraycopy, oop_arraycopy_nopush, \ + oop_arraycopy_nopush) \ do_stub(final, oop_arraycopy_uninit) \ do_entry_init(final, oop_arraycopy_uninit, oop_arraycopy_uninit, \ oop_arraycopy_uninit_entry, \ @@ -911,26 +921,44 @@ do_entry_init(final, jbyte_disjoint_arraycopy, \ jbyte_disjoint_arraycopy, jbyte_disjoint_arraycopy, \ StubRoutines::jbyte_copy) \ + do_entry(final, jbyte_disjoint_arraycopy, \ + jbyte_disjoint_arraycopy_nopush, \ + jbyte_disjoint_arraycopy_nopush) \ do_stub(final, jshort_disjoint_arraycopy) \ do_entry_init(final, jshort_disjoint_arraycopy, \ jshort_disjoint_arraycopy, jshort_disjoint_arraycopy, \ StubRoutines::jshort_copy) \ + do_entry(final, jshort_disjoint_arraycopy, \ + jshort_disjoint_arraycopy_nopush, \ + jshort_disjoint_arraycopy_nopush) \ do_stub(final, jint_disjoint_arraycopy) \ do_entry_init(final, jint_disjoint_arraycopy, \ jint_disjoint_arraycopy, jint_disjoint_arraycopy, \ StubRoutines::jint_copy) \ + do_entry(final, jint_disjoint_arraycopy, \ + jint_disjoint_arraycopy_nopush, \ + jint_disjoint_arraycopy_nopush) \ do_stub(final, jlong_disjoint_arraycopy) \ do_entry_init(final, jlong_disjoint_arraycopy, \ jlong_disjoint_arraycopy, jlong_disjoint_arraycopy, \ StubRoutines::jlong_copy) \ + do_entry(final, jlong_disjoint_arraycopy, \ + jlong_disjoint_arraycopy_nopush, \ + jlong_disjoint_arraycopy_nopush) \ do_stub(final, oop_disjoint_arraycopy) \ do_entry_init(final, oop_disjoint_arraycopy, oop_disjoint_arraycopy, \ oop_disjoint_arraycopy_entry, StubRoutines::oop_copy) \ + do_entry(final, oop_disjoint_arraycopy, \ + oop_disjoint_arraycopy_nopush, \ + oop_disjoint_arraycopy_nopush) \ do_stub(final, oop_disjoint_arraycopy_uninit) \ do_entry_init(final, oop_disjoint_arraycopy_uninit, \ oop_disjoint_arraycopy_uninit, \ oop_disjoint_arraycopy_uninit_entry, \ StubRoutines::oop_copy_uninit) \ + do_entry(final, oop_disjoint_arraycopy_uninit, \ + oop_disjoint_arraycopy_uninit_nopush, \ + oop_disjoint_arraycopy_uninit_nopush) \ do_stub(final, arrayof_jbyte_arraycopy) \ do_entry_init(final, arrayof_jbyte_arraycopy, \ arrayof_jbyte_arraycopy, arrayof_jbyte_arraycopy, \ @@ -960,34 +988,54 @@ arrayof_jbyte_disjoint_arraycopy, \ arrayof_jbyte_disjoint_arraycopy, \ StubRoutines::arrayof_jbyte_copy) \ + do_entry(final, arrayof_jbyte_disjoint_arraycopy, \ + arrayof_jbyte_disjoint_arraycopy_nopush, \ + arrayof_jbyte_disjoint_arraycopy_nopush) \ do_stub(final, arrayof_jshort_disjoint_arraycopy) \ do_entry_init(final, arrayof_jshort_disjoint_arraycopy, \ arrayof_jshort_disjoint_arraycopy, \ arrayof_jshort_disjoint_arraycopy, \ StubRoutines::arrayof_jshort_copy) \ + do_entry(final, arrayof_jshort_disjoint_arraycopy, \ + arrayof_jshort_disjoint_arraycopy_nopush, \ + arrayof_jshort_disjoint_arraycopy_nopush) \ do_stub(final, arrayof_jint_disjoint_arraycopy) \ do_entry_init(final, arrayof_jint_disjoint_arraycopy, \ arrayof_jint_disjoint_arraycopy, \ arrayof_jint_disjoint_arraycopy, \ StubRoutines::arrayof_jint_copy) \ + do_entry(final, arrayof_jint_disjoint_arraycopy, \ + arrayof_jint_disjoint_arraycopy_nopush, \ + arrayof_jint_disjoint_arraycopy_nopush) \ do_stub(final, arrayof_jlong_disjoint_arraycopy) \ do_entry_init(final, arrayof_jlong_disjoint_arraycopy, \ arrayof_jlong_disjoint_arraycopy, \ arrayof_jlong_disjoint_arraycopy, \ StubRoutines::arrayof_jlong_copy) \ + do_entry(final, arrayof_jlong_disjoint_arraycopy, \ + arrayof_jlong_disjoint_arraycopy_nopush, \ + arrayof_jlong_disjoint_arraycopy_nopush) \ do_stub(final, arrayof_oop_disjoint_arraycopy) \ do_entry_init(final, arrayof_oop_disjoint_arraycopy, \ arrayof_oop_disjoint_arraycopy, \ arrayof_oop_disjoint_arraycopy_entry, \ StubRoutines::arrayof_oop_copy) \ + do_entry(final, arrayof_oop_disjoint_arraycopy, \ + arrayof_oop_disjoint_arraycopy_nopush, \ + arrayof_oop_disjoint_arraycopy_nopush) \ do_stub(final, arrayof_oop_disjoint_arraycopy_uninit) \ do_entry_init(final, arrayof_oop_disjoint_arraycopy_uninit, \ arrayof_oop_disjoint_arraycopy_uninit, \ arrayof_oop_disjoint_arraycopy_uninit_entry, \ StubRoutines::arrayof_oop_copy_uninit) \ + do_entry(final, arrayof_oop_disjoint_arraycopy_uninit, \ + arrayof_oop_disjoint_arraycopy_uninit_nopush, \ + arrayof_oop_disjoint_arraycopy_uninit_nopush) \ do_stub(final, checkcast_arraycopy) \ do_entry(final, checkcast_arraycopy, checkcast_arraycopy, \ checkcast_arraycopy_entry) \ + do_entry(final, checkcast_arraycopy, checkcast_arraycopy_nopush, \ + checkcast_arraycopy_nopush) \ do_stub(final, checkcast_arraycopy_uninit) \ do_entry(final, checkcast_arraycopy_uninit, \ checkcast_arraycopy_uninit, \ From 6df01178c03968bee7994eddd187f790c74ba541 Mon Sep 17 00:00:00 2001 From: Saranya Natarajan Date: Wed, 17 Sep 2025 09:45:30 +0000 Subject: [PATCH 076/556] 8356779: IGV: dump the index of the SafePointNode containing the current JVMS during parsing Reviewed-by: epeter, chagedorn, qamai --- src/hotspot/share/opto/parse2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hotspot/share/opto/parse2.cpp b/src/hotspot/share/opto/parse2.cpp index 04b6e49b620..8b44d8b3491 100644 --- a/src/hotspot/share/opto/parse2.cpp +++ b/src/hotspot/share/opto/parse2.cpp @@ -2779,7 +2779,7 @@ void Parse::do_one_bytecode() { if (C->should_print_igv(perBytecode)) { IdealGraphPrinter* printer = C->igv_printer(); char buffer[256]; - jio_snprintf(buffer, sizeof(buffer), "Bytecode %d: %s", bci(), Bytecodes::name(bc())); + jio_snprintf(buffer, sizeof(buffer), "Bytecode %d: %s, map: %d", bci(), Bytecodes::name(bc()), map() == nullptr ? -1 : map()->_idx); bool old = printer->traverse_outs(); printer->set_traverse_outs(true); printer->print_graph(buffer); From c28142e7c142b2938823451c1f638f56a7f969d2 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Wed, 17 Sep 2025 10:26:26 +0000 Subject: [PATCH 077/556] 8367737: Parallel: Retry allocation after lock acquire in mem_allocate_work Reviewed-by: fandreuzzi, tschatzl, iwalulya --- .../gc/parallel/parallelScavengeHeap.cpp | 54 +++++++++++-------- .../gc/parallel/parallelScavengeHeap.hpp | 1 + 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp index 81412e2f614..213e8f95d63 100644 --- a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp +++ b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp @@ -275,38 +275,46 @@ HeapWord* ParallelScavengeHeap::mem_allocate(size_t size) { return mem_allocate_work(size, is_tlab); } +HeapWord* ParallelScavengeHeap::mem_allocate_cas_noexpand(size_t size, bool is_tlab) { + // Try young-gen first. + HeapWord* result = young_gen()->allocate(size); + if (result != nullptr) { + return result; + } + + // Try allocating from the old gen for non-TLAB in certain scenarios. + if (!is_tlab) { + if (!should_alloc_in_eden(size) || _is_heap_almost_full) { + result = old_gen()->cas_allocate_noexpand(size); + if (result != nullptr) { + return result; + } + } + } + + return nullptr; +} + HeapWord* ParallelScavengeHeap::mem_allocate_work(size_t size, bool is_tlab) { for (uint loop_count = 0; /* empty */; ++loop_count) { - // Try young-gen first. - HeapWord* result = young_gen()->allocate(size); + HeapWord* result = mem_allocate_cas_noexpand(size, is_tlab); if (result != nullptr) { return result; } - // Try allocating from the old gen for non-TLAB in certain scenarios. - if (!is_tlab) { - if (!should_alloc_in_eden(size) || _is_heap_almost_full) { - result = old_gen()->cas_allocate_noexpand(size); - if (result != nullptr) { - return result; - } - } - } - - // We don't want to have multiple collections for a single filled generation. - // To prevent this, each thread tracks the total_collections() value, and if - // the count has changed, does not do a new collection. - // - // The collection count must be read only while holding the heap lock. VM - // operations also hold the heap lock during collections. There is a lock - // contention case where thread A blocks waiting on the Heap_lock, while - // thread B is holding it doing a collection. When thread A gets the lock, - // the collection count has already changed. To prevent duplicate collections, - // The policy MUST attempt allocations during the same period it reads the - // total_collections() value! + // Read total_collections() under the lock so that multiple + // allocation-failures result in one GC. uint gc_count; { MutexLocker ml(Heap_lock); + + // Re-try after acquiring the lock, because a GC might have occurred + // while waiting for this lock. + result = mem_allocate_cas_noexpand(size, is_tlab); + if (result != nullptr) { + return result; + } + gc_count = total_collections(); } diff --git a/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp b/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp index b1176a1637b..fea827430ca 100644 --- a/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp +++ b/src/hotspot/share/gc/parallel/parallelScavengeHeap.hpp @@ -100,6 +100,7 @@ class ParallelScavengeHeap : public CollectedHeap { inline bool should_alloc_in_eden(size_t size) const; + HeapWord* mem_allocate_cas_noexpand(size_t size, bool is_tlab); HeapWord* mem_allocate_work(size_t size, bool is_tlab); HeapWord* expand_heap_and_allocate(size_t size, bool is_tlab); From 4719ed671a8a8e10b77c4748a0e1ee63c19dfefb Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Wed, 17 Sep 2025 11:25:49 +0000 Subject: [PATCH 078/556] 8366777: Build fails unknown pseudo-op with old AS on linux-aarch64 Reviewed-by: erikj, ihse --- make/autoconf/flags-cflags.m4 | 42 -------------------------- make/autoconf/flags-other.m4 | 56 +++++++++++++++++++++++++++++++++++ make/autoconf/flags.m4 | 1 + 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/make/autoconf/flags-cflags.m4 b/make/autoconf/flags-cflags.m4 index 6072cbc74dd..0e2825d14b0 100644 --- a/make/autoconf/flags-cflags.m4 +++ b/make/autoconf/flags-cflags.m4 @@ -934,48 +934,6 @@ AC_DEFUN([FLAGS_SETUP_CFLAGS_CPU_DEP], IF_FALSE: [$2FDLIBM_CFLAGS=""]) fi AC_SUBST($2FDLIBM_CFLAGS) - - # Check whether the compiler supports the Arm C Language Extensions (ACLE) - # for SVE. Set SVE_CFLAGS to -march=armv8-a+sve if it does. - # ACLE and this flag are required to build the aarch64 SVE related functions in - # libvectormath. Apple Silicon does not support SVE; use macOS as a proxy for - # that check. - if test "x$OPENJDK_TARGET_CPU" = "xaarch64" && test "x$OPENJDK_TARGET_OS" = "xlinux"; then - if test "x$TOOLCHAIN_TYPE" = xgcc || test "x$TOOLCHAIN_TYPE" = xclang; then - AC_LANG_PUSH(C) - OLD_CFLAGS="$CFLAGS" - CFLAGS="$CFLAGS -march=armv8-a+sve" - AC_MSG_CHECKING([if Arm SVE ACLE is supported]) - AC_COMPILE_IFELSE([AC_LANG_PROGRAM([#include ], - [ - svint32_t r = svdup_n_s32(1); - return 0; - ])], - [ - AC_MSG_RESULT([yes]) - $2SVE_CFLAGS="-march=armv8-a+sve" - # Switching the initialization mode with gcc from 'pattern' to 'zero' - # avoids the use of unsupported `__builtin_clear_padding` for variable - # length aggregates - if test "x$DEBUG_LEVEL" != xrelease && test "x$TOOLCHAIN_TYPE" = xgcc ; then - INIT_ZERO_FLAG="-ftrivial-auto-var-init=zero" - FLAGS_COMPILER_CHECK_ARGUMENTS(ARGUMENT: [$INIT_ZERO_FLAG], - IF_TRUE: [ - $2SVE_CFLAGS="${$2SVE_CFLAGS} $INIT_ZERO_FLAG" - ] - ) - fi - ], - [ - AC_MSG_RESULT([no]) - $2SVE_CFLAGS="" - ] - ) - CFLAGS="$OLD_CFLAGS" - AC_LANG_POP(C) - fi - fi - AC_SUBST($2SVE_CFLAGS) ]) AC_DEFUN_ONCE([FLAGS_SETUP_BRANCH_PROTECTION], diff --git a/make/autoconf/flags-other.m4 b/make/autoconf/flags-other.m4 index 9d41cf04791..4570f6ede78 100644 --- a/make/autoconf/flags-other.m4 +++ b/make/autoconf/flags-other.m4 @@ -107,6 +107,62 @@ AC_DEFUN([FLAGS_SETUP_NMFLAGS], AC_SUBST(NMFLAGS) ]) +# Check whether the compiler supports the Arm C Language Extensions (ACLE) +# for SVE. Set SVE_CFLAGS to -march=armv8-a+sve if it does. +# ACLE and this flag are required to build the aarch64 SVE related functions +# in libvectormath. +AC_DEFUN([FLAGS_SETUP_SVE], +[ + AARCH64_SVE_AVAILABLE=false + # Apple Silicon does not support SVE; use macOS as a proxy for that check. + if test "x$OPENJDK_TARGET_CPU" = "xaarch64" && test "x$OPENJDK_TARGET_OS" = "xlinux"; then + if test "x$TOOLCHAIN_TYPE" = xgcc || test "x$TOOLCHAIN_TYPE" = xclang; then + # check the compiler and binutils support sve or not + AC_MSG_CHECKING([if Arm SVE ACLE is supported]) + AC_LANG_PUSH([C]) + saved_cflags="$CFLAGS" + CFLAGS="$CFLAGS -march=armv8-a+sve $CFLAGS_WARNINGS_ARE_ERRORS ARG_ARGUMENT" + AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [ + #include + svfloat64_t a() {} + ], + [ + svint32_t r = svdup_n_s32(1) + ])], + [ + AARCH64_SVE_AVAILABLE=true + ] + ) + CFLAGS="$saved_cflags" + AC_LANG_POP([C]) + AC_MSG_RESULT([$AARCH64_SVE_AVAILABLE]) + fi + fi + + UTIL_ARG_ENABLE(NAME: aarch64-sve, DEFAULT: auto, + RESULT: AARCH64_SVE_ENABLED, + DESC: [Use SVE when compiling libsleef], + AVAILABLE: $AARCH64_SVE_AVAILABLE) + SVE_CFLAGS="" + if test "x$AARCH64_SVE_ENABLED" = xtrue; then + SVE_CFLAGS="-march=armv8-a+sve" + # Switching the initialization mode with gcc from 'pattern' to 'zero' + # avoids the use of unsupported `__builtin_clear_padding` for variable + # length aggregates + if test "x$DEBUG_LEVEL" != xrelease && test "x$TOOLCHAIN_TYPE" = xgcc ; then + AC_MSG_CHECKING([Switching the initialization mode with gcc from pattern to zero]) + INIT_ZERO_FLAG="-ftrivial-auto-var-init=zero" + FLAGS_COMPILER_CHECK_ARGUMENTS(ARGUMENT: [$INIT_ZERO_FLAG], + IF_TRUE: [ + SVE_CFLAGS="${SVE_CFLAGS} $INIT_ZERO_FLAG" + ] + ) + fi + fi + AC_SUBST(SVE_CFLAGS) +]) + ################################################################################ # platform independent AC_DEFUN([FLAGS_SETUP_ASFLAGS], diff --git a/make/autoconf/flags.m4 b/make/autoconf/flags.m4 index c810d15ebbc..10647305757 100644 --- a/make/autoconf/flags.m4 +++ b/make/autoconf/flags.m4 @@ -374,6 +374,7 @@ AC_DEFUN([FLAGS_SETUP_FLAGS], FLAGS_SETUP_RCFLAGS FLAGS_SETUP_NMFLAGS + FLAGS_SETUP_SVE FLAGS_SETUP_ASFLAGS FLAGS_SETUP_ASFLAGS_CPU_DEP([TARGET]) FLAGS_SETUP_ASFLAGS_CPU_DEP([BUILD], [OPENJDK_BUILD_]) From 7e738f0d906e574706a277fabbc2cc1df6f11f19 Mon Sep 17 00:00:00 2001 From: Aleksey Shipilev Date: Wed, 17 Sep 2025 11:36:23 +0000 Subject: [PATCH 079/556] 8367313: CTW: Execute in AWT headless mode Reviewed-by: epeter, kvn --- .../testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java b/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java index d721c7d63c5..11694f72837 100644 --- a/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java +++ b/test/hotspot/jtreg/testlibrary/ctw/src/sun/hotspot/tools/ctw/CtwRunner.java @@ -292,6 +292,8 @@ public class CtwRunner { "--add-exports", "java.base/jdk.internal.misc=ALL-UNNAMED", "--add-exports", "java.base/jdk.internal.reflect=ALL-UNNAMED", "--add-exports", "java.base/jdk.internal.access=ALL-UNNAMED", + // Graphics clinits may run, force headless mode + "-Djava.awt.headless=true", // enable diagnostic logging "-XX:+LogCompilation", // use phase specific log, hs_err and ciReplay files From b00e0dae9bbd4bd88f8e7307b7c96688fa3194fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Casta=C3=B1eda=20Lozano?= Date: Wed, 17 Sep 2025 12:47:59 +0000 Subject: [PATCH 080/556] 8367728: IGV: dump node address type Reviewed-by: mchevalier, dfenacci, chagedorn --- src/hotspot/share/opto/idealGraphPrinter.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/hotspot/share/opto/idealGraphPrinter.cpp b/src/hotspot/share/opto/idealGraphPrinter.cpp index cbf972166c2..19eaf1b369e 100644 --- a/src/hotspot/share/opto/idealGraphPrinter.cpp +++ b/src/hotspot/share/opto/idealGraphPrinter.cpp @@ -448,6 +448,11 @@ void IdealGraphPrinter::visit_node(Node* n, bool edges) { } } } + if (n->adr_type() != nullptr) { + stringStream adr_type_stream; + n->adr_type()->dump_on(&adr_type_stream); + print_prop("adr_type", adr_type_stream.freeze()); + } if (C->cfg() != nullptr) { Block* block = C->cfg()->get_block_for_node(node); From 1ba841410bf4af0377a7192717d4ebc5d6d9f3f9 Mon Sep 17 00:00:00 2001 From: Matthias Baesken Date: Wed, 17 Sep 2025 13:46:52 +0000 Subject: [PATCH 081/556] 8367573: JNI exception pending in os_getCmdlineAndUserInfo of ProcessHandleImpl_aix.c Reviewed-by: rriggs --- src/java.base/aix/native/libjava/ProcessHandleImpl_aix.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/java.base/aix/native/libjava/ProcessHandleImpl_aix.c b/src/java.base/aix/native/libjava/ProcessHandleImpl_aix.c index eff634acbde..43d23a481f4 100644 --- a/src/java.base/aix/native/libjava/ProcessHandleImpl_aix.c +++ b/src/java.base/aix/native/libjava/ProcessHandleImpl_aix.c @@ -224,6 +224,7 @@ void os_getCmdlineAndUserInfo(JNIEnv *env, jobject jinfo, pid_t pid) { } unix_getUserInfo(env, jinfo, psinfo.pr_uid); + JNU_CHECK_EXCEPTION(env); /* * Now read psinfo.pr_psargs which contains the first PRARGSZ characters of the From 91afdaff80459ec8ffff859f29fdadf1c631fddb Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Wed, 17 Sep 2025 13:58:06 +0000 Subject: [PATCH 082/556] 8367860: Remove unused NMethodToOopClosure::fix_relocations Reviewed-by: fandreuzzi, tschatzl --- src/hotspot/share/memory/iterator.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hotspot/share/memory/iterator.hpp b/src/hotspot/share/memory/iterator.hpp index 044951142b0..8310c2949e2 100644 --- a/src/hotspot/share/memory/iterator.hpp +++ b/src/hotspot/share/memory/iterator.hpp @@ -252,7 +252,6 @@ class NMethodToOopClosure : public NMethodClosure { NMethodToOopClosure(OopClosure* cl, bool fix_relocations) : _cl(cl), _fix_relocations(fix_relocations) {} void do_nmethod(nmethod* nm) override; - bool fix_relocations() const { return _fix_relocations; } const static bool FixRelocations = true; }; From d7eeacf2a0c24946de56471a99e744f21642d784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jeli=C5=84ski?= Date: Wed, 17 Sep 2025 14:01:32 +0000 Subject: [PATCH 083/556] 8367112: HttpClient does not support Named Groups set on SSLParameters Reviewed-by: jpai, dfuchs --- .../jdk/internal/net/http/common/Utils.java | 2 ++ .../net/httpclient/HttpClientBuilderTest.java | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index bcedff8844e..2916a41e62a 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -610,6 +610,8 @@ public final class Utils { p1.setSNIMatchers(p.getSNIMatchers()); p1.setServerNames(p.getServerNames()); p1.setUseCipherSuitesOrder(p.getUseCipherSuitesOrder()); + p1.setSignatureSchemes(p.getSignatureSchemes()); + p1.setNamedGroups(p.getNamedGroups()); return p1; } diff --git a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java index 451221c6e23..6074a3a855b 100644 --- a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java +++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -57,7 +57,7 @@ import static org.testng.Assert.*; /* * @test - * @bug 8209137 8326233 + * @bug 8209137 8326233 8367112 * @summary HttpClient[.Builder] API and behaviour checks * @library /test/lib * @build jdk.test.lib.net.SimpleSSLContext @@ -271,6 +271,20 @@ public class HttpClientBuilderTest { try (var closer = closeable(builder)) { assertTrue(closer.build().sslParameters().getProtocols()[0].equals("C")); } + SSLParameters d = new SSLParameters(); + d.setSignatureSchemes(new String[] { "C" }); + builder.sslParameters(d); + d.setSignatureSchemes(new String[] { "D" }); + try (var closer = closeable(builder)) { + assertTrue(closer.build().sslParameters().getSignatureSchemes()[0].equals("C")); + } + SSLParameters e = new SSLParameters(); + e.setNamedGroups(new String[] { "C" }); + builder.sslParameters(e); + e.setNamedGroups(new String[] { "D" }); + try (var closer = closeable(builder)) { + assertTrue(closer.build().sslParameters().getNamedGroups()[0].equals("C")); + } // test defaults for needClientAuth and wantClientAuth builder.sslParameters(new SSLParameters()); try (var closer = closeable(builder)) { From 9949ee3163a31f6f3c13c4fe34e8c0166210719e Mon Sep 17 00:00:00 2001 From: David Beaumont Date: Wed, 17 Sep 2025 14:46:47 +0000 Subject: [PATCH 084/556] 8367005: ImageReader refactor caused performance regressions for startup and footprint Reviewed-by: alanb, rriggs, jpai --- .../jdk/internal/jimage/ImageReader.java | 96 ++++++++++++++++++- .../internal/module/SystemModuleFinders.java | 22 ++--- .../jrt/JavaRuntimeURLConnection.java | 4 +- .../jdk/internal/jimage/ImageReaderTest.java | 62 ++++++++++++ 4 files changed, 162 insertions(+), 22 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java index 79e718c76e5..e062e1629ff 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -137,6 +137,45 @@ public final class ImageReader implements AutoCloseable { return reader.findNode(name); } + /** + * Returns a resource node in the given module, or null if no resource of + * that name exists. + * + *

        This is equivalent to: + *

        {@code
        +     * findNode("/modules/" + moduleName + "/" + resourcePath)
        +     * }
        + * but more performant, and returns {@code null} for directories. + * + * @param moduleName The module name of the requested resource. + * @param resourcePath Trailing module-relative resource path, not starting + * with {@code '/'}. + */ + public Node findResourceNode(String moduleName, String resourcePath) + throws IOException { + ensureOpen(); + return reader.findResourceNode(moduleName, resourcePath); + } + + /** + * Returns whether a resource exists in the given module. + * + *

        This is equivalent to: + *

        {@code
        +     * findResourceNode(moduleName, resourcePath) != null
        +     * }
        + * but more performant, and will not create or cache new nodes. + * + * @param moduleName The module name of the resource being tested for. + * @param resourcePath Trailing module-relative resource path, not starting + * with {@code '/'}. + */ + public boolean containsResource(String moduleName, String resourcePath) + throws IOException { + ensureOpen(); + return reader.containsResource(moduleName, resourcePath); + } + /** * Returns a copy of the content of a resource node. The buffer returned by * this method is not cached by the node, and each call returns a new array @@ -276,10 +315,7 @@ public final class ImageReader implements AutoCloseable { * Returns a node with the given name, or null if no resource or directory of * that name exists. * - *

        This is the only public API by which anything outside this class can access - * {@code Node} instances either directly, or by resolving symbolic links. - * - *

        Note also that there is no reentrant calling back to this method from within + *

        Note that there is no reentrant calling back to this method from within * the node handling code. * * @param name an absolute, {@code /}-separated path string, prefixed with either @@ -291,6 +327,9 @@ public final class ImageReader implements AutoCloseable { // We cannot get the root paths ("/modules" or "/packages") here // because those nodes are already in the nodes cache. if (name.startsWith(MODULES_ROOT + "/")) { + // This may perform two lookups, one for a directory (in + // "/modules/...") and one for a non-prefixed resource + // (with "/modules" removed). node = buildModulesNode(name); } else if (name.startsWith(PACKAGES_ROOT + "/")) { node = buildPackagesNode(name); @@ -307,6 +346,55 @@ public final class ImageReader implements AutoCloseable { return node; } + /** + * Returns a resource node in the given module, or null if no resource of + * that name exists. + * + *

        Note that there is no reentrant calling back to this method from within + * the node handling code. + */ + Node findResourceNode(String moduleName, String resourcePath) { + // Unlike findNode(), this method makes only one lookup in the + // underlying jimage, but can only reliably return resource nodes. + if (moduleName.indexOf('/') >= 0) { + throw new IllegalArgumentException("invalid module name: " + moduleName); + } + String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath; + // Synchronize as tightly as possible to reduce locking contention. + synchronized (this) { + Node node = nodes.get(nodeName); + if (node == null) { + ImageLocation loc = findLocation(moduleName, resourcePath); + if (loc != null && isResource(loc)) { + node = newResource(nodeName, loc); + nodes.put(node.getName(), node); + } + return node; + } else { + return node.isResource() ? node : null; + } + } + } + + /** + * Returns whether a resource exists in the given module. + * + *

        This method is expected to be called frequently for resources + * which do not exist in the given module (e.g. as part of classpath + * search). As such, it skips checking the nodes cache and only checks + * for an entry in the jimage file, as this is faster if the resource + * is not present. This also means it doesn't need synchronization. + */ + boolean containsResource(String moduleName, String resourcePath) { + if (moduleName.indexOf('/') >= 0) { + throw new IllegalArgumentException("invalid module name: " + moduleName); + } + // If the given module name is 'modules', then 'isResource()' + // returns false to prevent false positives. + ImageLocation loc = findLocation(moduleName, resourcePath); + return loc != null && isResource(loc); + } + /** * Builds a node in the "/modules/..." namespace. * diff --git a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java index 39f433d4041..370c151af84 100644 --- a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java +++ b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java @@ -414,26 +414,18 @@ public final class SystemModuleFinders { * Returns {@code true} if the given resource exists, {@code false} * if not found. */ - private boolean containsResource(String resourcePath) throws IOException { - Objects.requireNonNull(resourcePath); + private boolean containsResource(String module, String name) throws IOException { + Objects.requireNonNull(name); if (closed) throw new IOException("ModuleReader is closed"); ImageReader imageReader = SystemImage.reader(); - if (imageReader != null) { - ImageReader.Node node = imageReader.findNode("/modules" + resourcePath); - return node != null && node.isResource(); - } else { - // not an images build - return false; - } + return imageReader != null && imageReader.containsResource(module, name); } @Override public Optional find(String name) throws IOException { - Objects.requireNonNull(name); - String resourcePath = "/" + module + "/" + name; - if (containsResource(resourcePath)) { - URI u = JNUA.create("jrt", resourcePath); + if (containsResource(module, name)) { + URI u = JNUA.create("jrt", "/" + module + "/" + name); return Optional.of(u); } else { return Optional.empty(); @@ -465,9 +457,7 @@ public final class SystemModuleFinders { if (closed) { throw new IOException("ModuleReader is closed"); } - String nodeName = "/modules/" + module + "/" + name; - ImageReader.Node node = reader.findNode(nodeName); - return (node != null && node.isResource()) ? node : null; + return reader.findResourceNode(module, name); } @Override diff --git a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java index 20b735fbdf3..71080950b80 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java @@ -87,8 +87,8 @@ public class JavaRuntimeURLConnection extends URLConnection { if (module.isEmpty() || path == null) { throw new IOException("cannot connect to jrt:/" + module); } - Node node = READER.findNode("/modules/" + module + "/" + path); - if (node == null || !node.isResource()) { + Node node = READER.findResourceNode(module, path); + if (node == null) { throw new IOException(module + "/" + path + " not found"); } this.resourceNode = node; diff --git a/test/jdk/jdk/internal/jimage/ImageReaderTest.java b/test/jdk/jdk/internal/jimage/ImageReaderTest.java index 8db9a3768d4..de52ed1503d 100644 --- a/test/jdk/jdk/internal/jimage/ImageReaderTest.java +++ b/test/jdk/jdk/internal/jimage/ImageReaderTest.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import tests.Helper; import tests.JImageGenerator; @@ -43,9 +44,11 @@ import java.util.Set; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -122,6 +125,65 @@ public class ImageReaderTest { } } + @ParameterizedTest + @CsvSource(delimiter = ':', value = { + "modfoo:com/foo/Alpha.class", + "modbar:com/bar/One.class", + }) + public void testResource_present(String modName, String resPath) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertNotNull(reader.findResourceNode(modName, resPath)); + assertTrue(reader.containsResource(modName, resPath)); + + String canonicalNodeName = "/modules/" + modName + "/" + resPath; + Node node = reader.findNode(canonicalNodeName); + assertTrue(node != null && node.isResource()); + } + } + + @ParameterizedTest + @CsvSource(delimiter = ':', value = { + // Absolute resource names are not allowed. + "modfoo:/com/bar/One.class", + // Resource in wrong module. + "modfoo:com/bar/One.class", + "modbar:com/foo/Alpha.class", + // Directories are not returned. + "modfoo:com/foo", + "modbar:com/bar", + // JImage entries exist for these, but they are not resources. + "modules:modfoo/com/foo/Alpha.class", + "packages:com.foo/modfoo", + // Empty module names/paths do not find resources. + "'':modfoo/com/foo/Alpha.class", + "modfoo:''"}) + public void testResource_absent(String modName, String resPath) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertNull(reader.findResourceNode(modName, resPath)); + assertFalse(reader.containsResource(modName, resPath)); + + // Non-existent resources names should either not be found, + // or (in the case of directory nodes) not be resources. + String canonicalNodeName = "/modules/" + modName + "/" + resPath; + Node node = reader.findNode(canonicalNodeName); + assertTrue(node == null || !node.isResource()); + } + } + + @ParameterizedTest + @CsvSource(delimiter = ':', value = { + // Don't permit module names to contain paths. + "modfoo/com/bar:One.class", + "modfoo/com:bar/One.class", + "modules/modfoo/com:foo/Alpha.class", + }) + public void testResource_invalid(String modName, String resPath) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath)); + assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath)); + } + } + @Test public void testPackageDirectories() throws IOException { try (ImageReader reader = ImageReader.open(image)) { From 01d7554b87fb7be8cab5dc12fd67eaba6585d2f3 Mon Sep 17 00:00:00 2001 From: Stefan Karlsson Date: Wed, 17 Sep 2025 15:17:30 +0000 Subject: [PATCH 085/556] 8367486: Change prefix for platform-dependent AtomicAccess files Reviewed-by: kbarrett, ayang --- .../{atomic_aix_ppc.hpp => atomicAccess_aix_ppc.hpp} | 8 ++++---- ...omic_bsd_aarch64.hpp => atomicAccess_bsd_aarch64.hpp} | 9 ++++----- .../{atomic_bsd_x86.hpp => atomicAccess_bsd_x86.hpp} | 8 ++++---- .../{atomic_bsd_zero.hpp => atomicAccess_bsd_zero.hpp} | 8 ++++---- src/hotspot/os_cpu/bsd_zero/os_bsd_zero.cpp | 2 +- ..._linux_aarch64.hpp => atomicAccess_linux_aarch64.hpp} | 8 ++++---- .../{atomic_linux_arm.hpp => atomicAccess_linux_arm.hpp} | 8 ++++---- .../{atomic_linux_ppc.hpp => atomicAccess_linux_ppc.hpp} | 8 ++++---- ...omic_linux_riscv.hpp => atomicAccess_linux_riscv.hpp} | 8 ++++---- ...atomic_linux_s390.hpp => atomicAccess_linux_s390.hpp} | 6 +++--- .../{atomic_linux_x86.hpp => atomicAccess_linux_x86.hpp} | 8 ++++---- ...atomic_linux_zero.hpp => atomicAccess_linux_zero.hpp} | 8 ++++---- src/hotspot/os_cpu/linux_zero/os_linux_zero.cpp | 2 +- ...dows_aarch64.hpp => atomicAccess_windows_aarch64.hpp} | 6 +++--- ...omic_windows_x86.hpp => atomicAccess_windows_x86.hpp} | 6 +++--- src/hotspot/share/runtime/atomicAccess.hpp | 2 +- 16 files changed, 52 insertions(+), 53 deletions(-) rename src/hotspot/os_cpu/aix_ppc/{atomic_aix_ppc.hpp => atomicAccess_aix_ppc.hpp} (98%) rename src/hotspot/os_cpu/bsd_aarch64/{atomic_bsd_aarch64.hpp => atomicAccess_bsd_aarch64.hpp} (95%) rename src/hotspot/os_cpu/bsd_x86/{atomic_bsd_x86.hpp => atomicAccess_bsd_x86.hpp} (97%) rename src/hotspot/os_cpu/bsd_zero/{atomic_bsd_zero.hpp => atomicAccess_bsd_zero.hpp} (96%) rename src/hotspot/os_cpu/linux_aarch64/{atomic_linux_aarch64.hpp => atomicAccess_linux_aarch64.hpp} (97%) rename src/hotspot/os_cpu/linux_arm/{atomic_linux_arm.hpp => atomicAccess_linux_arm.hpp} (97%) rename src/hotspot/os_cpu/linux_ppc/{atomic_linux_ppc.hpp => atomicAccess_linux_ppc.hpp} (98%) rename src/hotspot/os_cpu/linux_riscv/{atomic_linux_riscv.hpp => atomicAccess_linux_riscv.hpp} (97%) rename src/hotspot/os_cpu/linux_s390/{atomic_linux_s390.hpp => atomicAccess_linux_s390.hpp} (98%) rename src/hotspot/os_cpu/linux_x86/{atomic_linux_x86.hpp => atomicAccess_linux_x86.hpp} (97%) rename src/hotspot/os_cpu/linux_zero/{atomic_linux_zero.hpp => atomicAccess_linux_zero.hpp} (96%) rename src/hotspot/os_cpu/windows_aarch64/{atomic_windows_aarch64.hpp => atomicAccess_windows_aarch64.hpp} (96%) rename src/hotspot/os_cpu/windows_x86/{atomic_windows_x86.hpp => atomicAccess_windows_x86.hpp} (97%) diff --git a/src/hotspot/os_cpu/aix_ppc/atomic_aix_ppc.hpp b/src/hotspot/os_cpu/aix_ppc/atomicAccess_aix_ppc.hpp similarity index 98% rename from src/hotspot/os_cpu/aix_ppc/atomic_aix_ppc.hpp rename to src/hotspot/os_cpu/aix_ppc/atomicAccess_aix_ppc.hpp index d32f7c93ecf..3540e364bc6 100644 --- a/src/hotspot/os_cpu/aix_ppc/atomic_aix_ppc.hpp +++ b/src/hotspot/os_cpu/aix_ppc/atomicAccess_aix_ppc.hpp @@ -23,8 +23,8 @@ * */ -#ifndef OS_CPU_AIX_PPC_ATOMIC_AIX_PPC_HPP -#define OS_CPU_AIX_PPC_ATOMIC_AIX_PPC_HPP +#ifndef OS_CPU_AIX_PPC_ATOMICACCESS_AIX_PPC_HPP +#define OS_CPU_AIX_PPC_ATOMICACCESS_AIX_PPC_HPP #ifndef PPC64 #error "Atomic currently only implemented for PPC64" @@ -33,7 +33,7 @@ #include "orderAccess_aix_ppc.hpp" #include "utilities/debug.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess // // machine barrier instructions: @@ -414,4 +414,4 @@ struct AtomicAccess::PlatformOrderedLoad { } }; -#endif // OS_CPU_AIX_PPC_ATOMIC_AIX_PPC_HPP +#endif // OS_CPU_AIX_PPC_ATOMICACCESS_AIX_PPC_HPP diff --git a/src/hotspot/os_cpu/bsd_aarch64/atomic_bsd_aarch64.hpp b/src/hotspot/os_cpu/bsd_aarch64/atomicAccess_bsd_aarch64.hpp similarity index 95% rename from src/hotspot/os_cpu/bsd_aarch64/atomic_bsd_aarch64.hpp rename to src/hotspot/os_cpu/bsd_aarch64/atomicAccess_bsd_aarch64.hpp index 1ecdd59f59e..3d2c632ace8 100644 --- a/src/hotspot/os_cpu/bsd_aarch64/atomic_bsd_aarch64.hpp +++ b/src/hotspot/os_cpu/bsd_aarch64/atomicAccess_bsd_aarch64.hpp @@ -24,12 +24,12 @@ * */ -#ifndef OS_CPU_BSD_AARCH64_ATOMIC_BSD_AARCH64_HPP -#define OS_CPU_BSD_AARCH64_ATOMIC_BSD_AARCH64_HPP +#ifndef OS_CPU_BSD_AARCH64_ATOMICACCESS_BSD_AARCH64_HPP +#define OS_CPU_BSD_AARCH64_ATOMICACCESS_BSD_AARCH64_HPP #include "utilities/debug.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess // Note that memory_order_conservative requires a full barrier after atomic stores. // See https://patchwork.kernel.org/patch/3575821/ @@ -129,5 +129,4 @@ struct AtomicAccess::PlatformOrderedStore void operator()(volatile T* p, T v) const { release_store(p, v); OrderAccess::fence(); } }; - -#endif // OS_CPU_BSD_AARCH64_ATOMIC_BSD_AARCH64_HPP +#endif // OS_CPU_BSD_AARCH64_ATOMICACCESS_BSD_AARCH64_HPP diff --git a/src/hotspot/os_cpu/bsd_x86/atomic_bsd_x86.hpp b/src/hotspot/os_cpu/bsd_x86/atomicAccess_bsd_x86.hpp similarity index 97% rename from src/hotspot/os_cpu/bsd_x86/atomic_bsd_x86.hpp rename to src/hotspot/os_cpu/bsd_x86/atomicAccess_bsd_x86.hpp index 8fbc319e766..975580fbd71 100644 --- a/src/hotspot/os_cpu/bsd_x86/atomic_bsd_x86.hpp +++ b/src/hotspot/os_cpu/bsd_x86/atomicAccess_bsd_x86.hpp @@ -22,10 +22,10 @@ * */ -#ifndef OS_CPU_BSD_X86_ATOMIC_BSD_X86_HPP -#define OS_CPU_BSD_X86_ATOMIC_BSD_X86_HPP +#ifndef OS_CPU_BSD_X86_ATOMICACCESS_BSD_X86_HPP +#define OS_CPU_BSD_X86_ATOMICACCESS_BSD_X86_HPP -// Implementation of class atomic +// Implementation of class AtomicAccess template struct AtomicAccess::PlatformAdd { @@ -230,4 +230,4 @@ struct AtomicAccess::PlatformOrderedStore<8, RELEASE_X_FENCE> }; #endif // AMD64 -#endif // OS_CPU_BSD_X86_ATOMIC_BSD_X86_HPP +#endif // OS_CPU_BSD_X86_ATOMICACCESS_BSD_X86_HPP diff --git a/src/hotspot/os_cpu/bsd_zero/atomic_bsd_zero.hpp b/src/hotspot/os_cpu/bsd_zero/atomicAccess_bsd_zero.hpp similarity index 96% rename from src/hotspot/os_cpu/bsd_zero/atomic_bsd_zero.hpp rename to src/hotspot/os_cpu/bsd_zero/atomicAccess_bsd_zero.hpp index b5cedac867b..6a720dac54e 100644 --- a/src/hotspot/os_cpu/bsd_zero/atomic_bsd_zero.hpp +++ b/src/hotspot/os_cpu/bsd_zero/atomicAccess_bsd_zero.hpp @@ -23,13 +23,13 @@ * */ -#ifndef OS_CPU_BSD_ZERO_ATOMIC_BSD_ZERO_HPP -#define OS_CPU_BSD_ZERO_ATOMIC_BSD_ZERO_HPP +#ifndef OS_CPU_BSD_ZERO_ATOMICACCESS_BSD_ZERO_HPP +#define OS_CPU_BSD_ZERO_ATOMICACCESS_BSD_ZERO_HPP #include "orderAccess_bsd_zero.hpp" #include "runtime/os.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess template struct AtomicAccess::PlatformAdd { @@ -149,4 +149,4 @@ inline void AtomicAccess::PlatformStore<8>::operator()(T volatile* dest, __atomic_store(dest, &store_value, __ATOMIC_RELAXED); } -#endif // OS_CPU_BSD_ZERO_ATOMIC_BSD_ZERO_HPP +#endif // OS_CPU_BSD_ZERO_ATOMICACCESS_BSD_ZERO_HPP diff --git a/src/hotspot/os_cpu/bsd_zero/os_bsd_zero.cpp b/src/hotspot/os_cpu/bsd_zero/os_bsd_zero.cpp index 3fefbdbe56c..facad184426 100644 --- a/src/hotspot/os_cpu/bsd_zero/os_bsd_zero.cpp +++ b/src/hotspot/os_cpu/bsd_zero/os_bsd_zero.cpp @@ -24,7 +24,6 @@ */ #include "asm/assembler.inline.hpp" -#include "atomic_bsd_zero.hpp" #include "classfile/vmSymbols.hpp" #include "code/vtableStubs.hpp" #include "interpreter/interpreter.hpp" @@ -36,6 +35,7 @@ #include "prims/jniFastGetField.hpp" #include "prims/jvm_misc.hpp" #include "runtime/arguments.hpp" +#include "runtime/atomicAccess.hpp" #include "runtime/frame.inline.hpp" #include "runtime/interfaceSupport.inline.hpp" #include "runtime/java.hpp" diff --git a/src/hotspot/os_cpu/linux_aarch64/atomic_linux_aarch64.hpp b/src/hotspot/os_cpu/linux_aarch64/atomicAccess_linux_aarch64.hpp similarity index 97% rename from src/hotspot/os_cpu/linux_aarch64/atomic_linux_aarch64.hpp rename to src/hotspot/os_cpu/linux_aarch64/atomicAccess_linux_aarch64.hpp index 4940cbdc246..6e5f53edfa3 100644 --- a/src/hotspot/os_cpu/linux_aarch64/atomic_linux_aarch64.hpp +++ b/src/hotspot/os_cpu/linux_aarch64/atomicAccess_linux_aarch64.hpp @@ -23,13 +23,13 @@ * */ -#ifndef OS_CPU_LINUX_AARCH64_ATOMIC_LINUX_AARCH64_HPP -#define OS_CPU_LINUX_AARCH64_ATOMIC_LINUX_AARCH64_HPP +#ifndef OS_CPU_LINUX_AARCH64_ATOMICACCESS_LINUX_AARCH64_HPP +#define OS_CPU_LINUX_AARCH64_ATOMICACCESS_LINUX_AARCH64_HPP #include "atomic_aarch64.hpp" #include "runtime/vm_version.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess // Note that memory_order_conservative requires a full barrier after atomic stores. // See https://patchwork.kernel.org/patch/3575821/ @@ -217,4 +217,4 @@ struct AtomicAccess::PlatformOrderedStore void operator()(volatile T* p, T v) const { release_store(p, v); OrderAccess::fence(); } }; -#endif // OS_CPU_LINUX_AARCH64_ATOMIC_LINUX_AARCH64_HPP +#endif // OS_CPU_LINUX_AARCH64_ATOMICACCESS_LINUX_AARCH64_HPP diff --git a/src/hotspot/os_cpu/linux_arm/atomic_linux_arm.hpp b/src/hotspot/os_cpu/linux_arm/atomicAccess_linux_arm.hpp similarity index 97% rename from src/hotspot/os_cpu/linux_arm/atomic_linux_arm.hpp rename to src/hotspot/os_cpu/linux_arm/atomicAccess_linux_arm.hpp index db00c347dea..5b5f9da51a6 100644 --- a/src/hotspot/os_cpu/linux_arm/atomic_linux_arm.hpp +++ b/src/hotspot/os_cpu/linux_arm/atomicAccess_linux_arm.hpp @@ -22,14 +22,14 @@ * */ -#ifndef OS_CPU_LINUX_ARM_ATOMIC_LINUX_ARM_HPP -#define OS_CPU_LINUX_ARM_ATOMIC_LINUX_ARM_HPP +#ifndef OS_CPU_LINUX_ARM_ATOMICACCESS_LINUX_ARM_HPP +#define OS_CPU_LINUX_ARM_ATOMICACCESS_LINUX_ARM_HPP #include "memory/allStatic.hpp" #include "runtime/os.hpp" #include "runtime/vm_version.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess class ARMAtomicFuncs : AllStatic { public: @@ -178,4 +178,4 @@ inline T AtomicAccess::PlatformCmpxchg<8>::operator()(T volatile* dest, return cmpxchg_using_helper(reorder_cmpxchg_long_func, dest, compare_value, exchange_value); } -#endif // OS_CPU_LINUX_ARM_ATOMIC_LINUX_ARM_HPP +#endif // OS_CPU_LINUX_ARM_ATOMICACCESS_LINUX_ARM_HPP diff --git a/src/hotspot/os_cpu/linux_ppc/atomic_linux_ppc.hpp b/src/hotspot/os_cpu/linux_ppc/atomicAccess_linux_ppc.hpp similarity index 98% rename from src/hotspot/os_cpu/linux_ppc/atomic_linux_ppc.hpp rename to src/hotspot/os_cpu/linux_ppc/atomicAccess_linux_ppc.hpp index 9f1d90c26bd..f4eac1207bf 100644 --- a/src/hotspot/os_cpu/linux_ppc/atomic_linux_ppc.hpp +++ b/src/hotspot/os_cpu/linux_ppc/atomicAccess_linux_ppc.hpp @@ -23,8 +23,8 @@ * */ -#ifndef OS_CPU_LINUX_PPC_ATOMIC_LINUX_PPC_HPP -#define OS_CPU_LINUX_PPC_ATOMIC_LINUX_PPC_HPP +#ifndef OS_CPU_LINUX_PPC_ATOMICACCESS_LINUX_PPC_HPP +#define OS_CPU_LINUX_PPC_ATOMICACCESS_LINUX_PPC_HPP #ifndef PPC64 #error "Atomic currently only implemented for PPC64" @@ -33,7 +33,7 @@ #include "orderAccess_linux_ppc.hpp" #include "utilities/debug.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess // // machine barrier instructions: @@ -392,4 +392,4 @@ struct AtomicAccess::PlatformOrderedLoad } }; -#endif // OS_CPU_LINUX_PPC_ATOMIC_LINUX_PPC_HPP +#endif // OS_CPU_LINUX_PPC_ATOMICACCESS_LINUX_PPC_HPP diff --git a/src/hotspot/os_cpu/linux_riscv/atomic_linux_riscv.hpp b/src/hotspot/os_cpu/linux_riscv/atomicAccess_linux_riscv.hpp similarity index 97% rename from src/hotspot/os_cpu/linux_riscv/atomic_linux_riscv.hpp rename to src/hotspot/os_cpu/linux_riscv/atomicAccess_linux_riscv.hpp index f713465edeb..6d57ea55a83 100644 --- a/src/hotspot/os_cpu/linux_riscv/atomic_linux_riscv.hpp +++ b/src/hotspot/os_cpu/linux_riscv/atomicAccess_linux_riscv.hpp @@ -23,12 +23,12 @@ * */ -#ifndef OS_CPU_LINUX_RISCV_ATOMIC_LINUX_RISCV_HPP -#define OS_CPU_LINUX_RISCV_ATOMIC_LINUX_RISCV_HPP +#ifndef OS_CPU_LINUX_RISCV_ATOMICACCESS_LINUX_RISCV_HPP +#define OS_CPU_LINUX_RISCV_ATOMICACCESS_LINUX_RISCV_HPP #include "runtime/vm_version.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess // Note that memory_order_conservative requires a full barrier after atomic stores. // See https://patchwork.kernel.org/patch/3575821/ @@ -226,4 +226,4 @@ struct AtomicAccess::PlatformOrderedStore #undef FULL_COMPILER_ATOMIC_SUPPORT -#endif // OS_CPU_LINUX_RISCV_ATOMIC_LINUX_RISCV_HPP +#endif // OS_CPU_LINUX_RISCV_ATOMICACCESS_LINUX_RISCV_HPP diff --git a/src/hotspot/os_cpu/linux_s390/atomic_linux_s390.hpp b/src/hotspot/os_cpu/linux_s390/atomicAccess_linux_s390.hpp similarity index 98% rename from src/hotspot/os_cpu/linux_s390/atomic_linux_s390.hpp rename to src/hotspot/os_cpu/linux_s390/atomicAccess_linux_s390.hpp index ec620e3907a..5849d69ae2f 100644 --- a/src/hotspot/os_cpu/linux_s390/atomic_linux_s390.hpp +++ b/src/hotspot/os_cpu/linux_s390/atomicAccess_linux_s390.hpp @@ -23,8 +23,8 @@ * */ -#ifndef OS_CPU_LINUX_S390_ATOMIC_LINUX_S390_HPP -#define OS_CPU_LINUX_S390_ATOMIC_LINUX_S390_HPP +#ifndef OS_CPU_LINUX_S390_ATOMICACCESS_LINUX_S390_HPP +#define OS_CPU_LINUX_S390_ATOMICACCESS_LINUX_S390_HPP #include "runtime/atomicAccess.hpp" #include "runtime/os.hpp" @@ -345,4 +345,4 @@ struct AtomicAccess::PlatformOrderedLoad T operator()(const volatile T* p) const { T t = *p; OrderAccess::acquire(); return t; } }; -#endif // OS_CPU_LINUX_S390_ATOMIC_LINUX_S390_HPP +#endif // OS_CPU_LINUX_S390_ATOMICACCESS_LINUX_S390_HPP diff --git a/src/hotspot/os_cpu/linux_x86/atomic_linux_x86.hpp b/src/hotspot/os_cpu/linux_x86/atomicAccess_linux_x86.hpp similarity index 97% rename from src/hotspot/os_cpu/linux_x86/atomic_linux_x86.hpp rename to src/hotspot/os_cpu/linux_x86/atomicAccess_linux_x86.hpp index 561224f56be..c9af982525d 100644 --- a/src/hotspot/os_cpu/linux_x86/atomic_linux_x86.hpp +++ b/src/hotspot/os_cpu/linux_x86/atomicAccess_linux_x86.hpp @@ -22,10 +22,10 @@ * */ -#ifndef OS_CPU_LINUX_X86_ATOMIC_LINUX_X86_HPP -#define OS_CPU_LINUX_X86_ATOMIC_LINUX_X86_HPP +#ifndef OS_CPU_LINUX_X86_ATOMICACCESS_LINUX_X86_HPP +#define OS_CPU_LINUX_X86_ATOMICACCESS_LINUX_X86_HPP -// Implementation of class atomic +// Implementation of class AtomicAccess template struct AtomicAccess::PlatformAdd { @@ -230,4 +230,4 @@ struct AtomicAccess::PlatformOrderedStore<8, RELEASE_X_FENCE> }; #endif // AMD64 -#endif // OS_CPU_LINUX_X86_ATOMIC_LINUX_X86_HPP +#endif // OS_CPU_LINUX_X86_ATOMICACCESS_LINUX_X86_HPP diff --git a/src/hotspot/os_cpu/linux_zero/atomic_linux_zero.hpp b/src/hotspot/os_cpu/linux_zero/atomicAccess_linux_zero.hpp similarity index 96% rename from src/hotspot/os_cpu/linux_zero/atomic_linux_zero.hpp rename to src/hotspot/os_cpu/linux_zero/atomicAccess_linux_zero.hpp index 05d567d3e28..376ef7a9dc9 100644 --- a/src/hotspot/os_cpu/linux_zero/atomic_linux_zero.hpp +++ b/src/hotspot/os_cpu/linux_zero/atomicAccess_linux_zero.hpp @@ -23,12 +23,12 @@ * */ -#ifndef OS_CPU_LINUX_ZERO_ATOMIC_LINUX_ZERO_HPP -#define OS_CPU_LINUX_ZERO_ATOMIC_LINUX_ZERO_HPP +#ifndef OS_CPU_LINUX_ZERO_ATOMICACCESS_LINUX_ZERO_HPP +#define OS_CPU_LINUX_ZERO_ATOMICACCESS_LINUX_ZERO_HPP #include "orderAccess_linux_zero.hpp" -// Implementation of class atomic +// Implementation of class AtomicAccess template struct AtomicAccess::PlatformAdd { @@ -149,4 +149,4 @@ inline void AtomicAccess::PlatformStore<8>::operator()(T volatile* dest, __atomic_store(dest, &store_value, __ATOMIC_RELAXED); } -#endif // OS_CPU_LINUX_ZERO_ATOMIC_LINUX_ZERO_HPP +#endif // OS_CPU_LINUX_ZERO_ATOMICACCESS_LINUX_ZERO_HPP diff --git a/src/hotspot/os_cpu/linux_zero/os_linux_zero.cpp b/src/hotspot/os_cpu/linux_zero/os_linux_zero.cpp index c9ac461851a..0ea379fec50 100644 --- a/src/hotspot/os_cpu/linux_zero/os_linux_zero.cpp +++ b/src/hotspot/os_cpu/linux_zero/os_linux_zero.cpp @@ -24,7 +24,6 @@ */ #include "asm/assembler.inline.hpp" -#include "atomic_linux_zero.hpp" #include "classfile/vmSymbols.hpp" #include "code/vtableStubs.hpp" #include "interpreter/interpreter.hpp" @@ -36,6 +35,7 @@ #include "prims/jniFastGetField.hpp" #include "prims/jvm_misc.hpp" #include "runtime/arguments.hpp" +#include "runtime/atomicAccess.hpp" #include "runtime/frame.inline.hpp" #include "runtime/interfaceSupport.inline.hpp" #include "runtime/java.hpp" diff --git a/src/hotspot/os_cpu/windows_aarch64/atomic_windows_aarch64.hpp b/src/hotspot/os_cpu/windows_aarch64/atomicAccess_windows_aarch64.hpp similarity index 96% rename from src/hotspot/os_cpu/windows_aarch64/atomic_windows_aarch64.hpp rename to src/hotspot/os_cpu/windows_aarch64/atomicAccess_windows_aarch64.hpp index 42c5b0e4a6c..62b6e3f87ec 100644 --- a/src/hotspot/os_cpu/windows_aarch64/atomic_windows_aarch64.hpp +++ b/src/hotspot/os_cpu/windows_aarch64/atomicAccess_windows_aarch64.hpp @@ -23,8 +23,8 @@ * */ -#ifndef OS_CPU_WINDOWS_AARCH64_ATOMIC_WINDOWS_AARCH64_HPP -#define OS_CPU_WINDOWS_AARCH64_ATOMIC_WINDOWS_AARCH64_HPP +#ifndef OS_CPU_WINDOWS_AARCH64_ATOMICACCESS_WINDOWS_AARCH64_HPP +#define OS_CPU_WINDOWS_AARCH64_ATOMICACCESS_WINDOWS_AARCH64_HPP #include #include "runtime/os.hpp" @@ -109,4 +109,4 @@ DEFINE_INTRINSIC_CMPXCHG(InterlockedCompareExchange64, __int64) #undef DEFINE_INTRINSIC_CMPXCHG -#endif // OS_CPU_WINDOWS_AARCH64_ATOMIC_WINDOWS_AARCH64_HPP +#endif // OS_CPU_WINDOWS_AARCH64_ATOMICACCESS_WINDOWS_AARCH64_HPP diff --git a/src/hotspot/os_cpu/windows_x86/atomic_windows_x86.hpp b/src/hotspot/os_cpu/windows_x86/atomicAccess_windows_x86.hpp similarity index 97% rename from src/hotspot/os_cpu/windows_x86/atomic_windows_x86.hpp rename to src/hotspot/os_cpu/windows_x86/atomicAccess_windows_x86.hpp index 4529da29092..a95da151688 100644 --- a/src/hotspot/os_cpu/windows_x86/atomic_windows_x86.hpp +++ b/src/hotspot/os_cpu/windows_x86/atomicAccess_windows_x86.hpp @@ -22,8 +22,8 @@ * */ -#ifndef OS_CPU_WINDOWS_X86_ATOMIC_WINDOWS_X86_HPP -#define OS_CPU_WINDOWS_X86_ATOMIC_WINDOWS_X86_HPP +#ifndef OS_CPU_WINDOWS_X86_ATOMICACCESS_WINDOWS_X86_HPP +#define OS_CPU_WINDOWS_X86_ATOMICACCESS_WINDOWS_X86_HPP #include #include "runtime/os.hpp" @@ -111,4 +111,4 @@ DEFINE_INTRINSIC_CMPXCHG(InterlockedCompareExchange64, __int64) #undef DEFINE_INTRINSIC_CMPXCHG -#endif // OS_CPU_WINDOWS_X86_ATOMIC_WINDOWS_X86_HPP +#endif // OS_CPU_WINDOWS_X86_ATOMICACCESS_WINDOWS_X86_HPP diff --git a/src/hotspot/share/runtime/atomicAccess.hpp b/src/hotspot/share/runtime/atomicAccess.hpp index 0ee60d2b7b7..ff972b31a26 100644 --- a/src/hotspot/share/runtime/atomicAccess.hpp +++ b/src/hotspot/share/runtime/atomicAccess.hpp @@ -859,7 +859,7 @@ class ScopedFence : public ScopedFenceGeneral { // platform specific in-line definitions - must come before shared definitions -#include OS_CPU_HEADER(atomic) +#include OS_CPU_HEADER(atomicAccess) // shared in-line definitions From 6f493b4d2e7120cbe34fb70d595f7626655b47a9 Mon Sep 17 00:00:00 2001 From: Cesar Soares Lucas Date: Wed, 17 Sep 2025 16:52:13 +0000 Subject: [PATCH 086/556] 8361699: C2: assert(can_reduce_phi(n->as_Phi())) failed: Sanity: previous reducible Phi is no longer reducible before SUT Reviewed-by: rcastanedalo --- src/hotspot/share/opto/escape.cpp | 8 +++ ...stReduceAllocationNotReducibleAnymore.java | 63 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/hotspot/jtreg/compiler/escapeAnalysis/TestReduceAllocationNotReducibleAnymore.java diff --git a/src/hotspot/share/opto/escape.cpp b/src/hotspot/share/opto/escape.cpp index eff482350b6..e6a593770b4 100644 --- a/src/hotspot/share/opto/escape.cpp +++ b/src/hotspot/share/opto/escape.cpp @@ -3129,6 +3129,14 @@ void ConnectionGraph::find_scalar_replaceable_allocs(GrowableArrayis_LocalVar()) { + Node* phi = use->ideal_node(); + if (phi->Opcode() == Op_Phi && reducible_merges.member(phi) && !can_reduce_phi(phi->as_Phi())) { + set_not_scalar_replaceable(jobj NOT_PRODUCT(COMMA "is merged in a non-reducible phi")); + reducible_merges.yank(phi); + found_nsr_alloc = true; + break; + } } } } diff --git a/test/hotspot/jtreg/compiler/escapeAnalysis/TestReduceAllocationNotReducibleAnymore.java b/test/hotspot/jtreg/compiler/escapeAnalysis/TestReduceAllocationNotReducibleAnymore.java new file mode 100644 index 00000000000..16bdd68c93c --- /dev/null +++ b/test/hotspot/jtreg/compiler/escapeAnalysis/TestReduceAllocationNotReducibleAnymore.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8361699 + * @summary Check that NSR state propagate correctly to initially reducible Phis + * and turns them into non-reducible Phis. + * @run main/othervm -XX:CompileCommand=compileonly,*TestReduceAllocationNotReducibleAnymore*::* + * -XX:CompileCommand=dontinline,*TestReduceAllocationNotReducibleAnymore*::* + * -Xcomp compiler.escapeAnalysis.TestReduceAllocationNotReducibleAnymore + * @run main compiler.escapeAnalysis.TestReduceAllocationNotReducibleAnymore + */ + +package compiler.escapeAnalysis; + +public class TestReduceAllocationNotReducibleAnymore { + public static void main(String[] args) { + for (int i = 0; i < 100; i++) { + test(4, null); + } + } + + static void test(int x, A a) { + Object[] objects = { new Object() }; + Object object = new Object(); + for (int i = 0; i < 150; i++) { + try { + objects[x] = object; + object = new byte[10]; + } catch (ArrayIndexOutOfBoundsException e) { + } + try { + a.foo(); + } catch (NullPointerException e) { + } + } + } + + class A { + void foo() {} + } +} From 18dc186a8f4820ed78c21173713dd127ef512e1f Mon Sep 17 00:00:00 2001 From: Damon Nguyen Date: Wed, 17 Sep 2025 18:07:29 +0000 Subject: [PATCH 087/556] 8367790: Remove java/awt/PopupMenu/PopupMenuLocation.java from ProblemList Reviewed-by: aivanov, azvegint --- test/jdk/ProblemList.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 528a33c1bd5..d1987c873a6 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -471,7 +471,7 @@ java/awt/event/MouseEvent/ClickDuringKeypress/ClickDuringKeypress.java 8233568 m java/awt/event/KeyEvent/DeadKey/DeadKeyMacOSXInputText.java 8233568 macosx-all java/awt/event/KeyEvent/DeadKey/deadKeyMacOSX.java 8233568 macosx-all java/awt/TrayIcon/RightClickWhenBalloonDisplayed/RightClickWhenBalloonDisplayed.java 8238720 windows-all -java/awt/PopupMenu/PopupMenuLocation.java 8259913,8315878 windows-all,macosx-aarch64 +java/awt/PopupMenu/PopupMenuLocation.java 8259913 windows-all java/awt/GridLayout/ComponentPreferredSize/ComponentPreferredSize.java 8238720,8324782 windows-all,macosx-all java/awt/GridLayout/ChangeGridSize/ChangeGridSize.java 8238720,8324782 windows-all,macosx-all java/awt/GridBagLayout/ComponentShortage.java 8355280 windows-all,linux-all From f7ce3a1b5f38143f17b5015ca5b714ec0e708f54 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Wed, 17 Sep 2025 19:22:30 +0000 Subject: [PATCH 088/556] 8365790: Shutdown hook for application image does not work on Windows Reviewed-by: almatvee --- .../native/applauncher/WinLauncher.cpp | 25 +++- .../windows/native/common/Executor.cpp | 6 +- .../windows/native/common/Executor.h | 13 +- .../tools/jpackage/apps/UseShutdownHook.java | 88 ++++++++++++ .../helpers/jdk/jpackage/test/CfgFile.java | 12 +- .../helpers/jdk/jpackage/test/TKit.java | 63 +++++---- .../jpackage/resources/Win8365790Test.ps1 | 83 ++++++++++++ .../jpackage/windows/Win8365790Test.java | 128 ++++++++++++++++++ 8 files changed, 380 insertions(+), 38 deletions(-) create mode 100644 test/jdk/tools/jpackage/apps/UseShutdownHook.java create mode 100644 test/jdk/tools/jpackage/resources/Win8365790Test.ps1 create mode 100644 test/jdk/tools/jpackage/windows/Win8365790Test.java diff --git a/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp b/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp index fe59cd94b84..8b3c3bb6aef 100644 --- a/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp +++ b/src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -256,6 +256,16 @@ bool needRestartLauncher(AppLauncher& appLauncher, CfgFile& cfgFile) { } +void enableConsoleCtrlHandler(bool enable) { + if (!SetConsoleCtrlHandler(NULL, enable ? FALSE : TRUE)) { + JP_THROW(SysError(tstrings::any() << "SetConsoleCtrlHandler(NULL, " + << (enable ? "FALSE" : "TRUE") + << ") failed", + SetConsoleCtrlHandler)); + } +} + + void launchApp() { // [RT-31061] otherwise UI can be left in back of other windows. ::AllowSetForegroundWindow(ASFW_ANY); @@ -310,6 +320,19 @@ void launchApp() { exec.arg(arg); }); + exec.afterProcessCreated([&](HANDLE pid) { + // + // Ignore Ctrl+C in the current process. + // This will prevent child process termination without allowing + // it to handle Ctrl+C events. + // + // Disable the default Ctrl+C handler *after* the child process + // has been created as it is inheritable and we want the child + // process to have the default handler. + // + enableConsoleCtrlHandler(false); + }); + DWORD exitCode = RunExecutorWithMsgLoop::apply(exec); exit(exitCode); diff --git a/src/jdk.jpackage/windows/native/common/Executor.cpp b/src/jdk.jpackage/windows/native/common/Executor.cpp index edb850afdbb..dfb6b299e5d 100644 --- a/src/jdk.jpackage/windows/native/common/Executor.cpp +++ b/src/jdk.jpackage/windows/native/common/Executor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -161,6 +161,10 @@ UniqueHandle Executor::startProcess(UniqueHandle* threadHandle) const { } } + if (afterProcessCreatedCallback) { + afterProcessCreatedCallback(processInfo.hProcess); + } + // Return process handle. return UniqueHandle(processInfo.hProcess); } diff --git a/src/jdk.jpackage/windows/native/common/Executor.h b/src/jdk.jpackage/windows/native/common/Executor.h index a6edcbd4f76..09c9f85bac6 100644 --- a/src/jdk.jpackage/windows/native/common/Executor.h +++ b/src/jdk.jpackage/windows/native/common/Executor.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,8 @@ #ifndef EXECUTOR_H #define EXECUTOR_H +#include + #include "tstrings.h" #include "UniqueHandle.h" @@ -97,6 +99,14 @@ public: */ int execAndWaitForExit() const; + /** + * Call provided function after the process hass been created. + */ + Executor& afterProcessCreated(const std::function& v) { + afterProcessCreatedCallback = v; + return *this; + } + private: UniqueHandle startProcess(UniqueHandle* threadHandle=0) const; @@ -106,6 +116,7 @@ private: HANDLE jobHandle; tstring_array argsArray; std::wstring appPath; + std::function afterProcessCreatedCallback; }; #endif // #ifndef EXECUTOR_H diff --git a/test/jdk/tools/jpackage/apps/UseShutdownHook.java b/test/jdk/tools/jpackage/apps/UseShutdownHook.java new file mode 100644 index 00000000000..c558ee85701 --- /dev/null +++ b/test/jdk/tools/jpackage/apps/UseShutdownHook.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +public class UseShutdownHook { + + public static void main(String[] args) throws InterruptedException { + trace("Started"); + + var outputFile = Path.of(args[0]); + trace(String.format("Write output in [%s] file", outputFile)); + + var shutdownTimeoutSeconds = Integer.parseInt(args[1]); + trace(String.format("Automatically shutdown the app in %ss", shutdownTimeoutSeconds)); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + output(outputFile, "shutdown hook executed"); + } + }); + + var startTime = System.currentTimeMillis(); + var lock = new Object(); + do { + synchronized (lock) { + lock.wait(shutdownTimeoutSeconds * 1000); + } + } while ((System.currentTimeMillis() - startTime) < (shutdownTimeoutSeconds * 1000)); + + output(outputFile, "exit"); + } + + private static void output(Path outputFilePath, String msg) { + + trace(String.format("Writing [%s] into [%s]", msg, outputFilePath)); + + try { + Files.createDirectories(outputFilePath.getParent()); + Files.writeString(outputFilePath, msg, StandardOpenOption.APPEND, StandardOpenOption.CREATE); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static void trace(String msg) { + Date time = new Date(System.currentTimeMillis()); + msg = String.format("UseShutdownHook [%s]: %s", SDF.format(time), msg); + System.out.println(msg); + try { + Files.write(traceFile, List.of(msg), StandardOpenOption.APPEND, StandardOpenOption.CREATE); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss.SSS"); + + private static final Path traceFile = Path.of(System.getProperty("jpackage.test.trace-file")); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java index 9df28b3915e..52e8ecf819b 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -59,13 +59,18 @@ public final class CfgFile { } } - public void addValue(String sectionName, String key, String value) { + public CfgFile addValue(String sectionName, String key, String value) { var section = getSection(sectionName); if (section == null) { section = new Section(sectionName, new ArrayList<>()); data.add(section); } section.data.add(Map.entry(key, value)); + return this; + } + + public CfgFile add(CfgFile other) { + return combine(this, other); } public CfgFile() { @@ -89,7 +94,7 @@ public final class CfgFile { this.id = id; } - public void save(Path path) { + public CfgFile save(Path path) { var lines = data.stream().flatMap(section -> { return Stream.concat( Stream.of(String.format("[%s]", section.name)), @@ -98,6 +103,7 @@ public final class CfgFile { })); }); TKit.createTextFile(path, lines); + return this; } private Section getSection(String name) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index a94dfa135c1..b3f188bb371 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -41,7 +41,6 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; -import java.nio.file.WatchService; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; @@ -611,44 +610,44 @@ public final class TKit { trace(String.format("Wait for file [%s] to be available", fileToWaitFor.toAbsolutePath())); - WatchService ws = FileSystems.getDefault().newWatchService(); + try (var ws = FileSystems.getDefault().newWatchService()) { - Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent(); - watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY); + Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent(); + watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY); - var waitUntil = Instant.now().plus(timeout); - for (;;) { - var remainderTimeout = Instant.now().until(waitUntil); - assertTrue(remainderTimeout.isPositive(), String.format( - "Check timeout value %dms is positive", remainderTimeout.toMillis())); + var waitUntil = Instant.now().plus(timeout); + for (;;) { + var remainderTimeout = Instant.now().until(waitUntil); + assertTrue(remainderTimeout.isPositive(), String.format( + "Check timeout value %dms is positive", remainderTimeout.toMillis())); - WatchKey key = ThrowingSupplier.toSupplier(() -> { - return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS); - }).get(); - if (key == null) { - if (Files.exists(fileToWaitFor)) { - trace(String.format( - "File [%s] is available after poll timeout expired", - fileToWaitFor)); - return; + WatchKey key = ThrowingSupplier.toSupplier(() -> { + return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS); + }).get(); + if (key == null) { + if (Files.exists(fileToWaitFor)) { + trace(String.format( + "File [%s] is available after poll timeout expired", + fileToWaitFor)); + return; + } + assertUnexpected(String.format("Timeout %dms expired", remainderTimeout.toMillis())); } - assertUnexpected(String.format("Timeout %dms expired", remainderTimeout.toMillis())); - } - for (WatchEvent event : key.pollEvents()) { - if (event.kind() == StandardWatchEventKinds.OVERFLOW) { - continue; + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + continue; + } + Path contextPath = (Path) event.context(); + if (Files.exists(fileToWaitFor) && Files.isSameFile(watchDirectory.resolve(contextPath), fileToWaitFor)) { + trace(String.format("File [%s] is available", fileToWaitFor)); + return; + } } - Path contextPath = (Path) event.context(); - if (Files.isSameFile(watchDirectory.resolve(contextPath), - fileToWaitFor)) { - trace(String.format("File [%s] is available", fileToWaitFor)); - return; - } - } - if (!key.reset()) { - assertUnexpected("Watch key invalidated"); + if (!key.reset()) { + assertUnexpected("Watch key invalidated"); + } } } } diff --git a/test/jdk/tools/jpackage/resources/Win8365790Test.ps1 b/test/jdk/tools/jpackage/resources/Win8365790Test.ps1 new file mode 100644 index 00000000000..3a7d8c9a90b --- /dev/null +++ b/test/jdk/tools/jpackage/resources/Win8365790Test.ps1 @@ -0,0 +1,83 @@ +# +# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +param ( + # Path to executable to start. + [Parameter(Mandatory=$true)] + [string]$Executable, + + # Timeout to wait after the executable has been started. + [Parameter(Mandatory=$true)] + [double]$TimeoutSeconds +) + +$type = @{ + TypeDefinition = @' +using System; +using System.Runtime.InteropServices; + +namespace Stuff { + + internal struct Details { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); + } + + public struct Facade { + public static void GenerateConsoleCtrlEvent() { + if (!Details.GenerateConsoleCtrlEvent(0, 0)) { + reportLastErrorAndExit("GenerateConsoleCtrlEvent"); + } + } + + internal static void reportLastErrorAndExit(String func) { + int errorCode = Marshal.GetLastWin32Error(); + Console.Error.WriteLine(func + " function failed with error code: " + errorCode); + Environment.Exit(100); + } + } +} +'@ +} +Add-Type @type + +Set-PSDebug -Trace 2 + +# Launch the target executable. +# `-NoNewWindow` parameter will attach the started process to the existing console. +$childProc = Start-Process -PassThru -NoNewWindow $Executable + +# Wait a bit to let the started process complete initialization. +Start-Sleep -Seconds $TimeoutSeconds + +# Call GenerateConsoleCtrlEvent to send a CTRL+C event to the launched executable. +# CTRL+C event will be sent to all processes attached to the console of the current process, +# i.e., it will be sent to this PowerShell process and to the started $Executable process because +# it was configured to attach to the existing console (the console of this PowerShell process). +[Stuff.Facade]::GenerateConsoleCtrlEvent() + +# Wait for child process termination +Wait-Process -InputObject $childProc + +Exit 0 diff --git a/test/jdk/tools/jpackage/windows/Win8365790Test.java b/test/jdk/tools/jpackage/windows/Win8365790Test.java new file mode 100644 index 00000000000..6376a16cecc --- /dev/null +++ b/test/jdk/tools/jpackage/windows/Win8365790Test.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import static jdk.jpackage.test.HelloApp.configureAndExecute; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import jdk.jpackage.test.AdditionalLauncher; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.CfgFile; +import jdk.jpackage.test.Executor; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.LauncherVerifier; +import jdk.jpackage.test.TKit; + +/** + * Test the child process has a chance to handle Ctrl+C signal. + */ + +/* + * @test + * @summary Test case for JDK-8365790 + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.* + * @build Win8365790Test + * @requires (os.family == "windows") + * @run main/othervm/timeout=100 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=Win8365790Test + */ +public class Win8365790Test { + + @Test + public void test() throws InterruptedException, IOException { + + var outputDir = TKit.createTempDirectory("response-dir"); + + var mainOutputFile = outputDir.resolve("output.txt"); + var mainTraceFile = outputDir.resolve("trace.txt"); + + var probeOutputFile = outputDir.resolve("probe-output.txt"); + var probeTraceFile = outputDir.resolve("probe-trace.txt"); + + var cmd = JPackageCommand + .helloAppImage(TEST_APP_JAVA + "*UseShutdownHook") + .ignoreFakeRuntime() + .addArguments("--java-options", "-Djpackage.test.trace-file=" + mainTraceFile.toString()) + .addArguments("--arguments", mainOutputFile.toString()) + .addArguments("--arguments", Long.toString(Duration.ofSeconds(TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS).getSeconds())); + + new AdditionalLauncher("probe") + .withoutVerifyActions(LauncherVerifier.Action.values()) + .addJavaOptions("-Djpackage.test.trace-file=" + probeTraceFile.toString()) + .addDefaultArguments(probeOutputFile.toString(), Long.toString(Duration.ofSeconds(TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS).getSeconds())) + .applyTo(cmd); + + cmd.executeAndAssertImageCreated(); + + cmd.readLauncherCfgFile("probe") + .add(new CfgFile().addValue("Application", "win.norestart", Boolean.TRUE.toString())) + .save(cmd.appLauncherCfgPath("probe")); + + // Try Ctrl+C signal on a launcher with disabled restart functionality. + // It will create a single launcher process instead of the parent and the child processes. + // Ctrl+C always worked for launcher with disabled restart functionality. + var probeOutput = runLauncher(cmd, "probe", probeTraceFile, probeOutputFile); + + if (!probeOutput.equals("shutdown hook executed")) { + // Ctrl+C signal didn't make it. Test environment doesn't support Ctrl+C signal + // delivery from the prowershell process to a child process, don't run the main + // test. + TKit.throwSkippedException( + "The environment does NOT support Ctrl+C signal delivery from the prowershell process to a child process"); + } + + var mainOutput = runLauncher(cmd, null, mainTraceFile, mainOutputFile); + + TKit.assertEquals("shutdown hook executed", mainOutput, "Check shutdown hook executed"); + } + + private static String runLauncher(JPackageCommand cmd, String launcherName, Path traceFile, Path outputFile) throws IOException { + // Launch the specified launcher and send Ctrl+C signal to it. + Thread.ofVirtual().start(() -> { + configureAndExecute(0, Executor.of("powershell", "-NonInteractive", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Unrestricted") + .addArgument("-File").addArgument(TEST_PS1) + .addArguments("-TimeoutSeconds", Long.toString(Duration.ofSeconds(5).getSeconds())) + .addArgument("-Executable").addArgument(cmd.appLauncherPath(launcherName)) + .dumpOutput()); + }); + + TKit.waitForFileCreated(traceFile, Duration.ofSeconds(20), Duration.ofSeconds(2)); + + try { + TKit.waitForFileCreated(outputFile, Duration.ofSeconds(TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS * 2), Duration.ofSeconds(2)); + } finally { + TKit.traceFileContents(traceFile, "Test app trace"); + } + + TKit.assertFileExists(outputFile); + return Files.readString(outputFile); + } + + private static final long TETS_APP_AUTOCLOSE_TIMEOUT_SECONDS = 30; + + private static final Path TEST_APP_JAVA = TKit.TEST_SRC_ROOT.resolve("apps/UseShutdownHook.java"); + private static final Path TEST_PS1 = TKit.TEST_SRC_ROOT.resolve(Path.of("resources/Win8365790Test.ps1")).normalize(); +} From 0756ecb214b8ab76cb69f354063d153b72f978c2 Mon Sep 17 00:00:00 2001 From: Roger Riggs Date: Wed, 17 Sep 2025 19:49:22 +0000 Subject: [PATCH 089/556] 8367031: [backout] Change java.time month/day field types to 'byte' Reviewed-by: alanb, liach, naoto, iris --- .../share/classes/java/time/LocalDate.java | 8 ++++---- .../share/classes/java/time/MonthDay.java | 8 ++++---- .../share/classes/java/time/YearMonth.java | 4 ++-- .../share/classes/java/time/chrono/HijrahDate.java | 14 +++++++------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/java.base/share/classes/java/time/LocalDate.java b/src/java.base/share/classes/java/time/LocalDate.java index 6724410da2b..016bdab5394 100644 --- a/src/java.base/share/classes/java/time/LocalDate.java +++ b/src/java.base/share/classes/java/time/LocalDate.java @@ -182,11 +182,11 @@ public final class LocalDate /** * @serial The month-of-year. */ - private final byte month; + private final short month; /** * @serial The day-of-month. */ - private final byte day; + private final short day; //----------------------------------------------------------------------- /** @@ -490,8 +490,8 @@ public final class LocalDate */ private LocalDate(int year, int month, int dayOfMonth) { this.year = year; - this.month = (byte) month; - this.day = (byte) dayOfMonth; + this.month = (short) month; + this.day = (short) dayOfMonth; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/MonthDay.java b/src/java.base/share/classes/java/time/MonthDay.java index 6244c14e6e1..1de4fa84d3e 100644 --- a/src/java.base/share/classes/java/time/MonthDay.java +++ b/src/java.base/share/classes/java/time/MonthDay.java @@ -146,11 +146,11 @@ public final class MonthDay /** * @serial The month-of-year, not null. */ - private final byte month; + private final int month; /** * @serial The day-of-month. */ - private final byte day; + private final int day; //----------------------------------------------------------------------- /** @@ -319,8 +319,8 @@ public final class MonthDay * @param dayOfMonth the day-of-month to represent, validated from 1 to 29-31 */ private MonthDay(int month, int dayOfMonth) { - this.month = (byte) month; - this.day = (byte) dayOfMonth; + this.month = month; + this.day = dayOfMonth; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/YearMonth.java b/src/java.base/share/classes/java/time/YearMonth.java index b24151de3f0..8ad1172811f 100644 --- a/src/java.base/share/classes/java/time/YearMonth.java +++ b/src/java.base/share/classes/java/time/YearMonth.java @@ -153,7 +153,7 @@ public final class YearMonth /** * @serial The month-of-year, not null. */ - private final byte month; + private final int month; //----------------------------------------------------------------------- /** @@ -306,7 +306,7 @@ public final class YearMonth */ private YearMonth(int year, int month) { this.year = year; - this.month = (byte) month; + this.month = month; } /** diff --git a/src/java.base/share/classes/java/time/chrono/HijrahDate.java b/src/java.base/share/classes/java/time/chrono/HijrahDate.java index 2d3e4f93e69..114a47e4797 100644 --- a/src/java.base/share/classes/java/time/chrono/HijrahDate.java +++ b/src/java.base/share/classes/java/time/chrono/HijrahDate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -137,11 +137,11 @@ public final class HijrahDate /** * The month-of-year. */ - private final transient byte monthOfYear; + private final transient int monthOfYear; /** * The day-of-month. */ - private final transient byte dayOfMonth; + private final transient int dayOfMonth; //------------------------------------------------------------------------- /** @@ -273,8 +273,8 @@ public final class HijrahDate this.chrono = chrono; this.prolepticYear = prolepticYear; - this.monthOfYear = (byte) monthOfYear; - this.dayOfMonth = (byte) dayOfMonth; + this.monthOfYear = monthOfYear; + this.dayOfMonth = dayOfMonth; } /** @@ -287,8 +287,8 @@ public final class HijrahDate this.chrono = chrono; this.prolepticYear = dateInfo[0]; - this.monthOfYear = (byte) dateInfo[1]; - this.dayOfMonth = (byte) dateInfo[2]; + this.monthOfYear = dateInfo[1]; + this.dayOfMonth = dateInfo[2]; } //----------------------------------------------------------------------- From f682f070079037f8fb646e91ea336af0bc778813 Mon Sep 17 00:00:00 2001 From: Kim Barrett Date: Wed, 17 Sep 2025 20:20:17 +0000 Subject: [PATCH 090/556] 8367796: Rename AtomicAccess gtests Reviewed-by: ayang, tschatzl --- .../gtest/runtime/test_atomicAccess.cpp | 103 +++++++++--------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/test/hotspot/gtest/runtime/test_atomicAccess.cpp b/test/hotspot/gtest/runtime/test_atomicAccess.cpp index 523f27ca870..6e05f429970 100644 --- a/test/hotspot/gtest/runtime/test_atomicAccess.cpp +++ b/test/hotspot/gtest/runtime/test_atomicAccess.cpp @@ -25,13 +25,14 @@ #include "runtime/atomicAccess.hpp" #include "unittest.hpp" -// These tests of Atomic only verify functionality. They don't verify atomicity. +// These tests of AtomicAccess only verify functionality. They don't verify +// atomicity. template -struct AtomicAddTestSupport { +struct AtomicAccessAddTestSupport { volatile T _test_value; - AtomicAddTestSupport() : _test_value{} {} + AtomicAccessAddTestSupport() : _test_value{} {} void test_add() { T zero = 0; @@ -52,19 +53,19 @@ struct AtomicAddTestSupport { } }; -TEST_VM(AtomicAddTest, int32) { - using Support = AtomicAddTestSupport; +TEST_VM(AtomicAccessAddTest, int32) { + using Support = AtomicAccessAddTestSupport; Support().test_add(); Support().test_fetch_add(); } -TEST_VM(AtomicAddTest, int64) { - using Support = AtomicAddTestSupport; +TEST_VM(AtomicAccessAddTest, int64) { + using Support = AtomicAccessAddTestSupport; Support().test_add(); Support().test_fetch_add(); } -TEST_VM(AtomicAddTest, ptr) { +TEST_VM(AtomicAccessAddTest, ptr) { uint _test_values[10] = {}; uint* volatile _test_value{}; @@ -84,10 +85,10 @@ TEST_VM(AtomicAddTest, ptr) { }; template -struct AtomicXchgTestSupport { +struct AtomicAccessXchgTestSupport { volatile T _test_value; - AtomicXchgTestSupport() : _test_value{} {} + AtomicAccessXchgTestSupport() : _test_value{} {} void test() { T zero = 0; @@ -99,21 +100,21 @@ struct AtomicXchgTestSupport { } }; -TEST_VM(AtomicXchgTest, int32) { - using Support = AtomicXchgTestSupport; +TEST_VM(AtomicAccessXchgTest, int32) { + using Support = AtomicAccessXchgTestSupport; Support().test(); } -TEST_VM(AtomicXchgTest, int64) { - using Support = AtomicXchgTestSupport; +TEST_VM(AtomicAccessXchgTest, int64) { + using Support = AtomicAccessXchgTestSupport; Support().test(); } template -struct AtomicCmpxchgTestSupport { +struct AtomicAccessCmpxchgTestSupport { volatile T _test_value; - AtomicCmpxchgTestSupport() : _test_value{} {} + AtomicAccessCmpxchgTestSupport() : _test_value{} {} void test() { T zero = 0; @@ -129,25 +130,25 @@ struct AtomicCmpxchgTestSupport { } }; -TEST_VM(AtomicCmpxchgTest, int32) { - using Support = AtomicCmpxchgTestSupport; +TEST_VM(AtomicAccessCmpxchgTest, int32) { + using Support = AtomicAccessCmpxchgTestSupport; Support().test(); } -TEST_VM(AtomicCmpxchgTest, int64) { +TEST_VM(AtomicAccessCmpxchgTest, int64) { // Check if 64-bit atomics are available on the machine. if (!VM_Version::supports_cx8()) return; - using Support = AtomicCmpxchgTestSupport; + using Support = AtomicAccessCmpxchgTestSupport; Support().test(); } -struct AtomicCmpxchg1ByteStressSupport { +struct AtomicAccessCmpxchg1ByteStressSupport { char _default_val; int _base; char _array[7+32+7]; - AtomicCmpxchg1ByteStressSupport() : _default_val(0x7a), _base(7), _array{} {} + AtomicAccessCmpxchg1ByteStressSupport() : _default_val(0x7a), _base(7), _array{} {} void validate(char val, char val2, int index) { for (int i = 0; i < 7; i++) { @@ -182,16 +183,16 @@ struct AtomicCmpxchg1ByteStressSupport { } }; -TEST_VM(AtomicCmpxchg1Byte, stress) { - AtomicCmpxchg1ByteStressSupport support; +TEST_VM(AtomicAccessCmpxchg1Byte, stress) { + AtomicAccessCmpxchg1ByteStressSupport support; support.test(); } template -struct AtomicEnumTestSupport { +struct AtomicAccessEnumTestSupport { volatile T _test_value; - AtomicEnumTestSupport() : _test_value{} {} + AtomicAccessEnumTestSupport() : _test_value{} {} void test_store_load(T value) { EXPECT_NE(value, AtomicAccess::load(&_test_value)); @@ -216,25 +217,25 @@ struct AtomicEnumTestSupport { } }; -namespace AtomicEnumTestUnscoped { // Scope the enumerators. +namespace AtomicAccessEnumTestUnscoped { // Scope the enumerators. enum TestEnum { A, B, C }; } -TEST_VM(AtomicEnumTest, unscoped_enum) { - using namespace AtomicEnumTestUnscoped; - using Support = AtomicEnumTestSupport; +TEST_VM(AtomicAccessEnumTest, unscoped_enum) { + using namespace AtomicAccessEnumTestUnscoped; + using Support = AtomicAccessEnumTestSupport; Support().test_store_load(B); Support().test_cmpxchg(B, C); Support().test_xchg(B, C); } -enum class AtomicEnumTestScoped { A, B, C }; +enum class AtomicAccessEnumTestScoped { A, B, C }; -TEST_VM(AtomicEnumTest, scoped_enum) { - const AtomicEnumTestScoped B = AtomicEnumTestScoped::B; - const AtomicEnumTestScoped C = AtomicEnumTestScoped::C; - using Support = AtomicEnumTestSupport; +TEST_VM(AtomicAccessEnumTest, scoped_enum) { + const AtomicAccessEnumTestScoped B = AtomicAccessEnumTestScoped::B; + const AtomicAccessEnumTestScoped C = AtomicAccessEnumTestScoped::C; + using Support = AtomicAccessEnumTestSupport; Support().test_store_load(B); Support().test_cmpxchg(B, C); @@ -242,14 +243,14 @@ TEST_VM(AtomicEnumTest, scoped_enum) { } template -struct AtomicBitopsTestSupport { +struct AtomicAccessBitopsTestSupport { volatile T _test_value; // At least one byte differs between _old_value and _old_value op _change_value. static const T _old_value = static_cast(UCONST64(0x7f5300007f530044)); static const T _change_value = static_cast(UCONST64(0x3800530038005322)); - AtomicBitopsTestSupport() : _test_value(0) {} + AtomicAccessBitopsTestSupport() : _test_value(0) {} void fetch_then_and() { AtomicAccess::store(&_test_value, _old_value); @@ -320,31 +321,31 @@ struct AtomicBitopsTestSupport { }; template -const T AtomicBitopsTestSupport::_old_value; +const T AtomicAccessBitopsTestSupport::_old_value; template -const T AtomicBitopsTestSupport::_change_value; +const T AtomicAccessBitopsTestSupport::_change_value; -TEST_VM(AtomicBitopsTest, int8) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, int8) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, uint8) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, uint8) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, int32) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, int32) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, uint32) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, uint32) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, int64) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, int64) { + AtomicAccessBitopsTestSupport()(); } -TEST_VM(AtomicBitopsTest, uint64) { - AtomicBitopsTestSupport()(); +TEST_VM(AtomicAccessBitopsTest, uint64) { + AtomicAccessBitopsTestSupport()(); } From 919f5faa4618473eddab39d65fe7c1cc732600b7 Mon Sep 17 00:00:00 2001 From: Joe Darcy Date: Wed, 17 Sep 2025 20:47:11 +0000 Subject: [PATCH 091/556] 8367787: Expand use of representation equivalence terminology in Float16 Reviewed-by: psandoz --- .../share/classes/jdk/incubator/vector/Float16.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java index 5d6a4bbfecb..3202ca2ba25 100644 --- a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java +++ b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java @@ -890,7 +890,9 @@ public final class Float16 * is {@code true} if and only if the argument is not * {@code null} and is a {@code Float16} object that * represents a {@code Float16} that has the same value as the - * {@code double} represented by this object. + * {@code Float16} represented by this object. + * {@linkplain Double##repEquivalence Representation + * equivalence} is used to compare the {@code Float16} values. * * @jls 15.21.1 Numerical Equality Operators == and != */ @@ -988,6 +990,13 @@ public final class Float16 /** * Compares the two specified {@code Float16} values. * + * @apiNote + * One idiom to implement {@linkplain Double##repEquivalence + * representation equivalence} on {@code Float16} values is + * {@snippet lang="java" : + * Float16.compare(a, b) == 0 + * } + * * @param f1 the first {@code Float16} to compare * @param f2 the second {@code Float16} to compare * @return the value {@code 0} if {@code f1} is From aa36799acb5834d730400fb073a9a3a8ee3c28ef Mon Sep 17 00:00:00 2001 From: Vladimir Ivanov Date: Wed, 17 Sep 2025 21:34:15 +0000 Subject: [PATCH 092/556] 8367333: C2: Vector math operation intrinsification failure Reviewed-by: epeter, shade, jbhateja --- src/hotspot/share/prims/vectorSupport.cpp | 19 +++ src/hotspot/share/prims/vectorSupport.hpp | 19 +++ .../compiler/vectorapi/TestVectorMathLib.java | 129 ++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java diff --git a/src/hotspot/share/prims/vectorSupport.cpp b/src/hotspot/share/prims/vectorSupport.cpp index 002f737e788..a98cad07227 100644 --- a/src/hotspot/share/prims/vectorSupport.cpp +++ b/src/hotspot/share/prims/vectorSupport.cpp @@ -592,6 +592,25 @@ int VectorSupport::vop2ideal(jint id, BasicType bt) { break; } + case VECTOR_OP_TAN: // fall-through + case VECTOR_OP_TANH: // fall-through + case VECTOR_OP_SIN: // fall-through + case VECTOR_OP_SINH: // fall-through + case VECTOR_OP_COS: // fall-through + case VECTOR_OP_COSH: // fall-through + case VECTOR_OP_ASIN: // fall-through + case VECTOR_OP_ACOS: // fall-through + case VECTOR_OP_ATAN: // fall-through + case VECTOR_OP_ATAN2: // fall-through + case VECTOR_OP_CBRT: // fall-through + case VECTOR_OP_LOG: // fall-through + case VECTOR_OP_LOG10: // fall-through + case VECTOR_OP_LOG1P: // fall-through + case VECTOR_OP_POW: // fall-through + case VECTOR_OP_EXP: // fall-through + case VECTOR_OP_EXPM1: // fall-through + case VECTOR_OP_HYPOT: return 0; // not supported; should be handled in Java code + default: fatal("unknown op: %d", vop); } return 0; // Unimplemented diff --git a/src/hotspot/share/prims/vectorSupport.hpp b/src/hotspot/share/prims/vectorSupport.hpp index 5ba18cdfaa8..9ec6500543c 100644 --- a/src/hotspot/share/prims/vectorSupport.hpp +++ b/src/hotspot/share/prims/vectorSupport.hpp @@ -101,6 +101,25 @@ class VectorSupport : AllStatic { VECTOR_OP_COMPRESS_BITS = 33, VECTOR_OP_EXPAND_BITS = 34, + VECTOR_OP_TAN = 101, + VECTOR_OP_TANH = 102, + VECTOR_OP_SIN = 103, + VECTOR_OP_SINH = 104, + VECTOR_OP_COS = 105, + VECTOR_OP_COSH = 106, + VECTOR_OP_ASIN = 107, + VECTOR_OP_ACOS = 108, + VECTOR_OP_ATAN = 109, + VECTOR_OP_ATAN2 = 110, + VECTOR_OP_CBRT = 111, + VECTOR_OP_LOG = 112, + VECTOR_OP_LOG10 = 113, + VECTOR_OP_LOG1P = 114, + VECTOR_OP_POW = 115, + VECTOR_OP_EXP = 116, + VECTOR_OP_EXPM1 = 117, + VECTOR_OP_HYPOT = 118, + VECTOR_OP_SADD = 119, VECTOR_OP_SSUB = 120, VECTOR_OP_SUADD = 121, diff --git a/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java b/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java new file mode 100644 index 00000000000..112220262b6 --- /dev/null +++ b/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package compiler.vectorapi; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; + +/* + * @test + * @bug 8367333 + * @requires vm.compiler2.enabled + * @modules jdk.incubator.vector + * @library /test/lib + * + * @run main/othervm -Xbatch -XX:-TieredCompilation -XX:+StressIncrementalInlining -XX:CompileCommand=quiet + * -XX:CompileCommand=compileonly,compiler.vectorapi.TestVectorMathLib::test* + * compiler.vectorapi.TestVectorMathLib + */ + +public class TestVectorMathLib { + private static final VectorSpecies SPECIES = FloatVector.SPECIES_PREFERRED; + + static FloatVector testTAN(FloatVector fv) { + return fv.lanewise(VectorOperators.TAN); + } + static FloatVector testTANH(FloatVector fv) { + return fv.lanewise(VectorOperators.TANH); + } + static FloatVector testSIN(FloatVector fv) { + return fv.lanewise(VectorOperators.SIN); + } + static FloatVector testSINH(FloatVector fv) { + return fv.lanewise(VectorOperators.SINH); + } + static FloatVector testCOS(FloatVector fv) { + return fv.lanewise(VectorOperators.COS); + } + static FloatVector testCOSH(FloatVector fv) { + return fv.lanewise(VectorOperators.COSH); + } + static FloatVector testASIN(FloatVector fv) { + return fv.lanewise(VectorOperators.ASIN); + } + static FloatVector testACOS(FloatVector fv) { + return fv.lanewise(VectorOperators.ACOS); + } + static FloatVector testATAN(FloatVector fv) { + return fv.lanewise(VectorOperators.ATAN); + } + static FloatVector testATAN2(FloatVector fv) { + return fv.lanewise(VectorOperators.ATAN2, fv); + } + static FloatVector testCBRT(FloatVector fv) { + return fv.lanewise(VectorOperators.CBRT); + } + static FloatVector testLOG(FloatVector fv) { + return fv.lanewise(VectorOperators.LOG); + } + static FloatVector testLOG10(FloatVector fv) { + return fv.lanewise(VectorOperators.LOG10); + } + static FloatVector testLOG1P(FloatVector fv) { + return fv.lanewise(VectorOperators.LOG1P); + } + static FloatVector testPOW(FloatVector fv) { + return fv.lanewise(VectorOperators.POW, fv); + } + static FloatVector testEXP(FloatVector fv) { + return fv.lanewise(VectorOperators.EXP); + } + static FloatVector testEXPM1(FloatVector fv) { + return fv.lanewise(VectorOperators.EXPM1); + } + static FloatVector testHYPOT(FloatVector fv) { + return fv.lanewise(VectorOperators.HYPOT, fv); + } + + public static void main(String[] args) { + FloatVector z = FloatVector.zero(SPECIES); + for (int i = 0; i < 20_000; i++) { + z.neg(); // unary + z.add(z); // binary + + testTAN(z); + testTANH(z); + testSIN(z); + testSINH(z); + testCOS(z); + testCOSH(z); + testASIN(z); + testACOS(z); + testATAN(z); + testATAN2(z); + testCBRT(z); + testLOG(z); + testLOG10(z); + testLOG1P(z); + testPOW(z); + testEXP(z); + testEXPM1(z); + testHYPOT(z); + } + + System.out.println("TEST PASSED"); + } +} + From 91a979430e2516b5853c397a336837799928f478 Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Thu, 18 Sep 2025 02:49:10 +0000 Subject: [PATCH 093/556] 8367366: Do not support -XX:+AOTClassLinking for dynamic CDS archive Reviewed-by: kvn, asmehra --- src/hotspot/share/cds/aotClassLinker.cpp | 13 ++--------- .../share/cds/aotLinkedClassBulkLoader.cpp | 22 ++++++++----------- .../share/cds/aotLinkedClassBulkLoader.hpp | 2 +- src/hotspot/share/cds/aotLinkedClassTable.cpp | 3 +-- src/hotspot/share/cds/aotLinkedClassTable.hpp | 12 +++------- src/hotspot/share/cds/aotMetaspace.cpp | 4 ++-- src/hotspot/share/cds/archiveBuilder.cpp | 8 ------- src/hotspot/share/cds/archiveBuilder.hpp | 1 - src/hotspot/share/cds/cdsConfig.cpp | 16 +++++++++----- src/hotspot/share/cds/dynamicArchive.cpp | 13 ++++++----- src/hotspot/share/cds/dynamicArchive.hpp | 2 +- .../classfile/systemDictionaryShared.cpp | 5 ----- test/hotspot/jtreg/TEST.groups | 12 +--------- .../aotClassLinking/BulkLoaderTest.java | 18 --------------- .../resolvedConstants/ResolvedConstants.java | 4 +++- 15 files changed, 41 insertions(+), 94 deletions(-) diff --git a/src/hotspot/share/cds/aotClassLinker.cpp b/src/hotspot/share/cds/aotClassLinker.cpp index 1f9a03de83f..0eb8f141c20 100644 --- a/src/hotspot/share/cds/aotClassLinker.cpp +++ b/src/hotspot/share/cds/aotClassLinker.cpp @@ -191,7 +191,7 @@ void AOTClassLinker::write_to_archive() { assert_at_safepoint(); if (CDSConfig::is_dumping_aot_linked_classes()) { - AOTLinkedClassTable* table = AOTLinkedClassTable::get(CDSConfig::is_dumping_static_archive()); + AOTLinkedClassTable* table = AOTLinkedClassTable::get(); table->set_boot(write_classes(nullptr, true)); table->set_boot2(write_classes(nullptr, false)); table->set_platform(write_classes(SystemDictionary::java_platform_loader(), false)); @@ -212,16 +212,7 @@ Array* AOTClassLinker::write_classes(oop class_loader, bool is_j continue; } - if (ik->in_aot_cache() && CDSConfig::is_dumping_dynamic_archive()) { - if (CDSConfig::is_using_aot_linked_classes()) { - // This class was recorded as AOT-linked for the base archive, - // so there's no need to do so again for the dynamic archive. - } else { - list.append(ik); - } - } else { - list.append(ArchiveBuilder::current()->get_buffered_addr(ik)); - } + list.append(ArchiveBuilder::current()->get_buffered_addr(ik)); } if (list.length() == 0) { diff --git a/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp b/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp index 6e5816cd589..8795a29fd5c 100644 --- a/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp +++ b/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp @@ -46,8 +46,8 @@ bool AOTLinkedClassBulkLoader::_platform_completed = false; bool AOTLinkedClassBulkLoader::_app_completed = false; bool AOTLinkedClassBulkLoader::_all_completed = false; -void AOTLinkedClassBulkLoader::serialize(SerializeClosure* soc, bool is_static_archive) { - AOTLinkedClassTable::get(is_static_archive)->serialize(soc); +void AOTLinkedClassBulkLoader::serialize(SerializeClosure* soc) { + AOTLinkedClassTable::get()->serialize(soc); } bool AOTLinkedClassBulkLoader::class_preloading_finished() { @@ -117,27 +117,24 @@ void AOTLinkedClassBulkLoader::exit_on_exception(JavaThread* current) { void AOTLinkedClassBulkLoader::load_classes_in_loader_impl(AOTLinkedClassCategory class_category, oop class_loader_oop, TRAPS) { Handle h_loader(THREAD, class_loader_oop); - load_table(AOTLinkedClassTable::for_static_archive(), class_category, h_loader, CHECK); - load_table(AOTLinkedClassTable::for_dynamic_archive(), class_category, h_loader, CHECK); + AOTLinkedClassTable* table = AOTLinkedClassTable::get(); + load_table(table, class_category, h_loader, CHECK); // Initialize the InstanceKlasses of all archived heap objects that are reachable from the // archived java class mirrors. - // - // Only the classes in the static archive can have archived mirrors. - AOTLinkedClassTable* static_table = AOTLinkedClassTable::for_static_archive(); switch (class_category) { case AOTLinkedClassCategory::BOOT1: // Delayed until finish_loading_javabase_classes(), as the VM is not ready to // execute some of the methods. break; case AOTLinkedClassCategory::BOOT2: - init_required_classes_for_loader(h_loader, static_table->boot2(), CHECK); + init_required_classes_for_loader(h_loader, table->boot2(), CHECK); break; case AOTLinkedClassCategory::PLATFORM: - init_required_classes_for_loader(h_loader, static_table->platform(), CHECK); + init_required_classes_for_loader(h_loader, table->platform(), CHECK); break; case AOTLinkedClassCategory::APP: - init_required_classes_for_loader(h_loader, static_table->app(), CHECK); + init_required_classes_for_loader(h_loader, table->app(), CHECK); break; case AOTLinkedClassCategory::UNREGISTERED: ShouldNotReachHere(); @@ -333,7 +330,7 @@ void AOTLinkedClassBulkLoader::load_hidden_class(ClassLoaderData* loader_data, I } void AOTLinkedClassBulkLoader::finish_loading_javabase_classes(TRAPS) { - init_required_classes_for_loader(Handle(), AOTLinkedClassTable::for_static_archive()->boot(), CHECK); + init_required_classes_for_loader(Handle(), AOTLinkedClassTable::get()->boot(), CHECK); } // Some AOT-linked classes for must be initialized early. This includes @@ -427,8 +424,7 @@ void AOTLinkedClassBulkLoader::replay_training_at_init(Array* cl void AOTLinkedClassBulkLoader::replay_training_at_init_for_preloaded_classes(TRAPS) { if (CDSConfig::is_using_aot_linked_classes() && TrainingData::have_data()) { - // Only static archive can have training data. - AOTLinkedClassTable* table = AOTLinkedClassTable::for_static_archive(); + AOTLinkedClassTable* table = AOTLinkedClassTable::get(); replay_training_at_init(table->boot(), CHECK); replay_training_at_init(table->boot2(), CHECK); replay_training_at_init(table->platform(), CHECK); diff --git a/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp b/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp index 0a8b0c4d537..95e64a7ddd4 100644 --- a/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp +++ b/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp @@ -57,7 +57,7 @@ class AOTLinkedClassBulkLoader : AllStatic { static void init_required_classes_for_loader(Handle class_loader, Array* classes, TRAPS); static void replay_training_at_init(Array* classes, TRAPS) NOT_CDS_RETURN; public: - static void serialize(SerializeClosure* soc, bool is_static_archive) NOT_CDS_RETURN; + static void serialize(SerializeClosure* soc) NOT_CDS_RETURN; static void load_javabase_classes(JavaThread* current) NOT_CDS_RETURN; static void load_non_javabase_classes(JavaThread* current) NOT_CDS_RETURN; diff --git a/src/hotspot/share/cds/aotLinkedClassTable.cpp b/src/hotspot/share/cds/aotLinkedClassTable.cpp index b602c599f54..79d78b05be1 100644 --- a/src/hotspot/share/cds/aotLinkedClassTable.cpp +++ b/src/hotspot/share/cds/aotLinkedClassTable.cpp @@ -27,8 +27,7 @@ #include "cds/serializeClosure.hpp" #include "oops/array.hpp" -AOTLinkedClassTable AOTLinkedClassTable::_for_static_archive; -AOTLinkedClassTable AOTLinkedClassTable::_for_dynamic_archive; +AOTLinkedClassTable AOTLinkedClassTable::_instance; void AOTLinkedClassTable::serialize(SerializeClosure* soc) { soc->do_ptr((void**)&_boot); diff --git a/src/hotspot/share/cds/aotLinkedClassTable.hpp b/src/hotspot/share/cds/aotLinkedClassTable.hpp index 2a199c15edd..0ec733d1df7 100644 --- a/src/hotspot/share/cds/aotLinkedClassTable.hpp +++ b/src/hotspot/share/cds/aotLinkedClassTable.hpp @@ -39,10 +39,7 @@ class SerializeClosure; // in a production run. // class AOTLinkedClassTable { - // The VM may load up to 2 CDS archives -- static and dynamic. Each - // archive can have its own AOTLinkedClassTable. - static AOTLinkedClassTable _for_static_archive; - static AOTLinkedClassTable _for_dynamic_archive; + static AOTLinkedClassTable _instance; Array* _boot; // only java.base classes Array* _boot2; // boot classes in other modules @@ -54,11 +51,8 @@ public: _boot(nullptr), _boot2(nullptr), _platform(nullptr), _app(nullptr) {} - static AOTLinkedClassTable* for_static_archive() { return &_for_static_archive; } - static AOTLinkedClassTable* for_dynamic_archive() { return &_for_dynamic_archive; } - - static AOTLinkedClassTable* get(bool is_static_archive) { - return is_static_archive ? for_static_archive() : for_dynamic_archive(); + static AOTLinkedClassTable* get() { + return &_instance; } Array* boot() const { return _boot; } diff --git a/src/hotspot/share/cds/aotMetaspace.cpp b/src/hotspot/share/cds/aotMetaspace.cpp index b3f859fc4a8..341371ad6bc 100644 --- a/src/hotspot/share/cds/aotMetaspace.cpp +++ b/src/hotspot/share/cds/aotMetaspace.cpp @@ -501,7 +501,7 @@ void AOTMetaspace::serialize(SerializeClosure* soc) { StringTable::serialize_shared_table_header(soc); HeapShared::serialize_tables(soc); SystemDictionaryShared::serialize_dictionary_headers(soc); - AOTLinkedClassBulkLoader::serialize(soc, true); + AOTLinkedClassBulkLoader::serialize(soc); FinalImageRecipes::serialize(soc); TrainingData::serialize(soc); InstanceMirrorKlass::serialize_offsets(soc); @@ -2001,7 +2001,7 @@ void AOTMetaspace::initialize_shared_spaces() { if (dynamic_mapinfo != nullptr) { intptr_t* buffer = (intptr_t*)dynamic_mapinfo->serialized_data(); ReadClosure rc(&buffer, (intptr_t)SharedBaseAddress); - ArchiveBuilder::serialize_dynamic_archivable_items(&rc); + DynamicArchive::serialize(&rc); DynamicArchive::setup_array_klasses(); } diff --git a/src/hotspot/share/cds/archiveBuilder.cpp b/src/hotspot/share/cds/archiveBuilder.cpp index 77f51443bb2..41a1d3d3c6d 100644 --- a/src/hotspot/share/cds/archiveBuilder.cpp +++ b/src/hotspot/share/cds/archiveBuilder.cpp @@ -24,7 +24,6 @@ #include "cds/aotArtifactFinder.hpp" #include "cds/aotClassLinker.hpp" -#include "cds/aotLinkedClassBulkLoader.hpp" #include "cds/aotLogging.hpp" #include "cds/aotMapLogger.hpp" #include "cds/aotMetaspace.hpp" @@ -1015,13 +1014,6 @@ void ArchiveBuilder::make_training_data_shareable() { _src_obj_table.iterate_all(clean_td); } -void ArchiveBuilder::serialize_dynamic_archivable_items(SerializeClosure* soc) { - SymbolTable::serialize_shared_table_header(soc, false); - SystemDictionaryShared::serialize_dictionary_headers(soc, false); - DynamicArchive::serialize_array_klasses(soc); - AOTLinkedClassBulkLoader::serialize(soc, false); -} - uintx ArchiveBuilder::buffer_to_offset(address p) const { address requested_p = to_requested(p); assert(requested_p >= _requested_static_archive_bottom, "must be"); diff --git a/src/hotspot/share/cds/archiveBuilder.hpp b/src/hotspot/share/cds/archiveBuilder.hpp index 170e61beba8..815a6f07273 100644 --- a/src/hotspot/share/cds/archiveBuilder.hpp +++ b/src/hotspot/share/cds/archiveBuilder.hpp @@ -382,7 +382,6 @@ public: bool gather_klass_and_symbol(MetaspaceClosure::Ref* ref, bool read_only); bool gather_one_source_obj(MetaspaceClosure::Ref* ref, bool read_only); void remember_embedded_pointer_in_enclosing_obj(MetaspaceClosure::Ref* ref); - static void serialize_dynamic_archivable_items(SerializeClosure* soc); DumpRegion* pz_region() { return &_pz_region; } DumpRegion* rw_region() { return &_rw_region; } diff --git a/src/hotspot/share/cds/cdsConfig.cpp b/src/hotspot/share/cds/cdsConfig.cpp index 0505ae20a78..20bf6b0d67c 100644 --- a/src/hotspot/share/cds/cdsConfig.cpp +++ b/src/hotspot/share/cds/cdsConfig.cpp @@ -756,6 +756,13 @@ void CDSConfig::setup_compiler_args() { void CDSConfig::prepare_for_dumping() { assert(CDSConfig::is_dumping_archive(), "sanity"); + if (is_dumping_dynamic_archive() && AOTClassLinking) { + if (FLAG_IS_CMDLINE(AOTClassLinking)) { + log_warning(cds)("AOTClassLinking is not supported for dynamic CDS archive"); + } + FLAG_SET_ERGO(AOTClassLinking, false); + } + if (is_dumping_dynamic_archive() && !is_using_archive()) { assert(!is_dumping_static_archive(), "cannot be dumping both static and dynamic archives"); @@ -1014,11 +1021,10 @@ void CDSConfig::stop_using_full_module_graph(const char* reason) { } bool CDSConfig::is_dumping_aot_linked_classes() { - if (is_dumping_preimage_static_archive()) { - return false; - } else if (is_dumping_dynamic_archive()) { - return is_using_full_module_graph() && AOTClassLinking; - } else if (is_dumping_static_archive()) { + if (is_dumping_classic_static_archive() || is_dumping_final_static_archive()) { + // FMG is required to guarantee that all cached boot/platform/app classes + // are visible in the production run, so they can be unconditionally + // loaded during VM bootstrap. return is_dumping_full_module_graph() && AOTClassLinking; } else { return false; diff --git a/src/hotspot/share/cds/dynamicArchive.cpp b/src/hotspot/share/cds/dynamicArchive.cpp index 58b354b9240..0ba911b41bb 100644 --- a/src/hotspot/share/cds/dynamicArchive.cpp +++ b/src/hotspot/share/cds/dynamicArchive.cpp @@ -160,11 +160,10 @@ public: SystemDictionaryShared::write_to_archive(false); cl_config = AOTClassLocationConfig::dumptime()->write_to_archive(); DynamicArchive::dump_array_klasses(); - AOTClassLinker::write_to_archive(); serialized_data = ro_region()->top(); WriteClosure wc(ro_region()); - ArchiveBuilder::serialize_dynamic_archivable_items(&wc); + DynamicArchive::serialize(&wc); } if (CDSConfig::is_dumping_lambdas_in_legacy_mode()) { @@ -414,6 +413,12 @@ public: GrowableArray* DynamicArchive::_array_klasses = nullptr; Array* DynamicArchive::_dynamic_archive_array_klasses = nullptr; +void DynamicArchive::serialize(SerializeClosure* soc) { + SymbolTable::serialize_shared_table_header(soc, false); + SystemDictionaryShared::serialize_dictionary_headers(soc, false); + soc->do_ptr(&_dynamic_archive_array_klasses); +} + void DynamicArchive::append_array_klass(ObjArrayKlass* ak) { if (_array_klasses == nullptr) { _array_klasses = new (mtClassShared) GrowableArray(50, mtClassShared); @@ -456,10 +461,6 @@ void DynamicArchive::setup_array_klasses() { } } -void DynamicArchive::serialize_array_klasses(SerializeClosure* soc) { - soc->do_ptr(&_dynamic_archive_array_klasses); -} - void DynamicArchive::make_array_klasses_shareable() { if (_array_klasses != nullptr) { int num_array_klasses = _array_klasses->length(); diff --git a/src/hotspot/share/cds/dynamicArchive.hpp b/src/hotspot/share/cds/dynamicArchive.hpp index 19086053d76..c42c4b7dfde 100644 --- a/src/hotspot/share/cds/dynamicArchive.hpp +++ b/src/hotspot/share/cds/dynamicArchive.hpp @@ -71,7 +71,7 @@ public: static void dump_array_klasses(); static void setup_array_klasses(); static void append_array_klass(ObjArrayKlass* oak); - static void serialize_array_klasses(SerializeClosure* soc); + static void serialize(SerializeClosure* soc); static void make_array_klasses_shareable(); static void post_dump(); static int num_array_klasses(); diff --git a/src/hotspot/share/classfile/systemDictionaryShared.cpp b/src/hotspot/share/classfile/systemDictionaryShared.cpp index eda823704ca..513ebe8bb84 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.cpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.cpp @@ -867,11 +867,6 @@ bool SystemDictionaryShared::should_be_excluded(Klass* k) { } else { InstanceKlass* ik = InstanceKlass::cast(k); - if (CDSConfig::is_dumping_dynamic_archive() && ik->in_aot_cache()) { - // ik is already part of the static archive, so it will never be considered as excluded. - return false; - } - if (!SafepointSynchronize::is_at_safepoint()) { if (!ik->is_linked()) { // should_be_excluded_impl() below doesn't link unlinked classes. We come diff --git a/test/hotspot/jtreg/TEST.groups b/test/hotspot/jtreg/TEST.groups index 3af6548fe33..18cd93d4856 100644 --- a/test/hotspot/jtreg/TEST.groups +++ b/test/hotspot/jtreg/TEST.groups @@ -534,17 +534,7 @@ hotspot_aot_classlinking = \ -runtime/cds/appcds/customLoader/ParallelTestSingleFP.java \ -runtime/cds/appcds/customLoader/SameNameInTwoLoadersTest.java \ -runtime/cds/appcds/DumpClassListWithLF.java \ - -runtime/cds/appcds/dynamicArchive/LambdaContainsOldInf.java \ - -runtime/cds/appcds/dynamicArchive/LambdaCustomLoader.java \ - -runtime/cds/appcds/dynamicArchive/LambdaForOldInfInBaseArchive.java \ - -runtime/cds/appcds/dynamicArchive/LambdaInBaseArchive.java \ - -runtime/cds/appcds/dynamicArchive/LambdasInTwoArchives.java \ - -runtime/cds/appcds/dynamicArchive/ModulePath.java \ - -runtime/cds/appcds/dynamicArchive/NestHostOldInf.java \ - -runtime/cds/appcds/dynamicArchive/OldClassAndInf.java \ - -runtime/cds/appcds/dynamicArchive/OldClassInBaseArchive.java \ - -runtime/cds/appcds/dynamicArchive/OldClassVerifierTrouble.java \ - -runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java \ + -runtime/cds/appcds/dynamicArchive \ -runtime/cds/appcds/HelloExtTest.java \ -runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java \ -runtime/cds/appcds/javaldr/GCDuringDump.java \ diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java index e1f5f548593..9e43d00e872 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java @@ -40,20 +40,6 @@ * @run driver BulkLoaderTest STATIC */ -/* - * @test id=dynamic - * @requires vm.cds.supports.aot.class.linking - * @library /test/jdk/lib/testlibrary /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @build InitiatingLoaderTester BadOldClassA BadOldClassB - * @build jdk.test.whitebox.WhiteBox BulkLoaderTest SimpleCusty - * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar BulkLoaderTestApp.jar BulkLoaderTestApp MyUtil InitiatingLoaderTester - * BadOldClassA BadOldClassB - * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar cust.jar - * SimpleCusty - * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar WhiteBox.jar jdk.test.whitebox.WhiteBox - * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:WhiteBox.jar BulkLoaderTest DYNAMIC - */ - /* * @test id=aot * @requires vm.cds.supports.aot.class.linking @@ -279,10 +265,6 @@ class BulkLoaderTestApp { } try { - // In dynamic dump, the VM loads BadOldClassB and then attempts to - // link it. This will leave BadOldClassB in a "failed verification" state. - // All refernces to BadOldClassB from the CP should be purged from the CDS - // archive. c = BadOldClassB.class; c.newInstance(); throw new RuntimeException("Must not succeed"); diff --git a/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java b/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java index 2610dac16c0..bc2ac9db2ab 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/resolvedConstants/ResolvedConstants.java @@ -73,7 +73,9 @@ public class ResolvedConstants { static boolean aotClassLinking; public static void main(String[] args) throws Exception { test(args, false); - test(args, true); + if (!args[0].equals("DYNAMIC")) { + test(args, true); + } } static void test(String[] args, boolean testMode) throws Exception { From a355edbbe43f7356f9439ecabf0ab8218fc9e3e1 Mon Sep 17 00:00:00 2001 From: Damon Fenacci Date: Thu, 18 Sep 2025 06:24:49 +0000 Subject: [PATCH 094/556] 8367278: Test compiler/startup/StartupOutput.java timed out after completion on Windows Reviewed-by: syan, chagedorn --- test/hotspot/jtreg/compiler/startup/StartupOutput.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/startup/StartupOutput.java b/test/hotspot/jtreg/compiler/startup/StartupOutput.java index 14897f7ab87..bb08a2c0a2d 100644 --- a/test/hotspot/jtreg/compiler/startup/StartupOutput.java +++ b/test/hotspot/jtreg/compiler/startup/StartupOutput.java @@ -64,7 +64,7 @@ public class StartupOutput { // On s390x, generated code is ~6x larger in fastdebug and ~1.4x in release builds vs. other archs, // hence we require slightly more minimum space. int minInitialSize = 800 + (Platform.isS390x() ? 800 : 0); - for (int i = 0; i < 200; i++) { + for (int i = 0; i < 50; i++) { int initialCodeCacheSizeInKb = minInitialSize + rand.nextInt(400); int reservedCodeCacheSizeInKb = initialCodeCacheSizeInKb + rand.nextInt(200); pb = ProcessTools.createLimitedTestJavaProcessBuilder("-XX:InitialCodeCacheSize=" + initialCodeCacheSizeInKb + "K", "-XX:ReservedCodeCacheSize=" + reservedCodeCacheSizeInKb + "k", "-version"); From a306f88a8456be454f4954c7e4fb8a1273344b5b Mon Sep 17 00:00:00 2001 From: Srinivas Mandalika Date: Thu, 18 Sep 2025 06:41:20 +0000 Subject: [PATCH 095/556] 8339791: Refactor MiscUndecorated/ActiveAWTWindowTest.java Reviewed-by: psadhukhan --- .../MiscUndecorated/ActiveAWTWindowTest.java | 207 ++++++------------ 1 file changed, 72 insertions(+), 135 deletions(-) diff --git a/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java b/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java index c7ec6c6d969..aabaf0b511b 100644 --- a/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java +++ b/test/jdk/java/awt/Frame/MiscUndecorated/ActiveAWTWindowTest.java @@ -24,18 +24,22 @@ /* * @test * @key headful - * @summary To check proper WINDOW_EVENTS are triggered when Frame gains or losses the focus - * @library /lib/client - * @build ExtendedRobot + * @summary To check proper WINDOW_EVENTS are triggered when Frame gains + * or loses the focus * @run main ActiveAWTWindowTest */ +import java.awt.AWTException; import java.awt.BorderLayout; import java.awt.Button; import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Frame; +import java.awt.Point; +import java.awt.Robot; import java.awt.TextField; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -43,101 +47,74 @@ import java.awt.event.InputEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.swing.JButton; +import javax.swing.JComponent; public class ActiveAWTWindowTest { - private Frame frame, frame2; - private Button button, button2; - private TextField textField, textField2; - private volatile int eventType; - private final Object lock1 = new Object(); - private final Object lock2 = new Object(); - private final Object lock3 = new Object(); - private boolean passed = true; - private final int delay = 150; + private static Frame frame, frame2; + private static Button button, button2; + private static TextField textField, textField2; + + private static CountDownLatch windowActivatedLatch = new CountDownLatch(1); + private static CountDownLatch windowDeactivatedLatch = new CountDownLatch(1); + private static CountDownLatch windowFocusGainedLatch = new CountDownLatch(1); public static void main(String[] args) throws Exception { - ActiveAWTWindowTest test = new ActiveAWTWindowTest(); - try { - test.doTest(); - } finally { - EventQueue.invokeAndWait(() -> { - if (test.frame != null) { - test.frame.dispose(); - } - if (test.frame2 != null) { - test.frame2.dispose(); - } - }); - } + EventQueue.invokeAndWait(() -> { + initializeGUI(); + }); + doTest(); + EventQueue.invokeAndWait(() -> { + if (frame != null) { + frame.dispose(); + } + if (frame2 != null) { + frame2.dispose(); + } + }); } - public ActiveAWTWindowTest() { - try{ - EventQueue.invokeAndWait( () -> { - initializeGUI(); - }); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("Interrupted or unexpected Exception occured"); - } - } - - private void initializeGUI() { + private static void initializeGUI() { frame = new Frame(); frame.setLayout(new FlowLayout()); - frame.setLocation(5, 20); frame.setSize(200, 200); frame.setUndecorated(true); + frame.addWindowFocusListener(new WindowFocusListener() { + @Override public void windowGainedFocus(WindowEvent event) { System.out.println("Frame Focus gained"); - synchronized (lock3) { - try { - lock3.notifyAll(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + windowFocusGainedLatch.countDown(); } + @Override public void windowLostFocus(WindowEvent event) { - System.out.println("Frame Focus lost"); + System.out.println("Frame Focus lost"); } }); + frame.addWindowListener(new WindowAdapter() { + @Override public void windowActivated(WindowEvent e) { - eventType = WindowEvent.WINDOW_ACTIVATED; - System.out.println("Undecorated Frame is activated\n"); - synchronized (lock1) { - try { - lock1.notifyAll(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + System.out.println("Undecorated Frame is activated"); + windowActivatedLatch.countDown(); } + @Override public void windowDeactivated(WindowEvent e) { - eventType = WindowEvent.WINDOW_DEACTIVATED; - System.out.println("Undecorated Frame got Deactivated\n"); - synchronized (lock2) { - try { - lock2.notifyAll(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + System.out.println("Undecorated Frame got Deactivated"); + windowDeactivatedLatch.countDown(); } }); + textField = new TextField("TextField"); button = new Button("Click me"); - button.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - textField.setText("Focus gained"); - } - }); + button.addActionListener(e -> textField.setText("Focus gained")); frame.setBackground(Color.green); frame.add(button); @@ -149,86 +126,46 @@ public class ActiveAWTWindowTest { frame2.setLocation(5, 250); frame2.setSize(200, 200); frame2.setBackground(Color.green); + button2 = new Button("Click me"); textField2 = new TextField("TextField"); - button2.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - textField2.setText("Got the focus"); - } - }); + button2.addActionListener(e -> textField2.setText("Got the focus")); frame2.add(button2, BorderLayout.SOUTH); frame2.add(textField2, BorderLayout.NORTH); frame2.setVisible(true); - - frame.toFront(); } - public void doTest() { - ExtendedRobot robot; - try { - robot = new ExtendedRobot(); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("Cannot create robot"); - } - - robot.setAutoDelay(delay); + private static void doTest() throws AWTException, InterruptedException { + Robot robot = new Robot(); + robot.setAutoDelay(150); robot.setAutoWaitForIdle(true); - - robot.waitForIdle(5*delay); - robot.mouseMove(button.getLocationOnScreen().x + button.getSize().width / 2, - button.getLocationOnScreen().y + button.getSize().height / 2); - robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); - robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); - - if (eventType != WindowEvent.WINDOW_ACTIVATED) { - synchronized (lock1) { - try { - lock1.wait(delay * 10); - } catch (Exception e) { - e.printStackTrace(); - } - } + if (!windowFocusGainedLatch.await(1000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Frame did not gain focus"); } - if (eventType != WindowEvent.WINDOW_ACTIVATED) { - passed = false; - System.err.println("WINDOW_ACTIVATED event did not occur when the " + - "undecorated frame is activated!"); + clickButtonCenter(robot, button); + + if (!windowActivatedLatch.await(1000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Frame was not activated"); } + clickButtonCenter(robot, button2); - eventType = -1; - - robot.mouseMove(button2.getLocationOnScreen().x + button2.getSize().width / 2, - button2.getLocationOnScreen().y + button2.getSize().height / 2); - robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); - robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); - - if (eventType != WindowEvent.WINDOW_DEACTIVATED) { - synchronized (lock2) { - try { - lock2.wait(delay * 10); - } catch (Exception e) { - } - } - } - if (eventType != WindowEvent.WINDOW_DEACTIVATED) { - passed = false; - System.err.println("FAIL: WINDOW_DEACTIVATED event did not occur for the " + - "undecorated frame when another frame gains focus!"); + if (!windowDeactivatedLatch.await(2000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Frame was not deactivated"); } if (frame.hasFocus()) { - passed = false; - System.err.println("FAIL: The undecorated frame has focus even when " + - "another frame is clicked!"); - } - - if (!passed) { - //captureScreenAndSave(); - System.err.println("Test failed!"); - throw new RuntimeException("Test failed."); - } else { - System.out.println("Test passed"); + throw new RuntimeException("Frame did not lose focus"); } } + + private static void clickButtonCenter(Robot robot, Component button) { + Point location = button.getLocationOnScreen(); + Dimension size = button.getSize(); + int x = location.x + size.width / 2; + int y = location.y + size.height / 2; + robot.mouseMove(x, y); + robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); + robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); + } } + From 4c5e901c96dee3885e1b29a53d3400174f9bba09 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Thu, 18 Sep 2025 08:25:05 +0000 Subject: [PATCH 096/556] 8367689: Revert removal of several compilation-related vmStructs fields Reviewed-by: kevinw, coleenp --- src/hotspot/share/ci/ciClassList.hpp | 1 + src/hotspot/share/runtime/vmStructs.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/hotspot/share/ci/ciClassList.hpp b/src/hotspot/share/ci/ciClassList.hpp index 618a052765e..bce1e52e80b 100644 --- a/src/hotspot/share/ci/ciClassList.hpp +++ b/src/hotspot/share/ci/ciClassList.hpp @@ -80,6 +80,7 @@ friend class ciObjectFactory; \ // Any more access must be given explicitly. #define CI_PACKAGE_ACCESS_TO \ friend class ciObjectFactory; \ +friend class VMStructs; \ friend class ciCallSite; \ friend class ciConstantPoolCache; \ friend class ciField; \ diff --git a/src/hotspot/share/runtime/vmStructs.cpp b/src/hotspot/share/runtime/vmStructs.cpp index bc026887b84..86874a967e3 100644 --- a/src/hotspot/share/runtime/vmStructs.cpp +++ b/src/hotspot/share/runtime/vmStructs.cpp @@ -666,6 +666,14 @@ static_field(VMRegImpl, regName[0], const char*) \ static_field(VMRegImpl, stack0, VMReg) \ \ + /******************************************************************************************/ \ + /* CI (NOTE: these CI fields are retained in VMStructs for the benefit of external tools, */ \ + /* to ease their migration to a future alternative.) */ \ + /******************************************************************************************/ \ + \ + nonstatic_field(CompilerThread, _env, ciEnv*) \ + nonstatic_field(ciEnv, _task, CompileTask*) \ + \ /************/ \ /* Monitors */ \ /************/ \ @@ -1148,6 +1156,12 @@ declare_toplevel_type(BasicLock) \ declare_toplevel_type(BasicObjectLock) \ \ + /*********************/ \ + /* CI */ \ + /*********************/ \ + \ + declare_toplevel_type(ciEnv) \ + \ /********************/ \ /* -XX flags */ \ /********************/ \ From 04dcaa3412d07c407aed604874095acaf81d7309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20H=C3=A4ssig?= Date: Thu, 18 Sep 2025 08:30:05 +0000 Subject: [PATCH 097/556] 8367721: Test compiler/arguments/TestCompileTaskTimeout.java crashed: SIGSEGV Reviewed-by: mchevalier, chagedorn --- src/hotspot/share/compiler/compileBroker.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hotspot/share/compiler/compileBroker.cpp b/src/hotspot/share/compiler/compileBroker.cpp index d1e3154bbd9..24f28821087 100644 --- a/src/hotspot/share/compiler/compileBroker.cpp +++ b/src/hotspot/share/compiler/compileBroker.cpp @@ -224,12 +224,15 @@ CompileTaskWrapper::CompileTaskWrapper(CompileTask* task) { CompileTaskWrapper::~CompileTaskWrapper() { CompilerThread* thread = CompilerThread::current(); + + // First, disarm the timeout. This still relies on the underlying task. + thread->timeout()->disarm(); + CompileTask* task = thread->task(); CompileLog* log = thread->log(); if (log != nullptr && !task->is_unloaded()) task->log_task_done(log); thread->set_task(nullptr); thread->set_env(nullptr); - thread->timeout()->disarm(); if (task->is_blocking()) { bool free_task = false; { From 4c7c009dd6aa2ce1f65f05c05d7376240f3c01cd Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Thu, 18 Sep 2025 09:09:27 +0000 Subject: [PATCH 098/556] 8367740: assembler_.inline.hpp should not include assembler.inline.hpp Reviewed-by: dfenacci, ayang --- src/hotspot/cpu/aarch64/assembler_aarch64.inline.hpp | 2 +- src/hotspot/cpu/ppc/assembler_ppc.inline.hpp | 2 +- src/hotspot/cpu/riscv/assembler_riscv.inline.hpp | 2 +- src/hotspot/cpu/s390/assembler_s390.inline.hpp | 2 +- src/hotspot/cpu/zero/assembler_zero.inline.hpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hotspot/cpu/aarch64/assembler_aarch64.inline.hpp b/src/hotspot/cpu/aarch64/assembler_aarch64.inline.hpp index e7efe472b82..fb14d588f04 100644 --- a/src/hotspot/cpu/aarch64/assembler_aarch64.inline.hpp +++ b/src/hotspot/cpu/aarch64/assembler_aarch64.inline.hpp @@ -26,7 +26,7 @@ #ifndef CPU_AARCH64_ASSEMBLER_AARCH64_INLINE_HPP #define CPU_AARCH64_ASSEMBLER_AARCH64_INLINE_HPP -#include "asm/assembler.inline.hpp" +#include "asm/assembler.hpp" #include "asm/codeBuffer.hpp" #include "code/codeCache.hpp" diff --git a/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp b/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp index 24601c5d3b0..7e49ec7455d 100644 --- a/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp +++ b/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp @@ -26,7 +26,7 @@ #ifndef CPU_PPC_ASSEMBLER_PPC_INLINE_HPP #define CPU_PPC_ASSEMBLER_PPC_INLINE_HPP -#include "asm/assembler.inline.hpp" +#include "asm/assembler.hpp" #include "asm/codeBuffer.hpp" #include "code/codeCache.hpp" #include "runtime/vm_version.hpp" diff --git a/src/hotspot/cpu/riscv/assembler_riscv.inline.hpp b/src/hotspot/cpu/riscv/assembler_riscv.inline.hpp index 1f9e6df2172..e85b64bd6ba 100644 --- a/src/hotspot/cpu/riscv/assembler_riscv.inline.hpp +++ b/src/hotspot/cpu/riscv/assembler_riscv.inline.hpp @@ -27,7 +27,7 @@ #ifndef CPU_RISCV_ASSEMBLER_RISCV_INLINE_HPP #define CPU_RISCV_ASSEMBLER_RISCV_INLINE_HPP -#include "asm/assembler.inline.hpp" +#include "asm/assembler.hpp" #include "asm/codeBuffer.hpp" #include "code/codeCache.hpp" diff --git a/src/hotspot/cpu/s390/assembler_s390.inline.hpp b/src/hotspot/cpu/s390/assembler_s390.inline.hpp index 567f3d75a62..3bab60f0bb6 100644 --- a/src/hotspot/cpu/s390/assembler_s390.inline.hpp +++ b/src/hotspot/cpu/s390/assembler_s390.inline.hpp @@ -26,7 +26,7 @@ #ifndef CPU_S390_ASSEMBLER_S390_INLINE_HPP #define CPU_S390_ASSEMBLER_S390_INLINE_HPP -#include "asm/assembler.inline.hpp" +#include "asm/assembler.hpp" #include "asm/codeBuffer.hpp" #include "code/codeCache.hpp" diff --git a/src/hotspot/cpu/zero/assembler_zero.inline.hpp b/src/hotspot/cpu/zero/assembler_zero.inline.hpp index 0a3f4fc25fa..d78eb39c973 100644 --- a/src/hotspot/cpu/zero/assembler_zero.inline.hpp +++ b/src/hotspot/cpu/zero/assembler_zero.inline.hpp @@ -26,7 +26,7 @@ #ifndef CPU_ZERO_ASSEMBLER_ZERO_INLINE_HPP #define CPU_ZERO_ASSEMBLER_ZERO_INLINE_HPP -#include "asm/assembler.inline.hpp" +#include "asm/assembler.hpp" #include "asm/codeBuffer.hpp" #include "code/codeCache.hpp" #include "runtime/handles.inline.hpp" From a49856bb044057a738ffc4186e1e5e3916c0254c Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 18 Sep 2025 11:09:40 +0000 Subject: [PATCH 099/556] 8367969: C2: compiler/vectorapi/TestVectorMathLib.java fails without UnlockDiagnosticVMOptions Reviewed-by: shade, mhaessig --- test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java b/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java index 112220262b6..af9e7c051f8 100644 --- a/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java +++ b/test/hotspot/jtreg/compiler/vectorapi/TestVectorMathLib.java @@ -34,7 +34,9 @@ import jdk.incubator.vector.VectorSpecies; * @modules jdk.incubator.vector * @library /test/lib * - * @run main/othervm -Xbatch -XX:-TieredCompilation -XX:+StressIncrementalInlining -XX:CompileCommand=quiet + * @run main/othervm -Xbatch -XX:-TieredCompilation + * -XX:+UnlockDiagnosticVMOptions -XX:+StressIncrementalInlining + * -XX:CompileCommand=quiet * -XX:CompileCommand=compileonly,compiler.vectorapi.TestVectorMathLib::test* * compiler.vectorapi.TestVectorMathLib */ From 5db1dfe5c8b5df40779bb450849e6433aa9825ab Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Thu, 18 Sep 2025 12:09:47 +0000 Subject: [PATCH 100/556] 8361950: Update to use jtreg 8 Reviewed-by: jpai, iris, joehw, erikj, ihse, liach, alanb --- make/autoconf/lib-tests.m4 | 2 +- make/conf/github-actions.conf | 2 +- make/conf/jib-profiles.js | 6 ++--- test/docs/TEST.ROOT | 2 +- test/hotspot/jtreg/TEST.ROOT | 2 +- .../ResourceExhausted/resexhausted003.java | 23 +++++-------------- test/jaxp/TEST.ROOT | 2 +- test/jdk/TEST.ROOT | 2 +- .../spi-calendar-provider/TestSPISigned.java | 9 ++++---- test/langtools/TEST.ROOT | 2 +- test/lib-test/TEST.ROOT | 2 +- 11 files changed, 22 insertions(+), 32 deletions(-) diff --git a/make/autoconf/lib-tests.m4 b/make/autoconf/lib-tests.m4 index 9eb5ee5a046..23f3d443a6c 100644 --- a/make/autoconf/lib-tests.m4 +++ b/make/autoconf/lib-tests.m4 @@ -28,7 +28,7 @@ ################################################################################ # Minimum supported versions -JTREG_MINIMUM_VERSION=7.5.2 +JTREG_MINIMUM_VERSION=8 GTEST_MINIMUM_VERSION=1.14.0 ################################################################################ diff --git a/make/conf/github-actions.conf b/make/conf/github-actions.conf index d2b6cd23128..438e4b3ce8d 100644 --- a/make/conf/github-actions.conf +++ b/make/conf/github-actions.conf @@ -26,7 +26,7 @@ # Versions and download locations for dependencies used by GitHub Actions (GHA) GTEST_VERSION=1.14.0 -JTREG_VERSION=7.5.2+1 +JTREG_VERSION=8+2 LINUX_X64_BOOT_JDK_EXT=tar.gz LINUX_X64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_linux-x64_bin.tar.gz diff --git a/make/conf/jib-profiles.js b/make/conf/jib-profiles.js index d4877604a90..7ed72005ced 100644 --- a/make/conf/jib-profiles.js +++ b/make/conf/jib-profiles.js @@ -1174,9 +1174,9 @@ var getJibProfilesDependencies = function (input, common) { jtreg: { server: "jpg", product: "jtreg", - version: "7.5.2", - build_number: "1", - file: "bundles/jtreg-7.5.2+1.zip", + version: "8", + build_number: "2", + file: "bundles/jtreg-8+2.zip", environment_name: "JT_HOME", environment_path: input.get("jtreg", "home_path") + "/bin", configure_args: "--with-jtreg=" + input.get("jtreg", "home_path"), diff --git a/test/docs/TEST.ROOT b/test/docs/TEST.ROOT index 5ca9b1f144f..bcbfd717dc0 100644 --- a/test/docs/TEST.ROOT +++ b/test/docs/TEST.ROOT @@ -38,7 +38,7 @@ groups=TEST.groups # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Use new module options useNewOptions=true diff --git a/test/hotspot/jtreg/TEST.ROOT b/test/hotspot/jtreg/TEST.ROOT index 2d0d972744c..f5b6922e7e1 100644 --- a/test/hotspot/jtreg/TEST.ROOT +++ b/test/hotspot/jtreg/TEST.ROOT @@ -104,7 +104,7 @@ requires.properties= \ jdk.static # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Path to libraries in the topmost test directory. This is needed so @library # does not need ../../../ notation to reach them diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java index 8cc997fa4ab..f1d18211f3d 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/ResourceExhausted/resexhausted003.java @@ -25,9 +25,10 @@ package nsk.jvmti.ResourceExhausted; import java.io.File; import java.io.FileInputStream; import java.io.PrintStream; +import java.net.URI; +import java.nio.file.Path; +import java.security.CodeSource; import java.security.ProtectionDomain; -import java.util.regex.Pattern; -import java.util.regex.Matcher; import nsk.share.Consts; import nsk.share.test.Stresser; @@ -80,24 +81,12 @@ public class resexhausted003 { public static int run(String args[], PrintStream out) { - String testclasspath = System.getProperty("test.class.path"); - String [] testpaths = testclasspath.split(System.getProperty("path.separator")); - String classesDir = ""; - - Pattern pattern = Pattern.compile("^(.*)classes(.*)vmTestbase(.*)$"); - for (int i = 0 ; i < testpaths.length; i++) { - if (pattern.matcher(testpaths[i]).matches()) { - classesDir = testpaths[i]; - } - } - if (classesDir.equals("")) { - System.err.println("TEST BUG: Classes directory not found in test,class.path."); - return Consts.TEST_FAILED; - } Stresser stress = new Stresser(args); String className = Helper.class.getName(); - byte[] bloatBytes = fileBytes(classesDir + File.separator + className.replace('.', '/') + ".class"); + CodeSource classCodeSource = Helper.class.getProtectionDomain().getCodeSource(); + Path classFilePath = Path.of(URI.create(classCodeSource.getLocation().toString())); + byte[] bloatBytes = fileBytes(classFilePath.resolve(className.replace('.', '/') + ".class").toString()); int count = 0; Helper.resetExhaustedEvent(); diff --git a/test/jaxp/TEST.ROOT b/test/jaxp/TEST.ROOT index 6398c399f0a..bafa67a700e 100644 --- a/test/jaxp/TEST.ROOT +++ b/test/jaxp/TEST.ROOT @@ -23,7 +23,7 @@ modules=java.xml groups=TEST.groups # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Path to libraries in the topmost test directory. This is needed so @library # does not need ../../ notation to reach them diff --git a/test/jdk/TEST.ROOT b/test/jdk/TEST.ROOT index 0fa78bebc3f..9d12d384399 100644 --- a/test/jdk/TEST.ROOT +++ b/test/jdk/TEST.ROOT @@ -121,7 +121,7 @@ requires.properties= \ jdk.static # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Path to libraries in the topmost test directory. This is needed so @library # does not need ../../ notation to reach them diff --git a/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java b/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java index 83c2d85f6e4..fb54d468d18 100644 --- a/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java +++ b/test/jdk/java/security/SignedJar/spi-calendar-provider/TestSPISigned.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, Red Hat, Inc. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,6 +31,7 @@ import java.util.Calendar; import java.util.Locale; import java.util.List; import java.util.ArrayList; +import java.net.URI; import java.nio.file.Paths; import java.nio.file.Path; import java.nio.file.Files; @@ -54,12 +56,9 @@ import static java.util.Calendar.WEDNESDAY; */ public class TestSPISigned { - private static final String TEST_CLASSES = System.getProperty("test.classes", "."); private static final String TEST_SRC = System.getProperty("test.src", "."); private static final Path META_INF_DIR = Paths.get(TEST_SRC, "provider", "meta"); - private static final Path PROVIDER_PARENT = Paths.get(TEST_CLASSES, ".."); - private static final Path PROVIDER_DIR = PROVIDER_PARENT.resolve("provider"); private static final Path MODS_DIR = Paths.get("mods"); private static final Path UNSIGNED_JAR = MODS_DIR.resolve("unsigned-with-locale.jar"); private static final Path SIGNED_JAR = MODS_DIR.resolve("signed-with-locale.jar"); @@ -81,7 +80,9 @@ public class TestSPISigned { // Set up signed jar with custom calendar data provider // // 1. Create jar with custom CalendarDataProvider - JarUtils.createJarFile(UNSIGNED_JAR, PROVIDER_DIR); + var codeSource = baz.CalendarDataProviderImpl.class.getProtectionDomain().getCodeSource(); + var providerDir = Path.of(URI.create(codeSource.getLocation().toString())); + JarUtils.createJarFile(UNSIGNED_JAR, providerDir); JarUtils.updateJarFile(UNSIGNED_JAR, META_INF_DIR); // create signer's keypair SecurityTools.keytool("-genkeypair -keyalg RSA -keystore ks " + diff --git a/test/langtools/TEST.ROOT b/test/langtools/TEST.ROOT index e3bd3b74bcf..1aaaa7dffe1 100644 --- a/test/langtools/TEST.ROOT +++ b/test/langtools/TEST.ROOT @@ -15,7 +15,7 @@ keys=intermittent randomness needs-src needs-src-jdk_javadoc groups=TEST.groups # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Use new module options useNewOptions=true diff --git a/test/lib-test/TEST.ROOT b/test/lib-test/TEST.ROOT index 162e6e15ec2..5710e2e9528 100644 --- a/test/lib-test/TEST.ROOT +++ b/test/lib-test/TEST.ROOT @@ -29,7 +29,7 @@ keys=randomness # Minimum jtreg version -requiredVersion=7.5.2+1 +requiredVersion=8+2 # Allow querying of various System properties in @requires clauses requires.extraPropDefns = ../jtreg-ext/requires/VMProps.java From feaa654b1bb5a1187785320603ccb17e2c43222d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Sj=C3=B6len?= Date: Thu, 18 Sep 2025 12:26:50 +0000 Subject: [PATCH 101/556] 8367249: [REDO] MemBaseline accesses VMT without using lock Reviewed-by: azafari, cnorrbin --- src/hotspot/share/nmt/memBaseline.cpp | 72 +++++++------------ src/hotspot/share/nmt/memBaseline.hpp | 13 ++-- src/hotspot/share/nmt/memReporter.cpp | 16 ++--- .../share/nmt/nmtNativeCallStackStorage.cpp | 18 +++++ .../share/nmt/nmtNativeCallStackStorage.hpp | 3 +- src/hotspot/share/nmt/regionsTree.cpp | 13 +++- src/hotspot/share/nmt/regionsTree.hpp | 10 ++- src/hotspot/share/nmt/vmatree.cpp | 7 ++ src/hotspot/share/nmt/vmatree.hpp | 12 +++- src/hotspot/share/utilities/rbTree.hpp | 12 ++-- src/hotspot/share/utilities/rbTree.inline.hpp | 49 +++++++++++++ test/hotspot/gtest/utilities/test_rbtree.cpp | 40 +++++++++++ 12 files changed, 189 insertions(+), 76 deletions(-) diff --git a/src/hotspot/share/nmt/memBaseline.cpp b/src/hotspot/share/nmt/memBaseline.cpp index d94aa10eab1..118e3ec64c0 100644 --- a/src/hotspot/share/nmt/memBaseline.cpp +++ b/src/hotspot/share/nmt/memBaseline.cpp @@ -27,8 +27,7 @@ #include "memory/metaspaceUtils.hpp" #include "nmt/memBaseline.hpp" #include "nmt/memTracker.hpp" -#include "runtime/javaThread.hpp" -#include "runtime/safepoint.hpp" +#include "nmt/regionsTree.inline.hpp" /* * Sizes are sorted in descenting order for reporting @@ -104,38 +103,6 @@ class MallocAllocationSiteWalker : public MallocSiteWalker { } }; -// Walk all virtual memory regions for baselining -class VirtualMemoryAllocationWalker : public VirtualMemoryWalker { - private: - typedef LinkedListImpl EntryList; - EntryList _virtual_memory_regions; - DEBUG_ONLY(address _last_base;) - public: - VirtualMemoryAllocationWalker() { - DEBUG_ONLY(_last_base = nullptr); - } - - bool do_allocation_site(const ReservedMemoryRegion* rgn) { - assert(rgn->base() >= _last_base, "region unordered?"); - DEBUG_ONLY(_last_base = rgn->base()); - if (rgn->size() > 0) { - if (_virtual_memory_regions.add(*rgn) != nullptr) { - return true; - } else { - return false; - } - } else { - // Ignore empty sites. - return true; - } - } - - LinkedList* virtual_memory_allocations() { - return &_virtual_memory_regions; - } -}; - void MemBaseline::baseline_summary() { MallocMemorySummary::snapshot(&_malloc_memory_snapshot); { @@ -158,14 +125,15 @@ bool MemBaseline::baseline_allocation_sites() { // The malloc sites are collected in size order _malloc_sites_order = by_size; - // Virtual memory allocation sites - VirtualMemoryAllocationWalker virtual_memory_walker; - if (!MemTracker::walk_virtual_memory(&virtual_memory_walker)) { - return false; - } + assert(_vma_allocations == nullptr, "must"); - // Virtual memory allocations are collected in call stack order - _virtual_memory_allocations.move(virtual_memory_walker.virtual_memory_allocations()); + { + MemTracker::NmtVirtualMemoryLocker locker; + _vma_allocations = new (mtNMT, std::nothrow) RegionsTree(*VirtualMemoryTracker::Instance::tree()); + if (_vma_allocations == nullptr) { + return false; + } + } if (!aggregate_virtual_memory_allocation_sites()) { return false; @@ -202,20 +170,28 @@ int compare_allocation_site(const VirtualMemoryAllocationSite& s1, bool MemBaseline::aggregate_virtual_memory_allocation_sites() { SortedLinkedList allocation_sites; - VirtualMemoryAllocationIterator itr = virtual_memory_allocations(); - const ReservedMemoryRegion* rgn; VirtualMemoryAllocationSite* site; - while ((rgn = itr.next()) != nullptr) { - VirtualMemoryAllocationSite tmp(*rgn->call_stack(), rgn->mem_tag()); + bool failed_oom = false; + _vma_allocations->visit_reserved_regions([&](ReservedMemoryRegion& rgn) { + VirtualMemoryAllocationSite tmp(*rgn.call_stack(), rgn.mem_tag()); site = allocation_sites.find(tmp); if (site == nullptr) { LinkedListNode* node = allocation_sites.add(tmp); - if (node == nullptr) return false; + if (node == nullptr) { + failed_oom = true; + return false; + } site = node->data(); } - site->reserve_memory(rgn->size()); - site->commit_memory(VirtualMemoryTracker::Instance::committed_size(rgn)); + site->reserve_memory(rgn.size()); + + site->commit_memory(_vma_allocations->committed_size(rgn)); + return true; + }); + + if (failed_oom) { + return false; } _virtual_memory_sites.move(&allocation_sites); diff --git a/src/hotspot/share/nmt/memBaseline.hpp b/src/hotspot/share/nmt/memBaseline.hpp index 2fff4cc666c..3f1ea46d815 100644 --- a/src/hotspot/share/nmt/memBaseline.hpp +++ b/src/hotspot/share/nmt/memBaseline.hpp @@ -35,7 +35,6 @@ typedef LinkedListIterator MallocSiteIterator; typedef LinkedListIterator VirtualMemorySiteIterator; -typedef LinkedListIterator VirtualMemoryAllocationIterator; /* * Baseline a memory snapshot @@ -71,7 +70,7 @@ class MemBaseline { LinkedListImpl _malloc_sites; // All virtual memory allocations - LinkedListImpl _virtual_memory_allocations; + RegionsTree* _vma_allocations; // Virtual memory allocations by allocation sites, always in by_address // order @@ -86,6 +85,7 @@ class MemBaseline { // create a memory baseline MemBaseline(): _instance_class_count(0), _array_class_count(0), _thread_count(0), + _vma_allocations(nullptr), _baseline_type(Not_baselined) { } @@ -110,9 +110,9 @@ class MemBaseline { // Virtual memory allocation iterator always returns in virtual memory // base address order. - VirtualMemoryAllocationIterator virtual_memory_allocations() { - assert(!_virtual_memory_allocations.is_empty(), "Not detail baseline"); - return VirtualMemoryAllocationIterator(_virtual_memory_allocations.head()); + RegionsTree* virtual_memory_allocations() { + assert(_vma_allocations != nullptr, "Not detail baseline"); + return _vma_allocations; } // Total reserved memory = total malloc'd memory + total reserved virtual @@ -185,7 +185,8 @@ class MemBaseline { _malloc_sites.clear(); _virtual_memory_sites.clear(); - _virtual_memory_allocations.clear(); + delete _vma_allocations; + _vma_allocations = nullptr; } private: diff --git a/src/hotspot/share/nmt/memReporter.cpp b/src/hotspot/share/nmt/memReporter.cpp index 65d4d76942b..772bda2885b 100644 --- a/src/hotspot/share/nmt/memReporter.cpp +++ b/src/hotspot/share/nmt/memReporter.cpp @@ -394,13 +394,11 @@ int MemDetailReporter::report_virtual_memory_allocation_sites() { void MemDetailReporter::report_virtual_memory_map() { // Virtual memory map always in base address order - VirtualMemoryAllocationIterator itr = _baseline.virtual_memory_allocations(); - const ReservedMemoryRegion* rgn; - output()->print_cr("Virtual memory map:"); - while ((rgn = itr.next()) != nullptr) { - report_virtual_memory_region(rgn); - } + _baseline.virtual_memory_allocations()->visit_reserved_regions([&](ReservedMemoryRegion& rgn) { + report_virtual_memory_region(&rgn); + return true; + }); } void MemDetailReporter::report_virtual_memory_region(const ReservedMemoryRegion* reserved_rgn) { @@ -421,7 +419,7 @@ void MemDetailReporter::report_virtual_memory_region(const ReservedMemoryRegion* outputStream* out = output(); const char* scale = current_scale(); const NativeCallStack* stack = reserved_rgn->call_stack(); - bool all_committed = reserved_rgn->size() == VirtualMemoryTracker::Instance::committed_size(reserved_rgn); + bool all_committed = reserved_rgn->size() == _baseline.virtual_memory_allocations()->committed_size(*reserved_rgn); const char* region_type = (all_committed ? "reserved and committed" : "reserved"); out->cr(); print_virtual_memory_region(region_type, reserved_rgn->base(), reserved_rgn->size()); @@ -435,7 +433,7 @@ void MemDetailReporter::report_virtual_memory_region(const ReservedMemoryRegion* if (all_committed) { bool reserved_and_committed = false; - VirtualMemoryTracker::Instance::tree()->visit_committed_regions(*reserved_rgn, + _baseline.virtual_memory_allocations()->visit_committed_regions(*reserved_rgn, [&](CommittedMemoryRegion& committed_rgn) { if (committed_rgn.equals(*reserved_rgn)) { // One region spanning the entire reserved region, with the same stack trace. @@ -468,7 +466,7 @@ void MemDetailReporter::report_virtual_memory_region(const ReservedMemoryRegion* ) }; - VirtualMemoryTracker::Instance::tree()->visit_committed_regions(*reserved_rgn, + _baseline.virtual_memory_allocations()->visit_committed_regions(*reserved_rgn, [&](CommittedMemoryRegion& crgn) { print_committed_rgn(crgn); return true; diff --git a/src/hotspot/share/nmt/nmtNativeCallStackStorage.cpp b/src/hotspot/share/nmt/nmtNativeCallStackStorage.cpp index 3e5c1d2f0ea..9a2ecd57ecc 100644 --- a/src/hotspot/share/nmt/nmtNativeCallStackStorage.cpp +++ b/src/hotspot/share/nmt/nmtNativeCallStackStorage.cpp @@ -57,3 +57,21 @@ NativeCallStackStorage::NativeCallStackStorage(bool is_detailed_mode, int table_ NativeCallStackStorage::~NativeCallStackStorage() { FREE_C_HEAP_ARRAY(LinkPtr, _table); } + +NativeCallStackStorage::NativeCallStackStorage(const NativeCallStackStorage& other) + : _table_size(other._table_size), + _table(nullptr), + _stacks(), + _is_detailed_mode(other._is_detailed_mode), + _fake_stack(other._fake_stack) { + if (_is_detailed_mode) { + _table = NEW_C_HEAP_ARRAY(TableEntryIndex, _table_size, mtNMT); + for (int i = 0; i < _table_size; i++) { + _table[i] = other._table[i]; + } + } + _stacks.reserve(other._stacks.length()); + for (int i = 0; i < other._stacks.length(); i++) { + _stacks.at_grow(i) = other._stacks.at(i); + } +} diff --git a/src/hotspot/share/nmt/nmtNativeCallStackStorage.hpp b/src/hotspot/share/nmt/nmtNativeCallStackStorage.hpp index 6f194cfa5a1..6ead8f49248 100644 --- a/src/hotspot/share/nmt/nmtNativeCallStackStorage.hpp +++ b/src/hotspot/share/nmt/nmtNativeCallStackStorage.hpp @@ -95,7 +95,8 @@ public: } NativeCallStackStorage(bool is_detailed_mode, int table_size = default_table_size); - + NativeCallStackStorage(const NativeCallStackStorage& other); + NativeCallStackStorage& operator=(const NativeCallStackStorage& other) = delete; ~NativeCallStackStorage(); }; diff --git a/src/hotspot/share/nmt/regionsTree.cpp b/src/hotspot/share/nmt/regionsTree.cpp index 370c69a2485..a2f5a5df67a 100644 --- a/src/hotspot/share/nmt/regionsTree.cpp +++ b/src/hotspot/share/nmt/regionsTree.cpp @@ -22,6 +22,8 @@ * */ #include "nmt/regionsTree.hpp" +#include "nmt/regionsTree.inline.hpp" +#include "nmt/virtualMemoryTracker.hpp" VMATree::SummaryDiff RegionsTree::commit_region(address addr, size_t size, const NativeCallStack& stack) { return commit_mapping((VMATree::position)addr, size, make_region_data(stack, mtNone), /*use tag inplace*/ true); @@ -54,4 +56,13 @@ void RegionsTree::print_on(outputStream* st) { return true; }); } -#endif \ No newline at end of file +#endif + +size_t RegionsTree::committed_size(const ReservedMemoryRegion& rgn) { + size_t result = 0; + visit_committed_regions(rgn, [&](CommittedMemoryRegion& crgn) { + result += crgn.size(); + return true; + }); + return result; +} diff --git a/src/hotspot/share/nmt/regionsTree.hpp b/src/hotspot/share/nmt/regionsTree.hpp index bf2ab711b2d..35272c27423 100644 --- a/src/hotspot/share/nmt/regionsTree.hpp +++ b/src/hotspot/share/nmt/regionsTree.hpp @@ -40,6 +40,12 @@ class RegionsTree : public VMATree { public: RegionsTree(bool with_storage) : VMATree() , _ncs_storage(with_storage), _with_storage(with_storage) { } + RegionsTree(const RegionsTree& other) + : VMATree(other), + _ncs_storage(other._ncs_storage), + _with_storage(other._with_storage) {} + RegionsTree& operator=(const RegionsTree& other) = delete; + ReservedMemoryRegion find_reserved_region(address addr); SummaryDiff commit_region(address addr, size_t size, const NativeCallStack& stack); @@ -91,6 +97,8 @@ class RegionsTree : public VMATree { NativeCallStackStorage::StackIndex si = node.out_stack_index(); return _ncs_storage.get(si); } + + size_t committed_size(const ReservedMemoryRegion& rgn); }; -#endif // NMT_REGIONSTREE_HPP \ No newline at end of file +#endif // NMT_REGIONSTREE_HPP diff --git a/src/hotspot/share/nmt/vmatree.cpp b/src/hotspot/share/nmt/vmatree.cpp index 4f6f8e12185..69887068cb2 100644 --- a/src/hotspot/share/nmt/vmatree.cpp +++ b/src/hotspot/share/nmt/vmatree.cpp @@ -744,3 +744,10 @@ void VMATree::SummaryDiff::print_on(outputStream* out) { } } #endif + +void VMATree::clear() { + _tree.remove_all(); +}; +bool VMATree::is_empty() { + return _tree.size() == 0; +}; diff --git a/src/hotspot/share/nmt/vmatree.hpp b/src/hotspot/share/nmt/vmatree.hpp index 1b5729054e4..dff2491c69c 100644 --- a/src/hotspot/share/nmt/vmatree.hpp +++ b/src/hotspot/share/nmt/vmatree.hpp @@ -30,6 +30,7 @@ #include "nmt/nmtNativeCallStackStorage.hpp" #include "utilities/globalDefinitions.hpp" #include "utilities/ostream.hpp" +#include "utilities/rbTree.hpp" #include "utilities/rbTree.inline.hpp" #include @@ -39,7 +40,7 @@ // For example, the state may go from released memory to committed memory, // or from committed memory of a certain MemTag to committed memory of a different MemTag. // The set of points is stored in a balanced binary tree for efficient querying and updating. -class VMATree { +class VMATree : public CHeapObjBase { friend class NMTVMATreeTest; friend class VMTWithVMATreeTest; // A position in memory. @@ -65,7 +66,6 @@ private: static const char* statetype_strings[static_cast(StateType::st_number_of_states)]; public: - NONCOPYABLE(VMATree); static const char* statetype_to_string(StateType type) { assert(type < StateType::st_number_of_states, "must be"); @@ -226,6 +226,11 @@ private: public: VMATree() : _tree() {} + VMATree(const VMATree& other) : _tree() { + bool success = other._tree.copy_into(_tree); + assert(success, "VMATree dies on OOM"); + } + VMATree& operator=(VMATree const&) = delete; struct SingleDiff { using delta = int64_t; @@ -329,5 +334,8 @@ public: _tree.visit_range_in_order(from, to, f); } VMARBTree& tree() { return _tree; } + + void clear(); + bool is_empty(); }; #endif diff --git a/src/hotspot/share/utilities/rbTree.hpp b/src/hotspot/share/utilities/rbTree.hpp index 4c358b53ff0..2c4edcc2070 100644 --- a/src/hotspot/share/utilities/rbTree.hpp +++ b/src/hotspot/share/utilities/rbTree.hpp @@ -428,6 +428,7 @@ public: template void visit_in_order(F f); + // Visit all RBNodes in ascending order whose keys are in range [from, to], calling f on each node. // If f returns `true` the iteration continues, otherwise it is stopped at the current node. template @@ -475,15 +476,10 @@ class RBTree : public AbstractRBTree, COMPARATOR> { public: RBTree() : BaseType(), _allocator() {} + NONCOPYABLE(RBTree); ~RBTree() { remove_all(); } - RBTree(const RBTree& other) : BaseType(), _allocator() { - assert(std::is_copy_constructible(), "Value type must be copy-constructible"); - other.visit_in_order([&](auto node) { - this->upsert(node->key(), node->val()); - return true; - }); - } - RBTree& operator=(const RBTree& other) = delete; + + bool copy_into(RBTree& other) const; typedef typename BaseType::Cursor Cursor; using BaseType::cursor; diff --git a/src/hotspot/share/utilities/rbTree.inline.hpp b/src/hotspot/share/utilities/rbTree.inline.hpp index f28923eb867..b7de0a7ac4c 100644 --- a/src/hotspot/share/utilities/rbTree.inline.hpp +++ b/src/hotspot/share/utilities/rbTree.inline.hpp @@ -753,4 +753,53 @@ void AbstractRBTree::print_on(outputStream* st, const P } } +template +bool RBTree::copy_into(RBTree& other) const { + assert(other.size() == 0, "You can only copy into an empty RBTree"); + assert(std::is_copy_constructible::value, "Key type must be copy-constructible when copying a RBTree"); + assert(std::is_copy_constructible::value, "Value type must be copy-constructible when copying a RBTree"); + enum class Dir { Left, Right }; + struct node_pair { const IntrusiveRBNode* current; IntrusiveRBNode* other_parent; Dir dir; }; + struct stack { + node_pair s[64]; + int idx = 0; + stack() : idx(0) {} + node_pair pop() { idx--; return s[idx]; }; + void push(node_pair n) { s[idx] = n; idx++; }; + bool is_empty() { return idx == 0; }; + }; + + stack visit_stack; + if (this->_root == nullptr) { + return true; + } + RBNode* root = static_cast*>(this->_root); + other._root = other.allocate_node(root->key(), root->val()); + if (other._root == nullptr) return false; + + visit_stack.push({this->_root->_left, other._root, Dir::Left}); + visit_stack.push({this->_root->_right, other._root, Dir::Right}); + while (!visit_stack.is_empty()) { + node_pair n = visit_stack.pop(); + const RBNode* current = static_cast*>(n.current); + if (current == nullptr) continue; + RBNode* new_node = other.allocate_node(current->key(), current->val()); + if (new_node == nullptr) { + return false; + } + if (n.dir == Dir::Left) { + n.other_parent->_left = new_node; + } else { + n.other_parent->_right = new_node; + } + new_node->set_parent(n.other_parent); + new_node->_parent |= n.current->_parent & 0x1; + visit_stack.push({n.current->_left, new_node, Dir::Left}); + visit_stack.push({n.current->_right, new_node, Dir::Right}); + } + other._num_nodes = this->_num_nodes; + DEBUG_ONLY(other._expected_visited = this->_expected_visited); + return true; +} + #endif // SHARE_UTILITIES_RBTREE_INLINE_HPP diff --git a/test/hotspot/gtest/utilities/test_rbtree.cpp b/test/hotspot/gtest/utilities/test_rbtree.cpp index ff234dd764a..a351e2141e8 100644 --- a/test/hotspot/gtest/utilities/test_rbtree.cpp +++ b/test/hotspot/gtest/utilities/test_rbtree.cpp @@ -1200,6 +1200,46 @@ TEST_VM_F(RBTreeTest, VerifyItThroughStressTest) { } } +TEST_VM_F(RBTreeTest, TestCopyInto) { + { + RBTreeInt rbtree1; + RBTreeInt rbtree2; + + rbtree1.copy_into(rbtree2); + rbtree2.verify_self(); + } + + RBTreeInt rbtree1; + RBTreeInt rbtree2; + + int size = 1000; + for (int i = 0; i < size; i++) { + rbtree1.upsert(i, i); + } + + rbtree1.copy_into(rbtree2); + rbtree2.verify_self(); + + ResourceMark rm; + GrowableArray allocations(size); + int size1 = 0; + rbtree1.visit_in_order([&](RBTreeIntNode* node) { + size1++; + allocations.append(node->key()); + return true; + }); + + int size2 = 0; + rbtree2.visit_in_order([&](RBTreeIntNode* node) { + EXPECT_EQ(node->key(), allocations.at(size2++)); + return true; + }); + + EXPECT_EQ(size1, size2); + EXPECT_EQ(rbtree1.size(), rbtree2.size()); + EXPECT_EQ(size2, static_cast(rbtree2.size())); +} + struct OomAllocator { void* allocate(size_t sz) { return nullptr; From c927291ecfa3a3871a1eed006687a3e9db4f6811 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Thu, 18 Sep 2025 15:08:37 +0000 Subject: [PATCH 102/556] 8367739: Serial: Retry allocation after lock acquire in mem_allocate_work Reviewed-by: fandreuzzi, tschatzl --- src/hotspot/share/gc/serial/serialHeap.cpp | 39 ++++++++++++++++------ src/hotspot/share/gc/serial/serialHeap.hpp | 1 + 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/hotspot/share/gc/serial/serialHeap.cpp b/src/hotspot/share/gc/serial/serialHeap.cpp index e019144d628..f26e4427062 100644 --- a/src/hotspot/share/gc/serial/serialHeap.cpp +++ b/src/hotspot/share/gc/serial/serialHeap.cpp @@ -292,27 +292,44 @@ HeapWord* SerialHeap::expand_heap_and_allocate(size_t size, bool is_tlab) { return result; } +HeapWord* SerialHeap::mem_allocate_cas_noexpand(size_t size, bool is_tlab) { + HeapWord* result = _young_gen->par_allocate(size); + if (result != nullptr) { + return result; + } + // Try old-gen allocation for non-TLAB. + if (!is_tlab) { + // If it's too large for young-gen or heap is too full. + if (size > heap_word_size(_young_gen->capacity_before_gc()) || _is_heap_almost_full) { + result = _old_gen->par_allocate(size); + if (result != nullptr) { + return result; + } + } + } + + return nullptr; +} + HeapWord* SerialHeap::mem_allocate_work(size_t size, bool is_tlab) { HeapWord* result = nullptr; for (uint try_count = 1; /* break */; try_count++) { - result = _young_gen->par_allocate(size); + result = mem_allocate_cas_noexpand(size, is_tlab); if (result != nullptr) { break; } - // Try old-gen allocation for non-TLAB. - if (!is_tlab) { - // If it's too large for young-gen or heap is too full. - if (size > heap_word_size(_young_gen->capacity_before_gc()) || _is_heap_almost_full) { - result = _old_gen->par_allocate(size); - if (result != nullptr) { - break; - } - } - } uint gc_count_before; // Read inside the Heap_lock locked region. { MutexLocker ml(Heap_lock); + + // Re-try after acquiring the lock, because a GC might have occurred + // while waiting for this lock. + result = mem_allocate_cas_noexpand(size, is_tlab); + if (result != nullptr) { + break; + } + gc_count_before = total_collections(); } diff --git a/src/hotspot/share/gc/serial/serialHeap.hpp b/src/hotspot/share/gc/serial/serialHeap.hpp index 86fb286f33f..388da13b1b0 100644 --- a/src/hotspot/share/gc/serial/serialHeap.hpp +++ b/src/hotspot/share/gc/serial/serialHeap.hpp @@ -222,6 +222,7 @@ private: // Try to allocate space by expanding the heap. HeapWord* expand_heap_and_allocate(size_t size, bool is_tlab); + HeapWord* mem_allocate_cas_noexpand(size_t size, bool is_tlab); HeapWord* mem_allocate_work(size_t size, bool is_tlab); MemoryPool* _eden_pool; From 72e5ad3d21effff6a4efae8ab3ed45c4f6bfba76 Mon Sep 17 00:00:00 2001 From: Ben Perez Date: Thu, 18 Sep 2025 15:23:05 +0000 Subject: [PATCH 103/556] 8365581: Optimize Java implementation of P256 arithmetic Reviewed-by: jnimeh --- .../MontgomeryIntegerPolynomialP256.java | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java b/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java index 1910746fe44..987b6967b50 100644 --- a/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java +++ b/src/java.base/share/classes/sun/security/util/math/intpoly/MontgomeryIntegerPolynomialP256.java @@ -32,6 +32,7 @@ import sun.security.util.math.IntegerFieldModuloP; import java.math.BigInteger; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; // Reference: // - [1] Shay Gueron and Vlad Krasnov "Fast Prime Field Elliptic Curve @@ -63,7 +64,7 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial private static final long[] zero = new long[] { 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L, 0x0000000000000000L }; - private static final long[] modulus = new long[] { + @Stable private static final long[] modulus = new long[] { 0x000fffffffffffffL, 0x00000fffffffffffL, 0x0000000000000000L, 0x0000001000000000L, 0x0000ffffffff0000L }; @@ -207,9 +208,8 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; nn1 = Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); n1 &= LIMB_MASK; - n2 = n * modulus[2]; - nn2 = Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - n2 &= LIMB_MASK; + n2 = 0; + nn2 = 0; n3 = n * modulus[3]; nn3 = Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); n3 &= LIMB_MASK; @@ -221,8 +221,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial d0 += n0; dd1 += nn1; d1 += n1; - dd2 += nn2; - d2 += n2; dd3 += nn3; d3 += n3; dd4 += nn4; @@ -259,9 +257,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -300,9 +295,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -341,9 +333,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -382,9 +371,6 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial n1 = n * modulus[1]; dd1 += Math.unsignedMultiplyHigh(n, modulus[1]) << shift1 | (n1 >>> shift2); d1 += n1 & LIMB_MASK; - n2 = n * modulus[2]; - dd2 += Math.unsignedMultiplyHigh(n, modulus[2]) << shift1 | (n2 >>> shift2); - d2 += n2 & LIMB_MASK; n3 = n * modulus[3]; dd3 += Math.unsignedMultiplyHigh(n, modulus[3]) << shift1 | (n3 >>> shift2); d3 += n3 & LIMB_MASK; @@ -411,7 +397,7 @@ public final class MontgomeryIntegerPolynomialP256 extends IntegerPolynomial c0 = c5 - modulus[0]; c1 = c6 - modulus[1] + (c0 >> BITS_PER_LIMB); c0 &= LIMB_MASK; - c2 = c7 - modulus[2] + (c1 >> BITS_PER_LIMB); + c2 = c7 + (c1 >> BITS_PER_LIMB); c1 &= LIMB_MASK; c3 = c8 - modulus[3] + (c2 >> BITS_PER_LIMB); c2 &= LIMB_MASK; From b8f2c7387f4b5a60a6b671619d968d1d77aa9c27 Mon Sep 17 00:00:00 2001 From: Chris Plummer Date: Thu, 18 Sep 2025 15:35:32 +0000 Subject: [PATCH 104/556] 8367614: Test vmTestbase/nsk/jdi/stress/serial/heapwalking001/TestDescription.java failed, passed and timed-out Reviewed-by: dholmes, sspitsyn --- .../referringObjects001.java | 1 + .../referringObjects003.java | 67 ++++++------------- 2 files changed, 23 insertions(+), 45 deletions(-) diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java index b5ea8c34cbd..8cc023acaae 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects001/referringObjects001.java @@ -244,6 +244,7 @@ public class referringObjects001 extends HeapwalkingDebugger { } } } catch (Throwable t) { + setSuccess(false); log.complain("Unexpected exception:"); t.printStackTrace(log.getOutStream()); } diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java index 862600dc697..a04b323d0d1 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects003/referringObjects003.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -45,12 +45,11 @@ * - Debugee VM * - stop all threads and remove all references to threads and thread group * - Debugger VM - * - check that thread group have only 1 referrer: parent thread group - * - check that threre are no references to test threads in target VM - * - Debugger VM - * - test ObjectReference.disableCollection, ObjectReference.enableCollection for ThreadGroupReference: - * can't force collection of thread group because of thread group always has 1 referrer - parent thread group, so - * just test disableCollection/enableCollection don't throw any unexpected exceptions + * - force GC + * - check that the test thread group has been collected. The reference from the + * parent thread group is weak and should not prevent the test thread group from + * being collected. + * - check that there are no references to test threads in target VM * * @requires !vm.graal.enabled * @library /vmTestbase @@ -107,9 +106,8 @@ public class referringObjects003 extends HeapwalkingDebugger { if (referrerCount != expectedCount) { setSuccess(false); - log - .complain("List with wrong size was returned by ObjectReference.referringObjects(ThreadGroupReference): " - + referrerCount + ", expected: " + expectedCount); + log.complain("List with wrong size was returned by ObjectReference.referringObjects(ThreadGroupReference): " + + referrerCount + ", expected: " + expectedCount); } } } @@ -181,57 +179,36 @@ public class referringObjects003 extends HeapwalkingDebugger { if (!isDebuggeeReady()) return; - checkDebugeeAnswer_instances("java.lang.ThreadGroup", threadGroupsToFilter.size() + 1); + // Force the test ThreadGroup to be collected. The only reference to it is a weak + // reference from the parent ThreadGroup. + forceGC(); + + checkDebugeeAnswer_instances("java.lang.ThreadGroup", threadGroupsToFilter.size()); checkDebugeeAnswer_instances("java.lang.Thread", threadsToFilter.size()); threadGroups = HeapwalkingDebugger.filterObjectReferrence(threadGroupsToFilter, HeapwalkingDebugger .getObjectReferences("java.lang.ThreadGroup", vm)); - // 1 referrer(parent thread group) is left - checkThreadGroupReferrersCount(threadGroups, 1); + if (threadGroups.size() != 0) { + setSuccess(false); + log.complain("All test threads groups should be removed"); + log.complain("Unexpected threads groups:"); + for (ObjectReference objectReference : threadGroups) { + log.complain(objectReference.toString()); + } + } threads = HeapwalkingDebugger.filterObjectReferrence(threadsToFilter, HeapwalkingDebugger.getObjectReferences( "java.lang.Thread", vm)); if (threads.size() != 0) { + setSuccess(false); log.complain("All test threads should be removed"); log.complain("Unexpected threads:"); for (ObjectReference objectReference : threads) { log.complain(objectReference.toString()); } } - - checkThreadGroupDisableCollection(threadGroups); - } - - // can't force collection of thread group because of 1 reference is always - // left in parent tread group - public void checkThreadGroupDisableCollection(List objectReferences) { - try { - for (ObjectReference objectReference : objectReferences) - objectReference.disableCollection(); - } catch (Throwable t) { - log.complain("Unexpected exception: " + t); - t.printStackTrace(log.getOutStream()); - } - - forceGC(); - try { - for (ObjectReference objectReference : objectReferences) - objectReference.enableCollection(); - } catch (Throwable t) { - log.complain("Unexpected exception: " + t); - t.printStackTrace(log.getOutStream()); - } - - forceGC(); - try { - for (ObjectReference objectReference : objectReferences) - objectReference.referringObjects(0); - } catch (Throwable t) { - log.complain("Unexpected exception: " + t); - t.printStackTrace(log.getOutStream()); - } } } From c597384ad64c7107fba4e970aa435a141276b2fd Mon Sep 17 00:00:00 2001 From: Kelvin Nilsen Date: Thu, 18 Sep 2025 16:06:59 +0000 Subject: [PATCH 105/556] 8367708: GenShen: Reduce total evacuation burden Reviewed-by: wkemper --- .../heuristics/shenandoahOldHeuristics.cpp | 2 +- .../share/gc/shenandoah/shenandoahGeneration.cpp | 10 +++++----- .../shenandoahGenerationalEvacuationTask.cpp | 8 +++++++- .../share/gc/shenandoah/shenandoah_globals.hpp | 16 +++++++++++++++- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp index 2d0bbfd5e4a..2361a50e76d 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp @@ -412,7 +412,7 @@ void ShenandoahOldHeuristics::prepare_for_old_collections() { size_t defrag_count = 0; size_t total_uncollected_old_regions = _last_old_region - _last_old_collection_candidate; - if (cand_idx > _last_old_collection_candidate) { + if ((ShenandoahGenerationalHumongousReserve > 0) && (cand_idx > _last_old_collection_candidate)) { // Above, we have added into the set of mixed-evacuation candidates all old-gen regions for which the live memory // that they contain is below a particular old-garbage threshold. Regions that were not selected for the collection // set hold enough live memory that it is not considered efficient (by "garbage-first standards") to compact these diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp index 25afcfcb10e..fafa3fde437 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp @@ -535,6 +535,8 @@ size_t ShenandoahGeneration::select_aged_regions(size_t old_available) { const size_t old_garbage_threshold = (ShenandoahHeapRegion::region_size_bytes() * ShenandoahOldGarbageThreshold) / 100; + const size_t pip_used_threshold = (ShenandoahHeapRegion::region_size_bytes() * ShenandoahGenerationalMinPIPUsage) / 100; + size_t old_consumed = 0; size_t promo_potential = 0; size_t candidates = 0; @@ -557,10 +559,8 @@ size_t ShenandoahGeneration::select_aged_regions(size_t old_available) { continue; } if (heap->is_tenurable(r)) { - if ((r->garbage() < old_garbage_threshold)) { - // This tenure-worthy region has too little garbage, so we do not want to expend the copying effort to - // reclaim the garbage; instead this region may be eligible for promotion-in-place to the - // old generation. + if ((r->garbage() < old_garbage_threshold) && (r->used() > pip_used_threshold)) { + // We prefer to promote this region in place because is has a small amount of garbage and a large usage. HeapWord* tams = ctx->top_at_mark_start(r); HeapWord* original_top = r->top(); if (!heap->is_concurrent_old_mark_in_progress() && tams == original_top) { @@ -586,7 +586,7 @@ size_t ShenandoahGeneration::select_aged_regions(size_t old_available) { // Else, we do not promote this region (either in place or by copy) because it has received new allocations. // During evacuation, we exclude from promotion regions for which age > tenure threshold, garbage < garbage-threshold, - // and get_top_before_promote() != tams + // used > pip_used_threshold, and get_top_before_promote() != tams } else { // Record this promotion-eligible candidate region. After sorting and selecting the best candidates below, // we may still decide to exclude this promotion-eligible region from the current collection set. If this diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalEvacuationTask.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalEvacuationTask.cpp index 3a0d7926865..971129beea8 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalEvacuationTask.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalEvacuationTask.cpp @@ -146,7 +146,13 @@ void ShenandoahGenerationalEvacuationTask::maybe_promote_region(ShenandoahHeapRe // more garbage than ShenandoahOldGarbageThreshold, we'll promote by evacuation. If there is room for evacuation // in this cycle, the region will be in the collection set. If there is not room, the region will be promoted // by evacuation in some future GC cycle. - promote_humongous(r); + + // We do not promote primitive arrays because there's no performance penalty keeping them in young. When/if they + // become garbage, reclaiming the memory from young is much quicker and more efficient than reclaiming them from old. + oop obj = cast_to_oop(r->bottom()); + if (!obj->is_typeArray()) { + promote_humongous(r); + } } else if (r->is_regular() && (r->get_top_before_promote() != nullptr)) { // Likewise, we cannot put promote-in-place regions into the collection set because that would also trigger // the LRB to copy on reference fetch. diff --git a/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp b/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp index 1321baa6366..c6a842c7754 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp @@ -34,6 +34,20 @@ range, \ constraint) \ \ + product(uintx, ShenandoahGenerationalMinPIPUsage, 30, EXPERIMENTAL, \ + "(Generational mode only) What percent of a heap region " \ + "should be used before we consider promoting a region in " \ + "place? Regions with less than this amount of used will " \ + "promoted by evacuation. A benefit of promoting in place " \ + "is that less work is required by the GC at the time the " \ + "region is promoted. A disadvantage of promoting in place " \ + "is that this introduces fragmentation of old-gen memory, " \ + "with old-gen regions scattered throughout the heap. Regions " \ + "that have been promoted in place may need to be evacuated at " \ + "a later time in order to compact old-gen memory to enable " \ + "future humongous allocations.") \ + range(0,100) \ + \ product(uintx, ShenandoahGenerationalHumongousReserve, 0, EXPERIMENTAL, \ "(Generational mode only) What percent of the heap should be " \ "reserved for humongous objects if possible. Old-generation " \ @@ -165,7 +179,7 @@ "collector accepts. In percents of heap region size.") \ range(0,100) \ \ - product(uintx, ShenandoahOldGarbageThreshold, 15, EXPERIMENTAL, \ + product(uintx, ShenandoahOldGarbageThreshold, 25, EXPERIMENTAL, \ "How much garbage an old region has to contain before it would " \ "be taken for collection.") \ range(0,100) \ From 000569da601afde85f83c361c9f1a7ba3814bff4 Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Thu, 18 Sep 2025 16:09:26 +0000 Subject: [PATCH 106/556] 8362561: Remove diagnostic option AllowArchivingWithJavaAgent Reviewed-by: sspitsyn, shade, dholmes, ayang --- src/hotspot/share/cds/aotMetaspace.cpp | 5 - src/hotspot/share/cds/cdsConfig.cpp | 11 -- src/hotspot/share/cds/cds_globals.hpp | 4 - src/hotspot/share/cds/dynamicArchive.cpp | 5 - src/hotspot/share/cds/filemap.cpp | 17 --- src/hotspot/share/cds/filemap.hpp | 1 - src/hotspot/share/classfile/classLoader.cpp | 18 --- src/hotspot/share/prims/jvmtiAgent.cpp | 19 +-- test/hotspot/jtreg/TEST.groups | 9 -- .../cds/appcds/LambdaWithJavaAgent.java | 95 ------------- .../appcds/TransformInterfaceOfLambda.java | 80 ----------- .../cds/appcds/aotCache/JavaAgent.java | 57 ++++++-- .../RedefineCallerClassTest.java | 108 --------------- ...ExceptionDuringDumpAtObjectsInitPhase.java | 87 ------------ .../cds/appcds/javaldr/GCDuringDump.java | 85 ------------ .../javaldr/GCSharedStringsDuringDump.java | 130 ------------------ .../cds/appcds/javaldr/LockDuringDump.java | 83 ----------- .../jvmti/dumpingWithAgent/AppWithBMH.java | 34 ----- .../DumpingWithJavaAgent.java | 129 ----------------- .../DumpingWithJvmtiAgent.java | 66 --------- .../OldClassWithJavaAgent.java | 61 -------- .../jvmti/dumpingWithAgent/SimpleAgent.java | 52 ------- .../jvmti/dumpingWithAgent/SimpleAgent.mf | 2 - .../libAddToSystemCLSearchOnLoad.c | 48 ------- .../OldClassAndRedefineClass.java | 3 +- 25 files changed, 52 insertions(+), 1157 deletions(-) delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/LambdaWithJavaAgent.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/TransformInterfaceOfLambda.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCDuringDump.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/javaldr/LockDuringDump.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/AppWithBMH.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJvmtiAgent.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/OldClassWithJavaAgent.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.java delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.mf delete mode 100644 test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/libAddToSystemCLSearchOnLoad.c diff --git a/src/hotspot/share/cds/aotMetaspace.cpp b/src/hotspot/share/cds/aotMetaspace.cpp index 341371ad6bc..51227768293 100644 --- a/src/hotspot/share/cds/aotMetaspace.cpp +++ b/src/hotspot/share/cds/aotMetaspace.cpp @@ -1076,11 +1076,6 @@ bool AOTMetaspace::write_static_archive(ArchiveBuilder* builder, FileMapInfo* ma return false; } builder->write_archive(map_info, heap_info); - - if (AllowArchivingWithJavaAgent) { - aot_log_warning(aot)("This %s was created with AllowArchivingWithJavaAgent. It should be used " - "for testing purposes only and should not be used in a production environment", CDSConfig::type_of_archive_being_loaded()); - } return true; } diff --git a/src/hotspot/share/cds/cdsConfig.cpp b/src/hotspot/share/cds/cdsConfig.cpp index 20bf6b0d67c..5bb46deb9bc 100644 --- a/src/hotspot/share/cds/cdsConfig.cpp +++ b/src/hotspot/share/cds/cdsConfig.cpp @@ -470,10 +470,6 @@ void CDSConfig::check_aot_flags() { assert(strcmp(AOTMode, "create") == 0, "checked by AOTModeConstraintFunc"); check_aotmode_create(); } - - // This is an old flag used by CDS regression testing only. It doesn't apply - // to the AOT workflow. - FLAG_SET_ERGO(AllowArchivingWithJavaAgent, false); } void CDSConfig::check_aotmode_off() { @@ -716,13 +712,6 @@ bool CDSConfig::check_vm_args_consistency(bool patch_mod_javabase, bool mode_fla } } - if (is_dumping_classic_static_archive() && AOTClassLinking) { - if (JvmtiAgentList::disable_agent_list()) { - FLAG_SET_ERGO(AllowArchivingWithJavaAgent, false); - log_warning(cds)("Disabled all JVMTI agents with -Xshare:dump -XX:+AOTClassLinking"); - } - } - return true; } diff --git a/src/hotspot/share/cds/cds_globals.hpp b/src/hotspot/share/cds/cds_globals.hpp index f4094aec1ac..3e3062097f9 100644 --- a/src/hotspot/share/cds/cds_globals.hpp +++ b/src/hotspot/share/cds/cds_globals.hpp @@ -63,10 +63,6 @@ "Average number of symbols per bucket in shared table") \ range(2, 246) \ \ - product(bool, AllowArchivingWithJavaAgent, false, DIAGNOSTIC, \ - "Allow Java agent to be run with CDS dumping (not applicable" \ - " to AOT") \ - \ develop(ccstr, ArchiveHeapTestClass, nullptr, \ "For JVM internal testing only. The static field named " \ "\"archivedObjects\" of the specified class is stored in the " \ diff --git a/src/hotspot/share/cds/dynamicArchive.cpp b/src/hotspot/share/cds/dynamicArchive.cpp index 0ba911b41bb..dd24f1e0c51 100644 --- a/src/hotspot/share/cds/dynamicArchive.cpp +++ b/src/hotspot/share/cds/dynamicArchive.cpp @@ -395,11 +395,6 @@ public: VMOp_Type type() const { return VMOp_PopulateDumpSharedSpace; } void doit() { ResourceMark rm; - if (AllowArchivingWithJavaAgent) { - aot_log_warning(aot)("This %s was created with AllowArchivingWithJavaAgent. It should be used " - "for testing purposes only and should not be used in a production environment", - CDSConfig::type_of_archive_being_loaded()); - } AOTClassLocationConfig::dumptime_check_nonempty_dirs(); _builder.doit(); } diff --git a/src/hotspot/share/cds/filemap.cpp b/src/hotspot/share/cds/filemap.cpp index 409052eae6a..8c2175622e9 100644 --- a/src/hotspot/share/cds/filemap.cpp +++ b/src/hotspot/share/cds/filemap.cpp @@ -259,7 +259,6 @@ void FileMapHeader::populate(FileMapInfo *info, size_t core_region_alignment, _has_platform_or_app_classes = AOTClassLocationConfig::dumptime()->has_platform_or_app_classes(); _requested_base_address = (char*)SharedBaseAddress; _mapped_base_address = (char*)SharedBaseAddress; - _allow_archiving_with_java_agent = AllowArchivingWithJavaAgent; } void FileMapHeader::copy_base_archive_name(const char* archive) { @@ -316,7 +315,6 @@ void FileMapHeader::print(outputStream* st) { st->print_cr("- _heap_ptrmap_start_pos: %zu", _heap_ptrmap_start_pos); st->print_cr("- _rw_ptrmap_start_pos: %zu", _rw_ptrmap_start_pos); st->print_cr("- _ro_ptrmap_start_pos: %zu", _ro_ptrmap_start_pos); - st->print_cr("- allow_archiving_with_java_agent:%d", _allow_archiving_with_java_agent); st->print_cr("- use_optimized_module_handling: %d", _use_optimized_module_handling); st->print_cr("- has_full_module_graph %d", _has_full_module_graph); st->print_cr("- has_aot_linked_classes %d", _has_aot_linked_classes); @@ -2051,21 +2049,6 @@ bool FileMapHeader::validate() { _has_platform_or_app_classes = false; } - // Java agents are allowed during run time. Therefore, the following condition is not - // checked: (!_allow_archiving_with_java_agent && AllowArchivingWithJavaAgent) - // Note: _allow_archiving_with_java_agent is set in the shared archive during dump time - // while AllowArchivingWithJavaAgent is set during the current run. - if (_allow_archiving_with_java_agent && !AllowArchivingWithJavaAgent) { - AOTMetaspace::report_loading_error("The setting of the AllowArchivingWithJavaAgent is different " - "from the setting in the %s.", file_type); - return false; - } - - if (_allow_archiving_with_java_agent) { - aot_log_warning(aot)("This %s was created with AllowArchivingWithJavaAgent. It should be used " - "for testing purposes only and should not be used in a production environment", file_type); - } - aot_log_info(aot)("The %s was created with UseCompressedOops = %d, UseCompressedClassPointers = %d, UseCompactObjectHeaders = %d", file_type, compressed_oops(), compressed_class_pointers(), compact_headers()); if (compressed_oops() != UseCompressedOops || compressed_class_pointers() != UseCompressedClassPointers) { diff --git a/src/hotspot/share/cds/filemap.hpp b/src/hotspot/share/cds/filemap.hpp index b40e793a0fd..a58271eefc7 100644 --- a/src/hotspot/share/cds/filemap.hpp +++ b/src/hotspot/share/cds/filemap.hpp @@ -135,7 +135,6 @@ private: char* _requested_base_address; // Archive relocation is not necessary if we map with this base address. char* _mapped_base_address; // Actual base address where archive is mapped. - bool _allow_archiving_with_java_agent; // setting of the AllowArchivingWithJavaAgent option bool _use_optimized_module_handling;// No module-relation VM options were specified, so we can skip // some expensive operations. bool _has_aot_linked_classes; // Was the CDS archive created with -XX:+AOTClassLinking diff --git a/src/hotspot/share/classfile/classLoader.cpp b/src/hotspot/share/classfile/classLoader.cpp index 1f2eb6d25cc..3c7f6f8130e 100644 --- a/src/hotspot/share/classfile/classLoader.cpp +++ b/src/hotspot/share/classfile/classLoader.cpp @@ -1306,24 +1306,6 @@ void ClassLoader::record_result_for_builtin_loader(s2 classpath_index, InstanceK AOTClassLocationConfig::dumptime_update_max_used_index(classpath_index); result->set_shared_classpath_index(classpath_index); - -#if INCLUDE_CDS_JAVA_HEAP - if (CDSConfig::is_dumping_heap() && AllowArchivingWithJavaAgent && result->defined_by_boot_loader() && - classpath_index < 0 && redefined) { - // When dumping the heap (which happens only during static dump), classes for the built-in - // loaders are always loaded from known locations (jimage, classpath or modulepath), - // so classpath_index should always be >= 0. - // The only exception is when a java agent is used during dump time (for testing - // purposes only). If a class is transformed by the agent, the AOTClassLocation of - // this class may point to an unknown location. This may break heap object archiving, - // which requires all the boot classes to be from known locations. This is an - // uncommon scenario (even in test cases). Let's simply disable heap object archiving. - ResourceMark rm; - log_warning(aot)("heap objects cannot be written because class %s maybe modified by ClassFileLoadHook.", - result->external_name()); - CDSConfig::disable_heap_dumping(); - } -#endif // INCLUDE_CDS_JAVA_HEAP } void ClassLoader::record_hidden_class(InstanceKlass* ik) { diff --git a/src/hotspot/share/prims/jvmtiAgent.cpp b/src/hotspot/share/prims/jvmtiAgent.cpp index 16a47042a69..66cb68b44b0 100644 --- a/src/hotspot/share/prims/jvmtiAgent.cpp +++ b/src/hotspot/share/prims/jvmtiAgent.cpp @@ -576,25 +576,14 @@ static bool invoke_Agent_OnAttach(JvmtiAgent* agent, outputStream* st) { } #if INCLUDE_CDS -// CDS dumping does not support native JVMTI agent. -// CDS dumping supports Java agent if the AllowArchivingWithJavaAgent diagnostic option is specified. static void check_cds_dump(JvmtiAgent* agent) { if (CDSConfig::new_aot_flags_used()) { // JEP 483 // Agents are allowed with -XX:AOTMode=record and -XX:AOTMode=on/auto. - // Agents are completely disabled when -XX:AOTMode=create + // Agents are completely disabled when -XX:AOTMode=create (see cdsConfig.cpp) assert(!CDSConfig::is_dumping_final_static_archive(), "agents should have been disabled with -XX:AOTMode=create"); - return; - } - - // This is classic CDS limitations -- we disallow agents by default. They can be used - // with -XX:+AllowArchivingWithJavaAgent, but that should be used for diagnostic purposes only. - assert(agent != nullptr, "invariant"); - if (!agent->is_instrument_lib()) { - vm_exit_during_cds_dumping("CDS dumping does not support native JVMTI agent, name", agent->name()); - } - if (!AllowArchivingWithJavaAgent) { - vm_exit_during_cds_dumping( - "Must enable AllowArchivingWithJavaAgent in order to run Java agent during CDS dumping"); + } else if (CDSConfig::is_dumping_classic_static_archive() || CDSConfig::is_dumping_dynamic_archive()) { + // Classic CDS (static or dynamic dump). Disallow agents. + vm_exit_during_cds_dumping("JVMTI agents are not allowed when dumping CDS archives"); } } #endif // INCLUDE_CDS diff --git a/test/hotspot/jtreg/TEST.groups b/test/hotspot/jtreg/TEST.groups index 18cd93d4856..8169ca87f4e 100644 --- a/test/hotspot/jtreg/TEST.groups +++ b/test/hotspot/jtreg/TEST.groups @@ -427,9 +427,6 @@ hotspot_appcds_dynamic = \ -runtime/cds/appcds/jigsaw/modulepath/OptimizeModuleHandlingTest.java \ -runtime/cds/appcds/loaderConstraints/DynamicLoaderConstraintsTest.java \ -runtime/cds/appcds/javaldr/ArrayTest.java \ - -runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java \ - -runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java \ - -runtime/cds/appcds/javaldr/LockDuringDump.java \ -runtime/cds/appcds/jcmd/JCmdTestStaticDump.java \ -runtime/cds/appcds/jcmd/JCmdTestDynamicDump.java \ -runtime/cds/appcds/jcmd/JCmdTestFileSafety.java \ @@ -451,7 +448,6 @@ hotspot_appcds_dynamic = \ -runtime/cds/appcds/LambdaInvokeVirtual.java \ -runtime/cds/appcds/LambdaProxyClasslist.java \ -runtime/cds/appcds/LambdaVerificationFailedDuringDump.java \ - -runtime/cds/appcds/LambdaWithJavaAgent.java \ -runtime/cds/appcds/LambdaWithUseImplMethodHandle.java \ -runtime/cds/appcds/LambdaWithOldClass.java \ -runtime/cds/appcds/LongClassListPath.java \ @@ -495,7 +491,6 @@ hotspot_cds_verify_shared_spaces = \ runtime/cds/appcds/customLoader/LoaderSegregationTest.java \ runtime/cds/appcds/javaldr/ArrayTest.java \ runtime/cds/appcds/jigsaw/modulepath/ExportModule.java \ - runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java \ runtime/cds/appcds/sharedStrings/SharedStringsBasic.java # No need to run every test with EpsilonGC. A small subset will provide enough @@ -536,9 +531,6 @@ hotspot_aot_classlinking = \ -runtime/cds/appcds/DumpClassListWithLF.java \ -runtime/cds/appcds/dynamicArchive \ -runtime/cds/appcds/HelloExtTest.java \ - -runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java \ - -runtime/cds/appcds/javaldr/GCDuringDump.java \ - -runtime/cds/appcds/javaldr/LockDuringDump.java \ -runtime/cds/appcds/jigsaw/classpathtests/EmptyClassInBootClassPath.java \ -runtime/cds/appcds/jigsaw/ExactOptionMatch.java \ -runtime/cds/appcds/jigsaw/JigsawOptionsCombo.java \ @@ -554,7 +546,6 @@ hotspot_aot_classlinking = \ -runtime/cds/appcds/JvmtiAddPath.java \ -runtime/cds/appcds/jvmti \ -runtime/cds/appcds/LambdaProxyClasslist.java \ - -runtime/cds/appcds/LambdaWithJavaAgent.java \ -runtime/cds/appcds/loaderConstraints/LoaderConstraintsTest.java \ -runtime/cds/appcds/methodHandles \ -runtime/cds/appcds/NestHostOldInf.java \ diff --git a/test/hotspot/jtreg/runtime/cds/appcds/LambdaWithJavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/LambdaWithJavaAgent.java deleted file mode 100644 index accb74025ce..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/LambdaWithJavaAgent.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @bug 8276126 - * @summary Test static dumping with java agent transforming a class loaded - * by the boot class loader. - * @requires vm.cds.write.archived.java.heap - * @requires vm.jvmti - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @compile test-classes/Hello.java - * @compile test-classes/TransformBootClass.java - * @run driver LambdaWithJavaAgent - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.cds.CDSTestUtils; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class LambdaWithJavaAgent { - - public static String agentClasses[] = { - TransformBootClass.class.getName(), - }; - - public static void main(String[] args) throws Exception { - String mainClass = Hello.class.getName(); - String namePrefix = "lambda-with-java-agent"; - JarBuilder.build(namePrefix, mainClass); - - String appJar = TestCommon.getTestJar(namePrefix + ".jar"); - String classList = namePrefix + ".list"; - String archiveName = namePrefix + ".jsa"; - - String agentJar = - ClassFileInstaller.writeJar("TransformBootClass.jar", - ClassFileInstaller.Manifest.fromSourceFile("test-classes/TransformBootClass.mf"), - agentClasses); - String useJavaAgent = "-javaagent:" + agentJar + "=jdk/internal/math/FDBigInteger"; - - // dump class list - CDSTestUtils.dumpClassList(classList, "-cp", appJar, mainClass); - - // create archive with the class list - CDSOptions opts = (new CDSOptions()) - .addPrefix("-XX:ExtraSharedClassListFile=" + classList, - "-cp", appJar, - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - useJavaAgent, - "-Xlog:class+load,cds+class=debug,aot,cds") - .setArchiveName(archiveName); - OutputAnalyzer output = CDSTestUtils.createArchiveAndCheck(opts); - output.shouldContain("heap objects cannot be written because class jdk.internal.math.FDBigInteger maybe modified by ClassFileLoadHook") - .shouldContain("Skipping jdk/internal/math/FDBigInteger: Unsupported location") - .shouldMatch(".class.load.*jdk.internal.math.FDBigInteger.*source.*modules"); - - // run with archive - CDSOptions runOpts = (new CDSOptions()) - .addPrefix("-cp", appJar, "-Xlog:class+load=debug,aot=debug,cds=debug,class+path=debug", - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - useJavaAgent) - .setArchiveName(archiveName) - .setUseVersion(false) - .addSuffix(mainClass); - output = CDSTestUtils.runWithArchive(runOpts); - TestCommon.checkExecReturn(output, 0, true, - "Hello source: shared objects file"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/TransformInterfaceOfLambda.java b/test/hotspot/jtreg/runtime/cds/appcds/TransformInterfaceOfLambda.java deleted file mode 100644 index 88620fc039f..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/TransformInterfaceOfLambda.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @bug 8323950 - * @summary Transforming an interface of an archived lambda proxy class should not - * crash the VM. The lambda proxy class should be regenerated during runtime. - * @requires vm.cds - * @requires vm.cds.default.archive.available - * @requires vm.jvmti - * @requires vm.flagless - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @compile test-classes/SimpleTest.java - * @compile test-classes/TransformBootClass.java - * @run driver TransformInterfaceOfLambda - */ - -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class TransformInterfaceOfLambda { - - public static String agentClasses[] = { - TransformBootClass.class.getName(), - }; - - public static void main(String[] args) throws Exception { - String mainClass = SimpleTest.class.getName(); - String namePrefix = "transform-interface-of-lambda"; - JarBuilder.build(namePrefix, mainClass); - - String appJar = TestCommon.getTestJar(namePrefix + ".jar"); - - String agentJar = - ClassFileInstaller.writeJar("TransformBootClass.jar", - ClassFileInstaller.Manifest.fromSourceFile("test-classes/TransformBootClass.mf"), - agentClasses); - String useJavaAgent = "-javaagent:" + agentJar + "=java/util/function/IntFunction"; - - ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder( - "-cp", appJar, "-Xlog:class+load,cds=debug", - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - useJavaAgent, - mainClass); - OutputAnalyzer out = new OutputAnalyzer(pb.start()); - System.out.println(out.getStdout()); - out.shouldHaveExitValue(0) - // the class loaded by the SimpleTest should be from the archive - .shouldContain("[class,load] java.text.SimpleDateFormat source: shared objects file") - // the IntFunction is the interface which is being transformed. The - // interface is a super type of the following lambda proxy class. - .shouldContain("Transforming class java/util/function/IntFunction") - // the lambda proxy class should be regenerated - .shouldMatch(".class.load.*sun.util.locale.provider.LocaleProviderAdapter[$][$]Lambda/0x.*source:.*sun.util.locale.provider.LocaleProviderAdapter"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java index a0feb76a910..491a5bf1fa7 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java @@ -26,7 +26,7 @@ /* * @test id=static * @bug 8361725 - * @summary -javaagent should be disabled with -Xshare:dump -XX:+AOTClassLinking + * @summary -javaagent is not allowed when creating static CDS archive * @requires vm.cds.supports.aot.class.linking * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes * @build JavaAgent JavaAgentTransformer Util @@ -34,6 +34,18 @@ * @run driver JavaAgent STATIC */ +/** + * @test id=dynamic + * @bug 8362561 + * @summary -javaagent is not allowed when creating dynamic CDS archive + * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes + * @build JavaAgent JavaAgentTransformer Util + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar JavaAgentApp JavaAgentApp$ShouldBeTransformed + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. JavaAgent DYNAMIC + */ + /* * @test id=aot * @summary -javaagent should be allowed in AOT workflow. However, classes transformed/redefined by agents will not @@ -64,7 +76,13 @@ public class JavaAgent { ClassFileInstaller.Manifest.fromSourceFile("JavaAgentTransformer.mf"), agentClasses); - new Tester().run(args); + Tester t = new Tester(); + if (args[0].equals("STATIC") || args[0].equals("DYNAMIC")) { + // Some child processes may have non-zero exits. These are checked by + // checkExecutionForStaticWorkflow() and checkExecutionForDynamicWorkflow + t.setCheckExitValue(false); + } + t.run(args); } static class Tester extends CDSAppTester { @@ -80,8 +98,6 @@ public class JavaAgent { @Override public String[] vmArgs(RunMode runMode) { return new String[] { - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", "-javaagent:" + agentJar, "-Xlog:aot,cds", "-XX:+AOTClassLinking", @@ -99,8 +115,10 @@ public class JavaAgent { public void checkExecution(OutputAnalyzer out, RunMode runMode) throws Exception { if (isAOTWorkflow()) { checkExecutionForAOTWorkflow(out, runMode); - } else { + } else if (isStaticWorkflow()) { checkExecutionForStaticWorkflow(out, runMode); + } else { + checkExecutionForDynamicWorkflow(out, runMode); } } @@ -133,12 +151,31 @@ public class JavaAgent { public void checkExecutionForStaticWorkflow(OutputAnalyzer out, RunMode runMode) throws Exception { switch (runMode) { - case RunMode.DUMP_STATIC: - out.shouldContain("Disabled all JVMTI agents with -Xshare:dump -XX:+AOTClassLinking"); - out.shouldNotContain(agentPremainFinished); - break; - default: + case RunMode.TRAINING: out.shouldContain(agentPremainFinished); + out.shouldHaveExitValue(0); + break; + case RunMode.DUMP_STATIC: + out.shouldContain("JVMTI agents are not allowed when dumping CDS archives"); + out.shouldNotHaveExitValue(0); + break; + case RunMode.PRODUCTION: + out.shouldContain("Unable to use shared archive: invalid archive"); + out.shouldNotHaveExitValue(0); + break; + } + } + + public void checkExecutionForDynamicWorkflow(OutputAnalyzer out, RunMode runMode) throws Exception { + switch (runMode) { + case RunMode.DUMP_DYNAMIC: + out.shouldContain("JVMTI agents are not allowed when dumping CDS archives"); + out.shouldNotHaveExitValue(0); + break; + case RunMode.PRODUCTION: + out.shouldContain("Unable to use shared archive: invalid archive"); + out.shouldNotHaveExitValue(0); + break; } } } diff --git a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java deleted file mode 100644 index 90d403bc380..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/RedefineCallerClassTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @bug 8276184 - * @summary If the caller class is redefined during dump time, the caller class - * and its lambda proxy class should not be archived. - * @requires vm.cds - * @requires vm.jvmti - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds - * /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * /test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/test-classes - * @build jdk.test.whitebox.WhiteBox OldProvider - * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox - * @run driver RedefineClassHelper - * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. RedefineCallerClassTest - */ - -import jdk.test.lib.helpers.ClassFileInstaller; - -public class RedefineCallerClassTest extends DynamicArchiveTestBase { - static String mainClass = RedefineCallerClass.class.getName(); - - static String providerClass = OldProvider.class.getName(); - - static String sharedClasses[] = { - mainClass, - "SimpleLambda", // caller class will be redefined in RedefineCallerClass - providerClass, // inteface with class file major version < 50 - "jdk/test/lib/compiler/InMemoryJavaCompiler", - "jdk/test/lib/compiler/InMemoryJavaCompiler$FileManagerWrapper", - "jdk/test/lib/compiler/InMemoryJavaCompiler$FileManagerWrapper$1", - "jdk/test/lib/compiler/InMemoryJavaCompiler$SourceFile", - "jdk/test/lib/compiler/InMemoryJavaCompiler$ClassFile" - }; - - public static void main(String[] args) throws Exception { - runTest(RedefineCallerClassTest::test); - } - - static void test() throws Exception { - String topArchiveName = getNewArchiveName(); - String appJar = ClassFileInstaller.writeJar("redefine_caller_class.jar", sharedClasses); - - String[] mainArgs = { - "redefineCaller", // redefine caller class only - "useOldInf", // use old interface only - "both" // both of the above - }; - - for (String mainArg : mainArgs) { - String[] options = { - "-Xlog:class+load,cds", - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - "-javaagent:redefineagent.jar", - "-cp", appJar, mainClass, mainArg - }; - - dump(topArchiveName, options) - .assertNormalExit(output -> { - output.shouldHaveExitValue(0); - if (mainArg.equals("both") || mainArg.equals("useOldInf")) { - output.shouldContain("Skipping OldProvider: Old class has been linked") - .shouldMatch("Skipping.SimpleLambda[$][$]Lambda.*0x.*:.*Old.class.has.been.linked"); - } - if (mainArg.equals("both") || mainArg.equals("redefineCaller")) { - output.shouldContain("Skipping SimpleLambda: Has been redefined"); - } - }); - - run(topArchiveName, options) - .assertNormalExit(output -> { - output.shouldHaveExitValue(0) - .shouldContain("RedefineCallerClass source: shared objects file (top)") - .shouldMatch(".class.load. SimpleLambda[$][$]Lambda.*/0x.*source:.*SimpleLambda"); - if (mainArg.equals("both") || mainArg.equals("useOldInf")) { - output.shouldMatch(".class.load. OldProvider.source:.*redefine_caller_class.jar"); - } - if (mainArg.equals("both") || mainArg.equals("redefineCaller")) { - output.shouldMatch(".class.load. SimpleLambda.source:.*redefine_caller_class.jar"); - } - }); - } - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java deleted file mode 100644 index 5d9ef9ef760..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/ExceptionDuringDumpAtObjectsInitPhase.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @summary Out of memory When dumping the CDS archive - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds.write.archived.java.heap - * @requires vm.jvmti - * @run driver ExceptionDuringDumpAtObjectsInitPhase - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class ExceptionDuringDumpAtObjectsInitPhase { - public static String appClasses[] = { - Hello.class.getName(), - }; - public static String agentClasses[] = { - GCDuringDumpTransformer.class.getName(), - GCDuringDumpTransformer.MyCleaner.class.getName(), - }; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("GCDuringDumpTransformer.jar", - ClassFileInstaller.Manifest.fromSourceFile("GCDuringDumpTransformer.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("GCDuringDumpApp.jar", appClasses); - - String gcLog = Boolean.getBoolean("test.cds.verbose.gc") ? - "-Xlog:gc*=info,gc+region=trace,gc+alloc+region=debug" : "-showversion"; - - // 1. Test with exception - System.out.println("1. Exception during dump"); - TestCommon.dump(appJar, - TestCommon.list(Hello.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - "-javaagent:" + agentJar, - "-Xlog:cds,class+load", - "-Xmx32m", - "-Dtest.with.exception=true", - gcLog).shouldNotHaveExitValue(0) - .shouldContain("Preload Warning: Cannot find jdk/internal/math/FDBigInteger") - .shouldContain("Unexpected exception, use -Xlog:aot,cds,exceptions=trace for detail"); - - // 2. Test with OOM - System.out.println("2. OOM during dump"); - TestCommon.dump(appJar, - TestCommon.list(Hello.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", - "-javaagent:" + agentJar, - "-Dtest.with.oom=true", - "-Xlog:cds,class+load", - "-Xmx12M", - gcLog).shouldNotHaveExitValue(0) - .shouldContain("Out of memory. Please run with a larger Java heap, current MaxHeapSize"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCDuringDump.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCDuringDump.java deleted file mode 100644 index dd57c7efa2e..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCDuringDump.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @summary When dumping the CDS archive, try to cause garbage collection while classes are being loaded. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds - * @requires vm.jvmti - * @run driver GCDuringDump - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class GCDuringDump { - public static String appClasses[] = { - Hello.class.getName(), - }; - public static String agentClasses[] = { - GCDuringDumpTransformer.class.getName(), - GCDuringDumpTransformer.MyCleaner.class.getName(), - }; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("GCDuringDumpTransformer.jar", - ClassFileInstaller.Manifest.fromSourceFile("GCDuringDumpTransformer.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("GCDuringDumpApp.jar", appClasses); - - String gcLog = Boolean.getBoolean("test.cds.verbose.gc") ? - "-Xlog:gc*=info,gc+region=trace,gc+alloc+region=debug" : "-showversion"; - - for (int i=0; i<3; i++) { - // i = 0 -- run without agent = no extra GCs - // i = 1 -- run with agent = cause extra GCs - // i = 2 -- run with agent = cause extra GCs + use java.lang.ref.Cleaner - - String extraArg = (i == 0) ? "-showversion" : "-javaagent:" + agentJar; - String extraOption = (i == 0) ? "-showversion" : "-XX:+AllowArchivingWithJavaAgent"; - String extraOption2 = (i != 2) ? "-showversion" : "-Dtest.with.cleaner=true"; - - TestCommon.testDump(appJar, TestCommon.list(Hello.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", extraOption, extraOption2, - "-Xlog:exceptions=trace", - extraArg, "-Xmx32m", gcLog); - - TestCommon.run( - "-cp", appJar, - "-Xmx32m", - "-Xlog:cds=info", - "-XX:+UnlockDiagnosticVMOptions", extraOption, - gcLog, - Hello.class.getName()) - .assertNormalExit(); - } - } -} - diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java deleted file mode 100644 index 1dac0149964..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/GCSharedStringsDuringDump.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @summary Similar to GCDuringDumping.java, this test adds the -XX:SharedArchiveConfigFile - * option for testing the interaction with GC and shared strings. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds.write.archived.java.heap - * @requires vm.jvmti - * @build jdk.test.whitebox.WhiteBox - * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox - * @run driver/timeout=480 GCSharedStringsDuringDump - */ - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.cds.CDSTestUtils; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class GCSharedStringsDuringDump { - static { - // EpsilonGC will run out of memory. - CDSOptions.disableRuntimePrefixForEpsilonGC(); - } - public static String appClasses[] = { - GCSharedStringsDuringDumpWb.class.getName(), - }; - public static String agentClasses[] = { - GCDuringDumpTransformer.class.getName(), - }; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("GCDuringDumpTransformer.jar", - ClassFileInstaller.Manifest.fromSourceFile("GCDuringDumpTransformer.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("GCSharedStringsDuringDumpApp.jar", appClasses); - - String gcLog = Boolean.getBoolean("test.cds.verbose.gc") ? - "-Xlog:gc*=info,gc+region=trace,gc+alloc+region=debug" : "-showversion"; - - String sharedArchiveCfgFile = - CDSTestUtils.getOutputDir() + File.separator + "GCSharedStringDuringDump_gen.txt"; - try (FileOutputStream fos = new FileOutputStream(sharedArchiveCfgFile)) { - PrintWriter out = new PrintWriter(new OutputStreamWriter(fos)); - out.println("VERSION: 1.0"); - out.println("@SECTION: String"); - out.println("31: shared_test_string_unique_14325"); - for (int i=0; i<100000; i++) { - String s = "generated_string " + i; - out.println(s.length() + ": " + s); - } - out.close(); - } - - JarBuilder.build(true, "WhiteBox", "jdk/test/whitebox/WhiteBox"); - String whiteBoxJar = TestCommon.getTestJar("WhiteBox.jar"); - String bootClassPath = "-Xbootclasspath/a:" + whiteBoxJar; - - for (int i=0; i<2; i++) { - // i = 0 -- run without agent = no extra GCs - // i = 1 -- run with agent = cause extra GCs - - String extraArg = (i == 0) ? "-showversion" : "-javaagent:" + agentJar; - String extraOption = (i == 0) ? "-showversion" : "-XX:+AllowArchivingWithJavaAgent"; - OutputAnalyzer output = TestCommon.dump( - appJar, TestCommon.list(GCSharedStringsDuringDumpWb.class.getName()), - bootClassPath, extraArg, "-Xmx32m", gcLog, - "-XX:SharedArchiveConfigFile=" + sharedArchiveCfgFile, - "-XX:+UnlockDiagnosticVMOptions", extraOption); - - if (output.getStdout().contains("Too many string space regions") || - output.getStderr().contains("Unable to write archive heap memory regions") || - output.getStdout().contains("Try increasing NewSize") || - !output.getStdout().contains("oa0 space:") || - output.getExitValue() != 0) { - // Try again with larger heap and NewSize, this should increase the - // G1 heap region size to 2M - TestCommon.testDump( - appJar, TestCommon.list(GCSharedStringsDuringDumpWb.class.getName()), - bootClassPath, extraArg, "-Xmx8g", "-XX:NewSize=8m", gcLog, - "-XX:SharedArchiveConfigFile=" + sharedArchiveCfgFile, - "-XX:+UnlockDiagnosticVMOptions", extraOption); - } - - TestCommon.run( - "-cp", appJar, - bootClassPath, - extraArg, - "-Xlog:cds=info,class+path=info", - "-Xmx32m", - "-Xlog:cds=info", - "-XX:+UnlockDiagnosticVMOptions", - extraOption, - "-XX:+WhiteBoxAPI", - gcLog, - GCSharedStringsDuringDumpWb.class.getName()) - .assertNormalExit(); - } - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/LockDuringDump.java b/test/hotspot/jtreg/runtime/cds/appcds/javaldr/LockDuringDump.java deleted file mode 100644 index 391596160d6..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/javaldr/LockDuringDump.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @bug 8249276 - * @summary When dumping the CDS archive, try to lock some objects. These objects should be archived - * without the locking bits in the markWord. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds - * @requires vm.cds - * @requires vm.jvmti - * @modules java.instrument - * @run driver LockDuringDump - */ - -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class LockDuringDump { - public static String appClasses[] = { - LockDuringDumpApp.class.getName(), - }; - public static String agentClasses[] = { - LockDuringDumpAgent.class.getName(), - }; - - private static final String MANIFEST = - "Manifest-Version: 1.0\nPremain-Class: LockDuringDumpAgent\n"; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("LockDuringDumpAgent.jar", - ClassFileInstaller.Manifest.fromString(MANIFEST), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("LockDuringDumpApp.jar", appClasses); - - for (int i = 0; i < 3; i++) { - // i = 0 -- dump without agent - // i = 1 -- dump with agent - - String agentArg = (i == 0) ? "-showversion" : "-javaagent:" + agentJar; - String agentArg2 = (i == 0) ? "-showversion" : "-XX:+AllowArchivingWithJavaAgent"; - - OutputAnalyzer out = - TestCommon.testDump(appJar, TestCommon.list(LockDuringDumpApp.class.getName()), - "-XX:+UnlockDiagnosticVMOptions", - agentArg, agentArg2); - if (i != 0 && !out.getStdout().contains("LockDuringDumpAgent timeout")) { - out.shouldContain("Let's hold the lock on the literal string"); - } - - TestCommon.run( - "-cp", appJar, - "-XX:+UnlockDiagnosticVMOptions", agentArg2, - LockDuringDumpApp.class.getName()) - .assertNormalExit("I am able to lock the literal string"); - } - } -} - diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/AppWithBMH.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/AppWithBMH.java deleted file mode 100644 index 7e08fd24cf6..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/AppWithBMH.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -// Application which loads BoundMethodHandle species classes like the following: -// java/lang/invoke/BoundMethodHandle$Species_LLLL -import java.lang.management.ManagementFactory; - -public class AppWithBMH { - public static void main(String[] args) { - System.out.println("Hello world!"); - ManagementFactory.getGarbageCollectorMXBeans(); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java deleted file mode 100644 index 54cb3fb23fb..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJavaAgent.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/* - * @test - * @summary CDS dumping with java agent. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds - * @requires vm.jvmti - * @build SimpleAgent Hello AppWithBMH - * @run main/othervm DumpingWithJavaAgent - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class DumpingWithJavaAgent { - public static String appClasses[] = { - "Hello", - "AppWithBMH", - }; - public static String agentClasses[] = { - "SimpleAgent", - "SimpleAgent$1" - }; - - public static String warningMessages[] = { - "This shared archive file was created with AllowArchivingWithJavaAgent", - "It should be used for testing purposes only and should not be used in a production environment", - }; - - public static String errorMessage = - "The setting of the AllowArchivingWithJavaAgent is different from the setting in the shared archive file."; - - public static String diagnosticOption = "-XX:+AllowArchivingWithJavaAgent"; - - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("SimpleAgent.jar", - ClassFileInstaller.Manifest.fromSourceFile("SimpleAgent.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("DumpingWithJavaAgent.jar", appClasses); - - // CDS dumping with a java agent performing class transformation on BoundMethodHandle$Species classes - OutputAnalyzer output = TestCommon.testDump(appJar, TestCommon.list("AppWithBMH"), - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "-javaagent:" + agentJar + "=doTransform", - "AppWithBMH"); - TestCommon.checkDump(output); - output.shouldContain(warningMessages[0]); - output.shouldContain(warningMessages[1]); - output.shouldContain("inside SimpleAgent"); - - // CDS dumping with a java agent with the AllowArchvingWithJavaAgent diagnostic option. - output = TestCommon.testDump(appJar, TestCommon.list("Hello"), - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "-javaagent:" + agentJar); - TestCommon.checkDump(output); - output.shouldContain(warningMessages[0]); - output.shouldContain(warningMessages[1]); - output.shouldContain("inside SimpleAgent"); - - // Using the archive with the AllowArchvingWithJavaAgent diagnostic option. - output = TestCommon.exec( - appJar, - "-Xlog:class+load=trace", - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "Hello"); - if (!TestCommon.isUnableToMap(output)) { - output.shouldHaveExitValue(0); - output.shouldContain(warningMessages[0]); - output.shouldContain(warningMessages[1]); - output.shouldContain("[class,load] Hello source: shared objects file"); - } - - // Using the archive with -Xshare:on without the diagnostic option. - // VM should exit with an error message. - output = TestCommon.exec( - appJar, - "Hello"); - output.shouldHaveExitValue(1); - output.shouldContain(errorMessage); - - // Using the archive with -Xshare:auto without the diagnostic option. - // VM should continue execution with a warning message. The archive - // will not be used. - output = TestCommon.execAuto( - "-cp", appJar, - "-Xlog:class+load=trace,cds=info", - "Hello"); - if (!TestCommon.isUnableToMap(output)) { - output.shouldHaveExitValue(0); - output.shouldContain(errorMessage); - output.shouldMatch(".class.load.* Hello source:.*DumpingWithJavaAgent.jar"); - - // CDS dumping with a java agent without the AllowArchvingWithJavaAgent diagnostic option. - // VM will exit with an error message. - output = TestCommon.dump(appJar, TestCommon.list("Hello"), - "-javaagent:" + agentJar); - } - output.shouldContain("Must enable AllowArchivingWithJavaAgent in order to run Java agent during CDS dumping") - .shouldHaveExitValue(1); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJvmtiAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJvmtiAgent.java deleted file mode 100644 index 9f3c5864f1b..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/DumpingWithJvmtiAgent.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/** - * @test - * @summary CDS dumping with JVMTI agent. - * @requires vm.cds - * @requires vm.jvmti - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds - * @compile ../../test-classes/Hello.java - * @run main/othervm/native DumpingWithJvmtiAgent - */ - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import jdk.test.lib.process.OutputAnalyzer; - -public class DumpingWithJvmtiAgent { - private static final String AGENT_LIB_ONLOAD = "AddToSystemCLSearchOnLoad"; - - public static void main(String[] args) throws Exception { - String appJar = JarBuilder.getOrCreateHelloJar(); - - // CDS dump with a JVMTI agent with the AllowArchivingWithJavaAgent option. - // vm should exit with an error message. - OutputAnalyzer out = TestCommon.dump( - appJar, - TestCommon.list("Hello"), - "-XX:+UnlockDiagnosticVMOptions", "-XX:+AllowArchivingWithJavaAgent", - "-agentlib:" + AGENT_LIB_ONLOAD + "=" + appJar, - "-Djava.library.path=" + System.getProperty("java.library.path")); - out.shouldContain("CDS dumping does not support native JVMTI agent, name: " + AGENT_LIB_ONLOAD) - .shouldHaveExitValue(1); - - // CDS dump with a JVMTI agent without the AllowArchivingWithJavaAgent option. - // vm should exit with an error message. - out = TestCommon.dump( - appJar, - TestCommon.list("Hello"), - "-agentlib:" + AGENT_LIB_ONLOAD + "=" + appJar, - "-Djava.library.path=" + System.getProperty("java.library.path")); - out.shouldContain("CDS dumping does not support native JVMTI agent, name: " + AGENT_LIB_ONLOAD) - .shouldHaveExitValue(1); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/OldClassWithJavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/OldClassWithJavaAgent.java deleted file mode 100644 index a0a34eab9f9..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/OldClassWithJavaAgent.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ -/* - * @test - * @bug 8261090 - * @summary Dump old class with java agent. - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes - * @requires vm.cds - * @requires vm.jvmti - * @compile ../../test-classes/OldSuper.jasm - * @compile SimpleAgent.java - * @run main/othervm OldClassWithJavaAgent - */ - -import jdk.test.lib.cds.CDSOptions; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.helpers.ClassFileInstaller; - -public class OldClassWithJavaAgent { - public static String appClasses[] = {"OldSuper"}; - public static String agentClasses[] = {"SimpleAgent"}; - public static String diagnosticOption = "-XX:+AllowArchivingWithJavaAgent"; - public static void main(String[] args) throws Throwable { - String agentJar = - ClassFileInstaller.writeJar("SimpleAgent.jar", - ClassFileInstaller.Manifest.fromSourceFile("SimpleAgent.mf"), - agentClasses); - - String appJar = - ClassFileInstaller.writeJar("OldClassWithJavaAgent.jar", appClasses); - OutputAnalyzer output = TestCommon.testDump(appJar, TestCommon.list("OldSuper"), - "-Xlog:cds=debug,class+load", - "-XX:+UnlockDiagnosticVMOptions", diagnosticOption, - "-javaagent:" + agentJar + "=OldSuper"); - - // The java agent will load and link the class. We will skip old classes - // which have been linked during CDS dump. - output.shouldContain("Skipping OldSuper: Old class has been linked"); - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.java deleted file mode 100644 index d280964c07d..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ -import java.lang.instrument.ClassFileTransformer; -import java.lang.instrument.Instrumentation; -import java.security.ProtectionDomain; -import java.util.Arrays; - -public class SimpleAgent { - public static void premain(String agentArg, Instrumentation instrumentation) throws Exception { - System.out.println("inside SimpleAgent"); - if (agentArg == null) return; - if (agentArg.equals("OldSuper")) { - // Only load the class if the test requires it. - Class cls = Class.forName("OldSuper", true, ClassLoader.getSystemClassLoader()); - } else if (agentArg.equals("doTransform")) { - ClassFileTransformer transformer = new ClassFileTransformer() { - @Override - public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { - if (loader == SimpleAgent.class.getClassLoader()) { - // Transform only classes loaded by the apploader. - System.out.printf("%n Transforming %s", className); - return Arrays.copyOf(classfileBuffer, classfileBuffer.length); - } else { - return null; - } - } - }; - instrumentation.addTransformer(transformer); - } - } -} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.mf b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.mf deleted file mode 100644 index f53b5299627..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/SimpleAgent.mf +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 -Premain-Class: SimpleAgent diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/libAddToSystemCLSearchOnLoad.c b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/libAddToSystemCLSearchOnLoad.c deleted file mode 100644 index 9d831f74936..00000000000 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/dumpingWithAgent/libAddToSystemCLSearchOnLoad.c +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - - static jvmtiEnv *jvmti = NULL; - - JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) { - int err = (*jvm)->GetEnv(jvm, (void**) &jvmti, JVMTI_VERSION_9); - if (err != JNI_OK) { - return JNI_ERR; - } - err = (*jvmti)->AddToSystemClassLoaderSearch(jvmti, (const char*)options); - if (err != JVMTI_ERROR_NONE) { - return JNI_ERR; - } - return JNI_OK; - } - -#ifdef __cplusplus -} -#endif diff --git a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java index 7581b2367aa..26bccdf764e 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/jvmti/redefineClasses/OldClassAndRedefineClass.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -67,7 +67,6 @@ public class OldClassAndRedefineClass { out = TestCommon.exec( appJar, "-XX:+UnlockDiagnosticVMOptions", - "-XX:+AllowArchivingWithJavaAgent", "-XX:+WhiteBoxAPI", "-Xlog:cds,class+load", agentCmdArg, From 14b9f53bb376c49b73b376c6e5a4b30105358b5a Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Thu, 18 Sep 2025 17:22:45 +0000 Subject: [PATCH 107/556] 8367626: Parallel: Remove ParallelCompactData::summarize_dense_prefix Reviewed-by: gli, fandreuzzi --- .../share/gc/parallel/psParallelCompact.cpp | 35 +++---------------- .../share/gc/parallel/psParallelCompact.hpp | 5 --- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.cpp b/src/hotspot/share/gc/parallel/psParallelCompact.cpp index e63e189861f..dd8455104a9 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.cpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.cpp @@ -297,29 +297,6 @@ void ParallelCompactData::clear_range(size_t beg_region, size_t end_region) { memset(_region_data + beg_region, 0, region_cnt * sizeof(RegionData)); } -void -ParallelCompactData::summarize_dense_prefix(HeapWord* beg, HeapWord* end) -{ - assert(is_region_aligned(beg), "not RegionSize aligned"); - assert(is_region_aligned(end), "not RegionSize aligned"); - - size_t cur_region = addr_to_region_idx(beg); - const size_t end_region = addr_to_region_idx(end); - HeapWord* addr = beg; - while (cur_region < end_region) { - _region_data[cur_region].set_destination(addr); - _region_data[cur_region].set_destination_count(0); - _region_data[cur_region].set_source_region(cur_region); - - // Update live_obj_size so the region appears completely full. - size_t live_size = RegionSize - _region_data[cur_region].partial_obj_size(); - _region_data[cur_region].set_live_obj_size(live_size); - - ++cur_region; - addr += RegionSize; - } -} - // The total live words on src_region would overflow the target space, so find // the overflowing object and record the split point. The invariant is that an // obj should not cross space boundary. @@ -894,7 +871,6 @@ void PSParallelCompact::summary_phase(bool should_do_max_compaction) if (dense_prefix_end != old_space->bottom()) { fill_dense_prefix_end(id); - _summary_data.summarize_dense_prefix(old_space->bottom(), dense_prefix_end); } // Compacting objs in [dense_prefix_end, old_space->top()) @@ -1539,17 +1515,16 @@ void PSParallelCompact::forward_to_new_addr() { #ifdef ASSERT void PSParallelCompact::verify_forward() { HeapWord* const old_dense_prefix_addr = dense_prefix(SpaceId(old_space_id)); - RegionData* old_region = _summary_data.region(_summary_data.addr_to_region_idx(old_dense_prefix_addr)); - HeapWord* bump_ptr = old_region->partial_obj_size() != 0 - ? old_dense_prefix_addr + old_region->partial_obj_size() - : old_dense_prefix_addr; + // The destination addr for the first live obj after dense-prefix. + HeapWord* bump_ptr = old_dense_prefix_addr + + _summary_data.addr_to_region_ptr(old_dense_prefix_addr)->partial_obj_size(); SpaceId bump_ptr_space = old_space_id; for (uint id = old_space_id; id < last_space_id; ++id) { MutableSpace* sp = PSParallelCompact::space(SpaceId(id)); - HeapWord* dense_prefix_addr = dense_prefix(SpaceId(id)); + // Only verify objs after dense-prefix, because those before dense-prefix are not moved (forwarded). + HeapWord* cur_addr = dense_prefix(SpaceId(id)); HeapWord* top = sp->top(); - HeapWord* cur_addr = dense_prefix_addr; while (cur_addr < top) { cur_addr = mark_bitmap()->find_obj_beg(cur_addr, top); diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.hpp b/src/hotspot/share/gc/parallel/psParallelCompact.hpp index d5ed641f485..4d212499b4c 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.hpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.hpp @@ -360,11 +360,6 @@ public: inline RegionData* region(size_t region_idx) const; inline size_t region(const RegionData* const region_ptr) const; - // Fill in the regions covering [beg, end) so that no data moves; i.e., the - // destination of region n is simply the start of region n. Both arguments - // beg and end must be region-aligned. - void summarize_dense_prefix(HeapWord* beg, HeapWord* end); - HeapWord* summarize_split_space(size_t src_region, SplitInfo& split_info, HeapWord* destination, HeapWord* target_end, HeapWord** target_next); From 4be4826ddb51c155eec3fe2923d891357f8d753b Mon Sep 17 00:00:00 2001 From: William Kemper Date: Thu, 18 Sep 2025 18:50:44 +0000 Subject: [PATCH 108/556] 8367450: Shenandoah: Log the composition of the collection set Reviewed-by: ysr, kdnilsen, phh --- .../shenandoahGenerationalHeuristics.cpp | 74 ++--------- .../shenandoahGenerationalHeuristics.hpp | 3 +- .../heuristics/shenandoahGlobalHeuristics.cpp | 2 - .../heuristics/shenandoahHeuristics.cpp | 22 +--- .../heuristics/shenandoahYoungHeuristics.cpp | 2 - .../gc/shenandoah/shenandoahCollectionSet.cpp | 35 +++++ .../gc/shenandoah/shenandoahCollectionSet.hpp | 21 ++- .../shenandoahCollectionSet.inline.hpp | 8 +- .../gc/shenandoah/shenandoahEvacInfo.hpp | 120 ------------------ .../share/gc/shenandoah/shenandoahTrace.cpp | 36 +++--- .../share/gc/shenandoah/shenandoahTrace.hpp | 10 +- 11 files changed, 89 insertions(+), 244 deletions(-) delete mode 100644 src/hotspot/share/gc/shenandoah/shenandoahEvacInfo.hpp diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.cpp index dfae9040242..c7067b2e5ab 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.cpp @@ -26,7 +26,6 @@ #include "gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.hpp" #include "gc/shenandoah/shenandoahCollectionSet.hpp" #include "gc/shenandoah/shenandoahCollectorPolicy.hpp" -#include "gc/shenandoah/shenandoahEvacInfo.hpp" #include "gc/shenandoah/shenandoahGeneration.hpp" #include "gc/shenandoah/shenandoahGenerationalHeap.inline.hpp" #include "gc/shenandoah/shenandoahHeapRegion.inline.hpp" @@ -185,59 +184,16 @@ void ShenandoahGenerationalHeuristics::choose_collection_set(ShenandoahCollectio heap->shenandoah_policy()->record_mixed_cycle(); } - size_t cset_percent = (total_garbage == 0) ? 0 : (collection_set->garbage() * 100 / total_garbage); - size_t collectable_garbage = collection_set->garbage() + immediate_garbage; - size_t collectable_garbage_percent = (total_garbage == 0) ? 0 : (collectable_garbage * 100 / total_garbage); + collection_set->summarize(total_garbage, immediate_garbage, immediate_regions); - log_info(gc, ergo)("Collectable Garbage: %zu%s (%zu%%), " - "Immediate: %zu%s (%zu%%), %zu regions, " - "CSet: %zu%s (%zu%%), %zu regions", - - byte_size_in_proper_unit(collectable_garbage), - proper_unit_for_byte_size(collectable_garbage), - collectable_garbage_percent, - - byte_size_in_proper_unit(immediate_garbage), - proper_unit_for_byte_size(immediate_garbage), - immediate_percent, - immediate_regions, - - byte_size_in_proper_unit(collection_set->garbage()), - proper_unit_for_byte_size(collection_set->garbage()), - cset_percent, - collection_set->count()); - - if (collection_set->garbage() > 0) { - size_t young_evac_bytes = collection_set->get_young_bytes_reserved_for_evacuation(); - size_t promote_evac_bytes = collection_set->get_young_bytes_to_be_promoted(); - size_t old_evac_bytes = collection_set->get_old_bytes_reserved_for_evacuation(); - size_t total_evac_bytes = young_evac_bytes + promote_evac_bytes + old_evac_bytes; - log_info(gc, ergo)("Evacuation Targets: YOUNG: %zu%s, " - "PROMOTE: %zu%s, " - "OLD: %zu%s, " - "TOTAL: %zu%s", - byte_size_in_proper_unit(young_evac_bytes), proper_unit_for_byte_size(young_evac_bytes), - byte_size_in_proper_unit(promote_evac_bytes), proper_unit_for_byte_size(promote_evac_bytes), - byte_size_in_proper_unit(old_evac_bytes), proper_unit_for_byte_size(old_evac_bytes), - byte_size_in_proper_unit(total_evac_bytes), proper_unit_for_byte_size(total_evac_bytes)); - - ShenandoahEvacuationInformation evacInfo; - evacInfo.set_collection_set_regions(collection_set->count()); - evacInfo.set_collection_set_used_before(collection_set->used()); - evacInfo.set_collection_set_used_after(collection_set->live()); - evacInfo.set_collected_old(old_evac_bytes); - evacInfo.set_collected_promoted(promote_evac_bytes); - evacInfo.set_collected_young(young_evac_bytes); - evacInfo.set_regions_promoted_humongous(humongous_regions_promoted); - evacInfo.set_regions_promoted_regular(regular_regions_promoted_in_place); - evacInfo.set_regular_promoted_garbage(regular_regions_promoted_garbage); - evacInfo.set_regular_promoted_free(regular_regions_promoted_free); - evacInfo.set_regions_immediate(immediate_regions); - evacInfo.set_immediate_size(immediate_garbage); - evacInfo.set_free_regions(free_regions); - - ShenandoahTracer().report_evacuation_info(&evacInfo); - } + ShenandoahTracer::report_evacuation_info(collection_set, + free_regions, + humongous_regions_promoted, + regular_regions_promoted_in_place, + regular_regions_promoted_garbage, + regular_regions_promoted_free, + immediate_regions, + immediate_garbage); } @@ -268,15 +224,3 @@ size_t ShenandoahGenerationalHeuristics::add_preselected_regions_to_collection_s return cur_young_garbage; } -void ShenandoahGenerationalHeuristics::log_cset_composition(ShenandoahCollectionSet* cset) const { - size_t collected_old = cset->get_old_bytes_reserved_for_evacuation(); - size_t collected_promoted = cset->get_young_bytes_to_be_promoted(); - size_t collected_young = cset->get_young_bytes_reserved_for_evacuation(); - - log_info(gc, ergo)( - "Chosen CSet evacuates young: %zu%s (of which at least: %zu%s are to be promoted), " - "old: %zu%s", - byte_size_in_proper_unit(collected_young), proper_unit_for_byte_size(collected_young), - byte_size_in_proper_unit(collected_promoted), proper_unit_for_byte_size(collected_promoted), - byte_size_in_proper_unit(collected_old), proper_unit_for_byte_size(collected_old)); -} diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.hpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.hpp index 6708c63f042..31c016bb4b7 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.hpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGenerationalHeuristics.hpp @@ -51,9 +51,8 @@ protected: size_t add_preselected_regions_to_collection_set(ShenandoahCollectionSet* cset, const RegionData* data, size_t size) const; - - void log_cset_composition(ShenandoahCollectionSet* cset) const; }; #endif //SHARE_GC_SHENANDOAH_HEURISTICS_SHENANDOAHGENERATIONALHEURISTICS_HPP + diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGlobalHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGlobalHeuristics.cpp index 331bd040575..93f9b18ad9f 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGlobalHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahGlobalHeuristics.cpp @@ -42,8 +42,6 @@ void ShenandoahGlobalHeuristics::choose_collection_set_from_regiondata(Shenandoa QuickSort::sort(data, (int) size, compare_by_garbage); choose_global_collection_set(cset, data, size, actual_free, 0 /* cur_young_garbage */); - - log_cset_composition(cset); } diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp index b151a75e6e7..c8a0c3dc518 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp @@ -153,27 +153,7 @@ void ShenandoahHeuristics::choose_collection_set(ShenandoahCollectionSet* collec choose_collection_set_from_regiondata(collection_set, candidates, cand_idx, immediate_garbage + free); } - size_t cset_percent = (total_garbage == 0) ? 0 : (collection_set->garbage() * 100 / total_garbage); - size_t collectable_garbage = collection_set->garbage() + immediate_garbage; - size_t collectable_garbage_percent = (total_garbage == 0) ? 0 : (collectable_garbage * 100 / total_garbage); - - log_info(gc, ergo)("Collectable Garbage: %zu%s (%zu%%), " - "Immediate: %zu%s (%zu%%), %zu regions, " - "CSet: %zu%s (%zu%%), %zu regions", - - byte_size_in_proper_unit(collectable_garbage), - proper_unit_for_byte_size(collectable_garbage), - collectable_garbage_percent, - - byte_size_in_proper_unit(immediate_garbage), - proper_unit_for_byte_size(immediate_garbage), - immediate_percent, - immediate_regions, - - byte_size_in_proper_unit(collection_set->garbage()), - proper_unit_for_byte_size(collection_set->garbage()), - cset_percent, - collection_set->count()); + collection_set->summarize(total_garbage, immediate_garbage, immediate_regions); } void ShenandoahHeuristics::record_cycle_start() { diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahYoungHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahYoungHeuristics.cpp index d236be8c9e6..15d1058d7cd 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahYoungHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahYoungHeuristics.cpp @@ -55,8 +55,6 @@ void ShenandoahYoungHeuristics::choose_collection_set_from_regiondata(Shenandoah size_t cur_young_garbage = add_preselected_regions_to_collection_set(cset, data, size); choose_young_collection_set(cset, data, size, actual_free, cur_young_garbage); - - log_cset_composition(cset); } void ShenandoahYoungHeuristics::choose_young_collection_set(ShenandoahCollectionSet* cset, diff --git a/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.cpp b/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.cpp index a58b9311183..745d45ace1e 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.cpp @@ -200,3 +200,38 @@ void ShenandoahCollectionSet::print_on(outputStream* out) const { } assert(regions == count(), "Must match"); } + +void ShenandoahCollectionSet::summarize(size_t total_garbage, size_t immediate_garbage, size_t immediate_regions) const { + const LogTarget(Info, gc, ergo) lt; + LogStream ls(lt); + if (lt.is_enabled()) { + const size_t cset_percent = (total_garbage == 0) ? 0 : (garbage() * 100 / total_garbage); + const size_t collectable_garbage = garbage() + immediate_garbage; + const size_t collectable_garbage_percent = (total_garbage == 0) ? 0 : (collectable_garbage * 100 / total_garbage); + const size_t immediate_percent = (total_garbage == 0) ? 0 : (immediate_garbage * 100 / total_garbage); + + ls.print_cr("Collectable Garbage: " PROPERFMT " (%zu%%), " + "Immediate: " PROPERFMT " (%zu%%), %zu regions, " + "CSet: " PROPERFMT " (%zu%%), %zu regions", + PROPERFMTARGS(collectable_garbage), + collectable_garbage_percent, + + PROPERFMTARGS(immediate_garbage), + immediate_percent, + immediate_regions, + + PROPERFMTARGS(garbage()), + cset_percent, + count()); + + if (garbage() > 0) { + const size_t young_evac_bytes = get_young_bytes_reserved_for_evacuation(); + const size_t promote_evac_bytes = get_young_bytes_to_be_promoted(); + const size_t old_evac_bytes = get_old_bytes_reserved_for_evacuation(); + const size_t total_evac_bytes = young_evac_bytes + promote_evac_bytes + old_evac_bytes; + ls.print_cr("Evacuation Targets: " + "YOUNG: " PROPERFMT ", " "PROMOTE: " PROPERFMT ", " "OLD: " PROPERFMT ", " "TOTAL: " PROPERFMT, + PROPERFMTARGS(young_evac_bytes), PROPERFMTARGS(promote_evac_bytes), PROPERFMTARGS(old_evac_bytes), PROPERFMTARGS(total_evac_bytes)); + } + } +} diff --git a/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.hpp b/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.hpp index 4f9f6fc2052..d4a590a3d89 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.hpp @@ -103,17 +103,26 @@ public: inline bool is_in(oop obj) const; inline bool is_in_loc(void* loc) const; + // Prints a detailed accounting of all regions in the collection set when gc+cset=debug void print_on(outputStream* out) const; - // It is not known how many of these bytes will be promoted. - inline size_t get_young_bytes_reserved_for_evacuation(); - inline size_t get_old_bytes_reserved_for_evacuation(); + // Prints a summary of the collection set when gc+ergo=info + void summarize(size_t total_garbage, size_t immediate_garbage, size_t immediate_regions) const; - inline size_t get_young_bytes_to_be_promoted(); + // Returns the amount of live bytes in young regions in the collection set. It is not known how many of these bytes will be promoted. + inline size_t get_young_bytes_reserved_for_evacuation() const; - size_t get_young_available_bytes_collected() { return _young_available_bytes_collected; } + // Returns the amount of live bytes in old regions in the collection set. + inline size_t get_old_bytes_reserved_for_evacuation() const; - inline size_t get_old_garbage(); + // Returns the amount of live bytes in young regions with an age above the tenuring threshold. + inline size_t get_young_bytes_to_be_promoted() const; + + // Returns the amount of free bytes in young regions in the collection set. + size_t get_young_available_bytes_collected() const { return _young_available_bytes_collected; } + + // Returns the amount of garbage in old regions in the collection set. + inline size_t get_old_garbage() const; bool is_preselected(size_t region_idx) { assert(_preselected_regions != nullptr, "Missing etsablish after abandon"); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.inline.hpp b/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.inline.hpp index 791e9c73b28..4adcec4fbb5 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.inline.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahCollectionSet.inline.hpp @@ -54,19 +54,19 @@ bool ShenandoahCollectionSet::is_in_loc(void* p) const { return _biased_cset_map[index] == 1; } -size_t ShenandoahCollectionSet::get_old_bytes_reserved_for_evacuation() { +size_t ShenandoahCollectionSet::get_old_bytes_reserved_for_evacuation() const { return _old_bytes_to_evacuate; } -size_t ShenandoahCollectionSet::get_young_bytes_reserved_for_evacuation() { +size_t ShenandoahCollectionSet::get_young_bytes_reserved_for_evacuation() const { return _young_bytes_to_evacuate - _young_bytes_to_promote; } -size_t ShenandoahCollectionSet::get_young_bytes_to_be_promoted() { +size_t ShenandoahCollectionSet::get_young_bytes_to_be_promoted() const { return _young_bytes_to_promote; } -size_t ShenandoahCollectionSet::get_old_garbage() { +size_t ShenandoahCollectionSet::get_old_garbage() const { return _old_garbage; } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahEvacInfo.hpp b/src/hotspot/share/gc/shenandoah/shenandoahEvacInfo.hpp deleted file mode 100644 index 8069fd13afa..00000000000 --- a/src/hotspot/share/gc/shenandoah/shenandoahEvacInfo.hpp +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef SHARE_GC_SHENANDOAH_SHENANDOAHEVACINFO_HPP -#define SHARE_GC_SHENANDOAH_SHENANDOAHEVACINFO_HPP - -#include "memory/allocation.hpp" - -class ShenandoahEvacuationInformation : public StackObj { - // Values for ShenandoahEvacuationInformation jfr event, sizes stored as bytes - size_t _collection_set_regions; - size_t _collection_set_used_before; - size_t _collection_set_used_after; - size_t _collected_old; - size_t _collected_promoted; - size_t _collected_young; - size_t _free_regions; - size_t _regions_promoted_humongous; - size_t _regions_promoted_regular; - size_t _regular_promoted_garbage; - size_t _regular_promoted_free; - size_t _regions_immediate; - size_t _immediate_size; - -public: - ShenandoahEvacuationInformation() : - _collection_set_regions(0), _collection_set_used_before(0), _collection_set_used_after(0), - _collected_old(0), _collected_promoted(0), _collected_young(0), _free_regions(0), - _regions_promoted_humongous(0), _regions_promoted_regular(0), _regular_promoted_garbage(0), - _regular_promoted_free(0), _regions_immediate(0), _immediate_size(0) { } - - void set_collection_set_regions(size_t collection_set_regions) { - _collection_set_regions = collection_set_regions; - } - - void set_collection_set_used_before(size_t used) { - _collection_set_used_before = used; - } - - void set_collection_set_used_after(size_t used) { - _collection_set_used_after = used; - } - - void set_collected_old(size_t collected) { - _collected_old = collected; - } - - void set_collected_promoted(size_t collected) { - _collected_promoted = collected; - } - - void set_collected_young(size_t collected) { - _collected_young = collected; - } - - void set_free_regions(size_t freed) { - _free_regions = freed; - } - - void set_regions_promoted_humongous(size_t humongous) { - _regions_promoted_humongous = humongous; - } - - void set_regions_promoted_regular(size_t regular) { - _regions_promoted_regular = regular; - } - - void set_regular_promoted_garbage(size_t garbage) { - _regular_promoted_garbage = garbage; - } - - void set_regular_promoted_free(size_t free) { - _regular_promoted_free = free; - } - - void set_regions_immediate(size_t immediate) { - _regions_immediate = immediate; - } - - void set_immediate_size(size_t size) { - _immediate_size = size; - } - - size_t collection_set_regions() { return _collection_set_regions; } - size_t collection_set_used_before() { return _collection_set_used_before; } - size_t collection_set_used_after() { return _collection_set_used_after; } - size_t collected_old() { return _collected_old; } - size_t collected_promoted() { return _collected_promoted; } - size_t collected_young() { return _collected_young; } - size_t regions_promoted_humongous() { return _regions_promoted_humongous; } - size_t regions_promoted_regular() { return _regions_promoted_regular; } - size_t regular_promoted_garbage() { return _regular_promoted_garbage; } - size_t regular_promoted_free() { return _regular_promoted_free; } - size_t free_regions() { return _free_regions; } - size_t regions_immediate() { return _regions_immediate; } - size_t immediate_size() { return _immediate_size; } -}; - -#endif // SHARE_GC_SHENANDOAH_SHENANDOAHEVACINFO_HPP diff --git a/src/hotspot/share/gc/shenandoah/shenandoahTrace.cpp b/src/hotspot/share/gc/shenandoah/shenandoahTrace.cpp index dd153718c9f..a786f8ae216 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahTrace.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahTrace.cpp @@ -22,31 +22,31 @@ * */ -#include "gc/shenandoah/shenandoahEvacInfo.hpp" +#include "gc/shenandoah/shenandoahCollectionSet.inline.hpp" #include "gc/shenandoah/shenandoahTrace.hpp" #include "jfr/jfrEvents.hpp" -void ShenandoahTracer::report_evacuation_info(ShenandoahEvacuationInformation* info) { - send_evacuation_info_event(info); -} +void ShenandoahTracer::report_evacuation_info(const ShenandoahCollectionSet* cset, + size_t free_regions, size_t regions_promoted_humongous, size_t regions_promoted_regular, + size_t regular_promoted_garbage, size_t regular_promoted_free, size_t regions_immediate, + size_t immediate_size) { -void ShenandoahTracer::send_evacuation_info_event(ShenandoahEvacuationInformation* info) { EventShenandoahEvacuationInformation e; if (e.should_commit()) { e.set_gcId(GCId::current()); - e.set_cSetRegions(info->collection_set_regions()); - e.set_cSetUsedBefore(info->collection_set_used_before()); - e.set_cSetUsedAfter(info->collection_set_used_after()); - e.set_collectedOld(info->collected_old()); - e.set_collectedPromoted(info->collected_promoted()); - e.set_collectedYoung(info->collected_young()); - e.set_regionsPromotedHumongous(info->regions_promoted_humongous()); - e.set_regionsPromotedRegular(info->regions_promoted_regular()); - e.set_regularPromotedGarbage(info->regular_promoted_garbage()); - e.set_regularPromotedFree(info->regular_promoted_free()); - e.set_freeRegions(info->free_regions()); - e.set_regionsImmediate(info->regions_immediate()); - e.set_immediateBytes(info->immediate_size()); + e.set_cSetRegions(cset->count()); + e.set_cSetUsedBefore(cset->used()); + e.set_cSetUsedAfter(cset->live()); + e.set_collectedOld(cset->get_old_bytes_reserved_for_evacuation()); + e.set_collectedPromoted(cset->get_young_bytes_to_be_promoted()); + e.set_collectedYoung(cset->get_young_bytes_reserved_for_evacuation()); + e.set_regionsPromotedHumongous(regions_promoted_humongous); + e.set_regionsPromotedRegular(regions_promoted_regular); + e.set_regularPromotedGarbage(regular_promoted_garbage); + e.set_regularPromotedFree(regular_promoted_free); + e.set_freeRegions(free_regions); + e.set_regionsImmediate(regions_immediate); + e.set_immediateBytes(immediate_size); e.commit(); } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahTrace.hpp b/src/hotspot/share/gc/shenandoah/shenandoahTrace.hpp index a5351f4ef28..116968103de 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahTrace.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahTrace.hpp @@ -28,15 +28,17 @@ #include "gc/shared/gcTrace.hpp" #include "memory/allocation.hpp" -class ShenandoahEvacuationInformation; +class ShenandoahCollectionSet; class ShenandoahTracer : public GCTracer, public CHeapObj { public: ShenandoahTracer() : GCTracer(Shenandoah) {} - void report_evacuation_info(ShenandoahEvacuationInformation* info); -private: - void send_evacuation_info_event(ShenandoahEvacuationInformation* info); + // Sends a JFR event (if enabled) summarizing the composition of the collection set + static void report_evacuation_info(const ShenandoahCollectionSet* cset, + size_t free_regions, size_t regions_promoted_humongous, size_t regions_promoted_regular, + size_t regular_promoted_garbage, size_t regular_promoted_free, size_t regions_immediate, + size_t immediate_size); }; #endif From e4cb86df2b05cef6dd7e29e8803ebbbf5b4fe5a2 Mon Sep 17 00:00:00 2001 From: William Kemper Date: Thu, 18 Sep 2025 18:53:08 +0000 Subject: [PATCH 109/556] 8367473: Shenandoah: Make the detailed evacuation metrics a runtime diagnostic option 8367722: [GenShen] ShenandoahEvacuationStats is always empty Reviewed-by: ysr, phh --- .../gc/shenandoah/shenandoahControlThread.cpp | 11 +++--- .../gc/shenandoah/shenandoahEvacTracker.cpp | 39 +++++++++++-------- .../shenandoah/shenandoahGenerationalHeap.cpp | 11 ++++-- .../share/gc/shenandoah/shenandoahHeap.cpp | 20 ++++++---- .../gc/shenandoah/shenandoah_globals.hpp | 7 ++++ 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp index 80e5b709ada..f4005e45f39 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp @@ -211,12 +211,11 @@ void ShenandoahControlThread::run_service() { ResourceMark rm; LogStream ls(lt); heap->phase_timings()->print_cycle_on(&ls); -#ifdef NOT_PRODUCT - ShenandoahEvacuationTracker* evac_tracker = heap->evac_tracker(); - ShenandoahCycleStats evac_stats = evac_tracker->flush_cycle_to_global(); - evac_tracker->print_evacuations_on(&ls, &evac_stats.workers, - &evac_stats.mutators); -#endif + if (ShenandoahEvacTracking) { + ShenandoahEvacuationTracker* evac_tracker = heap->evac_tracker(); + ShenandoahCycleStats evac_stats = evac_tracker->flush_cycle_to_global(); + evac_tracker->print_evacuations_on(&ls, &evac_stats.workers, &evac_stats.mutators); + } } } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp b/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp index 7883e2c5b29..bc8fad713af 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp @@ -108,8 +108,10 @@ void ShenandoahEvacuationStats::ShenandoahEvacuations::print_on(outputStream* st void ShenandoahEvacuationStats::print_on(outputStream* st) const { st->print("Young: "); _young.print_on(st); - st->print("Promotion: "); _promotion.print_on(st); - st->print("Old: "); _old.print_on(st); + if (ShenandoahHeap::heap()->mode()->is_generational()) { + st->print("Promotion: "); _promotion.print_on(st); + st->print("Old: "); _old.print_on(st); + } if (_use_age_table) { _age_table->print_on(st); @@ -123,25 +125,28 @@ void ShenandoahEvacuationTracker::print_global_on(outputStream* st) { void ShenandoahEvacuationTracker::print_evacuations_on(outputStream* st, ShenandoahEvacuationStats* workers, ShenandoahEvacuationStats* mutators) { - st->print_cr("Workers: "); - workers->print_on(st); - st->cr(); - st->print_cr("Mutators: "); - mutators->print_on(st); - st->cr(); + if (ShenandoahEvacTracking) { + st->print_cr("Workers: "); + workers->print_on(st); + st->cr(); + st->print_cr("Mutators: "); + mutators->print_on(st); + st->cr(); + } ShenandoahHeap* heap = ShenandoahHeap::heap(); - - AgeTable young_region_ages(false); - for (uint i = 0; i < heap->num_regions(); ++i) { - ShenandoahHeapRegion* r = heap->get_region(i); - if (r->is_young()) { - young_region_ages.add(r->age(), r->get_live_data_words()); + if (heap->mode()->is_generational()) { + AgeTable young_region_ages(false); + for (uint i = 0; i < heap->num_regions(); ++i) { + ShenandoahHeapRegion* r = heap->get_region(i); + if (r->is_young()) { + young_region_ages.add(r->age(), r->get_live_data_words()); + } } + st->print("Young regions: "); + young_region_ages.print_on(st); + st->cr(); } - st->print("Young regions: "); - young_region_ages.print_on(st); - st->cr(); } class ShenandoahStatAggregator : public ThreadClosure { diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp index e24109838a1..8a21ae376e1 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp @@ -324,8 +324,11 @@ oop ShenandoahGenerationalHeap::try_evacuate_object(oop p, Thread* thread, Shena return ShenandoahBarrierSet::resolve_forwarded(p); } + if (ShenandoahEvacTracking) { + evac_tracker()->begin_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen); + } + // Copy the object: - NOT_PRODUCT(evac_tracker()->begin_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen)); Copy::aligned_disjoint_words(cast_from_oop(p), copy, size); oop copy_val = cast_to_oop(copy); @@ -346,8 +349,10 @@ oop ShenandoahGenerationalHeap::try_evacuate_object(oop p, Thread* thread, Shena // safe to do this on the public copy (this is also done during concurrent mark). ContinuationGCSupport::relativize_stack_chunk(copy_val); - // Record that the evacuation succeeded - NOT_PRODUCT(evac_tracker()->end_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen)); + if (ShenandoahEvacTracking) { + // Record that the evacuation succeeded + evac_tracker()->end_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen); + } if (target_gen == OLD_GENERATION) { old_generation()->handle_evacuation(copy, size, from_region->is_young()); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp index c2dca09a344..714c9cb9f5b 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp @@ -1371,8 +1371,11 @@ oop ShenandoahHeap::try_evacuate_object(oop p, Thread* thread, ShenandoahHeapReg return ShenandoahBarrierSet::resolve_forwarded(p); } + if (ShenandoahEvacTracking) { + evac_tracker()->begin_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen); + } + // Copy the object: - NOT_PRODUCT(evac_tracker()->begin_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen)); Copy::aligned_disjoint_words(cast_from_oop(p), copy, size); // Try to install the new forwarding pointer. @@ -1382,7 +1385,9 @@ oop ShenandoahHeap::try_evacuate_object(oop p, Thread* thread, ShenandoahHeapReg // Successfully evacuated. Our copy is now the public one! ContinuationGCSupport::relativize_stack_chunk(copy_val); shenandoah_assert_correct(nullptr, copy_val); - NOT_PRODUCT(evac_tracker()->end_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen)); + if (ShenandoahEvacTracking) { + evac_tracker()->end_evacuation(thread, size * HeapWordSize, from_region->affiliation(), target_gen); + } return copy_val; } else { // Failed to evacuate. We need to deal with the object that is left behind. Since this @@ -1612,12 +1617,11 @@ void ShenandoahHeap::print_tracing_info() const { ResourceMark rm; LogStream ls(lt); -#ifdef NOT_PRODUCT - evac_tracker()->print_global_on(&ls); - - ls.cr(); - ls.cr(); -#endif + if (ShenandoahEvacTracking) { + evac_tracker()->print_global_on(&ls); + ls.cr(); + ls.cr(); + } phase_timings()->print_global_on(&ls); diff --git a/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp b/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp index c6a842c7754..d1531c51236 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoah_globals.hpp @@ -408,6 +408,13 @@ "events.") \ range(0,100) \ \ + product(bool, ShenandoahEvacTracking, false, DIAGNOSTIC, \ + "Collect additional metrics about evacuations. Enabling this " \ + "tracks how many objects and how many bytes were evacuated, and " \ + "how many were abandoned. The information will be categorized " \ + "by thread type (worker or mutator) and evacuation type (young, " \ + "old, or promotion.") \ + \ product(uintx, ShenandoahMinYoungPercentage, 20, EXPERIMENTAL, \ "The minimum percentage of the heap to use for the young " \ "generation. Heuristics will not adjust the young generation " \ From 6e4e966d9b71ec04618e19784b5a661f34595ef6 Mon Sep 17 00:00:00 2001 From: William Kemper Date: Thu, 18 Sep 2025 21:18:37 +0000 Subject: [PATCH 110/556] 8365792: GenShen: assertion "Generations aren't reconciled" Reviewed-by: xpeng, ysr --- src/hotspot/share/gc/shenandoah/shenandoahAsserts.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahAsserts.cpp b/src/hotspot/share/gc/shenandoah/shenandoahAsserts.cpp index 93c218e9e8b..3d9fa10b0fc 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahAsserts.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahAsserts.cpp @@ -543,7 +543,8 @@ void ShenandoahAsserts::assert_control_or_vm_thread_at_safepoint(bool at_safepoi } void ShenandoahAsserts::assert_generations_reconciled(const char* file, int line) { - if (!SafepointSynchronize::is_at_safepoint()) { + if (!ShenandoahSafepoint::is_at_shenandoah_safepoint()) { + // Only shenandoah safepoint operations participate in the active/gc generation scheme return; } @@ -554,7 +555,7 @@ void ShenandoahAsserts::assert_generations_reconciled(const char* file, int line return; } - ShenandoahMessageBuffer msg("Active(%d) & GC(%d) Generations aren't reconciled", agen->type(), ggen->type()); + ShenandoahMessageBuffer msg("Active(%s) & GC(%s) Generations aren't reconciled", agen->name(), ggen->name()); report_vm_error(file, line, msg.buffer()); } From e3a4c28409ac62feee9efe069e3a3482e7e2cdd2 Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Fri, 19 Sep 2025 01:04:28 +0000 Subject: [PATCH 111/556] 8362657: Make tables used in AOT assembly phase GC-safe Reviewed-by: shade, dholmes --- src/hotspot/share/cds/aotMetaspace.cpp | 1 + src/hotspot/share/cds/archiveHeapWriter.cpp | 22 +++++---- src/hotspot/share/cds/archiveHeapWriter.hpp | 3 +- src/hotspot/share/cds/cdsHeapVerifier.cpp | 8 ++-- src/hotspot/share/cds/cdsHeapVerifier.hpp | 5 +- src/hotspot/share/cds/heapShared.cpp | 51 +++++++++++++++++++-- src/hotspot/share/cds/heapShared.hpp | 20 ++++++-- 7 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/hotspot/share/cds/aotMetaspace.cpp b/src/hotspot/share/cds/aotMetaspace.cpp index 51227768293..f866461e7d4 100644 --- a/src/hotspot/share/cds/aotMetaspace.cpp +++ b/src/hotspot/share/cds/aotMetaspace.cpp @@ -720,6 +720,7 @@ void VM_PopulateDumpSharedSpace::doit() { _map_info->set_cloned_vtables(CppVtables::vtables_serialized_base()); _map_info->header()->set_class_location_config(cl_config); + HeapShared::delete_tables_with_raw_oops(); CDSConfig::set_is_at_aot_safepoint(false); } diff --git a/src/hotspot/share/cds/archiveHeapWriter.cpp b/src/hotspot/share/cds/archiveHeapWriter.cpp index c7750c70f1b..d1a8772874a 100644 --- a/src/hotspot/share/cds/archiveHeapWriter.cpp +++ b/src/hotspot/share/cds/archiveHeapWriter.cpp @@ -95,6 +95,11 @@ void ArchiveHeapWriter::init() { } } +void ArchiveHeapWriter::delete_tables_with_raw_oops() { + delete _source_objs; + _source_objs = nullptr; +} + void ArchiveHeapWriter::add_source_obj(oop src_obj) { _source_objs->append(src_obj); } @@ -145,7 +150,7 @@ oop ArchiveHeapWriter::requested_obj_from_buffer_offset(size_t offset) { oop ArchiveHeapWriter::source_obj_to_requested_obj(oop src_obj) { assert(CDSConfig::is_dumping_heap(), "dump-time only"); - HeapShared::CachedOopInfo* p = HeapShared::archived_object_cache()->get(src_obj); + HeapShared::CachedOopInfo* p = HeapShared::get_cached_oop_info(src_obj); if (p != nullptr) { return requested_obj_from_buffer_offset(p->buffer_offset()); } else { @@ -154,9 +159,9 @@ oop ArchiveHeapWriter::source_obj_to_requested_obj(oop src_obj) { } oop ArchiveHeapWriter::buffered_addr_to_source_obj(address buffered_addr) { - oop* p = _buffer_offset_to_source_obj_table->get(buffered_address_to_offset(buffered_addr)); - if (p != nullptr) { - return *p; + OopHandle* oh = _buffer_offset_to_source_obj_table->get(buffered_address_to_offset(buffered_addr)); + if (oh != nullptr) { + return oh->resolve(); } else { return nullptr; } @@ -356,12 +361,13 @@ void ArchiveHeapWriter::copy_source_objs_to_buffer(GrowableArrayCHeaplength(); i++) { int src_obj_index = _source_objs_order->at(i)._index; oop src_obj = _source_objs->at(src_obj_index); - HeapShared::CachedOopInfo* info = HeapShared::archived_object_cache()->get(src_obj); + HeapShared::CachedOopInfo* info = HeapShared::get_cached_oop_info(src_obj); assert(info != nullptr, "must be"); size_t buffer_offset = copy_one_source_obj_to_buffer(src_obj); info->set_buffer_offset(buffer_offset); - _buffer_offset_to_source_obj_table->put_when_absent(buffer_offset, src_obj); + OopHandle handle(Universe::vm_global(), src_obj); + _buffer_offset_to_source_obj_table->put_when_absent(buffer_offset, handle); _buffer_offset_to_source_obj_table->maybe_grow(); if (java_lang_Module::is_instance(src_obj)) { @@ -696,7 +702,7 @@ void ArchiveHeapWriter::relocate_embedded_oops(GrowableArrayCHeaplength(); i++) { int src_obj_index = _source_objs_order->at(i)._index; oop src_obj = _source_objs->at(src_obj_index); - HeapShared::CachedOopInfo* info = HeapShared::archived_object_cache()->get(src_obj); + HeapShared::CachedOopInfo* info = HeapShared::get_cached_oop_info(src_obj); assert(info != nullptr, "must be"); oop requested_obj = requested_obj_from_buffer_offset(info->buffer_offset()); update_header_for_requested_obj(requested_obj, src_obj, src_obj->klass()); @@ -758,7 +764,7 @@ void ArchiveHeapWriter::compute_ptrmap(ArchiveHeapInfo* heap_info) { NativePointerInfo info = _native_pointers->at(i); oop src_obj = info._src_obj; int field_offset = info._field_offset; - HeapShared::CachedOopInfo* p = HeapShared::archived_object_cache()->get(src_obj); + HeapShared::CachedOopInfo* p = HeapShared::get_cached_oop_info(src_obj); // requested_field_addr = the address of this field in the requested space oop requested_obj = requested_obj_from_buffer_offset(p->buffer_offset()); Metadata** requested_field_addr = (Metadata**)(cast_from_oop

        (requested_obj) + field_offset); diff --git a/src/hotspot/share/cds/archiveHeapWriter.hpp b/src/hotspot/share/cds/archiveHeapWriter.hpp index 18e647912f1..80e72c12e7e 100644 --- a/src/hotspot/share/cds/archiveHeapWriter.hpp +++ b/src/hotspot/share/cds/archiveHeapWriter.hpp @@ -152,7 +152,7 @@ private: }; static GrowableArrayCHeap* _source_objs_order; - typedef ResizeableHashTable BufferOffsetToSourceObjectTable; static BufferOffsetToSourceObjectTable* _buffer_offset_to_source_obj_table; @@ -227,6 +227,7 @@ private: public: static void init() NOT_CDS_JAVA_HEAP_RETURN; + static void delete_tables_with_raw_oops(); static void add_source_obj(oop src_obj); static bool is_too_large_to_archive(size_t size); static bool is_too_large_to_archive(oop obj); diff --git a/src/hotspot/share/cds/cdsHeapVerifier.cpp b/src/hotspot/share/cds/cdsHeapVerifier.cpp index a9f46c21ad3..9429a0d8264 100644 --- a/src/hotspot/share/cds/cdsHeapVerifier.cpp +++ b/src/hotspot/share/cds/cdsHeapVerifier.cpp @@ -36,6 +36,7 @@ #include "oops/fieldStreams.inline.hpp" #include "oops/klass.inline.hpp" #include "oops/oop.inline.hpp" +#include "oops/oopHandle.inline.hpp" #include "runtime/fieldDescriptor.inline.hpp" #if INCLUDE_CDS_JAVA_HEAP @@ -273,7 +274,8 @@ void CDSHeapVerifier::add_static_obj_field(InstanceKlass* ik, oop field, Symbol* // This function is called once for every archived heap object. Warn if this object is referenced by // a static field of a class that's not aot-initialized. -inline bool CDSHeapVerifier::do_entry(oop& orig_obj, HeapShared::CachedOopInfo& value) { +inline bool CDSHeapVerifier::do_entry(OopHandle& orig_obj_handle, HeapShared::CachedOopInfo& value) { + oop orig_obj = orig_obj_handle.resolve(); _archived_objs++; if (java_lang_String::is_instance(orig_obj) && HeapShared::is_dumped_interned_string(orig_obj)) { @@ -323,7 +325,7 @@ public: // Call this function (from gdb, etc) if you want to know why an object is archived. void CDSHeapVerifier::trace_to_root(outputStream* st, oop orig_obj) { - HeapShared::CachedOopInfo* info = HeapShared::archived_object_cache()->get(orig_obj); + HeapShared::CachedOopInfo* info = HeapShared::get_cached_oop_info(orig_obj); if (info != nullptr) { trace_to_root(st, orig_obj, nullptr, info); } else { @@ -357,7 +359,7 @@ const char* static_field_name(oop mirror, oop field) { int CDSHeapVerifier::trace_to_root(outputStream* st, oop orig_obj, oop orig_field, HeapShared::CachedOopInfo* info) { int level = 0; if (info->orig_referrer() != nullptr) { - HeapShared::CachedOopInfo* ref = HeapShared::archived_object_cache()->get(info->orig_referrer()); + HeapShared::CachedOopInfo* ref = HeapShared::get_cached_oop_info(info->orig_referrer()); assert(ref != nullptr, "sanity"); level = trace_to_root(st, info->orig_referrer(), orig_obj, ref) + 1; } else if (java_lang_String::is_instance(orig_obj)) { diff --git a/src/hotspot/share/cds/cdsHeapVerifier.hpp b/src/hotspot/share/cds/cdsHeapVerifier.hpp index 811751e8ca2..1cc03975c5c 100644 --- a/src/hotspot/share/cds/cdsHeapVerifier.hpp +++ b/src/hotspot/share/cds/cdsHeapVerifier.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,6 +27,7 @@ #include "cds/heapShared.hpp" #include "memory/iterator.hpp" +#include "oops/oopHandle.hpp" #include "utilities/growableArray.hpp" #include "utilities/hashTable.hpp" @@ -80,7 +81,7 @@ public: virtual void do_klass(Klass* k); // For HashTable::iterate() - inline bool do_entry(oop& orig_obj, HeapShared::CachedOopInfo& value); + inline bool do_entry(OopHandle& orig_obj, HeapShared::CachedOopInfo& value); static void verify(); diff --git a/src/hotspot/share/cds/heapShared.cpp b/src/hotspot/share/cds/heapShared.cpp index 6b7cffdf321..92f55ce5b33 100644 --- a/src/hotspot/share/cds/heapShared.cpp +++ b/src/hotspot/share/cds/heapShared.cpp @@ -58,6 +58,7 @@ #include "oops/fieldStreams.inline.hpp" #include "oops/objArrayOop.inline.hpp" #include "oops/oop.inline.hpp" +#include "oops/oopHandle.inline.hpp" #include "oops/typeArrayOop.inline.hpp" #include "prims/jvmtiExport.hpp" #include "runtime/arguments.hpp" @@ -159,12 +160,35 @@ bool HeapShared::is_subgraph_root_class(InstanceKlass* ik) { is_subgraph_root_class_of(fmg_archive_subgraph_entry_fields, ik); } +oop HeapShared::CachedOopInfo::orig_referrer() const { + return _orig_referrer.resolve(); +} + unsigned HeapShared::oop_hash(oop const& p) { + assert(SafepointSynchronize::is_at_safepoint() || + JavaThread::current()->is_in_no_safepoint_scope(), "sanity"); // Do not call p->identity_hash() as that will update the // object header. return primitive_hash(cast_from_oop(p)); } +unsigned int HeapShared::oop_handle_hash_raw(const OopHandle& oh) { + return oop_hash(oh.resolve()); +} + +unsigned int HeapShared::oop_handle_hash(const OopHandle& oh) { + oop o = oh.resolve(); + if (o == nullptr) { + return 0; + } else { + return o->identity_hash(); + } +} + +bool HeapShared::oop_handle_equals(const OopHandle& a, const OopHandle& b) { + return a.resolve() == b.resolve(); +} + static void reset_states(oop obj, TRAPS) { Handle h_obj(THREAD, obj); InstanceKlass* klass = InstanceKlass::cast(obj->klass()); @@ -216,7 +240,8 @@ HeapShared::ArchivedObjectCache* HeapShared::_archived_object_cache = nullptr; bool HeapShared::has_been_archived(oop obj) { assert(CDSConfig::is_dumping_heap(), "dump-time only"); - return archived_object_cache()->get(obj) != nullptr; + OopHandle oh(&obj); + return archived_object_cache()->get(oh) != nullptr; } int HeapShared::append_root(oop obj) { @@ -303,7 +328,9 @@ bool HeapShared::archive_object(oop obj, oop referrer, KlassSubGraphInfo* subgra count_allocation(obj->size()); ArchiveHeapWriter::add_source_obj(obj); CachedOopInfo info = make_cached_oop_info(obj, referrer); - archived_object_cache()->put_when_absent(obj, info); + + OopHandle oh(Universe::vm_global(), obj); + archived_object_cache()->put_when_absent(oh, info); archived_object_cache()->maybe_grow(); mark_native_pointers(obj); @@ -636,14 +663,16 @@ void HeapShared::mark_native_pointers(oop orig_obj) { } void HeapShared::get_pointer_info(oop src_obj, bool& has_oop_pointers, bool& has_native_pointers) { - CachedOopInfo* info = archived_object_cache()->get(src_obj); + OopHandle oh(&src_obj); + CachedOopInfo* info = archived_object_cache()->get(oh); assert(info != nullptr, "must be"); has_oop_pointers = info->has_oop_pointers(); has_native_pointers = info->has_native_pointers(); } void HeapShared::set_has_native_pointers(oop src_obj) { - CachedOopInfo* info = archived_object_cache()->get(src_obj); + OopHandle oh(&src_obj); + CachedOopInfo* info = archived_object_cache()->get(oh); assert(info != nullptr, "must be"); info->set_has_native_pointers(); } @@ -1453,7 +1482,7 @@ public: HeapShared::CachedOopInfo HeapShared::make_cached_oop_info(oop obj, oop referrer) { PointsToOopsChecker points_to_oops_checker; obj->oop_iterate(&points_to_oops_checker); - return CachedOopInfo(referrer, points_to_oops_checker.result()); + return CachedOopInfo(OopHandle(Universe::vm_global(), referrer), points_to_oops_checker.result()); } void HeapShared::init_box_classes(TRAPS) { @@ -2096,6 +2125,18 @@ bool HeapShared::is_dumped_interned_string(oop o) { return _dumped_interned_strings->get(o) != nullptr; } +// These tables should be used only within the CDS safepoint, so +// delete them before we exit the safepoint. Otherwise the table will +// contain bad oops after a GC. +void HeapShared::delete_tables_with_raw_oops() { + assert(_seen_objects_table == nullptr, "should have been deleted"); + + delete _dumped_interned_strings; + _dumped_interned_strings = nullptr; + + ArchiveHeapWriter::delete_tables_with_raw_oops(); +} + void HeapShared::debug_trace() { ResourceMark rm; oop referrer = _object_being_archived.referrer(); diff --git a/src/hotspot/share/cds/heapShared.hpp b/src/hotspot/share/cds/heapShared.hpp index 110cdef8796..c9a810a6c0b 100644 --- a/src/hotspot/share/cds/heapShared.hpp +++ b/src/hotspot/share/cds/heapShared.hpp @@ -167,6 +167,9 @@ private: public: static void debug_trace(); static unsigned oop_hash(oop const& p); + static unsigned oop_handle_hash(OopHandle const& oh); + static unsigned oop_handle_hash_raw(OopHandle const& oh); + static bool oop_handle_equals(const OopHandle& a, const OopHandle& b); static unsigned string_oop_hash(oop const& string) { return java_lang_String::hash_code(string); } @@ -175,7 +178,7 @@ public: class CachedOopInfo { // Used by CDSHeapVerifier. - oop _orig_referrer; + OopHandle _orig_referrer; // The location of this object inside ArchiveHeapWriter::_buffer size_t _buffer_offset; @@ -186,12 +189,12 @@ public: // One or more fields in this object are pointing to MetaspaceObj bool _has_native_pointers; public: - CachedOopInfo(oop orig_referrer, bool has_oop_pointers) + CachedOopInfo(OopHandle orig_referrer, bool has_oop_pointers) : _orig_referrer(orig_referrer), _buffer_offset(0), _has_oop_pointers(has_oop_pointers), _has_native_pointers(false) {} - oop orig_referrer() const { return _orig_referrer; } + oop orig_referrer() const; void set_buffer_offset(size_t offset) { _buffer_offset = offset; } size_t buffer_offset() const { return _buffer_offset; } bool has_oop_pointers() const { return _has_oop_pointers; } @@ -202,10 +205,11 @@ public: private: static const int INITIAL_TABLE_SIZE = 15889; // prime number static const int MAX_TABLE_SIZE = 1000000; - typedef ResizeableHashTable ArchivedObjectCache; + HeapShared::oop_handle_hash_raw, + HeapShared::oop_handle_equals> ArchivedObjectCache; static ArchivedObjectCache* _archived_object_cache; class DumpTimeKlassSubGraphInfoTable @@ -378,6 +382,11 @@ private: return _archived_object_cache; } + static CachedOopInfo* get_cached_oop_info(oop orig_obj) { + OopHandle oh(&orig_obj); + return _archived_object_cache->get(oh); + } + static int archive_exception_instance(oop exception); static bool archive_reachable_objects_from(int level, @@ -435,6 +444,7 @@ private: CDS_JAVA_HEAP_ONLY(return (idx == AOTMetaspace::hp);) NOT_CDS_JAVA_HEAP_RETURN_(false); } + static void delete_tables_with_raw_oops() NOT_CDS_JAVA_HEAP_RETURN; static void resolve_classes(JavaThread* current) NOT_CDS_JAVA_HEAP_RETURN; static void initialize_from_archived_subgraph(JavaThread* current, Klass* k) NOT_CDS_JAVA_HEAP_RETURN; From 7ec3fa5f0a7408bf70e6226814d80dabd8a1a93c Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Fri, 19 Sep 2025 01:36:41 +0000 Subject: [PATCH 112/556] 8367801: jtreg failure_handler - don't use the -L option for ps command Reviewed-by: ayang, shade --- test/failure_handler/src/share/conf/linux.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/failure_handler/src/share/conf/linux.properties b/test/failure_handler/src/share/conf/linux.properties index 08e4ea8bd87..5cd625ce95a 100644 --- a/test/failure_handler/src/share/conf/linux.properties +++ b/test/failure_handler/src/share/conf/linux.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -108,7 +108,7 @@ system.sysctl.args=-a process.top.app=top process.top.args=-b -n 1 process.ps.app=ps -process.ps.args=-Leo pid,pcpu,cputime,start,pmem,vsz,rssize,stackp,stat,sgi_p,wchan,user,args +process.ps.args=-eo pid,pcpu,cputime,start,pmem,vsz,rssize,stackp,stat,sgi_p,wchan,user,args memory.free.app=free memory.free.args=-h From c0815e40b6f5feeb4bfa791ccd91d662c205068d Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Fri, 19 Sep 2025 01:50:20 +0000 Subject: [PATCH 113/556] 8367904: Test java/net/InetAddress/ptr/Lookup.java should throw SkippedException Reviewed-by: fandreuzzi, dfuchs --- test/jdk/java/net/InetAddress/ptr/Lookup.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/jdk/java/net/InetAddress/ptr/Lookup.java b/test/jdk/java/net/InetAddress/ptr/Lookup.java index 1248916023e..39e5f720cef 100644 --- a/test/jdk/java/net/InetAddress/ptr/Lookup.java +++ b/test/jdk/java/net/InetAddress/ptr/Lookup.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -46,6 +46,7 @@ import java.util.stream.Collectors; import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; +import jtreg.SkippedException; public class Lookup { private static final String HOST = "icann.org"; @@ -91,8 +92,7 @@ public class Lookup { String tmp = lookupWithIPv4Prefer(); System.out.println("IPv4 lookup results: [" + tmp + "]"); if (SKIP.equals(tmp)) { - System.out.println(HOST + " can't be resolved - test skipped."); - return; + throw new SkippedException(HOST + " can't be resolved - test skipped."); } String[] strs = tmp.split(":"); @@ -104,8 +104,7 @@ public class Lookup { tmp = reverseWithIPv4Prefer(addr); System.out.println("IPv4 reverse lookup results: [" + tmp + "]"); if (SKIP.equals(tmp)) { - System.out.println(addr + " can't be resolved with preferIPv4 - test skipped."); - return; + throw new SkippedException(addr + " can't be resolved with preferIPv4 - test skipped."); } strs = tmp.split(":"); From 5855fd2f654175c05341cc03ebf188d4db3e407d Mon Sep 17 00:00:00 2001 From: Prasanta Sadhukhan Date: Fri, 19 Sep 2025 02:58:02 +0000 Subject: [PATCH 114/556] 8367784: java/awt/Focus/InitialFocusTest/InitialFocusTest1.java failed with Wrong focus owner Reviewed-by: honkar, dnguyen --- .../InitialFocusTest/InitialFocusTest1.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java b/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java index 9edbcf8f71c..602a376655c 100644 --- a/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java +++ b/test/jdk/java/awt/Focus/InitialFocusTest/InitialFocusTest1.java @@ -22,8 +22,10 @@ */ import java.awt.Button; +import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Frame; +import java.awt.Robot; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; @@ -39,27 +41,36 @@ public class InitialFocusTest1 extends Frame implements FocusListener { Button button1 = new Button("Button1"); Button button2 = new Button("Button2"); private static volatile Object focused; + private static InitialFocusTest1 app; public static void main(final String[] args) throws Exception { - InitialFocusTest1 app = new InitialFocusTest1(); try { - app.setSize(200, 200); - app.setLocationRelativeTo(null); - app.setLayout(new FlowLayout()); + Robot robot = new Robot(); + EventQueue.invokeAndWait(() -> { + app = new InitialFocusTest1(); + app.setLayout(new FlowLayout()); - app.button1.addFocusListener(app); - app.button2.addFocusListener(app); - app.add(app.button1); - app.add(app.button2); - app.setVisible(true); - app.button2.requestFocus(); + app.button1.addFocusListener(app); + app.button2.addFocusListener(app); + app.add(app.button1); + app.add(app.button2); + + app.setSize(200, 200); + app.setLocationRelativeTo(null); + app.setVisible(true); + }); + robot.waitForIdle(); + robot.delay(1000); + EventQueue.invokeAndWait(() -> { + app.button2.requestFocus(); + }); // wait for the very very last focus event - Thread.sleep(10000); + robot.delay(1000); if (app.button2 != focused) { throw new RuntimeException("Wrong focus owner: " + focused); } } finally { - app.dispose(); + EventQueue.invokeAndWait(() -> app.dispose()); } } From 898fcff03745da29318e29ead189d78f8daa6988 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 19 Sep 2025 04:33:48 +0000 Subject: [PATCH 115/556] 8367325: [s390x] build failure due to JDK-8361376 Reviewed-by: mdoerr, dlong --- .../gc/shared/barrierSetAssembler_s390.cpp | 1 + .../gc/shared/barrierSetAssembler_s390.hpp | 8 +++++ .../s390/gc/shared/barrierSetNMethod_s390.cpp | 33 +++++++++++++------ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.cpp b/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.cpp index 2d663061aec..c6f5a4e119c 100644 --- a/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.cpp +++ b/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.cpp @@ -171,6 +171,7 @@ void BarrierSetAssembler::try_resolve_jobject_in_native(MacroAssembler* masm, Re void BarrierSetAssembler::nmethod_entry_barrier(MacroAssembler* masm) { BarrierSetNMethod* bs_nm = BarrierSet::barrier_set()->barrier_set_nmethod(); + __ align(4, __ offset() + OFFSET_TO_PATCHABLE_DATA); // must align the following block which requires atomic updates __ block_comment("nmethod_entry_barrier (nmethod_entry_barrier) {"); // Load jump addr: diff --git a/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.hpp b/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.hpp index acc0d3b4988..3e0b2be4873 100644 --- a/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.hpp +++ b/src/hotspot/cpu/s390/gc/shared/barrierSetAssembler_s390.hpp @@ -66,6 +66,14 @@ public: OptoReg::Name refine_register(const Node* node, OptoReg::Name opto_reg) const; #endif // COMPILER2 + + static const int OFFSET_TO_PATCHABLE_DATA_INSTRUCTION = 6 + 6 + 6; // iihf(6) + iilf(6) + lg(6) + static const int BARRIER_TOTAL_LENGTH = OFFSET_TO_PATCHABLE_DATA_INSTRUCTION + 6 + 6 + 2; // cfi(6) + larl(6) + bcr(2) + + // first 2 bytes are for cfi instruction opcode and next 4 bytes will be the value/data to be patched, + // so we are skipping first 2 bytes and returning the address of value/data field + static const int OFFSET_TO_PATCHABLE_DATA = 6 + 6 + 6 + 2; // iihf(6) + iilf(6) + lg(6) + CFI_OPCODE(2) + }; #ifdef COMPILER2 diff --git a/src/hotspot/cpu/s390/gc/shared/barrierSetNMethod_s390.cpp b/src/hotspot/cpu/s390/gc/shared/barrierSetNMethod_s390.cpp index 8f43f4ef723..f6bf137da1c 100644 --- a/src/hotspot/cpu/s390/gc/shared/barrierSetNMethod_s390.cpp +++ b/src/hotspot/cpu/s390/gc/shared/barrierSetNMethod_s390.cpp @@ -26,26 +26,32 @@ #include "code/codeBlob.hpp" #include "code/nativeInst.hpp" #include "code/nmethod.hpp" +#include "gc/shared/barrierSetAssembler.hpp" #include "gc/shared/barrierSetNMethod.hpp" #include "utilities/debug.hpp" class NativeMethodBarrier: public NativeInstruction { private: - static const int PATCHABLE_INSTRUCTION_OFFSET = 3*6; // bytes address get_barrier_start_address() const { return NativeInstruction::addr_at(0); } address get_patchable_data_address() const { - address inst_addr = get_barrier_start_address() + PATCHABLE_INSTRUCTION_OFFSET; + address start_address = get_barrier_start_address(); +#ifdef ASSERT + address inst_addr = start_address + BarrierSetAssembler::OFFSET_TO_PATCHABLE_DATA_INSTRUCTION; - DEBUG_ONLY(Assembler::is_z_cfi(*((long*)inst_addr))); - return inst_addr + 2; + unsigned long instr = 0; + Assembler::get_instruction(inst_addr, &instr); + assert(Assembler::is_z_cfi(instr), "sanity check"); +#endif // ASSERT + + return start_address + BarrierSetAssembler::OFFSET_TO_PATCHABLE_DATA; } public: - static const int BARRIER_TOTAL_LENGTH = PATCHABLE_INSTRUCTION_OFFSET + 2*6 + 2; // bytes + static const int BARRIER_TOTAL_LENGTH = BarrierSetAssembler::BARRIER_TOTAL_LENGTH; int get_guard_value() const { address data_addr = get_patchable_data_address(); @@ -77,23 +83,30 @@ class NativeMethodBarrier: public NativeInstruction { #ifdef ASSERT void verify() const { + unsigned long instr = 0; int offset = 0; // bytes const address start = get_barrier_start_address(); - MacroAssembler::is_load_const(/* address */ start + offset); // two instructions + assert(MacroAssembler::is_load_const(/* address */ start + offset), "sanity check"); // two instructions offset += Assembler::instr_len(&start[offset]); offset += Assembler::instr_len(&start[offset]); - Assembler::is_z_lg(*((long*)(start + offset))); + Assembler::get_instruction(start + offset, &instr); + assert(Assembler::is_z_lg(instr), "sanity check"); offset += Assembler::instr_len(&start[offset]); - Assembler::is_z_cfi(*((long*)(start + offset))); + // it will be assignment operation, So it doesn't matter what value is already present in instr + // hence, no need to 0 it out. + Assembler::get_instruction(start + offset, &instr); + assert(Assembler::is_z_cfi(instr), "sanity check"); offset += Assembler::instr_len(&start[offset]); - Assembler::is_z_larl(*((long*)(start + offset))); + Assembler::get_instruction(start + offset, &instr); + assert(Assembler::is_z_larl(instr), "sanity check"); offset += Assembler::instr_len(&start[offset]); - Assembler::is_z_bcr(*((long*)(start + offset))); + Assembler::get_instruction(start + offset, &instr); + assert(Assembler::is_z_bcr(instr), "sanity check"); offset += Assembler::instr_len(&start[offset]); assert(offset == BARRIER_TOTAL_LENGTH, "check offset == barrier length constant"); From 48d394a245e7d16423b3829efa326fe72605c8ee Mon Sep 17 00:00:00 2001 From: "Tagir F. Valeev" Date: Fri, 19 Sep 2025 07:19:03 +0000 Subject: [PATCH 116/556] 8356995: Provide default methods min(T, T) and max(T, T) in Comparator interface Reviewed-by: rriggs, smarks --- .../share/classes/java/util/Comparator.java | 49 +++++++++- test/jdk/java/util/Comparator/MinMaxTest.java | 90 +++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 test/jdk/java/util/Comparator/MinMaxTest.java diff --git a/src/java.base/share/classes/java/util/Comparator.java b/src/java.base/share/classes/java/util/Comparator.java index 6e0420d26e8..ad48dc94ed6 100644 --- a/src/java.base/share/classes/java/util/Comparator.java +++ b/src/java.base/share/classes/java/util/Comparator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,7 +30,6 @@ import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; import java.util.function.ToDoubleFunction; -import java.util.Comparators; /** * A comparison function, which imposes a total ordering on @@ -189,6 +188,52 @@ public interface Comparator { return Collections.reverseOrder(this); } + /** + * Returns the greater of two values according to this comparator. + * If the arguments are equal with respect to this comparator, + * the {@code o1} argument is returned. + * + * @implSpec This default implementation behaves as if + * {@code compare(o1, o2) >= 0 ? o1 : o2}. + * + * @param o1 an argument. + * @param o2 another argument. + * @param the type of the arguments and the result. + * @return the larger of {@code o1} and {@code o2} according to this comparator. + * @throws NullPointerException if an argument is null and this + * comparator does not permit null arguments + * @throws ClassCastException if the arguments' types prevent them from + * being compared by this comparator. + * + * @since 26 + */ + default U max(U o1, U o2) { + return compare(o1, o2) >= 0 ? o1 : o2; + } + + /** + * Returns the smaller of two values according to this comparator. + * If the arguments are equal with respect to this comparator, + * the {@code o1} argument is returned. + * + * @implSpec This default implementation behaves as if + * {@code compare(o1, o2) <= 0 ? o1 : o2}. + * + * @param o1 an argument. + * @param o2 another argument. + * @param the type of the arguments and the result. + * @return the smaller of {@code o1} and {@code o2} according to this comparator. + * @throws NullPointerException if an argument is null and this + * comparator does not permit null arguments + * @throws ClassCastException if the arguments' types prevent them from + * being compared by this comparator. + * + * @since 26 + */ + default U min(U o1, U o2) { + return compare(o1, o2) <= 0 ? o1 : o2; + } + /** * Returns a lexicographic-order comparator with another comparator. * If this {@code Comparator} considers two elements equal, i.e. diff --git a/test/jdk/java/util/Comparator/MinMaxTest.java b/test/jdk/java/util/Comparator/MinMaxTest.java new file mode 100644 index 00000000000..f1ef7d2d164 --- /dev/null +++ b/test/jdk/java/util/Comparator/MinMaxTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8356995 + * @summary Comparator min/max method tests + * @run junit MinMaxTest + */ + +import org.junit.jupiter.api.Test; + +import java.util.Comparator; + +import static org.junit.jupiter.api.Assertions.*; + +public class MinMaxTest { + @Test + void testMin() { + Comparator c = Comparator.naturalOrder(); + assertEquals("a", c.min("a", "b")); + assertEquals("a", c.min("b", "a")); + } + + @Test + void testMax() { + Comparator c = Comparator.naturalOrder(); + assertEquals("b", c.max("a", "b")); + assertEquals("b", c.max("b", "a")); + } + + @Test + void testThrowsNPE() { + Comparator c = Comparator.naturalOrder(); + assertThrows(NullPointerException.class, () -> c.min(null, "a")); + assertThrows(NullPointerException.class, () -> c.min("a", null)); + assertThrows(NullPointerException.class, () -> c.max(null, "a")); + assertThrows(NullPointerException.class, () -> c.max("a", null)); + } + + @Test + void testThrowsCCE() { + @SuppressWarnings("unchecked") + Comparator c = (Comparator) (Comparator)Comparator.naturalOrder(); + assertThrows(ClassCastException.class, () -> c.min(1, "a")); + assertThrows(ClassCastException.class, () -> c.min("a", 1)); + assertThrows(ClassCastException.class, () -> c.max(1, "a")); + assertThrows(ClassCastException.class, () -> c.max("a", 1)); + } + + @Test + void testEqualReturnFirst() { + Comparator allEqual = (_, _) -> 0; + Object o1 = new Object(); + Object o2 = new Object(); + assertSame(o1, allEqual.min(o1, o2)); + assertSame(o1, allEqual.max(o1, o2)); + } + + @Test + void testComparatorSubtype() { + Comparator byLength = Comparator.comparing(CharSequence::length); + String s1 = "long_string"; + String s2 = "short"; + String min = byLength.min(s1, s2); + String max = byLength.max(s1, s2); + assertEquals(s1, max); + assertEquals(s2, min); + } +} From 28879d3d03ca6e8ea68a6063da349d83310b22ce Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Fri, 19 Sep 2025 07:37:14 +0000 Subject: [PATCH 117/556] 8367848: Parallel: Use NMethodToOopClosure during Young GC Reviewed-by: stefank, tschatzl --- src/hotspot/share/gc/parallel/psScavenge.cpp | 6 +++--- src/hotspot/share/runtime/threads.hpp | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psScavenge.cpp b/src/hotspot/share/gc/parallel/psScavenge.cpp index d32a8d239d1..676d9e74dbf 100644 --- a/src/hotspot/share/gc/parallel/psScavenge.cpp +++ b/src/hotspot/share/gc/parallel/psScavenge.cpp @@ -99,7 +99,7 @@ static void scavenge_roots_work(ParallelRootType::Value root_type, uint worker_i case ParallelRootType::code_cache: { - MarkingNMethodClosure code_closure(&roots_to_old_closure, NMethodToOopClosure::FixRelocations, false /* keepalive nmethods */); + NMethodToOopClosure code_closure(&roots_to_old_closure, NMethodToOopClosure::FixRelocations); ScavengableNMethods::nmethods_do(&code_closure); } break; @@ -234,7 +234,7 @@ public: }; class ScavengeRootsTask : public WorkerTask { - StrongRootsScope _strong_roots_scope; // needed for Threads::possibly_parallel_threads_do + ThreadsClaimTokenScope _threads_claim_token_scope; // needed for Threads::possibly_parallel_threads_do OopStorageSetStrongParState _oop_storage_strong_par_state; SequentialSubTasksDone _subtasks; PSOldGen* _old_gen; @@ -247,7 +247,7 @@ public: ScavengeRootsTask(PSOldGen* old_gen, uint active_workers) : WorkerTask("ScavengeRootsTask"), - _strong_roots_scope(active_workers), + _threads_claim_token_scope(), _subtasks(ParallelRootType::sentinel), _old_gen(old_gen), _gen_top(old_gen->object_space()->top()), diff --git a/src/hotspot/share/runtime/threads.hpp b/src/hotspot/share/runtime/threads.hpp index c6428c874f6..f630cde8747 100644 --- a/src/hotspot/share/runtime/threads.hpp +++ b/src/hotspot/share/runtime/threads.hpp @@ -145,4 +145,14 @@ public: struct Test; // For private gtest access. }; +// Used by GC for calling Threads::possibly_parallel_oops_do. +struct ThreadsClaimTokenScope : StackObj { + ThreadsClaimTokenScope() { + Threads::change_thread_claim_token(); + } + ~ThreadsClaimTokenScope() { + Threads::assert_all_threads_claimed(); + } +}; + #endif // SHARE_RUNTIME_THREADS_HPP From 930d7249239e464adfca3a007342ce0fcb8f070d Mon Sep 17 00:00:00 2001 From: Kevin Walls Date: Fri, 19 Sep 2025 08:15:28 +0000 Subject: [PATCH 118/556] 8367983: javax/management/monitor/ThreadPoolTest.java and StartStopTest.java fail with Unexpected Maximum Pool Size Overflow! Reviewed-by: cjplummer, dholmes --- test/jdk/javax/management/monitor/StartStopTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/jdk/javax/management/monitor/StartStopTest.java b/test/jdk/javax/management/monitor/StartStopTest.java index 4efbb45e609..2b0b5d92db5 100644 --- a/test/jdk/javax/management/monitor/StartStopTest.java +++ b/test/jdk/javax/management/monitor/StartStopTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -63,6 +63,8 @@ public class StartStopTest { static int maxPoolSize; static final AtomicInteger counter = new AtomicInteger(); + public static final int TIMEOUT = 2500; + // MBean class public class ObservedObject implements ObservedObjectMBean { volatile public boolean called = false; @@ -148,7 +150,7 @@ public class StartStopTest { for (int i = 0; i < nTasks; i++) monitor[i].start(); echo(">>> MONITORS started"); - doSleep(500); + doSleep(TIMEOUT); echo(">>> Check FLAGS true"); for (int i = 0; i < nTasks; i++) if (!monitored[i].called) { @@ -160,7 +162,7 @@ public class StartStopTest { for (int i = 0; i < nTasks; i++) monitor[i].stop(); echo(">>> MONITORS stopped"); - doSleep(500); + doSleep(TIMEOUT); echo(">>> Set FLAGS to false"); for (int i = 0; i < nTasks; i++) monitored[i].called = false; From 937e19e86aab9194c363fb8709bbbc6dead0c391 Mon Sep 17 00:00:00 2001 From: Stefan Johansson Date: Fri, 19 Sep 2025 08:48:35 +0000 Subject: [PATCH 119/556] 8366780: Enhance ProcSmapsParser and Printer to handle THPeligible field Reviewed-by: stuefe, ayang --- src/hotspot/os/linux/memMapPrinter_linux.cpp | 2 ++ src/hotspot/os/linux/procMapsParser.cpp | 10 +++++++++- src/hotspot/os/linux/procMapsParser.hpp | 3 ++- .../serviceability/dcmd/vm/SystemMapTestBase.java | 4 ++-- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/hotspot/os/linux/memMapPrinter_linux.cpp b/src/hotspot/os/linux/memMapPrinter_linux.cpp index 91fd552eec5..228a823dcb0 100644 --- a/src/hotspot/os/linux/memMapPrinter_linux.cpp +++ b/src/hotspot/os/linux/memMapPrinter_linux.cpp @@ -106,6 +106,7 @@ public: PRINTIF(info.swap > 0, "swap"); PRINTIF(info.ht, "huge"); PRINTIF(info.anonhugepages > 0, "thp"); + PRINTIF(info.thpeligible, "thpel"); PRINTIF(info.hg, "thpad"); PRINTIF(info.nh, "nothp"); if (num_printed == 0) { @@ -135,6 +136,7 @@ public: st->print_cr(" com: mapping committed (swap space reserved)"); st->print_cr(" swap: mapping partly or completely swapped out"); st->print_cr(" thp: mapping uses THP"); + st->print_cr(" thpel: mapping is THP-eligible"); st->print_cr(" thpad: mapping is THP-madvised"); st->print_cr(" nothp: mapping is forbidden to use THP"); st->print_cr(" huge: mapping uses hugetlb pages"); diff --git a/src/hotspot/os/linux/procMapsParser.cpp b/src/hotspot/os/linux/procMapsParser.cpp index 71b828bcefb..47c5c6cc594 100644 --- a/src/hotspot/os/linux/procMapsParser.cpp +++ b/src/hotspot/os/linux/procMapsParser.cpp @@ -76,8 +76,16 @@ void ProcSmapsParser::scan_additional_line(ProcSmapsInfo& out) { SCAN("Private_Hugetlb", out.private_hugetlb); SCAN("Shared_Hugetlb", out.shared_hugetlb); SCAN("Swap", out.swap); - int i = 0; #undef SCAN + + // scan THPeligible into a bool + int thpel = 0; + if (::sscanf(_line, "THPeligible: %d", &thpel) == 1) { + assert(thpel == 1 || thpel == 0, "Unexpected value %d", thpel); + out.thpeligible = (thpel == 1); + return; + } + // scan some flags too if (strncmp(_line, "VmFlags:", 8) == 0) { #define SCAN(flag) { out.flag = (::strstr(_line + 8, " " #flag) != nullptr); } diff --git a/src/hotspot/os/linux/procMapsParser.hpp b/src/hotspot/os/linux/procMapsParser.hpp index 0971c4fb084..06035333b2f 100644 --- a/src/hotspot/os/linux/procMapsParser.hpp +++ b/src/hotspot/os/linux/procMapsParser.hpp @@ -49,6 +49,7 @@ struct ProcSmapsInfo { size_t shared_hugetlb; size_t anonhugepages; size_t swap; + bool thpeligible; bool rd, wr, ex; bool sh; // shared bool nr; // no reserve @@ -64,7 +65,7 @@ struct ProcSmapsInfo { from = to = nullptr; prot[0] = filename[0] = '\0'; kernelpagesize = rss = private_hugetlb = shared_hugetlb = anonhugepages = swap = 0; - rd = wr = ex = sh = nr = hg = ht = nh = false; + thpeligible = rd = wr = ex = sh = nr = hg = ht = nh = false; } }; diff --git a/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java b/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java index 47590ec3d54..1447012073a 100644 --- a/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java +++ b/test/hotspot/jtreg/serviceability/dcmd/vm/SystemMapTestBase.java @@ -99,8 +99,8 @@ public class SystemMapTestBase { regexBase_java_heap + "JAVAHEAP.*", // metaspace regexBase_committed + "META.*", - // parts of metaspace should be uncommitted - regexBase + "-" + space + "META.*", + // parts of metaspace should be uncommitted, those parts may or may not be be thp-eligible + regexBase + "(-|thpel)" + space + "META.*", // code cache regexBase_committed + "CODE.*", // Main thread stack From 94a301a70e19be284f406ebb6d8b94b6f96e1a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20H=C3=A4ssig?= Date: Fri, 19 Sep 2025 09:08:29 +0000 Subject: [PATCH 120/556] 8366875: CompileTaskTimeout should be reset for each iteration of RepeatCompilation Reviewed-by: dlong, epeter --- src/hotspot/os/linux/compilerThreadTimeout_linux.hpp | 4 ++++ src/hotspot/share/compiler/compileBroker.cpp | 1 + src/hotspot/share/compiler/compilerThread.hpp | 1 + .../compiler/arguments/TestCompileTaskTimeout.java | 12 +++++++++++- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/hotspot/os/linux/compilerThreadTimeout_linux.hpp b/src/hotspot/os/linux/compilerThreadTimeout_linux.hpp index 2dc6fa7b9c9..7c27080eb5e 100644 --- a/src/hotspot/os/linux/compilerThreadTimeout_linux.hpp +++ b/src/hotspot/os/linux/compilerThreadTimeout_linux.hpp @@ -46,6 +46,10 @@ class CompilerThreadTimeoutLinux : public CHeapObj { bool init_timeout(); void arm(); void disarm(); + void reset() { + disarm(); + arm(); + }; }; #endif //LINUX_COMPILER_THREAD_TIMEOUT_LINUX_HPP diff --git a/src/hotspot/share/compiler/compileBroker.cpp b/src/hotspot/share/compiler/compileBroker.cpp index 24f28821087..226a6d3ad5c 100644 --- a/src/hotspot/share/compiler/compileBroker.cpp +++ b/src/hotspot/share/compiler/compileBroker.cpp @@ -2349,6 +2349,7 @@ void CompileBroker::invoke_compiler_on_method(CompileTask* task) { while (repeat_compilation_count > 0) { ResourceMark rm(thread); task->print_ul("NO CODE INSTALLED"); + thread->timeout()->reset(); comp->compile_method(&ci_env, target, osr_bci, false, directive); repeat_compilation_count--; } diff --git a/src/hotspot/share/compiler/compilerThread.hpp b/src/hotspot/share/compiler/compilerThread.hpp index e4641780a12..e5b14560872 100644 --- a/src/hotspot/share/compiler/compilerThread.hpp +++ b/src/hotspot/share/compiler/compilerThread.hpp @@ -51,6 +51,7 @@ class CompilerThreadTimeoutGeneric : public CHeapObj { CompilerThreadTimeoutGeneric() {}; void arm() {}; void disarm() {}; + void reset() {}; bool init_timeout() { return true; }; }; #endif // !LINUX diff --git a/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java b/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java index f19223e6dce..9ec3beb0d89 100644 --- a/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java +++ b/test/hotspot/jtreg/compiler/arguments/TestCompileTaskTimeout.java @@ -25,7 +25,7 @@ package compiler.arguments; /* * @test TestCompileTaskTimeout - * @bug 8308094 8365909 + * @bug 8308094 8365909 8366875 * @requires vm.debug & vm.flagless & os.name == "Linux" * @summary Check functionality of CompileTaskTimeout * @library /test/lib @@ -42,6 +42,7 @@ public class TestCompileTaskTimeout { timeoutFactor = Double.parseDouble(System.getProperty("test.timeout.factor", "1.0")); } catch (NumberFormatException ignored) {} + // Short timeouts crash the VM. ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=1", "--version") .shouldHaveExitValue(134) .shouldContain("timed out after"); @@ -54,8 +55,17 @@ public class TestCompileTaskTimeout { .shouldHaveExitValue(134) .shouldContain("timed out after"); + // A long enough timeout succeeds. int timeout = (int)(500.0 * timeoutFactor); ProcessTools.executeTestJava("-Xcomp", "-XX:CompileTaskTimeout=" + timeout, "--version") .shouldHaveExitValue(0); + + // Each repeated compilation has a new timeout. + ProcessTools.executeTestJava("-Xcomp", + "-XX:CompileTaskTimeout=" + timeout, + "-XX:RepeatCompilation=100", + "-XX:CompileCommand=compileonly,java/util/concurrent/ConcurrentHashMap.*", + "--version") + .shouldHaveExitValue(0); } } From 65aea485884134743fbd3da355bd1f861b410704 Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Fri, 19 Sep 2025 10:06:02 +0000 Subject: [PATCH 121/556] 8367859: Remove nio exception gensrc Reviewed-by: naoto, erikj, bpb --- make/modules/java.base/Gensrc.gmk | 1 - .../java.base/gensrc/GensrcExceptions.gmk | 57 ----- make/scripts/addNotices.sh | 45 ---- make/scripts/genExceptions.sh | 116 ----------- .../java/nio/BufferOverflowException.java | 46 +++++ .../java/nio/BufferUnderflowException.java | 46 +++++ .../java/nio/InvalidMarkException.java | 46 +++++ .../java/nio/ReadOnlyBufferException.java | 46 +++++ .../nio/channels/AcceptPendingException.java | 46 +++++ .../nio/channels/AlreadyBoundException.java | 46 +++++ .../channels/AlreadyConnectedException.java | 46 +++++ .../channels/AsynchronousCloseException.java | 47 +++++ .../nio/channels/CancelledKeyException.java | 46 +++++ .../channels/ClosedByInterruptException.java | 48 +++++ .../nio/channels/ClosedChannelException.java | 49 +++++ .../nio/channels/ClosedSelectorException.java | 46 +++++ .../channels/ConnectionPendingException.java | 47 +++++ .../FileLockInterruptionException.java | 47 +++++ .../IllegalBlockingModeException.java | 46 +++++ .../IllegalChannelGroupException.java | 46 +++++ .../channels/IllegalSelectorException.java | 47 +++++ .../InterruptedByTimeoutException.java | 46 +++++ .../NoConnectionPendingException.java | 47 +++++ .../channels/NonReadableChannelException.java | 46 +++++ .../channels/NonWritableChannelException.java | 46 +++++ .../nio/channels/NotYetBoundException.java | 46 +++++ .../channels/NotYetConnectedException.java | 46 +++++ .../OverlappingFileLockException.java | 48 +++++ .../nio/channels/ReadPendingException.java | 46 +++++ .../ShutdownChannelGroupException.java | 47 +++++ .../channels/UnresolvedAddressException.java | 46 +++++ .../UnsupportedAddressTypeException.java | 46 +++++ .../nio/channels/WritePendingException.java | 46 +++++ .../classes/java/nio/channels/exceptions | 194 ------------------ .../nio/charset/CharacterCodingException.java | 46 +++++ .../charset/IllegalCharsetNameException.java | 68 ++++++ .../charset/UnsupportedCharsetException.java | 68 ++++++ .../share/classes/java/nio/charset/exceptions | 53 ----- .../share/classes/java/nio/exceptions | 60 ------ 39 files changed, 1529 insertions(+), 526 deletions(-) delete mode 100644 make/modules/java.base/gensrc/GensrcExceptions.gmk delete mode 100644 make/scripts/addNotices.sh delete mode 100644 make/scripts/genExceptions.sh create mode 100644 src/java.base/share/classes/java/nio/BufferOverflowException.java create mode 100644 src/java.base/share/classes/java/nio/BufferUnderflowException.java create mode 100644 src/java.base/share/classes/java/nio/InvalidMarkException.java create mode 100644 src/java.base/share/classes/java/nio/ReadOnlyBufferException.java create mode 100644 src/java.base/share/classes/java/nio/channels/AcceptPendingException.java create mode 100644 src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java create mode 100644 src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java create mode 100644 src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java create mode 100644 src/java.base/share/classes/java/nio/channels/CancelledKeyException.java create mode 100644 src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java create mode 100644 src/java.base/share/classes/java/nio/channels/ClosedChannelException.java create mode 100644 src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java create mode 100644 src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java create mode 100644 src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java create mode 100644 src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java create mode 100644 src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java create mode 100644 src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java create mode 100644 src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java create mode 100644 src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java create mode 100644 src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java create mode 100644 src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java create mode 100644 src/java.base/share/classes/java/nio/channels/NotYetBoundException.java create mode 100644 src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java create mode 100644 src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java create mode 100644 src/java.base/share/classes/java/nio/channels/ReadPendingException.java create mode 100644 src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java create mode 100644 src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java create mode 100644 src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java create mode 100644 src/java.base/share/classes/java/nio/channels/WritePendingException.java delete mode 100644 src/java.base/share/classes/java/nio/channels/exceptions create mode 100644 src/java.base/share/classes/java/nio/charset/CharacterCodingException.java create mode 100644 src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java create mode 100644 src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java delete mode 100644 src/java.base/share/classes/java/nio/charset/exceptions delete mode 100644 src/java.base/share/classes/java/nio/exceptions diff --git a/make/modules/java.base/Gensrc.gmk b/make/modules/java.base/Gensrc.gmk index 2750a6c8791..79db438934e 100644 --- a/make/modules/java.base/Gensrc.gmk +++ b/make/modules/java.base/Gensrc.gmk @@ -33,7 +33,6 @@ include gensrc/GensrcBuffer.gmk include gensrc/GensrcCharacterData.gmk include gensrc/GensrcCharsetCoder.gmk include gensrc/GensrcCharsetMapping.gmk -include gensrc/GensrcExceptions.gmk include gensrc/GensrcMisc.gmk include gensrc/GensrcModuleLoaderMap.gmk include gensrc/GensrcRegex.gmk diff --git a/make/modules/java.base/gensrc/GensrcExceptions.gmk b/make/modules/java.base/gensrc/GensrcExceptions.gmk deleted file mode 100644 index baa61596d6b..00000000000 --- a/make/modules/java.base/gensrc/GensrcExceptions.gmk +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -include MakeIncludeStart.gmk -ifeq ($(INCLUDE), true) - -################################################################################ - -GENSRC_EXCEPTIONS := - -GENSRC_EXCEPTIONS_DST := $(SUPPORT_OUTPUTDIR)/gensrc/java.base/java/nio - -GENSRC_EXCEPTIONS_SRC := $(MODULE_SRC)/share/classes/java/nio -GENSRC_EXCEPTIONS_CMD := $(TOPDIR)/make/scripts/genExceptions.sh - -GENSRC_EXCEPTIONS_SRC_DIRS := . charset channels - -$(GENSRC_EXCEPTIONS_DST)/_the.%.marker: $(GENSRC_EXCEPTIONS_SRC)/%/exceptions \ - $(GENSRC_EXCEPTIONS_CMD) - $(call LogInfo, Generating exceptions java.nio $*) - $(call MakeDir, $(@D)/$*) - SCRIPTS="$(TOPDIR)/make/scripts" AWK="$(AWK)" SH="$(SH)" $(SH) \ - $(GENSRC_EXCEPTIONS_CMD) $< $(@D)/$* $(LOG_DEBUG) - $(TOUCH) $@ - -GENSRC_EXCEPTIONS += $(foreach D, $(GENSRC_EXCEPTIONS_SRC_DIRS), $(GENSRC_EXCEPTIONS_DST)/_the.$(D).marker) - -$(GENSRC_EXCEPTIONS): $(BUILD_TOOLS_JDK) - -TARGETS += $(GENSRC_EXCEPTIONS) - -################################################################################ - -endif # include guard -include MakeIncludeEnd.gmk diff --git a/make/scripts/addNotices.sh b/make/scripts/addNotices.sh deleted file mode 100644 index d9864818a14..00000000000 --- a/make/scripts/addNotices.sh +++ /dev/null @@ -1,45 +0,0 @@ -#! /bin/sh -# -# Copyright (c) 2007, 2020, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Parse the first contiguous comment block in this script and generate -# a java comment block. If this script is invoked with a copyright -# year/year range, the java comment block will contain a Sun copyright. - -COPYRIGHT_YEARS="$1" - -cat <<__END__ -/* -__END__ - -if [ "x$COPYRIGHT_YEARS" != x ]; then - cat <<__END__ - * Copyright (c) $COPYRIGHT_YEARS Oracle and/or its affiliates. All rights reserved. -__END__ -fi - -$AWK ' /^#.*Copyright.*Oracle/ { next } - /^#([^!]|$)/ { sub(/^#/, " *"); print } - /^$/ { print " */"; exit } ' $0 diff --git a/make/scripts/genExceptions.sh b/make/scripts/genExceptions.sh deleted file mode 100644 index 7c191189827..00000000000 --- a/make/scripts/genExceptions.sh +++ /dev/null @@ -1,116 +0,0 @@ -#! /bin/sh -# -# Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Generate exception classes - -SPEC=$1 -DST=$2 - -gen() { - ID=$1 - WHAT=$2 - SVUID=$3 - ARG_TYPE=$4 - ARG_ID=$5 - ARG_PROP=$6 - ARG_PHRASE=$7 - ARG_PARAM="$ARG_TYPE$ $ARG_ID" - echo '-->' $DST/$ID.java - out=$DST/${ID}.java - - $SH ${SCRIPTS}/addNotices.sh "$COPYRIGHT_YEARS" > $out - - cat >>$out <<__END__ - -// -- This file was mechanically generated: Do not edit! -- // - -package $PACKAGE; - - -/**$WHAT - * - * @since $SINCE - */ - -public `if [ ${ABSTRACT:-0} = 1 ]; - then echo 'abstract '; fi`class $ID - extends ${SUPER} -{ - - @java.io.Serial - private static final long serialVersionUID = $SVUID; -__END__ - - if [ $ARG_ID ]; then - - cat >>$out <<__END__ - - /** - * The $ARG_PHRASE. - * - * @serial - */ - private $ARG_TYPE $ARG_ID; - - /** - * Constructs an instance of this class. - * - * @param $ARG_ID - * The $ARG_PHRASE - */ - public $ID($ARG_TYPE $ARG_ID) { - super(String.valueOf($ARG_ID)); - this.$ARG_ID = $ARG_ID; - } - - /** - * Retrieves the $ARG_PHRASE. - * - * @return The $ARG_PHRASE - */ - public $ARG_TYPE get$ARG_PROP() { - return $ARG_ID; - } - -} -__END__ - - else - - cat >>$out <<__END__ - - /** - * Constructs an instance of this class. - */ - public $ID() { } - -} -__END__ - - fi -} - -. $SPEC diff --git a/src/java.base/share/classes/java/nio/BufferOverflowException.java b/src/java.base/share/classes/java/nio/BufferOverflowException.java new file mode 100644 index 00000000000..22195f8268b --- /dev/null +++ b/src/java.base/share/classes/java/nio/BufferOverflowException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a relative put operation reaches + * the target buffer's limit. + * + * @since 1.4 + */ + +public class BufferOverflowException + extends RuntimeException +{ + + @java.io.Serial + private static final long serialVersionUID = -5484897634319144535L; + + /** + * Constructs an instance of this class. + */ + public BufferOverflowException() { } +} diff --git a/src/java.base/share/classes/java/nio/BufferUnderflowException.java b/src/java.base/share/classes/java/nio/BufferUnderflowException.java new file mode 100644 index 00000000000..1561f1d4b62 --- /dev/null +++ b/src/java.base/share/classes/java/nio/BufferUnderflowException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a relative get operation reaches + * the source buffer's limit. + * + * @since 1.4 + */ + +public class BufferUnderflowException + extends RuntimeException +{ + + @java.io.Serial + private static final long serialVersionUID = -1713313658691622206L; + + /** + * Constructs an instance of this class. + */ + public BufferUnderflowException() { } +} diff --git a/src/java.base/share/classes/java/nio/InvalidMarkException.java b/src/java.base/share/classes/java/nio/InvalidMarkException.java new file mode 100644 index 00000000000..8ee16693c0d --- /dev/null +++ b/src/java.base/share/classes/java/nio/InvalidMarkException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when an attempt is made to reset a buffer + * when its mark is not defined. + * + * @since 1.4 + */ + +public class InvalidMarkException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 1698329710438510774L; + + /** + * Constructs an instance of this class. + */ + public InvalidMarkException() { } +} diff --git a/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java b/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java new file mode 100644 index 00000000000..e86123307f3 --- /dev/null +++ b/src/java.base/share/classes/java/nio/ReadOnlyBufferException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio; + +/** + * Unchecked exception thrown when a content-mutation method such as + * put or compact is invoked upon a read-only buffer. + * + * @since 1.4 + */ + +public class ReadOnlyBufferException + extends UnsupportedOperationException +{ + + @java.io.Serial + private static final long serialVersionUID = -1210063976496234090L; + + /** + * Constructs an instance of this class. + */ + public ReadOnlyBufferException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java b/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java new file mode 100644 index 00000000000..d02b3f289b9 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AcceptPendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to initiate an accept + * operation on a channel and a previous accept operation has not completed. + * + * @since 1.7 + */ + +public class AcceptPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2721339977965416421L; + + /** + * Constructs an instance of this class. + */ + public AcceptPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java b/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java new file mode 100644 index 00000000000..4602e8d1f46 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AlreadyBoundException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to bind the socket a + * network oriented channel that is already bound. + * + * @since 1.7 + */ + +public class AlreadyBoundException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 6796072983322737592L; + + /** + * Constructs an instance of this class. + */ + public AlreadyBoundException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java b/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java new file mode 100644 index 00000000000..48fe018a0b7 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AlreadyConnectedException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to connect a {@link + * SocketChannel} that is already connected. + * + * @since 1.4 + */ + +public class AlreadyConnectedException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -7331895245053773357L; + + /** + * Constructs an instance of this class. + */ + public AlreadyConnectedException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java b/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java new file mode 100644 index 00000000000..bc3ee02f50b --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/AsynchronousCloseException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when another thread closes the + * channel or the part of the channel upon which it is blocked in an I/O + * operation. + * + * @since 1.4 + */ + +public class AsynchronousCloseException + extends ClosedChannelException +{ + + @java.io.Serial + private static final long serialVersionUID = 6891178312432313966L; + + /** + * Constructs an instance of this class. + */ + public AsynchronousCloseException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java b/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java new file mode 100644 index 00000000000..946ea7205b1 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/CancelledKeyException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to use + * a selection key that is no longer valid. + * + * @since 1.4 + */ + +public class CancelledKeyException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -8438032138028814268L; + + /** + * Constructs an instance of this class. + */ + public CancelledKeyException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java b/src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java new file mode 100644 index 00000000000..c612458c9e4 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ClosedByInterruptException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when another thread interrupts it + * while it is blocked in an I/O operation upon a channel. Before this + * exception is thrown the channel will have been closed and the interrupt + * status of the previously-blocked thread will have been set. + * + * @since 1.4 + */ + +public class ClosedByInterruptException + extends AsynchronousCloseException +{ + + @java.io.Serial + private static final long serialVersionUID = -4488191543534286750L; + + /** + * Constructs an instance of this class. + */ + public ClosedByInterruptException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java b/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java new file mode 100644 index 00000000000..71452b6d1c2 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ClosedChannelException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception thrown when an attempt is made to invoke or complete an + * I/O operation upon channel that is closed, or at least closed to that + * operation. That this exception is thrown does not necessarily imply that + * the channel is completely closed. A socket channel whose write half has + * been shut down, for example, may still be open for reading. + * + * @since 1.4 + */ + +public class ClosedChannelException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = 882777185433553857L; + + /** + * Constructs an instance of this class. + */ + public ClosedChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java b/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java new file mode 100644 index 00000000000..dcefb98d69e --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ClosedSelectorException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a closed selector. + * + * @since 1.4 + */ + +public class ClosedSelectorException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 6466297122317847835L; + + /** + * Constructs an instance of this class. + */ + public ClosedSelectorException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java b/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java new file mode 100644 index 00000000000..2a5b80e3b25 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ConnectionPendingException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to connect a {@link + * SocketChannel} for which a non-blocking connection operation is already in + * progress. + * + * @since 1.4 + */ + +public class ConnectionPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2008393366501760879L; + + /** + * Constructs an instance of this class. + */ + public ConnectionPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java b/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java new file mode 100644 index 00000000000..7ecae1b4a46 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/FileLockInterruptionException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when another thread interrupts it + * while it is waiting to acquire a file lock. Before this exception is thrown + * the interrupt status of the previously-blocked thread will have been set. + * + * @since 1.4 + */ + +public class FileLockInterruptionException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = 7104080643653532383L; + + /** + * Constructs an instance of this class. + */ + public FileLockInterruptionException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java b/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java new file mode 100644 index 00000000000..28d1627ef34 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalBlockingModeException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when a blocking-mode-specific operation + * is invoked upon a channel in the incorrect blocking mode. + * + * @since 1.4 + */ + +public class IllegalBlockingModeException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3335774961855590474L; + + /** + * Constructs an instance of this class. + */ + public IllegalBlockingModeException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java b/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java new file mode 100644 index 00000000000..3e5da2b87c4 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalChannelGroupException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to open a channel + * in a group that was not created by the same provider. + * + * @since 1.7 + */ + +public class IllegalChannelGroupException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -2495041211157744253L; + + /** + * Constructs an instance of this class. + */ + public IllegalChannelGroupException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java b/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java new file mode 100644 index 00000000000..d1e26c3352f --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/IllegalSelectorException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to register a channel + * with a selector that was not created by the provider that created the + * channel. + * + * @since 1.4 + */ + +public class IllegalSelectorException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -8406323347253320987L; + + /** + * Constructs an instance of this class. + */ + public IllegalSelectorException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java b/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java new file mode 100644 index 00000000000..3dc6931e7fb --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/InterruptedByTimeoutException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Checked exception received by a thread when a timeout elapses before an + * asynchronous operation completes. + * + * @since 1.7 + */ + +public class InterruptedByTimeoutException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = -4268008601014042947L; + + /** + * Constructs an instance of this class. + */ + public InterruptedByTimeoutException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java b/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java new file mode 100644 index 00000000000..954486e4d35 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NoConnectionPendingException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when the {@link SocketChannel#finishConnect + * finishConnect} method of a {@link SocketChannel} is invoked without first + * successfully invoking its {@link SocketChannel#connect connect} method. + * + * @since 1.4 + */ + +public class NoConnectionPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -8296561183633134743L; + + /** + * Constructs an instance of this class. + */ + public NoConnectionPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java b/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java new file mode 100644 index 00000000000..4714ace0b30 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NonReadableChannelException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to read + * from a channel that was not originally opened for reading. + * + * @since 1.4 + */ + +public class NonReadableChannelException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3200915679294993514L; + + /** + * Constructs an instance of this class. + */ + public NonReadableChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java b/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java new file mode 100644 index 00000000000..70877bb46b9 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NonWritableChannelException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to write + * to a channel that was not originally opened for writing. + * + * @since 1.4 + */ + +public class NonWritableChannelException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -7071230488279011621L; + + /** + * Constructs an instance of this class. + */ + public NonWritableChannelException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java b/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java new file mode 100644 index 00000000000..12f222b7692 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NotYetBoundException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a server socket channel that is not yet bound. + * + * @since 1.4 + */ + +public class NotYetBoundException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 4640999303950202242L; + + /** + * Constructs an instance of this class. + */ + public NotYetBoundException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java b/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java new file mode 100644 index 00000000000..8ea6150de7d --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/NotYetConnectedException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke an I/O + * operation upon a socket channel that is not yet connected. + * + * @since 1.4 + */ + +public class NotYetConnectedException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 4697316551909513464L; + + /** + * Constructs an instance of this class. + */ + public NotYetConnectedException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java b/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java new file mode 100644 index 00000000000..a24019ffcd1 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/OverlappingFileLockException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to acquire a lock on a + * region of a file that overlaps a region already locked by the same Java + * virtual machine, or when another thread is already waiting to lock an + * overlapping region of the same file. + * + * @since 1.4 + */ + +public class OverlappingFileLockException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 2047812138163068433L; + + /** + * Constructs an instance of this class. + */ + public OverlappingFileLockException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ReadPendingException.java b/src/java.base/share/classes/java/nio/channels/ReadPendingException.java new file mode 100644 index 00000000000..113c3fb27a6 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ReadPendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to read from an + * asynchronous socket channel and a previous read has not completed. + * + * @since 1.7 + */ + +public class ReadPendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 1986315242191227217L; + + /** + * Constructs an instance of this class. + */ + public ReadPendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java b/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java new file mode 100644 index 00000000000..d25f6f568ae --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/ShutdownChannelGroupException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to construct a channel in + * a group that is shutdown or the completion handler for an I/O operation + * cannot be invoked because the channel group has terminated. + * + * @since 1.7 + */ + +public class ShutdownChannelGroupException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = -3903801676350154157L; + + /** + * Constructs an instance of this class. + */ + public ShutdownChannelGroupException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java b/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java new file mode 100644 index 00000000000..86ae3e6d7a6 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/UnresolvedAddressException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to invoke a network + * operation upon an unresolved socket address. + * + * @since 1.4 + */ + +public class UnresolvedAddressException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 6136959093620794148L; + + /** + * Constructs an instance of this class. + */ + public UnresolvedAddressException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java b/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java new file mode 100644 index 00000000000..0b63cd8d418 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/UnsupportedAddressTypeException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to bind or connect + * to a socket address of a type that is not supported. + * + * @since 1.4 + */ + +public class UnsupportedAddressTypeException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = -2964323842829700493L; + + /** + * Constructs an instance of this class. + */ + public UnsupportedAddressTypeException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/WritePendingException.java b/src/java.base/share/classes/java/nio/channels/WritePendingException.java new file mode 100644 index 00000000000..f4dca987247 --- /dev/null +++ b/src/java.base/share/classes/java/nio/channels/WritePendingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.channels; + +/** + * Unchecked exception thrown when an attempt is made to write to an + * asynchronous socket channel and a previous write has not completed. + * + * @since 1.7 + */ + +public class WritePendingException + extends IllegalStateException +{ + + @java.io.Serial + private static final long serialVersionUID = 7031871839266032276L; + + /** + * Constructs an instance of this class. + */ + public WritePendingException() { } +} diff --git a/src/java.base/share/classes/java/nio/channels/exceptions b/src/java.base/share/classes/java/nio/channels/exceptions deleted file mode 100644 index 3f75fb3fcd2..00000000000 --- a/src/java.base/share/classes/java/nio/channels/exceptions +++ /dev/null @@ -1,194 +0,0 @@ -# -# Copyright (c) 2000, 2009, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Generated exception classes for java.nio.channels - -SINCE=1.4 -PACKAGE=java.nio.channels -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2007," - - -SUPER=java.io.IOException - -gen ClosedChannelException " - * Checked exception thrown when an attempt is made to invoke or complete an - * I/O operation upon channel that is closed, or at least closed to that - * operation. That this exception is thrown does not necessarily imply that - * the channel is completely closed. A socket channel whose write half has - * been shut down, for example, may still be open for reading." \ - 882777185433553857L - -gen FileLockInterruptionException " - * Checked exception received by a thread when another thread interrupts it - * while it is waiting to acquire a file lock. Before this exception is thrown - * the interrupt status of the previously-blocked thread will have been set." \ - 7104080643653532383L - - -SUPER=ClosedChannelException - -gen AsynchronousCloseException " - * Checked exception received by a thread when another thread closes the - * channel or the part of the channel upon which it is blocked in an I/O - * operation." \ - 6891178312432313966L - - -SUPER=AsynchronousCloseException - -gen ClosedByInterruptException " - * Checked exception received by a thread when another thread interrupts it - * while it is blocked in an I/O operation upon a channel. Before this - * exception is thrown the channel will have been closed and the interrupt - * status of the previously-blocked thread will have been set." \ - -4488191543534286750L - - -SUPER=IllegalArgumentException - -gen IllegalSelectorException " - * Unchecked exception thrown when an attempt is made to register a channel - * with a selector that was not created by the provider that created the - * channel." \ - -8406323347253320987L - -gen UnresolvedAddressException " - * Unchecked exception thrown when an attempt is made to invoke a network - * operation upon an unresolved socket address." \ - 6136959093620794148L - -gen UnsupportedAddressTypeException " - * Unchecked exception thrown when an attempt is made to bind or connect - * to a socket address of a type that is not supported." \ - -2964323842829700493L - - -SUPER=IllegalStateException - -gen AlreadyConnectedException " - * Unchecked exception thrown when an attempt is made to connect a {@link - * SocketChannel} that is already connected." \ - -7331895245053773357L - -gen ConnectionPendingException " - * Unchecked exception thrown when an attempt is made to connect a {@link - * SocketChannel} for which a non-blocking connection operation is already in - * progress." \ - 2008393366501760879L - -gen ClosedSelectorException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a closed selector." \ - 6466297122317847835L - -gen CancelledKeyException " - * Unchecked exception thrown when an attempt is made to use - * a selection key that is no longer valid." \ - -8438032138028814268L - -gen IllegalBlockingModeException " - * Unchecked exception thrown when a blocking-mode-specific operation - * is invoked upon a channel in the incorrect blocking mode." \ - -3335774961855590474L - -gen NoConnectionPendingException " - * Unchecked exception thrown when the {@link SocketChannel#finishConnect - * finishConnect} method of a {@link SocketChannel} is invoked without first - * successfully invoking its {@link SocketChannel#connect connect} method." \ - -8296561183633134743L - -gen NonReadableChannelException " - * Unchecked exception thrown when an attempt is made to read - * from a channel that was not originally opened for reading." \ - -3200915679294993514L - -gen NonWritableChannelException " - * Unchecked exception thrown when an attempt is made to write - * to a channel that was not originally opened for writing." \ - -7071230488279011621L - -gen NotYetBoundException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a server socket channel that is not yet bound." \ - 4640999303950202242L - -gen NotYetConnectedException " - * Unchecked exception thrown when an attempt is made to invoke an I/O - * operation upon a socket channel that is not yet connected." \ - 4697316551909513464L - -gen OverlappingFileLockException " - * Unchecked exception thrown when an attempt is made to acquire a lock on a - * region of a file that overlaps a region already locked by the same Java - * virtual machine, or when another thread is already waiting to lock an - * overlapping region of the same file." \ - 2047812138163068433L - - -SINCE=1.7 - -SUPER=java.io.IOException - -gen InterruptedByTimeoutException " - * Checked exception received by a thread when a timeout elapses before an - * asynchronous operation completes." \ - -4268008601014042947L - -SUPER=IllegalArgumentException - -gen IllegalChannelGroupException " - * Unchecked exception thrown when an attempt is made to open a channel - * in a group that was not created by the same provider. " \ - -2495041211157744253L - - -SUPER=IllegalStateException - -gen AlreadyBoundException " - * Unchecked exception thrown when an attempt is made to bind the socket a - * network oriented channel that is already bound." \ - 6796072983322737592L - -gen AcceptPendingException " - * Unchecked exception thrown when an attempt is made to initiate an accept - * operation on a channel and a previous accept operation has not completed." \ - 2721339977965416421L - -gen ReadPendingException " - * Unchecked exception thrown when an attempt is made to read from an - * asynchronous socket channel and a previous read has not completed." \ - 1986315242191227217L - -gen WritePendingException " - * Unchecked exception thrown when an attempt is made to write to an - * asynchronous socket channel and a previous write has not completed." \ - 7031871839266032276L - -gen ShutdownChannelGroupException " - * Unchecked exception thrown when an attempt is made to construct a channel in - * a group that is shutdown or the completion handler for an I/O operation - * cannot be invoked because the channel group has terminated." \ - -3903801676350154157L diff --git a/src/java.base/share/classes/java/nio/charset/CharacterCodingException.java b/src/java.base/share/classes/java/nio/charset/CharacterCodingException.java new file mode 100644 index 00000000000..00a8efae4dd --- /dev/null +++ b/src/java.base/share/classes/java/nio/charset/CharacterCodingException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.charset; + +/** + * Checked exception thrown when a character encoding + * or decoding error occurs. + * + * @since 1.4 + */ + +public class CharacterCodingException + extends java.io.IOException +{ + + @java.io.Serial + private static final long serialVersionUID = 8421532232154627783L; + + /** + * Constructs an instance of this class. + */ + public CharacterCodingException() { } +} diff --git a/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java b/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java new file mode 100644 index 00000000000..992168883bb --- /dev/null +++ b/src/java.base/share/classes/java/nio/charset/IllegalCharsetNameException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.charset; + +/** + * Unchecked exception thrown when a string that is not a + * legal charset name is used as such. + * + * @since 1.4 + */ + +public class IllegalCharsetNameException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 1457525358470002989L; + + /** + * The illegal charset name. + * + * @serial + */ + private String charsetName; + + /** + * Constructs an instance of this class. + * + * @param charsetName + * The illegal charset name + */ + public IllegalCharsetNameException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /** + * Retrieves the illegal charset name. + * + * @return The illegal charset name + */ + public String getCharsetName() { + return charsetName; + } +} diff --git a/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java b/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java new file mode 100644 index 00000000000..da50e0cfd0c --- /dev/null +++ b/src/java.base/share/classes/java/nio/charset/UnsupportedCharsetException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.charset; + +/** + * Unchecked exception thrown when no support is available + * for a requested charset. + * + * @since 1.4 + */ + +public class UnsupportedCharsetException + extends IllegalArgumentException +{ + + @java.io.Serial + private static final long serialVersionUID = 1490765524727386367L; + + /** + * The name of the unsupported charset. + * + * @serial + */ + private String charsetName; + + /** + * Constructs an instance of this class. + * + * @param charsetName + * The name of the unsupported charset + */ + public UnsupportedCharsetException(String charsetName) { + super(String.valueOf(charsetName)); + this.charsetName = charsetName; + } + + /** + * Retrieves the name of the unsupported charset. + * + * @return The name of the unsupported charset + */ + public String getCharsetName() { + return charsetName; + } +} diff --git a/src/java.base/share/classes/java/nio/charset/exceptions b/src/java.base/share/classes/java/nio/charset/exceptions deleted file mode 100644 index c4773090ae0..00000000000 --- a/src/java.base/share/classes/java/nio/charset/exceptions +++ /dev/null @@ -1,53 +0,0 @@ -# -# Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Generated exception classes for java.nio.charset - -SINCE=1.4 -PACKAGE=java.nio.charset -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2021," - -SUPER=java.io.IOException - -gen CharacterCodingException " - * Checked exception thrown when a character encoding - * or decoding error occurs." \ - 8421532232154627783L - - -SUPER=IllegalArgumentException - -gen IllegalCharsetNameException " - * Unchecked exception thrown when a string that is not a - * legal charset name is used as such." \ - 1457525358470002989L \ - String charsetName CharsetName "illegal charset name" - -gen UnsupportedCharsetException " - * Unchecked exception thrown when no support is available - * for a requested charset." \ - 1490765524727386367L \ - String charsetName CharsetName "name of the unsupported charset" diff --git a/src/java.base/share/classes/java/nio/exceptions b/src/java.base/share/classes/java/nio/exceptions deleted file mode 100644 index 7465cdbed78..00000000000 --- a/src/java.base/share/classes/java/nio/exceptions +++ /dev/null @@ -1,60 +0,0 @@ -# -# Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -# Generated exception classes for java.nio - -SINCE=1.4 -PACKAGE=java.nio -# This year should only change if the generated source is modified. -COPYRIGHT_YEARS="2000, 2021," - - -SUPER=RuntimeException - -gen BufferOverflowException " - * Unchecked exception thrown when a relative put operation reaches - * the target buffer's limit." \ - -5484897634319144535L - -gen BufferUnderflowException " - * Unchecked exception thrown when a relative get operation reaches - * the source buffer's limit." \ - -1713313658691622206L - - -SUPER=IllegalStateException - -gen InvalidMarkException " - * Unchecked exception thrown when an attempt is made to reset a buffer - * when its mark is not defined." \ - 1698329710438510774L - - -SUPER=UnsupportedOperationException - -gen ReadOnlyBufferException " - * Unchecked exception thrown when a content-mutation method such as - * put or compact is invoked upon a read-only buffer." \ - -1210063976496234090L From 1b9a11682d5f73885213822423bfce8dfc17febd Mon Sep 17 00:00:00 2001 From: Francisco Ferrari Bihurriet Date: Fri, 19 Sep 2025 10:23:04 +0000 Subject: [PATCH 122/556] 8367782: VerifyJarEntryName.java: Fix modifyJarEntryName to operate on bytes and re-introduce verifySignatureEntryName Reviewed-by: hchao --- .../tools/jarsigner/VerifyJarEntryName.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java b/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java index f5589484f3d..e2554ee0f91 100644 --- a/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java +++ b/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java @@ -38,12 +38,13 @@ import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import jdk.test.lib.SecurityTools; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class VerifyJarEntryName { @@ -85,7 +86,7 @@ public class VerifyJarEntryName { */ @Test void verifyManifestEntryName() throws Exception { - modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "MANIFEST.MF"); + modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "META-INF/MANIFEST.MF"); SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR) .shouldContain("This JAR file contains internal " + "inconsistencies that may result in different " + @@ -95,6 +96,22 @@ public class VerifyJarEntryName { .shouldHaveExitValue(0); } + /* + * Modify a single byte in signature filename in LOC, and + * validate that jarsigner -verify emits a warning message. + */ + @Test + void verifySignatureEntryName() throws Exception { + modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "META-INF/MYKEY.SF"); + SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR) + .shouldContain("This JAR file contains internal " + + "inconsistencies that may result in different " + + "contents when reading via JarFile and JarInputStream:") + .shouldContain("- Entry XETA-INF/MYKEY.SF is present when reading " + + "via JarInputStream but missing when reading via JarFile") + .shouldHaveExitValue(0); + } + /* * Validate that jarsigner -verify on a valid JAR works without * emitting warnings about internal inconsistencies. @@ -111,9 +128,14 @@ public class VerifyJarEntryName { private void modifyJarEntryName(Path origJar, Path modifiedJar, String entryName) throws Exception { byte[] jarBytes = Files.readAllBytes(origJar); - var jarString = new String(jarBytes, StandardCharsets.UTF_8); - var pos = jarString.indexOf(entryName); - assertTrue(pos != -1, entryName + " is not present in the JAR"); + byte[] entryNameBytes = entryName.getBytes(StandardCharsets.UTF_8); + int pos = 0; + try { + while (!Arrays.equals(jarBytes, pos, pos + entryNameBytes.length, + entryNameBytes, 0, entryNameBytes.length)) pos++; + } catch (ArrayIndexOutOfBoundsException ignore) { + fail(entryName + " is not present in the JAR"); + } jarBytes[pos] = 'X'; Files.write(modifiedJar, jarBytes); } From 2bff4174e58e11ba78013bef8417334ff44fbb5c Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Fri, 19 Sep 2025 11:48:10 +0000 Subject: [PATCH 123/556] 8367759: G1: Move G1UpdateRegionLivenessAndSelectForRebuildTask into its own file Reviewed-by: ayang, iwalulya --- src/hotspot/share/gc/g1/g1ConcurrentMark.cpp | 188 ++---------------- .../gc/g1/g1ConcurrentMarkRemarkTasks.cpp | 173 ++++++++++++++++ .../gc/g1/g1ConcurrentMarkRemarkTasks.hpp | 66 ++++++ 3 files changed, 254 insertions(+), 173 deletions(-) create mode 100644 src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp create mode 100644 src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.hpp diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp index 3d95541ae3c..a7dc0a7f33f 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp @@ -31,6 +31,7 @@ #include "gc/g1/g1CollectionSetChooser.hpp" #include "gc/g1/g1CollectorState.hpp" #include "gc/g1/g1ConcurrentMark.inline.hpp" +#include "gc/g1/g1ConcurrentMarkRemarkTasks.hpp" #include "gc/g1/g1ConcurrentMarkThread.inline.hpp" #include "gc/g1/g1ConcurrentRebuildAndScrub.hpp" #include "gc/g1/g1DirtyCardQueue.hpp" @@ -1186,179 +1187,6 @@ void G1ConcurrentMark::verify_during_pause(G1HeapVerifier::G1VerifyType type, } } -// Update per-region liveness info based on CM stats. Then, reclaim empty -// regions right away and select certain regions (e.g. sparse ones) for remset -// rebuild. -class G1UpdateRegionLivenessAndSelectForRebuildTask : public WorkerTask { - G1CollectedHeap* _g1h; - G1ConcurrentMark* _cm; - G1HeapRegionClaimer _hrclaimer; - - uint volatile _total_selected_for_rebuild; - - // Reclaimed empty regions - G1FreeRegionList _cleanup_list; - - struct G1OnRegionClosure : public G1HeapRegionClosure { - G1CollectedHeap* _g1h; - G1ConcurrentMark* _cm; - // The number of regions actually selected for rebuild. - uint _num_selected_for_rebuild; - - size_t _freed_bytes; - uint _num_old_regions_removed; - uint _num_humongous_regions_removed; - G1FreeRegionList* _local_cleanup_list; - - G1OnRegionClosure(G1CollectedHeap* g1h, - G1ConcurrentMark* cm, - G1FreeRegionList* local_cleanup_list) : - _g1h(g1h), - _cm(cm), - _num_selected_for_rebuild(0), - _freed_bytes(0), - _num_old_regions_removed(0), - _num_humongous_regions_removed(0), - _local_cleanup_list(local_cleanup_list) {} - - void reclaim_empty_region(G1HeapRegion* hr) { - assert(!hr->has_pinned_objects(), "precondition"); - assert(hr->used() > 0, "precondition"); - - _freed_bytes += hr->used(); - hr->set_containing_set(nullptr); - hr->clear_cardtable(); - _cm->clear_statistics(hr); - G1HeapRegionPrinter::mark_reclaim(hr); - } - - void reclaim_empty_humongous_region(G1HeapRegion* hr) { - assert(hr->is_starts_humongous(), "precondition"); - - auto on_humongous_region = [&] (G1HeapRegion* hr) { - assert(hr->is_humongous(), "precondition"); - - reclaim_empty_region(hr); - _num_humongous_regions_removed++; - _g1h->free_humongous_region(hr, _local_cleanup_list); - }; - - _g1h->humongous_obj_regions_iterate(hr, on_humongous_region); - } - - void reclaim_empty_old_region(G1HeapRegion* hr) { - assert(hr->is_old(), "precondition"); - - reclaim_empty_region(hr); - _num_old_regions_removed++; - _g1h->free_region(hr, _local_cleanup_list); - } - - bool do_heap_region(G1HeapRegion* hr) override { - G1RemSetTrackingPolicy* tracker = _g1h->policy()->remset_tracker(); - if (hr->is_starts_humongous()) { - // The liveness of this humongous obj decided by either its allocation - // time (allocated after conc-mark-start, i.e. live) or conc-marking. - const bool is_live = _cm->top_at_mark_start(hr) == hr->bottom() - || _cm->contains_live_object(hr->hrm_index()) - || hr->has_pinned_objects(); - if (is_live) { - const bool selected_for_rebuild = tracker->update_humongous_before_rebuild(hr); - auto on_humongous_region = [&] (G1HeapRegion* hr) { - if (selected_for_rebuild) { - _num_selected_for_rebuild++; - } - _cm->update_top_at_rebuild_start(hr); - }; - - _g1h->humongous_obj_regions_iterate(hr, on_humongous_region); - } else { - reclaim_empty_humongous_region(hr); - } - } else if (hr->is_old()) { - uint region_idx = hr->hrm_index(); - hr->note_end_of_marking(_cm->top_at_mark_start(hr), _cm->live_bytes(region_idx), _cm->incoming_refs(region_idx)); - - const bool is_live = hr->live_bytes() != 0 - || hr->has_pinned_objects(); - if (is_live) { - const bool selected_for_rebuild = tracker->update_old_before_rebuild(hr); - if (selected_for_rebuild) { - _num_selected_for_rebuild++; - } - _cm->update_top_at_rebuild_start(hr); - } else { - reclaim_empty_old_region(hr); - } - } - - return false; - } - }; - -public: - G1UpdateRegionLivenessAndSelectForRebuildTask(G1CollectedHeap* g1h, - G1ConcurrentMark* cm, - uint num_workers) : - WorkerTask("G1 Update Region Liveness and Select For Rebuild"), - _g1h(g1h), - _cm(cm), - _hrclaimer(num_workers), - _total_selected_for_rebuild(0), - _cleanup_list("Empty Regions After Mark List") {} - - ~G1UpdateRegionLivenessAndSelectForRebuildTask() { - if (!_cleanup_list.is_empty()) { - log_debug(gc)("Reclaimed %u empty regions", _cleanup_list.length()); - // And actually make them available. - _g1h->prepend_to_freelist(&_cleanup_list); - } - } - - void work(uint worker_id) override { - G1FreeRegionList local_cleanup_list("Local Cleanup List"); - G1OnRegionClosure on_region_cl(_g1h, _cm, &local_cleanup_list); - _g1h->heap_region_par_iterate_from_worker_offset(&on_region_cl, &_hrclaimer, worker_id); - - AtomicAccess::add(&_total_selected_for_rebuild, on_region_cl._num_selected_for_rebuild); - - // Update the old/humongous region sets - _g1h->remove_from_old_gen_sets(on_region_cl._num_old_regions_removed, - on_region_cl._num_humongous_regions_removed); - - { - MutexLocker x(G1RareEvent_lock, Mutex::_no_safepoint_check_flag); - _g1h->decrement_summary_bytes(on_region_cl._freed_bytes); - - _cleanup_list.add_ordered(&local_cleanup_list); - assert(local_cleanup_list.is_empty(), "post-condition"); - } - } - - uint total_selected_for_rebuild() const { return _total_selected_for_rebuild; } - - static uint desired_num_workers(uint num_regions) { - const uint num_regions_per_worker = 384; - return (num_regions + num_regions_per_worker - 1) / num_regions_per_worker; - } -}; - -class G1UpdateRegionsAfterRebuild : public G1HeapRegionClosure { - G1CollectedHeap* _g1h; - -public: - G1UpdateRegionsAfterRebuild(G1CollectedHeap* g1h) : - _g1h(g1h) { - } - - virtual bool do_heap_region(G1HeapRegion* r) { - // Update the remset tracking state from updating to complete - // if remembered sets have been rebuilt. - _g1h->policy()->remset_tracker()->update_after_rebuild(r); - return false; - } -}; - class G1ObjectCountIsAliveClosure: public BoolObjectClosure { G1CollectedHeap* _g1h; public: @@ -1506,6 +1334,20 @@ void G1ConcurrentMark::compute_new_sizes() { _g1h->monitoring_support()->update_sizes(); } +class G1UpdateRegionsAfterRebuild : public G1HeapRegionClosure { + G1CollectedHeap* _g1h; + +public: + G1UpdateRegionsAfterRebuild(G1CollectedHeap* g1h) : _g1h(g1h) { } + + bool do_heap_region(G1HeapRegion* r) override { + // Update the remset tracking state from updating to complete + // if remembered sets have been rebuilt. + _g1h->policy()->remset_tracker()->update_after_rebuild(r); + return false; + } +}; + void G1ConcurrentMark::cleanup() { assert_at_safepoint_on_vm_thread(); diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp new file mode 100644 index 00000000000..02afc443d68 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "gc/g1/g1CollectedHeap.inline.hpp" +#include "gc/g1/g1ConcurrentMark.inline.hpp" +#include "gc/g1/g1ConcurrentMarkRemarkTasks.hpp" +#include "gc/g1/g1HeapRegion.inline.hpp" +#include "gc/g1/g1HeapRegionPrinter.hpp" +#include "gc/g1/g1RemSetTrackingPolicy.hpp" +#include "logging/log.hpp" +#include "runtime/atomicAccess.hpp" +#include "runtime/mutexLocker.hpp" + +struct G1UpdateRegionLivenessAndSelectForRebuildTask::G1OnRegionClosure : public G1HeapRegionClosure { + G1CollectedHeap* _g1h; + G1ConcurrentMark* _cm; + // The number of regions actually selected for rebuild. + uint _num_selected_for_rebuild; + + size_t _freed_bytes; + uint _num_old_regions_removed; + uint _num_humongous_regions_removed; + G1FreeRegionList* _local_cleanup_list; + + G1OnRegionClosure(G1CollectedHeap* g1h, + G1ConcurrentMark* cm, + G1FreeRegionList* local_cleanup_list) : + _g1h(g1h), + _cm(cm), + _num_selected_for_rebuild(0), + _freed_bytes(0), + _num_old_regions_removed(0), + _num_humongous_regions_removed(0), + _local_cleanup_list(local_cleanup_list) {} + + void reclaim_empty_region(G1HeapRegion* hr) { + assert(!hr->has_pinned_objects(), "precondition"); + assert(hr->used() > 0, "precondition"); + + _freed_bytes += hr->used(); + hr->set_containing_set(nullptr); + hr->clear_cardtable(); + _cm->clear_statistics(hr); + G1HeapRegionPrinter::mark_reclaim(hr); + } + + void reclaim_empty_humongous_region(G1HeapRegion* hr) { + assert(hr->is_starts_humongous(), "precondition"); + + auto on_humongous_region = [&] (G1HeapRegion* hr) { + assert(hr->is_humongous(), "precondition"); + + reclaim_empty_region(hr); + _num_humongous_regions_removed++; + _g1h->free_humongous_region(hr, _local_cleanup_list); + }; + + _g1h->humongous_obj_regions_iterate(hr, on_humongous_region); + } + + void reclaim_empty_old_region(G1HeapRegion* hr) { + assert(hr->is_old(), "precondition"); + + reclaim_empty_region(hr); + _num_old_regions_removed++; + _g1h->free_region(hr, _local_cleanup_list); + } + + bool do_heap_region(G1HeapRegion* hr) override { + G1RemSetTrackingPolicy* tracker = _g1h->policy()->remset_tracker(); + if (hr->is_starts_humongous()) { + // The liveness of this humongous obj decided by either its allocation + // time (allocated after conc-mark-start, i.e. live) or conc-marking. + const bool is_live = _cm->top_at_mark_start(hr) == hr->bottom() + || _cm->contains_live_object(hr->hrm_index()) + || hr->has_pinned_objects(); + if (is_live) { + const bool selected_for_rebuild = tracker->update_humongous_before_rebuild(hr); + auto on_humongous_region = [&] (G1HeapRegion* hr) { + if (selected_for_rebuild) { + _num_selected_for_rebuild++; + } + _cm->update_top_at_rebuild_start(hr); + }; + + _g1h->humongous_obj_regions_iterate(hr, on_humongous_region); + } else { + reclaim_empty_humongous_region(hr); + } + } else if (hr->is_old()) { + uint region_idx = hr->hrm_index(); + hr->note_end_of_marking(_cm->top_at_mark_start(hr), _cm->live_bytes(region_idx), _cm->incoming_refs(region_idx)); + + const bool is_live = hr->live_bytes() != 0 + || hr->has_pinned_objects(); + if (is_live) { + const bool selected_for_rebuild = tracker->update_old_before_rebuild(hr); + if (selected_for_rebuild) { + _num_selected_for_rebuild++; + } + _cm->update_top_at_rebuild_start(hr); + } else { + reclaim_empty_old_region(hr); + } + } + + return false; + } +}; + +G1UpdateRegionLivenessAndSelectForRebuildTask::G1UpdateRegionLivenessAndSelectForRebuildTask(G1CollectedHeap* g1h, + G1ConcurrentMark* cm, + uint num_workers) : + WorkerTask("G1 Update Region Liveness and Select For Rebuild"), + _g1h(g1h), + _cm(cm), + _hrclaimer(num_workers), + _total_selected_for_rebuild(0), + _cleanup_list("Empty Regions After Mark List") {} + +G1UpdateRegionLivenessAndSelectForRebuildTask::~G1UpdateRegionLivenessAndSelectForRebuildTask() { + if (!_cleanup_list.is_empty()) { + log_debug(gc)("Reclaimed %u empty regions", _cleanup_list.length()); + // And actually make them available. + _g1h->prepend_to_freelist(&_cleanup_list); + } +} + +void G1UpdateRegionLivenessAndSelectForRebuildTask::work(uint worker_id) { + G1FreeRegionList local_cleanup_list("Local Cleanup List"); + G1OnRegionClosure on_region_cl(_g1h, _cm, &local_cleanup_list); + _g1h->heap_region_par_iterate_from_worker_offset(&on_region_cl, &_hrclaimer, worker_id); + + AtomicAccess::add(&_total_selected_for_rebuild, on_region_cl._num_selected_for_rebuild); + + // Update the old/humongous region sets + _g1h->remove_from_old_gen_sets(on_region_cl._num_old_regions_removed, + on_region_cl._num_humongous_regions_removed); + + { + MutexLocker x(G1RareEvent_lock, Mutex::_no_safepoint_check_flag); + _g1h->decrement_summary_bytes(on_region_cl._freed_bytes); + + _cleanup_list.add_ordered(&local_cleanup_list); + assert(local_cleanup_list.is_empty(), "post-condition"); + } +} + +uint G1UpdateRegionLivenessAndSelectForRebuildTask::desired_num_workers(uint num_regions) { + const uint num_regions_per_worker = 384; + return (num_regions + num_regions_per_worker - 1) / num_regions_per_worker; +} diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.hpp b/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.hpp new file mode 100644 index 00000000000..161f0b4b9f5 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#ifndef SHARE_GC_G1_G1CONCURRENTMARKREMARKTASKS_HPP +#define SHARE_GC_G1_G1CONCURRENTMARKREMARKTASKS_HPP + +#include "gc/g1/g1HeapRegion.hpp" +#include "gc/g1/g1HeapRegionManager.hpp" +#include "gc/g1/g1HeapRegionSet.hpp" +#include "gc/shared/workerThread.hpp" + +class G1CollectedHeap; +class G1ConcurrentMark; + +// Update per-region liveness info based on CM stats. Then, reclaim empty +// regions right away and select certain regions (e.g. sparse ones) for remset +// rebuild. +class G1UpdateRegionLivenessAndSelectForRebuildTask : public WorkerTask { + G1CollectedHeap* _g1h; + G1ConcurrentMark* _cm; + G1HeapRegionClaimer _hrclaimer; + + uint volatile _total_selected_for_rebuild; + + // Reclaimed empty regions + G1FreeRegionList _cleanup_list; + + struct G1OnRegionClosure; + +public: + G1UpdateRegionLivenessAndSelectForRebuildTask(G1CollectedHeap* g1h, + G1ConcurrentMark* cm, + uint num_workers); + + ~G1UpdateRegionLivenessAndSelectForRebuildTask(); + + void work(uint worker_id) override; + + uint total_selected_for_rebuild() const { return _total_selected_for_rebuild; } + + static uint desired_num_workers(uint num_regions); +}; + +#endif /* SHARE_GC_G1_G1CONCURRENTMARKREMARKTASKS_HPP */ + From fa00b24954d63abed0093b696e5971c1918eec4d Mon Sep 17 00:00:00 2001 From: Coleen Phillimore Date: Fri, 19 Sep 2025 11:54:34 +0000 Subject: [PATCH 124/556] 8365823: Revert storing abstract and interface Klasses to non-class metaspace Reviewed-by: kvn, shade, stuefe --- src/hotspot/share/ci/ciKlass.hpp | 2 +- .../share/classfile/classFileParser.cpp | 9 --------- .../share/classfile/classFileParser.hpp | 5 ----- .../share/classfile/systemDictionaryShared.cpp | 2 +- .../types/traceid/jfrTraceIdKlassQueue.cpp | 13 ++++++------- src/hotspot/share/memory/allocation.cpp | 4 ++-- src/hotspot/share/memory/metaspace.cpp | 10 +++++----- src/hotspot/share/memory/metaspace.hpp | 4 ++-- src/hotspot/share/oops/array.inline.hpp | 6 +++--- src/hotspot/share/oops/arrayKlass.cpp | 4 ---- src/hotspot/share/oops/arrayKlass.hpp | 4 +--- src/hotspot/share/oops/instanceKlass.cpp | 18 ++++++------------ src/hotspot/share/oops/instanceKlass.hpp | 2 -- src/hotspot/share/oops/klass.cpp | 15 ++++++++------- src/hotspot/share/oops/klass.hpp | 6 ++---- src/hotspot/share/oops/klass.inline.hpp | 9 --------- src/hotspot/share/runtime/globals.hpp | 3 --- .../hotspot/HotSpotMetaspaceConstantImpl.java | 17 +---------------- .../jdk/vm/ci/hotspot/HotSpotVMConfig.java | 2 -- 19 files changed, 38 insertions(+), 97 deletions(-) diff --git a/src/hotspot/share/ci/ciKlass.hpp b/src/hotspot/share/ci/ciKlass.hpp index 37091471a2a..8d03b910de5 100644 --- a/src/hotspot/share/ci/ciKlass.hpp +++ b/src/hotspot/share/ci/ciKlass.hpp @@ -107,7 +107,7 @@ public: bool is_in_encoding_range() { Klass* k = get_Klass(); bool is_in_encoding_range = CompressedKlassPointers::is_encodable(k); - assert(is_in_encoding_range || k->is_interface() || k->is_abstract(), "sanity"); + assert(is_in_encoding_range, "sanity"); return is_in_encoding_range; } diff --git a/src/hotspot/share/classfile/classFileParser.cpp b/src/hotspot/share/classfile/classFileParser.cpp index 852d23cbc2e..11633c8cb11 100644 --- a/src/hotspot/share/classfile/classFileParser.cpp +++ b/src/hotspot/share/classfile/classFileParser.cpp @@ -5929,15 +5929,6 @@ bool ClassFileParser::is_java_lang_ref_Reference_subclass() const { return _super_klass->reference_type() != REF_NONE; } -// Returns true if the future Klass will need to be addressable with a narrow Klass ID. -bool ClassFileParser::klass_needs_narrow_id() const { - // Classes that are never instantiated need no narrow Klass Id, since the - // only point of having a narrow id is to put it into an object header. Keeping - // never instantiated classes out of class space lessens the class space pressure. - // For more details, see JDK-8338526. - return !is_interface() && !is_abstract(); -} - // ---------------------------------------------------------------------------- // debugging diff --git a/src/hotspot/share/classfile/classFileParser.hpp b/src/hotspot/share/classfile/classFileParser.hpp index 52e966f6260..5d4236132f1 100644 --- a/src/hotspot/share/classfile/classFileParser.hpp +++ b/src/hotspot/share/classfile/classFileParser.hpp @@ -515,11 +515,6 @@ class ClassFileParser { bool is_hidden() const { return _is_hidden; } bool is_interface() const { return _access_flags.is_interface(); } - bool is_abstract() const { return _access_flags.is_abstract(); } - - // Returns true if the Klass to be generated will need to be addressable - // with a narrow Klass ID. - bool klass_needs_narrow_id() const; ClassLoaderData* loader_data() const { return _loader_data; } const Symbol* class_name() const { return _class_name; } diff --git a/src/hotspot/share/classfile/systemDictionaryShared.cpp b/src/hotspot/share/classfile/systemDictionaryShared.cpp index 513ebe8bb84..04c2d7ffb84 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.cpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.cpp @@ -89,7 +89,7 @@ DEBUG_ONLY(bool SystemDictionaryShared::_class_loading_may_happen = true;) #ifdef ASSERT static void check_klass_after_loading(const Klass* k) { #ifdef _LP64 - if (k != nullptr && UseCompressedClassPointers && k->needs_narrow_id()) { + if (k != nullptr && UseCompressedClassPointers) { CompressedKlassPointers::check_encodable(k); } #endif diff --git a/src/hotspot/share/jfr/recorder/checkpoint/types/traceid/jfrTraceIdKlassQueue.cpp b/src/hotspot/share/jfr/recorder/checkpoint/types/traceid/jfrTraceIdKlassQueue.cpp index e821b528707..9c57374d6c6 100644 --- a/src/hotspot/share/jfr/recorder/checkpoint/types/traceid/jfrTraceIdKlassQueue.cpp +++ b/src/hotspot/share/jfr/recorder/checkpoint/types/traceid/jfrTraceIdKlassQueue.cpp @@ -29,6 +29,7 @@ #include "jfr/support/jfrThreadLocal.hpp" #include "jfr/utilities/jfrEpochQueue.inline.hpp" #include "jfr/utilities/jfrTypes.hpp" +#include "memory/metaspace.hpp" #include "oops/compressedKlass.inline.hpp" #include "utilities/macros.hpp" @@ -73,14 +74,13 @@ static size_t element_size(bool compressed) { return compressed ? NARROW_ELEMENT_SIZE : ELEMENT_SIZE; } -static bool can_compress_element(const Klass* klass) { - return CompressedKlassPointers::is_encodable(klass) && - JfrTraceId::load_raw(klass) < uncompressed_threshold; +static bool can_compress_element(traceid id) { + return Metaspace::using_class_space() && id < uncompressed_threshold; } static size_t element_size(const Klass* klass) { assert(klass != nullptr, "invariant"); - return element_size(can_compress_element(klass)); + return element_size(can_compress_element(JfrTraceId::load_raw(klass))); } static bool is_unloaded(traceid id, bool previous_epoch) { @@ -136,8 +136,7 @@ static inline void store_traceid(JfrEpochQueueNarrowKlassElement* element, trace } static void store_compressed_element(traceid id, const Klass* klass, u1* pos) { - assert(can_compress_element(klass), "invariant"); - assert(id == JfrTraceId::load_raw(klass), "invariant"); + assert(can_compress_element(id), "invariant"); JfrEpochQueueNarrowKlassElement* const element = new (pos) JfrEpochQueueNarrowKlassElement(); store_traceid(element, id); element->compressed_klass = encode(klass); @@ -153,7 +152,7 @@ static void store_element(const Klass* klass, u1* pos) { assert(pos != nullptr, "invariant"); assert(klass != nullptr, "invariant"); const traceid id = JfrTraceId::load_raw(klass); - if (can_compress_element(klass)) { + if (can_compress_element(id)) { store_compressed_element(id, klass, pos); return; } diff --git a/src/hotspot/share/memory/allocation.cpp b/src/hotspot/share/memory/allocation.cpp index f158fefdba0..9df91225d6f 100644 --- a/src/hotspot/share/memory/allocation.cpp +++ b/src/hotspot/share/memory/allocation.cpp @@ -73,7 +73,7 @@ void* MetaspaceObj::operator new(size_t size, ClassLoaderData* loader_data, MetaspaceObj::Type type, TRAPS) throw() { // Klass has its own operator new assert(type != ClassType, "class has its own operator new"); - return Metaspace::allocate(loader_data, word_size, type, /*use_class_space*/ false, THREAD); + return Metaspace::allocate(loader_data, word_size, type, THREAD); } void* MetaspaceObj::operator new(size_t size, ClassLoaderData* loader_data, @@ -81,7 +81,7 @@ void* MetaspaceObj::operator new(size_t size, ClassLoaderData* loader_data, MetaspaceObj::Type type) throw() { assert(!Thread::current()->is_Java_thread(), "only allowed by non-Java thread"); assert(type != ClassType, "class has its own operator new"); - return Metaspace::allocate(loader_data, word_size, type, /*use_class_space*/ false); + return Metaspace::allocate(loader_data, word_size, type); } // This is used for allocating training data. We are allocating training data in many cases where a GC cannot be triggered. diff --git a/src/hotspot/share/memory/metaspace.cpp b/src/hotspot/share/memory/metaspace.cpp index 1e3b8d0594f..e686b324004 100644 --- a/src/hotspot/share/memory/metaspace.cpp +++ b/src/hotspot/share/memory/metaspace.cpp @@ -872,7 +872,7 @@ size_t Metaspace::max_allocation_word_size() { // is suitable for calling from non-Java threads. // Callers are responsible for checking null. MetaWord* Metaspace::allocate(ClassLoaderData* loader_data, size_t word_size, - MetaspaceObj::Type type, bool use_class_space) { + MetaspaceObj::Type type) { assert(word_size <= Metaspace::max_allocation_word_size(), "allocation size too large (%zu)", word_size); @@ -882,7 +882,7 @@ MetaWord* Metaspace::allocate(ClassLoaderData* loader_data, size_t word_size, // Deal with concurrent unloading failed allocation starvation MetaspaceCriticalAllocation::block_if_concurrent_purge(); - MetadataType mdtype = use_class_space ? ClassType : NonClassType; + MetadataType mdtype = (type == MetaspaceObj::ClassType) ? ClassType : NonClassType; // Try to allocate metadata. MetaWord* result = loader_data->metaspace_non_null()->allocate(word_size, mdtype); @@ -906,7 +906,7 @@ MetaWord* Metaspace::allocate(ClassLoaderData* loader_data, size_t word_size, } MetaWord* Metaspace::allocate(ClassLoaderData* loader_data, size_t word_size, - MetaspaceObj::Type type, bool use_class_space, TRAPS) { + MetaspaceObj::Type type, TRAPS) { if (HAS_PENDING_EXCEPTION) { assert(false, "Should not allocate with exception pending"); @@ -914,10 +914,10 @@ MetaWord* Metaspace::allocate(ClassLoaderData* loader_data, size_t word_size, } assert(!THREAD->owns_locks(), "allocating metaspace while holding mutex"); - MetaWord* result = allocate(loader_data, word_size, type, use_class_space); + MetaWord* result = allocate(loader_data, word_size, type); if (result == nullptr) { - MetadataType mdtype = use_class_space ? ClassType : NonClassType; + MetadataType mdtype = (type == MetaspaceObj::ClassType) ? ClassType : NonClassType; tracer()->report_metaspace_allocation_failure(loader_data, word_size, type, mdtype); // Allocation failed. diff --git a/src/hotspot/share/memory/metaspace.hpp b/src/hotspot/share/memory/metaspace.hpp index 408dbf6d23f..01ef4b4dd49 100644 --- a/src/hotspot/share/memory/metaspace.hpp +++ b/src/hotspot/share/memory/metaspace.hpp @@ -120,12 +120,12 @@ public: static constexpr size_t min_allocation_word_size = min_allocation_alignment_words; static MetaWord* allocate(ClassLoaderData* loader_data, size_t word_size, - MetaspaceObj::Type type, bool use_class_space, TRAPS); + MetaspaceObj::Type type, TRAPS); // Non-TRAPS version of allocate which can be called by a non-Java thread, that returns // null on failure. static MetaWord* allocate(ClassLoaderData* loader_data, size_t word_size, - MetaspaceObj::Type type, bool use_class_space); + MetaspaceObj::Type type); // Returns true if the pointer points into class space, non-class metaspace, or the // metadata portion of the CDS archive. diff --git a/src/hotspot/share/oops/array.inline.hpp b/src/hotspot/share/oops/array.inline.hpp index 3fa7fd15fb3..30cf2e38f77 100644 --- a/src/hotspot/share/oops/array.inline.hpp +++ b/src/hotspot/share/oops/array.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,14 +34,14 @@ template inline void* Array::operator new(size_t size, ClassLoaderData* loader_data, int length, TRAPS) throw() { size_t word_size = Array::size(length); return (void*) Metaspace::allocate(loader_data, word_size, - MetaspaceObj::array_type(sizeof(T)), false, THREAD); + MetaspaceObj::array_type(sizeof(T)), THREAD); } template inline void* Array::operator new(size_t size, ClassLoaderData* loader_data, int length) throw() { size_t word_size = Array::size(length); return (void*) Metaspace::allocate(loader_data, word_size, - MetaspaceObj::array_type(sizeof(T)), false); + MetaspaceObj::array_type(sizeof(T))); } template diff --git a/src/hotspot/share/oops/arrayKlass.cpp b/src/hotspot/share/oops/arrayKlass.cpp index 32a86c7ab24..dc64abd6cd7 100644 --- a/src/hotspot/share/oops/arrayKlass.cpp +++ b/src/hotspot/share/oops/arrayKlass.cpp @@ -41,10 +41,6 @@ #include "oops/oop.inline.hpp" #include "runtime/handles.inline.hpp" -void* ArrayKlass::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, TRAPS) throw() { - return Metaspace::allocate(loader_data, word_size, MetaspaceObj::ClassType, true, THREAD); -} - ArrayKlass::ArrayKlass() { assert(CDSConfig::is_dumping_static_archive() || CDSConfig::is_using_archive(), "only for CDS"); } diff --git a/src/hotspot/share/oops/arrayKlass.hpp b/src/hotspot/share/oops/arrayKlass.hpp index 5bfe46573d3..02d72c3cde8 100644 --- a/src/hotspot/share/oops/arrayKlass.hpp +++ b/src/hotspot/share/oops/arrayKlass.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,8 +49,6 @@ class ArrayKlass: public Klass { ArrayKlass(Symbol* name, KlassKind kind); ArrayKlass(); - void* operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, TRAPS) throw(); - public: // Testing operation DEBUG_ONLY(bool is_array_klass_slow() const { return true; }) diff --git a/src/hotspot/share/oops/instanceKlass.cpp b/src/hotspot/share/oops/instanceKlass.cpp index f5e5628c99a..932d1f246ad 100644 --- a/src/hotspot/share/oops/instanceKlass.cpp +++ b/src/hotspot/share/oops/instanceKlass.cpp @@ -455,11 +455,6 @@ const char* InstanceKlass::nest_host_error() { } } -void* InstanceKlass::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, - bool use_class_space, TRAPS) throw() { - return Metaspace::allocate(loader_data, word_size, ClassType, use_class_space, THREAD); -} - InstanceKlass* InstanceKlass::allocate_instance_klass(const ClassFileParser& parser, TRAPS) { const int size = InstanceKlass::size(parser.vtable_size(), parser.itable_size(), @@ -472,27 +467,26 @@ InstanceKlass* InstanceKlass::allocate_instance_klass(const ClassFileParser& par assert(loader_data != nullptr, "invariant"); InstanceKlass* ik; - const bool use_class_space = UseClassMetaspaceForAllClasses || parser.klass_needs_narrow_id(); // Allocation if (parser.is_instance_ref_klass()) { // java.lang.ref.Reference - ik = new (loader_data, size, use_class_space, THREAD) InstanceRefKlass(parser); + ik = new (loader_data, size, THREAD) InstanceRefKlass(parser); } else if (class_name == vmSymbols::java_lang_Class()) { // mirror - java.lang.Class - ik = new (loader_data, size, use_class_space, THREAD) InstanceMirrorKlass(parser); + ik = new (loader_data, size, THREAD) InstanceMirrorKlass(parser); } else if (is_stack_chunk_class(class_name, loader_data)) { // stack chunk - ik = new (loader_data, size, use_class_space, THREAD) InstanceStackChunkKlass(parser); + ik = new (loader_data, size, THREAD) InstanceStackChunkKlass(parser); } else if (is_class_loader(class_name, parser)) { // class loader - java.lang.ClassLoader - ik = new (loader_data, size, use_class_space, THREAD) InstanceClassLoaderKlass(parser); + ik = new (loader_data, size, THREAD) InstanceClassLoaderKlass(parser); } else { // normal - ik = new (loader_data, size, use_class_space, THREAD) InstanceKlass(parser); + ik = new (loader_data, size, THREAD) InstanceKlass(parser); } - if (ik != nullptr && UseCompressedClassPointers && use_class_space) { + if (ik != nullptr && UseCompressedClassPointers) { assert(CompressedKlassPointers::is_encodable(ik), "Klass " PTR_FORMAT "needs a narrow Klass ID, but is not encodable", p2i(ik)); } diff --git a/src/hotspot/share/oops/instanceKlass.hpp b/src/hotspot/share/oops/instanceKlass.hpp index c2d5e9cc098..a24c38cf259 100644 --- a/src/hotspot/share/oops/instanceKlass.hpp +++ b/src/hotspot/share/oops/instanceKlass.hpp @@ -143,8 +143,6 @@ class InstanceKlass: public Klass { protected: InstanceKlass(const ClassFileParser& parser, KlassKind kind = Kind, ReferenceType reference_type = REF_NONE); - void* operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, bool use_class_space, TRAPS) throw(); - public: InstanceKlass(); diff --git a/src/hotspot/share/oops/klass.cpp b/src/hotspot/share/oops/klass.cpp index b6e60b4fa7d..a93875b86a5 100644 --- a/src/hotspot/share/oops/klass.cpp +++ b/src/hotspot/share/oops/klass.cpp @@ -279,18 +279,19 @@ static markWord make_prototype(const Klass* kls) { #ifdef _LP64 if (UseCompactObjectHeaders) { // With compact object headers, the narrow Klass ID is part of the mark word. - // We therfore seed the mark word with the narrow Klass ID. - // Note that only those Klass that can be instantiated have a narrow Klass ID. - // For those who don't, we leave the klass bits empty and assert if someone - // tries to use those. - const narrowKlass nk = CompressedKlassPointers::is_encodable(kls) ? - CompressedKlassPointers::encode(const_cast(kls)) : 0; + // We therefore seed the mark word with the narrow Klass ID. + precond(CompressedKlassPointers::is_encodable(kls)); + const narrowKlass nk = CompressedKlassPointers::encode(const_cast(kls)); prototype = prototype.set_narrow_klass(nk); } #endif return prototype; } +void* Klass::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, TRAPS) throw() { + return Metaspace::allocate(loader_data, word_size, MetaspaceObj::ClassType, THREAD); +} + Klass::Klass() : _kind(UnknownKlassKind) { assert(CDSConfig::is_dumping_static_archive() || CDSConfig::is_using_archive(), "only for cds"); } @@ -1060,7 +1061,7 @@ void Klass::verify_on(outputStream* st) { // This can be expensive, but it is worth checking that this klass is actually // in the CLD graph but not in production. #ifdef ASSERT - if (UseCompressedClassPointers && needs_narrow_id()) { + if (UseCompressedClassPointers) { // Stricter checks for both correct alignment and placement CompressedKlassPointers::check_encodable(this); } else { diff --git a/src/hotspot/share/oops/klass.hpp b/src/hotspot/share/oops/klass.hpp index ad03c1e2ed6..70d9ce3a881 100644 --- a/src/hotspot/share/oops/klass.hpp +++ b/src/hotspot/share/oops/klass.hpp @@ -207,6 +207,8 @@ protected: Klass(KlassKind kind); Klass(); + void* operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, TRAPS) throw(); + public: int kind() { return _kind; } @@ -794,10 +796,6 @@ public: static bool is_valid(Klass* k); static void on_secondary_supers_verification_failure(Klass* super, Klass* sub, bool linear_result, bool table_result, const char* msg); - - // Returns true if this Klass needs to be addressable via narrow Klass ID. - inline bool needs_narrow_id() const; - }; #endif // SHARE_OOPS_KLASS_HPP diff --git a/src/hotspot/share/oops/klass.inline.hpp b/src/hotspot/share/oops/klass.inline.hpp index 19d4954ccad..4ac50cbc180 100644 --- a/src/hotspot/share/oops/klass.inline.hpp +++ b/src/hotspot/share/oops/klass.inline.hpp @@ -175,13 +175,4 @@ inline bool Klass::search_secondary_supers(Klass *k) const { return result; } -// Returns true if this Klass needs to be addressable via narrow Klass ID. -inline bool Klass::needs_narrow_id() const { - // Classes that are never instantiated need no narrow Klass Id, since the - // only point of having a narrow id is to put it into an object header. Keeping - // never instantiated classes out of class space lessens the class space pressure. - // For more details, see JDK-8338526. - // Note: don't call this function before access flags are initialized. - return UseClassMetaspaceForAllClasses || (!is_abstract() && !is_interface()); -} #endif // SHARE_OOPS_KLASS_INLINE_HPP diff --git a/src/hotspot/share/runtime/globals.hpp b/src/hotspot/share/runtime/globals.hpp index e76b7474891..dac01d018bf 100644 --- a/src/hotspot/share/runtime/globals.hpp +++ b/src/hotspot/share/runtime/globals.hpp @@ -2000,9 +2000,6 @@ const int ObjectAlignmentInBytes = 8; "Minimal number of elements in a sorted collection to prefer" \ "binary search over simple linear search." ) \ \ - product(bool, UseClassMetaspaceForAllClasses, false, DIAGNOSTIC, \ - "Use the class metaspace for all classes including " \ - "abstract and interface classes.") \ // end of RUNTIME_FLAGS diff --git a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java index fd08361f682..641dd4676ae 100644 --- a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java +++ b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotMetaspaceConstantImpl.java @@ -43,9 +43,6 @@ final class HotSpotMetaspaceConstantImpl implements HotSpotMetaspaceConstant, VM private HotSpotMetaspaceConstantImpl(MetaspaceObject metaspaceObject, boolean compressed) { this.metaspaceObject = metaspaceObject; this.compressed = compressed; - if (compressed && !canBeStoredInCompressibleMetaSpace()) { - throw new IllegalArgumentException("constant cannot be compressed: " + metaspaceObject); - } } @Override @@ -88,19 +85,7 @@ final class HotSpotMetaspaceConstantImpl implements HotSpotMetaspaceConstant, VM @Override public boolean isCompressible() { - if (compressed) { - return false; - } - return canBeStoredInCompressibleMetaSpace(); - } - - private boolean canBeStoredInCompressibleMetaSpace() { - if (!HotSpotVMConfig.config().useClassMetaspaceForAllClasses && metaspaceObject instanceof HotSpotResolvedJavaType t && !t.isArray()) { - // As of JDK-8338526, interface and abstract types are not stored - // in compressible metaspace. - return !t.isInterface() && !t.isAbstract(); - } - return true; + return !compressed; } @Override diff --git a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java index 449b315e467..e4e23c6d8b8 100644 --- a/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java +++ b/src/jdk.internal.vm.ci/share/classes/jdk/vm/ci/hotspot/HotSpotVMConfig.java @@ -67,8 +67,6 @@ class HotSpotVMConfig extends HotSpotVMConfigAccess { final boolean useCompressedOops = getFlag("UseCompressedOops", Boolean.class); - final boolean useClassMetaspaceForAllClasses = getFlag("UseClassMetaspaceForAllClasses", Boolean.class); - final int objectAlignment = getFlag("ObjectAlignmentInBytes", Integer.class); final int klassOffsetInBytes = getFieldValue("CompilerToVM::Data::oopDesc_klass_offset_in_bytes", Integer.class, "int"); From 802d9c23dc83dcd37964fa3a894fa6d01f501176 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Fri, 19 Sep 2025 12:03:43 +0000 Subject: [PATCH 125/556] 8367107: JFR: Refactor policy tests out of TestRemoteDump Reviewed-by: mgronlun --- .../jfr/jmx/streaming/TestDumpRetention.java | 123 ++++++++++++++++++ .../jdk/jfr/jmx/streaming/TestRemoteDump.java | 75 +---------- 2 files changed, 124 insertions(+), 74 deletions(-) create mode 100644 test/jdk/jdk/jfr/jmx/streaming/TestDumpRetention.java diff --git a/test/jdk/jdk/jfr/jmx/streaming/TestDumpRetention.java b/test/jdk/jdk/jfr/jmx/streaming/TestDumpRetention.java new file mode 100644 index 00000000000..ffb16ec048a --- /dev/null +++ b/test/jdk/jdk/jfr/jmx/streaming/TestDumpRetention.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jfr.jmx.streaming; + +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; +import javax.management.MBeanServerConnection; + +import jdk.jfr.Event; +import jdk.jfr.Name; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; +import jdk.management.jfr.RemoteRecordingStream; + +/** + * @test + * @summary Tests retention with RemoteRecordingStream::dump(Path) + * @requires vm.flagless + * @requires vm.hasJFR + * @library /test/lib + * @run main/othervm jdk.jfr.jmx.streaming.TestDumpRetention + */ +public class TestDumpRetention { + + private static final MBeanServerConnection CONNECTION = ManagementFactory.getPlatformMBeanServer(); + + @Name("TestDumpRetention") + static class DumpEvent extends Event { + } + + public static void main(String... args) throws Exception { + testSetNoPolicy(); + testSetMaxAge(); + testSetMaxSize(); + } + + private static List recordWithPolicy(String filename, Consumer policy) throws Exception { + CountDownLatch latch1 = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(2); + CountDownLatch latch3 = new CountDownLatch(3); + try (var rs = new RemoteRecordingStream(CONNECTION)) { + policy.accept(rs); + rs.onEvent(e -> { + latch1.countDown(); + latch2.countDown(); + latch3.countDown(); + }); + rs.startAsync(); + DumpEvent e1 = new DumpEvent(); + e1.commit(); + latch1.await(); + // Force chunk rotation + try (Recording r = new Recording()) { + r.start(); + DumpEvent e2 = new DumpEvent(); + e2.commit(); + } + latch2.await(); + DumpEvent e3 = new DumpEvent(); + e3.commit(); + latch3.await(); + Path p = Path.of(filename); + rs.dump(p); + return RecordingFile.readAllEvents(p); + } + } + + private static void testSetNoPolicy() throws Exception { + var events = recordWithPolicy("no-policy.jfr", rs -> { + // use default policy, remove after consumption + }); + // Since latch3 have been triggered at least two events/chunks + // before must have been consumed, possibly 3, but it's a race. + if (events.size() > 1) { + throw new Exception("Expected at most one event to not be consumed"); + } + } + + private static void testSetMaxAge() throws Exception { + var events = recordWithPolicy("max-age.jfr", rs -> { + // keeps all events for the dump + rs.setMaxAge(Duration.ofDays(1)); + }); + if (events.size() != 3) { + throw new Exception("Expected all 3 events to be in dump after setMaxAge"); + } + } + + private static void testSetMaxSize() throws Exception { + var events = recordWithPolicy("max-size.jfr", rs -> { + // keeps all events for the dump + rs.setMaxSize(100_000_000); + }); + if (events.size() != 3) { + throw new Exception("Expected all 3 events to be in dump after setMaxSize"); + } + } +} diff --git a/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java b/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java index 6731f3c44cf..7319601d147 100644 --- a/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java +++ b/test/jdk/jdk/jfr/jmx/streaming/TestRemoteDump.java @@ -33,7 +33,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import java.util.function.Function; import javax.management.MBeanServerConnection; import jdk.jfr.Event; @@ -41,12 +40,11 @@ import jdk.jfr.Name; import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.consumer.RecordingFile; -import jdk.jfr.consumer.RecordingStream; import jdk.management.jfr.RemoteRecordingStream; /** * @test - * @summary Tests RecordingStream::dump(Path) + * @summary Tests RemoteRecordingStream::dump(Path) * @requires vm.flagless * @requires vm.hasJFR * @library /test/lib @@ -66,9 +64,6 @@ public class TestRemoteDump { testOneDump(); testMultipleDumps(); testEventAfterDump(); - testSetNoPolicy(); - testSetMaxAge(); - testSetMaxSize(); } private static void testUnstarted() throws Exception { @@ -97,74 +92,6 @@ public class TestRemoteDump { } } - private static List recordWithPolicy(String filename, boolean awaitEvents, Consumer policy) throws Exception { - CountDownLatch latch1 = new CountDownLatch(1); - CountDownLatch latch2 = new CountDownLatch(2); - CountDownLatch latch3 = new CountDownLatch(3); - try (var rs = new RemoteRecordingStream(CONNECTION)) { - policy.accept(rs); - rs.onEvent(e -> { - latch1.countDown(); - latch2.countDown(); - latch3.countDown(); - }); - rs.startAsync(); - DumpEvent e1 = new DumpEvent(); - e1.commit(); - if (awaitEvents) { - latch1.await(); - } - // Force chunk rotation - try (Recording r = new Recording()) { - r.start(); - DumpEvent e2 = new DumpEvent(); - e2.commit(); - } - if (awaitEvents) { - latch2.await(); - } - DumpEvent e3 = new DumpEvent(); - e3.commit(); - latch3.await(); - Path p = Path.of(filename); - rs.dump(p); - return RecordingFile.readAllEvents(p); - } - } - - private static void testSetMaxSize() throws Exception { - var events = recordWithPolicy("max-size.jfr", false, rs -> { - // keeps all events for the dump - rs.setMaxSize(100_000_000); - }); - if (events.size() != 3) { - throw new Exception("Expected all 3 events to be in dump after setMaxSize"); - } - - } - - private static void testSetMaxAge() throws Exception { - var events = recordWithPolicy("max-age.jfr", false, rs -> { - // keeps all events for the dump - rs.setMaxAge(Duration.ofDays(1)); - }); - if (events.size() != 3) { - throw new Exception("Expected all 3 events to be in dump after setMaxAge"); - } - - } - - private static void testSetNoPolicy() throws Exception { - var events = recordWithPolicy("no-policy.jfr", true, rs -> { - // use default policy, remove after consumption - }); - // Since latch3 have been triggered at least two events/chunks - // before must have been consumed, possibly 3, but it's a race. - if (events.size() > 1) { - throw new Exception("Expected at most one event to not be consumed"); - } - } - private static void testMultipleDumps() throws Exception { CountDownLatch latch = new CountDownLatch(1); try (var rs = new RemoteRecordingStream(CONNECTION)) { From 87d50425fce3b76ecc03f087dbb81b86edeed1cd Mon Sep 17 00:00:00 2001 From: Volkan Yazici Date: Fri, 19 Sep 2025 12:07:27 +0000 Subject: [PATCH 126/556] 8367067: Improve exception handling in HttpRequest.BodyPublishers Reviewed-by: jpai, dfuchs --- .../internal/net/http/CheckedIterable.java | 50 +++ .../internal/net/http/CheckedIterator.java | 68 ++++ .../jdk/internal/net/http/PullPublisher.java | 60 ++-- .../internal/net/http/RequestPublishers.java | 74 ++-- .../httpclient/FileChannelPublisherTest.java | 10 +- .../ByteBufferUtils.java | 91 +++++ .../FromPublisherTest.java | 100 ++++++ .../HttpRequestBodyPublishers/NoBodyTest.java | 57 ++++ .../OfByteArrayTest.java | 134 ++++++++ .../OfByteArraysTest.java | 317 ++++++++++++++++++ .../HttpRequestBodyPublishers/OfFileTest.java | 290 ++++++++++++++++ .../OfInputStreamTest.java | 226 +++++++++++++ .../OfStringTest.java | 119 +++++++ .../RecordingSubscriber.java | 136 ++++++++ 14 files changed, 1665 insertions(+), 67 deletions(-) create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/ByteBufferUtils.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/FromPublisherTest.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/NoBodyTest.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArrayTest.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfInputStreamTest.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfStringTest.java create mode 100644 test/jdk/java/net/httpclient/HttpRequestBodyPublishers/RecordingSubscriber.java diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java new file mode 100644 index 00000000000..bb74ac3d6e9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.util.Iterator; + +/** + * An {@link Iterable} clone supporting checked exceptions. + * + * @param the type of elements returned by the produced iterators + */ +@FunctionalInterface +interface CheckedIterable { + + /** + * {@return an {@linkplain CheckedIterator iterator} over elements of type {@code E}} + */ + CheckedIterator iterator() throws Exception; + + static CheckedIterable fromIterable(Iterable iterable) { + return () -> { + Iterator iterator = iterable.iterator(); + return CheckedIterator.fromIterator(iterator); + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java new file mode 100644 index 00000000000..42e8e2b6c87 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/CheckedIterator.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An {@link Iterator} clone supporting checked exceptions. + * + * @param the type of elements returned by this iterator + */ +interface CheckedIterator { + + /** + * {@return {@code true} if the iteration has more elements} + * @throws Exception if operation fails + */ + boolean hasNext() throws Exception; + + /** + * {@return the next element in the iteration} + * + * @throws NoSuchElementException if the iteration has no more elements + * @throws Exception if operation fails + */ + E next() throws Exception; + + static CheckedIterator fromIterator(Iterator iterator) { + return new CheckedIterator<>() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public E next() { + return iterator.next(); + } + + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java b/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java index 0556214648e..d1019c05629 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PullPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,46 +25,55 @@ package jdk.internal.net.http; -import java.util.Iterator; import java.util.concurrent.Flow; + import jdk.internal.net.http.common.Demand; import jdk.internal.net.http.common.SequentialScheduler; /** - * A Publisher that publishes items obtained from the given Iterable. Each new - * subscription gets a new Iterator. + * A {@linkplain Flow.Publisher publisher} that publishes items obtained from the given {@link CheckedIterable}. + * Each new subscription gets a new {@link CheckedIterator}. */ class PullPublisher implements Flow.Publisher { - // Only one of `iterable` and `throwable` can be non-null. throwable is + // Only one of `iterable` or `throwable` should be null, and the other non-null. throwable is // non-null when an error has been encountered, by the creator of // PullPublisher, while subscribing the subscriber, but before subscribe has // completed. - private final Iterable iterable; + private final CheckedIterable iterable; private final Throwable throwable; - PullPublisher(Iterable iterable, Throwable throwable) { + PullPublisher(CheckedIterable iterable, Throwable throwable) { + if ((iterable == null) == (throwable == null)) { + String message = String.format( + "only one of `iterable` or `throwable` should be null, and the other non-null, but %s are null", + throwable == null ? "both" : "none"); + throw new IllegalArgumentException(message); + } this.iterable = iterable; this.throwable = throwable; } - PullPublisher(Iterable iterable) { + PullPublisher(CheckedIterable iterable) { this(iterable, null); } @Override public void subscribe(Flow.Subscriber subscriber) { - Subscription sub; - if (throwable != null) { - assert iterable == null : "non-null iterable: " + iterable; - sub = new Subscription(subscriber, null, throwable); - } else { - assert throwable == null : "non-null exception: " + throwable; - sub = new Subscription(subscriber, iterable.iterator(), null); + Throwable failure = throwable; + CheckedIterator iterator = null; + if (failure == null) { + try { + iterator = iterable.iterator(); + } catch (Exception exception) { + failure = exception; + } } + Subscription sub = failure != null + ? new Subscription(subscriber, null, failure) + : new Subscription(subscriber, iterator, null); subscriber.onSubscribe(sub); - - if (throwable != null) { + if (failure != null) { sub.pullScheduler.runOrSchedule(); } } @@ -72,7 +81,7 @@ class PullPublisher implements Flow.Publisher { private class Subscription implements Flow.Subscription { private final Flow.Subscriber subscriber; - private final Iterator iter; + private final CheckedIterator iter; private volatile boolean completed; private volatile boolean cancelled; private volatile Throwable error; @@ -80,7 +89,7 @@ class PullPublisher implements Flow.Publisher { private final Demand demand = new Demand(); Subscription(Flow.Subscriber subscriber, - Iterator iter, + CheckedIterator iter, Throwable throwable) { this.subscriber = subscriber; this.iter = iter; @@ -117,7 +126,18 @@ class PullPublisher implements Flow.Publisher { } subscriber.onNext(next); } - if (!iter.hasNext() && !cancelled) { + + boolean hasNext; + try { + hasNext = iter.hasNext(); + } catch (Exception e) { + completed = true; + pullScheduler.stop(); + subscriber.onError(e); + return; + } + + if (!hasNext && !cancelled) { completed = true; pullScheduler.stop(); subscriber.onComplete(); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java b/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java index 88cabe15419..5aea5648e19 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java @@ -73,8 +73,11 @@ public final class RequestPublishers { this(content, offset, length, Utils.BUFSIZE); } - /* bufSize exposed for testing purposes */ - ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) { + private ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) { + Objects.checkFromIndexSize(offset, length, content.length); // Implicit null check on `content` + if (bufSize <= 0) { + throw new IllegalArgumentException("Invalid buffer size: " + bufSize); + } this.content = content; this.offset = offset; this.length = length; @@ -99,7 +102,7 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { List copy = copy(content, offset, length); - var delegate = new PullPublisher<>(copy); + var delegate = new PullPublisher<>(CheckedIterable.fromIterable(copy)); delegate.subscribe(subscriber); } @@ -121,7 +124,7 @@ public final class RequestPublishers { // The ByteBufferIterator will iterate over the byte[] arrays in // the content one at the time. // - class ByteBufferIterator implements Iterator { + private final class ByteBufferIterator implements CheckedIterator { final ConcurrentLinkedQueue buffers = new ConcurrentLinkedQueue<>(); final Iterator iterator = content.iterator(); @Override @@ -166,13 +169,9 @@ public final class RequestPublishers { } } - public Iterator iterator() { - return new ByteBufferIterator(); - } - @Override public void subscribe(Flow.Subscriber subscriber) { - Iterable iterable = this::iterator; + CheckedIterable iterable = () -> new ByteBufferIterator(); var delegate = new PullPublisher<>(iterable); delegate.subscribe(subscriber); } @@ -202,13 +201,13 @@ public final class RequestPublishers { public static class StringPublisher extends ByteArrayPublisher { public StringPublisher(String content, Charset charset) { - super(content.getBytes(charset)); + super(content.getBytes(Objects.requireNonNull(charset))); // Implicit null check on `content` } } public static class EmptyPublisher implements BodyPublisher { private final Flow.Publisher delegate = - new PullPublisher(Collections.emptyList(), null); + new PullPublisher<>(CheckedIterable.fromIterable(Collections.emptyList()), null); @Override public long contentLength() { @@ -290,7 +289,7 @@ public final class RequestPublishers { /** * Reads one buffer ahead all the time, blocking in hasNext() */ - public static class StreamIterator implements Iterator { + private static final class StreamIterator implements CheckedIterator { final InputStream is; final Supplier bufSupplier; private volatile boolean eof; @@ -331,20 +330,8 @@ public final class RequestPublishers { return n; } - /** - * Close stream in this instance. - * UncheckedIOException may be thrown if IOE happens at InputStream::close. - */ - private void closeStream() { - try { - is.close(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - @Override - public boolean hasNext() { + public boolean hasNext() throws IOException { stateLock.lock(); try { return hasNext0(); @@ -353,7 +340,7 @@ public final class RequestPublishers { } } - private boolean hasNext0() { + private boolean hasNext0() throws IOException { if (need2Read) { try { haveNext = read() != -1; @@ -363,10 +350,10 @@ public final class RequestPublishers { } catch (IOException e) { haveNext = false; need2Read = false; - throw new UncheckedIOException(e); + throw e; } finally { if (!haveNext) { - closeStream(); + is.close(); } } } @@ -374,7 +361,7 @@ public final class RequestPublishers { } @Override - public ByteBuffer next() { + public ByteBuffer next() throws IOException { stateLock.lock(); try { if (!hasNext()) { @@ -398,18 +385,23 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { - PullPublisher publisher; - InputStream is = streamSupplier.get(); - if (is == null) { - Throwable t = new IOException("streamSupplier returned null"); - publisher = new PullPublisher<>(null, t); - } else { - publisher = new PullPublisher<>(iterableOf(is), null); + InputStream is = null; + Exception exception = null; + try { + is = streamSupplier.get(); + if (is == null) { + exception = new IOException("Stream supplier returned null"); + } + } catch (Exception cause) { + exception = new IOException("Stream supplier has failed", cause); } + PullPublisher publisher = exception != null + ? new PullPublisher<>(null, exception) + : new PullPublisher<>(iterableOf(is), null); publisher.subscribe(subscriber); } - protected Iterable iterableOf(InputStream is) { + private CheckedIterable iterableOf(InputStream is) { return () -> new StreamIterator(is); } @@ -442,13 +434,13 @@ public final class RequestPublishers { @Override public void subscribe(Flow.Subscriber subscriber) { - Iterable iterable = () -> new FileChannelIterator(channel, position, limit); + CheckedIterable iterable = () -> new FileChannelIterator(channel, position, limit); new PullPublisher<>(iterable).subscribe(subscriber); } } - private static final class FileChannelIterator implements Iterator { + private static final class FileChannelIterator implements CheckedIterator { private final FileChannel channel; @@ -470,7 +462,7 @@ public final class RequestPublishers { } @Override - public ByteBuffer next() { + public ByteBuffer next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } @@ -487,7 +479,7 @@ public final class RequestPublishers { } } catch (IOException ioe) { terminated = true; - throw new UncheckedIOException(ioe); + throw ioe; } return buffer.flip(); } diff --git a/test/jdk/java/net/httpclient/FileChannelPublisherTest.java b/test/jdk/java/net/httpclient/FileChannelPublisherTest.java index 5b064efa078..4bc3c3ae315 100644 --- a/test/jdk/java/net/httpclient/FileChannelPublisherTest.java +++ b/test/jdk/java/net/httpclient/FileChannelPublisherTest.java @@ -531,9 +531,8 @@ class FileChannelPublisherTest { // Verifying the client failure LOGGER.log("Verifying the client failure"); - Exception requestFailure0 = assertThrows(ExecutionException.class, () -> responseFutureRef.get().get()); - Exception requestFailure1 = assertInstanceOf(UncheckedIOException.class, requestFailure0.getCause()); - assertInstanceOf(ClosedChannelException.class, requestFailure1.getCause()); + Exception requestFailure = assertThrows(ExecutionException.class, () -> responseFutureRef.get().get()); + assertInstanceOf(ClosedChannelException.class, requestFailure.getCause()); verifyServerIncompleteRead(pair, fileLength); @@ -578,9 +577,8 @@ class FileChannelPublisherTest { // Verifying the client failure LOGGER.log("Verifying the client failure"); Exception requestFailure0 = assertThrows(ExecutionException.class, responseFuture::get); - Exception requestFailure1 = assertInstanceOf(UncheckedIOException.class, requestFailure0.getCause()); - Exception requestFailure2 = assertInstanceOf(IOException.class, requestFailure1.getCause()); - String requestFailure2Message = requestFailure2.getMessage(); + Exception requestFailure1 = assertInstanceOf(IOException.class, requestFailure0.getCause()); + String requestFailure2Message = requestFailure1.getMessage(); assertTrue( requestFailure2Message.contains("Unexpected EOF"), "unexpected message: " + requestFailure2Message); diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/ByteBufferUtils.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/ByteBufferUtils.java new file mode 100644 index 00000000000..29276dc7b60 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/ByteBufferUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public final class ByteBufferUtils { + + private ByteBufferUtils() {} + + public static void assertEquals(ByteBuffer expectedBuffer, ByteBuffer actualBuffer, String message) { + assertEquals(bytes(expectedBuffer), bytes(actualBuffer), message); + } + + public static void assertEquals(byte[] expectedBytes, ByteBuffer actualBuffer, String message) { + assertEquals(expectedBytes, bytes(actualBuffer), message); + } + + public static void assertEquals(byte[] expectedBytes, byte[] actualBytes, String message) { + Objects.requireNonNull(expectedBytes); + Objects.requireNonNull(actualBytes); + int mismatchIndex = Arrays.mismatch(expectedBytes, actualBytes); + if (mismatchIndex >= 0) { + Byte expectedByte = mismatchIndex >= expectedBytes.length ? null : expectedBytes[mismatchIndex]; + Byte actualByte = mismatchIndex >= actualBytes.length ? null : actualBytes[mismatchIndex]; + String extendedMessage = String.format( + "%s" + + "array contents differ at index [%s], expected: <%s> but was: <%s>%n" + + "expected: %s%n" + + "actual: %s%n", + message == null ? "" : (message + ": "), + mismatchIndex, expectedByte, actualByte, + prettyPrintBytes(expectedBytes), + prettyPrintBytes(actualBytes)); + throw new AssertionError(extendedMessage); + } + } + + private static byte[] bytes(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + return bytes; + } + + private static String prettyPrintBytes(byte[] bytes) { + return IntStream.range(0, bytes.length) + .mapToObj(i -> "" + bytes[i]) + .collect(Collectors.joining(", ", "[", "]")); + } + + public static int findLengthExceedingMaxMemory() { + long memoryLength = Runtime.getRuntime().maxMemory(); + double length = Math.ceil(1.5D * memoryLength); + if (length < 1 || length > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Bogus or excessive memory: " + memoryLength); + } + return (int) length; + } + + public static byte[] byteArrayOfLength(int length) { + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + bytes[i] = (byte) i; + } + return bytes; + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/FromPublisherTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/FromPublisherTest.java new file mode 100644 index 00000000000..f05eab0a2e5 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/FromPublisherTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.http.HttpRequest; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::fromPublisher` behavior + * @build RecordingSubscriber + * @run junit FromPublisherTest + */ + +class FromPublisherTest { + + @Test + void testNullPublisher() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.fromPublisher(null)); + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.fromPublisher(null, 1)); + } + + @ParameterizedTest + @ValueSource(longs = {0L, -1L, Long.MIN_VALUE}) + void testInvalidContentLength(long contentLength) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> HttpRequest.BodyPublishers.fromPublisher(null, contentLength)); + String exceptionMessage = exception.getMessage(); + assertTrue( + exceptionMessage.contains("non-positive contentLength"), + "Unexpected exception message: " + exceptionMessage); + } + + @ParameterizedTest + @ValueSource(longs = {1, 2, 3, 4}) + void testValidContentLength(long contentLength) { + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.fromPublisher(HttpRequest.BodyPublishers.noBody(), contentLength); + assertEquals(contentLength, publisher.contentLength()); + } + + @Test + void testNoContentLength() { + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.fromPublisher(HttpRequest.BodyPublishers.noBody()); + assertEquals(-1, publisher.contentLength()); + } + + @Test + void testNullSubscriber() { + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.fromPublisher(HttpRequest.BodyPublishers.noBody()); + assertThrows(NullPointerException.class, () -> publisher.subscribe(null)); + } + + @Test + void testDelegation() throws InterruptedException { + BlockingQueue publisherInvocations = new LinkedBlockingQueue<>(); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.fromPublisher(subscriber -> { + publisherInvocations.add("subscribe"); + publisherInvocations.add(subscriber); + }); + RecordingSubscriber subscriber = new RecordingSubscriber(); + publisher.subscribe(subscriber); + assertEquals("subscribe", publisherInvocations.take()); + assertEquals(subscriber, publisherInvocations.take()); + assertTrue(subscriber.invocations.isEmpty()); + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/NoBodyTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/NoBodyTest.java new file mode 100644 index 00000000000..f58e9505c9a --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/NoBodyTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.Test; + +import java.net.http.HttpRequest; +import java.util.concurrent.Flow; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::noBody` behavior + * @build RecordingSubscriber + * @run junit NoBodyTest + */ + +class NoBodyTest { + + @Test + void test() throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.noBody(); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, 0); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + assertEquals("onComplete", subscriber.invocations.take()); + + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArrayTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArrayTest.java new file mode 100644 index 00000000000..9973272b435 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArrayTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.Flow; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofByteArray` behavior + * @build RecordingSubscriber + * @run junit OfByteArrayTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Djdk.httpclient.bufsize=-1 OfByteArrayTest testInvalidBufferSize + * @run main/othervm -Djdk.httpclient.bufsize=0 OfByteArrayTest testInvalidBufferSize + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking "" 0 0 "" + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 0 0 "" + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 1 0 "" + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking a 0 1 a + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking ab 0 1 a + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking ab 1 1 b + * @run main/othervm -Djdk.httpclient.bufsize=3 OfByteArrayTest testChunking ab 0 2 ab + * @run main/othervm -Djdk.httpclient.bufsize=1 OfByteArrayTest testChunking abc 0 3 a:b:c + * @run main/othervm -Djdk.httpclient.bufsize=2 OfByteArrayTest testChunking abc 0 3 ab:c + * @run main/othervm -Djdk.httpclient.bufsize=2 OfByteArrayTest testChunking abcdef 2 4 cd:ef + */ + +public class OfByteArrayTest { + + private static final Charset CHARSET = StandardCharsets.US_ASCII; + + @Test + void testNullContent() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofByteArray(null)); + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofByteArray(null, 1, 2)); + } + + @ParameterizedTest + @CsvSource({ + "abc,-1,1", // Negative offset + "abc,1,-1", // Negative length + "'',1,1", // Offset overflow on empty string + "a,2,1", // Offset overflow + "'',0,1", // Length overflow on empty string + "a,0,2", // Length overflow + }) + void testInvalidOffsetOrLength(String contentText, int offset, int length) { + byte[] content = contentText.getBytes(CHARSET); + assertThrows( + IndexOutOfBoundsException.class, + () -> HttpRequest.BodyPublishers.ofByteArray(content, offset, length)); + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws InterruptedException { + switch (args[0]) { + case "testInvalidBufferSize" -> testInvalidBufferSize(); + case "testChunking" -> testChunking( + parseStringArg(args[1]), + Integer.parseInt(args[2]), + Integer.parseInt(args[3]), + parseStringArg(args[4])); + default -> throw new IllegalArgumentException("Unexpected arguments: " + List.of(args)); + } + } + + private static String parseStringArg(String arg) { + return arg == null || arg.trim().equals("\"\"") ? "" : arg; + } + + private static void testInvalidBufferSize() { + assertThrows(IllegalArgumentException.class, () -> HttpRequest.BodyPublishers.ofByteArray(new byte[1])); + } + + private static void testChunking( + String contentText, int offset, int length, String expectedBuffersText) + throws InterruptedException { + + // Create the publisher + byte[] content = contentText.getBytes(CHARSET); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArray(content, offset, length); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, length); + + // Verify the state after `request()` + String[] expectedBuffers = expectedBuffersText.isEmpty() ? new String[0] : expectedBuffersText.split(":"); + subscription.request(Long.MAX_VALUE); + for (int bufferIndex = 0; bufferIndex < expectedBuffers.length; bufferIndex++) { + assertEquals("onNext", subscriber.invocations.take()); + String actualBuffer = CHARSET.decode((ByteBuffer) subscriber.invocations.take()).toString(); + String expectedBuffer = expectedBuffers[bufferIndex]; + assertEquals(expectedBuffer, actualBuffer, "buffer mismatch at index " + bufferIndex); + } + assertEquals("onComplete", subscriber.invocations.take()); + + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java new file mode 100644 index 00000000000..6b852407907 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Flow; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofByteArrays` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfByteArraysTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Xmx64m OfByteArraysTest testOOM + */ + +public class OfByteArraysTest { + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testIteratorOfLength(int length) throws InterruptedException { + + // Create the publisher + List buffers = IntStream + .range(0, length) + .mapToObj(i -> new byte[]{(byte) i}) + .toList(); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(buffers::iterator); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + for (int bufferIndex = 0; bufferIndex < length; bufferIndex++) { + assertEquals("onNext", subscriber.invocations.take()); + byte[] expectedBuffer = buffers.get(bufferIndex); + ByteBuffer actualBuffer = (ByteBuffer) subscriber.invocations.take(); + ByteBufferUtils.assertEquals(expectedBuffer, actualBuffer, "buffer mismatch at index " + bufferIndex); + } + assertEquals("onComplete", subscriber.invocations.take()); + + } + + @Test + void testDifferentIterators() throws InterruptedException { + + // Create a publisher using an iterable that returns a different iterator at each invocation + byte[] buffer1 = ByteBufferUtils.byteArrayOfLength(9); + byte[] buffer2 = ByteBufferUtils.byteArrayOfLength(9); + int[] iteratorRequestCount = {0}; + Iterable iterable = () -> switch (++iteratorRequestCount[0]) { + case 1 -> List.of(buffer1).iterator(); + case 2 -> List.of(buffer2).iterator(); + default -> throw new AssertionError(); + }; + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(iterable); + + // Subscribe twice (to force two `Iterable::iterator` invocations) + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription1 = subscriber.verifyAndSubscribe(publisher, -1); + Flow.Subscription subscription2 = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the content + byte[] actualBuffer1 = subscriber.drainToByteArray(subscription1, Long.MAX_VALUE); + byte[] actualBuffer2 = subscriber.drainToByteArray(subscription2, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(buffer1, actualBuffer1, null); + ByteBufferUtils.assertEquals(buffer2, actualBuffer2, null); + + } + + @Test + void testNullIterable() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofByteArrays(null)); + } + + @Test + void testNullIterator() throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(() -> null); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the NPE + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + assertInstanceOf(NullPointerException.class, subscriber.invocations.take()); + + } + + @Test + void testNullArray() throws InterruptedException { + + // Create the publisher + List iterable = new ArrayList<>(); + iterable.add(null); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(iterable); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the NPE + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + assertInstanceOf(NullPointerException.class, subscriber.invocations.take()); + + } + + @Test + void testThrowingIterable() throws InterruptedException { + + // Create the publisher + RuntimeException exception = new RuntimeException("failure for `testIteratorCreationException`"); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(() -> { + throw exception; + }); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the failure + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + Exception actualException = (Exception) subscriber.invocations.take(); + assertSame(exception, actualException); + + } + + static Stream testThrowingIteratorArgs() { + RuntimeException hasNextException = new RuntimeException("failure for `hasNext`"); + RuntimeException nextException = new RuntimeException("failure for `next`"); + return Stream.of( + Arguments.of(0, hasNextException, null, hasNextException), + Arguments.of(0, hasNextException, nextException, hasNextException), + Arguments.of(1, hasNextException, null, hasNextException), + Arguments.of(1, hasNextException, nextException, hasNextException), + Arguments.of(1, null, nextException, nextException)); + } + + @ParameterizedTest + @MethodSource("testThrowingIteratorArgs") + void testThrowingIterator( + int exceptionIndex, RuntimeException hasNextException, RuntimeException nextException, Exception expectedException) + throws InterruptedException { + + // Create the publisher + IteratorThrowingAtEnd iterator = + new IteratorThrowingAtEnd(exceptionIndex, hasNextException, nextException); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(() -> iterator); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain successful emissions + subscription.request(Long.MAX_VALUE); + for (int itemIndex = 0; itemIndex < exceptionIndex; itemIndex++) { + assertEquals("onNext", subscriber.invocations.take()); + ByteBuffer actualBuffer = (ByteBuffer) subscriber.invocations.take(); + ByteBuffer expectedBuffer = ByteBuffer.wrap(iterator.content, itemIndex, 1); + ByteBufferUtils.assertEquals(expectedBuffer, actualBuffer, null); + } + + // Verify the result + if (expectedException == null) { + assertEquals("onComplete", subscriber.invocations.take()); + } else { + assertEquals("onError", subscriber.invocations.take()); + Exception actualException = (Exception) subscriber.invocations.take(); + assertSame(expectedException, actualException); + } + + } + + private static final class IteratorThrowingAtEnd implements Iterator { + + private final byte[] content; + + private final RuntimeException hasNextException; + + private final RuntimeException nextException; + + private int position; + + private IteratorThrowingAtEnd( + int length, + RuntimeException hasNextException, + RuntimeException nextException) { + this.content = ByteBufferUtils.byteArrayOfLength(length); + this.hasNextException = hasNextException; + this.nextException = nextException; + } + + @Override + public synchronized boolean hasNext() { + if (position >= content.length && hasNextException != null) { + throw hasNextException; + } + // We always instruct to proceed, so `next()` can throw + return true; + } + + @Override + public synchronized byte[] next() { + if (position < content.length) { + return new byte[]{content[position++]}; + } + assertNotNull(nextException); + throw nextException; + } + + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws Exception { + if ("testOOM".equals(args[0])) { + testOOM(); + } else { + throw new IllegalArgumentException("Unknown arguments: " + List.of(args)); + } + } + + private static void testOOM() throws Exception { + + // Create the publisher + int length = ByteBufferUtils.findLengthExceedingMaxMemory(); + Iterable iterable = createIterableOfLength(length); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofByteArrays(iterable); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the received content length + final int[] readLength = {0}; + subscriber.drainToAccumulator(subscription, 1, buffer -> readLength[0] += buffer.limit()); + assertEquals(length, readLength[0]); + + } + + private static Iterable createIterableOfLength(int length) { + return () -> new Iterator<>() { + + // Instead of emitting `length` at once, doing it gradually using a buffer to avoid OOM. + private final byte[] buffer = new byte[8192]; + + private volatile int remainingLength = length; + + @Override + public boolean hasNext() { + return remainingLength > 0; + } + + @Override + public synchronized byte[] next() { + if (remainingLength >= buffer.length) { + remainingLength -= buffer.length; + return buffer; + } else { + byte[] remainingBuffer = new byte[remainingLength]; + remainingLength = 0; + return remainingBuffer; + } + } + + }; + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java new file mode 100644 index 00000000000..7705faef109 --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Flow; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofFile` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfFileTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Xmx64m OfFileTest testOOM + */ + +public class OfFileTest { + + private static final Path DEFAULT_FS_DIR = Path.of(System.getProperty("user.dir", ".")); + + private static final FileSystem ZIP_FS = zipFs(); + + private static final Path ZIP_FS_DIR = ZIP_FS.getRootDirectories().iterator().next(); + + private static final List PARENT_DIRS = List.of(DEFAULT_FS_DIR, ZIP_FS_DIR); + + private static FileSystem zipFs() { + try { + Path zipFile = DEFAULT_FS_DIR.resolve("file.zip"); + return FileSystems.newFileSystem(zipFile, Map.of("create", "true")); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + @AfterAll + static void closeZipFs() throws IOException { + ZIP_FS.close(); + } + + static List parentDirs() { + return PARENT_DIRS; + } + + @Test + void testNullPath() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofFile(null)); + } + + @ParameterizedTest + @MethodSource("parentDirs") + void testNonExistentPath(Path parentDir) { + Path nonExistentPath = createFilePath(parentDir, "testNonExistentPath"); + assertThrows(FileNotFoundException.class, () -> HttpRequest.BodyPublishers.ofFile(nonExistentPath)); + } + + @ParameterizedTest + @MethodSource("parentDirs") + void testNonExistentPathAtSubscribe(Path parentDir) throws Exception { + + // Create the publisher + byte[] fileBytes = ByteBufferUtils.byteArrayOfLength(3); + Path filePath = createFile(parentDir, "testNonExistentPathAtSubscribe", fileBytes); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Delete the file + Files.delete(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileBytes.length); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + FileNotFoundException actualException = (FileNotFoundException) subscriber.invocations.take(); + String actualExceptionMessage = actualException.getMessage(); + assertTrue( + actualExceptionMessage.contains("Not a regular file"), + "Unexpected message: " + actualExceptionMessage); + + } + + @ParameterizedTest + @MethodSource("parentDirs") + void testIrregularFile(Path parentDir) throws Exception { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(parentDir); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, Files.size(parentDir)); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + FileNotFoundException actualException = (FileNotFoundException) subscriber.invocations.take(); + String actualExceptionMessage = actualException.getMessage(); + assertTrue( + actualExceptionMessage.contains("Not a regular file"), + "Unexpected message: " + actualExceptionMessage); + + } + + /** + * A big enough file length to observe the effects of file + * modification whilst the file is getting read. + */ + private static final int BIG_FILE_LENGTH = 8 * 1024 * 1024; // 8 MiB + + @ParameterizedTest + @MethodSource("parentDirs") + void testFileModificationWhileReading(Path parentDir) throws Exception { + + // ZIP file system (sadly?) consumes the entire content at open. + // Hence, we cannot observe the effect of file modification while reading. + if (parentDir == ZIP_FS_DIR) { + return; + } + + // Create the publisher + byte[] fileBytes = ByteBufferUtils.byteArrayOfLength(BIG_FILE_LENGTH); + Path filePath = createFile(parentDir, "testFileModificationWhileReading", fileBytes); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileBytes.length); + + // Verify the state after the 1st `request()` + subscription.request(1); + assertEquals("onNext", subscriber.invocations.take()); + ByteBuffer buffer1 = (ByteBuffer) subscriber.invocations.take(); + assertTrue(buffer1.limit() > 0, "unexpected empty buffer"); + List buffers = new ArrayList<>(); + buffers.add(buffer1); + + // Truncate the file + Files.write(filePath, new byte[0]); + + // Drain emissions until completion, and verify the content + byte[] readBytes = subscriber.drainToByteArray(subscription, Long.MAX_VALUE, buffers); + assertTrue( + readBytes.length < fileBytes.length, + "was expecting less than the total amount (%s bytes), found: %s".formatted( + fileBytes.length, readBytes.length)); + ByteBuffer expectedReadBytes = ByteBuffer.wrap(fileBytes, 0, readBytes.length); + ByteBufferUtils.assertEquals(expectedReadBytes, ByteBuffer.wrap(readBytes), null); + + } + + static Stream testFileOfLengthParams() { + return PARENT_DIRS + .stream() + .flatMap(parentDir -> Stream + .of(0, 1, 2, 3, BIG_FILE_LENGTH) + .map(fileLength -> Arguments.of(parentDir, fileLength))); + } + + @ParameterizedTest + @MethodSource("testFileOfLengthParams") + void testFileOfLength(Path parentDir, int fileLength) throws Exception { + + // Create the publisher + byte[] fileBytes = ByteBufferUtils.byteArrayOfLength(fileLength); + Path filePath = createFile(parentDir, "testFileOfLength", fileBytes); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileBytes.length); + + // Drain emissions until completion, and verify the received content + byte[] readBytes = subscriber.drainToByteArray(subscription, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(fileBytes, readBytes, null); + + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws Exception { + if ("testOOM".equals(args[0])) { + testOOM(); + } else { + throw new IllegalArgumentException("Unknown arguments: " + List.of(args)); + } + } + + private static void testOOM() { + for (Path parentDir : PARENT_DIRS) { + try { + testOOM(parentDir); + } catch (Exception exception) { + throw new AssertionError("failed for parent directory: " + parentDir, exception); + } + } + } + + private static void testOOM(Path parentDir) throws Exception { + + // Create the publisher + int fileLength = ByteBufferUtils.findLengthExceedingMaxMemory(); + Path filePath = createFileOfLength(parentDir, "testOOM", fileLength); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofFile(filePath); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, fileLength); + + // Drain emissions until completion, and verify the received content length + final int[] readLength = {0}; + subscriber.drainToAccumulator(subscription, 1, buffer -> readLength[0] += buffer.limit()); + assertEquals(fileLength, readLength[0]); + + } + + private static Path createFileOfLength(Path parentDir, String identifier, int fileLength) throws IOException { + Path filePath = createFilePath(parentDir, identifier); + try (OutputStream fileStream = Files.newOutputStream(filePath)) { + byte[] buffer = ByteBufferUtils.byteArrayOfLength(8192); + for (int writtenLength = 0; writtenLength < fileLength; writtenLength += buffer.length) { + int remainingLength = fileLength - writtenLength; + byte[] effectiveBuffer = remainingLength < buffer.length + ? ByteBufferUtils.byteArrayOfLength(remainingLength) + : buffer; + fileStream.write(effectiveBuffer); + } + } + return filePath; + } + + private static Path createFile(Path parentDir, String identifier, byte[] fileBytes) throws IOException { + Path filePath = createFilePath(parentDir, identifier); + Files.write(filePath, fileBytes); + return filePath; + } + + private static Path createFilePath(Path parentDir, String identifier) { + String fileName = identifier.replaceAll("\\W*", ""); + return parentDir.resolve(fileName); + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfInputStreamTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfInputStreamTest.java new file mode 100644 index 00000000000..7688c1674ee --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfInputStreamTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.http.HttpRequest; +import java.util.List; +import java.util.concurrent.Flow; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofInputStream` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfInputStreamTest + * + * @comment Using `main/othervm` to initiate tests that depend on a custom-configured JVM + * @run main/othervm -Xmx64m OfInputStreamTest testOOM + */ + +public class OfInputStreamTest { + + @Test + void testNullInputStreamSupplier() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofInputStream(null)); + } + + @Test + void testThrowingInputStreamSupplier() throws InterruptedException { + + // Create the publisher + RuntimeException exception = new RuntimeException(); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> { throw exception; }); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + IOException actualException = (IOException) subscriber.invocations.take(); + assertEquals("Stream supplier has failed", actualException.getMessage()); + assertSame(exception, actualException.getCause()); + + } + + @Test + void testNullInputStream() throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> null); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the state after `request()` + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + IOException actualException = (IOException) subscriber.invocations.take(); + assertEquals("Stream supplier returned null", actualException.getMessage()); + + } + + @Test + void testInputStreamSupplierInvocations() throws InterruptedException { + + // Create a publisher from an `InputStream` supplier returning a different instance at each invocation + byte[] buffer1 = ByteBufferUtils.byteArrayOfLength(10); + byte[] buffer2 = ByteBufferUtils.byteArrayOfLength(10); + int[] inputStreamSupplierInvocationCount = {0}; + Supplier inputStreamSupplier = () -> + switch (++inputStreamSupplierInvocationCount[0]) { + case 1 -> new ByteArrayInputStream(buffer1); + case 2 -> new ByteArrayInputStream(buffer2); + default -> throw new AssertionError(); + }; + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(inputStreamSupplier); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription1 = subscriber.verifyAndSubscribe(publisher, -1); + Flow.Subscription subscription2 = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain each subscription and verify the received content + byte[] actualBuffer1 = subscriber.drainToByteArray(subscription1, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(buffer1, actualBuffer1, null); + byte[] actualBuffer2 = subscriber.drainToByteArray(subscription2, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(buffer2, actualBuffer2, null); + + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testInputStreamOfLength(int length) throws InterruptedException { + + // Create the publisher + byte[] content = ByteBufferUtils.byteArrayOfLength(length); + InputStream inputStream = new ByteArrayInputStream(content); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> inputStream); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the received content + byte[] actualContent = subscriber.drainToByteArray(subscription, Long.MAX_VALUE); + ByteBufferUtils.assertEquals(content, actualContent, null); + + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testThrowingInputStream(int exceptionIndex) throws InterruptedException { + + // Create the publisher + RuntimeException exception = new RuntimeException("failure for `read`"); + InputStream inputStream = new InputStreamThrowingOnCompletion(exceptionIndex, exception); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofInputStream(() -> inputStream); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Verify the failure + subscription.request(1); + assertEquals("onError", subscriber.invocations.take()); + Exception actualException = (Exception) subscriber.invocations.take(); + assertSame(exception, actualException); + + } + + private static final class InputStreamThrowingOnCompletion extends InputStream { + + private final int length; + + private final RuntimeException exception; + + private int position; + + private InputStreamThrowingOnCompletion(int length, RuntimeException exception) { + this.length = length; + this.exception = exception; + } + + @Override + public synchronized int read() { + if (position < length) { + return position++ & 0xFF; + } + throw exception; + } + + } + + /** + * Initiates tests that depend on a custom-configured JVM. + */ + public static void main(String[] args) throws Exception { + if ("testOOM".equals(args[0])) { + testOOM(); + } else { + throw new IllegalArgumentException("Unknown arguments: " + List.of(args)); + } + } + + private static void testOOM() throws InterruptedException { + + // Create the publisher using an `InputStream` that emits content exceeding the maximum memory + int length = ByteBufferUtils.findLengthExceedingMaxMemory(); + HttpRequest.BodyPublisher publisher = + HttpRequest.BodyPublishers.ofInputStream(() -> new InputStream() { + + private int position; + + @Override + public synchronized int read() { + return position < length ? (position++ & 0xFF) : -1; + } + + }); + + // Subscribe + RecordingSubscriber subscriber = new RecordingSubscriber(); + Flow.Subscription subscription = subscriber.verifyAndSubscribe(publisher, -1); + + // Drain emissions until completion, and verify the received content length + final int[] readLength = {0}; + subscriber.drainToAccumulator(subscription, 1, buffer -> readLength[0] += buffer.limit()); + assertEquals(length, readLength[0]); + + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfStringTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfStringTest.java new file mode 100644 index 00000000000..143b5ce17da --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfStringTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Flow; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @bug 8364733 + * @summary Verify all specified `HttpRequest.BodyPublishers::ofString` behavior + * @build ByteBufferUtils + * RecordingSubscriber + * @run junit OfStringTest + */ + +class OfStringTest { + + private static final Charset CHARSET = StandardCharsets.US_ASCII; + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + void testContentOfLength(int length) throws InterruptedException { + + // Create the publisher + char[] contentChars = new char[length]; + for (int i = 0; i < length; i++) { + contentChars[i] = (char) ('a' + i); + } + String content = new String(contentChars); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofString(content, CHARSET); + + // Subscribe + assertEquals(length, publisher.contentLength()); + RecordingSubscriber subscriber = new RecordingSubscriber(); + publisher.subscribe(subscriber); + assertEquals("onSubscribe", subscriber.invocations.take()); + Flow.Subscription subscription = (Flow.Subscription) subscriber.invocations.take(); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + if (length > 0) { + assertEquals("onNext", subscriber.invocations.take()); + String actualContent = CHARSET.decode((ByteBuffer) subscriber.invocations.take()).toString(); + assertEquals(content, actualContent); + } + assertEquals("onComplete", subscriber.invocations.take()); + + } + + @ParameterizedTest + @CsvSource({ + "a,UTF-8", + "b,UTF-16", + "ı,ISO-8859-9" + }) + void testCharset(String content, Charset charset) throws InterruptedException { + + // Create the publisher + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofString(content, charset); + + // Subscribe + ByteBuffer expectedBuffer = charset.encode(content); + assertEquals(expectedBuffer.limit(), publisher.contentLength()); + RecordingSubscriber subscriber = new RecordingSubscriber(); + publisher.subscribe(subscriber); + assertEquals("onSubscribe", subscriber.invocations.take()); + Flow.Subscription subscription = (Flow.Subscription) subscriber.invocations.take(); + + // Verify the state after `request()` + subscription.request(Long.MAX_VALUE); + assertEquals("onNext", subscriber.invocations.take()); + ByteBuffer actualBuffer = (ByteBuffer) subscriber.invocations.take(); + ByteBufferUtils.assertEquals(expectedBuffer, actualBuffer, null); + assertEquals("onComplete", subscriber.invocations.take()); + + } + + @Test + void testNullContent() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofString(null, CHARSET)); + } + + @Test + void testNullCharset() { + assertThrows(NullPointerException.class, () -> HttpRequest.BodyPublishers.ofString("foo", null)); + } + +} diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/RecordingSubscriber.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/RecordingSubscriber.java new file mode 100644 index 00000000000..0cd43c635ae --- /dev/null +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/RecordingSubscriber.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.net.http.HttpRequest; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Flow; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test subscriber recording received invocations. + */ +public final class RecordingSubscriber implements Flow.Subscriber { + + public final BlockingQueue invocations = new LinkedBlockingQueue<>(); + + @Override + public void onSubscribe(Flow.Subscription subscription) { + invocations.add("onSubscribe"); + invocations.add(subscription); + } + + @Override + public void onNext(ByteBuffer item) { + invocations.add("onNext"); + invocations.add(item); + } + + @Override + public synchronized void onError(Throwable throwable) { + invocations.add("onError"); + invocations.add(throwable); + } + + @Override + public synchronized void onComplete() { + invocations.add("onComplete"); + } + + /** + * Verifies the content length of the given publisher and subscribes to it. + */ + public Flow.Subscription verifyAndSubscribe(HttpRequest.BodyPublisher publisher, long contentLength) + throws InterruptedException { + assertEquals(contentLength, publisher.contentLength()); + publisher.subscribe(this); + assertEquals("onSubscribe", invocations.take()); + return (Flow.Subscription) invocations.take(); + } + + /** + * {@return the byte sequence collected by draining all emissions until completion} + * + * @param subscription a subscription to drain from + * @param itemCount the number of items to request per iteration + */ + public byte[] drainToByteArray(Flow.Subscription subscription, long itemCount) throws InterruptedException { + return drainToByteArray(subscription, itemCount, new ArrayList<>()); + } + + /** + * {@return the byte sequence collected by draining all emissions until completion} + * + * @param subscription a subscription to drain from + * @param itemCount the number of items to request per iteration + * @param buffers a list to accumulate the received content in + */ + public byte[] drainToByteArray(Flow.Subscription subscription, long itemCount, List buffers) + throws InterruptedException { + drainToAccumulator(subscription, itemCount, buffers::add); + return flattenBuffers(buffers); + } + + /** + * Drains all emissions until completion to the given {@code accumulator}. + * + * @param subscription a subscription to drain from + * @param itemCount the number of items to request per iteration + * @param accumulator an accumulator to pass the received content to + */ + public void drainToAccumulator( + Flow.Subscription subscription, long itemCount, Consumer accumulator) + throws InterruptedException { + boolean completed = false; + while (!completed) { + subscription.request(itemCount); + String op = (String) invocations.take(); + if ("onNext".equals(op)) { + ByteBuffer buffer = (ByteBuffer) invocations.take(); + accumulator.accept(buffer); + } else if ("onComplete".equals(op)) { + completed = true; + } else { + throw new AssertionError("Unexpected invocation: " + op); + } + } + } + + private static byte[] flattenBuffers(List buffers) { + int arrayLength = buffers.stream().mapToInt(ByteBuffer::limit).sum(); + byte[] array = new byte[arrayLength]; + for (int bufferIndex = 0, arrayOffset = 0; bufferIndex < buffers.size(); bufferIndex++) { + ByteBuffer buffer = buffers.get(bufferIndex); + int bufferLimit = buffer.limit(); + buffer.get(array, arrayOffset, bufferLimit); + arrayOffset += bufferLimit; + } + return array; + } + +} From 2b7eee4a4c1e8b9421c5db601da83000c344b78e Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Fri, 19 Sep 2025 13:04:18 +0000 Subject: [PATCH 127/556] 8366899: SetupExecute should add the command line to vardeps Reviewed-by: erikj --- make/common/Execute.gmk | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/make/common/Execute.gmk b/make/common/Execute.gmk index 4199e8f13b7..0311c4ecba1 100644 --- a/make/common/Execute.gmk +++ b/make/common/Execute.gmk @@ -148,9 +148,12 @@ define SetupExecuteBody $1_INFO := Running commands for $1 endif + $1_VARDEPS := $$($1_COMMAND) $$($1_PRE_COMMAND) $$($1_POST_COMMAND) + $1_VARDEPS_FILE := $$(call DependOnVariable, $1_VARDEPS) + ifneq ($$($1_PRE_COMMAND), ) - $$($1_PRE_MARKER): $$($1_DEPS) + $$($1_PRE_MARKER): $$($1_DEPS) $$($1_VARDEPS_FILE) ifneq ($$($1_WARN), ) $$(call LogWarn, $$($1_WARN)) endif @@ -176,7 +179,7 @@ define SetupExecuteBody $1 := $$($1_PRE_MARKER) $$($1_EXEC_RESULT) else - $$($1_EXEC_RESULT): $$($1_DEPS) + $$($1_EXEC_RESULT): $$($1_DEPS) $$($1_VARDEPS_FILE) ifneq ($$($1_WARN), ) $$(call LogWarn, $$($1_WARN)) endif From 3798dcf75b547a3707cdfdacf62886648c8653cf Mon Sep 17 00:00:00 2001 From: Artur Barashev Date: Fri, 19 Sep 2025 13:06:25 +0000 Subject: [PATCH 128/556] 8367104: Check for RSASSA-PSS parameters when validating certificates against algorithm constraints Reviewed-by: mullan --- .../provider/certpath/AlgorithmChecker.java | 2 +- .../certpath/PKIXCertPathValidator.java | 7 +- .../security/ssl/SSLAlgorithmConstraints.java | 181 ++++++++--- .../sun/security/ssl/SSLContextImpl.java | 40 +-- .../sun/security/ssl/SSLSessionImpl.java | 13 +- .../sun/security/ssl/SignatureScheme.java | 14 +- .../ssl/X509KeyManagerCertChecking.java | 35 +-- .../security/ssl/X509TrustManagerImpl.java | 43 +-- .../sun/security/validator/PKIXValidator.java | 3 +- ...NotAllowedInTLS13CertificateSignature.java | 12 +- .../SignatureScheme/RsaSsaPssConstraints.java | 274 +++++++++++++++++ .../CertChainAlgorithmConstraints.java | 284 ++++++++++++++++++ 12 files changed, 743 insertions(+), 165 deletions(-) create mode 100644 test/jdk/sun/security/ssl/SignatureScheme/RsaSsaPssConstraints.java create mode 100644 test/jdk/sun/security/ssl/X509TrustManagerImpl/CertChainAlgorithmConstraints.java diff --git a/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java b/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java index f127ac65cc7..4acc0833c5d 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/AlgorithmChecker.java @@ -221,7 +221,7 @@ public final class AlgorithmChecker extends PKIXCertPathChecker { currSigAlg, prevPubKey, currSigAlgParams)) { throw new CertPathValidatorException( "Algorithm constraints check failed on " + - currSigAlg + "signature and " + + currSigAlg + " signature and " + currPubKey.getAlgorithm() + " key with size of " + sun.security.util.KeyUtil.getKeySize(currPubKey) + "bits", diff --git a/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java b/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java index 270d10e82fa..397e8093cf7 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/PKIXCertPathValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -212,6 +212,11 @@ public final class PKIXCertPathValidator extends CertPathValidatorSpi { ((RevocationChecker)checker).init(anchor, params); } } + + // Set trust anchor for the user-specified AlgorithmChecker. + if (checker instanceof AlgorithmChecker algChecker) { + algChecker.trySetTrustAnchor(anchor); + } } // only add a RevocationChecker if revocation is enabled and // a PKIXRevocationChecker has not already been added diff --git a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java index 88cdfbca5ff..95cfc6082be 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java @@ -29,19 +29,30 @@ import java.security.AlgorithmConstraints; import java.security.AlgorithmParameters; import java.security.CryptoPrimitive; import java.security.Key; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.PSSParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Set; +import java.util.TreeSet; import javax.net.ssl.*; import sun.security.util.DisabledAlgorithmConstraints; import static sun.security.util.DisabledAlgorithmConstraints.*; /** * Algorithm constraints for disabled algorithms property - * + *

        * See the "jdk.certpath.disabledAlgorithms" specification in java.security * for the syntax of the disabled algorithm string. */ final class SSLAlgorithmConstraints implements AlgorithmConstraints { + public enum SIGNATURE_CONSTRAINTS_MODE { + PEER, // Check against peer supported signatures + LOCAL // Check against local supported signatures + } + private static final DisabledAlgorithmConstraints tlsDisabledAlgConstraints = new DisabledAlgorithmConstraints(PROPERTY_TLS_DISABLED_ALGS, new SSLAlgorithmDecomposer()); @@ -57,14 +68,15 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // the default algorithm constraints static final SSLAlgorithmConstraints DEFAULT = - new SSLAlgorithmConstraints(null, true); + new SSLAlgorithmConstraints(null, true); // the default SSL only algorithm constraints static final SSLAlgorithmConstraints DEFAULT_SSL_ONLY = - new SSLAlgorithmConstraints(null, false); + new SSLAlgorithmConstraints(null, false); - private SSLAlgorithmConstraints(AlgorithmConstraints userSpecifiedConstraints, - boolean enabledX509DisabledAlgConstraints) { + private SSLAlgorithmConstraints( + AlgorithmConstraints userSpecifiedConstraints, + boolean enabledX509DisabledAlgConstraints) { this(userSpecifiedConstraints, null, enabledX509DisabledAlgConstraints); } @@ -81,10 +93,12 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the provided * {@code userSpecifiedConstraints} in addition to standard checks. * Returns a singleton instance if parameter is null or DEFAULT. + * * @param userSpecifiedConstraints additional constraints to check * @return a SSLAlgorithmConstraints instance */ - static SSLAlgorithmConstraints wrap(AlgorithmConstraints userSpecifiedConstraints) { + static SSLAlgorithmConstraints wrap( + AlgorithmConstraints userSpecifiedConstraints) { return wrap(userSpecifiedConstraints, true); } @@ -102,23 +116,24 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the constraints * configured for the given {@code socket} in addition to standard checks. * Returns a singleton instance if the constraints are null or DEFAULT. + * * @param socket socket with configured constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE * @return a SSLAlgorithmConstraints instance */ - static AlgorithmConstraints forSocket(SSLSocket socket, - boolean withDefaultCertPathConstraints) { - AlgorithmConstraints userSpecifiedConstraints = - getUserSpecifiedConstraints(socket); - return wrap(userSpecifiedConstraints, withDefaultCertPathConstraints); - } - static SSLAlgorithmConstraints forSocket( SSLSocket socket, - String[] supportedAlgorithms, + SIGNATURE_CONSTRAINTS_MODE mode, boolean withDefaultCertPathConstraints) { + + if (socket == null) { + return wrap(null, withDefaultCertPathConstraints); + } + return new SSLAlgorithmConstraints( nullIfDefault(getUserSpecifiedConstraints(socket)), - new SupportedSignatureAlgorithmConstraints(supportedAlgorithms), + new SupportedSignatureAlgorithmConstraints( + socket.getHandshakeSession(), mode), withDefaultCertPathConstraints); } @@ -126,23 +141,24 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { * Returns a SSLAlgorithmConstraints instance that checks the constraints * configured for the given {@code engine} in addition to standard checks. * Returns a singleton instance if the constraints are null or DEFAULT. + * * @param engine engine with configured constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE * @return a SSLAlgorithmConstraints instance */ - static AlgorithmConstraints forEngine(SSLEngine engine, - boolean withDefaultCertPathConstraints) { - AlgorithmConstraints userSpecifiedConstraints = - getUserSpecifiedConstraints(engine); - return wrap(userSpecifiedConstraints, withDefaultCertPathConstraints); - } - static SSLAlgorithmConstraints forEngine( SSLEngine engine, - String[] supportedAlgorithms, + SIGNATURE_CONSTRAINTS_MODE mode, boolean withDefaultCertPathConstraints) { + + if (engine == null) { + return wrap(null, withDefaultCertPathConstraints); + } + return new SSLAlgorithmConstraints( nullIfDefault(getUserSpecifiedConstraints(engine)), - new SupportedSignatureAlgorithmConstraints(supportedAlgorithms), + new SupportedSignatureAlgorithmConstraints( + engine.getHandshakeSession(), mode), withDefaultCertPathConstraints); } @@ -159,7 +175,7 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // Please check the instance before casting to use SSLEngineImpl. if (engine instanceof SSLEngineImpl) { HandshakeContext hc = - ((SSLEngineImpl)engine).conContext.handshakeContext; + ((SSLEngineImpl) engine).conContext.handshakeContext; if (hc != null) { return hc.sslConfig.userSpecifiedAlgorithmConstraints; } @@ -179,7 +195,7 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { // Please check the instance before casting to use SSLSocketImpl. if (socket instanceof SSLSocketImpl) { HandshakeContext hc = - ((SSLSocketImpl)socket).conContext.handshakeContext; + ((SSLSocketImpl) socket).conContext.handshakeContext; if (hc != null) { return hc.sslConfig.userSpecifiedAlgorithmConstraints; } @@ -279,15 +295,55 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { } private static class SupportedSignatureAlgorithmConstraints - implements AlgorithmConstraints { - // supported signature algorithms - private final String[] supportedAlgorithms; + implements AlgorithmConstraints { - SupportedSignatureAlgorithmConstraints(String[] supportedAlgorithms) { - if (supportedAlgorithms != null) { - this.supportedAlgorithms = supportedAlgorithms.clone(); - } else { - this.supportedAlgorithms = null; + // Supported signature algorithms + private Set supportedAlgorithms; + // Supported signature schemes + private List supportedSignatureSchemes; + private boolean checksDisabled; + + SupportedSignatureAlgorithmConstraints( + SSLSession session, SIGNATURE_CONSTRAINTS_MODE mode) { + + if (mode == null + || !(session instanceof ExtendedSSLSession extSession + // "signature_algorithms_cert" TLS extension is only + // available starting with TLSv1.2. + && ProtocolVersion.useTLS12PlusSpec( + extSession.getProtocol()))) { + + checksDisabled = true; + return; + } + + supportedAlgorithms = new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + + switch (mode) { + case SIGNATURE_CONSTRAINTS_MODE.PEER: + supportedAlgorithms.addAll(Arrays.asList(extSession + .getPeerSupportedSignatureAlgorithms())); + break; + case SIGNATURE_CONSTRAINTS_MODE.LOCAL: + supportedAlgorithms.addAll(Arrays.asList(extSession + .getLocalSupportedSignatureAlgorithms())); + } + + // Do additional SignatureSchemes checks for in-house + // ExtendedSSLSession implementation. + if (extSession instanceof SSLSessionImpl sslSessionImpl) { + switch (mode) { + case SIGNATURE_CONSTRAINTS_MODE.PEER: + supportedSignatureSchemes = new ArrayList<>( + sslSessionImpl + .getPeerSupportedSignatureSchemes()); + break; + case SIGNATURE_CONSTRAINTS_MODE.LOCAL: + supportedSignatureSchemes = new ArrayList<>( + sslSessionImpl + .getLocalSupportedSignatureSchemes()); + } } } @@ -295,6 +351,10 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { + if (checksDisabled) { + return true; + } + if (algorithm == null || algorithm.isEmpty()) { throw new IllegalArgumentException( "No algorithm name specified"); @@ -305,24 +365,11 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { "No cryptographic primitive specified"); } - if (supportedAlgorithms == null || - supportedAlgorithms.length == 0) { + if (supportedAlgorithms == null || supportedAlgorithms.isEmpty()) { return false; } - // trim the MGF part: withand - int position = algorithm.indexOf("and"); - if (position > 0) { - algorithm = algorithm.substring(0, position); - } - - for (String supportedAlgorithm : supportedAlgorithms) { - if (algorithm.equalsIgnoreCase(supportedAlgorithm)) { - return true; - } - } - - return false; + return supportedAlgorithms.contains(algorithm); } @Override @@ -339,7 +386,41 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { "No algorithm name specified"); } - return permits(primitives, algorithm, parameters); + return permits(primitives, algorithm, parameters) + && checkRsaSsaPssParams(algorithm, key, parameters); + } + + // Additional check for RSASSA-PSS signature algorithm parameters. + private boolean checkRsaSsaPssParams( + String algorithm, Key key, AlgorithmParameters parameters) { + + if (supportedSignatureSchemes == null + || key == null + || parameters == null + || !"RSASSA-PSS".equalsIgnoreCase(algorithm)) { + return true; + } + + try { + String keyAlg = key.getAlgorithm(); + String paramDigestAlg = parameters.getParameterSpec( + PSSParameterSpec.class).getDigestAlgorithm(); + + return supportedSignatureSchemes.stream().anyMatch(ss -> + ss.algorithm.equalsIgnoreCase(algorithm) + && ss.keyAlgorithm.equalsIgnoreCase(keyAlg) + && ((PSSParameterSpec) ss.signAlgParams.parameterSpec) + .getDigestAlgorithm() + .equalsIgnoreCase(paramDigestAlg)); + + } catch (InvalidParameterSpecException e) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Invalid AlgorithmParameters: " + + parameters + "; Error: " + e.getMessage()); + } + + return true; + } } } } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java index f09270aa9e6..a0cb28201e9 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,7 @@ import java.util.*; import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.*; import sun.security.provider.certpath.AlgorithmChecker; +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.validator.Validator; /** @@ -1449,22 +1450,8 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager identityAlg, checkClientTrusted); } - // try the best to check the algorithm constraints - AlgorithmConstraints constraints; - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - if (session instanceof ExtendedSSLSession extSession) { - String[] peerSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forSocket( - sslSocket, peerSupportedSignAlgs, true); - } else { - constraints = - SSLAlgorithmConstraints.forSocket(sslSocket, true); - } - } else { - constraints = SSLAlgorithmConstraints.forSocket(sslSocket, true); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.LOCAL, true); checkAlgorithmConstraints(chain, constraints, checkClientTrusted); } @@ -1474,6 +1461,7 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager String authType, SSLEngine engine, boolean checkClientTrusted) throws CertificateException { if (engine != null) { + SSLSession session = engine.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); @@ -1487,22 +1475,8 @@ final class AbstractTrustManagerWrapper extends X509ExtendedTrustManager identityAlg, checkClientTrusted); } - // try the best to check the algorithm constraints - AlgorithmConstraints constraints; - if (ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - if (session instanceof ExtendedSSLSession extSession) { - String[] peerSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forEngine( - engine, peerSupportedSignAlgs, true); - } else { - constraints = - SSLAlgorithmConstraints.forEngine(engine, true); - } - } else { - constraints = SSLAlgorithmConstraints.forEngine(engine, true); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, true); checkAlgorithmConstraints(chain, constraints, checkClientTrusted); } diff --git a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java index 5eb9f72af46..1bf561c47e6 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLSessionImpl.java @@ -1388,10 +1388,10 @@ final class SSLSessionImpl extends ExtendedSSLSession { } /** - * Gets an array of supported signature schemes that the local side is + * Gets a collection of supported signature schemes that the local side is * willing to verify. */ - public Collection getLocalSupportedSignatureSchemes() { + Collection getLocalSupportedSignatureSchemes() { return localSupportedSignAlgs; } @@ -1404,6 +1404,15 @@ final class SSLSessionImpl extends ExtendedSSLSession { return SignatureScheme.getAlgorithmNames(peerSupportedSignAlgs); } + /** + * Gets a collection of supported signature schemes that the peer is + * willing to verify. Those are sent with the "signature_algorithms_cert" + * TLS extension. + */ + Collection getPeerSupportedSignatureSchemes() { + return peerSupportedSignAlgs; + } + /** * Obtains a List containing all {@link SNIServerName}s * of the requested Server Name Indication (SNI) extension. diff --git a/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java b/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java index b3ed5810c56..043a0d84c61 100644 --- a/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java +++ b/src/java.base/share/classes/sun/security/ssl/SignatureScheme.java @@ -146,12 +146,12 @@ enum SignatureScheme { "RSA", 511, ProtocolVersion.PROTOCOLS_TO_12); - final int id; // hash + signature - final String name; // literal name - private final String algorithm; // signature algorithm - final String keyAlgorithm; // signature key algorithm - private final SigAlgParamSpec signAlgParams; // signature parameters - private final NamedGroup namedGroup; // associated named group + final int id; // hash + signature + final String name; // literal name + final String algorithm; // signature algorithm + final String keyAlgorithm; // signature key algorithm + final SigAlgParamSpec signAlgParams; // signature parameters + private final NamedGroup namedGroup; // associated named group // The minimal required key size in bits. // @@ -185,7 +185,7 @@ enum SignatureScheme { RSA_PSS_SHA384 ("SHA-384", 48), RSA_PSS_SHA512 ("SHA-512", 64); - private final AlgorithmParameterSpec parameterSpec; + final AlgorithmParameterSpec parameterSpec; private final AlgorithmParameters parameters; private final boolean isAvailable; diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java index 6f18b80395a..162a938cddb 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java @@ -39,16 +39,15 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.StandardConstants; import javax.net.ssl.X509ExtendedKeyManager; import javax.security.auth.x500.X500Principal; import sun.security.provider.certpath.AlgorithmChecker; +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.util.KnownOIDs; import sun.security.validator.Validator; @@ -168,19 +167,8 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { } if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) { - SSLSession session = sslSocket.getHandshakeSession(); - - if (session instanceof ExtendedSSLSession extSession - && ProtocolVersion.useTLS12PlusSpec( - extSession.getProtocol())) { - // Use peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - return SSLAlgorithmConstraints.forSocket(sslSocket, - extSession.getPeerSupportedSignatureAlgorithms(), - true); - } - - return SSLAlgorithmConstraints.forSocket(sslSocket, true); + return SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.PEER, true); } return SSLAlgorithmConstraints.DEFAULT; @@ -193,21 +181,8 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { return null; } - if (engine != null) { - SSLSession session = engine.getHandshakeSession(); - - if (session instanceof ExtendedSSLSession extSession - && ProtocolVersion.useTLS12PlusSpec( - extSession.getProtocol())) { - // Use peer supported certificate signature algorithms - // sent with "signature_algorithms_cert" TLS extension. - return SSLAlgorithmConstraints.forEngine(engine, - extSession.getPeerSupportedSignatureAlgorithms(), - true); - } - } - - return SSLAlgorithmConstraints.forEngine(engine, true); + return SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true); } // Algorithm constraints check. diff --git a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java index 58794e5dce8..5001181fecf 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,6 +31,7 @@ import java.security.cert.*; import java.util.*; import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.*; +import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.util.AnchorCertificates; import sun.security.util.HostnameChecker; import sun.security.validator.*; @@ -198,32 +199,19 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager Validator v = checkTrustedInit(chain, authType, checkClientTrusted); X509Certificate[] trustedChain; - if ((socket != null) && socket.isConnected() && - (socket instanceof SSLSocket sslSocket)) { + if (socket instanceof SSLSocket sslSocket && sslSocket.isConnected()) { SSLSession session = sslSocket.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); } - // create the algorithm constraints - boolean isExtSession = (session instanceof ExtendedSSLSession); - AlgorithmConstraints constraints; - if (isExtSession && - ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - ExtendedSSLSession extSession = (ExtendedSSLSession)session; - String[] localSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forSocket( - sslSocket, localSupportedSignAlgs, false); - } else { - constraints = SSLAlgorithmConstraints.forSocket(sslSocket, false); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forSocket( + sslSocket, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); // Grab any stapled OCSP responses for use in validation List responseList = Collections.emptyList(); - if (!checkClientTrusted && isExtSession) { + if (!checkClientTrusted && session instanceof ExtendedSSLSession) { responseList = ((ExtendedSSLSession)session).getStatusResponses(); } @@ -255,29 +243,18 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager X509Certificate[] trustedChain; if (engine != null) { + SSLSession session = engine.getHandshakeSession(); if (session == null) { throw new CertificateException("No handshake session"); } - // create the algorithm constraints - boolean isExtSession = (session instanceof ExtendedSSLSession); - AlgorithmConstraints constraints; - if (isExtSession && - ProtocolVersion.useTLS12PlusSpec(session.getProtocol())) { - ExtendedSSLSession extSession = (ExtendedSSLSession)session; - String[] localSupportedSignAlgs = - extSession.getLocalSupportedSignatureAlgorithms(); - - constraints = SSLAlgorithmConstraints.forEngine( - engine, localSupportedSignAlgs, false); - } else { - constraints = SSLAlgorithmConstraints.forEngine(engine, false); - } + AlgorithmConstraints constraints = SSLAlgorithmConstraints.forEngine( + engine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); // Grab any stapled OCSP responses for use in validation List responseList = Collections.emptyList(); - if (!checkClientTrusted && isExtSession) { + if (!checkClientTrusted && session instanceof ExtendedSSLSession) { responseList = ((ExtendedSSLSession)session).getStatusResponses(); } diff --git a/src/java.base/share/classes/sun/security/validator/PKIXValidator.java b/src/java.base/share/classes/sun/security/validator/PKIXValidator.java index 7cbca031cdb..be9d041a4bc 100644 --- a/src/java.base/share/classes/sun/security/validator/PKIXValidator.java +++ b/src/java.base/share/classes/sun/security/validator/PKIXValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -261,7 +261,6 @@ public final class PKIXValidator extends Validator { // apparently issued by trust anchor? X509Certificate last = chain[chain.length - 1]; X500Principal issuer = last.getIssuerX500Principal(); - X500Principal subject = last.getSubjectX500Principal(); if (trustedSubjects.containsKey(issuer)) { return doValidate(chain, pkixParameters); } diff --git a/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java b/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java index 2fa046e1dc2..4b14f0c28ce 100644 --- a/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java +++ b/test/jdk/sun/security/ssl/SignatureScheme/MD5NotAllowedInTLS13CertificateSignature.java @@ -33,7 +33,6 @@ * @run main/othervm MD5NotAllowedInTLS13CertificateSignature */ -import static jdk.test.lib.Asserts.assertEquals; import static jdk.test.lib.Asserts.assertTrue; import static jdk.test.lib.Utils.runAndCheckException; @@ -99,11 +98,12 @@ public class MD5NotAllowedInTLS13CertificateSignature extends serverEx -> { Throwable clientEx = serverEx.getSuppressed()[0]; assertTrue(clientEx instanceof SSLHandshakeException); - assertEquals(clientEx.getMessage(), "(bad_certificate) " - + "PKIX path validation failed: " - + "java.security.cert.CertPathValidatorException: " - + "Algorithm constraints check failed on signature" - + " algorithm: MD5withRSA"); + assertTrue(clientEx.getMessage().startsWith( + "(bad_certificate)" + + " PKIX path validation failed: " + + "java.security.cert.CertPathValidatorException:" + + " Algorithm constraints check failed on " + + "MD5withRSA signature and RSA key")); }); // Should run fine on TLSv1.2. diff --git a/test/jdk/sun/security/ssl/SignatureScheme/RsaSsaPssConstraints.java b/test/jdk/sun/security/ssl/SignatureScheme/RsaSsaPssConstraints.java new file mode 100644 index 00000000000..f72f16f5374 --- /dev/null +++ b/test/jdk/sun/security/ssl/SignatureScheme/RsaSsaPssConstraints.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertTrue; +import static jdk.test.lib.Utils.runAndCheckException; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManagerFactory; +import jdk.test.lib.security.CertificateBuilder; +import sun.security.x509.AuthorityKeyIdentifierExtension; +import sun.security.x509.GeneralName; +import sun.security.x509.GeneralNames; +import sun.security.x509.KeyIdentifier; +import sun.security.x509.SerialNumber; +import sun.security.x509.X500Name; + + +/* + * @test + * @bug 8367104 + * @summary Check for RSASSA-PSS parameters when validating certificates + * against algorithm constraints. + * @modules java.base/sun.security.x509 + * java.base/sun.security.util + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS Rsa_pss_pss_Sha384 true + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS RsaSsa-Pss true + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS rsa_pss_Rsae_sha384 true + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS Rsa true + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS RSASSA-pSS true + * @run main/othervm RsaSsaPssConstraints RSA SHA384withRSA rsa_pkcs1_Sha384 true + * @run main/othervm RsaSsaPssConstraints EC SHA384withECDSA Ecdsa_Secp384r1_sha384 true + * @run main/othervm RsaSsaPssConstraints RSA SHA384withRSA SHA384withRsA true + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS rsa_pss_rsae_sha384 false + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS rsa_pss_pss_sha384 false + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha256 false + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha512 false + * @run main/othervm RsaSsaPssConstraints RSASSA-PSS RSASSA-PSS RSA false + * @run main/othervm RsaSsaPssConstraints RSA RSASSA-PSS rsa_pss_rsae_sha512 false + * @run main/othervm RsaSsaPssConstraints RSA SHA384withRSA rsa_pkcs1_sha256 false + * @run main/othervm RsaSsaPssConstraints EC SHA384withECDSA ecdsa_secp256r1_sha256 false + * @run main/othervm RsaSsaPssConstraints EC SHA384withECDSA SHA512withECDSA false + */ + +public class RsaSsaPssConstraints extends SSLSocketTemplate { + + private final String protocol; + private final String keyAlg; + private final String certSigAlg; + private X509Certificate trustedCert; + private X509Certificate serverCert; + private X509Certificate clientCert; + private KeyPair serverKeys; + private KeyPair clientKeys; + + protected RsaSsaPssConstraints( + String protocol, String keyAlg, + String certSigAlg) throws Exception { + super(); + this.protocol = protocol; + this.keyAlg = keyAlg; + this.certSigAlg = certSigAlg; + setupCertificates(); + } + + public static void main(String[] args) throws Exception { + if (args.length != 4) { + throw new RuntimeException("Wrong number of arguments"); + } + + String keyAlg = args[0]; + String certSigAlg = args[1]; + String constraintAlgo = args[2]; + boolean fail = Boolean.parseBoolean(args[3]); + + // Note: CertificateBuilder generates RSASSA-PSS certificate + // signature using SHA-384 digest algorithm by default. + Security.setProperty("jdk.tls.disabledAlgorithms", + constraintAlgo + " usage CertificateSignature"); + + for (String protocol : new String[]{"TLSv1.3", "TLSv1.2"}) { + var test = new RsaSsaPssConstraints(protocol, keyAlg, certSigAlg); + + final String errorMsg = protocol.equals("TLSv1.2") ? + "no cipher suites in common" : + "No available authentication scheme"; + + if (fail) { + runAndCheckException(test::run, + serverEx -> { + assertTrue( + serverEx instanceof SSLHandshakeException); + assertEquals(serverEx.getMessage(), + "(handshake_failure) " + errorMsg); + }); + } else { + test.run(); + } + } + + // Disable KeyManager's algorithm constraints checking and + // check against TrustManager's local supported signature + // algorithms on the client side. + System.setProperty( + "jdk.tls.SunX509KeyManager.certChecking", "false"); + + for (String protocol : new String[]{"TLSv1.3", "TLSv1.2"}) { + var test = new RsaSsaPssConstraints(protocol, keyAlg, certSigAlg); + + if (fail) { + runAndCheckException(test::run, + serverEx -> { + Throwable clientEx = serverEx.getSuppressed()[0]; + assertTrue(clientEx instanceof SSLHandshakeException + || serverEx instanceof SSLHandshakeException); + }); + } else { + test.run(); + } + } + } + + @Override + public SSLContext createServerSSLContext() throws Exception { + return getSSLContext( + trustedCert, serverCert, serverKeys.getPrivate(), protocol); + } + + @Override + public SSLContext createClientSSLContext() throws Exception { + return getSSLContext( + trustedCert, clientCert, clientKeys.getPrivate(), protocol); + } + + private static SSLContext getSSLContext( + X509Certificate trustedCertificate, X509Certificate keyCertificate, + PrivateKey privateKey, String protocol) + throws Exception { + + // create a key store + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + + // import the trusted cert + ks.setCertificateEntry("TLS Signer", trustedCertificate); + + // generate certificate chain + Certificate[] chain = new Certificate[2]; + chain[0] = keyCertificate; + chain[1] = trustedCertificate; + + // import the key entry. + final char[] passphrase = "passphrase".toCharArray(); + ks.setKeyEntry("Whatever", privateKey, passphrase, chain); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(ks); + + // create SSL context + SSLContext ctx = SSLContext.getInstance(protocol); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + return ctx; + } + + // Certificate-building helper methods. + + private void setupCertificates() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlg); + KeyPair caKeys = kpg.generateKeyPair(); + this.serverKeys = kpg.generateKeyPair(); + this.clientKeys = kpg.generateKeyPair(); + + this.trustedCert = createTrustedCert(caKeys, certSigAlg); + + this.serverCert = customCertificateBuilder( + "O=Some-Org, L=Some-City, ST=Some-State, C=US", + serverKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + + this.clientCert = customCertificateBuilder( + "CN=localhost, OU=SSL-Client, O=Some-Org, L=Some-City, ST=Some-State, C=US", + clientKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + } + + private static X509Certificate createTrustedCert( + KeyPair caKeys, String certSigAlg) + throws Exception { + SecureRandom random = new SecureRandom(); + + KeyIdentifier kid = new KeyIdentifier(caKeys.getPublic()); + GeneralNames gns = new GeneralNames(); + GeneralName name = new GeneralName(new X500Name( + "O=Trusted-Org, L=Some-City, ST=Some-State, C=US")); + gns.add(name); + BigInteger serialNumber = BigInteger.valueOf( + random.nextLong(1000000) + 1); + return customCertificateBuilder( + name.toString(), + caKeys.getPublic(), caKeys.getPublic()) + .setSerialNumber(serialNumber) + .addExtension(new AuthorityKeyIdentifierExtension(kid, gns, + new SerialNumber(serialNumber))) + .addBasicConstraintsExt(true, true, -1) + .build(null, caKeys.getPrivate(), certSigAlg); + } + + private static CertificateBuilder customCertificateBuilder( + String subjectName, PublicKey publicKey, PublicKey caKey) + throws CertificateException, IOException { + SecureRandom random = new SecureRandom(); + + CertificateBuilder builder = new CertificateBuilder() + .setSubjectName(subjectName) + .setPublicKey(publicKey) + .setNotBefore( + Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + .setNotAfter(Date.from(Instant.now().plus(1, ChronoUnit.HOURS))) + .setSerialNumber( + BigInteger.valueOf(random.nextLong(1000000) + 1)) + .addSubjectKeyIdExt(publicKey) + .addAuthorityKeyIdExt(caKey); + builder.addKeyUsageExt( + new boolean[]{true, true, true, true, true, true}); + + return builder; + } +} diff --git a/test/jdk/sun/security/ssl/X509TrustManagerImpl/CertChainAlgorithmConstraints.java b/test/jdk/sun/security/ssl/X509TrustManagerImpl/CertChainAlgorithmConstraints.java new file mode 100644 index 00000000000..125fec01a38 --- /dev/null +++ b/test/jdk/sun/security/ssl/X509TrustManagerImpl/CertChainAlgorithmConstraints.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertTrue; +import static jdk.test.lib.Utils.runAndCheckException; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import jdk.test.lib.security.CertificateBuilder; +import sun.security.provider.certpath.SunCertPathBuilderException; +import sun.security.validator.ValidatorException; +import sun.security.x509.AuthorityKeyIdentifierExtension; +import sun.security.x509.GeneralName; +import sun.security.x509.GeneralNames; +import sun.security.x509.KeyIdentifier; +import sun.security.x509.SerialNumber; +import sun.security.x509.X500Name; + +/* + * @test + * @bug 8367104 + * @summary Check for RSASSA-PSS parameters when validating certificates + * against algorithm constraints. + * @modules java.base/sun.security.x509 + * java.base/sun.security.util + * java.base/sun.security.validator + * java.base/sun.security.provider.certpath + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS Rsa_pss_pss_Sha384 true + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS RsaSsa-Pss true + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS rsa_pss_Rsae_sha384 true + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS Rsa true + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS RSASSA-pSS true + * @run main/othervm CertChainAlgorithmConstraints RSA SHA384withRSA rsa_pkcs1_Sha384 true + * @run main/othervm CertChainAlgorithmConstraints EC SHA384withECDSA Ecdsa_Secp384r1_sha384 true + * @run main/othervm CertChainAlgorithmConstraints RSA SHA384withRSA SHA384withRsA true + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS rsa_pss_rsae_sha384 false + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS rsa_pss_pss_sha384 false + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha256 false + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS rsa_pss_pss_sha512 false + * @run main/othervm CertChainAlgorithmConstraints RSASSA-PSS RSASSA-PSS RSA false + * @run main/othervm CertChainAlgorithmConstraints RSA RSASSA-PSS rsa_pss_rsae_sha512 false + * @run main/othervm CertChainAlgorithmConstraints RSA SHA384withRSA rsa_pkcs1_sha256 false + * @run main/othervm CertChainAlgorithmConstraints EC SHA384withECDSA ecdsa_secp256r1_sha256 false + * @run main/othervm CertChainAlgorithmConstraints EC SHA384withECDSA SHA512withECDSA false + */ + +// Testing that an algorithm can be disabled with both a PKIXCertPathValidator +// and a CertPathBuilder code paths. It is somewhat common for JSSE to fall +// back to using a CertPathBuilder to find a valid chain. +public class CertChainAlgorithmConstraints extends SSLEngineTemplate { + + private final String keyAlg; + private final String certSigAlg; + private final boolean fail; + + private X509Certificate trustedCert; + private X509Certificate serverCert; + private X509Certificate linkCert1; + private X509Certificate linkCert2; + + protected CertChainAlgorithmConstraints( + String keyAlg, String certSigAlg, boolean fail) + throws Exception { + super(); + this.keyAlg = keyAlg; + this.certSigAlg = certSigAlg; + this.fail = fail; + setupCertificates(); + } + + public static void main(String[] args) throws Exception { + if (args.length != 4) { + throw new RuntimeException("Wrong number of arguments"); + } + + String keyAlg = args[0]; + String certSigAlg = args[1]; + String constraintAlgo = args[2]; + boolean fail = Boolean.parseBoolean(args[3]); + + // Note: CertificateBuilder generates RSASSA-PSS certificate + // signature using SHA-384 digest algorithm by default. + Security.setProperty("jdk.tls.disabledAlgorithms", + constraintAlgo + " usage CertificateSignature"); + + new CertChainAlgorithmConstraints(keyAlg, certSigAlg, fail).run(); + } + + // Run things in TLS handshake order. + protected void run() throws Exception { + + // Produce client_hello + clientEngine.wrap(clientOut, cTOs); + cTOs.flip(); + + // Consume client_hello. + serverEngine.unwrap(cTOs, serverIn); + runDelegatedTasks(serverEngine); + + // Produce server_hello. + serverEngine.wrap(serverOut, sTOc); + sTOc.flip(); + + // Now that we have a Handshake session attached to the serverEngine, + // do the check. + checkChain(serverEngine); + } + + protected void checkChain(SSLEngine engine) throws Exception { + + // Create a key store. + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + + // Import the trusted cert. + ks.setCertificateEntry("Trusted", trustedCert); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + // Init TrustManager with a Key Store. + tmf.init(ks); + + // Generate a mixed-up certificate chain. + X509Certificate[] mixedUpChain = new X509Certificate[3]; + mixedUpChain[0] = linkCert2; + // Put EE cert between 2 link certs to mix up the chain. + mixedUpChain[1] = serverCert; + mixedUpChain[2] = linkCert1; + + // Generate a valid certificate chain - we should get the same + // results with it but a different code path to be used. + X509Certificate[] validChain = new X509Certificate[3]; + validChain[0] = serverCert; + validChain[1] = linkCert2; + validChain[2] = linkCert1; + + var tm = (X509ExtendedTrustManager) tmf.getTrustManagers()[0]; + + if (fail) { + // Mixed-up chain: CertPathBuilder code path. + runAndCheckException( + () -> tm.checkServerTrusted(mixedUpChain, "RSA", engine), + ex -> { + assertTrue(ex instanceof ValidatorException); + assertTrue( + ex.getCause() instanceof SunCertPathBuilderException); + assertEquals(ex.getMessage(), "PKIX path " + + "building failed: " + + "sun.security.provider.certpath." + + "SunCertPathBuilderException: unable to find " + + "valid certification path to requested target"); + }); + + // Valid chain: PKIXCertPathValidator code path. + runAndCheckException( + () -> tm.checkServerTrusted(validChain, "RSA", engine), + ex -> { + assertTrue(ex instanceof ValidatorException); + assertTrue( + ex.getCause() instanceof CertPathValidatorException); + assertTrue(ex.getMessage().startsWith("PKIX path " + + "validation failed: java.security.cert." + + "CertPathValidatorException: Algorithm " + + "constraints check failed on " + + certSigAlg + " signature and " + + keyAlg + " key")); + }); + } else { + tm.checkServerTrusted(mixedUpChain, "RSA", engine); + tm.checkServerTrusted(validChain, "RSA", engine); + } + } + + // Certificate-building helper methods. + + private void setupCertificates() throws Exception { + var kpg = KeyPairGenerator.getInstance(keyAlg); + var caKeys = kpg.generateKeyPair(); + var serverKeys = kpg.generateKeyPair(); + var linkKeys1 = kpg.generateKeyPair(); + var linkKeys2 = kpg.generateKeyPair(); + + this.trustedCert = createTrustedCert(caKeys, certSigAlg); + + this.linkCert1 = customCertificateBuilder( + "O=Link1, L=Some-City, ST=Some-State, C=US", + linkKeys1.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(true, true, -1) + .build(trustedCert, caKeys.getPrivate(), certSigAlg); + + this.linkCert2 = customCertificateBuilder( + "O=Link2, L=Some-City, ST=Some-State, C=US", + linkKeys2.getPublic(), linkKeys1.getPublic()) + .addBasicConstraintsExt(true, true, -1) + .build(linkCert1, linkKeys1.getPrivate(), certSigAlg); + + this.serverCert = customCertificateBuilder( + "O=Some-Org, L=Some-City, ST=Some-State, C=US", + serverKeys.getPublic(), linkKeys2.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(linkCert2, linkKeys2.getPrivate(), + certSigAlg); + } + + private static X509Certificate createTrustedCert( + KeyPair caKeys, String certSigAlg) + throws Exception { + SecureRandom random = new SecureRandom(); + + KeyIdentifier kid = new KeyIdentifier(caKeys.getPublic()); + GeneralNames gns = new GeneralNames(); + GeneralName name = new GeneralName(new X500Name( + "O=Trusted-Org, L=Some-City, ST=Some-State, C=US")); + gns.add(name); + BigInteger serialNumber = BigInteger.valueOf( + random.nextLong(1000000) + 1); + return customCertificateBuilder( + name.toString(), + caKeys.getPublic(), caKeys.getPublic()) + .setSerialNumber(serialNumber) + .addExtension(new AuthorityKeyIdentifierExtension(kid, gns, + new SerialNumber(serialNumber))) + .addBasicConstraintsExt(true, true, -1) + .build(null, caKeys.getPrivate(), certSigAlg); + } + + private static CertificateBuilder customCertificateBuilder( + String subjectName, PublicKey publicKey, PublicKey caKey) + throws CertificateException, IOException { + SecureRandom random = new SecureRandom(); + + CertificateBuilder builder = new CertificateBuilder() + .setSubjectName(subjectName) + .setPublicKey(publicKey) + .setNotBefore( + Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + .setNotAfter(Date.from(Instant.now().plus(1, ChronoUnit.HOURS))) + .setSerialNumber( + BigInteger.valueOf(random.nextLong(1000000) + 1)) + .addSubjectKeyIdExt(publicKey) + .addAuthorityKeyIdExt(caKey); + builder.addKeyUsageExt( + new boolean[]{true, true, true, true, true, true}); + + return builder; + } +} From 54206943a1715083a680f8c987b69f2e44e948c1 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Fri, 19 Sep 2025 14:15:37 +0000 Subject: [PATCH 129/556] 8367651: Parallel: Remove workers number checking in constructor of PSAdjustTask Reviewed-by: iwalulya, tschatzl --- src/hotspot/share/gc/parallel/psParallelCompact.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.cpp b/src/hotspot/share/gc/parallel/psParallelCompact.cpp index dd8455104a9..8c02e135379 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.cpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.cpp @@ -1359,9 +1359,7 @@ public: _nworkers(nworkers) { ClassLoaderDataGraph::verify_claimed_marks_cleared(ClassLoaderData::_claim_stw_fullgc_adjust); - if (nworkers > 1) { - Threads::change_thread_claim_token(); - } + Threads::change_thread_claim_token(); } ~PSAdjustTask() { From 1512d889dee2adb6d4536202dc7f205e5daf6fe7 Mon Sep 17 00:00:00 2001 From: Aleksey Shipilev Date: Fri, 19 Sep 2025 14:26:05 +0000 Subject: [PATCH 130/556] 8348278: Trim InitialRAMPercentage to improve startup in default modes Reviewed-by: eosterlund, sjohanss --- src/hotspot/share/gc/shared/gc_globals.hpp | 2 +- src/java.base/share/man/java.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hotspot/share/gc/shared/gc_globals.hpp b/src/hotspot/share/gc/shared/gc_globals.hpp index 240068f10c0..0b245026d68 100644 --- a/src/hotspot/share/gc/shared/gc_globals.hpp +++ b/src/hotspot/share/gc/shared/gc_globals.hpp @@ -289,7 +289,7 @@ "size on systems with small physical memory size") \ range(0.0, 100.0) \ \ - product(double, InitialRAMPercentage, 1.5625, \ + product(double, InitialRAMPercentage, 0.2, \ "Percentage of real memory used for initial heap size") \ range(0.0, 100.0) \ \ diff --git a/src/java.base/share/man/java.md b/src/java.base/share/man/java.md index 1a6a944594f..d628cfee817 100644 --- a/src/java.base/share/man/java.md +++ b/src/java.base/share/man/java.md @@ -2448,7 +2448,7 @@ Java HotSpot VM. : Sets the initial amount of memory that the JVM will use for the Java heap before applying ergonomics heuristics as a percentage of the maximum amount determined as described in the `-XX:MaxRAM` option. The default value is - 1.5625 percent. + 0.2 percent. The following example shows how to set the percentage of the initial amount of memory used for the Java heap: From 16458b60c9ccdfac60140c8186f31d5d8a57f2f9 Mon Sep 17 00:00:00 2001 From: Leonid Mesnik Date: Fri, 19 Sep 2025 15:20:34 +0000 Subject: [PATCH 131/556] 8367725: Incorrect reading of oop in SuspendResumeManager::suspend while thread is blocked Reviewed-by: pchilanomate, dholmes, sspitsyn --- src/hotspot/share/prims/jvmtiThreadState.cpp | 9 +++++++-- src/hotspot/share/prims/jvmtiThreadState.hpp | 1 + .../share/runtime/suspendResumeManager.cpp | 17 +++++++++++++++-- .../share/runtime/suspendResumeManager.hpp | 5 +++++ .../SelfSuspendDisablerTest.java | 18 ++++++++++++++---- 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/hotspot/share/prims/jvmtiThreadState.cpp b/src/hotspot/share/prims/jvmtiThreadState.cpp index 75b3a0f0157..00a48dec111 100644 --- a/src/hotspot/share/prims/jvmtiThreadState.cpp +++ b/src/hotspot/share/prims/jvmtiThreadState.cpp @@ -714,8 +714,7 @@ JvmtiVTSuspender::register_all_vthreads_resume() { } void -JvmtiVTSuspender::register_vthread_suspend(oop vt) { - int64_t id = java_lang_Thread::thread_id(vt); +JvmtiVTSuspender::register_vthread_suspend(int64_t id) { MutexLocker ml(JvmtiVThreadSuspend_lock, Mutex::_no_safepoint_check_flag); if (_SR_mode == SR_all) { @@ -730,6 +729,12 @@ JvmtiVTSuspender::register_vthread_suspend(oop vt) { } } +void +JvmtiVTSuspender::register_vthread_suspend(oop vt) { + int64_t id = java_lang_Thread::thread_id(vt); + register_vthread_suspend(id); +} + void JvmtiVTSuspender::register_vthread_resume(oop vt) { int64_t id = java_lang_Thread::thread_id(vt); diff --git a/src/hotspot/share/prims/jvmtiThreadState.hpp b/src/hotspot/share/prims/jvmtiThreadState.hpp index 89d4107e216..435964f7720 100644 --- a/src/hotspot/share/prims/jvmtiThreadState.hpp +++ b/src/hotspot/share/prims/jvmtiThreadState.hpp @@ -167,6 +167,7 @@ class JvmtiVTSuspender : AllStatic { public: static void register_all_vthreads_suspend(); static void register_all_vthreads_resume(); + static void register_vthread_suspend(int64_t id); static void register_vthread_suspend(oop vt); static void register_vthread_resume(oop vt); static bool is_vthread_suspended(oop vt); diff --git a/src/hotspot/share/runtime/suspendResumeManager.cpp b/src/hotspot/share/runtime/suspendResumeManager.cpp index 1c770eeec58..1b9606cf633 100644 --- a/src/hotspot/share/runtime/suspendResumeManager.cpp +++ b/src/hotspot/share/runtime/suspendResumeManager.cpp @@ -81,15 +81,28 @@ void SuspendResumeManager::set_suspended(bool is_suspend, bool register_vthread_ AtomicAccess::store(&_suspended, is_suspend); } +void SuspendResumeManager::set_suspended_current_thread(int64_t vthread_id, bool register_vthread_SR) { + assert(_target == JavaThread::current(), "should be current thread"); +#if INCLUDE_JVMTI + if (register_vthread_SR) { + assert(_target->is_vthread_mounted(), "sanity check"); + JvmtiVTSuspender::register_vthread_suspend(vthread_id); + } +#endif + AtomicAccess::store(&_suspended, true); +} + bool SuspendResumeManager::suspend(bool register_vthread_SR) { JVMTI_ONLY(assert(!_target->is_in_VTMS_transition(), "no suspend allowed in VTMS transition");) JavaThread* self = JavaThread::current(); if (_target == self) { // If target is the current thread we can bypass the handshake machinery - // and just suspend directly + // and just suspend directly. + // The vthread() oop must only be accessed before state is set to _thread_blocked. + int64_t id = java_lang_Thread::thread_id(_target->vthread()); ThreadBlockInVM tbivm(self); MutexLocker ml(_state_lock, Mutex::_no_safepoint_check_flag); - set_suspended(true, register_vthread_SR); + set_suspended_current_thread(id, register_vthread_SR); do_owner_suspend(); return true; } else { diff --git a/src/hotspot/share/runtime/suspendResumeManager.hpp b/src/hotspot/share/runtime/suspendResumeManager.hpp index 19b4ed02ef1..ef0dbe17777 100644 --- a/src/hotspot/share/runtime/suspendResumeManager.hpp +++ b/src/hotspot/share/runtime/suspendResumeManager.hpp @@ -59,6 +59,11 @@ class SuspendResumeManager { void set_suspended(bool to, bool register_vthread_SR); + // The specific 'set_suspended' implementation only for self suspend. + // It is called when the thread is already blocked, and it is not possible to + // read oop for vthread from the _target. + void set_suspended_current_thread(int64_t vthread_id, bool register_vthread_SR); + bool is_suspended() { return AtomicAccess::load(&_suspended); } diff --git a/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java index 5af6b1e7322..0c447610013 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java +++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/SelfSuspendDisablerTest/SelfSuspendDisablerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -82,37 +82,47 @@ public class SelfSuspendDisablerTest { }); Thread t2 = Thread.ofVirtual().factory().newThread(() -> { testJvmtiThreadState(Thread.currentThread(), RUNNABLE); - while(!isSuspended(t1)) { + selfSuspend(); + }); + Thread t3 = Thread.ofVirtual().factory().newThread(() -> { + testJvmtiThreadState(Thread.currentThread(), RUNNABLE); + while(!isSuspended(t1) || !isSuspended(t2)) { Thread.yield(); } Thread.yield(); // provoke unmount testJvmtiThreadState(t1, SUSPENDED); + testJvmtiThreadState(t2, SUSPENDED); resume(t1); + resume(t2); suspendAllVirtualThreads(); }); testJvmtiThreadState(t1, NEW); testJvmtiThreadState(t2, NEW); + testJvmtiThreadState(t3, NEW); t1.start(); t2.start(); + t3.start(); - while(!isSuspended(t2)) { + while(!isSuspended(t3)) { sleep(100); } - testJvmtiThreadState(t2, SUSPENDED); + testJvmtiThreadState(t3, SUSPENDED); resumeAllVirtualThreads(); + t3.join(); t2.join(); t1.join(); testJvmtiThreadState(t1, TERMINATED); testJvmtiThreadState(t2, TERMINATED); + testJvmtiThreadState(t3, TERMINATED); } } From 695e36b0031be4d013ad149a0f23c36c0669c422 Mon Sep 17 00:00:00 2001 From: Leonid Mesnik Date: Fri, 19 Sep 2025 15:49:18 +0000 Subject: [PATCH 132/556] 8367927: Remove 8043571-related tests from problemlists Reviewed-by: dholmes, sspitsyn --- test/hotspot/jtreg/ProblemList-Xcomp.txt | 3 --- test/jdk/ProblemList-Xcomp.txt | 3 +-- test/jdk/ProblemList.txt | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/test/hotspot/jtreg/ProblemList-Xcomp.txt b/test/hotspot/jtreg/ProblemList-Xcomp.txt index b30ab329495..03f8ce4352c 100644 --- a/test/hotspot/jtreg/ProblemList-Xcomp.txt +++ b/test/hotspot/jtreg/ProblemList-Xcomp.txt @@ -43,9 +43,6 @@ vmTestbase/vm/mlvm/indy/func/jvmti/mergeCP_indy2none_b/TestDescription.java 8308 vmTestbase/vm/mlvm/indy/func/jvmti/mergeCP_none2indy_b/TestDescription.java 8308367 generic-all vmTestbase/vm/mlvm/indy/func/jvmti/redefineClassInTarget/TestDescription.java 8308367 generic-all -vmTestbase/nsk/jdi/StepRequest/addClassFilter_rt/filter_rt001/TestDescription.java 8043571 generic-all -vmTestbase/nsk/jdi/StepRequest/addClassFilter_rt/filter_rt003/TestDescription.java 8043571 generic-all - vmTestbase/nsk/jvmti/scenarios/capability/CM03/cm03t001/TestDescription.java 8299493 macosx-x64,windows-x64 vmTestbase/nsk/stress/thread/thread006.java 8321476 linux-all diff --git a/test/jdk/ProblemList-Xcomp.txt b/test/jdk/ProblemList-Xcomp.txt index 680806e6e31..5ed171a1fea 100644 --- a/test/jdk/ProblemList-Xcomp.txt +++ b/test/jdk/ProblemList-Xcomp.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -29,4 +29,3 @@ java/lang/invoke/MethodHandles/CatchExceptionTest.java 8146623 generic-all java/lang/reflect/callerCache/ReflectionCallerCacheTest.java 8332028 generic-all -com/sun/jdi/InterruptHangTest.java 8043571 generic-all diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index d1987c873a6..8a4c484e292 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -722,8 +722,6 @@ javax/swing/plaf/synth/7158712/bug7158712.java 8324782 macosx-all # jdk_jdi -com/sun/jdi/RepStep.java 8043571 generic-all - com/sun/jdi/InvokeHangTest.java 8218463 linux-all ############################################################################ From bca1e6e9c394508ae7590d2fcb6587c52a644238 Mon Sep 17 00:00:00 2001 From: Brian Burkhalter Date: Fri, 19 Sep 2025 17:06:23 +0000 Subject: [PATCH 133/556] 8365626: (fs) Improve handling of broken links in Files.isSameFile() (win) Reviewed-by: alanb --- .../classes/sun/nio/fs/WindowsConstants.java | 1 + .../sun/nio/fs/WindowsFileSystemProvider.java | 205 ++++++++++++++---- .../sun/nio/fs/WindowsLinkSupport.java | 6 +- test/jdk/java/nio/file/Files/IsSameFile.java | 30 ++- 4 files changed, 186 insertions(+), 56 deletions(-) diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java b/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java index b1de66ac4f2..47fa16695d2 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsConstants.java @@ -110,6 +110,7 @@ class WindowsConstants { public static final int ERROR_PRIVILEGE_NOT_HELD = 1314; public static final int ERROR_NONE_MAPPED = 1332; public static final int ERROR_CANT_ACCESS_FILE = 1920; + public static final int ERROR_CANT_RESOLVE_FILENAME = 1921; public static final int ERROR_NOT_A_REPARSE_POINT = 4390; public static final int ERROR_INVALID_REPARSE_DATA = 4392; diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java b/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java index 5e740ec1f4d..58e6294cc12 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsFileSystemProvider.java @@ -418,65 +418,178 @@ class WindowsFileSystemProvider } } + /** + * Contains the attributes of a given file system entry and the open + * handle from which they were obtained. The handle must remain open + * until the volume serial number and file index of the attributes + * are no longer needed for comparison with other attributes. + * + * @param attrs the file system entry attributes + * @param handle the open Windows file handle + */ + private record EntryAttributes(WindowsFileAttributes attrs, long handle) { + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof EntryAttributes other) { + WindowsFileAttributes oattrs = other.attrs(); + return oattrs.volSerialNumber() == attrs.volSerialNumber() && + oattrs.fileIndexHigh() == attrs.fileIndexHigh() && + oattrs.fileIndexLow() == attrs.fileIndexLow(); + } + return false; + } + + public int hashCode() { + return attrs.volSerialNumber() + + attrs.fileIndexHigh() + attrs.fileIndexLow(); + } + } + + /** + * Returns the attributes of the file located by the given path if it is a + * symbolic link. The handle contained in the returned value must be closed + * once the attributes are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if no link is found + */ + private EntryAttributes linkAttributes(WindowsPath path) + throws WindowsException + { + long h = INVALID_HANDLE_VALUE; + try { + h = path.openForReadAttributeAccess(false); + } catch (WindowsException x) { + if (x.lastError() != ERROR_FILE_NOT_FOUND && + x.lastError() != ERROR_PATH_NOT_FOUND) + throw x; + return null; + } + + WindowsFileAttributes attrs = null; + try { + attrs = WindowsFileAttributes.readAttributes(h); + } finally { + if (attrs == null || !attrs.isSymbolicLink()) { + CloseHandle(h); + return null; + } + } + + return new EntryAttributes(attrs, h); + } + + /** + * Returns the attributes of the last symbolic link encountered in the + * specified path. Links are not resolved in the path taken as a whole, + * but rather the first link is followed, then its target, and so on, + * until no more links are encountered. The handle contained in the + * returned value must be closed once the attributes are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if no links are found + * @throws FileSystemLoopException if a symbolic link cycle is encountered + */ + private EntryAttributes lastLinkAttributes(WindowsPath path) + throws IOException, WindowsException + { + var linkAttrs = new LinkedHashSet(); + try { + while (path != null) { + EntryAttributes linkAttr = linkAttributes(path); + if (linkAttr == null) + break; + + if (!linkAttrs.add(linkAttr)) { + // the element was not added to the set so close its handle + // here as it would not be closed in the finally block + CloseHandle(linkAttr.handle()); + throw new FileSystemLoopException(path.toString()); + } + + String target = WindowsLinkSupport.readLink(path, linkAttr.handle()); + path = WindowsPath.parse(path.getFileSystem(), target); + } + + if (!linkAttrs.isEmpty()) + return linkAttrs.removeLast(); + } finally { + linkAttrs.stream().forEach(la -> CloseHandle(la.handle())); + } + + return null; + } + + /** + * Returns the attributes of the file located by the supplied parameter + * with all symbolic links in its path resolved. If the file located by + * the resolved path does not exist, then null is returned. The handle + * contained in the returned value must be closed once the attributes + * are no longer needed. + * + * @param path the file system path to examine + * @return the attributes and handle or null if the real path does not exist + */ + private EntryAttributes realPathAttributes(WindowsPath path) + throws WindowsException + { + long h; + try { + h = path.openForReadAttributeAccess(true); + } catch (WindowsException x) { + if (x.lastError() == ERROR_FILE_NOT_FOUND || + x.lastError() == ERROR_PATH_NOT_FOUND || + x.lastError() == ERROR_CANT_RESOLVE_FILENAME) + return null; + + throw x; + } + + WindowsFileAttributes attrs = null; + try { + attrs = WindowsFileAttributes.readAttributes(h); + } catch (WindowsException x) { + CloseHandle(h); + throw x; + } + + return new EntryAttributes(attrs, h); + } + @Override public boolean isSameFile(Path obj1, Path obj2) throws IOException { + // toWindowsPath verifies its argument is a non-null WindowsPath WindowsPath file1 = WindowsPath.toWindowsPath(obj1); if (file1.equals(obj2)) return true; if (obj2 == null) throw new NullPointerException(); - if (!(obj2 instanceof WindowsPath)) + if (!(obj2 instanceof WindowsPath file2)) return false; - WindowsPath file2 = (WindowsPath)obj2; - // open both files and see if they are the same - long h1 = 0L; + EntryAttributes attrs1 = null; + EntryAttributes attrs2 = null; + WindowsPath pathForException = file1; try { - h1 = file1.openForReadAttributeAccess(true); + if ((attrs1 = realPathAttributes(file1)) != null || + (attrs1 = lastLinkAttributes(file1)) != null) { + pathForException = file2; + if ((attrs2 = realPathAttributes(file2)) != null || + (attrs2 = lastLinkAttributes(file2)) != null) + return attrs1.equals(attrs2); + } } catch (WindowsException x) { - if (x.lastError() != ERROR_FILE_NOT_FOUND && - x.lastError() != ERROR_PATH_NOT_FOUND) - x.rethrowAsIOException(file1); - } - - // if file1 does not exist, it cannot equal file2 - if (h1 == 0L) - return false; - - try { - WindowsFileAttributes attrs1 = null; - try { - attrs1 = WindowsFileAttributes.readAttributes(h1); - } catch (WindowsException x) { - x.rethrowAsIOException(file1); - } - long h2 = 0L; - try { - h2 = file2.openForReadAttributeAccess(true); - } catch (WindowsException x) { - if (x.lastError() != ERROR_FILE_NOT_FOUND && - x.lastError() != ERROR_PATH_NOT_FOUND) - x.rethrowAsIOException(file2); - } - - // if file2 does not exist, it cannot equal file1, which does - if (h2 == 0L) - return false; - - try { - WindowsFileAttributes attrs2 = null; - try { - attrs2 = WindowsFileAttributes.readAttributes(h2); - } catch (WindowsException x) { - x.rethrowAsIOException(file2); - } - return WindowsFileAttributes.isSameFile(attrs1, attrs2); - } finally { - CloseHandle(h2); - } + x.rethrowAsIOException(pathForException); } finally { - CloseHandle(h1); + if (attrs1 != null) { + CloseHandle(attrs1.handle()); + if (attrs2 != null) + CloseHandle(attrs2.handle()); + } } + + return false; } @Override diff --git a/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java b/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java index a5adfb73ebc..fc79153914d 100644 --- a/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java +++ b/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java @@ -84,7 +84,7 @@ class WindowsLinkSupport { x.rethrowAsIOException(path); } try { - return readLinkImpl(path, handle); + return readLink(path, handle); } finally { CloseHandle(handle); } @@ -289,9 +289,7 @@ class WindowsLinkSupport { * Returns target of a symbolic link given the handle of an open file * (that should be a link). */ - private static String readLinkImpl(WindowsPath path, long handle) - throws IOException - { + static String readLink(WindowsPath path, long handle) throws IOException { int size = MAXIMUM_REPARSE_DATA_BUFFER_SIZE; try (NativeBuffer buffer = NativeBuffers.getNativeBuffer(size)) { try { diff --git a/test/jdk/java/nio/file/Files/IsSameFile.java b/test/jdk/java/nio/file/Files/IsSameFile.java index 44e03a15a02..ae7cc563472 100644 --- a/test/jdk/java/nio/file/Files/IsSameFile.java +++ b/test/jdk/java/nio/file/Files/IsSameFile.java @@ -22,9 +22,8 @@ */ /* @test - * @bug 8154364 8366254 + * @bug 8154364 8365626 8366254 * @summary Test of Files.isSameFile - * @requires (os.family != "windows") * @library .. /test/lib * @build IsSameFile jdk.test.lib.util.FileUtils * @run junit IsSameFile @@ -50,6 +49,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -66,6 +66,8 @@ public class IsSameFile { private Path c; private List allFiles; + private boolean supportsSymbolicLinks; + @BeforeAll public void init() throws IOException { home = Files.createTempDirectory("TestIsSameFile"); @@ -75,6 +77,12 @@ public class IsSameFile { allFiles.add(aa = home.resolve("a")); allFiles.add(b = home.resolve("b")); allFiles.add(c = home.resolve("c")); + + supportsSymbolicLinks = TestUtil.supportsSymbolicLinks(home); + } + + private boolean supportsSymbolicLinks() { + return supportsSymbolicLinks; } public void deleteFiles() throws IOException { @@ -203,9 +211,11 @@ public class IsSameFile { deleteFiles(); Files.createFile(a); Files.createFile(b); - Files.createSymbolicLink(c, a); List list = new ArrayList(); - list.add(Arguments.of(true, a, c)); + if (supportsSymbolicLinks) { + Files.createSymbolicLink(c, a); + list.add(Arguments.of(true, a, c)); + } list.add(Arguments.of(false, a, b)); return list.stream(); } @@ -220,13 +230,15 @@ public class IsSameFile { private Stream bcExistSource() throws IOException { deleteFiles(); Files.createFile(b); - Files.createSymbolicLink(c, a); List list = new ArrayList(); list.add(Arguments.of(true, a, a)); list.add(Arguments.of(true, a, aa)); list.add(Arguments.of(false, a, b)); list.add(Arguments.of(true, b, b)); - list.add(Arguments.of(false, a, c)); + if (supportsSymbolicLinks) { + list.add(Arguments.of(false, a, c)); + Files.createSymbolicLink(c, a); + } return list.stream(); } @@ -268,6 +280,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("equalFollowingSource") public void equalFollowing(boolean expect, Path x, Path y) @@ -310,6 +323,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("unequalFollowingSource") public void unequalFollowing(boolean expect, Path x, Path y) @@ -347,6 +361,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("unequalNotFollowingSource") public void unequalNotFollowing(boolean expect, Path x, Path y) @@ -382,6 +397,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("multiLinkSource") public void multiLink(boolean expect, Path x, Path y) @@ -419,6 +435,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("multiLinkNoTargetSource") public void multiLinkNoTarget(boolean expect, Path x, Path y) @@ -449,6 +466,7 @@ public class IsSameFile { return list.stream(); } + @EnabledIf("supportsSymbolicLinks") @ParameterizedTest @MethodSource("linkLoopSource") public void linkLoop(boolean expect, Path x, Path y) throws IOException { From 3d4e0491940c4b4a05ac84006933d939370e7e2b Mon Sep 17 00:00:00 2001 From: Srinivas Vamsi Parasa Date: Fri, 19 Sep 2025 18:18:53 +0000 Subject: [PATCH 134/556] 8367780: Enable UseAPX on Intel CPUs only when both APX_F and APX_NCI_NDD_NF cpuid features are present Reviewed-by: sviswanathan, vpaprotski --- src/hotspot/cpu/x86/vm_version_x86.cpp | 15 +++++++++++++-- src/hotspot/cpu/x86/vm_version_x86.hpp | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/hotspot/cpu/x86/vm_version_x86.cpp b/src/hotspot/cpu/x86/vm_version_x86.cpp index f380f2ad271..f0dfdb22745 100644 --- a/src/hotspot/cpu/x86/vm_version_x86.cpp +++ b/src/hotspot/cpu/x86/vm_version_x86.cpp @@ -139,7 +139,7 @@ class VM_Version_StubGenerator: public StubCodeGenerator { const uint32_t CPU_FAMILY_486 = (4 << CPU_FAMILY_SHIFT); bool use_evex = FLAG_IS_DEFAULT(UseAVX) || (UseAVX > 2); - Label detect_486, cpu486, detect_586, std_cpuid1, std_cpuid4, std_cpuid24; + Label detect_486, cpu486, detect_586, std_cpuid1, std_cpuid4, std_cpuid24, std_cpuid29; Label sef_cpuid, sefsl1_cpuid, ext_cpuid, ext_cpuid1, ext_cpuid5, ext_cpuid7; Label ext_cpuid8, done, wrapup, vector_save_restore, apx_save_restore_warning; Label legacy_setup, save_restore_except, legacy_save_restore, start_simd_check; @@ -338,6 +338,16 @@ class VM_Version_StubGenerator: public StubCodeGenerator { __ movl(Address(rsi, 0), rax); __ movl(Address(rsi, 4), rdx); + // + // cpuid(0x29) APX NCI NDD NF (EAX = 29H, ECX = 0). + // + __ bind(std_cpuid29); + __ movl(rax, 0x29); + __ movl(rcx, 0); + __ cpuid(); + __ lea(rsi, Address(rbp, in_bytes(VM_Version::std_cpuid29_offset()))); + __ movl(Address(rsi, 0), rbx); + // // cpuid(0x24) Converged Vector ISA Main Leaf (EAX = 24H, ECX = 0). // @@ -2914,7 +2924,8 @@ VM_Version::VM_Features VM_Version::CpuidInfo::feature_flags() const { if (std_cpuid1_ecx.bits.popcnt != 0) vm_features.set_feature(CPU_POPCNT); if (sefsl1_cpuid7_edx.bits.apx_f != 0 && - xem_xcr0_eax.bits.apx_f != 0) { + xem_xcr0_eax.bits.apx_f != 0 && + std_cpuid29_ebx.bits.apx_nci_ndd_nf != 0) { vm_features.set_feature(CPU_APX_F); } if (std_cpuid1_ecx.bits.avx != 0 && diff --git a/src/hotspot/cpu/x86/vm_version_x86.hpp b/src/hotspot/cpu/x86/vm_version_x86.hpp index 54b3a93d64b..cd8e957ca9a 100644 --- a/src/hotspot/cpu/x86/vm_version_x86.hpp +++ b/src/hotspot/cpu/x86/vm_version_x86.hpp @@ -306,6 +306,14 @@ class VM_Version : public Abstract_VM_Version { } bits; }; + union StdCpuidEax29Ecx0 { + uint32_t value; + struct { + uint32_t apx_nci_ndd_nf : 1, + : 31; + } bits; + }; + union StdCpuid24MainLeafEax { uint32_t value; struct { @@ -591,6 +599,10 @@ protected: StdCpuid24MainLeafEax std_cpuid24_eax; StdCpuid24MainLeafEbx std_cpuid24_ebx; + // cpuid function 0x29 APX Advanced Performance Extensions Leaf + // eax = 0x29, ecx = 0 + StdCpuidEax29Ecx0 std_cpuid29_ebx; + // cpuid function 0xB (processor topology) // ecx = 0 uint32_t tpl_cpuidB0_eax; @@ -711,6 +723,7 @@ public: static ByteSize std_cpuid0_offset() { return byte_offset_of(CpuidInfo, std_max_function); } static ByteSize std_cpuid1_offset() { return byte_offset_of(CpuidInfo, std_cpuid1_eax); } static ByteSize std_cpuid24_offset() { return byte_offset_of(CpuidInfo, std_cpuid24_eax); } + static ByteSize std_cpuid29_offset() { return byte_offset_of(CpuidInfo, std_cpuid29_ebx); } static ByteSize dcp_cpuid4_offset() { return byte_offset_of(CpuidInfo, dcp_cpuid4_eax); } static ByteSize sef_cpuid7_offset() { return byte_offset_of(CpuidInfo, sef_cpuid7_eax); } static ByteSize sefsl1_cpuid7_offset() { return byte_offset_of(CpuidInfo, sefsl1_cpuid7_eax); } @@ -760,7 +773,9 @@ public: _features.set_feature(CPU_SSE2); _features.set_feature(CPU_VZEROUPPER); } - static void set_apx_cpuFeatures() { _features.set_feature(CPU_APX_F); } + static void set_apx_cpuFeatures() { + _features.set_feature(CPU_APX_F); + } static void set_bmi_cpuFeatures() { _features.set_feature(CPU_BMI1); _features.set_feature(CPU_BMI2); From 25a4e26320340cdda082cd45639e73b137ce45a2 Mon Sep 17 00:00:00 2001 From: Man Cao Date: Fri, 19 Sep 2025 19:53:33 +0000 Subject: [PATCH 135/556] 8367613: Test compiler/runtime/TestDontCompileHugeMethods.java failed Reviewed-by: chagedorn, dfenacci --- .../jtreg/compiler/runtime/TestDontCompileHugeMethods.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java b/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java index c5e035edbd0..bd367e7ed5b 100644 --- a/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java +++ b/test/hotspot/jtreg/compiler/runtime/TestDontCompileHugeMethods.java @@ -23,7 +23,7 @@ /** * @test - * @bug 8366118 + * @bug 8366118 8367613 * @summary Check that a huge method is not compiled under -XX:+DontCompileHugeMethods. * @library /test/lib * @run main compiler.runtime.TestDontCompileHugeMethods @@ -99,6 +99,10 @@ public class TestDontCompileHugeMethods { private static void runTest(Path workDir, List jvmArgs) throws Exception { ArrayList command = new ArrayList<>(); + // Disable inlining for shortMethod(). + // Otherwise under "-Xcomp -XX:TieredStopAtLevel=1", shortMethod() is inlined into main(), + // and is not directly executed or compiled. + command.add("-XX:CompileCommand=dontinline,HugeSwitch::shortMethod"); command.add("-XX:+PrintCompilation"); command.add("-Xbatch"); command.addAll(jvmArgs); From 4b544f93ad0e2beae4c80e060cae727d143151ac Mon Sep 17 00:00:00 2001 From: Renjith Kannath Pariyangad Date: Fri, 19 Sep 2025 21:39:41 +0000 Subject: [PATCH 136/556] 8365379: SU3.applyInsets may produce wrong results Reviewed-by: aivanov, prr, serb --- .../com/sun/java/swing/SwingUtilities3.java | 10 ++- .../swing/plaf/basic/BasicMenuItemUI.java | 6 +- .../swing/plaf/synth/SynthGraphicsUtils.java | 14 +--- .../SwingUtilities3/ApplyInsetsTest.java | 65 +++++++++++++++++++ 4 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 test/jdk/com/sun/java/swing/SwingUtilities3/ApplyInsetsTest.java diff --git a/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java b/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java index 91e0f8dc54d..552ef870dbe 100644 --- a/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java +++ b/src/java.desktop/share/classes/com/sun/java/swing/SwingUtilities3.java @@ -146,11 +146,15 @@ public class SwingUtilities3 { } public static void applyInsets(Rectangle rect, Insets insets) { + applyInsets(rect, insets, true); + } + + public static void applyInsets(Rectangle rect, Insets insets, boolean leftToRight) { if (insets != null) { - rect.x += insets.left; + rect.x += leftToRight ? insets.left : insets.right; rect.y += insets.top; - rect.width -= (insets.right + rect.x); - rect.height -= (insets.bottom + rect.y); + rect.width -= (insets.left + insets.right); + rect.height -= (insets.top + insets.bottom); } } diff --git a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java index d361906b291..348d58bab21 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicMenuItemUI.java @@ -682,7 +682,7 @@ public class BasicMenuItemUI extends MenuItemUI g.setFont(mi.getFont()); Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight()); - applyInsets(viewRect, mi.getInsets()); + SwingUtilities3.applyInsets(viewRect, mi.getInsets()); MenuItemLayoutHelper lh = new MenuItemLayoutHelper(mi, checkIcon, arrowIcon, viewRect, defaultTextIconGap, acceleratorDelimiter, @@ -741,10 +741,6 @@ public class BasicMenuItemUI extends MenuItemUI SwingUtilities3.paintArrowIcon(g, lh, lr, foreground); } - private void applyInsets(Rectangle rect, Insets insets) { - SwingUtilities3.applyInsets(rect, insets); - } - /** * Draws the background of the menu item. * diff --git a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java index 95a9aed981a..0a0b25f9383 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/synth/SynthGraphicsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,6 +41,7 @@ import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicHTML; import javax.swing.text.View; +import com.sun.java.swing.SwingUtilities3; import sun.swing.MenuItemLayoutHelper; import sun.swing.SwingUtilities2; @@ -552,15 +553,6 @@ public class SynthGraphicsUtils { return result; } - static void applyInsets(Rectangle rect, Insets insets, boolean leftToRight) { - if (insets != null) { - rect.x += (leftToRight ? insets.left : insets.right); - rect.y += insets.top; - rect.width -= (leftToRight ? insets.right : insets.left) + rect.x; - rect.height -= (insets.bottom + rect.y); - } - } - static void paint(SynthContext context, SynthContext accContext, Graphics g, Icon checkIcon, Icon arrowIcon, String acceleratorDelimiter, int defaultTextIconGap, String propertyPrefix) { @@ -570,7 +562,7 @@ public class SynthGraphicsUtils { Rectangle viewRect = new Rectangle(0, 0, mi.getWidth(), mi.getHeight()); boolean leftToRight = SynthLookAndFeel.isLeftToRight(mi); - applyInsets(viewRect, mi.getInsets(), leftToRight); + SwingUtilities3.applyInsets(viewRect, mi.getInsets(), leftToRight); SynthMenuItemLayoutHelper lh = new SynthMenuItemLayoutHelper( context, accContext, mi, checkIcon, arrowIcon, viewRect, diff --git a/test/jdk/com/sun/java/swing/SwingUtilities3/ApplyInsetsTest.java b/test/jdk/com/sun/java/swing/SwingUtilities3/ApplyInsetsTest.java new file mode 100644 index 00000000000..047eaff66d4 --- /dev/null +++ b/test/jdk/com/sun/java/swing/SwingUtilities3/ApplyInsetsTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.awt.Insets; +import java.awt.Rectangle; + +import com.sun.java.swing.SwingUtilities3; + +/* + * @test + * @bug 8365379 + * @summary Verify SwingUtilities3 insets return correct result + * independent of initial values + * @modules java.desktop/com.sun.java.swing + * @run main ApplyInsetsTest + */ + +public class ApplyInsetsTest { + public static void main(String[] args) { + Rectangle rect = new Rectangle(10, 20, 60, 60); + Insets insets = new Insets(5, 10, 15, 20); + Rectangle expected = + new Rectangle(rect.x + insets.left, + rect.y + insets.top, + rect.width - (insets.left + insets.right), + rect.height - (insets.top + insets.bottom)); + + SwingUtilities3.applyInsets(rect, insets); + if (!rect.equals(expected)) { + throw new RuntimeException("Test failed: expected " + expected + + " but got " + rect); + } + + // Right to left test + rect.setRect(10, 20, 60, 60); + expected.x = rect.x + insets.right; + SwingUtilities3.applyInsets(rect, insets, false); + if (!rect.equals(expected)) { + throw new RuntimeException("Right to left test failed: expected " + + expected + " but got " + rect); + } + + System.out.println("Test passed."); + } +} From 5a684e3196593c4d44ee35f7624246fc461b4af6 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Sat, 20 Sep 2025 06:48:54 +0000 Subject: [PATCH 137/556] 8368029: Several tests in httpserver/simpleserver should throw SkipException Reviewed-by: djelinski, dfuchs --- .../simpleserver/CustomFileSystemTest.java | 98 +++++++------- .../simpleserver/SimpleFileServerTest.java | 126 +++++++++--------- 2 files changed, 116 insertions(+), 108 deletions(-) diff --git a/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java b/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java index d6ec3eb0695..0a46ac1ea8a 100644 --- a/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java +++ b/test/jdk/com/sun/net/httpserver/simpleserver/CustomFileSystemTest.java @@ -256,63 +256,65 @@ public class CustomFileSystemTest { @Test public void testNotReadableFileGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

        File not found

        -

        /aFile.txt

        - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = createDirectoryInCustomFs("testNotReadableFileGET"); - var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

        File not found

        +

        /aFile.txt

        + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = createDirectoryInCustomFs("testNotReadableFileGET"); + var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); - file.toFile().setReadable(false, false); - assert !Files.isReadable(file); + file.toFile().setReadable(false, false); + assert !Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - file.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + file.toFile().setReadable(true, false); } } @Test public void testNotReadableSegmentGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

        File not found

        -

        /dir/aFile.txt

        - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = createDirectoryInCustomFs("testNotReadableSegmentGET"); - var dir = Files.createDirectory(root.resolve("dir")); - var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

        File not found

        +

        /dir/aFile.txt

        + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = createDirectoryInCustomFs("testNotReadableSegmentGET"); + var dir = Files.createDirectory(root.resolve("dir")); + var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); - dir.toFile().setReadable(false, false); - assert !Files.isReadable(dir); - assert Files.isReadable(file); + dir.toFile().setReadable(false, false); + assert !Files.isReadable(dir); + assert Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - dir.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + dir.toFile().setReadable(true, false); } } diff --git a/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java b/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java index ea1c73b361b..167fcd3b5d3 100644 --- a/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java +++ b/test/jdk/com/sun/net/httpserver/simpleserver/SimpleFileServerTest.java @@ -309,63 +309,65 @@ public class SimpleFileServerTest { @Test public void testNotReadableFileGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

        File not found

        -

        /aFile.txt

        - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableFileGET")); - var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

        File not found

        +

        /aFile.txt

        + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableFileGET")); + var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE); - file.toFile().setReadable(false, false); - assert !Files.isReadable(file); + file.toFile().setReadable(false, false); + assert !Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - file.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + file.toFile().setReadable(true, false); } } @Test public void testNotReadableSegmentGET() throws Exception { - if (!Platform.isWindows()) { // not applicable on Windows - var expectedBody = openHTML + """ -

        File not found

        -

        /dir/aFile.txt

        - """ + closeHTML; - var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); - var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableSegmentGET")); - var dir = Files.createDirectory(root.resolve("dir")); - var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var expectedBody = openHTML + """ +

        File not found

        +

        /dir/aFile.txt

        + """ + closeHTML; + var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length); + var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableSegmentGET")); + var dir = Files.createDirectory(root.resolve("dir")); + var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE); - dir.toFile().setReadable(false, false); - assert !Files.isReadable(dir); - assert Files.isReadable(file); + dir.toFile().setReadable(false, false); + assert !Files.isReadable(dir); + assert Files.isReadable(file); - var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); - server.start(); - try { - var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); - var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); - var response = client.send(request, BodyHandlers.ofString()); - assertEquals(response.statusCode(), 404); - assertEquals(response.headers().firstValue("content-length").get(), expectedLength); - assertEquals(response.body(), expectedBody); - } finally { - server.stop(0); - dir.toFile().setReadable(true, false); - } + var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE); + server.start(); + try { + var client = HttpClient.newBuilder().proxy(NO_PROXY).build(); + var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 404); + assertEquals(response.headers().firstValue("content-length").get(), expectedLength); + assertEquals(response.body(), expectedBody); + } finally { + server.stop(0); + dir.toFile().setReadable(true, false); } } @@ -680,18 +682,22 @@ public class SimpleFileServerTest { var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO)); assertTrue(iae.getMessage().contains("does not exist")); } - { // not readable - if (!Platform.isWindows()) { // not applicable on Windows - Path p = Files.createDirectory(TEST_DIR.resolve("aDir")); - p.toFile().setReadable(false, false); - assert !Files.isReadable(p); - try { - var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO)); - assertTrue(iae.getMessage().contains("not readable")); - } finally { - p.toFile().setReadable(true, false); - } - } + } + + @Test + public void testNonReadablePath() throws Exception { + if (Platform.isWindows()) { + throw new SkipException("Not applicable on Windows"); + } + var addr = LOOPBACK_ADDR; + Path p = Files.createDirectory(TEST_DIR.resolve("aDir")); + p.toFile().setReadable(false, false); + assert !Files.isReadable(p); + try { + var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO)); + assertTrue(iae.getMessage().contains("not readable")); + } finally { + p.toFile().setReadable(true, false); } } From b03b6f54c5f538146c3088c4dc2cea70ba70d07a Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Sat, 20 Sep 2025 14:02:31 +0000 Subject: [PATCH 138/556] 8367988: NewFileSystemTests.readOnlyZipFileFailure fails when run by root user Reviewed-by: jpai, bpb --- test/jdk/jdk/nio/zipfs/NewFileSystemTests.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java b/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java index d15988176e3..d9da79be126 100644 --- a/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java +++ b/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java @@ -25,6 +25,7 @@ import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import org.testng.SkipException; import java.io.IOException; import java.net.URI; @@ -35,6 +36,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Iterator; import java.util.Map; +import jdk.test.lib.Platform; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -48,6 +50,7 @@ import static org.testng.Assert.assertTrue; * @bug 8218875 * @summary ZIP File System tests that leverage Files.newFileSystem * @modules jdk.zipfs + * @library /test/lib * @compile NewFileSystemTests.java * @run testng NewFileSystemTests */ @@ -219,6 +222,9 @@ public class NewFileSystemTests { */ @Test public void readOnlyZipFileFailure() throws IOException { + if (Platform.isRoot()) { + throw new SkipException("Test skipped when executed by root user."); + } // Underlying file is read-only. Path readOnlyZip = Utils.createJarFile("read_only.zip", Map.of("file.txt", "Hello World")); // In theory this can fail, and we should avoid unwanted false-negatives. From d21e73dee3dad6332b00f5932bd266b100e9090b Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Sat, 20 Sep 2025 17:26:07 +0000 Subject: [PATCH 139/556] 8366941: Excessive logging in serviceability tests causes timeout Reviewed-by: lmesnik, sspitsyn --- .../jtreg/serviceability/logging/TestBasicLogOutput.java | 4 ++-- .../hotspot/jtreg/serviceability/logging/TestFullNames.java | 4 ++-- .../jtreg/serviceability/logging/TestQuotedLogOutputs.java | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java b/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java index 895e5504feb..b7ed9cc72c8 100644 --- a/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java +++ b/test/hotspot/jtreg/serviceability/logging/TestBasicLogOutput.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ import jdk.test.lib.process.OutputAnalyzer; public class TestBasicLogOutput { public static void main(String[] args) throws Exception { - ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:all=trace", "-version"); + ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:all=info", "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldMatch("\\[logging *\\]"); // expected tag(s) output.shouldContain("Log configuration fully initialized."); // expected message diff --git a/test/hotspot/jtreg/serviceability/logging/TestFullNames.java b/test/hotspot/jtreg/serviceability/logging/TestFullNames.java index 0a3bc855d8f..cd3fcc55772 100644 --- a/test/hotspot/jtreg/serviceability/logging/TestFullNames.java +++ b/test/hotspot/jtreg/serviceability/logging/TestFullNames.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -64,7 +64,7 @@ public class TestFullNames { Asserts.assertFalse(file.exists()); // Run with logging=trace on stdout so that we can verify the log configuration afterwards. ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:logging=trace", - "-Xlog:all=trace:" + logOutput, + "-Xlog:all=info:" + logOutput, "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldHaveExitValue(0); diff --git a/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java b/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java index dea656130ab..db724aeb86a 100644 --- a/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java +++ b/test/hotspot/jtreg/serviceability/logging/TestQuotedLogOutputs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -74,7 +74,7 @@ public class TestQuotedLogOutputs { for (String logOutput : validOutputs) { // Run with logging=trace on stdout so that we can verify the log configuration afterwards. ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:logging=trace", - "-Xlog:all=trace:" + logOutput, + "-Xlog:all=info:" + logOutput, "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldHaveExitValue(0); @@ -99,7 +99,7 @@ public class TestQuotedLogOutputs { }; for (String logOutput : invalidOutputs) { ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:logging=trace", - "-Xlog:all=trace:" + logOutput, + "-Xlog:all=info:" + logOutput, "-version"); OutputAnalyzer output = new OutputAnalyzer(pb.start()); output.shouldHaveExitValue(1); From cc65836d00de7041e7d32e7f15d98108b1ae47a0 Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Sat, 20 Sep 2025 17:28:05 +0000 Subject: [PATCH 140/556] 8367719: Refactor JNI code that uses class_to_verify_considering_redefinition() Reviewed-by: coleenp, dholmes, sspitsyn --- src/hotspot/share/prims/jvm.cpp | 178 ++++++++---------- .../share/prims/jvmtiRedefineClasses.cpp | 6 +- src/hotspot/share/prims/jvmtiThreadState.hpp | 19 +- 3 files changed, 93 insertions(+), 110 deletions(-) diff --git a/src/hotspot/share/prims/jvm.cpp b/src/hotspot/share/prims/jvm.cpp index 2cbe764994d..ade9b45e2eb 100644 --- a/src/hotspot/share/prims/jvm.cpp +++ b/src/hotspot/share/prims/jvm.cpp @@ -2253,12 +2253,26 @@ JVM_END // Reflection for the verifier ///////////////////////////////////////////////////////////////// // RedefineClasses support: bug 6214132 caused verification to fail. -// All functions from this section should call the jvmtiThreadSate function: -// Klass* class_to_verify_considering_redefinition(Klass* klass). -// The function returns a Klass* of the _scratch_class if the verifier -// was invoked in the middle of the class redefinition. -// Otherwise it returns its argument value which is the _the_class Klass*. -// Please, refer to the description in the jvmtiThreadState.hpp. +// All functions from this section, unless noted otherwise, should call the functions +// get_klass_considering_redefinition(), or +// get_instance_klass_considering_redefinition() +// These functions return JvmtiThreadState::_scratch_class if the verifier +// was invoked in the middle of the redefinition of cls. +// See jvmtiThreadState.hpp for details. + +inline Klass* get_klass_considering_redefinition(jclass cls, JavaThread* thread) { + Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); + if (k->is_instance_klass()) { + return JvmtiThreadState::class_to_verify_considering_redefinition(InstanceKlass::cast(k), thread); + } else { + return k; + } +} + +inline InstanceKlass* get_instance_klass_considering_redefinition(jclass cls, JavaThread* thread) { + InstanceKlass* ik = java_lang_Class::as_InstanceKlass(JNIHandles::resolve_non_null(cls)); + return JvmtiThreadState::class_to_verify_considering_redefinition(ik, thread); +} JVM_ENTRY(jboolean, JVM_IsInterface(JNIEnv *env, jclass cls)) oop mirror = JNIHandles::resolve_non_null(cls); @@ -2266,26 +2280,24 @@ JVM_ENTRY(jboolean, JVM_IsInterface(JNIEnv *env, jclass cls)) return JNI_FALSE; } Klass* k = java_lang_Class::as_Klass(mirror); - // This isn't necessary since answer is the same since redefinition + // This isn't necessary since answer is the same because redefinition // has already checked this matches for the scratch class. - // k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); + // k = get_klass_considering_redefinition(cls, thread) jboolean result = k->is_interface(); assert(!result || k->is_instance_klass(), "all interfaces are instance types"); return result; JVM_END - JVM_ENTRY(const char*, JVM_GetClassNameUTF(JNIEnv *env, jclass cls)) + // No need to call get_klass_considering_redefinition() as redefinition cannot change a class's name. Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); return k->name()->as_utf8(); JVM_END JVM_ENTRY(void, JVM_GetClassCPTypes(JNIEnv *env, jclass cls, unsigned char *types)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); + Klass* k = get_klass_considering_redefinition(cls, thread); // types will have length zero if this is not an InstanceKlass // (length is determined by call to JVM_GetClassCPEntriesCount) if (k->is_instance_klass()) { @@ -2299,22 +2311,19 @@ JVM_END JVM_ENTRY(jint, JVM_GetClassCPEntriesCount(JNIEnv *env, jclass cls)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); + Klass* k = get_klass_considering_redefinition(cls, thread); return (!k->is_instance_klass()) ? 0 : InstanceKlass::cast(k)->constants()->length(); JVM_END JVM_ENTRY(jint, JVM_GetClassFieldsCount(JNIEnv *env, jclass cls)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); + Klass* k = get_klass_considering_redefinition(cls, thread); return (!k->is_instance_klass()) ? 0 : InstanceKlass::cast(k)->java_fields_count(); JVM_END JVM_ENTRY(jint, JVM_GetClassMethodsCount(JNIEnv *env, jclass cls)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); + Klass* k = get_klass_considering_redefinition(cls, thread); return (!k->is_instance_klass()) ? 0 : InstanceKlass::cast(k)->methods()->length(); JVM_END @@ -2325,9 +2334,8 @@ JVM_END // by the results of JVM_GetClass{Fields,Methods}Count, which return // zero for arrays. JVM_ENTRY(void, JVM_GetMethodIxExceptionIndexes(JNIEnv *env, jclass cls, jint method_index, unsigned short *exceptions)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); int length = method->checked_exceptions_length(); if (length > 0) { CheckedExceptionElement* table= method->checked_exceptions_start(); @@ -2339,33 +2347,29 @@ JVM_END JVM_ENTRY(jint, JVM_GetMethodIxExceptionsCount(JNIEnv *env, jclass cls, jint method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->checked_exceptions_length(); JVM_END JVM_ENTRY(void, JVM_GetMethodIxByteCode(JNIEnv *env, jclass cls, jint method_index, unsigned char *code)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); memcpy(code, method->code_base(), method->code_size()); JVM_END JVM_ENTRY(jint, JVM_GetMethodIxByteCodeLength(JNIEnv *env, jclass cls, jint method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->code_size(); JVM_END JVM_ENTRY(void, JVM_GetMethodIxExceptionTableEntry(JNIEnv *env, jclass cls, jint method_index, jint entry_index, JVM_ExceptionTableEntryType *entry)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); ExceptionTable extable(method); entry->start_pc = extable.start_pc(entry_index); entry->end_pc = extable.end_pc(entry_index); @@ -2375,81 +2379,71 @@ JVM_END JVM_ENTRY(jint, JVM_GetMethodIxExceptionTableLength(JNIEnv *env, jclass cls, int method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->exception_table_length(); JVM_END JVM_ENTRY(jint, JVM_GetMethodIxModifiers(JNIEnv *env, jclass cls, int method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->access_flags().as_method_flags(); JVM_END JVM_ENTRY(jint, JVM_GetFieldIxModifiers(JNIEnv *env, jclass cls, int field_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - return InstanceKlass::cast(k)->field_access_flags(field_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + return ik->field_access_flags(field_index); JVM_END JVM_ENTRY(jint, JVM_GetMethodIxLocalsCount(JNIEnv *env, jclass cls, int method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->max_locals(); JVM_END JVM_ENTRY(jint, JVM_GetMethodIxArgsSize(JNIEnv *env, jclass cls, int method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->size_of_parameters(); JVM_END JVM_ENTRY(jint, JVM_GetMethodIxMaxStack(JNIEnv *env, jclass cls, int method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->verifier_max_stack(); JVM_END JVM_ENTRY(jboolean, JVM_IsConstructorIx(JNIEnv *env, jclass cls, int method_index)) ResourceMark rm(THREAD); - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->name() == vmSymbols::object_initializer_name(); JVM_END JVM_ENTRY(jboolean, JVM_IsVMGeneratedMethodIx(JNIEnv *env, jclass cls, int method_index)) ResourceMark rm(THREAD); - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->is_overpass(); JVM_END JVM_ENTRY(const char*, JVM_GetMethodIxNameUTF(JNIEnv *env, jclass cls, jint method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->name()->as_utf8(); JVM_END JVM_ENTRY(const char*, JVM_GetMethodIxSignatureUTF(JNIEnv *env, jclass cls, jint method_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - Method* method = InstanceKlass::cast(k)->methods()->at(method_index); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + Method* method = ik->methods()->at(method_index); return method->signature()->as_utf8(); JVM_END @@ -2462,9 +2456,8 @@ JVM_END * constant pool, so we must use cp->uncached_x methods when appropriate. */ JVM_ENTRY(const char*, JVM_GetCPFieldNameUTF(JNIEnv *env, jclass cls, jint cp_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + ConstantPool* cp = ik->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_Fieldref: return cp->uncached_name_ref_at(cp_index)->as_utf8(); @@ -2477,9 +2470,8 @@ JVM_END JVM_ENTRY(const char*, JVM_GetCPMethodNameUTF(JNIEnv *env, jclass cls, jint cp_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + ConstantPool* cp = ik->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_InterfaceMethodref: case JVM_CONSTANT_Methodref: @@ -2493,9 +2485,8 @@ JVM_END JVM_ENTRY(const char*, JVM_GetCPMethodSignatureUTF(JNIEnv *env, jclass cls, jint cp_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + ConstantPool* cp = ik->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_InterfaceMethodref: case JVM_CONSTANT_Methodref: @@ -2509,9 +2500,8 @@ JVM_END JVM_ENTRY(const char*, JVM_GetCPFieldSignatureUTF(JNIEnv *env, jclass cls, jint cp_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + ConstantPool* cp = ik->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_Fieldref: return cp->uncached_signature_ref_at(cp_index)->as_utf8(); @@ -2524,18 +2514,16 @@ JVM_END JVM_ENTRY(const char*, JVM_GetCPClassNameUTF(JNIEnv *env, jclass cls, jint cp_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + ConstantPool* cp = ik->constants(); Symbol* classname = cp->klass_name_at(cp_index); return classname->as_utf8(); JVM_END JVM_ENTRY(const char*, JVM_GetCPFieldClassNameUTF(JNIEnv *env, jclass cls, jint cp_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + ConstantPool* cp = ik->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_Fieldref: { int class_index = cp->uncached_klass_ref_index_at(cp_index); @@ -2551,9 +2539,8 @@ JVM_END JVM_ENTRY(const char*, JVM_GetCPMethodClassNameUTF(JNIEnv *env, jclass cls, jint cp_index)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + ConstantPool* cp = ik->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_Methodref: case JVM_CONSTANT_InterfaceMethodref: { @@ -2570,18 +2557,15 @@ JVM_END JVM_ENTRY(jint, JVM_GetCPFieldModifiers(JNIEnv *env, jclass cls, int cp_index, jclass called_cls)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - Klass* k_called = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(called_cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - k_called = JvmtiThreadState::class_to_verify_considering_redefinition(k_called, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); - ConstantPool* cp_called = InstanceKlass::cast(k_called)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + InstanceKlass* ik_called = get_instance_klass_considering_redefinition(called_cls, thread); + ConstantPool* cp = ik->constants(); + ConstantPool* cp_called = ik_called->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_Fieldref: { Symbol* name = cp->uncached_name_ref_at(cp_index); Symbol* signature = cp->uncached_signature_ref_at(cp_index); - InstanceKlass* ik = InstanceKlass::cast(k_called); - for (JavaFieldStream fs(ik); !fs.done(); fs.next()) { + for (JavaFieldStream fs(ik_called); !fs.done(); fs.next()) { if (fs.name() == name && fs.signature() == signature) { return fs.access_flags().as_field_flags(); } @@ -2597,17 +2581,15 @@ JVM_END JVM_ENTRY(jint, JVM_GetCPMethodModifiers(JNIEnv *env, jclass cls, int cp_index, jclass called_cls)) - Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(cls)); - Klass* k_called = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(called_cls)); - k = JvmtiThreadState::class_to_verify_considering_redefinition(k, thread); - k_called = JvmtiThreadState::class_to_verify_considering_redefinition(k_called, thread); - ConstantPool* cp = InstanceKlass::cast(k)->constants(); + InstanceKlass* ik = get_instance_klass_considering_redefinition(cls, thread); + InstanceKlass* ik_called = get_instance_klass_considering_redefinition(called_cls, thread); + ConstantPool* cp = ik->constants(); switch (cp->tag_at(cp_index).value()) { case JVM_CONSTANT_Methodref: case JVM_CONSTANT_InterfaceMethodref: { Symbol* name = cp->uncached_name_ref_at(cp_index); Symbol* signature = cp->uncached_signature_ref_at(cp_index); - Array* methods = InstanceKlass::cast(k_called)->methods(); + Array* methods = ik_called->methods(); int methods_count = methods->length(); for (int i = 0; i < methods_count; i++) { Method* method = methods->at(i); diff --git a/src/hotspot/share/prims/jvmtiRedefineClasses.cpp b/src/hotspot/share/prims/jvmtiRedefineClasses.cpp index 4d841592501..74192d724f6 100644 --- a/src/hotspot/share/prims/jvmtiRedefineClasses.cpp +++ b/src/hotspot/share/prims/jvmtiRedefineClasses.cpp @@ -99,7 +99,7 @@ VM_RedefineClasses::VM_RedefineClasses(jint class_count, static inline InstanceKlass* get_ik(jclass def) { oop mirror = JNIHandles::resolve_non_null(def); - return InstanceKlass::cast(java_lang_Class::as_Klass(mirror)); + return java_lang_Class::as_InstanceKlass(mirror); } // If any of the classes are being redefined, wait @@ -1310,12 +1310,12 @@ int VM_RedefineClasses::find_new_operand_index(int old_index) { class RedefineVerifyMark : public StackObj { private: JvmtiThreadState* _state; - Klass* _scratch_class; + InstanceKlass* _scratch_class; OopHandle _scratch_mirror; public: - RedefineVerifyMark(Klass* the_class, Klass* scratch_class, + RedefineVerifyMark(InstanceKlass* the_class, InstanceKlass* scratch_class, JvmtiThreadState* state) : _state(state), _scratch_class(scratch_class) { _state->set_class_versions_map(the_class, scratch_class); diff --git a/src/hotspot/share/prims/jvmtiThreadState.hpp b/src/hotspot/share/prims/jvmtiThreadState.hpp index 435964f7720..17bdae4662e 100644 --- a/src/hotspot/share/prims/jvmtiThreadState.hpp +++ b/src/hotspot/share/prims/jvmtiThreadState.hpp @@ -27,6 +27,7 @@ #include "jvmtifiles/jvmti.h" #include "memory/allocation.hpp" +#include "oops/instanceKlass.hpp" #include "oops/oopHandle.hpp" #include "prims/jvmtiEventController.hpp" #include "prims/jvmtiExport.hpp" @@ -209,7 +210,7 @@ class JvmtiThreadState : public CHeapObj { // Used to send class being redefined/retransformed and kind of transform // info to the class file load hook event handler. - Klass* _class_being_redefined; + InstanceKlass* _class_being_redefined; JvmtiClassLoadKind _class_load_kind; GrowableArray* _classes_being_redefined; @@ -372,7 +373,7 @@ class JvmtiThreadState : public CHeapObj { // when class file load hook event is posted. // It is set while loading redefined class and cleared before the // class file load hook event is posted. - inline void set_class_being_redefined(Klass* k, JvmtiClassLoadKind kind) { + inline void set_class_being_redefined(InstanceKlass* k, JvmtiClassLoadKind kind) { _class_being_redefined = k; _class_load_kind = kind; } @@ -382,7 +383,7 @@ class JvmtiThreadState : public CHeapObj { _class_load_kind = jvmti_class_load_kind_load; } - inline Klass* get_class_being_redefined() { + inline InstanceKlass* get_class_being_redefined() { return _class_being_redefined; } @@ -421,12 +422,12 @@ class JvmtiThreadState : public CHeapObj { // used by the verifier, so there is no extra performance issue with it. private: - Klass* _the_class_for_redefinition_verification; - Klass* _scratch_class_for_redefinition_verification; + InstanceKlass* _the_class_for_redefinition_verification; + InstanceKlass* _scratch_class_for_redefinition_verification; public: - inline void set_class_versions_map(Klass* the_class, - Klass* scratch_class) { + inline void set_class_versions_map(InstanceKlass* the_class, + InstanceKlass* scratch_class) { _the_class_for_redefinition_verification = the_class; _scratch_class_for_redefinition_verification = scratch_class; } @@ -434,8 +435,8 @@ class JvmtiThreadState : public CHeapObj { inline void clear_class_versions_map() { set_class_versions_map(nullptr, nullptr); } static inline - Klass* class_to_verify_considering_redefinition(Klass* klass, - JavaThread *thread) { + InstanceKlass* class_to_verify_considering_redefinition(InstanceKlass* klass, + JavaThread* thread) { JvmtiThreadState *state = thread->jvmti_thread_state(); if (state != nullptr && state->_the_class_for_redefinition_verification != nullptr) { if (state->_the_class_for_redefinition_verification == klass) { From e6f8450d957f79beacf2fc70e545db3a4bb58742 Mon Sep 17 00:00:00 2001 From: erifan Date: Mon, 22 Sep 2025 02:03:03 +0000 Subject: [PATCH 141/556] 8363989: AArch64: Add missing backend support of VectorAPI expand operation Reviewed-by: epeter, eliu, xgong --- src/hotspot/cpu/aarch64/aarch64_vector.ad | 39 +++- src/hotspot/cpu/aarch64/aarch64_vector_ad.m4 | 39 +++- src/hotspot/cpu/aarch64/assembler_aarch64.hpp | 7 + .../cpu/aarch64/c2_MacroAssembler_aarch64.cpp | 87 ++++++++ .../cpu/aarch64/c2_MacroAssembler_aarch64.hpp | 6 + test/hotspot/gtest/aarch64/aarch64-asmtest.py | 1 + test/hotspot/gtest/aarch64/asmtest.out.h | 163 +++++++------- .../compiler/lib/ir_framework/IRNode.java | 30 +++ .../compiler/vectorapi/VectorExpandTest.java | 198 ++++++++++++++++++ 9 files changed, 473 insertions(+), 97 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java diff --git a/src/hotspot/cpu/aarch64/aarch64_vector.ad b/src/hotspot/cpu/aarch64/aarch64_vector.ad index 67c4dad27a7..ef35b66003d 100644 --- a/src/hotspot/cpu/aarch64/aarch64_vector.ad +++ b/src/hotspot/cpu/aarch64/aarch64_vector.ad @@ -216,11 +216,6 @@ source %{ return false; } break; - case Op_ExpandV: - if (UseSVE < 2 || is_subword_type(bt)) { - return false; - } - break; case Op_VectorMaskToLong: if (UseSVE > 0 && vlen > 64) { return false; @@ -7113,10 +7108,39 @@ instruct vcompressS(vReg dst, vReg src, pReg pg, ins_pipe(pipe_slow); %} -instruct vexpand(vReg dst, vReg src, pRegGov pg) %{ +instruct vexpand_neon(vReg dst, vReg src, vReg mask, vReg tmp1, vReg tmp2) %{ + predicate(UseSVE == 0); + match(Set dst (ExpandV src mask)); + effect(TEMP_DEF dst, TEMP tmp1, TEMP tmp2); + format %{ "vexpand_neon $dst, $src, $mask\t# KILL $tmp1, $tmp2" %} + ins_encode %{ + BasicType bt = Matcher::vector_element_basic_type(this); + int length_in_bytes = (int) Matcher::vector_length_in_bytes(this); + __ vector_expand_neon($dst$$FloatRegister, $src$$FloatRegister, $mask$$FloatRegister, + $tmp1$$FloatRegister, $tmp2$$FloatRegister, bt, length_in_bytes); + %} + ins_pipe(pipe_slow); +%} + +instruct vexpand_sve(vReg dst, vReg src, pRegGov pg, vReg tmp1, vReg tmp2) %{ + predicate(UseSVE == 1 || (UseSVE == 2 && type2aelembytes(Matcher::vector_element_basic_type(n)) < 4)); + match(Set dst (ExpandV src pg)); + effect(TEMP_DEF dst, TEMP tmp1, TEMP tmp2); + format %{ "vexpand_sve $dst, $src, $pg\t# KILL $tmp1, $tmp2" %} + ins_encode %{ + BasicType bt = Matcher::vector_element_basic_type(this); + int length_in_bytes = (int) Matcher::vector_length_in_bytes(this); + __ vector_expand_sve($dst$$FloatRegister, $src$$FloatRegister, $pg$$PRegister, + $tmp1$$FloatRegister, $tmp2$$FloatRegister, bt, length_in_bytes); + %} + ins_pipe(pipe_slow); +%} + +instruct vexpand_sve2_SD(vReg dst, vReg src, pRegGov pg) %{ + predicate(UseSVE == 2 && type2aelembytes(Matcher::vector_element_basic_type(n)) >= 4); match(Set dst (ExpandV src pg)); effect(TEMP_DEF dst); - format %{ "vexpand $dst, $pg, $src" %} + format %{ "vexpand_sve2_SD $dst, $src, $pg" %} ins_encode %{ // Example input: src = 1 2 3 4 5 6 7 8 // pg = 1 0 0 1 1 0 1 1 @@ -7127,7 +7151,6 @@ instruct vexpand(vReg dst, vReg src, pRegGov pg) %{ // for TBL whose value is used to select the indexed element from src vector. BasicType bt = Matcher::vector_element_basic_type(this); - assert(UseSVE == 2 && !is_subword_type(bt), "unsupported"); Assembler::SIMD_RegVariant size = __ elemType_to_regVariant(bt); // dst = 0 0 0 0 0 0 0 0 __ sve_dup($dst$$FloatRegister, size, 0); diff --git a/src/hotspot/cpu/aarch64/aarch64_vector_ad.m4 b/src/hotspot/cpu/aarch64/aarch64_vector_ad.m4 index 28f91204ec3..012de7e46d8 100644 --- a/src/hotspot/cpu/aarch64/aarch64_vector_ad.m4 +++ b/src/hotspot/cpu/aarch64/aarch64_vector_ad.m4 @@ -206,11 +206,6 @@ source %{ return false; } break; - case Op_ExpandV: - if (UseSVE < 2 || is_subword_type(bt)) { - return false; - } - break; case Op_VectorMaskToLong: if (UseSVE > 0 && vlen > 64) { return false; @@ -5101,10 +5096,39 @@ instruct vcompressS(vReg dst, vReg src, pReg pg, ins_pipe(pipe_slow); %} -instruct vexpand(vReg dst, vReg src, pRegGov pg) %{ +instruct vexpand_neon(vReg dst, vReg src, vReg mask, vReg tmp1, vReg tmp2) %{ + predicate(UseSVE == 0); + match(Set dst (ExpandV src mask)); + effect(TEMP_DEF dst, TEMP tmp1, TEMP tmp2); + format %{ "vexpand_neon $dst, $src, $mask\t# KILL $tmp1, $tmp2" %} + ins_encode %{ + BasicType bt = Matcher::vector_element_basic_type(this); + int length_in_bytes = (int) Matcher::vector_length_in_bytes(this); + __ vector_expand_neon($dst$$FloatRegister, $src$$FloatRegister, $mask$$FloatRegister, + $tmp1$$FloatRegister, $tmp2$$FloatRegister, bt, length_in_bytes); + %} + ins_pipe(pipe_slow); +%} + +instruct vexpand_sve(vReg dst, vReg src, pRegGov pg, vReg tmp1, vReg tmp2) %{ + predicate(UseSVE == 1 || (UseSVE == 2 && type2aelembytes(Matcher::vector_element_basic_type(n)) < 4)); + match(Set dst (ExpandV src pg)); + effect(TEMP_DEF dst, TEMP tmp1, TEMP tmp2); + format %{ "vexpand_sve $dst, $src, $pg\t# KILL $tmp1, $tmp2" %} + ins_encode %{ + BasicType bt = Matcher::vector_element_basic_type(this); + int length_in_bytes = (int) Matcher::vector_length_in_bytes(this); + __ vector_expand_sve($dst$$FloatRegister, $src$$FloatRegister, $pg$$PRegister, + $tmp1$$FloatRegister, $tmp2$$FloatRegister, bt, length_in_bytes); + %} + ins_pipe(pipe_slow); +%} + +instruct vexpand_sve2_SD(vReg dst, vReg src, pRegGov pg) %{ + predicate(UseSVE == 2 && type2aelembytes(Matcher::vector_element_basic_type(n)) >= 4); match(Set dst (ExpandV src pg)); effect(TEMP_DEF dst); - format %{ "vexpand $dst, $pg, $src" %} + format %{ "vexpand_sve2_SD $dst, $src, $pg" %} ins_encode %{ // Example input: src = 1 2 3 4 5 6 7 8 // pg = 1 0 0 1 1 0 1 1 @@ -5115,7 +5139,6 @@ instruct vexpand(vReg dst, vReg src, pRegGov pg) %{ // for TBL whose value is used to select the indexed element from src vector. BasicType bt = Matcher::vector_element_basic_type(this); - assert(UseSVE == 2 && !is_subword_type(bt), "unsupported"); Assembler::SIMD_RegVariant size = __ elemType_to_regVariant(bt); // dst = 0 0 0 0 0 0 0 0 __ sve_dup($dst$$FloatRegister, size, 0); diff --git a/src/hotspot/cpu/aarch64/assembler_aarch64.hpp b/src/hotspot/cpu/aarch64/assembler_aarch64.hpp index a5d2cbfac98..4c4251fbe9f 100644 --- a/src/hotspot/cpu/aarch64/assembler_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/assembler_aarch64.hpp @@ -4068,6 +4068,13 @@ public: INSN(sve_brkb, 0b10); // Break before first true condition #undef INSN + // SVE move prefix (unpredicated) + void sve_movprfx(FloatRegister Zd, FloatRegister Zn) { + starti; + f(0b00000100, 31, 24), f(0b00, 23, 22), f(0b1, 21), f(0b00000, 20, 16); + f(0b101111, 15, 10), rf(Zn, 5), rf(Zd, 0); + } + // Element count and increment scalar (SVE) #define INSN(NAME, TYPE) \ void NAME(Register Xdn, unsigned imm4 = 1, int pattern = 0b11111) { \ diff --git a/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp index b1562c54f4e..b61a0e4e378 100644 --- a/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp @@ -2771,3 +2771,90 @@ void C2_MacroAssembler::select_from_two_vectors(FloatRegister dst, FloatRegister select_from_two_vectors_neon(dst, src1, src2, dst, tmp, vector_length_in_bytes); } } + +// Vector expand implementation. Elements from the src vector are expanded into +// the dst vector under the control of the vector mask. +// Since there are no native instructions directly corresponding to expand before +// SVE2p2, the following implementations mainly leverages the TBL instruction to +// implement expand. To compute the index input for TBL, the prefix sum algorithm +// (https://en.wikipedia.org/wiki/Prefix_sum) is used. The same algorithm is used +// for NEON and SVE, but with different instructions where appropriate. + +// Vector expand implementation for NEON. +// +// An example of 128-bit Byte vector: +// Data direction: high <== low +// Input: +// src = g f e d c b a 9 8 7 6 5 4 3 2 1 +// mask = 0 0 -1 -1 0 0 -1 -1 0 0 -1 -1 0 0 -1 -1 +// Expected result: +// dst = 0 0 8 7 0 0 6 5 0 0 4 3 0 0 2 1 +void C2_MacroAssembler::vector_expand_neon(FloatRegister dst, FloatRegister src, FloatRegister mask, + FloatRegister tmp1, FloatRegister tmp2, BasicType bt, + int vector_length_in_bytes) { + assert(vector_length_in_bytes <= 16, "the vector length in bytes for NEON must be <= 16"); + assert_different_registers(dst, src, mask, tmp1, tmp2); + // Since the TBL instruction only supports byte table, we need to + // compute indices in byte type for all types. + SIMD_Arrangement size = vector_length_in_bytes == 16 ? T16B : T8B; + // tmp1 = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + dup(tmp1, size, zr); + // dst = 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 + negr(dst, size, mask); + // Calculate vector index for TBL with prefix sum algorithm. + // dst = 8 8 8 7 6 6 6 5 4 4 4 3 2 2 2 1 + for (int i = 1; i < vector_length_in_bytes; i <<= 1) { + ext(tmp2, size, tmp1, dst, vector_length_in_bytes - i); + addv(dst, size, tmp2, dst); + } + // tmp2 = 0 0 -1 -1 0 0 -1 -1 0 0 -1 -1 0 0 -1 -1 + orr(tmp2, size, mask, mask); + // tmp2 = 0 0 8 7 0 0 6 5 0 0 4 3 0 0 2 1 + bsl(tmp2, size, dst, tmp1); + // tmp1 = 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + movi(tmp1, size, 1); + // dst = -1 -1 7 6 -1 -1 5 4 -1 -1 3 2 -1 -1 1 0 + subv(dst, size, tmp2, tmp1); + // dst = 0 0 8 7 0 0 6 5 0 0 4 3 0 0 2 1 + tbl(dst, size, src, 1, dst); +} + +// Vector expand implementation for SVE. +// +// An example of 128-bit Short vector: +// Data direction: high <== low +// Input: +// src = gf ed cb a9 87 65 43 21 +// pg = 00 01 00 01 00 01 00 01 +// Expected result: +// dst = 00 87 00 65 00 43 00 21 +void C2_MacroAssembler::vector_expand_sve(FloatRegister dst, FloatRegister src, PRegister pg, + FloatRegister tmp1, FloatRegister tmp2, BasicType bt, + int vector_length_in_bytes) { + assert(UseSVE > 0, "expand implementation only for SVE"); + assert_different_registers(dst, src, tmp1, tmp2); + SIMD_RegVariant size = elemType_to_regVariant(bt); + + // tmp1 = 00 00 00 00 00 00 00 00 + sve_dup(tmp1, size, 0); + sve_movprfx(tmp2, tmp1); + // tmp2 = 00 01 00 01 00 01 00 01 + sve_cpy(tmp2, size, pg, 1, true); + // Calculate vector index for TBL with prefix sum algorithm. + // tmp2 = 04 04 03 03 02 02 01 01 + for (int i = type2aelembytes(bt); i < vector_length_in_bytes; i <<= 1) { + sve_movprfx(dst, tmp1); + // The EXT instruction operates on the full-width sve register. The correct + // index calculation method is: + // vector_length_in_bytes - i + MaxVectorSize - vector_length_in_bytes => + // MaxVectorSize - i. + sve_ext(dst, tmp2, MaxVectorSize - i); + sve_add(tmp2, size, dst, tmp2); + } + // dst = 00 04 00 03 00 02 00 01 + sve_sel(dst, size, pg, tmp2, tmp1); + // dst = -1 03 -1 02 -1 01 -1 00 + sve_sub(dst, size, 1); + // dst = 00 87 00 65 00 43 00 21 + sve_tbl(dst, size, src, dst); +} \ No newline at end of file diff --git a/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.hpp b/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.hpp index 0403a27910f..cb8ded142f4 100644 --- a/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.hpp @@ -204,4 +204,10 @@ FloatRegister index, FloatRegister tmp, BasicType bt, unsigned vector_length_in_bytes); + void vector_expand_neon(FloatRegister dst, FloatRegister src, FloatRegister mask, + FloatRegister tmp1, FloatRegister tmp2, BasicType bt, + int vector_length_in_bytes); + void vector_expand_sve(FloatRegister dst, FloatRegister src, PRegister pg, + FloatRegister tmp1, FloatRegister tmp2, BasicType bt, + int vector_length_in_bytes); #endif // CPU_AARCH64_C2_MACROASSEMBLER_AARCH64_HPP diff --git a/test/hotspot/gtest/aarch64/aarch64-asmtest.py b/test/hotspot/gtest/aarch64/aarch64-asmtest.py index e1abddf3e1c..bf4f2111999 100644 --- a/test/hotspot/gtest/aarch64/aarch64-asmtest.py +++ b/test/hotspot/gtest/aarch64/aarch64-asmtest.py @@ -2135,6 +2135,7 @@ generate(SpecialCases, [["ccmn", "__ ccmn(zr, zr, 3u, Assembler::LE);", ["punpkhi", "__ sve_punpkhi(p1, p0);", "punpkhi\tp1.h, p0.b"], ["compact", "__ sve_compact(z16, __ S, z16, p1);", "compact\tz16.s, p1, z16.s"], ["compact", "__ sve_compact(z16, __ D, z16, p1);", "compact\tz16.d, p1, z16.d"], + ["movprfx", "__ sve_movprfx(z17, z1);", "movprfx\tz17, z1"], ["ext", "__ sve_ext(z17, z16, 63);", "ext\tz17.b, z17.b, z16.b, #63"], ["facgt", "__ sve_fac(Assembler::GT, p1, __ H, p2, z4, z5);", "facgt\tp1.h, p2/z, z4.h, z5.h"], ["facgt", "__ sve_fac(Assembler::GT, p1, __ S, p2, z4, z5);", "facgt\tp1.s, p2/z, z4.s, z5.s"], diff --git a/test/hotspot/gtest/aarch64/asmtest.out.h b/test/hotspot/gtest/aarch64/asmtest.out.h index 7a3225eaed4..352ea33750e 100644 --- a/test/hotspot/gtest/aarch64/asmtest.out.h +++ b/test/hotspot/gtest/aarch64/asmtest.out.h @@ -1148,6 +1148,7 @@ __ sve_punpkhi(p1, p0); // punpkhi p1.h, p0.b __ sve_compact(z16, __ S, z16, p1); // compact z16.s, p1, z16.s __ sve_compact(z16, __ D, z16, p1); // compact z16.d, p1, z16.d + __ sve_movprfx(z17, z1); // movprfx z17, z1 __ sve_ext(z17, z16, 63); // ext z17.b, z17.b, z16.b, #63 __ sve_fac(Assembler::GT, p1, __ H, p2, z4, z5); // facgt p1.h, p2/z, z4.h, z5.h __ sve_fac(Assembler::GT, p1, __ S, p2, z4, z5); // facgt p1.s, p2/z, z4.s, z5.s @@ -1444,30 +1445,30 @@ 0x9101a1a0, 0xb10a5cc8, 0xd10810aa, 0xf10fd061, 0x120cb166, 0x321764bc, 0x52174681, 0x720c0227, 0x9241018e, 0xb25a2969, 0xd278b411, 0xf26aad01, - 0x14000000, 0x17ffffd7, 0x140004b6, 0x94000000, - 0x97ffffd4, 0x940004b3, 0x3400000a, 0x34fffa2a, - 0x3400960a, 0x35000008, 0x35fff9c8, 0x350095a8, - 0xb400000b, 0xb4fff96b, 0xb400954b, 0xb500001d, - 0xb5fff91d, 0xb50094fd, 0x10000013, 0x10fff8b3, - 0x10009493, 0x90000013, 0x36300016, 0x3637f836, - 0x36309416, 0x3758000c, 0x375ff7cc, 0x375893ac, + 0x14000000, 0x17ffffd7, 0x140004b7, 0x94000000, + 0x97ffffd4, 0x940004b4, 0x3400000a, 0x34fffa2a, + 0x3400962a, 0x35000008, 0x35fff9c8, 0x350095c8, + 0xb400000b, 0xb4fff96b, 0xb400956b, 0xb500001d, + 0xb5fff91d, 0xb500951d, 0x10000013, 0x10fff8b3, + 0x100094b3, 0x90000013, 0x36300016, 0x3637f836, + 0x36309436, 0x3758000c, 0x375ff7cc, 0x375893cc, 0x128313a0, 0x528a32c7, 0x7289173b, 0x92ab3acc, 0xd2a0bf94, 0xf2c285e8, 0x9358722f, 0x330e652f, 0x53067f3b, 0x93577c53, 0xb34a1aac, 0xd35a4016, 0x13946c63, 0x93c3dbc8, 0x54000000, 0x54fff5a0, - 0x54009180, 0x54000001, 0x54fff541, 0x54009121, - 0x54000002, 0x54fff4e2, 0x540090c2, 0x54000002, - 0x54fff482, 0x54009062, 0x54000003, 0x54fff423, - 0x54009003, 0x54000003, 0x54fff3c3, 0x54008fa3, - 0x54000004, 0x54fff364, 0x54008f44, 0x54000005, - 0x54fff305, 0x54008ee5, 0x54000006, 0x54fff2a6, - 0x54008e86, 0x54000007, 0x54fff247, 0x54008e27, - 0x54000008, 0x54fff1e8, 0x54008dc8, 0x54000009, - 0x54fff189, 0x54008d69, 0x5400000a, 0x54fff12a, - 0x54008d0a, 0x5400000b, 0x54fff0cb, 0x54008cab, - 0x5400000c, 0x54fff06c, 0x54008c4c, 0x5400000d, - 0x54fff00d, 0x54008bed, 0x5400000e, 0x54ffefae, - 0x54008b8e, 0x5400000f, 0x54ffef4f, 0x54008b2f, + 0x540091a0, 0x54000001, 0x54fff541, 0x54009141, + 0x54000002, 0x54fff4e2, 0x540090e2, 0x54000002, + 0x54fff482, 0x54009082, 0x54000003, 0x54fff423, + 0x54009023, 0x54000003, 0x54fff3c3, 0x54008fc3, + 0x54000004, 0x54fff364, 0x54008f64, 0x54000005, + 0x54fff305, 0x54008f05, 0x54000006, 0x54fff2a6, + 0x54008ea6, 0x54000007, 0x54fff247, 0x54008e47, + 0x54000008, 0x54fff1e8, 0x54008de8, 0x54000009, + 0x54fff189, 0x54008d89, 0x5400000a, 0x54fff12a, + 0x54008d2a, 0x5400000b, 0x54fff0cb, 0x54008ccb, + 0x5400000c, 0x54fff06c, 0x54008c6c, 0x5400000d, + 0x54fff00d, 0x54008c0d, 0x5400000e, 0x54ffefae, + 0x54008bae, 0x5400000f, 0x54ffef4f, 0x54008b4f, 0xd40658e1, 0xd4014d22, 0xd4046543, 0xd4273f60, 0xd44cad80, 0xd503201f, 0xd503203f, 0xd503205f, 0xd503209f, 0xd50320bf, 0xd503219f, 0xd50323bf, @@ -1686,66 +1687,66 @@ 0x25d8e184, 0x2518e407, 0x05214800, 0x05614800, 0x05a14800, 0x05e14800, 0x05214c00, 0x05614c00, 0x05a14c00, 0x05e14c00, 0x05304001, 0x05314001, - 0x05a18610, 0x05e18610, 0x05271e11, 0x6545e891, - 0x6585e891, 0x65c5e891, 0x6545c891, 0x6585c891, - 0x65c5c891, 0x45b0c210, 0x45f1c231, 0x1e601000, - 0x1e603000, 0x1e621000, 0x1e623000, 0x1e641000, - 0x1e643000, 0x1e661000, 0x1e663000, 0x1e681000, - 0x1e683000, 0x1e6a1000, 0x1e6a3000, 0x1e6c1000, - 0x1e6c3000, 0x1e6e1000, 0x1e6e3000, 0x1e701000, - 0x1e703000, 0x1e721000, 0x1e723000, 0x1e741000, - 0x1e743000, 0x1e761000, 0x1e763000, 0x1e781000, - 0x1e783000, 0x1e7a1000, 0x1e7a3000, 0x1e7c1000, - 0x1e7c3000, 0x1e7e1000, 0x1e7e3000, 0xf8268267, - 0xf82d023c, 0xf8301046, 0xf83d2083, 0xf8263290, - 0xf82d528c, 0xf8284299, 0xf8337160, 0xf8386286, - 0xf8bf820e, 0xf8a600e0, 0xf8af1353, 0xf8a922ea, - 0xf8b53396, 0xf8a251e3, 0xf8b340f4, 0xf8a470fd, - 0xf8a06209, 0xf8f48097, 0xf8f002ea, 0xf8eb10d9, - 0xf8ff21b0, 0xf8f7302c, 0xf8ee52a9, 0xf8f041fa, - 0xf8e471e4, 0xf8e863c6, 0xf864823d, 0xf87d013a, - 0xf86f1162, 0xf87d20e3, 0xf86132bb, 0xf870510e, - 0xf8704336, 0xf86572b4, 0xf8706217, 0xb83e8294, - 0xb8200264, 0xb8381284, 0xb8242358, 0xb8333102, - 0xb828530e, 0xb83042df, 0xb824703f, 0xb82a6194, - 0xb8a080e9, 0xb8b80090, 0xb8bb1146, 0xb8bb21b8, - 0xb8b032df, 0xb8b653f4, 0xb8bd41c9, 0xb8b47287, - 0xb8bc6169, 0xb8ee828c, 0xb8e10138, 0xb8f3126d, - 0xb8f020b0, 0xb8e03183, 0xb8e851ef, 0xb8f041e4, - 0xb8fe7005, 0xb8ea6376, 0xb8638120, 0xb873015d, - 0xb8781284, 0xb86723b8, 0xb86e3175, 0xb87b51ed, - 0xb87f41d1, 0xb863721e, 0xb87660f4, 0xce216874, - 0xce104533, 0xce648c15, 0xce8e3302, 0xce6e82ab, - 0xce6c87d1, 0xcec08063, 0xce638937, 0x25e0c358, - 0x25a1c7d3, 0x0580785a, 0x05426328, 0x05009892, - 0x25a0cc29, 0x2561cec8, 0x058044b3, 0x05401c99, - 0x05006b49, 0x25e0d6f7, 0x2561c528, 0x0583c8bc, - 0x0542522f, 0x05001ec0, 0x25e0de65, 0x25a1c113, - 0x05803cad, 0x0540f3c0, 0x0500ab15, 0x2560c28c, - 0x2561d7c0, 0x05801ed7, 0x0542633b, 0x05003696, - 0x2560d4b4, 0x25e1c918, 0x058021ff, 0x05400e15, - 0x0500f3de, 0x0473025a, 0x04bd05ab, 0x658e0025, - 0x658a08e2, 0x659a0493, 0x043e1062, 0x04f418b4, - 0x046d15bd, 0x04611fce, 0x04d6a07c, 0x04001929, - 0x041a09da, 0x04d098f4, 0x04db10d4, 0x0459a3ad, - 0x041aa029, 0x041919fb, 0x04d39e24, 0x04118302, - 0x04101dba, 0x04d7ae16, 0x04dea571, 0x04180210, - 0x05e786fc, 0x05e4915c, 0x04881cf1, 0x044a0f04, - 0x04090969, 0x048b16c4, 0x044101e4, 0x04dcbf44, - 0x65809745, 0x658d833f, 0x65c68468, 0x65c79b07, - 0x65829e38, 0x049dafca, 0x6582bba8, 0x65c0b7ff, - 0x65c1b4e0, 0x658dbadd, 0x65819a9d, 0x65ed9246, - 0x65b30815, 0x65e6263c, 0x65eebb94, 0x65bad14e, - 0x65efe178, 0x65fc5697, 0x65e07f14, 0x040c55a6, - 0x04977f4d, 0x043d3046, 0x04b733a0, 0x046830a4, - 0x04ed322d, 0x05686948, 0x05bd6c13, 0x65c88ef0, - 0x450db3d7, 0x4540b6d9, 0x043e3979, 0x445896ce, - 0x445a9005, 0x44d98069, 0x445b87ae, 0x04da348e, - 0x04982edb, 0x0499397f, 0x0408338c, 0x04ca309c, - 0x65c721e6, 0x65c63641, 0x65982882, 0x04812b8b, - 0x0e251083, 0x4e3712d5, 0x0e61101f, 0x4e6d118b, - 0x0eba1338, 0x4eb712d5, 0x2e31120f, 0x6e2e11ac, - 0x2e6810e6, 0x6e6f11cd, 0x2eaa1128, 0x6eb1120f, - + 0x05a18610, 0x05e18610, 0x0420bc31, 0x05271e11, + 0x6545e891, 0x6585e891, 0x65c5e891, 0x6545c891, + 0x6585c891, 0x65c5c891, 0x45b0c210, 0x45f1c231, + 0x1e601000, 0x1e603000, 0x1e621000, 0x1e623000, + 0x1e641000, 0x1e643000, 0x1e661000, 0x1e663000, + 0x1e681000, 0x1e683000, 0x1e6a1000, 0x1e6a3000, + 0x1e6c1000, 0x1e6c3000, 0x1e6e1000, 0x1e6e3000, + 0x1e701000, 0x1e703000, 0x1e721000, 0x1e723000, + 0x1e741000, 0x1e743000, 0x1e761000, 0x1e763000, + 0x1e781000, 0x1e783000, 0x1e7a1000, 0x1e7a3000, + 0x1e7c1000, 0x1e7c3000, 0x1e7e1000, 0x1e7e3000, + 0xf8268267, 0xf82d023c, 0xf8301046, 0xf83d2083, + 0xf8263290, 0xf82d528c, 0xf8284299, 0xf8337160, + 0xf8386286, 0xf8bf820e, 0xf8a600e0, 0xf8af1353, + 0xf8a922ea, 0xf8b53396, 0xf8a251e3, 0xf8b340f4, + 0xf8a470fd, 0xf8a06209, 0xf8f48097, 0xf8f002ea, + 0xf8eb10d9, 0xf8ff21b0, 0xf8f7302c, 0xf8ee52a9, + 0xf8f041fa, 0xf8e471e4, 0xf8e863c6, 0xf864823d, + 0xf87d013a, 0xf86f1162, 0xf87d20e3, 0xf86132bb, + 0xf870510e, 0xf8704336, 0xf86572b4, 0xf8706217, + 0xb83e8294, 0xb8200264, 0xb8381284, 0xb8242358, + 0xb8333102, 0xb828530e, 0xb83042df, 0xb824703f, + 0xb82a6194, 0xb8a080e9, 0xb8b80090, 0xb8bb1146, + 0xb8bb21b8, 0xb8b032df, 0xb8b653f4, 0xb8bd41c9, + 0xb8b47287, 0xb8bc6169, 0xb8ee828c, 0xb8e10138, + 0xb8f3126d, 0xb8f020b0, 0xb8e03183, 0xb8e851ef, + 0xb8f041e4, 0xb8fe7005, 0xb8ea6376, 0xb8638120, + 0xb873015d, 0xb8781284, 0xb86723b8, 0xb86e3175, + 0xb87b51ed, 0xb87f41d1, 0xb863721e, 0xb87660f4, + 0xce216874, 0xce104533, 0xce648c15, 0xce8e3302, + 0xce6e82ab, 0xce6c87d1, 0xcec08063, 0xce638937, + 0x25e0c358, 0x25a1c7d3, 0x0580785a, 0x05426328, + 0x05009892, 0x25a0cc29, 0x2561cec8, 0x058044b3, + 0x05401c99, 0x05006b49, 0x25e0d6f7, 0x2561c528, + 0x0583c8bc, 0x0542522f, 0x05001ec0, 0x25e0de65, + 0x25a1c113, 0x05803cad, 0x0540f3c0, 0x0500ab15, + 0x2560c28c, 0x2561d7c0, 0x05801ed7, 0x0542633b, + 0x05003696, 0x2560d4b4, 0x25e1c918, 0x058021ff, + 0x05400e15, 0x0500f3de, 0x0473025a, 0x04bd05ab, + 0x658e0025, 0x658a08e2, 0x659a0493, 0x043e1062, + 0x04f418b4, 0x046d15bd, 0x04611fce, 0x04d6a07c, + 0x04001929, 0x041a09da, 0x04d098f4, 0x04db10d4, + 0x0459a3ad, 0x041aa029, 0x041919fb, 0x04d39e24, + 0x04118302, 0x04101dba, 0x04d7ae16, 0x04dea571, + 0x04180210, 0x05e786fc, 0x05e4915c, 0x04881cf1, + 0x044a0f04, 0x04090969, 0x048b16c4, 0x044101e4, + 0x04dcbf44, 0x65809745, 0x658d833f, 0x65c68468, + 0x65c79b07, 0x65829e38, 0x049dafca, 0x6582bba8, + 0x65c0b7ff, 0x65c1b4e0, 0x658dbadd, 0x65819a9d, + 0x65ed9246, 0x65b30815, 0x65e6263c, 0x65eebb94, + 0x65bad14e, 0x65efe178, 0x65fc5697, 0x65e07f14, + 0x040c55a6, 0x04977f4d, 0x043d3046, 0x04b733a0, + 0x046830a4, 0x04ed322d, 0x05686948, 0x05bd6c13, + 0x65c88ef0, 0x450db3d7, 0x4540b6d9, 0x043e3979, + 0x445896ce, 0x445a9005, 0x44d98069, 0x445b87ae, + 0x04da348e, 0x04982edb, 0x0499397f, 0x0408338c, + 0x04ca309c, 0x65c721e6, 0x65c63641, 0x65982882, + 0x04812b8b, 0x0e251083, 0x4e3712d5, 0x0e61101f, + 0x4e6d118b, 0x0eba1338, 0x4eb712d5, 0x2e31120f, + 0x6e2e11ac, 0x2e6810e6, 0x6e6f11cd, 0x2eaa1128, + 0x6eb1120f, }; // END Generated code -- do not edit diff --git a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java index bb69e5bfe80..88b34841e57 100644 --- a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java +++ b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java @@ -2755,6 +2755,36 @@ public class IRNode { vectorNode(EXPAND_BITS_VL, "ExpandBitsV", TYPE_LONG); } + public static final String EXPAND_VB = VECTOR_PREFIX + "EXPAND_VB" + POSTFIX; + static { + vectorNode(EXPAND_VB, "ExpandV", TYPE_BYTE); + } + + public static final String EXPAND_VS = VECTOR_PREFIX + "EXPAND_VS" + POSTFIX; + static { + vectorNode(EXPAND_VS, "ExpandV", TYPE_SHORT); + } + + public static final String EXPAND_VI = VECTOR_PREFIX + "EXPAND_VI" + POSTFIX; + static { + vectorNode(EXPAND_VI, "ExpandV", TYPE_INT); + } + + public static final String EXPAND_VL = VECTOR_PREFIX + "EXPAND_VL" + POSTFIX; + static { + vectorNode(EXPAND_VL, "ExpandV", TYPE_LONG); + } + + public static final String EXPAND_VF = VECTOR_PREFIX + "EXPAND_VF" + POSTFIX; + static { + vectorNode(EXPAND_VF, "ExpandV", TYPE_FLOAT); + } + + public static final String EXPAND_VD = VECTOR_PREFIX + "EXPAND_VD" + POSTFIX; + static { + vectorNode(EXPAND_VD, "ExpandV", TYPE_DOUBLE); + } + public static final String Z_LOAD_P_WITH_BARRIER_FLAG = COMPOSITE_PREFIX + "Z_LOAD_P_WITH_BARRIER_FLAG" + POSTFIX; static { String regex = START + "zLoadP\\S*" + MID + "barrier\\(\\s*" + IS_REPLACED + "\\s*\\)" + END; diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java new file mode 100644 index 00000000000..ce8fc0fb7b0 --- /dev/null +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package compiler.vectorapi; + +import compiler.lib.generators.*; +import compiler.lib.ir_framework.*; +import jdk.incubator.vector.*; +import jdk.test.lib.Asserts; + +/** + * @test + * @bug 8363989 + * @key randomness + * @library /test/lib / + * @summary AArch64: Add missing backend support of VectorAPI expand operation + * @modules jdk.incubator.vector + * + * @run driver compiler.vectorapi.VectorExpandTest + */ + +public class VectorExpandTest { + static final VectorSpecies B_SPECIES = ByteVector.SPECIES_MAX; + static final VectorSpecies S_SPECIES = ShortVector.SPECIES_MAX; + static final VectorSpecies I_SPECIES = IntVector.SPECIES_MAX; + static final VectorSpecies F_SPECIES = FloatVector.SPECIES_MAX; + static final VectorSpecies L_SPECIES = LongVector.SPECIES_MAX; + static final VectorSpecies D_SPECIES = DoubleVector.SPECIES_MAX; + static final int LENGTH = 512; + static final Generators RD = Generators.G; + static byte[] ba, bb; + static short[] sa, sb; + static int[] ia, ib; + static long[] la, lb; + static float[] fa, fb; + static double[] da, db; + static boolean[] ma; + + static { + ba = new byte[LENGTH]; + bb = new byte[LENGTH]; + sa = new short[LENGTH]; + sb = new short[LENGTH]; + ia = new int[LENGTH]; + ib = new int[LENGTH]; + la = new long[LENGTH]; + lb = new long[LENGTH]; + fa = new float[LENGTH]; + fb = new float[LENGTH]; + da = new double[LENGTH]; + db = new double[LENGTH]; + ma = new boolean[LENGTH]; + + Generator iGen = RD.ints(); + Generator lGen = RD.longs(); + Generator fGen = RD.floats(); + Generator dGen = RD.doubles(); + + for (int i = 0; i < LENGTH; i++) { + ba[i] = iGen.next().byteValue(); + sa[i] = iGen.next().shortValue(); + ma[i] = iGen.next() % 2 == 0; + } + RD.fill(iGen, ia); + RD.fill(lGen, la); + RD.fill(fGen, fa); + RD.fill(dGen, da); + } + + @Test + @IR(counts = { IRNode.EXPAND_VB, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandByte(ByteVector av, VectorMask m) { + av.expand(m).intoArray(bb, 0); + } + + @Run(test = "testVectorExpandByte") + public static void testVectorExpandByte_runner() { + ByteVector av = ByteVector.fromArray(B_SPECIES, ba, 0); + VectorMask m = VectorMask.fromArray(B_SPECIES, ma, 0); + testVectorExpandByte(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? ba[index++] : (byte)0, bb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VS, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandShort(ShortVector av, VectorMask m) { + av.expand(m).intoArray(sb, 0); + } + + @Run(test = "testVectorExpandShort") + public static void testVectorExpandShort_runner() { + ShortVector av = ShortVector.fromArray(S_SPECIES, sa, 0); + VectorMask m = VectorMask.fromArray(S_SPECIES, ma, 0); + testVectorExpandShort(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? sa[index++] : (short)0, sb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VI, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandInt(IntVector av, VectorMask m) { + av.expand(m).intoArray(ib, 0); + } + + @Run(test = "testVectorExpandInt") + public static void testVectorExpandInt_runner() { + IntVector av = IntVector.fromArray(I_SPECIES, ia, 0); + VectorMask m = VectorMask.fromArray(I_SPECIES, ma, 0); + testVectorExpandInt(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? ia[index++] : (int)0, ib[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VL, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandLong(LongVector av, VectorMask m) { + av.expand(m).intoArray(lb, 0); + } + + @Run(test = "testVectorExpandLong") + public static void testVectorExpandLong_runner() { + LongVector av = LongVector.fromArray(L_SPECIES, la, 0); + VectorMask m = VectorMask.fromArray(L_SPECIES, ma, 0); + testVectorExpandLong(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? la[index++] : (long)0, lb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VF, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandFloat(FloatVector av, VectorMask m) { + av.expand(m).intoArray(fb, 0); + } + + @Run(test = "testVectorExpandFloat") + public static void testVectorExpandFloat_runner() { + FloatVector av = FloatVector.fromArray(F_SPECIES, fa, 0); + VectorMask m = VectorMask.fromArray(F_SPECIES, ma, 0); + testVectorExpandFloat(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? fa[index++] : (float)0, fb[i]); + } + } + + @Test + @IR(counts = { IRNode.EXPAND_VD, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + public static void testVectorExpandDouble(DoubleVector av, VectorMask m) { + av.expand(m).intoArray(db, 0); + } + + @Run(test = "testVectorExpandDouble") + public static void testVectorExpandDouble_runner() { + DoubleVector av = DoubleVector.fromArray(D_SPECIES, da, 0); + VectorMask m = VectorMask.fromArray(D_SPECIES, ma, 0); + testVectorExpandDouble(av, m); + int index = 0; + for (int i = 0; i < m.length(); i++) { + Asserts.assertEquals(m.laneIsSet(i) ? da[index++] : (double)0, db[i]); + } + } + + public static void main(String[] args) { + TestFramework testFramework = new TestFramework(); + testFramework.setDefaultWarmup(10000) + .addFlags("--add-modules=jdk.incubator.vector") + .start(); + } +} From 5e12ff9ff64f2d7ebb501cdb19d5f013dde17be4 Mon Sep 17 00:00:00 2001 From: Tejesh R Date: Mon, 22 Sep 2025 03:45:37 +0000 Subject: [PATCH 142/556] 8213530: Test java/awt/Modal/ToFront/DialogToFrontModeless1Test.java fails on Linux Reviewed-by: psadhukhan, dnguyen --- test/jdk/ProblemList.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 8a4c484e292..8a84bbf60d5 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -409,7 +409,6 @@ java/awt/Mouse/EnterExitEvents/ResizingFrameTest.java 8005021 macosx-all java/awt/Mouse/EnterExitEvents/FullscreenEnterEventTest.java 8051455 macosx-all java/awt/Mouse/MouseModifiersUnitTest/MouseModifiersUnitTest_Standard.java 7124407,8302787 macosx-all,windows-all java/awt/Mouse/RemovedComponentMouseListener/RemovedComponentMouseListener.java 8157170 macosx-all -java/awt/Modal/ToFront/DialogToFrontModeless1Test.java 8213530 linux-all java/awt/Modal/ToFront/DialogToFrontNonModalTest.java 8221899 linux-all java/awt/Modal/ToBack/ToBackAppModal1Test.java 8196441 linux-all,macosx-all java/awt/Modal/ToBack/ToBackAppModal2Test.java 8196441 linux-all,macosx-all From a1b43c3046ecf42fd5b8f40274625cae120b3a3c Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Mon, 22 Sep 2025 05:06:25 +0000 Subject: [PATCH 143/556] 8368087: ZGC: Make ZStatLoad::print() logging conditional on os::loadavg support Reviewed-by: eosterlund, stefank, jsikstro --- src/hotspot/share/gc/z/zStat.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hotspot/share/gc/z/zStat.cpp b/src/hotspot/share/gc/z/zStat.cpp index f703b3a1791..03aa9061184 100644 --- a/src/hotspot/share/gc/z/zStat.cpp +++ b/src/hotspot/share/gc/z/zStat.cpp @@ -1410,11 +1410,12 @@ ZStatWorkersStats ZStatWorkers::stats() { // void ZStatLoad::print() { double loadavg[3] = {}; - os::loadavg(loadavg, ARRAY_SIZE(loadavg)); - log_info(gc, load)("Load: %.2f (%.0f%%) / %.2f (%.0f%%) / %.2f (%.0f%%)", - loadavg[0], percent_of(loadavg[0], (double) ZCPU::count()), - loadavg[1], percent_of(loadavg[1], (double) ZCPU::count()), - loadavg[2], percent_of(loadavg[2], (double) ZCPU::count())); + if (os::loadavg(loadavg, ARRAY_SIZE(loadavg)) != -1) { + log_info(gc, load)("Load: %.2f (%.0f%%) / %.2f (%.0f%%) / %.2f (%.0f%%)", + loadavg[0], percent_of(loadavg[0], (double) ZCPU::count()), + loadavg[1], percent_of(loadavg[1], (double) ZCPU::count()), + loadavg[2], percent_of(loadavg[2], (double) ZCPU::count())); + } } // From 5efaa9970ace463f7d9bcd8f4028b1d60665cfad Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Mon, 22 Sep 2025 05:32:11 +0000 Subject: [PATCH 144/556] 8367298: ZGC: Enhance zaddress type system's assert messages Reviewed-by: stefank, tschatzl --- src/hotspot/share/gc/z/zAddress.inline.hpp | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/hotspot/share/gc/z/zAddress.inline.hpp b/src/hotspot/share/gc/z/zAddress.inline.hpp index 64beb5ba35d..f088e4a87d1 100644 --- a/src/hotspot/share/gc/z/zAddress.inline.hpp +++ b/src/hotspot/share/gc/z/zAddress.inline.hpp @@ -149,18 +149,18 @@ inline bool operator>=(offset_type first, offset_type##_end second) { inline uintptr_t untype(zoffset offset) { const uintptr_t value = static_cast(offset); - assert(value < ZAddressOffsetMax, "must have no other bits"); + assert(value < ZAddressOffsetMax, "Offset out of bounds (" PTR_FORMAT " < " PTR_FORMAT ")", value, ZAddressOffsetMax); return value; } inline uintptr_t untype(zoffset_end offset) { const uintptr_t value = static_cast(offset); - assert(value <= ZAddressOffsetMax, "must have no other bits"); + assert(value <= ZAddressOffsetMax, "Offset out of bounds (" PTR_FORMAT " <= " PTR_FORMAT ")", value, ZAddressOffsetMax); return value; } inline zoffset to_zoffset(uintptr_t value) { - assert(value < ZAddressOffsetMax, "must have no other bits"); + assert(value < ZAddressOffsetMax, "Value out of bounds (" PTR_FORMAT " < " PTR_FORMAT ")", value, ZAddressOffsetMax); return zoffset(value); } @@ -186,7 +186,7 @@ inline zoffset_end to_zoffset_end(zoffset start, size_t size) { } inline zoffset_end to_zoffset_end(uintptr_t value) { - assert(value <= ZAddressOffsetMax, "Overflow"); + assert(value <= ZAddressOffsetMax, "Value out of bounds (" PTR_FORMAT " <= " PTR_FORMAT ")", value, ZAddressOffsetMax); return zoffset_end(value); } @@ -200,18 +200,18 @@ CREATE_ZOFFSET_OPERATORS(zoffset) inline uintptr_t untype(zbacking_offset offset) { const uintptr_t value = static_cast(offset); - assert(value < ZBackingOffsetMax, "must have no other bits"); + assert(value < ZBackingOffsetMax, "Offset out of bounds (" PTR_FORMAT " < " PTR_FORMAT ")", value, ZAddressOffsetMax); return value; } inline uintptr_t untype(zbacking_offset_end offset) { const uintptr_t value = static_cast(offset); - assert(value <= ZBackingOffsetMax, "must have no other bits"); + assert(value <= ZBackingOffsetMax, "Offset out of bounds (" PTR_FORMAT " <= " PTR_FORMAT ")", value, ZAddressOffsetMax); return value; } inline zbacking_offset to_zbacking_offset(uintptr_t value) { - assert(value < ZBackingOffsetMax, "must have no other bits"); + assert(value < ZBackingOffsetMax, "Value out of bounds (" PTR_FORMAT " < " PTR_FORMAT ")", value, ZAddressOffsetMax); return zbacking_offset(value); } @@ -228,7 +228,7 @@ inline zbacking_offset_end to_zbacking_offset_end(zbacking_offset start, size_t } inline zbacking_offset_end to_zbacking_offset_end(uintptr_t value) { - assert(value <= ZBackingOffsetMax, "must have no other bits"); + assert(value <= ZBackingOffsetMax, "Value out of bounds (" PTR_FORMAT " <= " PTR_FORMAT ")", value, ZAddressOffsetMax); return zbacking_offset_end(value); } @@ -242,18 +242,18 @@ CREATE_ZOFFSET_OPERATORS(zbacking_offset) inline uint32_t untype(zbacking_index index) { const uint32_t value = static_cast(index); - assert(value < ZBackingIndexMax, "must have no other bits"); + assert(value < ZBackingIndexMax, "Offset out of bounds (" UINT32_FORMAT_X_0 " < " UINT32_FORMAT_X_0 ")", value, ZBackingIndexMax); return value; } inline uint32_t untype(zbacking_index_end index) { const uint32_t value = static_cast(index); - assert(value <= ZBackingIndexMax, "must have no other bits"); + assert(value <= ZBackingIndexMax, "Offset out of bounds (" UINT32_FORMAT_X_0 " <= " UINT32_FORMAT_X_0 ")", value, ZBackingIndexMax); return value; } inline zbacking_index to_zbacking_index(uint32_t value) { - assert(value < ZBackingIndexMax, "must have no other bits"); + assert(value < ZBackingIndexMax, "Value out of bounds (" UINT32_FORMAT_X_0 " < " UINT32_FORMAT_X_0 ")", value, ZBackingIndexMax); return zbacking_index(value); } @@ -266,12 +266,12 @@ inline zbacking_index_end to_zbacking_index_end(zbacking_index start, size_t siz const uint32_t start_value = untype(start); const uint32_t value = start_value + checked_cast(size); assert(value <= ZBackingIndexMax && start_value <= value, - "Overflow start: %x size: %zu value: %x", start_value, size, value); + "Overflow start: " UINT32_FORMAT_X_0 " size: %zu value: " UINT32_FORMAT_X_0 "", start_value, size, value); return zbacking_index_end(value); } inline zbacking_index_end to_zbacking_index_end(uint32_t value) { - assert(value <= ZBackingIndexMax, "must have no other bits"); + assert(value <= ZBackingIndexMax, "Value out of bounds (" UINT32_FORMAT_X_0 " <= " UINT32_FORMAT_X_0 ")", value, ZBackingIndexMax); return zbacking_index_end(value); } @@ -287,7 +287,7 @@ CREATE_ZOFFSET_OPERATORS(zbacking_index) inline zbacking_index to_zbacking_index(zbacking_offset offset) { const uintptr_t value = untype(offset); - assert(is_aligned(value, ZGranuleSize), "must be granule aligned"); + assert(is_aligned(value, ZGranuleSize), "Must be granule aligned: " PTR_FORMAT, value); return to_zbacking_index((uint32_t)(value >> ZGranuleSizeShift)); } @@ -420,7 +420,7 @@ inline bool is_null_any(zpointer ptr) { // Is it null - colored or not? inline bool is_null_assert_load_good(zpointer ptr) { const bool result = is_null_any(ptr); - assert(!result || ZPointer::is_load_good(ptr), "Got bad colored null"); + assert(!result || ZPointer::is_load_good(ptr), "Got bad colored null: " PTR_FORMAT, untype(ptr)); return result; } @@ -620,7 +620,7 @@ inline zaddress ZPointer::uncolor_store_good(zpointer ptr) { } inline zaddress_unsafe ZPointer::uncolor_unsafe(zpointer ptr) { - assert(ZPointer::is_store_bad(ptr), "Unexpected ptr"); + assert(ZPointer::is_store_bad(ptr), "Should be store bad: " PTR_FORMAT, untype(ptr)); const uintptr_t raw_addr = untype(ptr); return to_zaddress_unsafe(raw_addr >> ZPointer::load_shift_lookup(raw_addr)); } @@ -642,7 +642,7 @@ inline bool ZPointer::is_load_good_or_null(zpointer ptr) { // the barrier as if it was null. This should be harmless as such // addresses should ever be passed through the barrier. const bool result = !is_load_bad(ptr); - assert((is_load_good(ptr) || is_null(ptr)) == result, "Bad address"); + assert((is_load_good(ptr) || is_null(ptr)) == result, "Bad address: " PTR_FORMAT, untype(ptr)); return result; } @@ -673,7 +673,7 @@ inline bool ZPointer::is_mark_good_or_null(zpointer ptr) { // the barrier as if it was null. This should be harmless as such // addresses should ever be passed through the barrier. const bool result = !is_mark_bad(ptr); - assert((is_mark_good(ptr) || is_null(ptr)) == result, "Bad address"); + assert((is_mark_good(ptr) || is_null(ptr)) == result, "Bad address: " PTR_FORMAT, untype(ptr)); return result; } @@ -694,7 +694,7 @@ inline bool ZPointer::is_store_good_or_null(zpointer ptr) { // the barrier as if it was null. This should be harmless as such // addresses should ever be passed through the barrier. const bool result = !is_store_bad(ptr); - assert((is_store_good(ptr) || is_null(ptr)) == result, "Bad address"); + assert((is_store_good(ptr) || is_null(ptr)) == result, "Bad address: " PTR_FORMAT, untype(ptr)); return result; } From 682fd7846c9a6f80c399c7e44f3fccb9a07c6c47 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Mon, 22 Sep 2025 06:02:20 +0000 Subject: [PATCH 145/556] 8366678: Use JUnit in test/langtools/tools/javac Reviewed-by: liach --- .../tools/javac/file/FSInfoTest.java | 6 +- .../MultiReleaseJarAwareSJFM.java | 26 +++++---- .../MultiReleaseJar/MultiReleaseJarTest.java | 35 ++++++----- .../lambda/lambdaExecution/InInterface.java | 14 ++--- .../lambdaExecution/InnerConstructor.java | 14 ++--- .../LambdaTranslationTest1.java | 15 ++--- .../LambdaTranslationTest2.java | 29 +++++++--- .../MethodReferenceTestFDCCE.java | 29 ++++++---- .../MethodReferenceTestInnerDefault.java | 17 +++--- .../MethodReferenceTestInnerInstance.java | 16 ++--- .../MethodReferenceTestInnerVarArgsThis.java | 45 +++++++------- .../MethodReferenceTestInstance.java | 16 ++--- .../MethodReferenceTestKinds.java | 54 +++++++++-------- .../MethodReferenceTestMethodHandle.java | 12 ++-- .../MethodReferenceTestNew.java | 22 +++---- .../MethodReferenceTestNewInner.java | 19 +++--- ...thodReferenceTestNewInnerImplicitArgs.java | 17 +++--- .../MethodReferenceTestSueCase1.java | 13 ++--- .../MethodReferenceTestSueCase2.java | 13 ++--- .../MethodReferenceTestSueCase4.java | 13 ++--- .../MethodReferenceTestSuper.java | 23 ++++---- .../MethodReferenceTestSuperDefault.java | 17 +++--- .../MethodReferenceTestTypeConversion.java | 16 ++--- .../MethodReferenceTestVarArgs.java | 45 +++++++------- .../MethodReferenceTestVarArgsExt.java | 45 +++++++------- .../MethodReferenceTestVarArgsSuper.java | 45 +++++++------- ...ethodReferenceTestVarArgsSuperDefault.java | 45 +++++++------- .../MethodReferenceTestVarArgsThis.java | 45 +++++++------- .../MethodReferenceTestVarHandle.java | 11 ++-- .../tools/javac/lambdaShapes/TEST.properties | 2 +- .../org/openjdk/tests/javac/FDTest.java | 28 ++++----- .../openjdk/tests/separate/TestHarness.java | 21 ++----- .../openjdk/tests/vm/DefaultMethodsTest.java | 47 ++++++++++++--- .../tests/vm/FDSeparateCompilationTest.java | 32 ++++------ .../javac/records/BigRecordsToStringTest.java | 10 ++-- .../javac/records/RecordMemberTests.java | 54 +++++++++-------- .../javac/records/VarargsRecordsTest.java | 58 ++++++++++--------- test/langtools/tools/javac/tree/T8024415.java | 21 +++---- .../TypeVariableCastTest.java | 24 ++++---- 39 files changed, 556 insertions(+), 458 deletions(-) diff --git a/test/langtools/tools/javac/file/FSInfoTest.java b/test/langtools/tools/javac/file/FSInfoTest.java index bbd9da9bc97..e9b0c9f55bf 100644 --- a/test/langtools/tools/javac/file/FSInfoTest.java +++ b/test/langtools/tools/javac/file/FSInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,7 +23,6 @@ import com.sun.tools.javac.file.FSInfo; import com.sun.tools.javac.util.Context; -import org.testng.annotations.Test; import java.io.IOException; import java.nio.file.Files; @@ -31,6 +30,7 @@ import java.nio.file.Path; import java.util.Locale; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; /* * @test @@ -38,7 +38,7 @@ import java.util.jar.Manifest; * @summary Test com.sun.tools.javac.file.FSInfo * @modules jdk.compiler/com.sun.tools.javac.util * jdk.compiler/com.sun.tools.javac.file - * @run testng FSInfoTest + * @run junit FSInfoTest */ public class FSInfoTest { diff --git a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java index 54519aa6afa..291e8386fe3 100644 --- a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java +++ b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarAwareSJFM.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,14 +30,9 @@ * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * @build toolbox.ToolBox - * @run testng MultiReleaseJarAwareSJFM + * @run junit MultiReleaseJarAwareSJFM */ -import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; import javax.tools.FileObject; import javax.tools.JavaFileManager; @@ -50,11 +45,18 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import toolbox.JarTask; import toolbox.JavacTask; import toolbox.ToolBox; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class MultiReleaseJarAwareSJFM { private static final int CURRENT_VERSION = Runtime.version().major(); @@ -113,7 +115,7 @@ public class MultiReleaseJarAwareSJFM { } }; - @BeforeClass + @BeforeAll public void setup() throws Exception { tb.createDirectories("classes", "classes/META-INF/versions/9", @@ -140,7 +142,7 @@ public class MultiReleaseJarAwareSJFM { .run(); } - @AfterClass + @AfterAll public void teardown() throws Exception { tb.deleteFiles( "classes/META-INF/versions/" + CURRENT_VERSION + "/version/Version.class", @@ -159,7 +161,6 @@ public class MultiReleaseJarAwareSJFM { ); } - @DataProvider(name = "versions") public Object[][] data() { return new Object[][] { {"", 8}, @@ -169,7 +170,8 @@ public class MultiReleaseJarAwareSJFM { }; } - @Test(dataProvider = "versions") + @ParameterizedTest + @MethodSource("data") public void test(String version, int expected) throws Throwable { StandardJavaFileManager jfm = ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null); jfm.setLocation(jloc, List.of(new File("multi-release.jar"))); @@ -183,7 +185,7 @@ public class MultiReleaseJarAwareSJFM { MethodType mt = MethodType.methodType(int.class); MethodHandle mh = MethodHandles.lookup().findVirtual(versionClass, "getVersion", mt); int v = (int)mh.invoke(versionClass.newInstance()); - Assert.assertEquals(v, expected); + Assertions.assertEquals(expected, v); jfm.close(); } diff --git a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java index 09209e14c9d..1c1704510ca 100644 --- a/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java +++ b/test/langtools/tools/javac/file/MultiReleaseJar/MultiReleaseJarTest.java @@ -30,13 +30,14 @@ * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.main * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask - * @run testng/timeout=480 MultiReleaseJarTest + * @run junit/timeout=480 MultiReleaseJarTest */ -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import toolbox.JarTask; import toolbox.JavacTask; @@ -44,6 +45,7 @@ import toolbox.Task; import toolbox.ToolBox; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class MultiReleaseJarTest { private final String main1 = @@ -84,7 +86,7 @@ public class MultiReleaseJarTest { private final ToolBox tb = new ToolBox(); - @BeforeClass + @BeforeAll public void setup() throws Exception { tb.createDirectories("classes", "classes/META-INF/versions/9"); new JavacTask(tb) @@ -111,7 +113,7 @@ public class MultiReleaseJarTest { ); } - @AfterClass + @AfterAll public void teardown() throws Exception { tb.deleteFiles( "multi-release.jar", @@ -120,8 +122,9 @@ public class MultiReleaseJarTest { ); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main1Runtime(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -134,8 +137,9 @@ public class MultiReleaseJarTest { } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 8 -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main1Release8(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -148,8 +152,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 9 -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main1Release9(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main1); Task.Result result = new JavacTask(tb, mode) @@ -162,8 +167,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main2Runtime(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -176,8 +182,9 @@ public class MultiReleaseJarTest { } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 8 -cp multi-release.jar Main.java -> fails + @MethodSource("createModes") public void main2Release8(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -190,8 +197,9 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @Test(dataProvider="modes") + @ParameterizedTest // javac -d classes --release 9 -cp multi-release.jar Main.java -> succeeds + @MethodSource("createModes") public void main2Release9(Task.Mode mode) throws Exception { tb.writeFile("Main.java", main2); Task.Result result = new JavacTask(tb, mode) @@ -204,7 +212,6 @@ public class MultiReleaseJarTest { tb.deleteFiles("Main.java"); } - @DataProvider(name="modes") public Object[][] createModes() { return new Object[][] { new Object[] {Task.Mode.API}, diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java b/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java index 91ba372bcb3..1a560125e23 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/InInterface.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,11 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng InInterface + * @run junit InInterface */ -import static org.testng.Assert.assertEquals; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; interface LTII { @@ -51,12 +51,12 @@ interface LTII { } -@Test public class InInterface implements LTII { + @Test public void testLambdaInDefaultMethod() { - assertEquals(t1().m(), "yo"); - assertEquals(t2().m("p"), "snurp"); + assertEquals("yo", t1().m()); + assertEquals("snurp", t2().m("p")); } } diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java b/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java index d5282afc9df..c8322668059 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/InnerConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng InnerConstructor + * @run junit InnerConstructor */ -import static org.testng.Assert.assertEquals; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class InnerConstructor { + @Test public void testLambdaWithInnerConstructor() { - assertEquals(seq1().m().toString(), "Cbl:nada"); - assertEquals(seq2().m("rats").toString(), "Cbl:rats"); + assertEquals("Cbl:nada", seq1().m().toString()); + assertEquals("Cbl:rats", seq2().m("rats").toString()); } Ib1 seq1() { diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java index b207fd1a7d5..fb602badb25 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,14 +25,12 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest1 + * @run junit/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest1 */ -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -import static org.testng.Assert.assertEquals; - -@Test public class LambdaTranslationTest1 extends LT1Sub { String cntxt = "blah"; @@ -43,7 +41,7 @@ public class LambdaTranslationTest1 extends LT1Sub { private static void appendResult(Object s) { result.set(result.get().toString() + s); } private static void assertResult(String expected) { - assertEquals(result.get().toString(), expected); + assertEquals(expected, result.get().toString()); } static Integer count(String s) { @@ -66,6 +64,7 @@ public class LambdaTranslationTest1 extends LT1Sub { setResult(String.format("d:%f", d)); } + @Test public void testLambdas() { TBlock b = t -> {setResult("Sink0::" + t);}; b.apply("Howdy"); @@ -127,6 +126,7 @@ public class LambdaTranslationTest1 extends LT1Sub { assertResult("b11: *999*"); } + @Test public void testMethodRefs() { LT1IA ia = LambdaTranslationTest1::eye; ia.doit(1234); @@ -147,6 +147,7 @@ public class LambdaTranslationTest1 extends LT1Sub { assertEquals((Integer) 6, a.doit("shower")); } + @Test public void testInner() throws Exception { (new In()).doInner(); } diff --git a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java index e7e484730b2..7eec78b55d2 100644 --- a/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java +++ b/test/langtools/tools/javac/lambda/lambdaExecution/LambdaTranslationTest2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,26 +25,26 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest2 + * @run junit/othervm -Duser.language=en -Duser.country=US LambdaTranslationTest2 */ -import org.testng.annotations.Test; import java.util.ArrayList; import java.util.List; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; /** * LambdaTranslationTest2 -- end-to-end smoke tests for lambda evaluation */ -@Test public class LambdaTranslationTest2 { final String dummy = "dummy"; + @Test public void testLambdas() { TPredicate isEmpty = s -> s.isEmpty(); assertTrue(isEmpty.test("")); @@ -100,6 +100,7 @@ public class LambdaTranslationTest2 { String make(); } + @Test public void testBridges() { Factory of = () -> "y"; Factory ef = () -> "z"; @@ -112,6 +113,7 @@ public class LambdaTranslationTest2 { assertEquals("z", ((Factory) ef).make()); } + @Test public void testBridgesImplicitSpecialization() { StringFactory sf = () -> "x"; @@ -121,6 +123,7 @@ public class LambdaTranslationTest2 { assertEquals("x", ((Factory) sf).make()); } + @Test public void testBridgesExplicitSpecialization() { StringFactory2 sf = () -> "x"; @@ -130,6 +133,7 @@ public class LambdaTranslationTest2 { assertEquals("x", ((Factory) sf).make()); } + @Test public void testSuperCapture() { class A { String make() { return "x"; } @@ -201,6 +205,7 @@ public class LambdaTranslationTest2 { return String.format("f%f d%f", a0, a1); } + @Test public void testPrimitiveWidening() { WidenS ws1 = LambdaTranslationTest2::pwS1; assertEquals("b1 s2", ws1.m((byte) 1, (short) 2)); @@ -225,11 +230,13 @@ public class LambdaTranslationTest2 { return String.format("b%d s%d c%c i%d j%d z%b f%f d%f", a0, a1, a2, a3, a4, a5, a6, a7); } + @Test public void testUnboxing() { Unbox u = LambdaTranslationTest2::pu; assertEquals("b1 s2 cA i4 j5 ztrue f6.000000 d7.000000", u.m((byte)1, (short) 2, 'A', 4, 5L, true, 6.0f, 7.0)); } + @Test public void testBoxing() { Box b = LambdaTranslationTest2::pb; assertEquals("b1 s2 cA i4 j5 ztrue f6.000000 d7.000000", b.m((byte) 1, (short) 2, 'A', 4, 5L, true, 6.0f, 7.0)); @@ -239,6 +246,7 @@ public class LambdaTranslationTest2 { return ((String) o).equals("foo"); } + @Test public void testArgCastingAdaptation() { TPredicate p = LambdaTranslationTest2::cc; assertTrue(p.test("foo")); @@ -247,12 +255,14 @@ public class LambdaTranslationTest2 { interface SonOfPredicate extends TPredicate { } + @Test public void testExtendsSAM() { SonOfPredicate p = s -> s.isEmpty(); assertTrue(p.test("")); assertTrue(!p.test("foo")); } + @Test public void testConstructorRef() { Factory> lf = ArrayList::new; List list = lf.make(); @@ -266,6 +276,7 @@ public class LambdaTranslationTest2 { return "private"; } + @Test public void testPrivateMethodRef() { Factory sf = LambdaTranslationTest2::privateMethod; assertEquals("private", sf.make()); @@ -275,6 +286,7 @@ public class LambdaTranslationTest2 { String make(); } + @Test public void testPrivateIntf() { PrivateIntf p = () -> "foo"; assertEquals("foo", p.make()); @@ -284,11 +296,12 @@ public class LambdaTranslationTest2 { public T op(T a, T b); } + @Test public void testBoxToObject() { Op maxer = Math::max; for (int i=-100000; i < 100000; i += 100) for (int j=-100000; j < 100000; j += 99) { - assertEquals((int) maxer.op(i,j), Math.max(i,j)); + assertEquals(Math.max(i,j), (int) maxer.op(i,j)); } } @@ -296,6 +309,7 @@ public class LambdaTranslationTest2 { return "protected"; } + @Test public void testProtectedMethodRef() { Factory sf = LambdaTranslationTest2::protectedMethod; assertEquals("protected", sf.make()); @@ -331,6 +345,7 @@ public class LambdaTranslationTest2 { } } + @Test public void testInnerClassMethodRef() { Factory fs = new Inner1()::m1; assertEquals("Inner1.m1()", fs.make()); diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java index a0ecb7c370c..9238b1d2e4e 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestFDCCE.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,27 +25,26 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestFDCCE + * @run junit MethodReferenceTestFDCCE */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; /** * Method references and raw types. * @author Robert Field */ -@Test @SuppressWarnings({"rawtypes", "unchecked"}) public class MethodReferenceTestFDCCE { static void assertCCE(Throwable t) { - assertEquals(t.getClass().getName(), "java.lang.ClassCastException"); + assertEquals("java.lang.ClassCastException", t.getClass().getName()); } interface Pred { boolean accept(T x); } @@ -79,18 +78,21 @@ public class MethodReferenceTestFDCCE { return 123; } + @Test public void testMethodReferenceFDPrim1() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; assertTrue(p2.accept((Byte)(byte)15)); } + @Test public void testMethodReferenceFDPrim2() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; assertTrue(p2.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimICCE() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; @@ -102,6 +104,7 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDPrimOCCE() { Pred p = MethodReferenceTestFDCCE::isMinor; Pred p2 = p; @@ -113,12 +116,14 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDRef() { Pred p = MethodReferenceTestFDCCE::tst; Pred p2 = p; assertTrue(p2.accept(new B())); } + @Test public void testMethodReferenceFDRefCCE() { Pred p = MethodReferenceTestFDCCE::tst; Pred p2 = p; @@ -130,23 +135,27 @@ public class MethodReferenceTestFDCCE { } } + @Test public void testMethodReferenceFDPrimPrim() { Ps p = MethodReferenceTestFDCCE::isMinor; assertTrue(p.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimBoxed() { Ps p = MethodReferenceTestFDCCE::stst; assertTrue(p.accept((byte)15)); } + @Test public void testMethodReferenceFDPrimRef() { Oo p = MethodReferenceTestFDCCE::otst; - assertEquals(p.too(15).getClass().getName(), "java.lang.Integer"); + assertEquals("java.lang.Integer", p.too(15).getClass().getName()); } + @Test public void testMethodReferenceFDRet1() { Reto p = MethodReferenceTestFDCCE::ritst; - assertEquals(p.m(), (Short)(short)123); + assertEquals((Short)(short)123, p.m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java index d2a9763a201..f3cce62e7e2 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerDefault + * @run junit MethodReferenceTestInnerDefault */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -60,9 +59,9 @@ interface InDefB extends InDefA { } } -@Test public class MethodReferenceTestInnerDefault implements InDefB { + @Test public void testMethodReferenceInnerDefault() { (new In()).testMethodReferenceInnerDefault(); } @@ -73,13 +72,13 @@ public class MethodReferenceTestInnerDefault implements InDefB { IDSs q; q = MethodReferenceTestInnerDefault.this::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = MethodReferenceTestInnerDefault.this::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = MethodReferenceTestInnerDefault.this::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java index 76a82891c3a..326f8057a76 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,24 +25,24 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerInstance + * @run junit MethodReferenceTestInnerInstance */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestInnerInstance { + @Test public void testMethodReferenceInnerInstance() { cia().cib().testMethodReferenceInstance(); } + @Test public void testMethodReferenceInnerExternal() { cia().cib().testMethodReferenceExternal(); } @@ -63,14 +63,14 @@ public class MethodReferenceTestInnerInstance { SI q; q = CIA.this::xI; - assertEquals(q.m(55), "xI:55"); + assertEquals("xI:55", q.m(55)); } public void testMethodReferenceExternal() { SI q; q = (new E())::xI; - assertEquals(q.m(77), "ExI:77"); + assertEquals("ExI:77", q.m(77)); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java index a9cc399c27e..ef7eae72429 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInnerVarArgsThis.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,19 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInnerVarArgsThis + * @run junit MethodReferenceTestInnerVarArgsThis */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestInnerVarArgsThis { interface NsII { @@ -134,62 +133,62 @@ public class MethodReferenceTestInnerVarArgsThis { NsII q; q = CIA.this::xvO; - assertEquals(q.m(55, 66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55, 66)); } public void testVarArgsNsArray() { Nsai q; q = CIA.this::xvO; - assertEquals(q.m(new int[]{55, 66}), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[]{55, 66})); } public void testVarArgsNsII() { NsII q; q = CIA.this::xvI; - assertEquals(q.m(33, 7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33, 7)); q = CIA.this::xIvI; - assertEquals(q.m(50, 40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50, 40)); q = CIA.this::xvi; - assertEquals(q.m(100, 23), "xvi:123"); + assertEquals("xvi:123", q.m(100, 23)); q = CIA.this::xIvi; - assertEquals(q.m(9, 21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9, 21)); } public void testVarArgsNsiii() { Nsiii q; q = CIA.this::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = CIA.this::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = CIA.this::xvi; - assertEquals(q.m(900, 80, 7), "xvi:987"); + assertEquals("xvi:987", q.m(900, 80, 7)); q = CIA.this::xIvi; - assertEquals(q.m(333, 27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333, 27, 72)); } public void testVarArgsNsi() { Nsi q; q = CIA.this::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = CIA.this::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = CIA.this::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = CIA.this::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args @@ -197,7 +196,7 @@ public class MethodReferenceTestInnerVarArgsThis { NsaO q; q = CIA.this::xvO; - assertEquals(q.m(new String[]{"yo", "there", "dude"}), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[]{"yo", "there", "dude"})); } } @@ -218,28 +217,34 @@ public class MethodReferenceTestInnerVarArgsThis { } // These should be processed as var args + @Test public void testVarArgsNsSuperclass() { cia().cib().testVarArgsNsSuperclass(); } + @Test public void testVarArgsNsArray() { cia().cib().testVarArgsNsArray(); } + @Test public void testVarArgsNsII() { cia().cib().testVarArgsNsII(); } + @Test public void testVarArgsNsiii() { cia().cib().testVarArgsNsiii(); } + @Test public void testVarArgsNsi() { cia().cib().testVarArgsNsi(); } // These should NOT be processed as var args + @Test public void testVarArgsNsaO() { cia().cib().testVarArgsNsaO(); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java index 15b4a481995..52f650c4c52 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestInstance + * @run junit MethodReferenceTestInstance */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -42,7 +41,6 @@ class MethodReferenceTestInstance_E { } } -@Test public class MethodReferenceTestInstance { interface SI { String m(Integer a); } @@ -51,18 +49,20 @@ public class MethodReferenceTestInstance { return "xI:" + i; } + @Test public void testMethodReferenceInstance() { SI q; q = this::xI; - assertEquals(q.m(55), "xI:55"); + assertEquals("xI:55", q.m(55)); } + @Test public void testMethodReferenceExternal() { SI q; q = (new MethodReferenceTestInstance_E())::xI; - assertEquals(q.m(77), "ExI:77"); + assertEquals("ExI:77", q.m(77)); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java index e67ed379d13..2b255b3da3e 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestKinds.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestKinds + * @run junit MethodReferenceTestKinds */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestKinds extends MethodReferenceTestKindsSup { interface S0 { String get(); } @@ -65,76 +63,86 @@ public class MethodReferenceTestKinds extends MethodReferenceTestKindsSup { static String staticMethod0() { return "SM:0"; } static String staticMethod1(MethodReferenceTestKinds x) { return "SM:1-" + x; } - MethodReferenceTestKinds(String val) { - super(val); - } - MethodReferenceTestKinds() { super("blank"); } MethodReferenceTestKinds inst(String val) { - return new MethodReferenceTestKinds(val); + var inst = new MethodReferenceTestKinds(); + inst.val = val; // simulate `MethodReferenceTestKinds(String val)` constructor + return inst; } + @Test public void testMRBound() { S0 var = this::instanceMethod0; - assertEquals(var.get(), "IM:0-MethodReferenceTestKinds(blank)"); + assertEquals("IM:0-MethodReferenceTestKinds(blank)", var.get()); } + @Test public void testMRBoundArg() { S1 var = this::instanceMethod1; - assertEquals(var.get(inst("arg")), "IM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)"); + assertEquals("IM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRUnbound() { S1 var = MethodReferenceTestKinds::instanceMethod0; - assertEquals(var.get(inst("rcvr")), "IM:0-MethodReferenceTestKinds(rcvr)"); + assertEquals("IM:0-MethodReferenceTestKinds(rcvr)", var.get(inst("rcvr"))); } + @Test public void testMRUnboundArg() { S2 var = MethodReferenceTestKinds::instanceMethod1; - assertEquals(var.get(inst("rcvr"), inst("arg")), "IM:1-MethodReferenceTestKinds(rcvr)MethodReferenceTestKinds(arg)"); + assertEquals("IM:1-MethodReferenceTestKinds(rcvr)MethodReferenceTestKinds(arg)", var.get(inst("rcvr"), inst("arg"))); } + @Test public void testMRSuper() { S0 var = super::instanceMethod0; - assertEquals(var.get(), "SIM:0-MethodReferenceTestKinds(blank)"); + assertEquals("SIM:0-MethodReferenceTestKinds(blank)", var.get()); } + @Test public void testMRSuperArg() { S1 var = super::instanceMethod1; - assertEquals(var.get(inst("arg")), "SIM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)"); + assertEquals("SIM:1-MethodReferenceTestKinds(blank)MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRStatic() { S0 var = MethodReferenceTestKinds::staticMethod0; - assertEquals(var.get(), "SM:0"); + assertEquals("SM:0", var.get()); } + @Test public void testMRStaticArg() { S1 var = MethodReferenceTestKinds::staticMethod1; - assertEquals(var.get(inst("arg")), "SM:1-MethodReferenceTestKinds(arg)"); + assertEquals("SM:1-MethodReferenceTestKinds(arg)", var.get(inst("arg"))); } + @Test public void testMRTopLevel() { SN0 var = MethodReferenceTestKindsBase::new; - assertEquals(var.make().toString(), "MethodReferenceTestKindsBase(blank)"); + assertEquals("MethodReferenceTestKindsBase(blank)", var.make().toString()); } + @Test public void testMRTopLevelArg() { SN1 var = MethodReferenceTestKindsBase::new; - assertEquals(var.make("name").toString(), "MethodReferenceTestKindsBase(name)"); + assertEquals("MethodReferenceTestKindsBase(name)", var.make("name").toString()); } + @Test public void testMRImplicitInner() { SN0 var = MethodReferenceTestKinds.In::new; - assertEquals(var.make().toString(), "In(blank)"); + assertEquals("In(blank)", var.make().toString()); } + @Test public void testMRImplicitInnerArg() { SN1 var = MethodReferenceTestKinds.In::new; - assertEquals(var.make("name").toString(), "In(name)"); + assertEquals("In(name)", var.make("name").toString()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java index 05a9f9f3997..003ca2b4781 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestMethodHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,16 +25,15 @@ * @test * @bug 8028739 * @summary javac generates incorrect descriptor for MethodHandle::invoke - * @run testng MethodReferenceTestMethodHandle + * @run junit MethodReferenceTestMethodHandle */ import java.lang.invoke.*; import java.util.*; -import org.testng.annotations.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class MethodReferenceTestMethodHandle { MethodHandles.Lookup lookup = MethodHandles.lookup(); @@ -51,6 +50,7 @@ public class MethodReferenceTestMethodHandle { void apply(List st, int idx, Object v) throws Throwable; } + @Test public void testVirtual() throws Throwable { MethodType mt = MethodType.methodType(String.class, char.class, char.class); @@ -69,6 +69,7 @@ public class MethodReferenceTestMethodHandle { assertEquals("oome otring to oearch", ((ReplaceItf) ms::invoke).apply("some string to search", 's', 'o')); } + @Test public void testStatic() throws Throwable { MethodType fmt = MethodType.methodType(String.class, String.class, (new Object[1]).getClass()); MethodHandle fms = lookup.findStatic(String.class, "format", fmt); @@ -83,6 +84,7 @@ public class MethodReferenceTestMethodHandle { assertEquals("Testing One 2 3 four", ff2.apply("Testing %s %d %x %s", "One", new Integer(2), 3, "four")); } + @Test public void testVoid() throws Throwable { MethodType pmt = MethodType.methodType(void.class, int.class, Object.class); MethodHandle pms = lookup.findVirtual(List.class, "add", pmt); diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java index 15a35a7d0b3..26cf3ffd61d 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNew.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestNew + * @run junit MethodReferenceTestNew */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestNew { interface M0 { @@ -105,32 +103,36 @@ public class MethodReferenceTestNew { } } + @Test public void testConstructorReference0() { M0 q; q = N0::new; - assertEquals(q.m().getClass().getSimpleName(), "N0"); + assertEquals("N0", q.m().getClass().getSimpleName()); } + @Test public void testConstructorReference1() { M1 q; q = N1::new; - assertEquals(q.m(14).getClass().getSimpleName(), "N1"); + assertEquals("N1", q.m(14).getClass().getSimpleName()); } + @Test public void testConstructorReference2() { M2 q; q = N2::new; - assertEquals(q.m(7, "hi").toString(), "N2(7,hi)"); + assertEquals("N2(7,hi)", q.m(7, "hi").toString()); } + @Test public void testConstructorReferenceVarArgs() { MV q; q = NV::new; - assertEquals(q.m(5, 45).toString(), "NV(50)"); + assertEquals("NV(50)", q.m(5, 45).toString()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java index 1f61de4da8f..324428653da 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestNewInner + * @run junit MethodReferenceTestNewInner */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestNewInner { String note = "NO NOTE"; @@ -120,26 +118,29 @@ public class MethodReferenceTestNewInner { assertEquals(q.m(new MethodReferenceTestNewInner()).getClass().getSimpleName(), "N0"); } */ + @Test public void testConstructorReference0() { M0 q; q = N0::new; - assertEquals(q.m().getClass().getSimpleName(), "N0"); + assertEquals("N0", q.m().getClass().getSimpleName()); } + @Test public void testConstructorReference1() { M1 q; q = N1::new; - assertEquals(q.m(14).getClass().getSimpleName(), "N1"); + assertEquals("N1", q.m(14).getClass().getSimpleName()); } + @Test public void testConstructorReference2() { M2 q; note = "T2"; q = N2::new; - assertEquals(q.m(7, "hi").toString(), "T2:N2(7,hi)"); + assertEquals("T2:N2(7,hi)", q.m(7, "hi").toString()); } /*** diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java index ae3d7d6cc83..a172dbd8ce0 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestNewInnerImplicitArgs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8011591 * @summary BootstrapMethodError when capturing constructor ref to local classes - * @run testng MethodReferenceTestNewInnerImplicitArgs + * @run junit MethodReferenceTestNewInnerImplicitArgs */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * Test the case that a constructor has implicit parameters added to @@ -39,7 +38,6 @@ import static org.testng.Assert.assertEquals; * @author Robert Field */ -@Test public class MethodReferenceTestNewInnerImplicitArgs { @@ -56,7 +54,8 @@ public class MethodReferenceTestNewInnerImplicitArgs { S m(int i, int j); } - public static void testConstructorReferenceImplicitParameters() { + @Test + public void testConstructorReferenceImplicitParameters() { String title = "Hey"; String a2 = "!!!"; class MS extends S { @@ -66,7 +65,7 @@ public class MethodReferenceTestNewInnerImplicitArgs { } I result = MS::new; - assertEquals(result.m().b, "Hey!!!"); + assertEquals("Hey!!!", result.m().b); class MS2 extends S { MS2(int x, int y) { @@ -75,6 +74,6 @@ public class MethodReferenceTestNewInnerImplicitArgs { } I2 result2 = MS2::new; - assertEquals(result2.m(8, 4).b, "Hey8!!!4"); + assertEquals("Hey8!!!4", result2.m(8, 4).b); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java index 02fca01ee4a..10aeed014df 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase1 + * @run junit MethodReferenceTestSueCase1 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase1 { public interface Sam2 { public String get(T target, String s); } @@ -46,7 +44,8 @@ public class MethodReferenceTestSueCase1 { String m() { return var.get(new MethodReferenceTestSueCase1(), ""); } + @Test public void testSueCase1() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java index 4d558484337..f69fb1e4778 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase2 + * @run junit MethodReferenceTestSueCase2 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase2 { public interface Sam2 { public String get(T target, String s); } @@ -46,7 +44,8 @@ public class MethodReferenceTestSueCase2 { String m() { return var.get(new MethodReferenceTestSueCase2(), ""); } + @Test public void testSueCase2() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java index 9824163d5a3..abe86b37aad 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSueCase4.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,18 +25,16 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSueCase4 + * @run junit MethodReferenceTestSueCase4 */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestSueCase4 { public interface Sam2 { public String get(T target, String s); } @@ -51,7 +49,8 @@ public class MethodReferenceTestSueCase4 { String instanceMethod(String s) { return "2"; } } + @Test public void testSueCase4() { - assertEquals(m(), "2"); + assertEquals("2", m()); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java index 1c1afcbd70d..29bf0e56469 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSuper + * @run junit MethodReferenceTestSuper */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -77,7 +76,6 @@ class SPRB extends SPRA { } -@Test public class MethodReferenceTestSuper extends SPRB { String xsA_M(String s) { @@ -93,26 +91,27 @@ public class MethodReferenceTestSuper extends SPRB { return "_BMxsM:" + s; } + @Test public void testMethodReferenceSuper() { SPRI q; q = super::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = super::xsA_M; - assertEquals(q.m("*"), "A_MxsA:*"); + assertEquals("A_MxsA:*", q.m("*")); q = super::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = super::xsABM; - assertEquals(q.m("*"), "ABMxsB:*"); + assertEquals("ABMxsB:*", q.m("*")); q = super::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); q = super::xs_BM; - assertEquals(q.m("*"), "_BMxsB:*"); + assertEquals("_BMxsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java index 316f876e5f4..3db1119f8c5 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestSuperDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestSuperDefault + * @run junit MethodReferenceTestSuperDefault */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -61,20 +60,20 @@ interface DSPRB extends DSPRA { } -@Test public class MethodReferenceTestSuperDefault implements DSPRB { + @Test public void testMethodReferenceSuper() { DSPRI q; q = DSPRB.super::xsA__; - assertEquals(q.m("*"), "A__xsA:*"); + assertEquals("A__xsA:*", q.m("*")); q = DSPRB.super::xsAB_; - assertEquals(q.m("*"), "AB_xsB:*"); + assertEquals("AB_xsB:*", q.m("*")); q = DSPRB.super::xs_B_; - assertEquals(q.m("*"), "_B_xsB:*"); + assertEquals("_B_xsB:*", q.m("*")); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java index 46962975582..f0f66756a3f 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestTypeConversion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,11 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestTypeConversion + * @run junit MethodReferenceTestTypeConversion */ -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -40,21 +39,22 @@ class MethodReferenceTestTypeConversion_E { T xI(T t) { return t; } } -@Test public class MethodReferenceTestTypeConversion { interface ISi { int m(Short a); } interface ICc { char m(Character a); } + @Test public void testUnboxObjectToNumberWiden() { ISi q = (new MethodReferenceTestTypeConversion_E())::xI; - assertEquals(q.m((short)77), (short)77); + assertEquals((short)77, q.m((short)77)); } + @Test public void testUnboxObjectToChar() { ICc q = (new MethodReferenceTestTypeConversion_E())::xI; - assertEquals(q.m('@'), '@'); + assertEquals('@', q.m('@')); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java index c430af2ef24..ef0af7ac1af 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,19 +25,18 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgs + * @run junit MethodReferenceTestVarArgs */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field */ -@Test public class MethodReferenceTestVarArgs { interface SII { @@ -127,75 +126,81 @@ public class MethodReferenceTestVarArgs { return sb.toString(); } + @Test public void testVarArgsSuperclass() { SII q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsArray() { Sai q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsII() { SII q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsiii() { Siii q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsi() { Si q; q = MethodReferenceTestVarArgs::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = MethodReferenceTestVarArgs::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = MethodReferenceTestVarArgs::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = MethodReferenceTestVarArgs::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsaO() { SaO q; q = MethodReferenceTestVarArgs::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java index 989ed97aa6a..1a84660f592 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsExt.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsExt + * @run junit MethodReferenceTestVarArgsExt */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -49,80 +49,85 @@ interface NXai { String m(int[] a); } interface NXvi { String m(int... va); } -@Test public class MethodReferenceTestVarArgsExt { // These should be processed as var args + @Test public void testVarArgsNXSuperclass() { NXII q; q = (new Ext())::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsNXArray() { NXai q; q = (new Ext())::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsNXII() { NXII q; q = (new Ext())::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = (new Ext())::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = (new Ext())::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = (new Ext())::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsNXiii() { NXiii q; q = (new Ext())::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = (new Ext())::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = (new Ext())::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = (new Ext())::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsNXi() { NXi q; q = (new Ext())::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = (new Ext())::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = (new Ext())::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = (new Ext())::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsNXaO() { NXaO q; q = (new Ext())::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java index 575cb2b2a43..7eebb7c0772 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsSuper + * @run junit MethodReferenceTestVarArgsSuper */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -95,7 +95,6 @@ class MethodReferenceTestVarArgsSuper_Sub { } } -@Test public class MethodReferenceTestVarArgsSuper extends MethodReferenceTestVarArgsSuper_Sub { interface SPRII { String m(Integer a, Integer b); } @@ -132,74 +131,80 @@ public class MethodReferenceTestVarArgsSuper extends MethodReferenceTestVarArgsS // These should be processed as var args + @Test public void testVarArgsSPRSuperclass() { SPRII q; q = super::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsSPRArray() { SPRai q; q = super::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsSPRII() { SPRII q; q = super::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = super::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = super::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = super::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsSPRiii() { SPRiii q; q = super::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = super::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = super::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = super::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsSPRi() { SPRi q; q = super::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = super::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = super::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = super::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsSPRaO() { SPRaO q; q = super::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java index 81ea6cbee66..e3b8ce395cb 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsSuperDefault.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsSuperDefault + * @run junit MethodReferenceTestVarArgsSuperDefault */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -95,7 +95,6 @@ interface MethodReferenceTestVarArgsSuperDefault_I { } } -@Test public class MethodReferenceTestVarArgsSuperDefault implements MethodReferenceTestVarArgsSuperDefault_I { interface DSPRII { String m(Integer a, Integer b); } @@ -112,75 +111,81 @@ public class MethodReferenceTestVarArgsSuperDefault implements MethodReferenceTe // These should be processed as var args + @Test public void testVarArgsSPRSuperclass() { DSPRII q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsSPRArray() { DSPRai q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsSPRII() { DSPRII q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsSPRiii() { DSPRiii q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsSPRi() { DSPRi q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = MethodReferenceTestVarArgsSuperDefault_I.super::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsSPRaO() { DSPRaO q; q = MethodReferenceTestVarArgsSuperDefault_I.super::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java index 5066758ee10..6369cf387b6 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarArgsThis.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,13 @@ * @test * @bug 8003639 * @summary convert lambda testng tests to jtreg and add them - * @run testng MethodReferenceTestVarArgsThis + * @run junit MethodReferenceTestVarArgsThis */ -import org.testng.annotations.Test; import java.lang.reflect.Array; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; /** * @author Robert Field @@ -49,7 +49,6 @@ interface Nsai { String m(int[] a); } interface Nsvi { String m(int... va); } -@Test public class MethodReferenceTestVarArgsThis { // These should be processed as var args @@ -109,75 +108,81 @@ public class MethodReferenceTestVarArgsThis { return sb.toString(); } + @Test public void testVarArgsNsSuperclass() { NsII q; q = this::xvO; - assertEquals(q.m(55,66), "xvO:55*66*"); + assertEquals("xvO:55*66*", q.m(55,66)); } + @Test public void testVarArgsNsArray() { Nsai q; q = this::xvO; - assertEquals(q.m(new int[] { 55,66 } ), "xvO:[55,66,]*"); + assertEquals("xvO:[55,66,]*", q.m(new int[] { 55,66 } )); } + @Test public void testVarArgsNsII() { NsII q; q = this::xvI; - assertEquals(q.m(33,7), "xvI:33-7-"); + assertEquals("xvI:33-7-", q.m(33,7)); q = this::xIvI; - assertEquals(q.m(50,40), "xIvI:5040-"); + assertEquals("xIvI:5040-", q.m(50,40)); q = this::xvi; - assertEquals(q.m(100,23), "xvi:123"); + assertEquals("xvi:123", q.m(100,23)); q = this::xIvi; - assertEquals(q.m(9,21), "xIvi:(9)21"); + assertEquals("xIvi:(9)21", q.m(9,21)); } + @Test public void testVarArgsNsiii() { Nsiii q; q = this::xvI; - assertEquals(q.m(3, 2, 1), "xvI:3-2-1-"); + assertEquals("xvI:3-2-1-", q.m(3, 2, 1)); q = this::xIvI; - assertEquals(q.m(888, 99, 2), "xIvI:88899-2-"); + assertEquals("xIvI:88899-2-", q.m(888, 99, 2)); q = this::xvi; - assertEquals(q.m(900,80,7), "xvi:987"); + assertEquals("xvi:987", q.m(900,80,7)); q = this::xIvi; - assertEquals(q.m(333,27, 72), "xIvi:(333)99"); + assertEquals("xIvi:(333)99", q.m(333,27, 72)); } + @Test public void testVarArgsNsi() { Nsi q; q = this::xvI; - assertEquals(q.m(3), "xvI:3-"); + assertEquals("xvI:3-", q.m(3)); q = this::xIvI; - assertEquals(q.m(888), "xIvI:888"); + assertEquals("xIvI:888", q.m(888)); q = this::xvi; - assertEquals(q.m(900), "xvi:900"); + assertEquals("xvi:900", q.m(900)); q = this::xIvi; - assertEquals(q.m(333), "xIvi:(333)0"); + assertEquals("xIvi:(333)0", q.m(333)); } // These should NOT be processed as var args + @Test public void testVarArgsNsaO() { NsaO q; q = this::xvO; - assertEquals(q.m(new String[] { "yo", "there", "dude" }), "xvO:yo*there*dude*"); + assertEquals("xvO:yo*there*dude*", q.m(new String[] { "yo", "there", "dude" })); } diff --git a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java index aaf9df950d4..fa835995e0a 100644 --- a/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java +++ b/test/langtools/tools/javac/lambda/methodReferenceExecution/MethodReferenceTestVarHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,16 +24,15 @@ /** * @test * @summary test for VarHandle signature polymorphic methods - * @run testng MethodReferenceTestVarHandle + * @run junit MethodReferenceTestVarHandle */ import java.lang.invoke.*; import java.util.*; -import org.testng.annotations.Test; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -@Test public class MethodReferenceTestVarHandle { interface Setter { @@ -44,6 +43,7 @@ public class MethodReferenceTestVarHandle { int apply(int[] arr, int idx); } + @Test public void testSet() throws Throwable { VarHandle vh = MethodHandles.arrayElementVarHandle(int[].class); @@ -54,6 +54,7 @@ public class MethodReferenceTestVarHandle { assertEquals(42, data[0]); } + @Test public void testGet() throws Throwable { VarHandle vh = MethodHandles.arrayElementVarHandle(int[].class); diff --git a/test/langtools/tools/javac/lambdaShapes/TEST.properties b/test/langtools/tools/javac/lambdaShapes/TEST.properties index 8a7db8dbee7..07bc537f116 100644 --- a/test/langtools/tools/javac/lambdaShapes/TEST.properties +++ b/test/langtools/tools/javac/lambdaShapes/TEST.properties @@ -1,4 +1,4 @@ -TestNG.dirs = tools/javac/lambdaShapes +JUnit.dirs = tools/javac/lambdaShapes modules = \ jdk.compiler/com.sun.tools.javac.util diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java index 813c588349f..2ecf714092f 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/javac/FDTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -42,12 +42,15 @@ import javax.tools.SimpleJavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; -import org.testng.annotations.AfterSuite; -import org.testng.annotations.Test; -import org.testng.annotations.BeforeSuite; -import org.testng.annotations.DataProvider; -import static org.testng.Assert.*; +import org.junit.jupiter.api.AfterAll; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.MethodSource; +@ParameterizedClass +@MethodSource("caseGenerator") public class FDTest { public enum TestKind { @@ -63,7 +66,7 @@ public class FDTest { public static JavaCompiler comp; public static StandardJavaFileManager fm; - @BeforeSuite + @BeforeAll static void init() { // create default shared JavaCompiler - reused across multiple // compilations @@ -72,7 +75,7 @@ public class FDTest { fm = comp.getStandardFileManager(null, null, null); } - @AfterSuite + @AfterAll static void teardown() throws IOException { fm.close(); } @@ -87,14 +90,13 @@ public class FDTest { teardown(); } - @Test(dataProvider = "fdCases") - public void testOneCase(TestKind tk, Hierarchy hs) + @Test + public void testOneCase() throws Exception { FDTest.runTest(tk, hs, comp, fm); } - @DataProvider(name = "fdCases") - public Object[][] caseGenerator() { + public static Object[][] caseGenerator() { List> cases = generateCases(); Object[][] fdCases = new Object[cases.size()][]; for (int i = 0; i < cases.size(); ++i) { @@ -127,8 +129,6 @@ public class FDTest { DefenderTestSource source; DiagnosticChecker diagChecker; - public FDTest() {} - FDTest(TestKind tk, Hierarchy hs) { this.tk = tk; this.hs = hs; diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java index a1a37559e6d..dcfff28949e 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/separate/TestHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,17 +23,16 @@ package org.openjdk.tests.separate; -import org.testng.ITestResult; -import org.testng.annotations.AfterMethod; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import org.junit.jupiter.api.AfterEach; import static org.openjdk.tests.separate.SourceModel.Class; import static org.openjdk.tests.separate.SourceModel.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class TestHarness { @@ -69,7 +68,7 @@ public class TestHarness { verboseLocal.set(Boolean.TRUE); } - @AfterMethod + @AfterEach public void reset() { if (!this.verbose) { verboseLocal.set(Boolean.FALSE); @@ -87,16 +86,6 @@ public class TestHarness { return flags.toArray(new Compiler.Flags[0]); } - @AfterMethod - public void printError(ITestResult result) { - if (result.getStatus() == ITestResult.FAILURE) { - String clsName = result.getTestClass().getName(); - clsName = clsName.substring(clsName.lastIndexOf(".") + 1); - System.out.println("Test " + clsName + "." + - result.getName() + " FAILED"); - } - } - private static final ConcreteMethod stdCM = ConcreteMethod.std("-1"); private static final AbstractMethod stdAM = new AbstractMethod("int", stdMethodName); @@ -193,7 +182,7 @@ public class TestHarness { Object res = m.invoke(null); assertNotNull(res); if (value != null) { - assertEquals(res, value); + assertEquals(value, res); } } catch (InvocationTargetException | IllegalAccessException e) { fail("Unexpected exception thrown: " + e.getCause(), e.getCause()); diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java index 93629947f3a..b876a2538e0 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/DefaultMethodsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ package org.openjdk.tests.vm; import org.openjdk.tests.separate.Compiler; import org.openjdk.tests.separate.TestHarness; -import org.testng.annotations.Test; import static org.openjdk.tests.separate.SourceModel.AbstractMethod; import static org.openjdk.tests.separate.SourceModel.AccessFlag; @@ -36,11 +35,11 @@ import static org.openjdk.tests.separate.SourceModel.Extends; import static org.openjdk.tests.separate.SourceModel.Interface; import static org.openjdk.tests.separate.SourceModel.MethodParameter; import static org.openjdk.tests.separate.SourceModel.TypeParameter; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Test; -@Test(groups = "vm") public class DefaultMethodsTest extends TestHarness { public DefaultMethodsTest() { super(false, false); @@ -51,6 +50,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() == 22 */ + @Test public void testHarnessInvokeVirtual() { Class C = new Class("C", ConcreteMethod.std("22")); assertInvokeVirtualEquals(22, C); @@ -62,6 +62,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: I i = new C(); i.m() == 33; */ + @Test public void testHarnessInvokeInterface() { Interface I = new Interface("I", AbstractMethod.std()); Class C = new Class("C", I, ConcreteMethod.std("33")); @@ -73,6 +74,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws NoSuchMethod */ + @Test public void testHarnessThrows() { Class C = new Class("C"); assertThrows(NoSuchMethodError.class, C); @@ -85,6 +87,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: I i = new C(); i.m() == 44; */ + @Test public void testBasicDefault() { Interface I = new Interface("I", DefaultMethod.std("44")); Class C = new Class("C", I); @@ -102,6 +105,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: I i = new C(); i.m() == 44; */ + @Test public void testFarDefault() { Interface I = new Interface("I", DefaultMethod.std("44")); Interface J = new Interface("J", I); @@ -121,6 +125,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 44; * TEST: K k = new C(); k.m() == 44; */ + @Test public void testOverrideAbstract() { Interface I = new Interface("I", AbstractMethod.std()); Interface J = new Interface("J", I, DefaultMethod.std("44")); @@ -138,6 +143,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 55; * TEST: I i = new C(); i.m() == 55; */ + @Test public void testExisting() { Interface I = new Interface("I", DefaultMethod.std("44")); Class C = new Class("C", I, ConcreteMethod.std("55")); @@ -154,6 +160,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testInherited() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -171,6 +178,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: D d = new D(); d.m() == 11; * TEST: I i = new D(); i.m() == 11; */ + @Test public void testExistingInherited() { Interface I = new Interface("I", DefaultMethod.std("99")); Class C = new Class("C", ConcreteMethod.std("11")); @@ -188,6 +196,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: D d = new D(); d.m() == 22; * TEST: I i = new D(); i.m() == 22; */ + @Test public void testExistingInheritedOverride() { Interface I = new Interface("I", DefaultMethod.std("99")); Class C = new Class("C", I, ConcreteMethod.std("11")); @@ -207,6 +216,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: E e = new E(); e.m() == 22; * TEST: J j = new E(); j.m() == 22; */ + @Test public void testExistingInheritedPlusDefault() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -226,6 +236,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 77; * TEST: I i = new C(); i.m() == 77; */ + @Test public void testInheritedWithConcrete() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -243,6 +254,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 66; * TEST: I i = new C(); i.m() == 66; */ + @Test public void testInheritedWithConcreteAndImpl() { Interface I = new Interface("I", DefaultMethod.std("99")); Class B = new Class("B", I); @@ -259,6 +271,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws ICCE */ + @Test public void testConflict() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -274,6 +287,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() == 88 */ + @Test public void testAmbiguousReabstract() { Interface I = new Interface("I", AbstractMethod.std()); Interface J = new Interface("J", DefaultMethod.std("88")); @@ -293,6 +307,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: K k = new C(); k.m() == 99 * TEST: I i = new C(); i.m() == 99 */ + @Test public void testDiamond() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I); @@ -320,6 +335,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: L l = new C(); l.m() == 99 * TEST: M m = new C(); m.m() == 99 */ + @Test public void testExpandedDiamond() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I); @@ -343,6 +359,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m() throws AME */ + @Test public void testReabstract() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I, AbstractMethod.std()); @@ -360,6 +377,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testShadow() { Interface I = new Interface("I", DefaultMethod.std("88")); Interface J = new Interface("J", I, DefaultMethod.std("99")); @@ -379,6 +397,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m() == 99; * TEST: I i = new C(); i.m() == 99; */ + @Test public void testDisqualified() { Interface I = new Interface("I", DefaultMethod.std("88")); Interface J = new Interface("J", I, DefaultMethod.std("99")); @@ -396,6 +415,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m("string") == 88; * TEST: I i = new C(); i.m("string") == 88; */ + @Test public void testSelfFill() { // This test ensures that a concrete method overrides a default method // that matches at the language-level, but has a different method @@ -426,6 +446,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C.class.getMethod("m").invoke(new C()) == 99 */ + @Test public void testReflectCall() { Interface I = new Interface("I", DefaultMethod.std("99")); //workaround accessibility issue when loading C with DirectedClassLoader @@ -468,7 +489,7 @@ public class DefaultMethodsTest extends TestHarness { } assertNotNull(res); - assertEquals(res.intValue(), 99); + assertEquals(99, res.intValue()); compiler.cleanup(); } @@ -485,6 +506,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: J j = new C(); j.m("A","B","C") == 88; * TEST: K k = new C(); k.m("A","B","C") == 88; */ + @Test public void testBridges() { DefaultMethod dm = new DefaultMethod("int", stdMethodName, "return 99;", new MethodParameter("T", "t"), new MethodParameter("V", "v"), @@ -538,6 +560,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 88; * TEST: I i = new C(); i.m() == 88; */ + @Test public void testSuperBasic() { Interface J = new Interface("J", DefaultMethod.std("88")); Interface I = new Interface("I", J, new DefaultMethod( @@ -559,6 +582,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() throws ICCE * TODO: add case for K k = new C(); k.m() throws ICCE */ + @Test public void testSuperConflict() { Interface K = new Interface("K", DefaultMethod.std("99")); Interface L = new Interface("L", DefaultMethod.std("101")); @@ -581,6 +605,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() == 99 * TODO: add case for J j = new C(); j.m() == ??? */ + @Test public void testSuperDisqual() { Interface I = new Interface("I", DefaultMethod.std("99")); Interface J = new Interface("J", I, DefaultMethod.std("55")); @@ -600,6 +625,7 @@ public class DefaultMethodsTest extends TestHarness { * TEST: C c = new C(); c.m() throws AME * TODO: add case for I i = new C(); i.m() throws AME */ + @Test public void testSuperNull() { Interface J = new Interface("J", AbstractMethod.std()); Interface I = new Interface("I", J, new DefaultMethod( @@ -621,6 +647,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: I i = new C(); i.m("") == 88; */ + @Test public void testSuperGeneric() { Interface J = new Interface("J", new TypeParameter("T"), new DefaultMethod("int", stdMethodName, "return 88;", @@ -646,6 +673,7 @@ public class DefaultMethodsTest extends TestHarness { * * TEST: C c = new C(); c.m("string") == 44 */ + @Test public void testSuperGenericDisqual() { MethodParameter t = new MethodParameter("T", "t"); MethodParameter s = new MethodParameter("String", "s"); @@ -672,6 +700,7 @@ public class DefaultMethodsTest extends TestHarness { * class S { Object foo() { return (new D()).m(); } // link sig: ()LInteger; * TEST: S s = new S(); s.foo() == new Integer(99) */ + @Test public void testCovarBridge() { Interface I = new Interface("I", new DefaultMethod( "Integer", "m", "return new Integer(88);")); @@ -700,6 +729,7 @@ public class DefaultMethodsTest extends TestHarness { * class S { Object foo() { return (new D()).m(); } // link sig: ()LInteger; * TEST: S s = new S(); s.foo() == new Integer(88) */ + @Test public void testNoCovarNoBridge() { Interface I = new Interface("I", new DefaultMethod( "Integer", "m", "return new Integer(88);")); @@ -732,6 +762,7 @@ public class DefaultMethodsTest extends TestHarness { * It verifies that default method analysis occurs when mirandas have been * inherited and the supertypes don't have any overpass methods. */ + @Test public void testNoNewMiranda() { Interface J = new Interface("J", AbstractMethod.std()); Interface I = new Interface("I", J, DefaultMethod.std("99")); @@ -755,6 +786,7 @@ public class DefaultMethodsTest extends TestHarness { * Test that a erased-signature-matching method does not implement * non-language-level matching methods */ + @Test public void testNonConcreteFill() { AbstractMethod ipm = new AbstractMethod("int", "m", new MethodParameter("T", "t"), @@ -809,6 +841,7 @@ public class DefaultMethodsTest extends TestHarness { assertInvokeInterfaceEquals(99, C, I.with("String", "String", "String"), ipm, a, a, a); } + @Test public void testStrictfpDefault() { try { java.lang.Class.forName("org.openjdk.tests.vm.StrictfpDefault"); diff --git a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java index 4fef2df615d..904883a0c3e 100644 --- a/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java +++ b/test/langtools/tools/javac/lambdaShapes/org/openjdk/tests/vm/FDSeparateCompilationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,11 +28,8 @@ package org.openjdk.tests.vm; import java.util.*; -import org.testng.ITestResult; -import org.testng.annotations.Test; -import org.testng.annotations.DataProvider; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.AfterSuite; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.openjdk.tests.separate.*; import org.openjdk.tests.separate.Compiler; @@ -41,7 +38,10 @@ import org.openjdk.tests.shapegen.Hierarchy; import org.openjdk.tests.shapegen.HierarchyGenerator; import org.openjdk.tests.shapegen.ClassCase; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.openjdk.tests.separate.SourceModel.*; import static org.openjdk.tests.separate.SourceModel.Class; import static org.openjdk.tests.separate.SourceModel.Method; @@ -55,7 +55,6 @@ public class FDSeparateCompilationTest extends TestHarness { super(false, true); } - @DataProvider(name = "allShapes", parallel = true) public Object[][] hierarchyGenerator() { ArrayList allCases = new ArrayList<>(); @@ -92,7 +91,9 @@ public class FDSeparateCompilationTest extends TestHarness { private static final ConcreteMethod canonicalMethod = new ConcreteMethod( "String", "m", "returns " + EMPTY + ";", AccessFlag.PUBLIC); - @Test(enabled = false, groups = "vm", dataProvider = "allShapes") + @Disabled + @ParameterizedTest + @MethodSource("hierarchyGenerator") public void separateCompilationTest(Hierarchy hs) { ClassCase cc = hs.root; Type type = sourceTypeFrom(hs.root); @@ -118,17 +119,8 @@ public class FDSeparateCompilationTest extends TestHarness { } } - @AfterMethod - public void printCaseError(ITestResult result) { - if (result.getStatus() == ITestResult.FAILURE) { - Hierarchy hs = (Hierarchy)result.getParameters()[0]; - System.out.println("Separate compilation case " + hs); - printCaseDetails(hs); - } - } - - @AfterSuite - public void cleanupCompilerCache() { + @AfterAll + public static void cleanupCompilerCache() { Compiler.purgeCache(); } diff --git a/test/langtools/tools/javac/records/BigRecordsToStringTest.java b/test/langtools/tools/javac/records/BigRecordsToStringTest.java index 87e269a9532..1e42cf03ebc 100644 --- a/test/langtools/tools/javac/records/BigRecordsToStringTest.java +++ b/test/langtools/tools/javac/records/BigRecordsToStringTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ * @test * @bug 8261847 * @summary test the output of the toString method of records with a large number of components - * @run testng BigRecordsToStringTest + * @run junit BigRecordsToStringTest */ import java.lang.reflect.Constructor; @@ -36,10 +36,9 @@ import java.lang.reflect.Parameter; import java.util.List; import java.util.function.Supplier; -import org.testng.annotations.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; -@Test public class BigRecordsToStringTest { record BigInt( int i1,int i2,int i3,int i4,int i5,int i6,int i7,int i8,int i9,int i10, @@ -164,6 +163,7 @@ public class BigRecordsToStringTest { "i111=111, i112=112, i113=113, i114=114, i115=115, i116=116, i117=117, i118=118, i119=119, i120=120, i121=121, i122=122, i123=123, " + "i124=124, i125=125, i126=126, i127=127]"; + @Test public void testToStringOutput() { assertTrue(bigInt.toString().equals(BIG_INT_TO_STRING_OUTPUT)); assertTrue(bigLong.toString().equals(BIG_LONG_TO_STRING_OUTPUT)); diff --git a/test/langtools/tools/javac/records/RecordMemberTests.java b/test/langtools/tools/javac/records/RecordMemberTests.java index 7d358c21953..f7291e70cb0 100644 --- a/test/langtools/tools/javac/records/RecordMemberTests.java +++ b/test/langtools/tools/javac/records/RecordMemberTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ * @test * @bug 8246774 * @summary test several assertions on record classes members - * @run testng RecordMemberTests + * @run junit RecordMemberTests */ import java.lang.reflect.Constructor; @@ -36,10 +36,9 @@ import java.lang.reflect.Parameter; import java.util.List; import java.util.function.Supplier; -import org.testng.annotations.*; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; -@Test public class RecordMemberTests { public record R1(int i, int j) {} @@ -63,43 +62,47 @@ public class RecordMemberTests { R3 r3 = new R3(1, 2); R4 r4 = new R4(1, 2); + @Test public void testConstruction() { for (int i : new int[] { r1.i, r2.i, r3.i, r1.i(), r2.i(), r3.i() }) - assertEquals(i, 1); + assertEquals(1, i); for (int j : new int[] { r1.j, r2.j, r3.j, r1.j(), r2.j(), r3.j() }) - assertEquals(j, 2); + assertEquals(2, j); - assertEquals(r4.i, 0); - assertEquals(r4.j, 0); + assertEquals(0, r4.i); + assertEquals(0, r4.j); } + @Test public void testConstructorParameterNames() throws ReflectiveOperationException { for (Class cl : List.of(R1.class, R2.class, R3.class, R4.class)) { Constructor c = cl.getConstructor(int.class, int.class); assertNotNull(c); Parameter[] parameters = c.getParameters(); - assertEquals(parameters.length, 2); - assertEquals(parameters[0].getName(), "i"); - assertEquals(parameters[1].getName(), "j"); + assertEquals(2, parameters.length); + assertEquals("i", parameters[0].getName()); + assertEquals("j", parameters[1].getName()); } } + @Test public void testSuperclass() { - assertEquals(R1.class.getSuperclass(), Record.class); + assertEquals(Record.class, R1.class.getSuperclass()); // class is final assertTrue((R1.class.getModifiers() & Modifier.FINAL) != 0); } + @Test public void testMandatedMembersPresent() throws ReflectiveOperationException { // fields are present, of the right type, final and private - assertEquals(R1.class.getDeclaredFields().length, 2); + assertEquals(2, R1.class.getDeclaredFields().length); for (String s : List.of("i", "j")) { Field iField = R1.class.getDeclaredField(s); - assertEquals(iField.getType(), int.class); - assertEquals((iField.getModifiers() & Modifier.STATIC), 0); + assertEquals(int.class, iField.getType()); + assertEquals(0, (iField.getModifiers() & Modifier.STATIC)); assertTrue((iField.getModifiers() & Modifier.PRIVATE) != 0); assertTrue((iField.getModifiers() & Modifier.FINAL) != 0); } @@ -107,15 +110,15 @@ public class RecordMemberTests { // methods are present, of the right descriptor, and public/instance/concrete for (String s : List.of("i", "j")) { Method iMethod = R1.class.getDeclaredMethod(s); - assertEquals(iMethod.getReturnType(), int.class); - assertEquals(iMethod.getParameterCount(), 0); - assertEquals((iMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT)), 0); + assertEquals(int.class, iMethod.getReturnType()); + assertEquals(0, iMethod.getParameterCount()); + assertEquals(0, (iMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT))); } Constructor c = R1.class.getConstructor(int.class, int.class); R1 r1 = (R1) c.newInstance(1, 2); - assertEquals(r1.i(), 1); - assertEquals(r1.j(), 2); + assertEquals(1, r1.i()); + assertEquals(2, r1.j()); } record OrdinaryMembers(int x) { @@ -124,21 +127,23 @@ public class RecordMemberTests { public String sf () { return "instance"; } } + @Test public void testOrdinaryMembers() { OrdinaryMembers.ss = "foo"; - assertEquals(OrdinaryMembers.ssf(), "foo"); + assertEquals("foo", OrdinaryMembers.ssf()); OrdinaryMembers o = new OrdinaryMembers(3); - assertEquals(o.sf(), "instance"); + assertEquals("instance", o.sf()); } class LocalRecordHelper { Class m(int x) { record R (int x) { } - assertEquals(new R(x).x(), x); + assertEquals(x, new R(x).x()); return R.class; } } + @Test public void testLocalRecordsStatic() { Class c = new LocalRecordHelper().m(3); String message = c.toGenericString(); @@ -181,6 +186,7 @@ public class RecordMemberTests { } } + @Test public void testNestedRecordsStatic() { NestedRecordHelper n = new NestedRecordHelper(); for (Class c : List.of(NestedRecordHelper.R1.class, diff --git a/test/langtools/tools/javac/records/VarargsRecordsTest.java b/test/langtools/tools/javac/records/VarargsRecordsTest.java index 14b9a9481d2..60c6e70d3ad 100644 --- a/test/langtools/tools/javac/records/VarargsRecordsTest.java +++ b/test/langtools/tools/javac/records/VarargsRecordsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,16 +27,15 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; -import org.testng.annotations.Test; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; /** * @test * @bug 8246774 * @summary test for varargs record components - * @run testng VarargsRecordsTest + * @run junit VarargsRecordsTest */ -@Test public class VarargsRecordsTest { public record RI(int... xs) { } public record RII(int x, int... xs) { } @@ -49,49 +48,52 @@ public class VarargsRecordsTest { RII r5 = new RII(1, 2); RII r6 = new RII(1, 2, 3); + @Test public void assertVarargsInstances() { - assertEquals(r1.xs.length, 0); - assertEquals(r2.xs.length, 1); - assertEquals(r3.xs.length, 2); - assertEquals(r4.xs.length, 0); - assertEquals(r5.xs.length, 1); - assertEquals(r6.xs.length, 2); + assertEquals(0, r1.xs.length); + assertEquals(1, r2.xs.length); + assertEquals(2, r3.xs.length); + assertEquals(0, r4.xs.length); + assertEquals(1, r5.xs.length); + assertEquals(2, r6.xs.length); - assertEquals(r2.xs[0], 1); - assertEquals(r3.xs[0], 1); - assertEquals(r3.xs[1], 2); + assertEquals(1, r2.xs[0]); + assertEquals(1, r3.xs[0]); + assertEquals(2, r3.xs[1]); - assertEquals(r5.xs[0], 2); - assertEquals(r6.xs[0], 2); - assertEquals(r6.xs[1], 3); + assertEquals(2, r5.xs[0]); + assertEquals(2, r6.xs[0]); + assertEquals(3, r6.xs[1]); } + @Test public void testMembers() throws ReflectiveOperationException { Constructor c = RI.class.getConstructor(int[].class); assertNotNull(c); assertTrue(c.isVarArgs()); Parameter[] parameters = c.getParameters(); - assertEquals(parameters.length, 1); - assertEquals(parameters[0].getName(), "xs"); + assertEquals(1, parameters.length); + assertEquals("xs", parameters[0].getName()); RI ri = (RI) c.newInstance(new int[]{1, 2}); - assertEquals(ri.xs()[0], 1); - assertEquals(ri.xs()[1], 2); + assertEquals(1, ri.xs()[0]); + assertEquals(2, ri.xs()[1]); Field xsField = RI.class.getDeclaredField("xs"); - assertEquals(xsField.getType(), int[].class); - assertEquals((xsField.getModifiers() & Modifier.STATIC), 0); + assertEquals(int[].class, xsField.getType()); + assertEquals(0, (xsField.getModifiers() & Modifier.STATIC)); assertTrue((xsField.getModifiers() & Modifier.PRIVATE) != 0); assertTrue((xsField.getModifiers() & Modifier.FINAL) != 0); - assertEquals(((int[]) xsField.get(ri))[0], 1); + assertEquals(1, ((int[]) xsField.get(ri))[0]); Method xsMethod = RI.class.getDeclaredMethod("xs"); - assertEquals(xsMethod.getReturnType(), int[].class); - assertEquals(xsMethod.getParameterCount(), 0); - assertEquals((xsMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT)), 0); - assertEquals(((int[]) xsMethod.invoke(ri))[0], 1); + assertEquals(int[].class, xsMethod.getReturnType()); + assertEquals(0, xsMethod.getParameterCount()); + assertEquals(0, (xsMethod.getModifiers() & (Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.ABSTRACT))); + assertEquals(1, ((int[]) xsMethod.invoke(ri))[0]); } + @Test public void testNotVarargs() throws ReflectiveOperationException { Constructor c = RX.class.getConstructor(int[].class); assertFalse(c.isVarArgs()); diff --git a/test/langtools/tools/javac/tree/T8024415.java b/test/langtools/tools/javac/tree/T8024415.java index ef4b0429a81..a869f63f079 100644 --- a/test/langtools/tools/javac/tree/T8024415.java +++ b/test/langtools/tools/javac/tree/T8024415.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,16 +29,15 @@ * @modules jdk.compiler/com.sun.tools.javac.file * jdk.compiler/com.sun.tools.javac.tree * jdk.compiler/com.sun.tools.javac.util - * @run testng T8024415 + * @run junit T8024415 */ -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.io.StringWriter; -import org.testng.annotations.Test; import com.sun.tools.javac.file.JavacFileManager; import com.sun.tools.javac.tree.JCTree; @@ -47,13 +46,13 @@ import com.sun.tools.javac.tree.Pretty; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Names; +import org.junit.jupiter.api.Test; /* * Test verifies that the precedence rules of conditional expressions * (JCConditional) are correct. */ -@Test public class T8024415 { TreeMaker maker; @@ -72,6 +71,7 @@ public class T8024415 { // JLS 15.25: The conditional operator is syntactically right-associative // (it groups right-to-left). Thus, a?b:c?d:e?f:g means the same as // a?b:(c?d:(e?f:g)). + @Test public void testAssociativity() throws IOException { JCTree left = maker.Conditional(maker.Conditional(x, x, x), x, x); @@ -80,8 +80,8 @@ public class T8024415 { String prettyLeft = prettyPrint(left); String prettyRight = prettyPrint(right); - assertEquals(prettyLeft.replaceAll("\\s", ""), "(x?x:x)?x:x"); - assertEquals(prettyRight.replaceAll("\\s", ""), "x?x:x?x:x"); + assertEquals("(x?x:x)?x:x", prettyLeft.replaceAll("\\s", "")); + assertEquals("x?x:x?x:x", prettyRight.replaceAll("\\s", "")); } @@ -89,6 +89,7 @@ public class T8024415 { // The true-part of a conditional expression is surrounded by ? and : // and can thus always be parsed unambiguously without surrounding // parentheses. + @Test public void testPrecedence() throws IOException { JCTree left = maker.Conditional(maker.Assign(x, x), x, x); @@ -99,9 +100,9 @@ public class T8024415 { String prettyMiddle = prettyPrint(middle); String prettyRight = prettyPrint(right); - assertEquals(prettyLeft.replaceAll("\\s", ""), "(x=x)?x:x"); - assertEquals(prettyMiddle.replaceAll("\\s", ""), "x?x=x:x"); - assertEquals(prettyRight.replaceAll("\\s", ""), "x?x:(x=x)"); + assertEquals("(x=x)?x:x", prettyLeft.replaceAll("\\s", "")); + assertEquals("x?x=x:x", prettyMiddle.replaceAll("\\s", "")); + assertEquals("x?x:(x=x)", prettyRight.replaceAll("\\s", "")); } diff --git a/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java b/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java index eb7953a0baf..929d2c58685 100644 --- a/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java +++ b/test/langtools/tools/javac/typeVariableCast/TypeVariableCastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,14 +24,14 @@ /* * @test * @bug 8209022 - * @run testng TypeVariableCastTest + * @run junit TypeVariableCastTest * @summary Missing checkcast when casting to type parameter bounded by intersection type */ import java.util.*; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; -@Test public class TypeVariableCastTest { static & Runnable> void f() { Runnable r = (T) new ArrayList<>(); @@ -41,13 +41,17 @@ public class TypeVariableCastTest { Runnable r = (T) new ArrayList<>(); } - @Test(expectedExceptions = ClassCastException.class) - static void testMethodF() { - f(); + @Test + void testMethodF() { + Assertions.assertThrows(ClassCastException.class, () -> { + f(); + }); } - @Test(expectedExceptions = ClassCastException.class) - static void testMethodG() { - g(); + @Test + void testMethodG() { + Assertions.assertThrows(ClassCastException.class, () -> { + g(); + }); } } From 258fcf9f5ea089891f0119bbf8058da389f72321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Sj=C3=B6len?= Date: Mon, 22 Sep 2025 07:15:41 +0000 Subject: [PATCH 146/556] 8367987: Memory leak in MemBaseline: Must delete _vma_allocations Reviewed-by: phubner, azafari --- src/hotspot/share/nmt/memBaseline.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hotspot/share/nmt/memBaseline.hpp b/src/hotspot/share/nmt/memBaseline.hpp index 3f1ea46d815..a9d604deb19 100644 --- a/src/hotspot/share/nmt/memBaseline.hpp +++ b/src/hotspot/share/nmt/memBaseline.hpp @@ -89,6 +89,10 @@ class MemBaseline { _baseline_type(Not_baselined) { } + ~MemBaseline() { + delete _vma_allocations; + } + void baseline(bool summaryOnly = true); BaselineType baseline_type() const { return _baseline_type; } From f10fbe1fb40645633b91fad2af3d7c2cbb005b39 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Mon, 22 Sep 2025 07:20:00 +0000 Subject: [PATCH 147/556] 8368072: Remove redundant arguments of MarkingNMethodClosure Reviewed-by: stefank, fandreuzzi --- src/hotspot/share/gc/g1/g1FullGCMarkTask.cpp | 2 +- .../share/gc/parallel/psParallelCompact.cpp | 4 +--- src/hotspot/share/gc/serial/serialFullGC.cpp | 4 +--- .../shenandoah/shenandoahRootProcessor.inline.hpp | 2 +- src/hotspot/share/memory/iterator.cpp | 14 ++++---------- src/hotspot/share/memory/iterator.hpp | 8 +++----- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1FullGCMarkTask.cpp b/src/hotspot/share/gc/g1/g1FullGCMarkTask.cpp index 25002186280..52b0d04a500 100644 --- a/src/hotspot/share/gc/g1/g1FullGCMarkTask.cpp +++ b/src/hotspot/share/gc/g1/g1FullGCMarkTask.cpp @@ -41,7 +41,7 @@ void G1FullGCMarkTask::work(uint worker_id) { Ticks start = Ticks::now(); ResourceMark rm; G1FullGCMarker* marker = collector()->marker(worker_id); - MarkingNMethodClosure code_closure(marker->mark_closure(), !NMethodToOopClosure::FixRelocations, true /* keepalive nmethods */); + MarkingNMethodClosure code_closure(marker->mark_closure()); if (ClassUnloading) { _root_processor.process_strong_roots(marker->mark_closure(), diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.cpp b/src/hotspot/share/gc/parallel/psParallelCompact.cpp index 8c02e135379..f4383e573af 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.cpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.cpp @@ -1065,9 +1065,7 @@ public: ParCompactionManager* cm = ParCompactionManager::gc_thread_compaction_manager(_worker_id); - MarkingNMethodClosure mark_and_push_in_blobs(&cm->_mark_and_push_closure, - !NMethodToOopClosure::FixRelocations, - true /* keepalive nmethods */); + MarkingNMethodClosure mark_and_push_in_blobs(&cm->_mark_and_push_closure); thread->oops_do(&cm->_mark_and_push_closure, &mark_and_push_in_blobs); diff --git a/src/hotspot/share/gc/serial/serialFullGC.cpp b/src/hotspot/share/gc/serial/serialFullGC.cpp index fc63b81b7ce..d45454a768f 100644 --- a/src/hotspot/share/gc/serial/serialFullGC.cpp +++ b/src/hotspot/share/gc/serial/serialFullGC.cpp @@ -485,9 +485,7 @@ void SerialFullGC::phase1_mark(bool clear_all_softrefs) { { StrongRootsScope srs(0); - MarkingNMethodClosure mark_code_closure(&follow_root_closure, - !NMethodToOopClosure::FixRelocations, - true); + MarkingNMethodClosure mark_code_closure(&follow_root_closure); // Start tracing from roots, there are 3 kinds of roots in full-gc. // diff --git a/src/hotspot/share/gc/shenandoah/shenandoahRootProcessor.inline.hpp b/src/hotspot/share/gc/shenandoah/shenandoahRootProcessor.inline.hpp index fa3fa90b2f5..6aebec28163 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahRootProcessor.inline.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahRootProcessor.inline.hpp @@ -152,7 +152,7 @@ public: // we risk executing that code cache blob, and crashing. template void ShenandoahSTWRootScanner::roots_do(T* oops, uint worker_id) { - MarkingNMethodClosure nmethods_cl(oops, !NMethodToOopClosure::FixRelocations, true /*FIXME*/); + MarkingNMethodClosure nmethods_cl(oops); CLDToOopClosure clds(oops, ClassLoaderData::_claim_strong); ResourceMark rm; diff --git a/src/hotspot/share/memory/iterator.cpp b/src/hotspot/share/memory/iterator.cpp index 961130c2b3f..09e924164e8 100644 --- a/src/hotspot/share/memory/iterator.cpp +++ b/src/hotspot/share/memory/iterator.cpp @@ -53,16 +53,10 @@ void MarkingNMethodClosure::do_nmethod(nmethod* nm) { // Process the oops in the nmethod nm->oops_do(_cl); - if (_keepalive_nmethods) { - // CodeCache unloading support - nm->mark_as_maybe_on_stack(); + // CodeCache unloading support + nm->mark_as_maybe_on_stack(); - BarrierSetNMethod* bs_nm = BarrierSet::barrier_set()->barrier_set_nmethod(); - bs_nm->disarm(nm); - } - - if (_fix_relocations) { - nm->fix_oop_relocations(); - } + BarrierSetNMethod* bs_nm = BarrierSet::barrier_set()->barrier_set_nmethod(); + bs_nm->disarm(nm); } } diff --git a/src/hotspot/share/memory/iterator.hpp b/src/hotspot/share/memory/iterator.hpp index 8310c2949e2..25820d6de02 100644 --- a/src/hotspot/share/memory/iterator.hpp +++ b/src/hotspot/share/memory/iterator.hpp @@ -255,13 +255,11 @@ class NMethodToOopClosure : public NMethodClosure { const static bool FixRelocations = true; }; -class MarkingNMethodClosure : public NMethodToOopClosure { - bool _keepalive_nmethods; +class MarkingNMethodClosure : public NMethodClosure { + OopClosure* _cl; public: - MarkingNMethodClosure(OopClosure* cl, bool fix_relocations, bool keepalive_nmethods) : - NMethodToOopClosure(cl, fix_relocations), - _keepalive_nmethods(keepalive_nmethods) {} + MarkingNMethodClosure(OopClosure* cl) : _cl(cl) {} // Called for each nmethod. virtual void do_nmethod(nmethod* nm); From 44454633eb163de17bba939e84311e8d954a2f53 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Mon, 22 Sep 2025 07:54:25 +0000 Subject: [PATCH 148/556] 8368086: G1: Use ThreadsClaimTokenScope in G1CMRemarkTask Reviewed-by: fandreuzzi, stefank, iwalulya --- src/hotspot/share/gc/g1/g1ConcurrentMark.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp index a7dc0a7f33f..e52d380e26b 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp @@ -1651,6 +1651,8 @@ class G1RemarkThreadsClosure : public ThreadClosure { }; class G1CMRemarkTask : public WorkerTask { + // For Threads::possibly_parallel_threads_do + ThreadsClaimTokenScope _threads_claim_token_scope; G1ConcurrentMark* _cm; public: void work(uint worker_id) { @@ -1674,7 +1676,7 @@ public: } G1CMRemarkTask(G1ConcurrentMark* cm, uint active_workers) : - WorkerTask("Par Remark"), _cm(cm) { + WorkerTask("Par Remark"), _threads_claim_token_scope(), _cm(cm) { _cm->terminator()->reset_for_reuse(active_workers); } }; @@ -1693,8 +1695,6 @@ void G1ConcurrentMark::finalize_marking() { // through the task. { - StrongRootsScope srs(active_workers); - G1CMRemarkTask remarkTask(this, active_workers); // We will start all available threads, even if we decide that the // active_workers will be fewer. The extra ones will just bail out From 433d2ec534bbf6ec08157c976b567b81b748b128 Mon Sep 17 00:00:00 2001 From: Leo Korinth Date: Mon, 22 Sep 2025 09:53:56 +0000 Subject: [PATCH 149/556] 8367409: G1: Remove unused G1MonotonicArena::Segment::copy_to() Reviewed-by: ayang, tschatzl --- src/hotspot/share/gc/g1/g1MonotonicArena.hpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1MonotonicArena.hpp b/src/hotspot/share/gc/g1/g1MonotonicArena.hpp index 0434a222b21..828c7f4e86d 100644 --- a/src/hotspot/share/gc/g1/g1MonotonicArena.hpp +++ b/src/hotspot/share/gc/g1/g1MonotonicArena.hpp @@ -176,11 +176,6 @@ public: static Segment* create_segment(uint slot_size, uint num_slots, Segment* next, MemTag mem_tag); static void delete_segment(Segment* segment); - // Copies the contents of this segment into the destination. - void copy_to(void* dest) const { - ::memcpy(dest, _bottom, length() * _slot_size); - } - bool is_full() const { return _next_allocate >= _num_slots; } }; From e8db14f584fa92db170e056bc68074ccabae82c9 Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Mon, 22 Sep 2025 10:12:12 +0000 Subject: [PATCH 150/556] 8349910: Implement JEP 517: HTTP/3 for the HTTP Client API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aleksei Efimov Co-authored-by: Bradford Wetmore Co-authored-by: Daniel Jeliński Co-authored-by: Darragh Clarke Co-authored-by: Jaikiran Pai Co-authored-by: Michael McMahon Co-authored-by: Volkan Yazici Co-authored-by: Conor Cleary Co-authored-by: Patrick Concannon Co-authored-by: Rahul Yadav Co-authored-by: Daniel Fuchs Reviewed-by: djelinski, jpai, aefimov, abarashev, michaelm --- .../net/quic/QuicKeyUnavailableException.java | 45 + .../internal/net/quic/QuicOneRttContext.java | 38 + .../jdk/internal/net/quic/QuicTLSContext.java | 152 + .../jdk/internal/net/quic/QuicTLSEngine.java | 508 ++ .../net/quic/QuicTransportErrors.java | 349 ++ .../net/quic/QuicTransportException.java | 111 + .../quic/QuicTransportParametersConsumer.java | 39 + .../jdk/internal/net/quic/QuicVersion.java | 100 + src/java.base/share/classes/module-info.java | 3 + .../share/classes/sun/security/ssl/Alert.java | 8 +- .../sun/security/ssl/AlpnExtension.java | 20 +- .../sun/security/ssl/CertificateMessage.java | 29 +- .../classes/sun/security/ssl/ClientHello.java | 2 +- .../classes/sun/security/ssl/Finished.java | 20 + .../classes/sun/security/ssl/KeyUpdate.java | 6 + .../sun/security/ssl/OutputRecord.java | 14 +- .../security/ssl/PostHandshakeContext.java | 17 +- .../classes/sun/security/ssl/QuicCipher.java | 699 +++ .../security/ssl/QuicEngineOutputRecord.java | 245 + .../sun/security/ssl/QuicKeyManager.java | 1216 +++++ .../sun/security/ssl/QuicTLSEngineImpl.java | 893 ++++ .../ssl/QuicTransportParametersExtension.java | 189 + .../security/ssl/SSLAlgorithmConstraints.java | 40 + .../sun/security/ssl/SSLConfiguration.java | 21 +- .../sun/security/ssl/SSLContextImpl.java | 4 + .../sun/security/ssl/SSLExtension.java | 27 +- .../classes/sun/security/ssl/ServerHello.java | 38 +- .../security/ssl/SunX509KeyManagerImpl.java | 17 + .../sun/security/ssl/TransportContext.java | 6 +- .../sun/security/ssl/X509Authentication.java | 46 +- .../ssl/X509KeyManagerCertChecking.java | 20 + .../sun/security/ssl/X509KeyManagerImpl.java | 17 + .../security/ssl/X509TrustManagerImpl.java | 65 + .../share/conf/security/java.security | 27 + .../classes/java/net/http/HttpClient.java | 100 +- .../classes/java/net/http/HttpOption.java | 176 + .../classes/java/net/http/HttpRequest.java | 64 +- .../java/net/http/HttpRequestOptionImpl.java | 34 + .../classes/java/net/http/HttpResponse.java | 119 +- .../java/net/http/StreamLimitException.java | 104 + .../UnsupportedProtocolVersionException.java | 55 + .../classes/java/net/http/package-info.java | 21 +- .../net/http/AltServicesRegistry.java | 569 +++ .../internal/net/http/AltSvcProcessor.java | 495 ++ .../jdk/internal/net/http/Exchange.java | 102 +- .../jdk/internal/net/http/ExchangeImpl.java | 434 +- .../net/http/H3FrameOrderVerifier.java | 200 + .../jdk/internal/net/http/Http1Exchange.java | 2 +- .../internal/net/http/Http2ClientImpl.java | 3 +- .../internal/net/http/Http2Connection.java | 46 +- .../internal/net/http/Http3ClientImpl.java | 844 ++++ .../net/http/Http3ClientProperties.java | 171 + .../internal/net/http/Http3Connection.java | 1657 +++++++ .../net/http/Http3ConnectionPool.java | 207 + .../internal/net/http/Http3ExchangeImpl.java | 1795 +++++++ .../net/http/Http3PendingConnections.java | 224 + .../internal/net/http/Http3PushManager.java | 811 +++ .../net/http/Http3PushPromiseStream.java | 746 +++ .../jdk/internal/net/http/Http3Stream.java | 693 +++ .../jdk/internal/net/http/HttpClientImpl.java | 155 +- .../jdk/internal/net/http/HttpConnection.java | 75 +- .../internal/net/http/HttpQuicConnection.java | 690 +++ .../net/http/HttpRequestBuilderImpl.java | 52 + .../internal/net/http/HttpRequestImpl.java | 48 +- .../internal/net/http/HttpResponseImpl.java | 14 +- .../net/http/ImmutableHttpRequest.java | 16 +- .../jdk/internal/net/http/MultiExchange.java | 433 +- .../classes/jdk/internal/net/http/Origin.java | 41 + .../net/http/PlainHttpConnection.java | 68 +- .../jdk/internal/net/http/PushGroup.java | 19 +- .../jdk/internal/net/http/Response.java | 28 +- .../net/http/ResponseSubscribers.java | 12 +- .../classes/jdk/internal/net/http/Stream.java | 41 +- .../jdk/internal/net/http/common/Alpns.java | 5 + .../common/ConnectionExpiredException.java | 4 +- .../internal/net/http/common/Deadline.java | 54 +- .../common/HttpBodySubscriberWrapper.java | 5 + .../net/http/common/HttpHeadersBuilder.java | 11 +- .../jdk/internal/net/http/common/Log.java | 240 +- .../net/http/common/OperationTrackers.java | 2 + .../internal/net/http/common/TimeSource.java | 3 +- .../jdk/internal/net/http/common/Utils.java | 572 ++- .../internal/net/http/frame/AltSvcFrame.java | 77 + .../net/http/frame/FramesDecoder.java | 36 +- .../net/http/frame/FramesEncoder.java | 16 + .../internal/net/http/frame/Http2Frame.java | 3 +- .../jdk/internal/net/http/hpack/Decoder.java | 2 +- .../internal/net/http/hpack/ISO_8859_1.java | 7 +- .../internal/net/http/hpack/QuickHuffman.java | 30 +- .../net/http/http3/ConnectionSettings.java | 63 + .../internal/net/http/http3/Http3Error.java | 308 ++ .../http/http3/frames/AbstractHttp3Frame.java | 118 + .../http/http3/frames/CancelPushFrame.java | 115 + .../net/http/http3/frames/DataFrame.java | 60 + .../net/http/http3/frames/FramesDecoder.java | 331 ++ .../net/http/http3/frames/GoAwayFrame.java | 123 + .../net/http/http3/frames/HeadersFrame.java | 60 + .../net/http/http3/frames/Http3Frame.java | 214 + .../net/http/http3/frames/Http3FrameType.java | 201 + .../net/http/http3/frames/MalformedFrame.java | 124 + .../net/http/http3/frames/MaxPushIdFrame.java | 117 + .../net/http/http3/frames/PartialFrame.java | 154 + .../http/http3/frames/PushPromiseFrame.java | 84 + .../net/http/http3/frames/SettingsFrame.java | 364 ++ .../net/http/http3/frames/UnknownFrame.java | 67 + .../net/http/http3/streams/Http3Streams.java | 117 + .../streams/PeerUniStreamDispatcher.java | 328 ++ .../http/http3/streams/QueuingStreamPair.java | 183 + .../http3/streams/QuicStreamIntReader.java | 192 + .../net/http/http3/streams/UniStreamPair.java | 505 ++ .../jdk/internal/net/http/qpack/Decoder.java | 400 ++ .../net/http/qpack/DecodingCallback.java | 202 + .../internal/net/http/qpack/DynamicTable.java | 1069 ++++ .../jdk/internal/net/http/qpack/Encoder.java | 672 +++ .../net/http/qpack/FieldSectionPrefix.java | 75 + .../internal/net/http/qpack/HeaderField.java | 37 + .../internal/net/http/qpack/HeadersTable.java | 64 + .../net/http/qpack/InsertionPolicy.java | 29 + .../jdk/internal/net/http/qpack/QPACK.java | 229 + .../net/http/qpack/QPackException.java | 68 + .../internal/net/http/qpack/StaticTable.java | 192 + .../internal/net/http/qpack/TableEntry.java | 76 + .../net/http/qpack/TablesIndexer.java | 112 + .../internal/net/http/qpack/package-info.java | 34 + .../readers/DecoderInstructionsReader.java | 159 + .../readers/EncoderInstructionsReader.java | 245 + .../FieldLineIndexedPostBaseReader.java | 83 + .../qpack/readers/FieldLineIndexedReader.java | 86 + .../readers/FieldLineLiteralsReader.java | 119 + .../FieldLineNameRefPostBaseReader.java | 125 + .../readers/FieldLineNameReferenceReader.java | 126 + .../http/qpack/readers/FieldLineReader.java | 128 + .../http/qpack/readers/HeaderFrameReader.java | 414 ++ .../net/http/qpack/readers/IntegerReader.java | 177 + .../net/http/qpack/readers/ReaderError.java | 49 + .../net/http/qpack/readers/StringReader.java | 152 + .../writers/BinaryRepresentationWriter.java | 33 + .../writers/DecoderInstructionsWriter.java | 121 + .../writers/EncoderDuplicateEntryWriter.java | 56 + .../EncoderDynamicTableCapacityWriter.java | 54 + .../EncoderInsertIndexedNameWriter.java | 102 + .../EncoderInsertLiteralNameWriter.java | 101 + .../writers/EncoderInstructionsWriter.java | 190 + .../writers/FieldLineIndexedNameWriter.java | 144 + .../qpack/writers/FieldLineIndexedWriter.java | 105 + .../writers/FieldLineLiteralsWriter.java | 104 + .../writers/FieldLineSectionPrefixWriter.java | 96 + .../http/qpack/writers/HeaderFrameWriter.java | 108 + .../net/http/qpack/writers/IntegerWriter.java | 133 + .../net/http/qpack/writers/StringWriter.java | 139 + .../internal/net/http/quic/BuffersReader.java | 707 +++ .../internal/net/http/quic/CodingContext.java | 169 + .../net/http/quic/ConnectionTerminator.java | 38 + .../http/quic/ConnectionTerminatorImpl.java | 475 ++ .../net/http/quic/IdleTimeoutManager.java | 528 ++ .../net/http/quic/LocalConnIdManager.java | 175 + .../internal/net/http/quic/OrderedFlow.java | 389 ++ .../internal/net/http/quic/PacketEmitter.java | 134 + .../net/http/quic/PacketSpaceManager.java | 2370 +++++++++ .../net/http/quic/PeerConnIdManager.java | 520 ++ .../net/http/quic/PeerConnectionId.java | 92 + .../internal/net/http/quic/QuicClient.java | 585 +++ .../http/quic/QuicCongestionController.java | 75 + .../net/http/quic/QuicConnection.java | 229 + .../net/http/quic/QuicConnectionId.java | 151 + .../http/quic/QuicConnectionIdFactory.java | 354 ++ .../net/http/quic/QuicConnectionImpl.java | 4353 +++++++++++++++++ .../internal/net/http/quic/QuicEndpoint.java | 2062 ++++++++ .../internal/net/http/quic/QuicInstance.java | 150 + .../net/http/quic/QuicPacketReceiver.java | 144 + .../quic/QuicRenoCongestionController.java | 220 + .../net/http/quic/QuicRttEstimator.java | 170 + .../internal/net/http/quic/QuicSelector.java | 536 ++ .../http/quic/QuicStreamLimitException.java | 38 + .../net/http/quic/QuicTimedEvent.java | 160 + .../net/http/quic/QuicTimerQueue.java | 522 ++ .../http/quic/QuicTransportParameters.java | 1319 +++++ .../net/http/quic/TerminationCause.java | 211 + .../net/http/quic/VariableLengthEncoder.java | 341 ++ .../net/http/quic/frames/AckFrame.java | 931 ++++ .../quic/frames/ConnectionCloseFrame.java | 238 + .../net/http/quic/frames/CryptoFrame.java | 153 + .../http/quic/frames/DataBlockedFrame.java | 91 + .../http/quic/frames/HandshakeDoneFrame.java | 71 + .../net/http/quic/frames/MaxDataFrame.java | 91 + .../http/quic/frames/MaxStreamDataFrame.java | 101 + .../net/http/quic/frames/MaxStreamsFrame.java | 108 + .../quic/frames/NewConnectionIDFrame.java | 141 + .../net/http/quic/frames/NewTokenFrame.java | 103 + .../net/http/quic/frames/PaddingFrame.java | 102 + .../http/quic/frames/PathChallengeFrame.java | 89 + .../http/quic/frames/PathResponseFrame.java | 89 + .../net/http/quic/frames/PingFrame.java | 64 + .../net/http/quic/frames/QuicFrame.java | 387 ++ .../http/quic/frames/ResetStreamFrame.java | 109 + .../quic/frames/RetireConnectionIDFrame.java | 91 + .../http/quic/frames/StopSendingFrame.java | 93 + .../quic/frames/StreamDataBlockedFrame.java | 118 + .../net/http/quic/frames/StreamFrame.java | 264 + .../http/quic/frames/StreamsBlockedFrame.java | 107 + .../internal/net/http/quic/package-info.java | 40 + .../http/quic/packets/HandshakePacket.java | 97 + .../net/http/quic/packets/InitialPacket.java | 125 + .../net/http/quic/packets/LongHeader.java | 57 + .../http/quic/packets/LongHeaderPacket.java | 71 + .../net/http/quic/packets/OneRttPacket.java | 125 + .../net/http/quic/packets/PacketSpace.java | 245 + .../net/http/quic/packets/QuicPacket.java | 249 + .../http/quic/packets/QuicPacketDecoder.java | 1748 +++++++ .../http/quic/packets/QuicPacketEncoder.java | 1746 +++++++ .../http/quic/packets/QuicPacketNumbers.java | 197 + .../net/http/quic/packets/RetryPacket.java | 100 + .../http/quic/packets/ShortHeaderPacket.java | 54 + .../packets/VersionNegotiationPacket.java | 76 + .../net/http/quic/packets/ZeroRttPacket.java | 103 + .../http/quic/streams/AbstractQuicStream.java | 118 + .../http/quic/streams/CryptoWriterQueue.java | 213 + .../net/http/quic/streams/QuicBidiStream.java | 135 + .../http/quic/streams/QuicBidiStreamImpl.java | 151 + .../quic/streams/QuicConnectionStreams.java | 1590 ++++++ .../http/quic/streams/QuicReceiverStream.java | 190 + .../quic/streams/QuicReceiverStreamImpl.java | 942 ++++ .../http/quic/streams/QuicSenderStream.java | 197 + .../quic/streams/QuicSenderStreamImpl.java | 662 +++ .../net/http/quic/streams/QuicStream.java | 149 + .../http/quic/streams/QuicStreamReader.java | 138 + .../http/quic/streams/QuicStreamWriter.java | 169 + .../net/http/quic/streams/QuicStreams.java | 90 + .../quic/streams/StreamCreationPermit.java | 317 ++ .../http/quic/streams/StreamWriterQueue.java | 550 +++ .../share/classes/module-info.java | 107 +- .../security/pkcs11/P11SecretKeyFactory.java | 4 + test/jdk/com/sun/net/httpserver/SANTest.java | 12 + .../java/net/httpclient/AbstractNoBody.java | 83 +- .../AbstractThrowingPublishers.java | 134 +- .../AbstractThrowingPushPromises.java | 150 +- .../AbstractThrowingSubscribers.java | 131 +- .../httpclient/AggregateRequestBodyTest.java | 70 +- .../net/httpclient/AltServiceUsageTest.java | 454 ++ .../net/httpclient/AsFileDownloadTest.java | 160 +- .../net/httpclient/AsyncExecutorShutdown.java | 197 +- .../java/net/httpclient/AsyncShutdownNow.java | 122 +- .../net/httpclient/AuthFilterCacheTest.java | 111 +- .../java/net/httpclient/BasicAuthTest.java | 14 +- .../java/net/httpclient/BasicHTTP2Test.java | 320 ++ .../java/net/httpclient/BasicHTTP3Test.java | 482 ++ .../net/httpclient/BasicRedirectTest.java | 173 +- .../net/httpclient/CancelRequestTest.java | 101 +- .../httpclient/CancelStreamedBodyTest.java | 81 +- ...java => CancelledPartialResponseTest.java} | 170 +- .../net/httpclient/CancelledResponse.java | 9 +- .../net/httpclient/CancelledResponse2.java | 88 +- .../net/httpclient/ConcurrentResponses.java | 69 +- .../httpclient/ContentLengthHeaderTest.java | 48 +- .../java/net/httpclient/CookieHeaderTest.java | 23 +- .../httpclient/CustomRequestPublisher.java | 63 +- .../httpclient/CustomResponseSubscriber.java | 2 +- .../net/httpclient/DependentActionsTest.java | 147 +- .../DependentPromiseActionsTest.java | 263 +- .../java/net/httpclient/DigestEchoClient.java | 185 +- .../net/httpclient/DigestEchoClientSSL.java | 10 +- .../java/net/httpclient/DigestEchoServer.java | 100 +- .../net/httpclient/EmptyAuthenticate.java | 14 +- .../net/httpclient/EncodedCharsInURI.java | 65 +- .../net/httpclient/EscapedOctetsInURI.java | 64 +- .../java/net/httpclient/ExecutorShutdown.java | 93 +- .../net/httpclient/ExpectContinueTest.java | 18 +- .../httpclient/FlowAdapterPublisherTest.java | 31 +- .../httpclient/FlowAdapterSubscriberTest.java | 31 +- .../net/httpclient/ForbiddenHeadTest.java | 29 +- .../net/httpclient/GZIPInputStreamTest.java | 156 +- .../net/httpclient/HandshakeFailureTest.java | 1 + test/jdk/java/net/httpclient/HeadTest.java | 87 +- .../net/httpclient/HeadersLowerCaseTest.java | 231 + .../net/httpclient/HttpClientBuilderTest.java | 11 + .../java/net/httpclient/HttpClientClose.java | 130 +- .../net/httpclient/HttpClientShutdown.java | 102 +- .../httpclient/HttpGetInCancelledFuture.java | 105 +- .../java/net/httpclient/HttpRedirectTest.java | 35 +- .../httpclient/HttpRequestBuilderTest.java | 78 +- .../httpclient/HttpRequestNewBuilderTest.java | 30 +- .../HttpResponseConnectionLabelTest.java | 64 +- .../httpclient/HttpResponseLimitingTest.java | 48 +- .../net/httpclient/HttpSlowServerTest.java | 27 +- .../java/net/httpclient/ISO_8859_1_Test.java | 82 +- .../httpclient/IdleConnectionTimeoutTest.java | 360 ++ .../net/httpclient/ImmutableFlowItems.java | 1 - .../httpclient/ImmutableSSLSessionTest.java | 381 ++ ...InvalidInputStreamSubscriptionRequest.java | 206 +- .../InvalidSubscriptionRequest.java | 137 +- .../net/httpclient/LargeHandshakeTest.java | 33 +- .../net/httpclient/LargeResponseTest.java | 34 +- .../net/httpclient/LineBodyHandlerTest.java | 65 +- .../jdk/java/net/httpclient/ManyRequests.java | 19 +- .../java/net/httpclient/ManyRequests2.java | 6 +- .../net/httpclient/ManyRequestsLegacy.java | 20 +- .../httpclient/MappingResponseSubscriber.java | 7 +- .../java/net/httpclient/NoBodyPartOne.java | 10 + .../java/net/httpclient/NoBodyPartThree.java | 17 +- .../java/net/httpclient/NoBodyPartTwo.java | 16 +- .../net/httpclient/NonAsciiCharsInURI.java | 67 +- .../BodyHandlerOfFileDownloadTest.java | 74 +- .../PathSubscriber/BodyHandlerOfFileTest.java | 69 +- .../BodySubscriberOfFileTest.java | 70 +- .../ProxyAuthDisabledSchemesSSL.java | 15 +- test/jdk/java/net/httpclient/ProxyTest.java | 22 +- .../net/httpclient/RedirectMethodChange.java | 50 +- .../net/httpclient/RedirectTimeoutTest.java | 65 +- .../net/httpclient/RedirectWithCookie.java | 38 +- .../java/net/httpclient/ReferenceTracker.java | 29 + .../net/httpclient/RequestBuilderTest.java | 19 +- .../java/net/httpclient/Response1xxTest.java | 219 +- .../net/httpclient/Response204V2Test.java | 54 +- .../httpclient/ResponseBodyBeforeError.java | 2 +- .../net/httpclient/ResponsePublisher.java | 73 +- .../net/httpclient/RestrictedHeadersTest.java | 4 +- .../java/net/httpclient/RetryWithCookie.java | 40 +- test/jdk/java/net/httpclient/ShutdownNow.java | 68 +- test/jdk/java/net/httpclient/SmokeTest.java | 22 +- .../net/httpclient/SpecialHeadersTest.java | 9 +- .../java/net/httpclient/SplitResponse.java | 10 +- .../java/net/httpclient/StreamCloseTest.java | 6 +- .../java/net/httpclient/StreamingBody.java | 37 +- test/jdk/java/net/httpclient/TEST.properties | 17 +- .../jdk/java/net/httpclient/TimeoutBasic.java | 82 +- .../java/net/httpclient/TlsContextTest.java | 49 +- .../java/net/httpclient/UnauthorizedTest.java | 37 +- .../httpclient/UserAuthWithAuthenticator.java | 465 +- .../java/net/httpclient/UserCookieTest.java | 32 +- test/jdk/java/net/httpclient/VersionTest.java | 5 +- .../net/http/Http3ConnectionAccess.java | 64 + .../common/ImmutableSSLSessionAccess.java | 42 + .../altsvc/AltServiceReasonableAssurance.java | 688 +++ .../httpclient/altsvc/altsvc-dns-hosts.txt | 23 + .../net/http/common/TestLoggerUtil.java | 46 + .../httpclient/http2/BadPushPromiseTest.java | 2 +- .../http2/ContinuationFrameTest.java | 24 +- .../java/net/httpclient/http2/ErrorTest.java | 19 +- .../http2/HpackBinaryTestDriver.java | 2 +- .../httpclient/http2/HpackHuffmanDriver.java | 2 +- .../http2/IdleConnectionTimeoutTest.java | 227 - .../http2/IdlePooledConnectionTest.java | 5 +- .../java/net/httpclient/http2/ProxyTest2.java | 25 +- .../http2/PushPromiseContinuation.java | 14 +- .../net/httpclient/http2/RedirectTest.java | 8 +- .../java/net/httpclient/http2/SimpleGet.java | 225 + .../http2/StreamFlowControlTest.java | 2 +- .../httpclient/http2/TrailingHeadersTest.java | 12 +- .../net/httpclient/http2/UserInfoTest.java | 13 +- .../http3/BadCipherSuiteErrorTest.java | 119 + .../httpclient/http3/FramesDecoderTest.java | 227 + .../net/httpclient/http3/GetHTTP3Test.java | 476 ++ .../httpclient/http3/H3BadHeadersTest.java | 330 ++ .../net/httpclient/http3/H3BasicTest.java | 405 ++ .../httpclient/http3/H3ConcurrentPush.java | 471 ++ .../http3/H3ConnectionPoolTest.java | 581 +++ .../httpclient/http3/H3DataLimitsTest.java | 270 + .../httpclient/http3/H3ErrorHandlingTest.java | 1063 ++++ .../http3/H3FixedThreadPoolTest.java | 300 ++ .../net/httpclient/http3/H3GoAwayTest.java | 180 + .../http3/H3HeaderSizeLimitTest.java | 162 + .../httpclient/http3/H3HeadersEncoding.java | 315 ++ .../http3/H3ImplicitPushCancel.java | 258 + .../http3/H3InsertionsLimitTest.java | 179 + .../http3/H3MalformedResponseTest.java | 437 ++ .../http3/H3MaxInitialTimeoutTest.java | 250 + .../http3/H3MemoryHandlingTest.java | 232 + .../H3MultipleConnectionsToSameHost.java | 338 ++ .../net/httpclient/http3/H3ProxyTest.java | 396 ++ .../net/httpclient/http3/H3PushCancel.java | 508 ++ .../httpclient/http3/H3QuicTLSConnection.java | 363 ++ .../net/httpclient/http3/H3RedirectTest.java | 260 + .../net/httpclient/http3/H3ServerPush.java | 396 ++ .../httpclient/http3/H3ServerPushCancel.java | 607 +++ .../httpclient/http3/H3ServerPushTest.java | 1224 +++++ .../http3/H3ServerPushWithDiffTypes.java | 292 ++ .../net/httpclient/http3/H3SimpleGet.java | 315 ++ .../net/httpclient/http3/H3SimplePost.java | 207 + .../net/httpclient/http3/H3SimpleTest.java | 129 + .../httpclient/http3/H3StopSendingTest.java | 259 + .../http3/H3StreamLimitReachedTest.java | 971 ++++ .../java/net/httpclient/http3/H3Timeout.java | 187 + .../http3/H3UnsupportedSSLParametersTest.java | 78 + .../net/httpclient/http3/H3UserInfoTest.java | 193 + .../net/httpclient/http3/HTTP3NoBodyTest.java | 324 ++ .../http3/Http3ExpectContinueTest.java | 250 + .../http3/PeerUniStreamDispatcherTest.java | 436 ++ .../net/httpclient/http3/PostHTTP3Test.java | 522 ++ .../net/httpclient/http3/StopSendingTest.java | 215 + .../net/httpclient/http3/StreamLimitTest.java | 266 + .../test/lib/common/DynamicKeyStoreUtil.java | 266 + .../test/lib/common/HttpServerAdapters.java | 880 +++- .../lib/common/RequestPathMatcherUtil.java | 68 + .../lib/common/TestServerConfigurator.java | 19 +- .../httpclient/test/lib/common/TestUtil.java | 58 + .../test/lib/http2/BodyOutputStream.java | 48 +- .../test/lib/http2/EchoHandler.java | 7 +- .../test/lib/http2/Http2EchoHandler.java | 34 +- .../test/lib/http2/Http2Handler.java | 3 +- .../test/lib/http2/Http2RedirectHandler.java | 9 +- .../test/lib/http2/Http2TestExchange.java | 170 +- .../test/lib/http2/Http2TestExchangeImpl.java | 25 +- .../test/lib/http2/Http2TestServer.java | 259 +- .../lib/http2/Http2TestServerConnection.java | 261 +- .../test/lib/http2/OutgoingPushPromise.java | 16 +- .../test/lib/http3/Http3ServerConnection.java | 801 +++ .../test/lib/http3/Http3ServerExchange.java | 801 +++ .../test/lib/http3/Http3ServerStreamImpl.java | 489 ++ .../test/lib/http3/Http3TestServer.java | 371 ++ .../lib/http3/UnknownOrReservedFrame.java | 94 + .../test/lib/quic/ClientConnection.java | 134 + .../test/lib/quic/ConnectedBidiStream.java | 129 + .../test/lib/quic/DatagramDeliveryPolicy.java | 315 ++ .../httpclient/test/lib/quic/OutStream.java | 75 + .../test/lib/quic/QueueInputStream.java | 164 + .../httpclient/test/lib/quic/QuicServer.java | 913 ++++ .../test/lib/quic/QuicServerConnection.java | 570 +++ .../test/lib/quic/QuicServerHandler.java | 87 + .../test/lib/quic/QuicStandaloneServer.java | 185 + .../test/lib/quic/RetryCodingContext.java | 82 + .../qpack/BlockingDecodingTest.java | 374 ++ .../qpack/DecoderInstructionsReaderTest.java | 165 + .../qpack/DecoderInstructionsWriterTest.java | 177 + .../qpack/DecoderSectionSizeLimitTest.java | 265 + .../net/httpclient/qpack/DecoderTest.java | 392 ++ ...namicTableFieldLineRepresentationTest.java | 404 ++ .../httpclient/qpack/DynamicTableTest.java | 366 ++ .../qpack/EncoderDecoderConnectionTest.java | 241 + .../qpack/EncoderDecoderConnector.java | 509 ++ .../httpclient/qpack/EncoderDecoderTest.java | 442 ++ .../qpack/EncoderInstructionsReaderTest.java | 373 ++ .../qpack/EncoderInstructionsWriterTest.java | 343 ++ .../net/httpclient/qpack/EncoderTest.java | 670 +++ .../httpclient/qpack/EntriesEvictionTest.java | 190 + .../qpack/FieldSectionPrefixTest.java | 133 + .../qpack/IntegerReaderMaxValuesTest.java | 84 + .../qpack/StaticTableFieldsTest.java | 182 + .../qpack/StringLengthLimitsTest.java | 482 ++ .../httpclient/qpack/TablesIndexerTest.java | 196 + .../qpack/UnacknowledgedInsertionTest.java | 243 + .../net/httpclient/quic/AckElicitingTest.java | 729 +++ .../net/httpclient/quic/AckFrameTest.java | 387 ++ .../httpclient/quic/BuffersReaderTest.java | 520 ++ .../httpclient/quic/BuffersReaderVLTest.java | 325 ++ .../httpclient/quic/ConnectionIDSTest.java | 170 + .../quic/CryptoWriterQueueTest.java | 73 + .../net/httpclient/quic/KeyUpdateTest.java | 272 + .../net/httpclient/quic/OrderedFlowTest.java | 401 ++ .../httpclient/quic/PacketEncodingTest.java | 1440 ++++++ .../net/httpclient/quic/PacketLossTest.java | 253 + .../httpclient/quic/PacketNumbersTest.java | 159 + .../quic/PacketSpaceManagerTest.java | 1094 +++++ .../quic/QuicFramesDecoderTest.java | 298 ++ .../quic/QuicRequestResponseTest.java | 172 + .../quic/StatelessResetReceiptTest.java | 303 ++ .../httpclient/quic/VariableLengthTest.java | 348 ++ .../quic/VersionNegotiationTest.java | 146 + .../quic/quic-tls-keylimits-java.security | 4 + .../quic/tls/PacketEncryptionTest.java | 457 ++ .../tls/QuicTLSEngineBadParametersTest.java | 122 + .../quic/tls/QuicTLSEngineFailedALPNTest.java | 110 + .../QuicTLSEngineMissingParametersTest.java | 112 + .../quic/tls/Quicv2PacketEncryptionTest.java | 451 ++ .../ssl/QuicTLSEngineImplAccessor.java | 59 + .../httpclient/ssltest/CertificateTest.java | 7 +- .../java/net/httpclient/ssltest/Server.java | 10 +- .../httpclient/ssltest/TlsVersionTest.java | 14 +- .../websocket/HandshakeUrlEncodingTest.java | 1 - .../httpclient/websocket/ReaderDriver.java | 2 +- .../httpclient/whitebox/AltSvcFrameTest.java | 239 + .../whitebox/AltSvcRegistryTest.java | 161 + .../internal/net/http/HttpClientAccess.java | 10 + .../quic/packets/QuicPacketNumbersTest.java | 137 + 473 files changed, 104314 insertions(+), 2646 deletions(-) create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java create mode 100644 src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java create mode 100644 src/java.base/share/classes/sun/security/ssl/QuicCipher.java create mode 100644 src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java create mode 100644 src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java create mode 100644 src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java create mode 100644 src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java create mode 100644 src/java.net.http/share/classes/java/net/http/HttpOption.java create mode 100644 src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java create mode 100644 src/java.net.http/share/classes/java/net/http/StreamLimitException.java create mode 100644 src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathChallengeFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java create mode 100644 src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java create mode 100644 test/jdk/java/net/httpclient/AltServiceUsageTest.java create mode 100644 test/jdk/java/net/httpclient/BasicHTTP2Test.java create mode 100644 test/jdk/java/net/httpclient/BasicHTTP3Test.java rename test/jdk/java/net/httpclient/{http2/ExpectContinueResetTest.java => CancelledPartialResponseTest.java} (52%) create mode 100644 test/jdk/java/net/httpclient/HeadersLowerCaseTest.java create mode 100644 test/jdk/java/net/httpclient/IdleConnectionTimeoutTest.java create mode 100644 test/jdk/java/net/httpclient/ImmutableSSLSessionTest.java create mode 100644 test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/Http3ConnectionAccess.java create mode 100644 test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/common/ImmutableSSLSessionAccess.java create mode 100644 test/jdk/java/net/httpclient/altsvc/AltServiceReasonableAssurance.java create mode 100644 test/jdk/java/net/httpclient/altsvc/altsvc-dns-hosts.txt create mode 100644 test/jdk/java/net/httpclient/debug/java.net.http/jdk/internal/net/http/common/TestLoggerUtil.java delete mode 100644 test/jdk/java/net/httpclient/http2/IdleConnectionTimeoutTest.java create mode 100644 test/jdk/java/net/httpclient/http2/SimpleGet.java create mode 100644 test/jdk/java/net/httpclient/http3/BadCipherSuiteErrorTest.java create mode 100644 test/jdk/java/net/httpclient/http3/FramesDecoderTest.java create mode 100644 test/jdk/java/net/httpclient/http3/GetHTTP3Test.java create mode 100644 test/jdk/java/net/httpclient/http3/H3BadHeadersTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3BasicTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ConcurrentPush.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ConnectionPoolTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3DataLimitsTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3FixedThreadPoolTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3GoAwayTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3HeaderSizeLimitTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ImplicitPushCancel.java create mode 100644 test/jdk/java/net/httpclient/http3/H3InsertionsLimitTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3MaxInitialTimeoutTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3MemoryHandlingTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ProxyTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3PushCancel.java create mode 100644 test/jdk/java/net/httpclient/http3/H3QuicTLSConnection.java create mode 100644 test/jdk/java/net/httpclient/http3/H3RedirectTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ServerPush.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ServerPushCancel.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ServerPushTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3ServerPushWithDiffTypes.java create mode 100644 test/jdk/java/net/httpclient/http3/H3SimpleGet.java create mode 100644 test/jdk/java/net/httpclient/http3/H3SimplePost.java create mode 100644 test/jdk/java/net/httpclient/http3/H3SimpleTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3StopSendingTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3StreamLimitReachedTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3Timeout.java create mode 100644 test/jdk/java/net/httpclient/http3/H3UnsupportedSSLParametersTest.java create mode 100644 test/jdk/java/net/httpclient/http3/H3UserInfoTest.java create mode 100644 test/jdk/java/net/httpclient/http3/HTTP3NoBodyTest.java create mode 100644 test/jdk/java/net/httpclient/http3/Http3ExpectContinueTest.java create mode 100644 test/jdk/java/net/httpclient/http3/PeerUniStreamDispatcherTest.java create mode 100644 test/jdk/java/net/httpclient/http3/PostHTTP3Test.java create mode 100644 test/jdk/java/net/httpclient/http3/StopSendingTest.java create mode 100644 test/jdk/java/net/httpclient/http3/StreamLimitTest.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/DynamicKeyStoreUtil.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/RequestPathMatcherUtil.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestUtil.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerConnection.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerExchange.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerStreamImpl.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3TestServer.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/UnknownOrReservedFrame.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ClientConnection.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ConnectedBidiStream.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/DatagramDeliveryPolicy.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/OutStream.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QueueInputStream.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServer.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerConnection.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerHandler.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicStandaloneServer.java create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/RetryCodingContext.java create mode 100644 test/jdk/java/net/httpclient/qpack/BlockingDecodingTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/DecoderInstructionsReaderTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/DecoderInstructionsWriterTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/DecoderSectionSizeLimitTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/DecoderTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/DynamicTableFieldLineRepresentationTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/DynamicTableTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/EncoderDecoderConnectionTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/EncoderDecoderConnector.java create mode 100644 test/jdk/java/net/httpclient/qpack/EncoderDecoderTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/EncoderInstructionsReaderTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/EncoderInstructionsWriterTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/EncoderTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/EntriesEvictionTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/FieldSectionPrefixTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/IntegerReaderMaxValuesTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/StaticTableFieldsTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/StringLengthLimitsTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/TablesIndexerTest.java create mode 100644 test/jdk/java/net/httpclient/qpack/UnacknowledgedInsertionTest.java create mode 100644 test/jdk/java/net/httpclient/quic/AckElicitingTest.java create mode 100644 test/jdk/java/net/httpclient/quic/AckFrameTest.java create mode 100644 test/jdk/java/net/httpclient/quic/BuffersReaderTest.java create mode 100644 test/jdk/java/net/httpclient/quic/BuffersReaderVLTest.java create mode 100644 test/jdk/java/net/httpclient/quic/ConnectionIDSTest.java create mode 100644 test/jdk/java/net/httpclient/quic/CryptoWriterQueueTest.java create mode 100644 test/jdk/java/net/httpclient/quic/KeyUpdateTest.java create mode 100644 test/jdk/java/net/httpclient/quic/OrderedFlowTest.java create mode 100644 test/jdk/java/net/httpclient/quic/PacketEncodingTest.java create mode 100644 test/jdk/java/net/httpclient/quic/PacketLossTest.java create mode 100644 test/jdk/java/net/httpclient/quic/PacketNumbersTest.java create mode 100644 test/jdk/java/net/httpclient/quic/PacketSpaceManagerTest.java create mode 100644 test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java create mode 100644 test/jdk/java/net/httpclient/quic/QuicRequestResponseTest.java create mode 100644 test/jdk/java/net/httpclient/quic/StatelessResetReceiptTest.java create mode 100644 test/jdk/java/net/httpclient/quic/VariableLengthTest.java create mode 100644 test/jdk/java/net/httpclient/quic/VersionNegotiationTest.java create mode 100644 test/jdk/java/net/httpclient/quic/quic-tls-keylimits-java.security create mode 100644 test/jdk/java/net/httpclient/quic/tls/PacketEncryptionTest.java create mode 100644 test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineBadParametersTest.java create mode 100644 test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineFailedALPNTest.java create mode 100644 test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineMissingParametersTest.java create mode 100644 test/jdk/java/net/httpclient/quic/tls/Quicv2PacketEncryptionTest.java create mode 100644 test/jdk/java/net/httpclient/quic/tls/java.base/sun/security/ssl/QuicTLSEngineImplAccessor.java create mode 100644 test/jdk/java/net/httpclient/whitebox/AltSvcFrameTest.java create mode 100644 test/jdk/java/net/httpclient/whitebox/AltSvcRegistryTest.java create mode 100644 test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/HttpClientAccess.java create mode 100644 test/jdk/jdk/internal/net/http/quic/packets/QuicPacketNumbersTest.java diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java new file mode 100644 index 00000000000..89d15eb3439 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicKeyUnavailableException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + + +import java.util.Objects; + +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; + +/** + * Thrown when an operation on {@link QuicTLSEngine} doesn't have the necessary + * QUIC keys for encrypting or decrypting packets. This can either be because + * the keys aren't available for a particular {@linkplain KeySpace keyspace} or + * the keys for the {@code keyspace} have been discarded. + */ +public final class QuicKeyUnavailableException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 8553365136999153478L; + + public QuicKeyUnavailableException(final String message, final KeySpace keySpace) { + super(Objects.requireNonNull(keySpace) + " keyspace: " + message); + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java new file mode 100644 index 00000000000..fd0b405069c --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicOneRttContext.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +/** + * Supplies contextual 1-RTT information that's available in the QUIC implementation of the + * {@code java.net.http} module, to the QUIC TLS layer in the {@code java.base} module. + */ +public interface QuicOneRttContext { + + /** + * {@return the largest packet number that was acknowledged by + * the peer in the 1-RTT packet space} + */ + long getLargestPeerAckedPN(); +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java new file mode 100644 index 00000000000..5cf0c999fb9 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSContext.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.Objects; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLContextSpi; +import javax.net.ssl.SSLParameters; + +import sun.security.ssl.QuicTLSEngineImpl; +import sun.security.ssl.SSLContextImpl; + +/** + * Instances of this class act as a factory for creation + * of {@link QuicTLSEngine QUIC TLS engine}. + */ +public final class QuicTLSContext { + + // In this implementation, we have a dependency on + // sun.security.ssl.SSLContextImpl. We can only support + // Quic on SSLContext instances created by the default + // SunJSSE Provider + private final SSLContextImpl sslCtxImpl; + + /** + * {@return {@code true} if the given {@code sslContext} supports QUIC TLS, {@code false} otherwise} + * @param sslContext an {@link SSLContext} + */ + public static boolean isQuicCompatible(final SSLContext sslContext) { + boolean parametersSupported = isQuicCompatible(sslContext.getSupportedSSLParameters()); + if (!parametersSupported) { + return false; + } + // horrible hack - what we do here is try and get hold of a SSLContext + // that has already been initialised and configured with the HttpClient. + // We see if that SSLContext is created using an implementation of + // sun.security.ssl.SSLContextImpl. Since there's no API + // available to get hold of that underlying implementation, we use + // MethodHandle lookup to get access to the field which holds that + // detail. + final Object underlyingImpl = CONTEXT_SPI.get(sslContext); + if (!(underlyingImpl instanceof SSLContextImpl ssci)) { + return false; + } + return ssci.isUsableWithQuic(); + } + + /** + * {@return {@code true} if protocols of the given {@code parameters} support QUIC TLS, {@code false} otherwise} + */ + public static boolean isQuicCompatible(SSLParameters parameters) { + String[] protocols = parameters.getProtocols(); + return protocols != null && Arrays.asList(protocols).contains("TLSv1.3"); + } + + private static SSLContextImpl getSSLContextImpl( + final SSLContext sslContext) { + final Object underlyingImpl = CONTEXT_SPI.get(sslContext); + assert underlyingImpl instanceof SSLContextImpl; + return (SSLContextImpl) underlyingImpl; + } + + /** + * Constructs a QuicTLSContext for the given {@code sslContext} + * + * @param sslContext The SSLContext + * @throws IllegalArgumentException If the passed {@code sslContext} isn't + * supported by the QuicTLSContext + * @see #isQuicCompatible(SSLContext) + */ + public QuicTLSContext(final SSLContext sslContext) { + Objects.requireNonNull(sslContext); + if (!isQuicCompatible(sslContext)) { + throw new IllegalArgumentException( + "Cannot construct a QUIC TLS context with the given SSLContext"); + } + this.sslCtxImpl = getSSLContextImpl(sslContext); + } + + /** + * Creates a {@link QuicTLSEngine} using this context + *

        + * This method does not provide hints for session caching. + * + * @return the newly created QuicTLSEngine + */ + public QuicTLSEngine createEngine() { + return createEngine(null, -1); + } + + /** + * Creates a {@link QuicTLSEngine} using this context using + * advisory peer information. + *

        + * The provided parameters will be used as hints for session caching. + * The {@code peerHost} parameter will be used in the server_name extension, + * unless overridden later. + * + * @param peerHost The peer hostname or IP address. Can be null. + * @param peerPort The peer port, can be -1 if the port is unknown + * @return the newly created QuicTLSEngine + */ + public QuicTLSEngine createEngine(final String peerHost, final int peerPort) { + return new QuicTLSEngineImpl(this.sslCtxImpl, peerHost, peerPort); + } + + // This VarHandle is used to access the SSLContext::contextSpi + // field which is not publicly accessible. + // In this implementation, Quic is only supported for SSLContext + // instances whose underlying implementation is provided by a + // sun.security.ssl.SSLContextImpl + private static final VarHandle CONTEXT_SPI; + static { + try { + final MethodHandles.Lookup lookup = + MethodHandles.privateLookupIn(SSLContext.class, + MethodHandles.lookup()); + final VarHandle vh = lookup.findVarHandle(SSLContext.class, + "contextSpi", SSLContextSpi.class); + CONTEXT_SPI = vh; + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } +} + diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java new file mode 100644 index 00000000000..70ed86bbf01 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTLSEngine.java @@ -0,0 +1,508 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import javax.crypto.AEADBadTagException; +import javax.crypto.ShortBufferException; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.function.IntFunction; + +/** + * One instance of these per QUIC connection. Configuration methods not shown + * but would be similar to SSLEngine. + */ +public interface QuicTLSEngine { + + /** + * Represents the encryption level associated with a packet encryption or + * decryption. A QUIC connection has a current keyspace for sending and + * receiving which can be queried. + */ + enum KeySpace { + INITIAL, + HANDSHAKE, + RETRY, // Special algorithm used for this packet + ZERO_RTT, + ONE_RTT + } + + enum HandshakeState { + /** + * Need to receive a CRYPTO frame + */ + NEED_RECV_CRYPTO, + /** + * Need to receive a HANDSHAKE_DONE frame from server to complete the + * handshake, but application data can be sent in this state (client + * only state). + */ + NEED_RECV_HANDSHAKE_DONE, + /** + * Need to send a CRYPTO frame + */ + NEED_SEND_CRYPTO, + /** + * Need to send a HANDSHAKE_DONE frame to complete the handshake, but + * application data can be sent in this state (server only state) + */ + NEED_SEND_HANDSHAKE_DONE, + /** + * Need to execute a task + */ + NEED_TASK, + /** + * Handshake is confirmed, as specified in section 4.1.2 of RFC-9001 + */ + // On client side this happens when client receives HANDSHAKE_DONE + // frame. On server side this happens when the TLS stack has both + // sent a Finished message and verified the peer's Finished message. + HANDSHAKE_CONFIRMED, + } + + /** + * {@return the QUIC versions supported by this engine} + */ + Set getSupportedQuicVersions(); + + /** + * If {@code mode} is {@code true} then configures this QuicTLSEngine to + * operate in client mode. If {@code false}, then this QuicTLSEngine + * operates in server mode. + * + * @param mode true to make this QuicTLSEngine operate in client + * mode, false otherwise + */ + void setUseClientMode(boolean mode); + + /** + * {@return true if this QuicTLSEngine is operating in client mode, false + * otherwise} + */ + boolean getUseClientMode(); + + /** + * {@return the SSLParameters in effect for this engine.} + */ + SSLParameters getSSLParameters(); + + /** + * Sets the {@code SSLParameters} to be used by this engine + * + * @param sslParameters the SSLParameters + * @throws IllegalArgumentException if + * {@linkplain SSLParameters#getProtocols() TLS protocol versions} on the + * {@code sslParameters} is either empty or contains anything other + * than {@code TLSv1.3} + * @throws NullPointerException if {@code sslParameters} is null + */ + void setSSLParameters(SSLParameters sslParameters); + + /** + * {@return the most recent application protocol value negotiated by the + * engine. Returns null if no application protocol has yet been negotiated + * by the engine} + */ + String getApplicationProtocol(); + + /** + * {@return the SSLSession} + * + * @see SSLEngine#getSession() + */ + SSLSession getSession(); + + /** + * Returns the SSLSession being constructed during a QUIC handshake. + * + * @return null if this instance is not currently handshaking, or if the + * current handshake has not progressed far enough to create + * a basic SSLSession. Otherwise, this method returns the + * {@code SSLSession} currently being negotiated. + * + * @see SSLEngine#getHandshakeSession() + */ + SSLSession getHandshakeSession(); + + /** + * Returns the current handshake state of the connection. Sometimes packets + * that could be decrypted can be received before the handshake has + * completed, but should not be decrypted until it is complete + * + * @return the HandshakeState + */ + HandshakeState getHandshakeState(); + + /** + * Returns true if the TLS handshake is considered complete. + *

        + * The TLS handshake is considered complete when the TLS stack + * has reported that the handshake is complete. This happens when + * the TLS stack has both sent a {@code Finished} message and verified + * the peer's {@code Finished} message. + * + * @return true if TLS handshake is complete, false otherwise. + */ + boolean isTLSHandshakeComplete(); + + /** + * {@return the current sending key space (encryption level)} + */ + KeySpace getCurrentSendKeySpace(); + + /** + * Checks whether the keys for the given key space are available. + *

        + * Keys are available when they are already computed and not discarded yet. + * + * @param keySpace key space to check + * @return true if the given keys are available + */ + boolean keysAvailable(KeySpace keySpace); + + /** + * Discard the keys used by the {@code keySpace}. + *

        + * Once the keys for a particular {@code keySpace} have been discarded, the + * keySpace will no longer be able to + * {@linkplain #encryptPacket(KeySpace, long, IntFunction, + * ByteBuffer, ByteBuffer) encrypt} or + * {@linkplain #decryptPacket(KeySpace, long, int, ByteBuffer, int, ByteBuffer) + * decrypt} packets. + * + * @param keySpace The keyspace whose current keys should be discarded + */ + void discardKeys(KeySpace keySpace); + + /** + * Provide quic_transport_parameters for inclusion in handshake message. + * + * @param params encoded quic_transport_parameters + */ + void setLocalQuicTransportParameters(ByteBuffer params); + + /** + * Reset the handshake state and produce a new ClientHello message. + * + * When a Quic client receives a Version Negotiation packet, + * it restarts the handshake by calling this method after updating the + * {@linkplain #setLocalQuicTransportParameters(ByteBuffer) transport parameters} + * with the new version information. + */ + void restartHandshake() throws IOException; + + /** + * Set consumer for quic_transport_parameters sent by the remote side. + * Consumer will receive a byte buffer containing the value of + * quic_transport_parameters extension sent by the remote endpoint. + * + * @param consumer consumer for remote quic transport parameters + */ + void setRemoteQuicTransportParametersConsumer( + QuicTransportParametersConsumer consumer); + + /** + * Derive initial keys for the given QUIC version and connection ID + * @param quicVersion QUIC protocol version + * @param connectionId initial destination connection ID + * @throws IllegalArgumentException if the {@code quicVersion} isn't + * {@linkplain #getSupportedQuicVersions() supported} on this + * {@code QuicTLSEngine} + */ + void deriveInitialKeys(QuicVersion quicVersion, ByteBuffer connectionId) throws IOException; + + /** + * Get the sample size for header protection algorithm + * + * @param keySpace Packet key space + * @return required sample size for header protection + * @throws IllegalArgumentException when keySpace does not require + * header protection + */ + int getHeaderProtectionSampleSize(KeySpace keySpace); + + /** + * Compute the header protection mask for the given sample, + * packet key space and direction (incoming/outgoing). + * + * @param keySpace Packet key space + * @param incoming true for incoming packets, false for outgoing + * @param sample sampled data + * @return mask bytes, at least 5. + * @throws IllegalArgumentException when keySpace does not require + * header protection or sample length is different from required + * @see #getHeaderProtectionSampleSize(KeySpace) + * @spec https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati + * RFC 9001, Section 5.4.1 Header Protection Application + */ + ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, + boolean incoming, ByteBuffer sample) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Get the authentication tag size. Encryption adds this number of bytes. + * + * @return authentication tag size + */ + int getAuthTagSize(); + + /** + * Encrypt into {@code output}, the given {@code packetPayload} bytes using the + * keys for the given {@code keySpace}. + *

        + * Before encrypting the {@code packetPayload}, this method invokes the {@code headerGenerator} + * passing it the key phase corresponding to the encryption key that's in use. + * For {@code KeySpace}s where key phase isn't applicable, the {@code headerGenerator} will + * be invoked with a value of {@code 0} for the key phase. + *

        + * The {@code headerGenerator} is expected to return a {@code ByteBuffer} representing the + * packet header and where applicable, the returned header must contain the key phase + * that was passed to the {@code headerGenerator}. The packet header will be used as + * the Additional Authentication Data (AAD) for encrypting the {@code packetPayload}. + *

        + * Upon return, the {@code output} will contain the encrypted packet payload bytes + * and the authentication tag. The {@code packetPayload} and the packet header, returned + * by the {@code headerGenerator}, will have their {@code position} equal to their + * {@code limit}. The limit of either of those buffers will not have changed. + *

        + * It is recommended to do the encryption in place by using slices of a bigger + * buffer as the input and output buffer: + *

        +     *          +--------+-------------------+
        +     * input:   | header | plaintext payload |
        +     *          +--------+-------------------+----------+
        +     * output:           | encrypted payload | AEAD tag |
        +     *                   +-------------------+----------+
        +     * 
        + * + * @param keySpace Packet key space + * @param packetNumber full packet number + * @param headerGenerator an {@link IntFunction} which takes a key phase and returns + * the packet header + * @param packetPayload buffer containing unencrypted packet payload + * @param output buffer into which the encrypted packet payload will be written + * @throws QuicKeyUnavailableException if keys are not available + * @throws QuicTransportException if encrypting the packet would result + * in exceeding the AEAD cipher confidentiality limit + */ + void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, + ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException; + + /** + * Decrypt the given packet bytes using keys for the given packet key space. + * Header protection must be removed before calling this method. + *

        + * The input buffer contains the packet header and the encrypted packet payload. + * The packet header (first {@code headerLength} bytes of the input buffer) + * is consumed by this method, but is not decrypted. + * The packet payload (bytes following the packet header) is decrypted + * by this method. This method consumes the entire input buffer. + *

        + * The decrypted payload bytes are written + * to the output buffer. + *

        + * It is recommended to do the decryption in place by using slices of a bigger + * buffer as the input and output buffer: + *

        +     *          +--------+-------------------+----------+
        +     * input:   | header | encrypted payload | AEAD tag |
        +     *          +--------+-------------------+----------+
        +     * output:           | decrypted payload |
        +     *                   +-------------------+
        +     * 
        + * + * @param keySpace Packet key space + * @param packetNumber full packet number + * @param keyPhase key phase bit (0 or 1) found on the packet, or -1 + * if the packet does not have a key phase bit + * @param packet buffer containing encrypted packet bytes + * @param headerLength length of the packet header + * @param output buffer where decrypted packet bytes will be stored + * @throws IllegalArgumentException if keyPhase bit is invalid + * @throws QuicKeyUnavailableException if keys are not available + * @throws AEADBadTagException if the provided packet's authentication tag + * is incorrect + * @throws QuicTransportException if decrypting the invalid packet resulted + * in exceeding the AEAD cipher integrity limit + */ + void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) + throws IllegalArgumentException, QuicKeyUnavailableException, + AEADBadTagException, QuicTransportException, ShortBufferException; + + /** + * Sign the provided retry packet. Input buffer contains the retry packet + * payload. Integrity tag is stored in the output buffer. + * + * @param version Quic version + * @param originalConnectionId original destination connection ID, + * without length + * @param packet retry packet bytes without tag + * @param output buffer where integrity tag will be stored + * @throws ShortBufferException if output buffer is too short to + * hold the tag + * @throws IllegalArgumentException if originalConnectionId is + * longer than 255 bytes + * @throws IllegalArgumentException if {@code version} isn't + * {@linkplain #getSupportedQuicVersions() supported} + */ + void signRetryPacket(QuicVersion version, ByteBuffer originalConnectionId, + ByteBuffer packet, ByteBuffer output) throws ShortBufferException, QuicTransportException; + + /** + * Verify the provided retry packet. + * + * @param version Quic version + * @param originalConnectionId original destination connection ID, + * without length + * @param packet retry packet bytes with tag + * @throws AEADBadTagException if integrity tag is invalid + * @throws IllegalArgumentException if originalConnectionId is + * longer than 255 bytes + * @throws IllegalArgumentException if {@code version} isn't + * {@linkplain #getSupportedQuicVersions() supported} + */ + void verifyRetryPacket(QuicVersion version, ByteBuffer originalConnectionId, + ByteBuffer packet) throws AEADBadTagException, QuicTransportException; + + /** + * If the current handshake state is {@link HandshakeState#NEED_SEND_CRYPTO} + * meaning that a CRYPTO frame needs to be sent then this method is called + * to obtain the contents of the frame. Current handshake state + * can be obtained from {@link #getHandshakeState()}, and the current + * key space can be obtained with {@link #getCurrentSendKeySpace()} + * The bytes returned by this call are used to build a CRYPTO frame. + * + * @param keySpace the key space of the packet in which the + * requested data will be placed + * @return buffer containing data that will be put by caller in a CRYPTO + * frame, or null if there are no more handshake bytes to send in + * this key space at this time. + */ + ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException; + + /** + * This method consumes crypto stream. + * + * @param keySpace the key space of the packet in which the provided + * crypto data was encountered. + * @param payload contents of the next CRYPTO frame + * @throws IllegalArgumentException if keySpace is ZERORTT or + * payload is empty + * @throws QuicTransportException if the handshake failed + */ + void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) + throws QuicTransportException; + + /** + * Returns a delegated {@code Runnable} task for + * this {@code QuicTLSEngine}. + *

        + * {@code QuicTLSEngine} operations may require the results of + * operations that block, or may take an extended period of time to + * complete. This method is used to obtain an outstanding {@link + * java.lang.Runnable} operation (task). Each task must be assigned + * a thread (possibly the current) to perform the {@link + * java.lang.Runnable#run() run} operation. Once the + * {@code run} method returns, the {@code Runnable} object + * is no longer needed and may be discarded. + *

        + * A call to this method will return each outstanding task + * exactly once. + *

        + * Multiple delegated tasks can be run in parallel. + * + * @return a delegated {@code Runnable} task, or null + * if none are available. + */ + Runnable getDelegatedTask(); + + /** + * Called to check if a {@code HANDSHAKE_DONE} frame needs to be sent by the + * server. This method will only be called for a {@code QuicTLSEngine} which + * is in {@linkplain #getUseClientMode() server mode}. If the current TLS handshake + * state is + * {@link HandshakeState#NEED_SEND_HANDSHAKE_DONE + * NEED_SEND_HANDSHAKE_DONE} then this method returns {@code true} and + * advances the TLS handshake state to + * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else + * returns {@code false}. + * + * @return true if handshake state was {@code NEED_SEND_HANDSHAKE_DONE}, + * false otherwise + * @throws IllegalStateException If this {@code QuicTLSEngine} is + * not in server mode + */ + boolean tryMarkHandshakeDone() throws IllegalStateException; + + /** + * Called when HANDSHAKE_DONE message is received from the server. This + * method will only be called for a {@code QuicTLSEngine} which is in + * {@linkplain #getUseClientMode() client mode}. If the current TLS handshake state + * is + * {@link HandshakeState#NEED_RECV_HANDSHAKE_DONE + * NEED_RECV_HANDSHAKE_DONE} then this method returns {@code true} and + * advances the TLS handshake state to + * {@link HandshakeState#HANDSHAKE_CONFIRMED HANDSHAKE_CONFIRMED}. Else + * returns {@code false}. + * + * @return true if handshake state was {@code NEED_RECV_HANDSHAKE_DONE}, + * false otherwise + * @throws IllegalStateException if this {@code QuicTLSEngine} is + * not in client mode + */ + boolean tryReceiveHandshakeDone() throws IllegalStateException; + + /** + * Called when the client and the server, during the connection creation + * handshake, have settled on a Quic version to use for the connection. This + * can happen either due to an explicit version negotiation (as outlined in + * Quic RFC) or the server accepting the Quic version that the client chose + * in its first INITIAL packet. In either of those cases, this method will + * be called. + * + * @param quicVersion the negotiated {@code QuicVersion} + * @throws IllegalArgumentException if the {@code quicVersion} isn't + * {@linkplain #getSupportedQuicVersions() supported} on this engine + */ + void versionNegotiated(QuicVersion quicVersion); + + /** + * Sets the {@link QuicOneRttContext} on the {@code QuicTLSEngine}. + *

        The {@code ctx} will be used by the {@code QuicTLSEngine} to access contextual 1-RTT + * data that might be required for the TLS operations. + * + * @param ctx the 1-RTT context to set + * @throws NullPointerException if {@code ctx} is null + */ + void setOneRttContext(QuicOneRttContext ctx); +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java new file mode 100644 index 00000000000..d081458d40e --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportErrors.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import sun.security.ssl.Alert; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * An enum to model Quic transport errors. + * Some errors have a single possible code value, some, like + * {@link #CRYPTO_ERROR} have a range of possible values. + * Usually, the value (a long) would be used instead of the + * enum, but the enum itself can be useful - for instance in + * switch statements. + * This enum models QUIC transport error codes as defined in + * RFC 9000, section 20.1. + */ +public enum QuicTransportErrors { + /** + * No error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint uses this with CONNECTION_CLOSE to signal that
        +     * the connection is being closed abruptly in the absence
        +     * of any error.
        +     * }
        + */ + NO_ERROR(0x00), + + /** + * Internal Error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * The endpoint encountered an internal error and cannot
        +     * continue with the connection.
        +     * }
        + */ + INTERNAL_ERROR(0x01), + + /** + * Connection refused error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * The server refused to accept a new connection.
        +     * }
        + */ + CONNECTION_REFUSED(0x02), + + /** + * Flow control error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint received more data than it permitted in its advertised data limits;
        +     * see Section 4.
        +     * }
        + * @see + * RFC 9000, Section 20.1: + *
        {@code
        +     * An endpoint received a frame for a stream identifier that exceeded its advertised
        +     * stream limit for the corresponding stream type.
        +     * }
        + */ + STREAM_LIMIT_ERROR(0x04), + + /** + * Stream state error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint received a frame for a stream that was not in a state that permitted
        +     * that frame; see Section 3.
        +     * }
        + * @see + * RFC 9000, Section 20.1: + *
        {@code
        +     * (1) An endpoint received a STREAM frame containing data that exceeded the previously
        +     *     established final size,
        +     * (2) an endpoint received a STREAM frame or a RESET_STREAM frame containing a final
        +     *     size that was lower than the size of stream data that was already received, or
        +     * (3) an endpoint received a STREAM frame or a RESET_STREAM frame containing a
        +     *     different final size to the one already established.
        +     * }
        + */ + FINAL_SIZE_ERROR(0x06), + + /** + * Frame encoding error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint received a frame that was badly formatted -- for instance,
        +     * a frame of an unknown type or an ACK frame that has more
        +     * acknowledgment ranges than the remainder of the packet could carry.
        +     * }
        + */ + FRAME_ENCODING_ERROR(0x07), + + /** + * Transport parameter error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint received transport parameters that were badly
        +     * formatted, included an invalid value, omitted a mandatory
        +     * transport parameter, included a forbidden transport
        +     * parameter, or were otherwise in error.
        +     * }
        + */ + TRANSPORT_PARAMETER_ERROR(0x08), + + /** + * Connection id limit error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * The number of connection IDs provided by the peer exceeds
        +     * the advertised active_connection_id_limit.
        +     * }
        + */ + CONNECTION_ID_LIMIT_ERROR(0x09), + + /** + * Protocol violiation error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint detected an error with protocol compliance that
        +     * was not covered by more specific error codes.
        +     * }
        + */ + PROTOCOL_VIOLATION(0x0a), + + /** + * Invalid token error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * A server received a client Initial that contained an invalid Token field.
        +     * }
        + */ + INVALID_TOKEN(0x0b), + + /** + * Application error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * The application or application protocol caused the connection to be closed.
        +     * }
        + */ + APPLICATION_ERROR(0x0c), + + /** + * Crypto buffer exceeded error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint has received more data in CRYPTO frames than it can buffer.
        +     * }
        + */ + CRYPTO_BUFFER_EXCEEDED(0x0d), + + /** + * Key update error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint detected errors in performing key updates; see Section 6 of [QUIC-TLS].
        +     * }
        + * @see Section 6 of RFC 9001 [QUIC-TLS] + */ + KEY_UPDATE_ERROR(0x0e), + + /** + * AEAD limit reached error + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint has reached the confidentiality or integrity limit
        +     * for the AEAD algorithm used by the given connection.
        +     * }
        + */ + AEAD_LIMIT_REACHED(0x0f), + + /** + * No viable path error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * An endpoint has determined that the network path is incapable of
        +     * supporting QUIC. An endpoint is unlikely to receive a
        +     * CONNECTION_CLOSE frame carrying this code except when the
        +     * path does not support a large enough MTU.
        +     * }
        + */ + NO_VIABLE_PATH(0x10), + + /** + * Error negotiating version. + * @spec https://www.rfc-editor.org/rfc/rfc9368#name-version-downgrade-preventio + * RFC 9368, Section 4 + */ + VERSION_NEGOTIATION_ERROR(0x11), + + /** + * Crypto error. + *

        + * From + * RFC 9000, Section 20.1: + *

        {@code
        +     * The cryptographic handshake failed. A range of 256 values is
        +     * reserved for carrying error codes specific to the cryptographic
        +     * handshake that is used. Codes for errors occurring when
        +     * TLS is used for the cryptographic handshake are described
        +     * in Section 4.8 of [QUIC-TLS].
        +     * }
        + * @see Section 4.8 of RFC 9001 [QUIC-TLS] + */ + CRYPTO_ERROR(0x0100, 0x01ff); + + private final long from; + private final long to; + + QuicTransportErrors(long code) { + this(code, code); + } + + QuicTransportErrors(long from, long to) { + assert from <= to; + this.from = from; + this.to = to; + } + + /** + * {@return the code for this transport error, if this error + * {@linkplain #hasCode() has a single possible code value}, + * {@code -1} otherwise} + */ + public long code() { return hasCode() ? from : -1;} + + /** + * {@return true if this error has a single possible code value} + */ + public boolean hasCode() { return from == to; } + + /** + * {@return true if this error has a range of possible code values} + */ + public boolean hasRange() { return from < to;} + + /** + * {@return the first possible code value in the range, or the + * code value if this error has a single possible code value} + */ + public long from() {return from;} + + /** + * {@return the last possible code value in the range, or the + * code value if this error has a single possible code value} + */ + public long to() { return to; } + + /** + * Tells whether the given {@code code} value corresponds to + * this error. + * @param code an error code value + * @return true if the given {@code code} value corresponds to + * this error. + */ + boolean isFor(long code) { + return code >= from && code <= to; + } + + /** + * {@return the {@link QuicTransportErrors} instance corresponding + * to the given {@code code} value, if any} + * @param code a {@code code} value + */ + public static Optional ofCode(long code) { + return Stream.of(values()).filter(e -> e.isFor(code)).findAny(); + } + + public static String toString(long code) { + Optional c = Stream.of(values()).filter(e -> e.isFor(code)).findAny(); + if (c.isEmpty()) return "Unknown [0x"+Long.toHexString(code) + "]"; + if (c.get().hasCode()) return c.get().toString(); + if (c.get() == CRYPTO_ERROR) + return c.get() + "|" + Alert.nameOf((byte)code); + return c.get() + " [0x" + Long.toHexString(code) + "]"; + + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java new file mode 100644 index 00000000000..3341cc527f2 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportException.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +/** + * Exception that wraps QUIC transport error codes. + * Thrown in response to packets or frames that violate QUIC protocol. + * This is a fatal exception; connection is always closed when this exception is caught. + * + *

        For a list of errors see: + * https://www.rfc-editor.org/rfc/rfc9000.html#name-transport-error-codes + */ +public final class QuicTransportException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 5259674758792412464L; + + private final QuicTLSEngine.KeySpace keySpace; + private final long frameType; + private final long errorCode; + + /** + * Constructs a new {@code QuicTransportException}. + * + * @param reason the reason why the exception occurred + * @param keySpace the key space in which the frame appeared. + * May be {@code null}, for instance, in + * case of {@link QuicTransportErrors#INTERNAL_ERROR}. + * @param frameType the frame type of the frame whose parsing / handling + * caused the error. + * May be 0 if not related to any specific frame. + * @param errorCode a quic transport error + */ + public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace, + long frameType, QuicTransportErrors errorCode) { + super(reason); + this.keySpace = keySpace; + this.frameType = frameType; + this.errorCode = errorCode.code(); + } + + /** + * Constructs a new {@code QuicTransportException}. For use with TLS alerts. + * + * @param reason the reason why the exception occurred + * @param keySpace the key space in which the frame appeared. + * May be {@code null}, for instance, in + * case of {@link QuicTransportErrors#INTERNAL_ERROR}. + * @param frameType the frame type of the frame whose parsing / handling + * caused the error. + * May be 0 if not related to any specific frame. + * @param errorCode a quic transport error code + * @param cause the cause + */ + public QuicTransportException(String reason, QuicTLSEngine.KeySpace keySpace, + long frameType, long errorCode, Throwable cause) { + super(reason, cause); + this.keySpace = keySpace; + this.frameType = frameType; + this.errorCode = errorCode; + } + + /** + * {@return the reason to include in the {@code ConnectionCloseFrame}} + */ + public String getReason() { + return getMessage(); + } + + /** + * {@return the key space for which the error occurred, or {@code null}} + */ + public QuicTLSEngine.KeySpace getKeySpace() { + return keySpace; + } + + /** + * {@return the frame type for which the error occurred, or 0} + */ + public long getFrameType() { + return frameType; + } + + /** + * {@return the transport error that occurred} + */ + public long getErrorCode() { + return errorCode; + } +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java new file mode 100644 index 00000000000..9429f6bf26f --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicTransportParametersConsumer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import java.nio.ByteBuffer; + +/** + * Interface for consumer of QUIC transport parameters, in wire-encoded format + */ +public interface QuicTransportParametersConsumer { + /** + * Consumes the provided QUIC transport parameters + * @param buffer byte buffer containing encoded quic transport parameters + * @throws QuicTransportException if buffer does not represent valid parameters + */ + void accept(ByteBuffer buffer) throws QuicTransportException; +} diff --git a/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java new file mode 100644 index 00000000000..14bbaba816d --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/net/quic/QuicVersion.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.quic; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents the Quic versions defined in their corresponding RFCs + */ +public enum QuicVersion { + // the version numbers are defined in their respective RFCs + QUIC_V1(1), // RFC-9000 + QUIC_V2(0x6b3343cf); // RFC 9369 + + // 32 bits unsigned integer representing the version as + // defined in RFC. This is the version number as sent + // in long headers packets (see RFC 9000). + private final int versionNumber; + + private QuicVersion(final int versionNumber) { + this.versionNumber = versionNumber; + } + + /** + * {@return the version number} + */ + public int versionNumber() { + return this.versionNumber; + } + + /** + * {@return the QuicVersion corresponding to the {@code versionNumber} or + * {@link Optional#empty() an empty Optional} if the {@code versionNumber} + * doesn't correspond to a Quic version} + * + * @param versionNumber The version number + */ + public static Optional of(int versionNumber) { + for (QuicVersion qv : QuicVersion.values()) { + if (qv.versionNumber == versionNumber) { + return Optional.of(qv); + } + } + return Optional.empty(); + } + + /** + * From among the {@code quicVersions}, selects a {@code QuicVersion} to be used in the + * first packet during connection initiation. + * + * @param quicVersions the available QUIC versions + * @return the QUIC version to use in the first packet + * @throws NullPointerException if {@code quicVersions} is null or any element + * in it is null + * @throws IllegalArgumentException if {@code quicVersions} is empty + */ + public static QuicVersion firstFlightVersion(final Collection quicVersions) { + Objects.requireNonNull(quicVersions); + if (quicVersions.isEmpty()) { + throw new IllegalArgumentException("Empty quic versions"); + } + if (quicVersions.size() == 1) { + return quicVersions.iterator().next(); + } + for (final QuicVersion version : quicVersions) { + if (version == QUIC_V1) { + // we always prefer QUIC v1 for first flight version + return QUIC_V1; + } + } + // the given versions did not have QUIC v1, which implies the + // only available first flight version is QUIC v2 + return QUIC_V2; + } +} diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java index 2a51a0af38d..3ae84fdf198 100644 --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -190,6 +190,8 @@ module java.base { jdk.jlink; exports jdk.internal.logger to java.logging; + exports jdk.internal.net.quic to + java.net.http; exports jdk.internal.org.xml.sax to jdk.jfr; exports jdk.internal.org.xml.sax.helpers to @@ -260,6 +262,7 @@ module java.base { jdk.jfr; exports jdk.internal.util to java.desktop, + java.net.http, java.prefs, java.security.jgss, java.smartcardio, diff --git a/src/java.base/share/classes/sun/security/ssl/Alert.java b/src/java.base/share/classes/sun/security/ssl/Alert.java index 4e1ccf385c7..960b3f3b37d 100644 --- a/src/java.base/share/classes/sun/security/ssl/Alert.java +++ b/src/java.base/share/classes/sun/security/ssl/Alert.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,9 +34,9 @@ import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLProtocolException; /** - * SSL/(D)TLS Alter description + * SSL/(D)TLS Alert description */ -enum Alert { +public enum Alert { // Please refer to TLS Alert Registry for the latest (D)TLS Alert values: // https://www.iana.org/assignments/tls-parameters/ CLOSE_NOTIFY ((byte)0, "close_notify", false), @@ -103,7 +103,7 @@ enum Alert { return null; } - static String nameOf(byte id) { + public static String nameOf(byte id) { for (Alert al : Alert.values()) { if (al.id == id) { return al.description; diff --git a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java index d44ec034411..aa5933ddab0 100644 --- a/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/AlpnExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -71,7 +71,7 @@ final class AlpnExtension { /** * The "application_layer_protocol_negotiation" extension. - * + *

        * See RFC 7301 for the specification of this extension. */ static final class AlpnSpec implements SSLExtensionSpec { @@ -344,6 +344,13 @@ final class AlpnExtension { // The producing happens in server side only. ServerHandshakeContext shc = (ServerHandshakeContext)context; + if (shc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST use ALPN + throw shc.conContext.fatal( + Alert.NO_APPLICATION_PROTOCOL, + "Client did not offer application layer protocol"); + } + // Please don't use the previous negotiated application protocol. shc.applicationProtocol = ""; shc.conContext.applicationProtocol = ""; @@ -513,6 +520,15 @@ final class AlpnExtension { // The producing happens in client side only. ClientHandshakeContext chc = (ClientHandshakeContext)context; + if (chc.sslConfig.isQuic) { + // RFC 9001: QUIC clients MUST use error 0x0178 + // [no_application_protocol] to terminate a connection when + // ALPN negotiation fails + throw chc.conContext.fatal( + Alert.NO_APPLICATION_PROTOCOL, + "Server did not offer application layer protocol"); + } + // Please don't use the previous negotiated application protocol. chc.applicationProtocol = ""; chc.conContext.applicationProtocol = ""; diff --git a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java index 609a81571ed..d4587d35ae9 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java +++ b/src/java.base/share/classes/sun/security/ssl/CertificateMessage.java @@ -1219,12 +1219,19 @@ final class CertificateMessage { certs.clone(), authType, engine); - } else { - SSLSocket socket = (SSLSocket)shc.conContext.transport; + } else if (shc.conContext.transport instanceof SSLSocket socket){ ((X509ExtendedTrustManager)tm).checkClientTrusted( certs.clone(), authType, socket); + } else if (shc.conContext.transport + instanceof QuicTLSEngineImpl qtlse) { + if (tm instanceof X509TrustManagerImpl tmImpl) { + tmImpl.checkClientTrusted(certs.clone(), authType, qtlse); + } else { + throw new CertificateException( + "QUIC only supports SunJSSE trust managers"); + } } } else { // Unlikely to happen, because we have wrapped the old @@ -1268,18 +1275,26 @@ final class CertificateMessage { try { X509TrustManager tm = chc.sslContext.getX509TrustManager(); - if (tm instanceof X509ExtendedTrustManager) { + if (tm instanceof X509ExtendedTrustManager x509ExtTm) { if (chc.conContext.transport instanceof SSLEngine engine) { - ((X509ExtendedTrustManager)tm).checkServerTrusted( + x509ExtTm.checkServerTrusted( certs.clone(), authType, engine); - } else { - SSLSocket socket = (SSLSocket)chc.conContext.transport; - ((X509ExtendedTrustManager)tm).checkServerTrusted( + } else if (chc.conContext.transport instanceof SSLSocket socket) { + x509ExtTm.checkServerTrusted( certs.clone(), authType, socket); + } else if (chc.conContext.transport instanceof QuicTLSEngineImpl qtlse) { + if (x509ExtTm instanceof X509TrustManagerImpl tmImpl) { + tmImpl.checkServerTrusted(certs.clone(), authType, qtlse); + } else { + throw new CertificateException( + "QUIC only supports SunJSSE trust managers"); + } + } else { + throw new AssertionError("Unexpected transport type"); } } else { // Unlikely to happen, because we have wrapped the old diff --git a/src/java.base/share/classes/sun/security/ssl/ClientHello.java b/src/java.base/share/classes/sun/security/ssl/ClientHello.java index 3e43921520d..c9432ea3979 100644 --- a/src/java.base/share/classes/sun/security/ssl/ClientHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ClientHello.java @@ -568,7 +568,7 @@ final class ClientHello { } if (sessionId.length() == 0 && chc.maximumActiveProtocol.useTLS13PlusSpec() && - SSLConfiguration.useCompatibilityMode) { + chc.sslConfig.isUseCompatibilityMode()) { // In compatibility mode, the TLS 1.3 legacy_session_id // field MUST be non-empty, so a client not offering a // pre-TLS 1.3 session MUST generate a new 32-byte value. diff --git a/src/java.base/share/classes/sun/security/ssl/Finished.java b/src/java.base/share/classes/sun/security/ssl/Finished.java index 9421d12ec15..04fe61760d0 100644 --- a/src/java.base/share/classes/sun/security/ssl/Finished.java +++ b/src/java.base/share/classes/sun/security/ssl/Finished.java @@ -846,6 +846,16 @@ final class Finished { // update the context for the following key derivation shc.handshakeKeyDerivation = secretKD; + if (shc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + engine.deriveOneRTTKeys(); + } catch (IOException e) { + throw shc.conContext.fatal(Alert.INTERNAL_ERROR, + "Failure to derive application secrets", e); + } + } } catch (GeneralSecurityException gse) { throw shc.conContext.fatal(Alert.INTERNAL_ERROR, "Failure to derive application secrets", gse); @@ -1010,6 +1020,16 @@ final class Finished { // update the context for the following key derivation chc.handshakeKeyDerivation = secretKD; + if (chc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + engine.deriveOneRTTKeys(); + } catch (IOException e) { + throw chc.conContext.fatal(Alert.INTERNAL_ERROR, + "Failure to derive application secrets", e); + } + } } catch (GeneralSecurityException gse) { throw chc.conContext.fatal(Alert.INTERNAL_ERROR, "Failure to derive application secrets", gse); diff --git a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java index c4549070f02..2b17c7406a3 100644 --- a/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java +++ b/src/java.base/share/classes/sun/security/ssl/KeyUpdate.java @@ -269,6 +269,12 @@ final class KeyUpdate { HandshakeMessage message) throws IOException { // The producing happens in server side only. PostHandshakeContext hc = (PostHandshakeContext)context; + if (hc.sslConfig.isQuic) { + // Quic doesn't allow KEY_UPDATE TLS message. It has its own Quic specific + // key update mechanism, RFC-9001, section 6: + // Endpoints MUST NOT send a TLS KeyUpdate message. + return null; + } KeyUpdateMessage km = (KeyUpdateMessage)message; if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { SSLLogger.fine( diff --git a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java index 0fa831f6351..f2c30b3ff72 100644 --- a/src/java.base/share/classes/sun/security/ssl/OutputRecord.java +++ b/src/java.base/share/classes/sun/security/ssl/OutputRecord.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,6 +31,8 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.quic.QuicTLSEngine; import sun.security.ssl.SSLCipher.SSLWriteCipher; /** @@ -154,6 +156,16 @@ abstract class OutputRecord throw new UnsupportedOperationException(); } + // apply to QuicEngine only + byte[] getHandshakeMessage() { + throw new UnsupportedOperationException(); + } + + // apply to QuicEngine only + QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() { + throw new UnsupportedOperationException(); + } + // apply to SSLEngine only void encodeV2NoCipher() throws IOException { throw new UnsupportedOperationException(); diff --git a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java index b06549b40e3..a4f87616245 100644 --- a/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java +++ b/src/java.base/share/classes/sun/security/ssl/PostHandshakeContext.java @@ -47,17 +47,15 @@ final class PostHandshakeContext extends HandshakeContext { context.conSession.getLocalSupportedSignatureSchemes()); // Add the potential post-handshake consumers. - if (context.sslConfig.isClientMode) { + if (!context.sslConfig.isQuic) { handshakeConsumers.putIfAbsent( SSLHandshake.KEY_UPDATE.id, SSLHandshake.KEY_UPDATE); + } + if (context.sslConfig.isClientMode) { handshakeConsumers.putIfAbsent( SSLHandshake.NEW_SESSION_TICKET.id, SSLHandshake.NEW_SESSION_TICKET); - } else { - handshakeConsumers.putIfAbsent( - SSLHandshake.KEY_UPDATE.id, - SSLHandshake.KEY_UPDATE); } handshakeFinished = true; @@ -93,6 +91,15 @@ final class PostHandshakeContext extends HandshakeContext { static boolean isConsumable(TransportContext context, byte handshakeType) { if (handshakeType == SSLHandshake.KEY_UPDATE.id) { + // Quic doesn't allow KEY_UPDATE TLS message. It has its own + // Quic-specific key update mechanism, RFC-9001, section 6: + // Endpoints MUST NOT send a TLS KeyUpdate message. Endpoints + // MUST treat the receipt of a TLS KeyUpdate message as a + // connection error of type 0x010a, equivalent to a fatal + // TLS alert of unexpected_message; + if (context.sslConfig.isQuic) { + return false; + } // The KeyUpdate handshake message does not apply to TLS 1.2 and // previous protocols. return context.protocolVersion.useTLS13PlusSpec(); diff --git a/src/java.base/share/classes/sun/security/ssl/QuicCipher.java b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java new file mode 100644 index 00000000000..c1d812e4c40 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicCipher.java @@ -0,0 +1,699 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.security.ssl; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.GeneralSecurityException; +import java.security.Security; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.ChaCha20ParameterSpec; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import sun.security.util.KeyUtil; + +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT; +import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR; + +abstract class QuicCipher { + private static final String + SEC_PROP_QUIC_TLS_KEY_LIMITS = "jdk.quic.tls.keyLimits"; + + private static final Map KEY_LIMITS; + + static { + final String propVal = Security.getProperty( + SEC_PROP_QUIC_TLS_KEY_LIMITS); + if (propVal == null) { + KEY_LIMITS = Map.of(); // no specific limits + } else { + final Map limits = new HashMap<>(); + for (final String entry : propVal.split(",")) { + // each entry is of the form + // example: + // AES/GCM/NoPadding 2^23 + // ChaCha20-Poly1305 -1 + final String[] parts = entry.trim().split(" "); + if (parts.length != 2) { + // TODO: exception type + throw new RuntimeException("invalid value for " + + SEC_PROP_QUIC_TLS_KEY_LIMITS + + " security property"); + } + final String cipher = parts[0]; + if (limits.containsKey(cipher)) { + throw new RuntimeException( + "key limit defined more than once for cipher " + + cipher); + } + final String limitVal = parts[1]; + final long limit; + final int index = limitVal.indexOf("^"); + if (index >= 0) { + // of the form x^y (example: 2^23) + limit = (long) Math.pow( + Integer.parseInt(limitVal.substring(0, index)), + Integer.parseInt(limitVal.substring(index + 1))); + } else { + limit = Long.parseLong(limitVal); + } + if (limit == 0 || limit < -1) { + // we allow -1 to imply no limits, but any other zero + // or negative value is invalid + // TODO: exception type + throw new RuntimeException("invalid value for " + + SEC_PROP_QUIC_TLS_KEY_LIMITS + + " security property"); + } + limits.put(cipher, limit); + } + KEY_LIMITS = Collections.unmodifiableMap(limits); + } + } + + private final CipherSuite cipherSuite; + private final QuicHeaderProtectionCipher hpCipher; + private final SecretKey baseSecret; + private final int keyPhase; + + protected QuicCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final QuicHeaderProtectionCipher hpCipher, final int keyPhase) { + assert keyPhase == 0 || keyPhase == 1 : + "invalid key phase: " + keyPhase; + this.cipherSuite = cipherSuite; + this.baseSecret = baseSecret; + this.hpCipher = hpCipher; + this.keyPhase = keyPhase; + } + + final SecretKey getBaseSecret() { + return this.baseSecret; + } + + final CipherSuite getCipherSuite() { + return this.cipherSuite; + } + + final SecretKey getHeaderProtectionKey() { + return this.hpCipher.headerProtectionKey; + } + + final ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + return hpCipher.computeHeaderProtectionMask(sample); + } + + final int getKeyPhase() { + return this.keyPhase; + } + + final void discard(boolean destroyHP) { + safeDiscard(this.baseSecret); + if (destroyHP) { + this.hpCipher.discard(); + } + this.doDiscard(); + } + + protected abstract void doDiscard(); + + static QuicReadCipher createReadCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + return switch (cipherSuite) { + case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 -> + new T13GCMReadCipher( + cipherSuite, baseSecret, key, iv, hp, keyPhase); + case TLS_CHACHA20_POLY1305_SHA256 -> + new T13CC20P1305ReadCipher( + cipherSuite, baseSecret, key, iv, hp, keyPhase); + default -> throw new IllegalArgumentException("Cipher suite " + + cipherSuite + " not supported"); + }; + } + + static QuicWriteCipher createWriteCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + return switch (cipherSuite) { + case TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384 -> + new T13GCMWriteCipher(cipherSuite, baseSecret, key, iv, hp, + keyPhase); + case TLS_CHACHA20_POLY1305_SHA256 -> + new T13CC20P1305WriteCipher(cipherSuite, baseSecret, key, iv, + hp, keyPhase); + default -> throw new IllegalArgumentException("Cipher suite " + + cipherSuite + " not supported"); + }; + } + + static void safeDiscard(final SecretKey secretKey) { + KeyUtil.destroySecretKeys(secretKey); + } + + abstract static class QuicReadCipher extends QuicCipher { + private final AtomicLong lowestDecryptedPktNum = new AtomicLong(-1); + + QuicReadCipher(CipherSuite cipherSuite, SecretKey baseSecret, + QuicHeaderProtectionCipher hpCipher, int keyPhase) { + super(cipherSuite, baseSecret, hpCipher, keyPhase); + } + + final void decryptPacket(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + doDecrypt(packetNumber, packet, headerLength, output); + boolean updated; + do { + final long current = lowestDecryptedPktNum.get(); + assert packetNumber >= 0 : + "unexpected packet number: " + packetNumber; + final long newLowest = current == -1 ? packetNumber : + Math.min(current, packetNumber); + updated = lowestDecryptedPktNum.compareAndSet(current, + newLowest); + } while (!updated); + } + + protected abstract void doDecrypt(long packetNumber, + ByteBuffer packet, int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException; + + /** + * Returns the maximum limit on the number of packets that fail + * decryption, across all key (updates), using this + * {@code QuicReadCipher}. This method must not return a value less + * than 0. + * + * @return the limit + */ + // RFC-9001, section 6.6 + abstract long integrityLimit(); + + /** + * {@return the lowest packet number that this {@code QuicReadCipher} + * has decrypted. If no packets have yet been decrypted by this + * instance, then this method returns -1} + */ + final long lowestDecryptedPktNum() { + return this.lowestDecryptedPktNum.get(); + } + + /** + * {@return true if this {@code QuicReadCipher} has successfully + * decrypted any packet sent by the peer, else returns false} + */ + final boolean hasDecryptedAny() { + return this.lowestDecryptedPktNum.get() != -1; + } + } + + abstract static class QuicWriteCipher extends QuicCipher { + private final AtomicLong numPacketsEncrypted = new AtomicLong(); + private final AtomicLong lowestEncryptedPktNum = new AtomicLong(-1); + + QuicWriteCipher(CipherSuite cipherSuite, SecretKey baseSecret, + QuicHeaderProtectionCipher hpCipher, int keyPhase) { + super(cipherSuite, baseSecret, hpCipher, keyPhase); + } + + final void encryptPacket(final long packetNumber, + final ByteBuffer packetHeader, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicTransportException, ShortBufferException { + final long confidentialityLimit = confidentialityLimit(); + final long numEncrypted = this.numPacketsEncrypted.get(); + if (confidentialityLimit > 0 && + numEncrypted > confidentialityLimit) { + // the OneRttKeyManager is responsible for detecting and + // initiating a key update before this limit is hit. The fact + // that we hit this limit indicates that either the key + // update wasn't initiated or the key update failed. In + // either case we just throw an exception which + // should lead to the connection being closed as required by + // RFC-9001, section 6.6: + // If a key update is not possible or integrity limits are + // reached, the endpoint MUST stop using the connection and + // only send stateless resets in response to receiving + // packets. It is RECOMMENDED that endpoints immediately + // close the connection with a connection error of type + // AEAD_LIMIT_REACHED before reaching a state where key + // updates are not possible. + throw new QuicTransportException("confidentiality limit " + + "reached", ONE_RTT, 0, + QuicTransportErrors.AEAD_LIMIT_REACHED); + } + this.numPacketsEncrypted.incrementAndGet(); + doEncryptPacket(packetNumber, packetHeader, packetPayload, output); + boolean updated; + do { + final long current = lowestEncryptedPktNum.get(); + assert packetNumber >= 0 : + "unexpected packet number: " + packetNumber; + final long newLowest = current == -1 ? packetNumber : + Math.min(current, packetNumber); + updated = lowestEncryptedPktNum.compareAndSet(current, + newLowest); + } while (!updated); + } + + /** + * {@return the lowest packet number that this {@code QuicWriteCipher} + * has encrypted. If no packets have yet been encrypted by this + * instance, then this method returns -1} + */ + final long lowestEncryptedPktNum() { + return this.lowestEncryptedPktNum.get(); + } + + /** + * {@return true if this {@code QuicWriteCipher} has successfully + * encrypted any packet to send to the peer, else returns false} + */ + final boolean hasEncryptedAny() { + // rely on the lowestEncryptedPktNum field instead of the + // numPacketsEncrypted field. this avoids a race where the + // lowestEncryptedPktNum() might return a value contradicting + // the return value of this method. + return this.lowestEncryptedPktNum.get() != -1; + } + + /** + * {@return the number of packets encrypted by this {@code + * QuicWriteCipher}} + */ + final long getNumEncrypted() { + return this.numPacketsEncrypted.get(); + } + + abstract void doEncryptPacket(long packetNumber, ByteBuffer packetHeader, + ByteBuffer packetPayload, ByteBuffer output) + throws ShortBufferException, QuicTransportException; + + /** + * Returns the maximum limit on the number of packets that are allowed + * to be encrypted with this instance of {@code QuicWriteCipher}. A + * value less than 0 implies that there's no limit. + * + * @return the limit or -1 + */ + // RFC-9001, section 6.6: The confidentiality limit applies to the + // number of + // packets encrypted with a given key. + abstract long confidentialityLimit(); + } + + abstract static class QuicHeaderProtectionCipher { + protected final SecretKey headerProtectionKey; + + protected QuicHeaderProtectionCipher( + final SecretKey headerProtectionKey) { + this.headerProtectionKey = headerProtectionKey; + } + + int getHeaderProtectionSampleSize() { + return 16; + } + + abstract ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException; + + final void discard() { + safeDiscard(this.headerProtectionKey); + } + } + + static final class T13GCMReadCipher extends QuicReadCipher { + // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM, + // the integrity limit is 2^52 invalid packets + private static final long INTEGRITY_LIMIT = 1L << 52; + + private final Cipher cipher; + private final SecretKey key; + private final byte[] iv; + + T13GCMReadCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final SecretKey key, final byte[] iv, final SecretKey hp, + final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance("AES/GCM/NoPadding"); + } + + @Override + protected void doDecrypt(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv); + synchronized (cipher) { + try { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + int limit = packet.limit(); + packet.limit(packet.position() + headerLength); + cipher.updateAAD(packet); + packet.limit(limit); + cipher.doFinal(packet, output); + } catch (AEADBadTagException | ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Decryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long integrityLimit() { + return INTEGRITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13GCMWriteCipher extends QuicWriteCipher { + private static final String CIPHER_ALGORITHM_NAME = "AES/GCM/NoPadding"; + private static final long CONFIDENTIALITY_LIMIT; + + static { + // RFC-9001, section 6.6: For AEAD_AES_128_GCM and AEAD_AES_256_GCM, + // the confidentiality limit is 2^23 encrypted packets + final long defaultVal = 1 << 23; + long limit = + KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal); + // don't allow the configuration to increase the confidentiality + // limit, but only let it lower the limit + limit = limit > defaultVal ? defaultVal : limit; + CONFIDENTIALITY_LIMIT = limit; + } + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13GCMWriteCipher(final CipherSuite cipherSuite, final SecretKey baseSecret, + final SecretKey key, final byte[] iv, final SecretKey hp, + final int keyPhase) throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13AESHPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME); + } + + @Override + void doEncryptPacket(long packetNumber, ByteBuffer packetHeader, + ByteBuffer packetPayload, ByteBuffer output) + throws ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final GCMParameterSpec ivSpec = new GCMParameterSpec(128, iv); + synchronized (cipher) { + try { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + cipher.updateAAD(packetHeader); + cipher.doFinal(packetPayload, output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long confidentialityLimit() { + return CONFIDENTIALITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13AESHPCipher extends QuicHeaderProtectionCipher { + private final Cipher cipher; + + T13AESHPCipher(SecretKey hp) throws GeneralSecurityException { + super(hp); + cipher = Cipher.getInstance("AES/ECB/NoPadding"); + } + + @Override + public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + if (sample.remaining() != getHeaderProtectionSampleSize()) { + throw new IllegalArgumentException("Invalid sample size"); + } + ByteBuffer output = ByteBuffer.allocate(sample.remaining()); + try { + synchronized (cipher) { + // Some providers (Jipher) don't re-initialize the cipher + // after doFinal, and need init every time. + cipher.init(Cipher.ENCRYPT_MODE, headerProtectionKey); + cipher.doFinal(sample, output); + } + output.flip(); + assert output.remaining() >= 5; + return output; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + static final class T13CC20P1305ReadCipher extends QuicReadCipher { + // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305, + // the integrity limit is 2^36 invalid packets + private static final long INTEGRITY_LIMIT = 1L << 36; + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13CC20P1305ReadCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance("ChaCha20-Poly1305"); + } + + @Override + protected void doDecrypt(long packetNumber, ByteBuffer packet, + int headerLength, ByteBuffer output) + throws AEADBadTagException, ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + while (packetNumber > 0) { + iv[i] ^= (byte) (packetNumber & 0xFF); + packetNumber = packetNumber >>> 8; + i--; + } + final IvParameterSpec ivSpec = new IvParameterSpec(iv); + synchronized (cipher) { + try { + cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); + int limit = packet.limit(); + packet.limit(packet.position() + headerLength); + cipher.updateAAD(packet); + packet.limit(limit); + cipher.doFinal(packet, output); + } catch (AEADBadTagException | ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Decryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long integrityLimit() { + return INTEGRITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13CC20P1305WriteCipher extends QuicWriteCipher { + private static final String CIPHER_ALGORITHM_NAME = "ChaCha20-Poly1305"; + private static final long CONFIDENTIALITY_LIMIT; + + static { + // RFC-9001, section 6.6: For AEAD_CHACHA20_POLY1305, the + // confidentiality limit is greater than the number of possible + // packets (2^62) and so can be disregarded. + final long defaultVal = -1; // no limit + long limit = + KEY_LIMITS.getOrDefault(CIPHER_ALGORITHM_NAME, defaultVal); + limit = limit < 0 ? -1 /* no limit */ : limit; + CONFIDENTIALITY_LIMIT = limit; + } + + private final SecretKey key; + private final Cipher cipher; + private final byte[] iv; + + T13CC20P1305WriteCipher(final CipherSuite cipherSuite, + final SecretKey baseSecret, final SecretKey key, + final byte[] iv, final SecretKey hp, + final int keyPhase) + throws GeneralSecurityException { + super(cipherSuite, baseSecret, new T13CC20HPCipher(hp), keyPhase); + this.key = key; + this.iv = iv; + this.cipher = Cipher.getInstance(CIPHER_ALGORITHM_NAME); + } + + @Override + void doEncryptPacket(final long packetNumber, final ByteBuffer packetHeader, + final ByteBuffer packetPayload, final ByteBuffer output) + throws ShortBufferException, QuicTransportException { + byte[] iv = this.iv.clone(); + + // apply packet number to IV + int i = 11; + long pn = packetNumber; + while (pn > 0) { + iv[i] ^= (byte) (pn & 0xFF); + pn = pn >>> 8; + i--; + } + final IvParameterSpec ivSpec = new IvParameterSpec(iv); + synchronized (cipher) { + try { + cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec); + cipher.updateAAD(packetHeader); + cipher.doFinal(packetPayload, output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } + + @Override + long confidentialityLimit() { + return CONFIDENTIALITY_LIMIT; + } + + @Override + protected final void doDiscard() { + safeDiscard(this.key); + } + } + + static final class T13CC20HPCipher extends QuicHeaderProtectionCipher { + private final Cipher cipher; + + T13CC20HPCipher(final SecretKey hp) throws GeneralSecurityException { + super(hp); + cipher = Cipher.getInstance("ChaCha20"); + } + + @Override + public ByteBuffer computeHeaderProtectionMask(ByteBuffer sample) + throws QuicTransportException { + if (sample.remaining() != getHeaderProtectionSampleSize()) { + throw new IllegalArgumentException("Invalid sample size"); + } + try { + // RFC 7539: [counter is a] 32-bit block count parameter, + // treated as a 32-bit little-endian integer + // RFC 9001: + // counter = sample[0..3] + // nonce = sample[4..15] + // mask = ChaCha20(hp_key, counter, nonce, {0,0,0,0,0}) + + sample.order(ByteOrder.LITTLE_ENDIAN); + byte[] nonce = new byte[12]; + int counter = sample.getInt(); + sample.get(nonce); + ChaCha20ParameterSpec ivSpec = + new ChaCha20ParameterSpec(nonce, counter); + byte[] output = new byte[5]; + + synchronized (cipher) { + // DECRYPT produces the same output as ENCRYPT, but does + // not throw when the same IV is used repeatedly + cipher.init(Cipher.DECRYPT_MODE, headerProtectionKey, + ivSpec); + int numBytes = cipher.doFinal(output, 0, 5, output); + assert numBytes == 5; + } + return ByteBuffer.wrap(output); + } catch (Exception e) { + throw new QuicTransportException("Encryption failed", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java new file mode 100644 index 00000000000..893eb282116 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicEngineOutputRecord.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package sun.security.ssl; + +import jdk.internal.net.quic.QuicTLSEngine; +import sun.security.ssl.SSLCipher.SSLWriteCipher; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.LinkedList; + +/** + * {@code OutputRecord} implementation for {@code QuicTLSEngineImpl}. + */ +final class QuicEngineOutputRecord extends OutputRecord implements SSLRecord { + + private final HandshakeFragment fragmenter = new HandshakeFragment(); + + private volatile boolean isCloseWaiting; + + private Alert alert; + + QuicEngineOutputRecord(HandshakeHash handshakeHash) { + super(handshakeHash, SSLWriteCipher.nullTlsWriteCipher()); + + this.packetSize = SSLRecord.maxRecordSize; + this.protocolVersion = ProtocolVersion.NONE; + } + + @Override + public void close() throws IOException { + recordLock.lock(); + try { + if (!isClosed) { + if (!fragmenter.isEmpty()) { + isCloseWaiting = true; + } else { + super.close(); + } + } + } finally { + recordLock.unlock(); + } + } + + boolean isClosed() { + return isClosed || isCloseWaiting; + } + + @Override + void encodeAlert(byte level, byte description) throws IOException { + recordLock.lock(); + try { + if (isClosed()) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("outbound has closed, ignore outbound " + + "alert message: " + Alert.nameOf(description)); + } + return; + } + if (level == Alert.Level.WARNING.level) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Suppressing warning-level " + + "alert message: " + Alert.nameOf(description)); + } + return; + } + + if (alert != null) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("Suppressing subsequent alert: " + + description + ", original: " + alert.id); + } + return; + } + + alert = Alert.valueOf(description); + } finally { + recordLock.unlock(); + } + } + + @Override + void encodeHandshake(byte[] source, + int offset, int length) throws IOException { + recordLock.lock(); + try { + if (isClosed()) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.warning("outbound has closed, ignore outbound " + + "handshake message", + ByteBuffer.wrap(source, offset, length)); + } + return; + } + + firstMessage = false; + + byte handshakeType = source[offset]; + if (handshakeHash.isHashable(handshakeType)) { + handshakeHash.deliver(source, offset, length); + } + + fragmenter.queueUpFragment(source, offset, length); + } finally { + recordLock.unlock(); + } + } + + @Override + void encodeChangeCipherSpec() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + void changeWriteCiphers(SSLWriteCipher writeCipher, boolean useChangeCipherSpec) throws IOException { + recordLock.lock(); + try { + fragmenter.changePacketSpace(); + } finally { + recordLock.unlock(); + } + } + + @Override + void changeWriteCiphers(SSLWriteCipher writeCipher, byte keyUpdateRequest) throws IOException { + throw new UnsupportedOperationException("Should not call this"); + } + + @Override + byte[] getHandshakeMessage() { + recordLock.lock(); + try { + return fragmenter.acquireCiphertext(); + } finally { + recordLock.unlock(); + } + } + + @Override + QuicTLSEngine.KeySpace getHandshakeMessageKeySpace() { + recordLock.lock(); + try { + return switch (fragmenter.currentPacketSpace) { + case 0-> QuicTLSEngine.KeySpace.INITIAL; + case 1-> QuicTLSEngine.KeySpace.HANDSHAKE; + case 2-> QuicTLSEngine.KeySpace.ONE_RTT; + default -> throw new IllegalStateException("Unexpected state"); + }; + } finally { + recordLock.unlock(); + } + } + + @Override + boolean isEmpty() { + recordLock.lock(); + try { + return fragmenter.isEmpty(); + } finally { + recordLock.unlock(); + } + } + + Alert getAlert() { + recordLock.lock(); + try { + return alert; + } finally { + recordLock.unlock(); + } + } + + // buffered record fragment + private static class HandshakeMemo { + boolean changeSpace; + byte[] fragment; + } + + static final class HandshakeFragment { + private final LinkedList handshakeMemos = + new LinkedList<>(); + + private int currentPacketSpace; + + void queueUpFragment(byte[] source, + int offset, int length) throws IOException { + HandshakeMemo memo = new HandshakeMemo(); + + memo.fragment = new byte[length]; + assert Record.getInt24(ByteBuffer.wrap(source, offset + 1, 3)) + == length - 4 : "Invalid handshake message length"; + System.arraycopy(source, offset, memo.fragment, 0, length); + + handshakeMemos.add(memo); + } + + void changePacketSpace() { + HandshakeMemo lastMemo = handshakeMemos.peekLast(); + if (lastMemo != null) { + lastMemo.changeSpace = true; + } else { + currentPacketSpace++; + } + } + + byte[] acquireCiphertext() { + HandshakeMemo hsMemo = handshakeMemos.pollFirst(); + if (hsMemo == null) { + return null; + } + if (hsMemo.changeSpace) { + currentPacketSpace++; + } + return hsMemo.fragment; + } + + boolean isEmpty() { + return handshakeMemos.isEmpty(); + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java new file mode 100644 index 00000000000..fb9077af022 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicKeyManager.java @@ -0,0 +1,1216 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.security.ssl; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.IntFunction; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.KDF; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.HKDFParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.vm.annotation.Stable; +import sun.security.ssl.QuicCipher.QuicReadCipher; +import sun.security.ssl.QuicCipher.QuicWriteCipher; +import sun.security.util.KeyUtil; + +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.HANDSHAKE; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.INITIAL; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.ONE_RTT; +import static jdk.internal.net.quic.QuicTransportErrors.AEAD_LIMIT_REACHED; +import static jdk.internal.net.quic.QuicTransportErrors.KEY_UPDATE_ERROR; +import static sun.security.ssl.QuicTLSEngineImpl.BASE_CRYPTO_ERROR; + +sealed abstract class QuicKeyManager + permits QuicKeyManager.HandshakeKeyManager, + QuicKeyManager.InitialKeyManager, QuicKeyManager.OneRttKeyManager { + + private record QuicKeys(SecretKey key, byte[] iv, SecretKey hp) { + } + + private record CipherPair(QuicReadCipher readCipher, + QuicWriteCipher writeCipher) { + void discard(boolean destroyHP) { + writeCipher.discard(destroyHP); + readCipher.discard(destroyHP); + } + + /** + * {@return true if the keys represented by this {@code CipherPair} + * were used by both this endpoint and the peer, thus implying these + * keys are available to both of them} + */ + boolean usedByBothEndpoints() { + return this.readCipher.hasDecryptedAny() && + this.writeCipher.hasEncryptedAny(); + } + } + + final QuicTLSEngine.KeySpace keySpace; + // counter towards the integrity limit + final AtomicLong invalidPackets = new AtomicLong(); + volatile boolean keysDiscarded; + + private QuicKeyManager(final QuicTLSEngine.KeySpace keySpace) { + this.keySpace = keySpace; + } + + protected abstract boolean keysAvailable(); + + protected abstract QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException; + + protected abstract QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException; + + abstract void discardKeys(); + + void decryptPacket(final long packetNumber, final int keyPhase, + final ByteBuffer packet,final int headerLength, + final ByteBuffer output) throws QuicKeyUnavailableException, + IllegalArgumentException, AEADBadTagException, + QuicTransportException, ShortBufferException { + // keyPhase is only applicable for 1-RTT packets; the decryptPacket + // method is overridden by OneRttKeyManager, so this check is for + // other packet types + if (keyPhase != -1) { + throw new IllegalArgumentException( + "Unexpected key phase value: " + keyPhase); + } + // use current keys to decrypt + QuicReadCipher readCipher = getReadCipher(); + try { + readCipher.decryptPacket(packetNumber, packet, headerLength, output); + } catch (AEADBadTagException e) { + if (invalidPackets.incrementAndGet() >= + readCipher.integrityLimit()) { + throw new QuicTransportException("Integrity limit reached", + keySpace, 0, AEAD_LIMIT_REACHED); + } + throw e; + } + } + + void encryptPacket(final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + // generate the packet header passing the generator the key phase + final ByteBuffer header = headerGenerator.apply(0); // key phase is always 0 for non-ONE_RTT + getWriteCipher().encryptPacket(packetNumber, header, packetPayload, output); + } + + private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion, + final CipherSuite cs, final SecretKey traffic_secret) + throws IOException { + final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs, + traffic_secret); + final QuicTLSData tlsData = getQuicData(quicVersion); + final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel()); + final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel()); + final SecretKey quic_hp = kd.deriveKey(tlsData.getTlsHpLabel()); + return new QuicKeys(quic_key, quic_iv, quic_hp); + } + + // Used in 1RTT when advancing the keyphase. quic_hp is not advanced. + private static QuicKeys deriveQuicKeys(final QuicVersion quicVersion, + final CipherSuite cs, final SecretKey traffic_secret, + final SecretKey quic_hp) throws IOException { + final SSLKeyDerivation kd = new QuicTLSKeyDerivation(cs, + traffic_secret); + final QuicTLSData tlsData = getQuicData(quicVersion); + final SecretKey quic_key = kd.deriveKey(tlsData.getTlsKeyLabel()); + final byte[] quic_iv = kd.deriveData(tlsData.getTlsIvLabel()); + return new QuicKeys(quic_key, quic_iv, quic_hp); + } + + private static QuicTLSData getQuicData(final QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> QuicTLSData.V1; + case QUIC_V2 -> QuicTLSData.V2; + }; + } + + private static byte[] createHkdfInfo(final String label, final int length) { + final byte[] tls13Label = + ("tls13 " + label).getBytes(StandardCharsets.UTF_8); + return createHkdfInfo(tls13Label, length); + } + + private static byte[] createHkdfInfo(final byte[] tls13Label, + final int length) { + final byte[] info = new byte[4 + tls13Label.length]; + final ByteBuffer m = ByteBuffer.wrap(info); + try { + Record.putInt16(m, length); + Record.putBytes8(m, tls13Label); + Record.putInt8(m, 0x00); // zero-length context + } catch (IOException ioe) { + // unlikely + throw new UncheckedIOException("Unexpected exception", ioe); + } + return info; + } + + static final class InitialKeyManager extends QuicKeyManager { + + private volatile CipherPair cipherPair; + + InitialKeyManager() { + super(INITIAL); + } + + @Override + protected boolean keysAvailable() { + return this.cipherPair != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.writeCipher; + } + + @Override + void discardKeys() { + final CipherPair toDiscard = this.cipherPair; + this.keysDiscarded = true; + this.cipherPair = null; // no longer needed + if (toDiscard == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding keys (keyphase=" + + toDiscard.writeCipher.getKeyPhase() + + ") of " + this.keySpace + " key space"); + } + toDiscard.discard(true); + } + + void deriveKeys(final QuicVersion quicVersion, + final byte[] connectionId, + final boolean clientMode) throws IOException{ + Objects.requireNonNull(quicVersion); + final CipherSuite cs = CipherSuite.TLS_AES_128_GCM_SHA256; + final CipherSuite.HashAlg hashAlg = cs.hashAlg; + + KDF hkdf; + try { + hkdf = KDF.getInstance(hashAlg.hkdfAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new SSLHandshakeException("Could not generate secret", e); + } + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + SecretKey initial_secret = null; + SecretKey server_initial_secret = null; + SecretKey client_initial_secret = null; + try { + initial_secret = hkdf.deriveKey("TlsInitialSecret", + HKDFParameterSpec.ofExtract() + .addSalt(tlsData.getInitialSalt()) + .addIKM(connectionId).extractOnly()); + + byte[] clientInfo = createHkdfInfo("client in", + hashAlg.hashLength); + client_initial_secret = + hkdf.deriveKey("TlsClientInitialTrafficSecret", + HKDFParameterSpec.expandOnly( + initial_secret, + clientInfo, + hashAlg.hashLength)); + QuicKeys clientKeys = deriveQuicKeys(quicVersion, cs, + client_initial_secret); + + byte[] serverInfo = createHkdfInfo("server in", + hashAlg.hashLength); + server_initial_secret = + hkdf.deriveKey("TlsServerInitialTrafficSecret", + HKDFParameterSpec.expandOnly( + initial_secret, + serverInfo, + hashAlg.hashLength)); + QuicKeys serverKeys = deriveQuicKeys(quicVersion, cs, + server_initial_secret); + + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + final int keyPhase = 0; + if (clientMode) { + readCipher = QuicCipher.createReadCipher(cs, + server_initial_secret, + serverKeys.key, serverKeys.iv, serverKeys.hp, + keyPhase); + writeCipher = QuicCipher.createWriteCipher(cs, + client_initial_secret, + clientKeys.key, clientKeys.iv, clientKeys.hp, + keyPhase); + } else { + readCipher = QuicCipher.createReadCipher(cs, + client_initial_secret, + clientKeys.key, clientKeys.iv, clientKeys.hp, + keyPhase); + writeCipher = QuicCipher.createWriteCipher(cs, + server_initial_secret, + serverKeys.key, serverKeys.iv, serverKeys.hp, + keyPhase); + } + final CipherPair old = this.cipherPair; + // we don't check if keys are already available, since it's a + // valid case where the INITIAL keys are regenerated due to a + // RETRY packet from the peer or even for the case where a + // different quic version was negotiated by the server + this.cipherPair = new CipherPair(readCipher, writeCipher); + if (old != null) { + old.discard(true); + } + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } finally { + KeyUtil.destroySecretKeys(initial_secret, client_initial_secret, + server_initial_secret); + } + } + + static Cipher getRetryCipher(final QuicVersion quicVersion, + final boolean incoming) throws QuicTransportException { + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + return tlsData.getRetryCipher(incoming); + } + } + + static final class HandshakeKeyManager extends QuicKeyManager { + private volatile CipherPair cipherPair; + + HandshakeKeyManager() { + super(HANDSHAKE); + } + + @Override + protected boolean keysAvailable() { + return this.cipherPair != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final CipherPair pair = this.cipherPair; + if (pair == null) { + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + return pair.writeCipher; + } + + @Override + void discardKeys() { + final CipherPair toDiscard = this.cipherPair; + this.cipherPair = null; // no longer needed + this.keysDiscarded = true; + if (toDiscard == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding keys (keyphase=" + + toDiscard.writeCipher.getKeyPhase() + + ") of " + this.keySpace + " key space"); + } + toDiscard.discard(true); + } + + void deriveKeys(final QuicVersion quicVersion, + final HandshakeContext handshakeContext, + final boolean clientMode) throws IOException { + Objects.requireNonNull(quicVersion); + if (keysAvailable()) { + throw new IllegalStateException( + "Keys already derived for " + this.keySpace + + " key space"); + } + SecretKey client_handshake_traffic_secret = null; + SecretKey server_handshake_traffic_secret = null; + try { + final SSLKeyDerivation kd = + handshakeContext.handshakeKeyDerivation; + client_handshake_traffic_secret = kd.deriveKey( + "TlsClientHandshakeTrafficSecret"); + final QuicKeys clientKeys = deriveQuicKeys(quicVersion, + handshakeContext.negotiatedCipherSuite, + client_handshake_traffic_secret); + server_handshake_traffic_secret = kd.deriveKey( + "TlsServerHandshakeTrafficSecret"); + final QuicKeys serverKeys = deriveQuicKeys(quicVersion, + handshakeContext.negotiatedCipherSuite, + server_handshake_traffic_secret); + + final CipherSuite negotiatedCipherSuite = + handshakeContext.negotiatedCipherSuite; + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + final int keyPhase = 0; + if (clientMode) { + readCipher = + QuicCipher.createReadCipher(negotiatedCipherSuite, + server_handshake_traffic_secret, + serverKeys.key, serverKeys.iv, + serverKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + client_handshake_traffic_secret, + clientKeys.key, clientKeys.iv, + clientKeys.hp, keyPhase); + } else { + readCipher = + QuicCipher.createReadCipher(negotiatedCipherSuite, + client_handshake_traffic_secret, + clientKeys.key, clientKeys.iv, + clientKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + server_handshake_traffic_secret, + serverKeys.key, serverKeys.iv, + serverKeys.hp, keyPhase); + } + synchronized (this) { + if (this.cipherPair != null) { + // don't allow setting more than once + throw new IllegalStateException("Keys already " + + "available for keyspace: " + + this.keySpace); + } + this.cipherPair = new CipherPair(readCipher, writeCipher); + } + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } finally { + KeyUtil.destroySecretKeys(client_handshake_traffic_secret, + server_handshake_traffic_secret); + } + } + } + + static final class OneRttKeyManager extends QuicKeyManager { + // a series of keys that the 1-RTT key manager uses + private record KeySeries(QuicReadCipher old, CipherPair current, + CipherPair next) { + private KeySeries { + Objects.requireNonNull(current); + if (old != null) { + if (old.getKeyPhase() == + current.writeCipher.getKeyPhase()) { + throw new IllegalArgumentException("Both old keys and" + + " current keys have the same key phase: " + + current.writeCipher.getKeyPhase()); + } + } + if (next != null) { + if (next.writeCipher.getKeyPhase() == + current.writeCipher.getKeyPhase()) { + throw new IllegalArgumentException("Both next keys " + + "and current keys have the same key phase: " + + current.writeCipher.getKeyPhase()); + } + } + } + + /** + * {@return true if this {@code KeySeries} has an old decryption key + * and the {@code pktNum} is lower than the least packet number the + * current decryption key has decrypted so far} + * + * @param pktNum the packet number for which the old key + * might be needed + */ + boolean canUseOldDecryptKey(final long pktNum) { + assert pktNum >= 0 : "unexpected packet number: " + pktNum; + if (this.old == null) { + return false; + } + final QuicReadCipher currentKey = this.current.readCipher; + final long lowestDecrypted = currentKey.lowestDecryptedPktNum(); + // if the incoming packet number is lesser than the lowest + // decrypted packet number by the current key, then it + // implies that this might be a delayed packet and thus is + // allowed to use the old key (if available) from + // the previous key phase. + // see RFC-9001, section 6.5 + if (lowestDecrypted == -1) { + return true; + } + return pktNum < lowestDecrypted; + } + } + + // will be set when the keys are derived + private volatile QuicVersion negotiatedVersion; + + private final Lock keySeriesLock = new ReentrantLock(); + // will be set when keys are derived and will + // be updated whenever keys are updated. + // Must be updated/written only + // when holding the keySeriesLock lock + private volatile KeySeries keySeries; + + @Stable + private volatile QuicOneRttContext oneRttContext; + + OneRttKeyManager() { + super(ONE_RTT); + } + + @Override + protected boolean keysAvailable() { + return this.keySeries != null && !this.keysDiscarded; + } + + @Override + protected QuicReadCipher getReadCipher() + throws QuicKeyUnavailableException { + final KeySeries series = requireKeySeries(); + return series.current.readCipher; + } + + @Override + protected QuicWriteCipher getWriteCipher() + throws QuicKeyUnavailableException { + final KeySeries series = requireKeySeries(); + return series.current.writeCipher; + } + + @Override + void discardKeys() { + this.keysDiscarded = true; + final KeySeries series; + this.keySeriesLock.lock(); + try { + series = this.keySeries; + this.keySeries = null; // no longer available + } finally { + this.keySeriesLock.unlock(); + } + if (series == null) { + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding key (series) of " + + this.keySpace + " key space"); + } + if (series.old != null) { + series.old.discard(false); + } + discardKeys(series.current); + discardKeys(series.next); + } + + @Override + void decryptPacket(final long packetNumber, final int keyPhase, + final ByteBuffer packet, final int headerLength, + final ByteBuffer output) throws QuicKeyUnavailableException, + QuicTransportException, AEADBadTagException, ShortBufferException { + if (keyPhase != 0 && keyPhase != 1) { + throw new IllegalArgumentException("Unexpected key phase " + + "value: " + keyPhase); + } + final KeySeries series = requireKeySeries(); + final CipherPair current = series.current; + // Use the write cipher's key phase to detect a key update as noted + // in RFC-9001, section 6.2: + // An endpoint detects a key update when processing a packet with + // a key phase that differs from the value used to protect the + // last packet it sent. + final int currentKeyPhase = current.writeCipher.getKeyPhase(); + if (keyPhase == currentKeyPhase) { + current.readCipher.decryptPacket(packetNumber, packet, + headerLength, output); + return; + } + // incoming packet is using a key phase which doesn't match the + // current key phase. this implies that either a key update + // is being initiated or a key update initiated by the current + // endpoint is in progress and some older packet with the + // previous key phase has arrived. + if (series.canUseOldDecryptKey(packetNumber)) { + final QuicReadCipher oldReadCipher = series.old; + assert oldReadCipher != null : "old key is unexpectedly null"; + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("using old read key to decrypt packet: " + + packetNumber + ", with incoming key phase: " + + keyPhase + ", current key phase: " + + currentKeyPhase); + } + oldReadCipher.decryptPacket( + packetNumber, packet, headerLength, output); + // we were able to decrypt using an old key. now verify + // that it was OK to use this old key for this packet. + if (!series.current.usedByBothEndpoints() + && series.current.writeCipher.hasEncryptedAny() + && oneRttContext.getLargestPeerAckedPN() + >= series.current.writeCipher.lowestEncryptedPktNum()) { + // RFC-9001, section 6.2: + // An endpoint that receives an acknowledgment that is + // carried in a packet protected with old keys where any + // acknowledged packet was protected with newer keys MAY + // treat that as a connection error of type + // KEY_UPDATE_ERROR. This indicates that a peer has + // received and acknowledged a packet that initiates a key + // update, but has not updated keys in response. + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("peer used incorrect key, was" + + " expected to use updated key of" + + " key phase: " + currentKeyPhase + + ", incoming key phase: " + keyPhase + + ", packet number: " + packetNumber); + } + throw new QuicTransportException("peer used incorrect" + + " key, was expected to use updated key", + this.keySpace, 0, KEY_UPDATE_ERROR); + } + return; + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("detected ONE_RTT key update, current key " + + "phase: " + currentKeyPhase + + ", incoming key phase: " + keyPhase + + ", packet number: " + packetNumber); + } + decryptUsingNextKeys( + series, packetNumber, packet, headerLength, output); + } + + @Override + void encryptPacket(final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, + final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + KeySeries currentSeries = requireKeySeries(); + if (currentSeries.next == null) { + // next keys haven't yet been generated, + // generate them now + try { + currentSeries = generateNextKeys( + this.negotiatedVersion, currentSeries); + } catch (GeneralSecurityException | IOException e) { + throw new QuicTransportException("Failed to update keys", + ONE_RTT, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + maybeInitiateKeyUpdate(currentSeries, packetNumber); + // call getWriteCipher() afresh so that it can use + // the new keyseries if at all the key update was + // initiated + final QuicWriteCipher writeCipher = getWriteCipher(); + final int keyPhase = writeCipher.getKeyPhase(); + // generate the packet header passing the generator the key phase + final ByteBuffer header = headerGenerator.apply(keyPhase); + writeCipher.encryptPacket(packetNumber, header, packetPayload, output); + } + + void setOneRttContext(final QuicOneRttContext ctx) { + Objects.requireNonNull(ctx); + this.oneRttContext = ctx; + } + + private KeySeries requireKeySeries() + throws QuicKeyUnavailableException { + final KeySeries series = this.keySeries; + if (series != null) { + return series; + } + final String msg = this.keysDiscarded + ? "Keys have been discarded" + : "Keys not available"; + throw new QuicKeyUnavailableException(msg, this.keySpace); + } + + // based on certain internal criteria, this method may trigger a key + // update. + // returns true if it does trigger the key update. false otherwise. + private boolean maybeInitiateKeyUpdate(final KeySeries currentSeries, + final long packetNumber) { + final QuicWriteCipher cipher = currentSeries.current.writeCipher; + // when we notice that we have reached 80% (which is arbitrary) + // of the confidentiality limit, we trigger a key update instead + // of waiting to hit the limit + final long confidentialityLimit = cipher.confidentialityLimit(); + if (confidentialityLimit < 0) { + return false; + } + final long numEncrypted = cipher.getNumEncrypted(); + if (numEncrypted >= 0.8 * confidentialityLimit) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("about to reach confidentiality limit, " + + "attempting to initiate a 1-RTT key update," + + " packet number: " + + packetNumber + ", current key phase: " + + cipher.getKeyPhase()); + } + final boolean initiated = initiateKeyUpdate(currentSeries); + if (initiated) { + final int newKeyPhase = + this.keySeries.current.writeCipher.getKeyPhase(); + assert cipher.getKeyPhase() != newKeyPhase + : "key phase of updated key unexpectedly matches " + + "the key phase " + + cipher.getKeyPhase() + " of current keys"; + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "1-RTT key update initiated, new key phase: " + + newKeyPhase); + } + } + return initiated; + } + return false; + } + + private boolean initiateKeyUpdate(final KeySeries series) { + // we only initiate a key update if this current endpoint and the + // peer have both been using this current key + if (!series.current.usedByBothEndpoints()) { + // RFC-9001, section 6.1: + // An endpoint MUST NOT initiate a subsequent key update + // unless it has received + // an acknowledgment for a packet that was sent protected + // with keys from the + // current key phase. This ensures that keys are + // available to both peers before + // another key update can be initiated. + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "skipping key update initiation because peer " + + "hasn't yet sent us a packet encrypted with " + + "current key of key phase: " + + series.current.readCipher.getKeyPhase()); + } + return false; + } + // OK to initiate a key update. + // An endpoint initiates a key update by updating its packet + // protection write secret + // and using that to protect new packets. + rolloverKeys(this.negotiatedVersion, series); + return true; + } + + private static void discardKeys(final CipherPair cipherPair) { + if (cipherPair == null) { + return; + } + cipherPair.discard(true); + } + + /** + * uses "next" keys to try and decrypt the incoming packet. if that + * succeeded then it implies that the key update was indeed initiated by + * the peer and this method then rolls over the keys to start using + * these "next" keys. this method then returns true in such cases. if + * the packet decryption using the "next" key fails, then this method + * just returns back false (and doesn't roll over the keys) + */ + private void decryptUsingNextKeys( + final KeySeries currentKeySeries, + final long packetNumber, + final ByteBuffer packet, + final int headerLength, + final ByteBuffer output) + throws QuicKeyUnavailableException, AEADBadTagException, + ShortBufferException, QuicTransportException { + if (currentKeySeries.next == null) { + // this can happen if the peer initiated another + // key update before we could generate the next + // keys during our encryption flow. in such + // cases we reject the key update for the packet + // (we avoid timing attacks by not generating + // keys during decryption, our key generation + // only happens during encryption) + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("next keys unavailable," + + " won't decrypt a packet which appears to be" + + " a key update"); + } + throw new QuicKeyUnavailableException( + "next keys unavailable to handle key update", + this.keySpace); + } + // use the next keys to attempt decrypting + currentKeySeries.next.readCipher.decryptPacket(packetNumber, packet, + headerLength, output); + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "decrypted using next keys for peer-initiated" + + " key update; will now switch to new key phase: " + + currentKeySeries.next.readCipher.getKeyPhase()); + } + // we have successfully decrypted the packet using the new/next + // read key. So we now update even the write key as noted in + // RFC-9001, section 6.2: + // If a packet is successfully processed using the next key and + // IV, then the peer has initiated a key update. The endpoint + // MUST update its send keys to the corresponding + // key phase in response, as described in Section 6.1. Sending + // keys MUST be updated before sending an acknowledgment for the + // packet that was received with updated keys. rollover the + // keys == old gets discarded and is replaced by + // current, current is replaced by next and next is set to null + // (a new set of next keys will be generated separately on + // a schedule) + rolloverKeys(this.negotiatedVersion, currentKeySeries); + } + + void deriveKeys(final QuicVersion negotiatedVersion, + final HandshakeContext handshakeContext, + final boolean clientMode) throws IOException { + Objects.requireNonNull(negotiatedVersion); + if (keysAvailable()) { + throw new IllegalStateException("Keys already derived for " + + this.keySpace + " key space"); + } + this.negotiatedVersion = negotiatedVersion; + + try { + SSLKeyDerivation kd = handshakeContext.handshakeKeyDerivation; + SecretKey client_application_traffic_secret_0 = kd.deriveKey( + "TlsClientAppTrafficSecret"); + SecretKey server_application_traffic_secret_0 = kd.deriveKey( + "TlsServerAppTrafficSecret"); + + deriveOneRttKeys(this.negotiatedVersion, + client_application_traffic_secret_0, + server_application_traffic_secret_0, + handshakeContext.negotiatedCipherSuite, + clientMode); + } catch (GeneralSecurityException e) { + throw new SSLException("Missing cipher algorithm", e); + } + } + + void deriveOneRttKeys(final QuicVersion version, + final SecretKey client_application_traffic_secret_0, + final SecretKey server_application_traffic_secret_0, + final CipherSuite negotiatedCipherSuite, + final boolean clientMode) throws IOException, + GeneralSecurityException { + final QuicKeys clientKeys = deriveQuicKeys(version, + negotiatedCipherSuite, + client_application_traffic_secret_0); + final QuicKeys serverKeys = deriveQuicKeys(version, + negotiatedCipherSuite, + server_application_traffic_secret_0); + final QuicReadCipher readCipher; + final QuicWriteCipher writeCipher; + // this method always derives the first key for the 1-RTT, so key + // phase is always 0 + final int keyPhase = 0; + if (clientMode) { + readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite, + server_application_traffic_secret_0, serverKeys.key, + serverKeys.iv, serverKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + client_application_traffic_secret_0, clientKeys.key, + clientKeys.iv, clientKeys.hp, keyPhase); + } else { + readCipher = QuicCipher.createReadCipher(negotiatedCipherSuite, + client_application_traffic_secret_0, clientKeys.key, + clientKeys.iv, clientKeys.hp, keyPhase); + writeCipher = + QuicCipher.createWriteCipher(negotiatedCipherSuite, + server_application_traffic_secret_0, serverKeys.key, + serverKeys.iv, serverKeys.hp, keyPhase); + } + // generate the next set of keys beforehand to prevent any timing + // attacks + // during key update + final QuicReadCipher nPlus1ReadCipher = + generateNextReadCipher(version, readCipher); + final QuicWriteCipher nPlus1WriteCipher = + generateNextWriteCipher(version, writeCipher); + this.keySeriesLock.lock(); + try { + if (this.keySeries != null) { + // don't allow deriving the first set of 1-RTT keys more + // than once + throw new IllegalStateException("Keys already available " + + "for keyspace: " + + this.keySpace); + } + this.keySeries = new KeySeries(null, + new CipherPair(readCipher, writeCipher), + new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher)); + } finally { + this.keySeriesLock.unlock(); + } + } + + private static QuicWriteCipher generateNextWriteCipher( + final QuicVersion quicVersion, final QuicWriteCipher current) + throws IOException, GeneralSecurityException { + final SSLKeyDerivation kd = + new QuicTLSKeyDerivation(current.getCipherSuite(), + current.getBaseSecret()); + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + final SecretKey nplus1Secret = + kd.deriveKey(tlsData.getTlsKeyUpdateLabel()); + final QuicKeys quicKeys = + QuicKeyManager.deriveQuicKeys(quicVersion, + current.getCipherSuite(), + nplus1Secret, current.getHeaderProtectionKey()); + final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0; + // toggle the 1 bit keyphase + final QuicWriteCipher next = + QuicCipher.createWriteCipher(current.getCipherSuite(), + nplus1Secret, quicKeys.key, quicKeys.iv, + quicKeys.hp, nextKeyPhase); + return next; + } + + private static QuicReadCipher generateNextReadCipher( + final QuicVersion quicVersion, final QuicReadCipher current) + throws IOException, GeneralSecurityException { + final SSLKeyDerivation kd = + new QuicTLSKeyDerivation(current.getCipherSuite(), + current.getBaseSecret()); + final QuicTLSData tlsData = QuicKeyManager.getQuicData(quicVersion); + final SecretKey nPlus1Secret = + kd.deriveKey(tlsData.getTlsKeyUpdateLabel()); + final QuicKeys quicKeys = + QuicKeyManager.deriveQuicKeys(quicVersion, + current.getCipherSuite(), + nPlus1Secret, current.getHeaderProtectionKey()); + final int nextKeyPhase = current.getKeyPhase() == 0 ? 1 : 0; + // toggle the 1 bit keyphase + final QuicReadCipher next = + QuicCipher.createReadCipher(current.getCipherSuite(), + nPlus1Secret, quicKeys.key, + quicKeys.iv, quicKeys.hp, nextKeyPhase); + return next; + } + + private KeySeries generateNextKeys(final QuicVersion version, + final KeySeries currentSeries) + throws GeneralSecurityException, IOException { + this.keySeriesLock.lock(); + try { + // nothing to do if some other thread + // already changed the keySeries + if (this.keySeries != currentSeries) { + return this.keySeries; + } + final QuicReadCipher nPlus1ReadCipher = + generateNextReadCipher(version, + currentSeries.current.readCipher); + final QuicWriteCipher nPlus1WriteCipher = + generateNextWriteCipher(version, + currentSeries.current.writeCipher); + // only the next keys will differ in the new series + // as compared to the current series + final KeySeries newSeries = new KeySeries(currentSeries.old, + currentSeries.current, + new CipherPair(nPlus1ReadCipher, nPlus1WriteCipher)); + this.keySeries = newSeries; + return newSeries; + } finally { + this.keySeriesLock.unlock(); + } + } + + /** + * Updates the key series by "left shifting" the series of keys. + * i.e. old keys (if any) are discarded, current keys + * are moved to old keys and next keys are moved to current keys. + * Note that no new keys will be generated by this method. + * @return the key series that will be in use going forward + */ + private KeySeries rolloverKeys(final QuicVersion version, + final KeySeries currentSeries) { + this.keySeriesLock.lock(); + try { + // nothing to do if some other thread + // already changed the keySeries + if (this.keySeries != currentSeries) { + return this.keySeries; + } + assert currentSeries.next != null : "Key series missing next" + + " keys"; + // discard the old read cipher which will no longer be used + final QuicReadCipher oldReadCipher = currentSeries.old; + // once we move current key to old, we won't be using the + // write cipher of that + // moved pair + final QuicWriteCipher writeCipherToDiscard = + currentSeries.current.writeCipher; + final KeySeries newSeries = new KeySeries( + currentSeries.current.readCipher, currentSeries.next, + null); + // update the key series + this.keySeries = newSeries; + if (oldReadCipher != null) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest( + "discarding old read key of key phase: " + + oldReadCipher.getKeyPhase()); + } + oldReadCipher.discard(false); + } + if (SSLLogger.isOn && SSLLogger.isOn("ssl")) { + SSLLogger.finest("discarding write key of key phase: " + + writeCipherToDiscard.getKeyPhase()); + } + writeCipherToDiscard.discard(false); + return newSeries; + } finally { + this.keySeriesLock.unlock(); + } + } + } + + private static final class QuicTLSKeyDerivation + implements SSLKeyDerivation { + + private enum HkdfLabel { + // RFC 9001: quic version 1 + quickey("quic key"), + quiciv("quic iv"), + quichp("quic hp"), + quicku("quic ku"), + + // RFC 9369: quic version 2 + quicv2key("quicv2 key"), + quicv2iv("quicv2 iv"), + quicv2hp("quicv2 hp"), + quicv2ku("quicv2 ku"); + + private final String label; + private final byte[] tls13LabelBytes; + + HkdfLabel(final String label) { + Objects.requireNonNull(label); + this.label = label; + this.tls13LabelBytes = + ("tls13 " + label).getBytes(StandardCharsets.UTF_8); + } + + private static HkdfLabel fromLabel(final String label) { + Objects.requireNonNull(label); + for (final HkdfLabel hkdfLabel : HkdfLabel.values()) { + if (hkdfLabel.label.equals(label)) { + return hkdfLabel; + } + } + throw new IllegalArgumentException( + "unrecognized label: " + label); + } + } + + private final CipherSuite cs; + private final SecretKey secret; + + private QuicTLSKeyDerivation(final CipherSuite cs, + final SecretKey secret) { + this.cs = Objects.requireNonNull(cs); + this.secret = Objects.requireNonNull(secret); + } + + @Override + public SecretKey deriveKey(final String algorithm) throws IOException { + final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm); + try { + final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm); + final int keyLength = getKeyLength(hkdfLabel); + final byte[] hkdfInfo = + createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength); + final String keyAlgo = getKeyAlgorithm(hkdfLabel); + return hkdf.deriveKey(keyAlgo, + HKDFParameterSpec.expandOnly( + secret, hkdfInfo, keyLength)); + } catch (GeneralSecurityException gse) { + throw new SSLHandshakeException("Could not derive key", gse); + } + } + + @Override + public byte[] deriveData(final String algorithm) throws IOException { + final HkdfLabel hkdfLabel = HkdfLabel.fromLabel(algorithm); + try { + final KDF hkdf = KDF.getInstance(this.cs.hashAlg.hkdfAlgorithm); + final int keyLength = getKeyLength(hkdfLabel); + final byte[] hkdfInfo = + createHkdfInfo(hkdfLabel.tls13LabelBytes, keyLength); + return hkdf.deriveData(HKDFParameterSpec.expandOnly( + secret, hkdfInfo, keyLength)); + } catch (GeneralSecurityException gse) { + throw new SSLHandshakeException("Could not derive key", gse); + } + } + + private int getKeyLength(final HkdfLabel hkdfLabel) { + return switch (hkdfLabel) { + case quicku, quicv2ku -> { + // RFC-9001, section 6.1: + // secret_ = HKDF-Expand-Label(secret_, "quic + // ku", "", Hash.length) + yield this.cs.hashAlg.hashLength; + } + case quiciv, quicv2iv -> this.cs.bulkCipher.ivSize; + default -> this.cs.bulkCipher.keySize; + }; + } + + private String getKeyAlgorithm(final HkdfLabel hkdfLabel) { + return switch (hkdfLabel) { + case quicku, quicv2ku -> "TlsUpdateNplus1"; + case quiciv, quicv2iv -> + throw new IllegalArgumentException("IV not expected"); + default -> this.cs.bulkCipher.algorithm; + }; + } + } + + private enum QuicTLSData { + V1("38762cf7f55934b34d179ae6a4c80cadccbb7f0a", + "be0c690b9f66575a1d766b54e368c84e", + "461599d35d632bf2239825bb", + "quic key", "quic iv", "quic hp", "quic ku"), + V2("0dede3def700a6db819381be6e269dcbf9bd2ed9", + "8fb4b01b56ac48e260fbcbcead7ccc92", + "d86969bc2d7c6d9990efb04a", + "quicv2 key", "quicv2 iv", "quicv2 hp", "quicv2 ku"); + + private final byte[] initialSalt; + private final SecretKey retryKey; + private final GCMParameterSpec retryIvSpec; + private final String keyLabel; + private final String ivLabel; + private final String hpLabel; + private final String kuLabel; + + QuicTLSData(String initialSalt, String retryKey, String retryIv, + String keyLabel, String ivLabel, String hpLabel, + String kuLabel) { + this.initialSalt = HexFormat.of() + .parseHex(initialSalt); + this.retryKey = new SecretKeySpec(HexFormat.of() + .parseHex(retryKey), "AES"); + retryIvSpec = new GCMParameterSpec(128, + HexFormat.of().parseHex(retryIv)); + this.keyLabel = keyLabel; + this.ivLabel = ivLabel; + this.hpLabel = hpLabel; + this.kuLabel = kuLabel; + } + + public byte[] getInitialSalt() { + return initialSalt; + } + + public Cipher getRetryCipher(boolean incoming) throws QuicTransportException { + Cipher retryCipher = null; + try { + retryCipher = Cipher.getInstance("AES/GCM/NoPadding"); + retryCipher.init(incoming ? Cipher.DECRYPT_MODE : + Cipher.ENCRYPT_MODE, + retryKey, retryIvSpec); + } catch (Exception e) { + throw new QuicTransportException("Cipher not available", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + return retryCipher; + } + + public String getTlsKeyLabel() { + return keyLabel; + } + + public String getTlsIvLabel() { + return ivLabel; + } + + public String getTlsHpLabel() { + return hpLabel; + } + + public String getTlsKeyUpdateLabel() { + return kuLabel; + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java new file mode 100644 index 00000000000..6765f554fcc --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicTLSEngineImpl.java @@ -0,0 +1,893 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.security.ssl; + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.quic.QuicVersion; +import sun.security.ssl.QuicKeyManager.HandshakeKeyManager; +import sun.security.ssl.QuicKeyManager.InitialKeyManager; +import sun.security.ssl.QuicKeyManager.OneRttKeyManager; + +import javax.crypto.AEADBadTagException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.security.AlgorithmConstraints; +import java.security.GeneralSecurityException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntFunction; + +import static jdk.internal.net.quic.QuicTLSEngine.HandshakeState.*; +import static jdk.internal.net.quic.QuicTLSEngine.KeySpace.*; + +/** + * One instance per QUIC connection. Configuration methods similar to + * SSLEngine. + *

        + * The implementation of this class uses the {@link QuicKeyManager} to maintain + * all state relating to keys for each encryption levels. + */ +public final class QuicTLSEngineImpl implements QuicTLSEngine, SSLTransport { + + private static final Map messageTypeMap = + Map.of(SSLHandshake.CLIENT_HELLO.id, INITIAL, + SSLHandshake.SERVER_HELLO.id, INITIAL, + SSLHandshake.ENCRYPTED_EXTENSIONS.id, HANDSHAKE, + SSLHandshake.CERTIFICATE_REQUEST.id, HANDSHAKE, + SSLHandshake.CERTIFICATE.id, HANDSHAKE, + SSLHandshake.CERTIFICATE_VERIFY.id, HANDSHAKE, + SSLHandshake.FINISHED.id, HANDSHAKE, + SSLHandshake.NEW_SESSION_TICKET.id, ONE_RTT); + static final long BASE_CRYPTO_ERROR = 256; + + private static final Set SUPPORTED_QUIC_VERSIONS = + Set.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2); + + // VarHandles are used to access compareAndSet semantics. + private static final VarHandle HANDSHAKE_STATE_HANDLE; + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + final Class quicTlsEngineImpl = QuicTLSEngineImpl.class; + HANDSHAKE_STATE_HANDLE = lookup.findVarHandle( + quicTlsEngineImpl, + "handshakeState", + HandshakeState.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private final TransportContext conContext; + private final String peerHost; + private final int peerPort; + private volatile HandshakeState handshakeState; + private volatile KeySpace sendKeySpace; + // next message to send or receive + private volatile ByteBuffer localQuicTransportParameters; + private volatile QuicTransportParametersConsumer + remoteQuicTransportParametersConsumer; + + // keymanagers for individual keyspaces + private final InitialKeyManager initialKeyManager = new InitialKeyManager(); + private final HandshakeKeyManager handshakeKeyManager = + new HandshakeKeyManager(); + private final OneRttKeyManager oneRttKeyManager = new OneRttKeyManager(); + + // buffer for crypto data that was received but not yet processed (i.e. + // incomplete messages) + private volatile ByteBuffer incomingCryptoBuffer; + // key space for incomingCryptoBuffer + private volatile KeySpace incomingCryptoSpace; + + private volatile QuicVersion negotiatedVersion; + + public QuicTLSEngineImpl(SSLContextImpl sslContextImpl) { + this(sslContextImpl, null, -1); + } + + public QuicTLSEngineImpl(SSLContextImpl sslContextImpl, final String peerHost, final int peerPort) { + this.peerHost = peerHost; + this.peerPort = peerPort; + this.sendKeySpace = INITIAL; + HandshakeHash handshakeHash = new HandshakeHash(); + this.conContext = new TransportContext(sslContextImpl, this, + new SSLEngineInputRecord(handshakeHash), + new QuicEngineOutputRecord(handshakeHash)); + conContext.sslConfig.enabledProtocols = List.of(ProtocolVersion.TLS13); + if (peerHost != null) { + conContext.sslConfig.serverNames = + Utilities.addToSNIServerNameList( + conContext.sslConfig.serverNames, peerHost); + } + conContext.setQuic(true); + } + + @Override + public void setUseClientMode(boolean mode) { + conContext.setUseClientMode(mode); + this.handshakeState = mode + ? HandshakeState.NEED_SEND_CRYPTO + : HandshakeState.NEED_RECV_CRYPTO; + } + + @Override + public boolean getUseClientMode() { + return conContext.sslConfig.isClientMode; + } + + @Override + public void setSSLParameters(final SSLParameters params) { + Objects.requireNonNull(params); + // section, 4.2 of RFC-9001 + // Clients MUST NOT offer TLS versions older than 1.3 + final String[] protos = params.getProtocols(); + if (protos == null || protos.length == 0) { + throw new IllegalArgumentException("No TLS protocols set"); + } + boolean tlsv13Present = false; + Set unsupported = new HashSet<>(); + for (String p : protos) { + if ("TLSv1.3".equals(p)) { + tlsv13Present = true; + } else { + unsupported.add(p); + } + } + if (!tlsv13Present) { + throw new IllegalArgumentException( + "required TLSv1.3 protocol version hasn't been set"); + } + if (!unsupported.isEmpty()) { + throw new IllegalArgumentException( + "Unsupported TLS protocol versions " + unsupported); + } + conContext.sslConfig.setSSLParameters(params); + } + + @Override + public SSLSession getSession() { + return conContext.conSession; + } + + @Override + public SSLSession getHandshakeSession() { + final HandshakeContext handshakeContext = conContext.handshakeContext; + return handshakeContext == null + ? null + : handshakeContext.handshakeSession; + } + + /** + * {@return the {@link AlgorithmConstraints} that are applicable for this engine, + * or null if none are applicable} + */ + AlgorithmConstraints getAlgorithmConstraints() { + final HandshakeContext handshakeContext = conContext.handshakeContext; + // if we are handshaking then use the handshake context + // to determine the constraints, else use the configured + // SSLParameters + return handshakeContext == null + ? getSSLParameters().getAlgorithmConstraints() + : handshakeContext.sslConfig.userSpecifiedAlgorithmConstraints; + } + + @Override + public SSLParameters getSSLParameters() { + return conContext.sslConfig.getSSLParameters(); + } + + @Override + public String getApplicationProtocol() { + // TODO: review thread safety when dealing with conContext + return conContext.applicationProtocol; + } + + @Override + public Set getSupportedQuicVersions() { + return SUPPORTED_QUIC_VERSIONS; + } + + @Override + public void setOneRttContext(final QuicOneRttContext ctx) { + this.oneRttKeyManager.setOneRttContext(ctx); + } + + private QuicVersion getNegotiatedVersion() { + final QuicVersion negotiated = this.negotiatedVersion; + if (negotiated == null) { + throw new IllegalStateException( + "Quic version hasn't been negotiated yet"); + } + return negotiated; + } + + private boolean isEnabled(final QuicVersion quicVersion) { + final Set enabled = getSupportedQuicVersions(); + if (enabled == null) { + return false; + } + return enabled.contains(quicVersion); + } + + /** + * Returns the current handshake state of the connection. Sometimes packets + * that could be decrypted can be received before the handshake has + * completed, but should not be decrypted until it is complete + * + * @return the HandshakeState + */ + @Override + public HandshakeState getHandshakeState() { + return handshakeState; + } + + /** + * Returns the current sending key space (encryption level) + * + * @return the current sending key space + */ + @Override + public KeySpace getCurrentSendKeySpace() { + return sendKeySpace; + } + + @Override + public boolean keysAvailable(KeySpace keySpace) { + return switch (keySpace) { + case INITIAL -> this.initialKeyManager.keysAvailable(); + case HANDSHAKE -> this.handshakeKeyManager.keysAvailable(); + case ONE_RTT -> this.oneRttKeyManager.keysAvailable(); + case ZERO_RTT -> false; + case RETRY -> true; + default -> throw new IllegalArgumentException( + keySpace + " not expected here"); + }; + } + + @Override + public void discardKeys(KeySpace keySpace) { + switch (keySpace) { + case INITIAL -> this.initialKeyManager.discardKeys(); + case HANDSHAKE -> this.handshakeKeyManager.discardKeys(); + case ONE_RTT -> this.oneRttKeyManager.discardKeys(); + default -> throw new IllegalArgumentException( + "key discarding not implemented for " + keySpace); + } + } + + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return switch (keySpace) { + case INITIAL, HANDSHAKE, ZERO_RTT, ONE_RTT -> 16; + default -> throw new IllegalArgumentException( + "Type '" + keySpace + "' not expected here"); + }; + } + + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, + boolean incoming, ByteBuffer sample) + throws QuicKeyUnavailableException, QuicTransportException { + final QuicKeyManager keyManager = keyManager(keySpace); + if (incoming) { + final QuicCipher.QuicReadCipher quicCipher = + keyManager.getReadCipher(); + return quicCipher.computeHeaderProtectionMask(sample); + } else { + final QuicCipher.QuicWriteCipher quicCipher = + keyManager.getWriteCipher(); + return quicCipher.computeHeaderProtectionMask(sample); + } + } + + @Override + public int getAuthTagSize() { + // RFC-9001, section 5.3 + // QUIC can use any of the cipher suites defined in [TLS13] with the + // exception of TLS_AES_128_CCM_8_SHA256. ... + // These cipher suites have a 16-byte authentication tag and produce + // an output 16 bytes larger than their input. + return 16; + } + + @Override + public void encryptPacket(final KeySpace keySpace, final long packetNumber, + final IntFunction headerGenerator, + final ByteBuffer packetPayload, final ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException, ShortBufferException { + final QuicKeyManager keyManager = keyManager(keySpace); + keyManager.encryptPacket(packetNumber, headerGenerator, packetPayload, output); + } + + @Override + public void decryptPacket(final KeySpace keySpace, + final long packetNumber, final int keyPhase, + final ByteBuffer packet, final int headerLength, + final ByteBuffer output) + throws QuicKeyUnavailableException, AEADBadTagException, + QuicTransportException, ShortBufferException { + if (keySpace == ONE_RTT && !isTLSHandshakeComplete()) { + // RFC-9001, section 5.7 specifies that the server or the client MUST NOT + // decrypt 1-RTT packets, even if 1-RTT keys are available, before the + // TLS handshake is complete. + throw new QuicKeyUnavailableException("QUIC TLS handshake not yet complete", ONE_RTT); + } + final QuicKeyManager keyManager = keyManager(keySpace); + keyManager.decryptPacket(packetNumber, keyPhase, packet, headerLength, + output); + } + + @Override + public void signRetryPacket(final QuicVersion quicVersion, + final ByteBuffer originalConnectionId, final ByteBuffer packet, + final ByteBuffer output) + throws ShortBufferException, QuicTransportException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException( + "Quic version " + quicVersion + " isn't enabled"); + } + int connIdLength = originalConnectionId.remaining(); + if (connIdLength >= 256 || connIdLength < 0) { + throw new IllegalArgumentException("connection ID length"); + } + final Cipher cipher = InitialKeyManager.getRetryCipher( + quicVersion, false); + cipher.updateAAD(new byte[]{(byte) connIdLength}); + cipher.updateAAD(originalConnectionId); + cipher.updateAAD(packet); + try { + // No data to encrypt, just outputting the tag which will be + // verified later. + cipher.doFinal(ByteBuffer.allocate(0), output); + } catch (ShortBufferException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Failed to sign packet", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + + @Override + public void verifyRetryPacket(final QuicVersion quicVersion, + final ByteBuffer originalConnectionId, + final ByteBuffer packet) + throws AEADBadTagException, QuicTransportException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException( + "Quic version " + quicVersion + " isn't enabled"); + } + int connIdLength = originalConnectionId.remaining(); + if (connIdLength >= 256 || connIdLength < 0) { + throw new IllegalArgumentException("connection ID length"); + } + int originalLimit = packet.limit(); + packet.limit(originalLimit - 16); + final Cipher cipher = + InitialKeyManager.getRetryCipher(quicVersion, true); + cipher.updateAAD(new byte[]{(byte) connIdLength}); + cipher.updateAAD(originalConnectionId); + cipher.updateAAD(packet); + packet.limit(originalLimit); + try { + assert packet.remaining() == 16; + int outBufLength = cipher.getOutputSize(packet.remaining()); + // No data to decrypt, just checking the tag. + ByteBuffer outBuffer = ByteBuffer.allocate(outBufLength); + cipher.doFinal(packet, outBuffer); + assert outBuffer.position() == 0; + } catch (AEADBadTagException e) { + throw e; + } catch (Exception e) { + throw new QuicTransportException("Failed to verify packet", + null, 0, BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + } + + private QuicKeyManager keyManager(final KeySpace keySpace) { + return switch (keySpace) { + case INITIAL -> this.initialKeyManager; + case HANDSHAKE -> this.handshakeKeyManager; + case ONE_RTT -> this.oneRttKeyManager; + default -> throw new IllegalArgumentException( + "No key manager available for key space: " + keySpace); + }; + } + + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) throws IOException { + if (keySpace != sendKeySpace) { + throw new IllegalStateException("Unexpected key space: " + + keySpace + " (expected " + sendKeySpace + ")"); + } + if (handshakeState == HandshakeState.NEED_SEND_CRYPTO || + !conContext.outputRecord.isEmpty()) { // session ticket + byte[] bytes = produceNextHandshakeMessage(); + return ByteBuffer.wrap(bytes); + } else { + return null; + } + } + + private byte[] produceNextHandshakeMessage() throws IOException { + if (!conContext.isNegotiated && !conContext.isBroken && + !conContext.isInboundClosed() && + !conContext.isOutboundClosed()) { + conContext.kickstart(); + } + byte[] message = conContext.outputRecord.getHandshakeMessage(); + if (handshakeState == NEED_SEND_CRYPTO) { + if (conContext.outputRecord.isEmpty()) { + if (conContext.isNegotiated) { + // client, done + handshakeState = NEED_RECV_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + handshakeState = NEED_RECV_CRYPTO; + } + } else if (sendKeySpace == INITIAL && !getUseClientMode()) { + // Server sends handshake messages immediately after + // the initial server hello. Need to check the next key space. + sendKeySpace = conContext.outputRecord.getHandshakeMessageKeySpace(); + } + } else { + assert conContext.isNegotiated; + } + return message; + } + + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) + throws QuicTransportException { + if (!payload.hasRemaining()) { + throw new IllegalArgumentException("Empty crypto buffer"); + } + if (keySpace == KeySpace.ZERO_RTT) { + throw new IllegalArgumentException("Crypto in zero-rtt"); + } + if (incomingCryptoSpace != null && incomingCryptoSpace != keySpace) { + throw new QuicTransportException("Unexpected message", null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException( + "Unfinished message in " + incomingCryptoSpace)); + } + try { + if (!conContext.isNegotiated && !conContext.isBroken && + !conContext.isInboundClosed() && + !conContext.isOutboundClosed()) { + conContext.kickstart(); + } + } catch (IOException e) { + throw new QuicTransportException(e.toString(), null, 0, + BASE_CRYPTO_ERROR + Alert.INTERNAL_ERROR.id, e); + } + // previously unconsumed bytes in incomingCryptoBuffer, new bytes in + // payload. if incomingCryptoBuffer is not null, it's either 4 bytes + // or large enough to hold the entire message. + while (payload.hasRemaining()) { + if (keySpace != KeySpace.ONE_RTT && + handshakeState != HandshakeState.NEED_RECV_CRYPTO) { + // in one-rtt we may receive session tickets at any time; + // during handshake we're either sending or receiving + throw new QuicTransportException("Unexpected message", null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException( + "Not expecting a handshake message, state: " + + handshakeState)); + } + if (incomingCryptoBuffer != null) { + // message type validated already; pump more bytes + if (payload.remaining() <= incomingCryptoBuffer.remaining()) { + incomingCryptoBuffer.put(payload); + } else { + // more than one message in buffer, or we don't have a + // header yet + int remaining = incomingCryptoBuffer.remaining(); + incomingCryptoBuffer.put(incomingCryptoBuffer.position(), + payload, payload.position(), remaining); + incomingCryptoBuffer.position(incomingCryptoBuffer.limit()); + payload.position(payload.position() + remaining); + if (incomingCryptoBuffer.capacity() == 4) { + // small buffer for header only; retrieve size and + // expand if necessary + int messageSize = + ((incomingCryptoBuffer.get(1) & 0xFF) << 16) | + ((incomingCryptoBuffer.get(2) & 0xFF) << 8) | + (incomingCryptoBuffer.get(3) & 0xFF); + if (messageSize != 0) { + if (messageSize > SSLConfiguration.maxHandshakeMessageSize) { + throw new QuicTransportException( + "The size of the handshake message (" + + messageSize + + ") exceeds the maximum allowed size (" + + SSLConfiguration.maxHandshakeMessageSize + + ")", + null, 0, + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + ByteBuffer newBuffer = + ByteBuffer.allocate(messageSize + 4); + incomingCryptoBuffer.flip(); + newBuffer.put(incomingCryptoBuffer); + incomingCryptoBuffer = newBuffer; + assert incomingCryptoBuffer.position() == 4 : + incomingCryptoBuffer.position(); + // start over with larger buffer + continue; + } + // message size was zero... can it really happen? + } + } + } else { + // incoming crypto buffer is null. Validate message type, + // check if size is available + byte messageType = payload.get(payload.position()); + if (SSLLogger.isOn) { + SSLLogger.fine("Received message of type 0x" + + Integer.toHexString(messageType & 0xFF)); + } + KeySpace expected = messageTypeMap.get(messageType); + if (expected != keySpace) { + throw new QuicTransportException("Unexpected message", + null, 0, + BASE_CRYPTO_ERROR + Alert.UNEXPECTED_MESSAGE.id, + new SSLHandshakeException("Message " + messageType + + " received in " + keySpace + + " but should be " + expected)); + } + if (payload.remaining() < 4) { + // partial message, length missing. Store in + // incomingCryptoBuffer + incomingCryptoBuffer = ByteBuffer.allocate(4); + incomingCryptoBuffer.put(payload); + incomingCryptoSpace = keySpace; + return; + } + int payloadPos = payload.position(); + int messageSize = ((payload.get(payloadPos + 1) & 0xFF) << 16) + | ((payload.get(payloadPos + 2) & 0xFF) << 8) + | (payload.get(payloadPos + 3) & 0xFF); + if (payload.remaining() < messageSize + 4) { + // partial message, length known. Store in + // incomingCryptoBuffer + if (messageSize > SSLConfiguration.maxHandshakeMessageSize) { + throw new QuicTransportException( + "The size of the handshake message (" + + messageSize + + ") exceeds the maximum allowed size (" + + SSLConfiguration.maxHandshakeMessageSize + + ")", + null, 0, + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + incomingCryptoBuffer = ByteBuffer.allocate(messageSize + 4); + incomingCryptoBuffer.put(payload); + incomingCryptoSpace = keySpace; + return; + } + incomingCryptoSpace = keySpace; + incomingCryptoBuffer = payload.slice(payloadPos, + messageSize + 4); + // set position at end to indicate that the buffer is ready + // for processing + incomingCryptoBuffer.position(messageSize + 4); + assert !incomingCryptoBuffer.hasRemaining() : + incomingCryptoBuffer.remaining(); + payload.position(payloadPos + messageSize + 4); + } + if (!incomingCryptoBuffer.hasRemaining()) { + incomingCryptoBuffer.flip(); + handleHandshakeMessage(keySpace, incomingCryptoBuffer); + incomingCryptoBuffer = null; + incomingCryptoSpace = null; + } else { + assert !payload.hasRemaining() : payload.remaining(); + return; + } + } + } + + private void handleHandshakeMessage(KeySpace keySpace, ByteBuffer message) + throws QuicTransportException { + // message param contains one whole TLS message + boolean useClientMode = getUseClientMode(); + byte messageType = message.get(); + int messageSize = ((message.get() & 0xFF) << 16) + | ((message.get() & 0xFF) << 8) + | (message.get() & 0xFF); + + assert message.remaining() == messageSize : + message.remaining() - messageSize; + try { + if (conContext.inputRecord.handshakeHash.isHashable(messageType)) { + ByteBuffer temp = message.duplicate(); + temp.position(0); + conContext.inputRecord.handshakeHash.receive(temp); + } + if (conContext.handshakeContext == null) { + if (!conContext.isNegotiated) { + throw new QuicTransportException( + "Cannot process crypto message, broken: " + + conContext.isBroken, + null, 0, QuicTransportErrors.INTERNAL_ERROR); + } + conContext.handshakeContext = + new PostHandshakeContext(conContext); + } + conContext.handshakeContext.dispatch(messageType, message.slice()); + } catch (SSLHandshakeException e) { + if (e.getCause() instanceof QuicTransportException qte) { + // rethrow quic transport parameters validation exception + throw qte; + } + Alert alert = ((QuicEngineOutputRecord) + conContext.outputRecord).getAlert(); + throw new QuicTransportException(alert.description, keySpace, 0, + BASE_CRYPTO_ERROR + alert.id, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + if (handshakeState == NEED_RECV_CRYPTO) { + if (conContext.outputRecord.isEmpty()) { + if (conContext.isNegotiated) { + // dead code? done, server side, no session ticket + handshakeState = NEED_SEND_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + // expect more messages + // client side: if we're still in INITIAL, switch + // to HANDSHAKE + if (sendKeySpace == INITIAL) { + sendKeySpace = HANDSHAKE; + } + } + } else { + // our turn to send + if (conContext.isNegotiated && !useClientMode) { + // done, server side, wants to send session ticket + handshakeState = NEED_SEND_HANDSHAKE_DONE; + sendKeySpace = ONE_RTT; + } else { + // more messages needed to finish handshake + handshakeState = HandshakeState.NEED_SEND_CRYPTO; + } + } + } else { + assert conContext.isNegotiated; + } + } + + @Override + public void deriveInitialKeys(final QuicVersion quicVersion, + final ByteBuffer connectionId) throws IOException { + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException("Quic version " + quicVersion + + " isn't enabled"); + } + final byte[] connectionIdBytes = new byte[connectionId.remaining()]; + connectionId.get(connectionIdBytes); + this.initialKeyManager.deriveKeys(quicVersion, connectionIdBytes, + getUseClientMode()); + } + + @Override + public void versionNegotiated(final QuicVersion quicVersion) { + Objects.requireNonNull(quicVersion); + if (!isEnabled(quicVersion)) { + throw new IllegalArgumentException("Quic version " + quicVersion + + " is not enabled"); + } + synchronized (this) { + final QuicVersion prevNegotiated = this.negotiatedVersion; + if (prevNegotiated != null) { + throw new IllegalStateException("A Quic version has already " + + "been negotiated previously"); + } + this.negotiatedVersion = quicVersion; + } + } + + public void deriveHandshakeKeys() throws IOException { + final QuicVersion quicVersion = getNegotiatedVersion(); + this.handshakeKeyManager.deriveKeys(quicVersion, + this.conContext.handshakeContext, + getUseClientMode()); + } + + public void deriveOneRTTKeys() throws IOException { + final QuicVersion quicVersion = getNegotiatedVersion(); + this.oneRttKeyManager.deriveKeys(quicVersion, + this.conContext.handshakeContext, + getUseClientMode()); + } + + // for testing (PacketEncryptionTest) + void deriveOneRTTKeys(final QuicVersion version, + final SecretKey client_application_traffic_secret_0, + final SecretKey server_application_traffic_secret_0, + final CipherSuite negotiatedCipherSuite, + final boolean clientMode) throws IOException, + GeneralSecurityException { + this.oneRttKeyManager.deriveOneRttKeys(version, + client_application_traffic_secret_0, + server_application_traffic_secret_0, + negotiatedCipherSuite, clientMode); + } + + @Override + public Runnable getDelegatedTask() { + // TODO: actually delegate tasks + return null; + } + + @Override + public String getPeerHost() { + return peerHost; + } + + @Override + public int getPeerPort() { + return peerPort; + } + + @Override + public boolean useDelegatedTask() { + return true; + } + + public byte[] getLocalQuicTransportParameters() { + ByteBuffer ltp = localQuicTransportParameters; + if (ltp == null) { + return null; + } + byte[] result = new byte[ltp.remaining()]; + ltp.get(0, result); + return result; + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + this.localQuicTransportParameters = params; + } + + @Override + public void restartHandshake() throws IOException { + if (negotiatedVersion != null) { + throw new IllegalStateException("Version already negotiated"); + } + if (sendKeySpace != INITIAL || handshakeState != NEED_RECV_CRYPTO) { + throw new IllegalStateException("Unexpected handshake state"); + } + HandshakeContext context = conContext.handshakeContext; + ClientHandshakeContext chc = (ClientHandshakeContext)context; + + // Refresh handshake hash + chc.handshakeHash.finish(); // reset the handshake hash + + // Update the initial ClientHello handshake message. + chc.initialClientHelloMsg.extensions.reproduce(chc, + new SSLExtension[] { + SSLExtension.CH_QUIC_TRANSPORT_PARAMETERS, + SSLExtension.CH_PRE_SHARED_KEY + }); + + // produce handshake message + chc.initialClientHelloMsg.write(chc.handshakeOutput); + handshakeState = NEED_SEND_CRYPTO; + } + + @Override + public void setRemoteQuicTransportParametersConsumer( + QuicTransportParametersConsumer consumer) { + this.remoteQuicTransportParametersConsumer = consumer; + } + + void processRemoteQuicTransportParameters(ByteBuffer buffer) + throws QuicTransportException{ + remoteQuicTransportParametersConsumer.accept(buffer); + } + + @Override + public boolean tryMarkHandshakeDone() { + if (getUseClientMode()) { + // not expected to be called on client + throw new IllegalStateException( + "Not expected to be called in client mode"); + } + final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this, + NEED_SEND_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED); + if (confirmed) { + if (SSLLogger.isOn) { + SSLLogger.fine("QuicTLSEngine (server) marked handshake " + + "state as HANDSHAKE_CONFIRMED"); + } + } + return confirmed; + } + + @Override + public boolean tryReceiveHandshakeDone() { + final boolean isClient = getUseClientMode(); + if (!isClient) { + throw new IllegalStateException( + "Not expected to receive HANDSHAKE_DONE in server mode"); + } + final boolean confirmed = HANDSHAKE_STATE_HANDLE.compareAndSet(this, + NEED_RECV_HANDSHAKE_DONE, HANDSHAKE_CONFIRMED); + if (confirmed) { + if (SSLLogger.isOn) { + SSLLogger.fine( + "QuicTLSEngine (client) received HANDSHAKE_DONE," + + " marking state as HANDSHAKE_DONE"); + } + } + return confirmed; + } + + @Override + public boolean isTLSHandshakeComplete() { + final boolean isClient = getUseClientMode(); + final HandshakeState hsState = this.handshakeState; + if (isClient) { + // the client has received TLS Finished message from server and + // has sent its own TLS Finished message and is waiting for the server + // to send QUIC HANDSHAKE_DONE frame. + // OR + // the client has received TLS Finished message from server and + // has sent its own TLS Finished message and has even received the + // QUIC HANDSHAKE_DONE frame. + // Either of these implies the TLS handshake is complete for the client + return hsState == NEED_RECV_HANDSHAKE_DONE || hsState == HANDSHAKE_CONFIRMED; + } + // on the server side the TLS handshake is complete only when the server has + // sent a TLS Finished message and received the client's Finished message. + return hsState == HANDSHAKE_CONFIRMED; + } + + /** + * {@return the key phase being used when decrypting incoming 1-RTT + * packets} + */ + // this is only used in tests + public int getOneRttKeyPhase() throws QuicKeyUnavailableException { + return this.oneRttKeyManager.getReadCipher().getKeyPhase(); + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java new file mode 100644 index 00000000000..83e977ee446 --- /dev/null +++ b/src/java.base/share/classes/sun/security/ssl/QuicTransportParametersExtension.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package sun.security.ssl; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jdk.internal.net.quic.QuicTransportException; +import sun.security.ssl.SSLExtension.ExtensionConsumer; +import sun.security.ssl.SSLHandshake.HandshakeMessage; + +/** + * Pack of the "quic_transport_parameters" extensions [RFC 9001]. + */ +final class QuicTransportParametersExtension { + + static final HandshakeProducer chNetworkProducer = + new T13CHQuicParametersProducer(); + static final ExtensionConsumer chOnLoadConsumer = + new T13CHQuicParametersConsumer(); + static final HandshakeAbsence chOnLoadAbsence = + new T13CHQuicParametersAbsence(); + static final HandshakeProducer eeNetworkProducer = + new T13EEQuicParametersProducer(); + static final ExtensionConsumer eeOnLoadConsumer = + new T13EEQuicParametersConsumer(); + static final HandshakeAbsence eeOnLoadAbsence = + new T13EEQuicParametersAbsence(); + + private static final class T13CHQuicParametersProducer + implements HandshakeProducer { + // Prevent instantiation of this class. + private T13CHQuicParametersProducer() { + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) throws IOException { + + ClientHandshakeContext chc = (ClientHandshakeContext) context; + if (!chc.sslConfig.isQuic) { + return null; + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) chc.conContext.transport; + + return quicTLSEngine.getLocalQuicTransportParameters(); + } + + } + + private static final class T13CHQuicParametersConsumer + implements ExtensionConsumer { + // Prevent instantiation of this class. + private T13CHQuicParametersConsumer() { + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + ServerHandshakeContext shc = (ServerHandshakeContext) context; + if (!shc.sslConfig.isQuic) { + throw shc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION, + "Client sent the quic_transport_parameters " + + "extension in a non-QUIC context"); + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + quicTLSEngine.processRemoteQuicTransportParameters(buffer); + } catch (QuicTransportException e) { + throw shc.conContext.fatal(Alert.DECODE_ERROR, e); + } + + } + } + + private static final class T13CHQuicParametersAbsence + implements HandshakeAbsence { + // Prevent instantiation of this class. + private T13CHQuicParametersAbsence() { + } + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) throws IOException { + // The producing happens in server side only. + ServerHandshakeContext shc = (ServerHandshakeContext)context; + + if (shc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST send quic_transport_parameters + throw shc.conContext.fatal( + Alert.MISSING_EXTENSION, + "Client did not send QUIC transport parameters"); + } + } + } + + private static final class T13EEQuicParametersProducer + implements HandshakeProducer { + // Prevent instantiation of this class. + private T13EEQuicParametersProducer() { + } + + @Override + public byte[] produce(ConnectionContext context, + HandshakeMessage message) { + + ServerHandshakeContext shc = (ServerHandshakeContext) context; + if (!shc.sslConfig.isQuic) { + return null; + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) shc.conContext.transport; + + return quicTLSEngine.getLocalQuicTransportParameters(); + } + } + + private static final class T13EEQuicParametersConsumer + implements ExtensionConsumer { + // Prevent instantiation of this class. + private T13EEQuicParametersConsumer() { + } + + @Override + public void consume(ConnectionContext context, + HandshakeMessage message, ByteBuffer buffer) + throws IOException { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + if (!chc.sslConfig.isQuic) { + throw chc.conContext.fatal(Alert.UNSUPPORTED_EXTENSION, + "Server sent the quic_transport_parameters " + + "extension in a non-QUIC context"); + } + QuicTLSEngineImpl quicTLSEngine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + quicTLSEngine.processRemoteQuicTransportParameters(buffer); + } catch (QuicTransportException e) { + throw chc.conContext.fatal(Alert.DECODE_ERROR, e); + } + } + } + + private static final class T13EEQuicParametersAbsence + implements HandshakeAbsence { + // Prevent instantiation of this class. + private T13EEQuicParametersAbsence() { + } + + @Override + public void absent(ConnectionContext context, + HandshakeMessage message) throws IOException { + ClientHandshakeContext chc = (ClientHandshakeContext) context; + + if (chc.sslConfig.isQuic) { + // RFC 9001: endpoints MUST send quic_transport_parameters + throw chc.conContext.fatal( + Alert.MISSING_EXTENSION, + "Server did not send QUIC transport parameters"); + } + } + } +} diff --git a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java index 95cfc6082be..1d5a4c4e73d 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLAlgorithmConstraints.java @@ -37,6 +37,8 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.net.ssl.*; + +import jdk.internal.net.quic.QuicTLSEngine; import sun.security.util.DisabledAlgorithmConstraints; import static sun.security.util.DisabledAlgorithmConstraints.*; @@ -162,6 +164,33 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { withDefaultCertPathConstraints); } + /** + * Returns an {@link AlgorithmConstraints} instance that uses the + * constraints configured for the given {@code engine} in addition + * to the platform configured constraints. + *

        + * If the given {@code allowedAlgorithms} is non-null then the returned + * {@code AlgorithmConstraints} will only permit those allowed algorithms. + * + * @param engine QuicTLSEngine used to determine the constraints + * @param mode SIGNATURE_CONSTRAINTS_MODE + * @param withDefaultCertPathConstraints whether or not to apply the default certpath + * algorithm constraints too + * @return a AlgorithmConstraints instance + */ + static AlgorithmConstraints forQUIC(QuicTLSEngine engine, + SIGNATURE_CONSTRAINTS_MODE mode, + boolean withDefaultCertPathConstraints) { + if (engine == null) { + return wrap(null, withDefaultCertPathConstraints); + } + + return new SSLAlgorithmConstraints( + nullIfDefault(getUserSpecifiedConstraints(engine)), + new SupportedSignatureAlgorithmConstraints(engine.getHandshakeSession(), mode), + withDefaultCertPathConstraints); + } + private static AlgorithmConstraints nullIfDefault( AlgorithmConstraints constraints) { return constraints == DEFAULT ? null : constraints; @@ -207,6 +236,17 @@ final class SSLAlgorithmConstraints implements AlgorithmConstraints { return null; } + private static AlgorithmConstraints getUserSpecifiedConstraints( + QuicTLSEngine quicEngine) { + if (quicEngine != null) { + if (quicEngine instanceof QuicTLSEngineImpl engineImpl) { + return engineImpl.getAlgorithmConstraints(); + } + return quicEngine.getSSLParameters().getAlgorithmConstraints(); + } + return null; + } + @Override public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { diff --git a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java index bb032e019d3..aacac465027 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -78,6 +78,7 @@ final class SSLConfiguration implements Cloneable { boolean noSniExtension; boolean noSniMatcher; + boolean isQuic; // To switch off the extended_master_secret extension. static final boolean useExtendedMasterSecret; @@ -91,7 +92,7 @@ final class SSLConfiguration implements Cloneable { Utilities.getBooleanProperty("jdk.tls.allowLegacyMasterSecret", true); // Use TLS1.3 middlebox compatibility mode. - static final boolean useCompatibilityMode = Utilities.getBooleanProperty( + private static final boolean useCompatibilityMode = Utilities.getBooleanProperty( "jdk.tls.client.useCompatibilityMode", true); // Respond a close_notify alert if receiving close_notify alert. @@ -524,6 +525,14 @@ final class SSLConfiguration implements Cloneable { } } + public boolean isUseCompatibilityMode() { + return useCompatibilityMode && !isQuic; + } + + public void setQuic(boolean quic) { + isQuic = quic; + } + @Override @SuppressWarnings({"unchecked", "CloneDeclaresCloneNotSupported"}) public Object clone() { @@ -567,7 +576,10 @@ final class SSLConfiguration implements Cloneable { */ private static String[] getCustomizedSignatureScheme(String propertyName) { String property = System.getProperty(propertyName); - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + // this method is called from class initializer; logging here + // will occasionally pin threads and deadlock if called from a virtual thread + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "System property " + propertyName + " is set to '" + property + "'"); @@ -595,7 +607,8 @@ final class SSLConfiguration implements Cloneable { if (scheme != null && scheme.isAvailable) { signatureSchemes.add(schemeName); } else { - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "The current installed providers do not " + "support signature scheme: " + schemeName); diff --git a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java index a0cb28201e9..85dde5b0dbb 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java @@ -481,6 +481,10 @@ public abstract class SSLContextImpl extends SSLContextSpi { return availableProtocols; } + public boolean isUsableWithQuic() { + return trustManager instanceof X509TrustManagerImpl; + } + /* * The SSLContext implementation for SSL/(D)TLS algorithm * diff --git a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java index c7175ea7fdc..082914b4b4b 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLExtension.java @@ -458,6 +458,28 @@ enum SSLExtension implements SSLStringizer { null, null, null, null, KeyShareExtension.hrrStringizer), + // Extension defined in RFC 9001 + CH_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters", + SSLHandshake.CLIENT_HELLO, + ProtocolVersion.PROTOCOLS_OF_13, + QuicTransportParametersExtension.chNetworkProducer, + QuicTransportParametersExtension.chOnLoadConsumer, + QuicTransportParametersExtension.chOnLoadAbsence, + null, + null, + // TODO properly stringize, rather than hex output. + null), + EE_QUIC_TRANSPORT_PARAMETERS (0x0039, "quic_transport_parameters", + SSLHandshake.ENCRYPTED_EXTENSIONS, + ProtocolVersion.PROTOCOLS_OF_13, + QuicTransportParametersExtension.eeNetworkProducer, + QuicTransportParametersExtension.eeOnLoadConsumer, + QuicTransportParametersExtension.eeOnLoadAbsence, + null, + null, + // TODO properly stringize, rather than hex output + null), + // Extensions defined in RFC 5746 (TLS Renegotiation Indication Extension) CH_RENEGOTIATION_INFO (0xff01, "renegotiation_info", SSLHandshake.CLIENT_HELLO, @@ -820,7 +842,10 @@ enum SSLExtension implements SSLStringizer { private static Collection getDisabledExtensions( String propertyName) { String property = System.getProperty(propertyName); - if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx")) { + // this method is called from class initializer; logging here + // will occasionally pin threads and deadlock if called from a virtual thread + if (SSLLogger.isOn && SSLLogger.isOn("ssl,sslctx") + && !Thread.currentThread().isVirtual()) { SSLLogger.fine( "System property " + propertyName + " is set to '" + property + "'"); diff --git a/src/java.base/share/classes/sun/security/ssl/ServerHello.java b/src/java.base/share/classes/sun/security/ssl/ServerHello.java index d092d6c07de..1d2faa5351f 100644 --- a/src/java.base/share/classes/sun/security/ssl/ServerHello.java +++ b/src/java.base/share/classes/sun/security/ssl/ServerHello.java @@ -235,7 +235,8 @@ final class ServerHello { serverVersion.name, Utilities.toHexString(serverRandom.randomBytes), sessionId.toString(), - cipherSuite.name + "(" + Utilities.byte16HexString(cipherSuite.id) + ")", + cipherSuite.name + + "(" + Utilities.byte16HexString(cipherSuite.id) + ")", HexFormat.of().toHexDigits(compressionMethod), Utilities.indent(extensions.toString(), " ") }; @@ -534,8 +535,9 @@ final class ServerHello { // consider the handshake extension impact SSLExtension[] enabledExtensions = - shc.sslConfig.getEnabledExtensions( - SSLHandshake.CLIENT_HELLO, shc.negotiatedProtocol); + shc.sslConfig.getEnabledExtensions( + SSLHandshake.CLIENT_HELLO, + shc.negotiatedProtocol); clientHello.extensions.consumeOnTrade(shc, enabledExtensions); shc.negotiatedProtocol = @@ -670,6 +672,17 @@ final class ServerHello { // Update the context for master key derivation. shc.handshakeKeyDerivation = kd; + if (shc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) shc.conContext.transport; + try { + engine.deriveHandshakeKeys(); + } catch (IOException e) { + // unlikely + throw shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, + "Failed to derive keys", e); + } + } // Check if the server supports stateless resumption if (sessionCache.statelessEnabled()) { shc.statelessResumption = true; @@ -784,9 +797,9 @@ final class ServerHello { // first handshake message. This may either be after // a ServerHello or a HelloRetryRequest. // (RFC 8446, Appendix D.4) - shc.conContext.outputRecord.changeWriteCiphers( - SSLWriteCipher.nullTlsWriteCipher(), - (clientHello.sessionId.length() != 0)); + if (clientHello.sessionId.length() != 0) { + shc.conContext.outputRecord.encodeChangeCipherSpec(); + } // Stateless, shall we clean up the handshake context as well? shc.handshakeHash.finish(); // forgot about the handshake hash @@ -1366,10 +1379,21 @@ final class ServerHello { // Should use resumption_master_secret for TLS 1.3. // chc.handshakeSession.setMasterSecret(masterSecret); - // Update the context for master key derivation. chc.handshakeKeyDerivation = secretKD; + if (chc.sslConfig.isQuic) { + QuicTLSEngineImpl engine = + (QuicTLSEngineImpl) chc.conContext.transport; + try { + engine.deriveHandshakeKeys(); + } catch (IOException e) { + // unlikely + throw chc.conContext.fatal(Alert.HANDSHAKE_FAILURE, + "Failed to derive keys", e); + } + } + // update the consumers and producers // // The server sends a dummy change_cipher_spec record immediately diff --git a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java index 2441ad91fde..6bf138f4e45 100644 --- a/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/SunX509KeyManagerImpl.java @@ -195,6 +195,13 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking { getAlgorithmConstraints(engine), null, null); } + @Override + String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT, + getAlgorithmConstraints(quicTLSEngine), null, null); + } + /* * Choose an alias to authenticate the server side of a secure * socket given the public key type and the list of @@ -222,6 +229,16 @@ final class SunX509KeyManagerImpl extends X509KeyManagerCertChecking { X509TrustManagerImpl.getRequestedServerNames(engine), "HTTPS"); } + @Override + String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER, + getAlgorithmConstraints(quicTLSEngine), + X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine), + "HTTPS"); + } + /* * Get the matching aliases for authenticating the client side of a secure * socket given the public key type and the list of diff --git a/src/java.base/share/classes/sun/security/ssl/TransportContext.java b/src/java.base/share/classes/sun/security/ssl/TransportContext.java index 717c81723ff..49fd664e9ed 100644 --- a/src/java.base/share/classes/sun/security/ssl/TransportContext.java +++ b/src/java.base/share/classes/sun/security/ssl/TransportContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -489,6 +489,10 @@ final class TransportContext implements ConnectionContext { isUnsureMode = false; } + public void setQuic(boolean quic) { + sslConfig.setQuic(quic); + } + // The OutputRecord is closed and not buffered output record. boolean isOutboundDone() { return outputRecord.isClosed() && outputRecord.isEmpty(); diff --git a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java index 4e91df2806e..5abc2cb1bf4 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509Authentication.java +++ b/src/java.base/share/classes/sun/security/ssl/X509Authentication.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -218,6 +218,28 @@ enum X509Authentication implements SSLAuthentication { chc.peerSupportedAuthorities == null ? null : chc.peerSupportedAuthorities.clone(), engine); + } else if (chc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) { + // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that + // takes QuicTLSEngine. + // For now, in context of QUIC, for KeyManager implementations other than + // subclasses of sun.security.ssl.X509KeyManagerCertChecking + // we don't take into account + // any algorithm constraints when choosing the client alias and + // just call the functionally limited + // javax.net.ssl.X509KeyManager.chooseClientAlias(...) + if (km instanceof X509KeyManagerCertChecking xkm) { + clientAlias = xkm.chooseQuicClientAlias(keyTypes, + chc.peerSupportedAuthorities == null + ? null + : chc.peerSupportedAuthorities.clone(), + quicEngineImpl); + } else { + clientAlias = km.chooseClientAlias(keyTypes, + chc.peerSupportedAuthorities == null + ? null + : chc.peerSupportedAuthorities.clone(), + null); + } } if (clientAlias == null) { @@ -290,6 +312,28 @@ enum X509Authentication implements SSLAuthentication { shc.peerSupportedAuthorities == null ? null : shc.peerSupportedAuthorities.clone(), engine); + } else if (shc.conContext.transport instanceof QuicTLSEngineImpl quicEngineImpl) { + // TODO add a method on javax.net.ssl.X509ExtendedKeyManager that + // takes QuicTLSEngine. + // For now, in context of QUIC, for KeyManager implementations other than + // subclasses of sun.security.ssl.X509KeyManagerCertChecking + // we don't take into account + // any algorithm constraints when choosing the server alias + // and just call the functionally limited + // javax.net.ssl.X509KeyManager.chooseServerAlias(...) + if (km instanceof X509KeyManagerCertChecking xkm) { + serverAlias = xkm.chooseQuicServerAlias(keyType, + shc.peerSupportedAuthorities == null + ? null + : shc.peerSupportedAuthorities.clone(), + quicEngineImpl); + } else { + serverAlias = km.chooseServerAlias(keyType, + shc.peerSupportedAuthorities == null + ? null + : shc.peerSupportedAuthorities.clone(), + null); + } } if (serverAlias == null) { diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java index 162a938cddb..9484ab4f830 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerCertChecking.java @@ -74,6 +74,15 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { abstract boolean isCheckingDisabled(); + // TODO move this method to a public interface / class + abstract String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine); + + // TODO move this method to a public interface / class + abstract String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine); + // Entry point to do all certificate checks. protected EntryStatus checkAlias(int keyStoreIndex, String alias, Certificate[] chain, Date verificationDate, List keyTypes, @@ -185,6 +194,17 @@ abstract class X509KeyManagerCertChecking extends X509ExtendedKeyManager { engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true); } + // Gets algorithm constraints of QUIC TLS engine. + protected AlgorithmConstraints getAlgorithmConstraints(QuicTLSEngineImpl engine) { + + if (checksDisabled) { + return null; + } + + return SSLAlgorithmConstraints.forQUIC( + engine, SIGNATURE_CONSTRAINTS_MODE.PEER, true); + } + // Algorithm constraints check. private boolean conformsToAlgorithmConstraints( AlgorithmConstraints constraints, Certificate[] chain, diff --git a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java index df6ecaf7a42..e48096cc363 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/X509KeyManagerImpl.java @@ -129,6 +129,13 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { getAlgorithmConstraints(engine), null, null); } + @Override + String chooseQuicClientAlias(String[] keyTypes, Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyTypes), issuers, CheckType.CLIENT, + getAlgorithmConstraints(quicTLSEngine), null, null); + } + @Override public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { @@ -165,6 +172,16 @@ final class X509KeyManagerImpl extends X509KeyManagerCertChecking { // It is not a really HTTPS endpoint identification. } + @Override + String chooseQuicServerAlias(String keyType, + X500Principal[] issuers, + QuicTLSEngineImpl quicTLSEngine) { + return chooseAlias(getKeyTypes(keyType), issuers, CheckType.SERVER, + getAlgorithmConstraints(quicTLSEngine), + X509TrustManagerImpl.getRequestedServerNames(quicTLSEngine), + "HTTPS"); + } + @Override public String[] getClientAliases(String keyType, Principal[] issuers) { return getAliases(keyType, issuers, CheckType.CLIENT); diff --git a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java index 5001181fecf..d82b94a1d7d 100644 --- a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java +++ b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java @@ -30,7 +30,9 @@ import java.security.*; import java.security.cert.*; import java.util.*; import java.util.concurrent.locks.ReentrantLock; + import javax.net.ssl.*; + import sun.security.ssl.SSLAlgorithmConstraints.SIGNATURE_CONSTRAINTS_MODE; import sun.security.util.AnchorCertificates; import sun.security.util.HostnameChecker; @@ -145,6 +147,16 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager checkTrusted(chain, authType, engine, false); } + public void checkClientTrusted(X509Certificate[] chain, String authType, + QuicTLSEngineImpl quicTLSEngine) throws CertificateException { + checkTrusted(chain, authType, quicTLSEngine, true); + } + + void checkServerTrusted(X509Certificate[] chain, String authType, + QuicTLSEngineImpl quicTLSEngine) throws CertificateException { + checkTrusted(chain, authType, quicTLSEngine, false); + } + private Validator checkTrustedInit(X509Certificate[] chain, String authType, boolean checkClientTrusted) { if (chain == null || chain.length == 0) { @@ -236,6 +248,52 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager } } + private void checkTrusted(X509Certificate[] chain, + String authType, QuicTLSEngineImpl quicTLSEngine, + boolean checkClientTrusted) throws CertificateException { + Validator v = checkTrustedInit(chain, authType, checkClientTrusted); + + final X509Certificate[] trustedChain; + if (quicTLSEngine != null) { + + final SSLSession session = quicTLSEngine.getHandshakeSession(); + if (session == null) { + throw new CertificateException("No handshake session"); + } + + // create the algorithm constraints + final AlgorithmConstraints constraints = SSLAlgorithmConstraints.forQUIC( + quicTLSEngine, SIGNATURE_CONSTRAINTS_MODE.LOCAL, false); + final List responseList; + // grab any stapled OCSP responses for use in validation + if (!checkClientTrusted && + session instanceof ExtendedSSLSession extSession) { + responseList = extSession.getStatusResponses(); + } else { + responseList = Collections.emptyList(); + } + // do the certificate chain validation + trustedChain = v.validate(chain, null, responseList, + constraints, checkClientTrusted ? null : authType); + + // check endpoint identity + String identityAlg = quicTLSEngine.getSSLParameters(). + getEndpointIdentificationAlgorithm(); + if (identityAlg != null && !identityAlg.isEmpty()) { + checkIdentity(session, trustedChain, + identityAlg, checkClientTrusted); + } + } else { + trustedChain = v.validate(chain, null, Collections.emptyList(), + null, checkClientTrusted ? null : authType); + } + + if (SSLLogger.isOn && SSLLogger.isOn("ssl,trustmanager")) { + SSLLogger.fine("Found trusted certificate", + trustedChain[trustedChain.length - 1]); + } + } + private void checkTrusted(X509Certificate[] chain, String authType, SSLEngine engine, boolean checkClientTrusted) throws CertificateException { @@ -344,6 +402,13 @@ final class X509TrustManagerImpl extends X509ExtendedTrustManager return Collections.emptyList(); } + static List getRequestedServerNames(QuicTLSEngineImpl engine) { + if (engine != null) { + return getRequestedServerNames(engine.getHandshakeSession()); + } + return Collections.emptyList(); + } + private static List getRequestedServerNames( SSLSession session) { if (session instanceof ExtendedSSLSession) { diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security index 32d1ddaf0f7..2464361b9ef 100644 --- a/src/java.base/share/conf/security/java.security +++ b/src/java.base/share/conf/security/java.security @@ -971,6 +971,33 @@ jdk.tls.legacyAlgorithms=NULL, anon, RC4, DES, 3DES_EDE_CBC jdk.tls.keyLimits=AES/GCM/NoPadding KeyUpdate 2^37, \ ChaCha20-Poly1305 KeyUpdate 2^37 +# +# QUIC TLS key limits on symmetric cryptographic algorithms +# +# This security property sets limits on algorithms key usage in QUIC. +# When the number of encrypted datagrams reaches the algorithm value +# listed below, key update operation will be initiated. +# +# The syntax for the property is described below: +# KeyLimits: +# " KeyLimit { , KeyLimit } " +# +# KeyLimit: +# AlgorithmName Length +# +# AlgorithmName: +# A full algorithm transformation. +# +# Length: +# The amount of encrypted data in a session before the Action occurs +# This value may be an integer value in bytes, or as a power of two, 2^23. +# +# Note: This property is currently used by OpenJDK's JSSE implementation. It +# is not guaranteed to be examined and used by other implementations. +# +jdk.quic.tls.keyLimits=AES/GCM/NoPadding 2^23, \ + ChaCha20-Poly1305 2^23 + # # Cryptographic Jurisdiction Policy defaults # diff --git a/src/java.net.http/share/classes/java/net/http/HttpClient.java b/src/java.net.http/share/classes/java/net/http/HttpClient.java index 59afff013c7..4ce77486e70 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpClient.java +++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java @@ -28,9 +28,11 @@ package java.net.http; import java.io.IOException; import java.io.UncheckedIOException; import java.net.InetAddress; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; +import java.net.URI; import java.nio.channels.Selector; import java.net.Authenticator; import java.net.CookieHandler; @@ -59,7 +61,7 @@ import jdk.internal.net.http.HttpClientBuilderImpl; * The {@link #newBuilder() newBuilder} method returns a builder that creates * instances of the default {@code HttpClient} implementation. * The builder can be used to configure per-client state, like: the preferred - * protocol version ( HTTP/1.1 or HTTP/2 ), whether to follow redirects, a + * protocol version ( HTTP/1.1, HTTP/2 or HTTP/3 ), whether to follow redirects, a * proxy, an authenticator, etc. Once built, an {@code HttpClient} is immutable, * and can be used to send multiple requests. * @@ -162,6 +164,59 @@ import jdk.internal.net.http.HttpClientBuilderImpl; * prevent the resources allocated by the associated client from * being reclaimed by the garbage collector. * + *

        + * The default implementation of the {@code HttpClient} supports HTTP/1.1, + * HTTP/2, and HTTP/3. Which version of the protocol is actually used when sending + * a request can depend on multiple factors. In the case of HTTP/2, it may depend + * on an initial upgrade to succeed (when using a plain connection), or on HTTP/2 + * being successfully negotiated during the Transport Layer Security (TLS) handshake. + * + *

        If {@linkplain Version#HTTP_2 HTTP/2} is selected over a clear + * connection, and no HTTP/2 connection to the + * origin server + * already exists, the client will create a new connection and attempt an upgrade + * from HTTP/1.1 to HTTP/2. + * If the upgrade succeeds, then the response to this request will use HTTP/2. + * If the upgrade fails, then the response will be handled using HTTP/1.1. + * + *

        Other constraints may also affect the selection of protocol version. + * For example, if HTTP/2 is requested through a proxy, and if the implementation + * does not support this mode, then HTTP/1.1 may be used. + *

        + * The HTTP/3 protocol is not selected by default, but can be enabled by setting + * the {@linkplain Builder#version(Version) HttpClient preferred version} or the + * {@linkplain HttpRequest.Builder#version(Version) HttpRequest preferred version} to + * {@linkplain Version#HTTP_3 HTTP/3}. Like for HTTP/2, which protocol version is + * actually used when HTTP/3 is enabled may depend on several factors. + * {@linkplain HttpOption#H3_DISCOVERY Configuration hints} can + * be {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) provided} + * to help the {@code HttpClient} implementation decide how to establish + * and carry out the HTTP exchange when the HTTP/3 protocol is enabled. + * If no configuration hints are provided, the {@code HttpClient} will select + * one as explained in the {@link HttpOption#H3_DISCOVERY H3_DISCOVERY} + * option API documentation. + *
        Note that a request whose {@linkplain URI#getScheme() URI scheme} is not + * {@code "https"} will never be sent over HTTP/3. In this implementation, + * HTTP/3 is not used if a proxy is selected. + * + *

        + * If a concrete instance of {@link HttpClient} doesn't support sending a + * request through HTTP/3, an {@link UnsupportedProtocolVersionException} may be + * thrown, either when {@linkplain Builder#build() building} the client with + * a {@linkplain Builder#version(Version) preferred version} set to HTTP/3, + * or when attempting to send a request with {@linkplain HttpRequest.Builder#version(Version) + * HTTP/3 enabled} when {@link Http3DiscoveryMode#HTTP_3_URI_ONLY HTTP_3_URI_ONLY} + * was {@linkplain HttpRequest.Builder#setOption(HttpOption, Object) specified}. + * This may typically happen if the {@link #sslContext() SSLContext} + * or {@link #sslParameters() SSLParameters} configured on the client instance cannot + * be used with HTTP/3. + * + * @see UnsupportedProtocolVersionException + * @see Builder#version(Version) + * @see HttpRequest.Builder#version(Version) + * @see HttpRequest.Builder#setOption(HttpOption, Object) + * @see HttpOption#H3_DISCOVERY + * * @since 11 */ public abstract class HttpClient implements AutoCloseable { @@ -320,23 +375,19 @@ public abstract class HttpClient implements AutoCloseable { public Builder followRedirects(Redirect policy); /** - * Requests a specific HTTP protocol version where possible. + * Sets the default preferred HTTP protocol version for requests + * issued by this client. * *

        If this method is not invoked prior to {@linkplain #build() * building}, then newly built clients will prefer {@linkplain * Version#HTTP_2 HTTP/2}. * - *

        If set to {@linkplain Version#HTTP_2 HTTP/2}, then each request - * will attempt to upgrade to HTTP/2. If the upgrade succeeds, then the - * response to this request will use HTTP/2 and all subsequent requests - * and responses to the same - * origin server - * will use HTTP/2. If the upgrade fails, then the response will be - * handled using HTTP/1.1 + *

        If a request doesn't have a preferred version, then + * the effective preferred version for the request is the + * client's preferred version.

        * - * @implNote Constraints may also affect the selection of protocol version. - * For example, if HTTP/2 is requested through a proxy, and if the implementation - * does not support this mode, then HTTP/1.1 may be used + * @implNote Some constraints may also affect the {@linkplain + * HttpClient##ProtocolVersionSelection selection of the actual protocol version}. * * @param version the requested HTTP protocol version * @return this builder @@ -439,9 +490,14 @@ public abstract class HttpClient implements AutoCloseable { * @return a new {@code HttpClient} * * @throws UncheckedIOException may be thrown if underlying IO resources required - * by the implementation cannot be allocated. For instance, + * by the implementation cannot be allocated, or if the resulting configuration + * does not satisfy the implementation requirements. For instance, * if the implementation requires a {@link Selector}, and opening - * one fails due to {@linkplain Selector#open() lack of necessary resources}. + * one fails due to {@linkplain Selector#open() lack of necessary resources}, + * or if the {@linkplain #version(Version) preferred protocol version} is not + * {@linkplain HttpClient##UnsupportedProtocolVersion supported by + * the implementation or cannot be used in this configuration}. + * */ public HttpClient build(); } @@ -525,9 +581,11 @@ public abstract class HttpClient implements AutoCloseable { * Returns the preferred HTTP protocol version for this client. The default * value is {@link HttpClient.Version#HTTP_2} * - * @implNote Constraints may also affect the selection of protocol version. - * For example, if HTTP/2 is requested through a proxy, and if the - * implementation does not support this mode, then HTTP/1.1 may be used + * @implNote + * The protocol version that the {@code HttpClient} eventually + * decides to use for a request is affected by various factors noted + * in {@linkplain ##ProtocolVersionSelection protocol version selection} + * section. * * @return the HTTP protocol version requested */ @@ -562,7 +620,13 @@ public abstract class HttpClient implements AutoCloseable { /** * HTTP version 2 */ - HTTP_2 + HTTP_2, + + /** + * HTTP version 3 + * @since 26 + */ + HTTP_3 } /** diff --git a/src/java.net.http/share/classes/java/net/http/HttpOption.java b/src/java.net.http/share/classes/java/net/http/HttpOption.java new file mode 100644 index 00000000000..cbff11f71ee --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/HttpOption.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.net.http; + +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest.Builder; + +/** + * This interface is used to provide additional request configuration + * option hints on how an HTTP request/response exchange should + * be carried out by the {@link HttpClient} implementation. + * Request configuration option hints can be provided to an + * {@link HttpRequest} with the {@link + * Builder#setOption(HttpOption, Object) HttpRequest.Builder + * setOption} method. + * + *

        Concrete instances of this class and its subclasses are immutable. + * + * @apiNote + * In this version, the {@code HttpOption} interface is sealed and + * only allows the {@link #H3_DISCOVERY} option. However, it could be + * extended in the future to support additional options. + *

        + * The {@link #H3_DISCOVERY} option can be used to help the + * {@link HttpClient} decide how to select or establish an + * HTTP/3 connection through which to carry out an HTTP/3 + * request/response exchange. + * + * @param The {@linkplain #type() type of the option value} + * + * @since 26 + */ +public sealed interface HttpOption permits HttpRequestOptionImpl { + /** + * {@return the option name} + * + * @implSpec Different options must have different names. + */ + String name(); + + /** + * {@return the type of the value associated with the option} + * + * @apiNote Different options may have the same type. + */ + Class type(); + + /** + * An option that can be used to configure how the {@link HttpClient} will + * select or establish an HTTP/3 connection through which to carry out + * the request. If {@link Version#HTTP_3} is not selected either as + * the {@linkplain Builder#version(Version) request preferred version} + * or the {@linkplain HttpClient.Builder#version(Version) HttpClient + * preferred version} setting this option on the request has no effect. + *

        + * The {@linkplain #name() name of this option} is {@code "H3_DISCOVERY"}. + * + * @implNote + * The JDK built-in implementation of the {@link HttpClient} understands the + * request option {@link #H3_DISCOVERY} hint. + *
        + * If no {@code H3_DISCOVERY} hint is provided, and the {@linkplain Version#HTTP_3 + * HTTP/3 version} is selected, either as {@linkplain Builder#version(Version) + * request preferred version} or {@linkplain HttpClient.Builder#version(Version) + * client preferred version}, the JDK built-in implementation will establish + * the exchange as per {@link Http3DiscoveryMode#ANY}. + *

        + * In case of {@linkplain HttpClient.Redirect redirect}, the + * {@link #H3_DISCOVERY} option, if present, is always transferred to + * the new request. + *

        + * In this implementation, HTTP/3 through proxies is not supported. + * Unless {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified, if + * a {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy} is {@linkplain + * ProxySelector#select(URI) selected} for the {@linkplain HttpRequest#uri() + * request URI}, the protocol version is downgraded to HTTP/2 or + * HTTP/1.1 and the {@link #H3_DISCOVERY} option is ignored. If, on the + * other hand, {@link Http3DiscoveryMode#HTTP_3_URI_ONLY} is specified, + * the request will fail. + * + * @see Http3DiscoveryMode + * @see Builder#setOption(HttpOption, Object) + */ + HttpOption H3_DISCOVERY = + new HttpRequestOptionImpl<>(Http3DiscoveryMode.class, "H3_DISCOVERY"); + + /** + * This enumeration can be used to help the {@link HttpClient} decide + * how an HTTP/3 exchange should be established, and can be provided + * as the value of the {@link HttpOption#H3_DISCOVERY} option + * to {@link Builder#setOption(HttpOption, Object) Builder.setOption}. + *

        + * Note that if neither the {@linkplain Builder#version(Version) request preferred + * version} nor the {@linkplain HttpClient.Builder#version(Version) client preferred + * version} is {@linkplain Version#HTTP_3 HTTP/3}, no HTTP/3 exchange will + * be established and the {@code Http3DiscoveryMode} is ignored. + * + * @since 26 + */ + enum Http3DiscoveryMode { + /** + * This instructs the {@link HttpClient} to use its own implementation + * specific algorithm to find or establish a connection for the exchange. + * Typically, if no connection was previously established with the origin + * server defined by the request URI, the {@link HttpClient} implementation + * may attempt to establish both an HTTP/3 connection over QUIC and an HTTP + * connection over TLS/TCP at the authority present in the request URI, + * and use the first that succeeds. The exchange may then be carried out with + * any of the {@linkplain Version + * three HTTP protocol versions}, depending on which method succeeded first. + * + * @implNote + * If the {@linkplain Builder#version(Version) request preferred version} is {@linkplain + * Version#HTTP_3 HTTP/3}, the {@code HttpClient} may give priority to HTTP/3 by + * attempting to establish an HTTP/3 connection, before attempting a TLS + * connection over TCP. + *

        + * When attempting an HTTP/3 connection in this mode, the {@code HttpClient} may + * use any HTTP Alternative Services + * information it may have previously obtained from the origin server. If no + * such information is available, a direct HTTP/3 connection at the authority (host, port) + * present in the {@linkplain HttpRequest#uri() request URI} will be attempted. + */ + ANY, + /** + * This instructs the {@link HttpClient} to only use the + * HTTP Alternative Services + * to find or establish an HTTP/3 connection with the origin server. + * The exchange may then be carried out with any of the {@linkplain + * Version three HTTP protocol versions}, depending on + * whether an Alternate Service record for HTTP/3 could be found, and which HTTP version + * was negotiated with the origin server, if no such record could be found. + *

        + * In this mode, requests sent to the origin server will be sent through HTTP/1.1 or HTTP/2 + * until a {@code h3} HTTP Alternative Services + * endpoint for that server is advertised to the client. Usually, an alternate service is + * advertised by a server when responding to a request, so that subsequent requests can make + * use of that alternative service. + */ + ALT_SVC, + /** + * This instructs the {@link HttpClient} to only attempt an HTTP/3 connection + * with the origin server. The connection will only succeed if the origin server + * is listening for incoming HTTP/3 connections over QUIC at the same authority (host, port) + * as defined in the {@linkplain HttpRequest#uri() request URI}. In this mode, + * HTTP Alternative Services + * are not used. + */ + HTTP_3_URI_ONLY + } + +} diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequest.java b/src/java.net.http/share/classes/java/net/http/HttpRequest.java index 84a521336b6..c56328ba4b4 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpRequest.java +++ b/src/java.net.http/share/classes/java/net/http/HttpRequest.java @@ -29,6 +29,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.http.HttpClient.Version; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; @@ -91,6 +92,24 @@ public abstract class HttpRequest { */ protected HttpRequest() {} + /** + * {@return the value configured on this request for the given option, if any} + * @param option a request configuration option + * @param the type of the option + * + * @see Builder#setOption(HttpOption, Object) + * + * @implSpec + * The default implementation of this method returns {@link Optional#empty()} + * if {@code option} is non-null, otherwise throws {@link NullPointerException}. + * + * @since 26 + */ + public Optional getOption(HttpOption option) { + Objects.requireNonNull(option); + return Optional.empty(); + } + /** * A builder of {@linkplain HttpRequest HTTP requests}. * @@ -144,14 +163,53 @@ public abstract class HttpRequest { * *

        The corresponding {@link HttpResponse} should be checked for the * version that was actually used. If the version is not set in a - * request, then the version requested will be that of the sending - * {@link HttpClient}. + * request, then the version requested will be {@linkplain + * HttpClient.Builder#version(Version) that of the sending + * {@code HttpClient}}. + * + * @implNote + * Constraints may also affect the {@linkplain HttpClient##ProtocolVersionSelection + * selection of the actual protocol version}. * * @param version the HTTP protocol version requested * @return this builder */ public Builder version(HttpClient.Version version); + /** + * Provides request configuration option hints modeled as key value pairs + * to help an {@link HttpClient} implementation decide how the + * request/response exchange should be established or carried out. + * + *

        An {@link HttpClient} implementation may decide to ignore request + * configuration option hints, or fail the request, if provided with any + * option hints that it does not understand. + *

        + * If this method is invoked twice for the same {@linkplain HttpOption + * request option}, any value previously provided to this builder for the + * corresponding option is replaced by the new value. + * If {@code null} is supplied as a value, any value previously + * provided is discarded. + * + * @implSpec + * The default implementation of this method discards the provided option + * hint and does nothing. + * + * @implNote + * The JDK built-in implementation of the {@link HttpClient} understands the + * request option {@link HttpOption#H3_DISCOVERY} hint. + * + * @param option the request configuration option + * @param value the request configuration option value (can be null) + * + * @return this builder + * + * @see HttpRequest#getOption(HttpOption) + * + * @since 26 + */ + public default Builder setOption(HttpOption option, T value) { return this; } + /** * Adds the given name value pair to the set of headers for this request. * The given value is added to the list of values for that name. @@ -394,6 +452,8 @@ public abstract class HttpRequest { } } ); + request.getOption(HttpOption.H3_DISCOVERY) + .ifPresent(opt -> builder.setOption(HttpOption.H3_DISCOVERY, opt)); return builder; } diff --git a/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java new file mode 100644 index 00000000000..f5562c7068b --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/HttpRequestOptionImpl.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.net.http; + +// Package private implementation of HttpRequest options +record HttpRequestOptionImpl(Class type, String name) + implements HttpOption { + @Override + public String toString() { + return name(); + } +} diff --git a/src/java.net.http/share/classes/java/net/http/HttpResponse.java b/src/java.net.http/share/classes/java/net/http/HttpResponse.java index 52f5298452a..9843e4c7c5b 100644 --- a/src/java.net.http/share/classes/java/net/http/HttpResponse.java +++ b/src/java.net.http/share/classes/java/net/http/HttpResponse.java @@ -803,24 +803,66 @@ public interface HttpResponse { /** * A handler for push promises. * - *

        A push promise is a synthetic request sent by an HTTP/2 server + *

        A push promise is a synthetic request sent by an HTTP/2 or HTTP/3 server * when retrieving an initiating client-sent request. The server has * determined, possibly through inspection of the initiating request, that * the client will likely need the promised resource, and hence pushes a * synthetic push request, in the form of a push promise, to the client. The * client can choose to accept or reject the push promise request. * - *

        A push promise request may be received up to the point where the + *

        For HTTP/2, a push promise request may be received up to the point where the * response body of the initiating client-sent request has been fully * received. The delivery of a push promise response, however, is not * coordinated with the delivery of the response to the initiating - * client-sent request. + * client-sent request. These are delivered with the + * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)} method. + *

        + * For HTTP/3, push promises are handled in a similar way, except that promises + * of the same resource (request URI, request headers and response body) can be + * promised multiple times, but are only delivered by the server (and this API) + * once though the method {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)}. + * Subsequent promises of the same resource, receive a notification only + * of the promise by the method {@link #notifyAdditionalPromise(HttpRequest, PushId)}. + * The same {@link PushPromiseHandler.PushId} is supplied for each of these + * notifications. Additionally, HTTP/3 push promises are not restricted to a context + * of a single initiating request. The same push promise can be delivered and then notified + * across multiple client initiated requests within the same HTTP/3 (QUIC) connection. * * @param the push promise response body type * @since 11 */ public interface PushPromiseHandler { + /** + * Represents a HTTP/3 PushID. PushIds can be shared across + * multiple client initiated requests on the same HTTP/3 connection. + * @since 26 + */ + public sealed interface PushId { + + /** + * Represents an HTTP/3 PushId. + * + * @param pushId the pushId as a long + * @param connectionLabel the {@link HttpResponse#connectionLabel()} + * of the HTTP/3 connection + * @apiNote + * The {@code connectionLabel} should be considered opaque, and ensures that + * two long pushId emitted by different connections correspond to distinct + * instances of {@code PushId}. The {@code pushId} corresponds to the + * unique push ID assigned by the server that identifies a given server + * push on that connection, as defined by + * RFC 9114, + * section 4.6 + * + * @spec https://www.rfc-editor.org/info/rfc9114 + * RFC 9114: HTTP/3 + * + * @since 26 + */ + record Http3PushId(long pushId, String connectionLabel) implements PushId { } + } + /** * Notification of an incoming push promise. * @@ -838,6 +880,12 @@ public interface HttpResponse { * then the push promise is rejected. The {@code acceptor} function will * throw an {@code IllegalStateException} if invoked more than once. * + *

        This method is invoked for all HTTP/2 push promises and also + * by default for the first promise of all HTTP/3 push promises. + * If {@link #applyPushPromise(HttpRequest, HttpRequest, PushId, Function)} + * is overridden, then this method is not directly invoked for HTTP/3 + * push promises. + * * @param initiatingRequest the initiating client-send request * @param pushPromiseRequest the synthetic push request * @param acceptor the acceptor function that must be successfully @@ -849,6 +897,67 @@ public interface HttpResponse { Function,CompletableFuture>> acceptor ); + /** + * Notification of the first occurrence of an HTTP/3 incoming push promise. + * + * Subsequent promises of the same resource (with the same PushId) are notified + * using {@link #notifyAdditionalPromise(HttpRequest, PushId) + * notifyAdditionalPromise(HttpRequest, PushId)}. + * + *

        This method is invoked once for each push promise received, up + * to the point where the response body of the initiating client-sent + * request has been fully received. + * + *

        A push promise is accepted by invoking the given {@code acceptor} + * function. The {@code acceptor} function must be passed a non-null + * {@code BodyHandler}, that is to be used to handle the promise's + * response body. The acceptor function will return a {@code + * CompletableFuture} that completes with the promise's response. + * + *

        If the {@code acceptor} function is not successfully invoked, + * then the push promise is rejected. The {@code acceptor} function will + * throw an {@code IllegalStateException} if invoked more than once. + * + * @implSpec the default implementation invokes + * {@link #applyPushPromise(HttpRequest, HttpRequest, Function)}. This allows + * {@code PushPromiseHandlers} from previous releases to handle HTTP/3 push + * promise in a reasonable way. + * + * @param initiatingRequest the client request that resulted in the promise + * @param pushPromiseRequest the promised HttpRequest from the server + * @param pushid the PushId which can be linked to subsequent notifications + * @param acceptor the acceptor function that must be successfully + * invoked to accept the push promise + * + * @since 26 + */ + public default void applyPushPromise( + HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function,CompletableFuture>> acceptor + ) { + applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor); + } + + /** + * Invoked for each additional HTTP/3 Push Promise. The {@code pushid} links the promise to the + * original promised {@link HttpRequest} and {@link HttpResponse}. Additional promises + * generally result from different client initiated requests. + * + * @implSpec + * The default implementation of this method does nothing. + * + * @param initiatingRequest the client initiated request which resulted in the push + * @param pushid the pushid which may have been notified previously + * + * @since 26 + */ + public default void notifyAdditionalPromise( + HttpRequest initiatingRequest, + PushId pushid + ) { + } /** * Returns a push promise handler that accumulates push promises, and @@ -915,7 +1024,7 @@ public interface HttpResponse { * * @apiNote To ensure that all resources associated with the corresponding * HTTP exchange are properly released, an implementation of {@code - * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request + * BodySubscriber} should ensure to {@linkplain Flow.Subscription#request(long) * request} more data until one of {@link #onComplete() onComplete} or * {@link #onError(Throwable) onError} are signalled, or {@link * Flow.Subscription#cancel cancel} its {@linkplain @@ -957,7 +1066,7 @@ public interface HttpResponse { * {@snippet : * // Streams the response body to a File * HttpResponse response = client - * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html")); } + * .send(request, responseInfo -> BodySubscribers.ofFile(Paths.get("example.html"))); } * * {@snippet : * // Accumulates the response body and returns it as a byte[] diff --git a/src/java.net.http/share/classes/java/net/http/StreamLimitException.java b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java new file mode 100644 index 00000000000..583b515b01b --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/StreamLimitException.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.net.http; + +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.net.http.HttpClient.Version; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.util.Objects; + +/** + * An exception raised when the limit imposed for stream creation on an + * HTTP connection is reached, and the client is unable to create a new + * stream. + *

        + * A {@code StreamLimitException} may be raised when attempting to send + * a new request on any {@linkplain #version() + * protocol version} that supports multiplexing on a single connection. Both + * {@linkplain HttpClient.Version#HTTP_2 HTTP/2} and {@linkplain + * HttpClient.Version#HTTP_3 HTTP/3} allow multiplexing concurrent requests + * to the same server on a single connection. Each request/response exchange + * is carried over a single stream, as defined by the corresponding + * protocol. + *

        + * Whether and when a {@code StreamLimitException} may be + * relayed to the code initiating a request/response exchange is + * implementation and protocol version dependent. + * + * @see HttpClient#send(HttpRequest, BodyHandler) + * @see HttpClient#sendAsync(HttpRequest, BodyHandler) + * @see HttpClient#sendAsync(HttpRequest, BodyHandler, PushPromiseHandler) + * + * @since 26 + */ +public final class StreamLimitException extends IOException { + + @java.io.Serial + private static final long serialVersionUID = 2614981180406031159L; + + /** + * The version of the HTTP protocol on which the stream limit exception occurred. + * Must not be null. + * @serial + */ + private final Version version; + + /** + * Creates a new {@code StreamLimitException} + * @param version the version of the protocol on which the stream limit exception + * occurred. Must not be null. + * @param message the detailed exception message, which can be null. + */ + public StreamLimitException(final Version version, final String message) { + super(message); + this.version = Objects.requireNonNull(version); + } + + /** + * {@return the protocol version for which the exception was raised} + */ + public final Version version() { + return version; + } + + /** + * Restores the state of a {@code StreamLimitException} from the stream + * @param in the input stream + * @throws IOException if the class of a serialized object could not be found. + * @throws ClassNotFoundException if an I/O error occurs. + * @throws InvalidObjectException if {@code version} is null. + */ + @java.io.Serial + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + if (version == null) { + throw new InvalidObjectException("version must not be null"); + } + } +} diff --git a/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java new file mode 100644 index 00000000000..eecc039e5d2 --- /dev/null +++ b/src/java.net.http/share/classes/java/net/http/UnsupportedProtocolVersionException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.net.http; + +import java.io.IOException; +import java.io.Serial; +import java.net.http.HttpClient.Builder; + +/** + * Thrown when the HTTP client doesn't support a particular HTTP version. + * @apiNote + * Typically, this exception may be thrown when attempting to + * {@linkplain Builder#build() build} an {@link java.net.http.HttpClient} + * configured to use {@linkplain java.net.http.HttpClient.Version#HTTP_3 + * HTTP version 3} by default, when the underlying {@link javax.net.ssl.SSLContext + * SSLContext} implementation does not meet the requirements for supporting + * the HttpClient's implementation of the underlying QUIC transport protocol. + * @since 26 + */ +public final class UnsupportedProtocolVersionException extends IOException { + + @Serial + private static final long serialVersionUID = 981344214212332893L; + + /** + * Constructs an {@code UnsupportedProtocolVersionException} with the given detail message. + * + * @param message The detail message; can be {@code null} + */ + public UnsupportedProtocolVersionException(String message) { + super(message); + } +} diff --git a/src/java.net.http/share/classes/java/net/http/package-info.java b/src/java.net.http/share/classes/java/net/http/package-info.java index 9958fd94da0..1b8395c2706 100644 --- a/src/java.net.http/share/classes/java/net/http/package-info.java +++ b/src/java.net.http/share/classes/java/net/http/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,7 +26,7 @@ /** *

        HTTP Client and WebSocket APIs

        * - *

        Provides high-level client interfaces to HTTP (versions 1.1 and 2) and + *

        Provides high-level client interfaces to HTTP (versions 1.1, 2, and 3) and * low-level client interfaces to WebSocket. The main types defined are: * *

          @@ -37,10 +37,12 @@ *
        * *

        The protocol-specific requirements are defined in the - * Hypertext Transfer Protocol - * Version 2 (HTTP/2), the + * Hypertext Transfer Protocol + * Version 3 (HTTP/3), the + * Hypertext Transfer Protocol Version 2 (HTTP/2), the + * * Hypertext Transfer Protocol (HTTP/1.1), and - * The WebSocket Protocol. + * The WebSocket Protocol. * *

        In general, asynchronous tasks execute in either the thread invoking * the operation, e.g. {@linkplain HttpClient#send(HttpRequest, BodyHandler) @@ -66,6 +68,15 @@ *

        Unless otherwise stated, {@code null} parameter values will cause methods * of all classes in this package to throw {@code NullPointerException}. * + * @spec https://www.rfc-editor.org/info/rfc9114 + * RFC 9114: HTTP/3 + * @spec https://www.rfc-editor.org/info/rfc7540 + * RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) + * @spec https://www.rfc-editor.org/info/rfc2616 + * RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1 + * @spec https://www.rfc-editor.org/info/rfc6455 + * RFC 6455: The WebSocket Protocol + * * @since 11 */ package java.net.http; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java new file mode 100644 index 00000000000..08161bcd110 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltServicesRegistry.java @@ -0,0 +1,569 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.net.URI; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import javax.net.ssl.SNIServerName; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; + +/** + * A registry for Alternate Services advertised by server endpoints. + * There is one registry per HttpClient. + */ +public final class AltServicesRegistry { + + // id and logger for debugging purposes: the id is the same for the HttpClientImpl. + private final long id; + private final Logger debug = Utils.getDebugLogger(this::dbgString); + + // The key is the origin of the alternate service + // The value is a list of AltService records declared by the origin. + private final Map> altServices = new HashMap<>(); + // alt services which were marked invalid in context of an origin. the reason for + // them being invalid can be connection issues (for example: the alt service didn't present the + // certificate of the origin) + private final InvalidAltServices invalidAltServices = new InvalidAltServices(); + + // used while dealing with both altServices Map and the invalidAltServices Set + private final ReentrantLock registryLock = new ReentrantLock(); + + public AltServicesRegistry(long id) { + this.id = id; + } + + String dbgString() { + return "AltServicesRegistry(" + id + ")"; + } + + public static final class AltService { + // As defined in RFC-7838, section 2, formally an alternate service is a combination of + // ALPN, host and port + public record Identity(String alpn, String host, int port) { + public Identity { + Objects.requireNonNull(alpn); + Objects.requireNonNull(host); + if (port <= 0) { + throw new IllegalArgumentException("Invalid port: " + port); + } + } + + public boolean matches(AltService service) { + return equals(service.identity()); + } + + @Override + public String toString() { + return alpn + "=\"" + Origin.toAuthority(host, port) +"\""; + } + } + + private record AltServiceData(Identity id, Origin origin, Deadline deadline, + boolean persist, boolean advertised, + String authority, + boolean sameAuthorityAsOrigin) { + public String pretty() { + return "AltSvc: " + id + + "; origin=\"" + origin + "\"" + + "; deadline=" + deadline + + "; persist=" + persist + + "; advertised=" + advertised + + "; sameAuthorityAsOrigin=" + sameAuthorityAsOrigin + + ';'; + } + } + private final AltServiceData svc; + + /** + * @param id the alpn, host and port of this alternate service + * @param origin the {@link Origin} for this alternate service + * @param deadline the deadline until which this endpoint is valid + * @param persist whether that information can be persisted (we don't use this) + * @param advertised Whether or not this alt service was advertised as an alt service. + * In certain cases, an alt service is created when no origin server + * has advertised it. In those cases, this param is {@code false} + */ + private AltService(final Identity id, final Origin origin, Deadline deadline, + final boolean persist, + final boolean advertised) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + assert origin.isSecure() : "origin " + origin + " is not secure"; + deadline = deadline == null ? Deadline.MAX : deadline; + final String authority = Origin.toAuthority(id.host, id.port); + final String originAuthority = Origin.toAuthority(origin.host(), origin.port()); + // keep track of whether the authority of this alt service is same as that + // of the origin + final boolean sameAuthorityAsOrigin = authority.equals(originAuthority); + svc = new AltServiceData(id, origin, deadline, persist, advertised, + authority, sameAuthorityAsOrigin); + } + + public Identity identity() { + return svc.id; + } + + /** + * @return {@code host:port} of the alternate service + */ + public String authority() { + return svc.authority; + } + + /** + * @return {@code identity().host()} + */ + public String host() { + return svc.id.host; + } + + /** + * @return {@code identity().port()} + */ + public int port() { + return svc.id.port; + } + + public boolean isPersist() { + return svc.persist; + } + + public boolean wasAdvertised() { + return svc.advertised; + } + + public String alpn() { + return svc.id.alpn; + } + + public Origin origin() { + return svc.origin; + } + + public Deadline deadline() { + return svc.deadline; + } + + /** + * {@return true if the origin, for which this is an alternate service, has the + * same authority as this alternate service. false otherwise.} + */ + public boolean originHasSameAuthority() { + return svc.sameAuthorityAsOrigin; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AltService service)) return false; + return svc.equals(service.svc); + } + + @Override + public int hashCode() { + return svc.hashCode(); + } + + @Override + public String toString() { + return svc.pretty(); + } + + public static Optional create(final Identity id, final Origin origin, + final Deadline deadline, final boolean persist) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + if (!origin.isSecure()) { + return Optional.empty(); + } + return Optional.of(new AltService(id, origin, deadline, persist, true)); + } + + private static Optional createUnadvertised(final Logger debug, + final Identity id, final Origin origin, + final HttpConnection conn, + final Deadline deadline, final boolean persist) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + if (!origin.isSecure()) { + return Optional.empty(); + } + final List sniServerNames = AltSvcProcessor.getSNIServerNames(conn); + if (sniServerNames == null || sniServerNames.isEmpty()) { + if (debug.on()) { + debug.log("Skipping unadvertised altsvc creation of %s because connection %s" + + " didn't use SNI during connection establishment", id, conn); + } + return Optional.empty(); + } + return Optional.of(new AltService(id, origin, deadline, persist, false)); + } + + } + + // A size limited collection which keeps track of unique InvalidAltSvc instances. + // Upon reaching a pre-defined size limit, after adding newer entries, the collection + // then removes the eldest (the least recently added) entry from the collection. + // The implementation of this class is not thread safe and any concurrent access + // to instances of this class should be guarded externally. + private static final class InvalidAltServices extends LinkedHashMap { + + private static final long serialVersionUID = 2772562283544644819L; + + // we track only a reasonably small number of invalid alt services + private static final int MAX_TRACKED_INVALID_ALT_SVCS = 20; + + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > MAX_TRACKED_INVALID_ALT_SVCS; + } + + private boolean contains(final InvalidAltSvc invalidAltSvc) { + return this.containsKey(invalidAltSvc); + } + + private boolean addUnique(final InvalidAltSvc invalidAltSvc) { + if (contains(invalidAltSvc)) { + return false; + } + this.put(invalidAltSvc, null); + return true; + } + } + + // An alt-service is invalid for a particular origin + private record InvalidAltSvc(Origin origin, AltService.Identity id) { + } + + private boolean keepAltServiceFor(Origin origin, AltService svc) { + // skip invalid alt services + if (isMarkedInvalid(origin, svc.identity())) { + if (debug.on()) { + debug.log("Not registering alt-service which was previously" + + " marked invalid: " + svc); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService skipped (was previously marked invalid): " + svc); + } + return false; + } + return true; + } + + /** + * Declare a new Alternate Service endpoint for the given origin. + * + * @param origin the origin + * @param services a set of alt services for the origin + */ + public void replace(final Origin origin, final List services) { + Objects.requireNonNull(origin); + Objects.requireNonNull(services); + List added; + registryLock.lock(); + try { + // the list needs to be thread safe to ensure that we won't + // get a ConcurrentModificationException when iterating + // through the elements in list::stream(); + added = altServices.compute(origin, (key, list) -> { + Stream svcs = services.stream() + .filter(AltService.class::isInstance) // filter null + .filter((s) -> keepAltServiceFor(origin, s)); + List newList = svcs.toList(); + return newList.isEmpty() ? null : newList; + }); + } finally { + registryLock.unlock(); + } + if (debug.on()) { + debug.log("parsed services: %s", services); + debug.log("resulting services: %s", added); + } + if (Log.altsvc()) { + if (added != null) { + added.forEach((svc) -> Log.logAltSvc("AltService registry updated: {0}", svc)); + } + } + } + + // should be invoked while holding registryLock + private boolean isMarkedInvalid(final Origin origin, final AltService.Identity id) { + assert registryLock.isHeldByCurrentThread() : "Thread isn't holding registry lock"; + return this.invalidAltServices.contains(new InvalidAltSvc(origin, id)); + } + + /** + * Registers an unadvertised alt service for the given origin and the alpn. + * + * @param id The alt service identity + * @param origin The origin + * @return An {@code Optional} containing the registered {@code AltService}, + * or {@link Optional#empty()} if the service was not registered. + */ + Optional registerUnadvertised(final AltService.Identity id, + final Origin origin, + final HttpConnection conn) { + Objects.requireNonNull(id); + Objects.requireNonNull(origin); + registryLock.lock(); + try { + // an unadvertised alt service is registered by an origin only after a + // successful connection has completed with that alt service. This effectively means + // that we shouldn't check our "invalid alt services" collection, since a successful + // connection against the alt service implies a valid alt service. + // Additionally, we remove it from the "invalid alt services" collection for this + // origin, if at all it was part of that collection + this.invalidAltServices.remove(new InvalidAltSvc(origin, id)); + // default max age as per AltService RFC-7838, section 3.1 is 24 hours. we use + // that same value for unadvertised alt-service(s) for an origin. + final long defaultMaxAgeInSecs = 3600 * 24; + final Deadline deadline = TimeSource.now().plusSeconds(defaultMaxAgeInSecs); + final Optional created = AltService.createUnadvertised(debug, + id, origin, conn, deadline, true); + if (created.isEmpty()) { + return Optional.empty(); + } + final AltService altSvc = created.get(); + altServices.compute(origin, (key, list) -> { + Stream old = list == null ? Stream.empty() : list.stream(); + List newList = Stream.concat(old, Stream.of(altSvc)).toList(); + return newList.isEmpty() ? null : newList; + }); + if (debug.on()) { + debug.log("Added unadvertised AltService: %s", created); + } + if (Log.altsvc()) { + Log.logAltSvc("Added unadvertised AltService: {0}", created); + } + return created; + } finally { + registryLock.unlock(); + } + } + + /** + * Clear the alternate services of the specified origin from the registry + * + * @param origin The origin whose alternate services need to be cleared + */ + public void clear(final Origin origin) { + Objects.requireNonNull(origin); + registryLock.lock(); + try { + if (Log.altsvc()) { + Log.logAltSvc("Clearing AltServices for: " + origin); + } + altServices.remove(origin); + } finally { + registryLock.unlock(); + } + } + + public void markInvalid(final AltService altService) { + Objects.requireNonNull(altService); + markInvalid(altService.origin(), altService.identity()); + } + + private void markInvalid(final Origin origin, final AltService.Identity id) { + Objects.requireNonNull(origin); + Objects.requireNonNull(id); + registryLock.lock(); + try { + // remove this alt service from the current active set of the origin + this.altServices.computeIfPresent(origin, + (key, currentActive) -> { + assert currentActive != null; // should never be null according to spec + List newList = currentActive.stream() + .filter(Predicate.not(id::matches)).toList(); + return newList.isEmpty() ? null : newList; + + }); + // additionally keep track of this as an invalid alt service, so that it cannot be + // registered again in the future. Banning is temporary. + // Banned alt services may get removed from the set at some point due to + // implementation constraints. In which case they may get registered again + // and banned again, if connecting to the endpoint fails again. + this.invalidAltServices.addUnique(new InvalidAltSvc(origin, id)); + if (debug.on()) { + debug.log("AltService marked invalid: " + id + " for origin " + origin); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService marked invalid: " + id + " for origin " + origin); + } + } finally { + registryLock.unlock(); + } + + } + + public Stream lookup(final URI uri, final String alpn) { + final Origin origin; + try { + origin = Origin.from(uri); + } catch (IllegalArgumentException iae) { + return Stream.empty(); + } + return lookup(origin, alpn); + } + + /** + * A stream of {@code AlternateService} that are available for the + * given origin and the given ALPN. + * + * @param origin the URI of the origin server + * @param alpn the ALPN of the alternate service + * @return a stream of {@code AlternateService} that are available for the + * given origin and that support the given ALPN + */ + public Stream lookup(final Origin origin, final String alpn) { + return lookup(origin, Predicate.isEqual(alpn)); + } + + public Stream lookup(final URI uri, + final Predicate alpnMatcher) { + final Origin origin; + try { + origin = Origin.from(uri); + } catch (IllegalArgumentException iae) { + return Stream.empty(); + } + return lookup(origin, alpnMatcher); + } + + private boolean isExpired(AltService service, Deadline now) { + var deadline = service.deadline(); + if (now.equals(deadline) || now.isAfter(deadline)) { + // expired, remove from the list + if (debug.on()) { + debug.log("Removing expired alt-service " + service); + } + if (Log.altsvc()) { + Log.logAltSvc("AltService has expired: {0}", service); + } + return true; + } + return false; + } + + /** + * A stream of {@code AlternateService} that are available for the + * given origin and the given ALPN. + * + * @param origin the URI of the origin server + * @param alpnMatcher a predicate to select particular AltService(s) based on the alpn + * of the alternate service + * @return a stream of {@code AlternateService} that are available for the + * given origin and whose ALPN satisfies the {@code alpn} predicate. + */ + private Stream lookup(final Origin origin, + final Predicate alpnMatcher) { + if (debug.on()) debug.log("looking up alt-service for: %s", origin); + final List services; + registryLock.lock(); + try { + // we first drop any expired services + final Deadline now = TimeSource.now(); + services = altServices.compute(origin, (key, list) -> { + if (list == null) return null; + List newList = list.stream() + .filter((s) -> !isExpired(s, now)) + .toList(); + return newList.isEmpty() ? null : newList; + }); + } finally { + registryLock.unlock(); + } + // the order is important - since preferred service are at the head + return services == null + ? Stream.empty() + : services.stream().sequential().filter(s -> alpnMatcher.test(s.identity().alpn())); + } + + /** + * @param altService The alternate service + * {@return true if the {@code service} is known to this registry and the + * service isn't past its max age. false otherwise} + * @throws NullPointerException if {@code service} is null + */ + public boolean isActive(final AltService altService) { + Objects.requireNonNull(altService); + return isActive(altService.origin(), altService.identity()); + } + + private boolean isActive(final Origin origin, final AltService.Identity id) { + Objects.requireNonNull(origin); + Objects.requireNonNull(id); + registryLock.lock(); + try { + final List currentActive = this.altServices.get(origin); + if (currentActive == null) { + return false; + } + AltService svc = null; + for (AltService s : currentActive) { + if (s.identity().equals(id)) { + svc = s; + break; + } + } + if (svc == null) { + return false; + } + // verify that the service hasn't expired + final Deadline now = TimeSource.now(); + final Deadline deadline = svc.deadline(); + final boolean expired = now.equals(deadline) || now.isAfter(deadline); + if (expired) { + // remove from the registry + altServices.put(origin, currentActive.stream() + .filter(Predicate.not(svc::equals)).toList()); + if (debug.on()) { + debug.log("Removed expired alt-service " + svc + " for origin " + origin); + } + if (Log.altsvc()) { + Log.logAltSvc("Removed AltService: {0}", svc); + } + return false; + } + return true; + } finally { + registryLock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java new file mode 100644 index 00000000000..b172a242346 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AltSvcProcessor.java @@ -0,0 +1,495 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.frame.AltSvcFrame; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; + +import static jdk.internal.net.http.Http3ClientProperties.ALTSVC_ALLOW_LOCAL_HOST_ORIGIN; +import static jdk.internal.net.http.common.Alpns.isSecureALPNName; + + +/** + * Responsible for parsing the Alt-Svc values from an Alt-Svc header and/or AltSvc HTTP/2 frame. + */ +final class AltSvcProcessor { + + private static final String HEADER = "alt-svc"; + private static final Logger debug = Utils.getDebugLogger(() -> "AltSvc"); + // a special value that we return back while parsing the header values, + // indicate that all existing alternate services for a origin need to be cleared + private static final List CLEAR_ALL_ALT_SVCS = List.of(); + // whether or not alt service can be created from "localhost" origin host + private static final boolean allowLocalHostOrigin = ALTSVC_ALLOW_LOCAL_HOST_ORIGIN; + + private static final SNIHostName LOCALHOST_SNI = new SNIHostName("localhost"); + + private record ParsedHeaderValue(String rawValue, String alpnName, String host, int port, + Map parameters) { + } + + private AltSvcProcessor() { + throw new UnsupportedOperationException("Instantiation not supported"); + } + + + /** + * Parses the alt-svc header received from origin and update + * registry with the processed values. + * + * @param response response passed on by the server + * @param client client that holds alt-svc registry + * @param request request that holds the origin details + */ + static void processAltSvcHeader(Response response, HttpClientImpl client, + HttpRequestImpl request) { + + // we don't support AltSvc from unsecure origins + if (!request.secure()) { + return; + } + if (response.statusCode == 421) { + // As per AltSvc spec (RFC-7838), section 6: + // An Alt-Svc header field in a 421 (Misdirected Request) response MUST be ignored. + return; + } + final var altSvcHeaderVal = response.headers().firstValue(HEADER); + if (altSvcHeaderVal.isEmpty()) { + return; + } + if (debug.on()) { + debug.log("Processing alt-svc header"); + } + final HttpConnection conn = response.exchange.exchImpl.connection(); + final List sniServerNames = getSNIServerNames(conn); + if (sniServerNames.isEmpty()) { + // we don't trust the alt-svc advertisement if the connection over which it + // was advertised didn't use SNI during TLS handshake while establishing the connection + if (debug.on()) { + debug.log("ignoring alt-svc header because connection %s didn't use SNI during" + + " connection establishment", conn); + } + return; + } + final Origin origin; + try { + origin = Origin.from(request.uri()); + } catch (IllegalArgumentException iae) { + if (debug.on()) { + debug.log("ignoring alt-svc header due to: " + iae); + } + // ignore the alt-svc + return; + } + String altSvcValue = altSvcHeaderVal.get(); + processValueAndUpdateRegistry(client, origin, altSvcValue); + } + + static void processAltSvcFrame(final int streamId, + final AltSvcFrame frame, + final HttpConnection conn, + final HttpClientImpl client) { + final String value = frame.getAltSvcValue(); + if (value == null || value.isBlank()) { + return; + } + if (!conn.isSecure()) { + // don't support alt svc from unsecure origins + return; + } + final List sniServerNames = getSNIServerNames(conn); + if (sniServerNames.isEmpty()) { + // we don't trust the alt-svc advertisement if the connection over which it + // was advertised didn't use SNI during TLS handshake while establishing the connection + if (debug.on()) { + debug.log("ignoring altSvc frame because connection %s didn't use SNI during" + + " connection establishment", conn); + } + return; + } + debug.log("processing AltSvcFrame %s", value); + final Origin origin; + if (streamId == 0) { + // section 4, RFC-7838 - alt-svc frame on stream 0 with empty (zero length) origin + // is invalid and MUST be ignored + if (frame.getOrigin().isBlank()) { + // invalid frame, ignore it + debug.log("Ignoring invalid alt-svc frame on stream 0 of " + conn); + return; + } + // parse origin from frame.getOrigin() string which is in ASCII + // serialized form of an origin (defined in section 6.2 of RFC-6454) + final Origin parsedOrigin; + try { + parsedOrigin = Origin.fromASCIISerializedForm(frame.getOrigin()); + } catch (IllegalArgumentException iae) { + // invalid origin value, ignore the frame + debug.log("origin couldn't be parsed, ignoring invalid alt-svc frame" + + " on stream " + streamId + " of " + conn); + return; + } + // currently we do not allow an alt service to be advertised for a different origin. + // if the origin advertised in the alt-svc frame doesn't match the origin of the + // connection, then we ignore it. the RFC allows us to do that: + // RFC-7838, section 4: + // An ALTSVC frame from a server to a client on stream 0 indicates that + // the conveyed alternative service is associated with the origin + // contained in the Origin field of the frame. An association with an + // origin that the client does not consider authoritative for the + // current connection MUST be ignored. + if (!parsedOrigin.equals(conn.getOriginServer())) { + debug.log("ignoring alt-svc frame on stream 0 for origin: " + parsedOrigin + + " received on connection of origin: " + conn.getOriginServer()); + return; + } + origin = parsedOrigin; + } else { + // (section 4, RFC-7838) - for non-zero stream id, the alt-svc is for the origin of + // the stream. Additionally, an ALTSVC frame on a stream other than stream 0 containing + // non-empty "Origin" information is invalid and MUST be ignored. + if (!frame.getOrigin().isEmpty()) { + // invalid frame, ignore it + debug.log("non-empty origin in alt-svc frame on stream " + streamId + " of " + + conn + ", ignoring alt-svc frame"); + return; + } + origin = conn.getOriginServer(); + assert origin != null : "origin server is null on connection: " + conn; + } + processValueAndUpdateRegistry(client, origin, value); + } + + private static void processValueAndUpdateRegistry(HttpClientImpl client, + Origin origin, + String altSvcValue) { + final List altServices = processHeaderValue(origin, altSvcValue); + // intentional identity check + if (altServices == CLEAR_ALL_ALT_SVCS) { + // clear all existing alt services for this origin + debug.log("clearing AltServiceRegistry for " + origin); + client.registry().clear(origin); + return; + } + debug.log("AltServices: %s", altServices); + if (altServices.isEmpty()) { + return; + } + // AltService RFC-7838, section 3.1 states: + // + // When an Alt-Svc response header field is received from an origin, its + // value invalidates and replaces all cached alternative services for + // that origin. + client.registry().replace(origin, altServices); + } + + static List getSNIServerNames(final HttpConnection conn) { + final List sniServerNames = conn.getSNIServerNames(); + if (sniServerNames != null && !sniServerNames.isEmpty()) { + return sniServerNames; + } + // no SNI server name(s) were used when establishing this connection. check if + // this connection is to a loopback address and if it is then see if a configuration + // has been set to allow alt services advertised by loopback addresses to be trusted/accepted. + // if such a configuration has been set, then we return a SNIHostName for "localhost" + final InetSocketAddress addr = conn.address(); + final boolean isLoopbackAddr = addr.isUnresolved() + ? false + : conn.address.getAddress().isLoopbackAddress(); + if (!isLoopbackAddr) { + return List.of(); // no SNI server name(s) used for this connection + } + if (!allowLocalHostOrigin) { + // this is a connection to a loopback address, with no SNI server name(s) used + // during TLS handshake and the configuration doesn't allow accepting/trusting + // alt services from loopback address, so we return no SNI server name(s) for this + // connection + return List.of(); + } + // at this point, we have identified this as a loopback address and the configuration + // has been set to accept/trust alt services from loopback address, so we return a + // SNIHostname corresponding to "localhost" + return List.of(LOCALHOST_SNI); + } + + // Here are five examples of values for the Alt-Svc header: + // String svc1 = """foo=":443"; ma=2592000; persist=1""" + // String svc2 = """h3="localhost:5678""""; + // String svc3 = """bar3=":446"; ma=2592000; persist=1"""; + // String svc4 = """h3-34=":5678"; ma=2592000; persist=1"""; + // String svc5 = "%s, %s, %s, %s".formatted(svc1, svc2, svc3, svc4); + // The last one (svc5) should result in two services being registered: + // AltService[origin=https://localhost:64077/, alpn=h3, endpoint=localhost/127.0.0.1:5678, + // deadline=2021-03-13T01:41:01.369488Z, persist=false] + // AltService[origin=https://localhost:64077/, alpn=h3-34, endpoint=localhost/127.0.0.1:5678, + // deadline=2021-04-11T01:41:01.369912Z, persist=true] + private static List processHeaderValue(final Origin origin, + final String headerValue) { + final List altServices = new ArrayList<>(); + // multiple alternate services can be specified with comma as a delimiter + final var altSvcs = headerValue.split(","); + for (var altSvc : altSvcs) { + altSvc = altSvc.trim(); + + // each value is expected to be of the following form, as noted in RFC-7838, section 3 + // Alt-Svc = clear / 1#alt-value + // clear = %s"clear"; "clear", case-sensitive + // alt-value = alternative *( OWS ";" OWS parameter ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol name + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // parameter = token "=" ( token / quoted-string ) + + // As per the spec, the value "clear" is expected to be case-sensitive + if (altSvc.equals("clear")) { + return CLEAR_ALL_ALT_SVCS; + } + final ParsedHeaderValue parsed = parseAltValue(origin, altSvc); + if (parsed == null) { + // this implies the alt-svc header value couldn't be parsed and thus is malformed. + // we skip such header values. + debug.log("skipping %s", altSvc); + continue; + } + final var deadline = getValidTill(parsed.parameters().get("ma")); + final var persist = getPersist(parsed.parameters().get("persist")); + final AltService.Identity altSvcId = new AltService.Identity(parsed.alpnName(), + parsed.host(), parsed.port()); + AltService.create(altSvcId, origin, deadline, persist) + .ifPresent((altsvc) -> { + altServices.add(altsvc); + if (Log.altsvc()) { + final var s = altsvc; + Log.logAltSvc("Created AltService: {0}", s); + } else if (debug.on()) { + debug.log("Created AltService for id=%s, origin=%s%n", altSvcId, origin); + } + }); + } + return altServices; + } + + private static ParsedHeaderValue parseAltValue(final Origin origin, final String altValue) { + // header value is expected to be of the following form, as noted in RFC-7838, section 3 + // Alt-Svc = clear / 1#alt-value + // clear = %s"clear"; "clear", case-sensitive + // alt-value = alternative *( OWS ";" OWS parameter ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol name + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // parameter = token "=" ( token / quoted-string ) + + // find the = sign that separates the protocol-id and alt-authority + debug.log("parsing %s", altValue); + final int alternativeDelimIndex = altValue.indexOf("="); + if (alternativeDelimIndex == -1 || alternativeDelimIndex == altValue.length() - 1) { + // not a valid alt value + debug.log("no \"=\" character in %s", altValue); + return null; + } + // key is always the protocol-id. example, in 'h3="localhost:5678"; ma=23232; persist=1' + // "h3" acts as the key with '"localhost:5678"; ma=23232; persist=1' as the value + final String protocolId = altValue.substring(0, alternativeDelimIndex); + // the protocol-id can be percent encoded as per the spec, so we decode it to get the alpn name + final var alpnName = decodePotentialPercentEncoded(protocolId); + debug.log("alpn is %s in %s", alpnName, altValue); + if (!isSecureALPNName(alpnName)) { + // no reasonable assurance that the alternate service will be under the control + // of the origin (section 2.1, RFC-7838) + debug.log("alpn %s is not secure, skipping", alpnName); + return null; + } + String remaining = altValue.substring(alternativeDelimIndex + 1); + // now parse alt-authority + if (!remaining.startsWith("\"") || remaining.length() == 1) { + // we expect a quoted string for alt-authority + debug.log("no quoted authority in %s", altValue); + return null; + } + remaining = remaining.substring(1); // skip the starting double quote + final int nextDoubleQuoteIndex = remaining.indexOf("\""); + if (nextDoubleQuoteIndex == -1) { + // malformed value + debug.log("missing closing quote in %s", altValue); + return null; + } + final String altAuthority = remaining.substring(0, nextDoubleQuoteIndex); + final HostPort hostPort = getHostPort(origin, altAuthority); + if (hostPort == null) return null; // host port could not be parsed + if (nextDoubleQuoteIndex == remaining.length() - 1) { + // there's nothing more left to parse + return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), Map.of()); + } + // parse the semicolon delimited parameters out of the rest of the remaining string + remaining = remaining.substring(nextDoubleQuoteIndex + 1); + final Map parameters = extractParameters(remaining); + return new ParsedHeaderValue(altValue, alpnName, hostPort.host(), hostPort.port(), parameters); + } + + private static String decodePotentialPercentEncoded(final String val) { + if (!val.contains("%")) { + return val; + } + // TODO: impl this + // In practice this method is only used for the ALPN. + // We only support h3 for now, so we do not need to + // decode percents: anything else but h3 will eventually be ignored. + return val; + } + + private static Map extractParameters(final String val) { + // As per the spec, parameters take the form of: + // *( OWS ";" OWS parameter ) + // ... + // parameter = token "=" ( token / quoted-string ) + // + // where * represents "any number of" and OWS means "optional whitespace" + + final var tokenizer = new StringTokenizer(val, ";"); + if (!tokenizer.hasMoreTokens()) { + return Map.of(); + } + Map parameters = null; + while (tokenizer.hasMoreTokens()) { + final var parameter = tokenizer.nextToken().trim(); + if (parameter.isEmpty()) { + continue; + } + final var equalSignIndex = parameter.indexOf('='); + if (equalSignIndex == -1 || equalSignIndex == parameter.length() - 1) { + // a parameter is expected to have a "=" delimiter which separates a key and a value. + // we skip parameters which don't conform to that rule + continue; + } + final var paramKey = parameter.substring(0, equalSignIndex); + final var paramValue = parameter.substring(equalSignIndex + 1); + if (parameters == null) { + parameters = new HashMap<>(); + } + parameters.put(paramKey, paramValue); + } + if (parameters == null) { + return Map.of(); + } + return Collections.unmodifiableMap(parameters); + } + + private record HostPort(String host, int port) {} + + private static HostPort getHostPort(Origin origin, String altAuthority) { + // The AltService spec defines an alt-authority as follows: + // + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + // + // When this method is called the passed altAuthority is already stripped of the leading and trailing + // double-quotes. The value will this be of the form [uri-host]:port where uri-host is optional. + String host; int port; + try { + // Use URI to do the parsing, with a special case for optional host + URI uri = new URI("http://" + altAuthority + "/"); + host = uri.getHost(); + port = uri.getPort(); + if (host == null && port == -1) { + var auth = uri.getRawAuthority(); + if (auth.isEmpty()) return null; + if (auth.charAt(0) == ':') { + uri = new URI("http://x" + altAuthority + "/"); + if ("x".equals(uri.getHost())) { + port = uri.getPort(); + } + } + } + if (port == -1) { + debug.log("Can't parse authority: " + altAuthority); + return null; + } + String hostport; + if (host == null || host.isEmpty()) { + hostport = ":" + port; + host = origin.host(); + } else { + hostport = host + ":" + port; + } + // reject anything unexpected. altAuthority should match hostport + if (!hostport.equals(altAuthority)) { + debug.log("Authority \"%s\" doesn't match host:port \"%s\"", + altAuthority, hostport); + return null; + } + } catch (URISyntaxException x) { + debug.log("Failed to parse authority: %s - %s", + altAuthority, x); + return null; + } + return new HostPort(host, port); + } + + private static Deadline getValidTill(final String maxAge) { + // There's a detailed algorithm in RFC-7234 section 4.2.3, for calculating the age. This + // RFC section is referenced from the alternate service RFC-7838 section 3.1. + // For now though, we use "now" as the instant against which the age will be applied. + final Deadline responseGenerationInstant = TimeSource.now(); + // default max age as per AltService RFC-7838, section 3.1 is 24 hours + final long defaultMaxAgeInSecs = 3600 * 24; + if (maxAge == null) { + return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs); + } + try { + final long seconds = Long.parseLong(maxAge); + // negative values aren't allowed for max-age as per RFC-7234, section 1.2.1 + return seconds < 0 ? responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs) + : responseGenerationInstant.plusSeconds(seconds); + } catch (NumberFormatException nfe) { + return responseGenerationInstant.plusSeconds(defaultMaxAgeInSecs); + } + } + + private static boolean getPersist(final String persist) { + // AltService RFC-7838, section 3.1, states: + // + // This specification only defines a single value for "persist". + // Clients MUST ignore "persist" parameters with values other than "1". + // + return "1".equals(persist); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java index 1ee54ed2bef..c50a4922e80 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java @@ -27,6 +27,7 @@ package jdk.internal.net.http; import java.io.IOException; import java.net.ProtocolException; +import java.net.http.HttpClient.Version; import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -62,6 +63,7 @@ final class Exchange { volatile ExchangeImpl exchImpl; volatile CompletableFuture> exchangeCF; volatile CompletableFuture bodyIgnored; + volatile boolean streamLimitReached; // used to record possible cancellation raised before the exchImpl // has been established. @@ -74,11 +76,18 @@ final class Exchange { final String dbgTag; // Keeps track of the underlying connection when establishing an HTTP/2 - // exchange so that it can be aborted/timed out mid setup. + // or HTTP/3 exchange so that it can be aborted/timed out mid-setup. final ConnectionAborter connectionAborter = new ConnectionAborter(); final AtomicInteger nonFinalResponses = new AtomicInteger(); + // This will be set to true only when it is guaranteed that the server hasn't processed + // the request. Typically, this happens when the server explicitly states (through a GOAWAY frame + // or a relevant error code in reset frame) that the corresponding stream (id) wasn't processed. + // However, there can be cases where the client is certain that the request wasn't sent + // to the server (and thus not processed). In such cases, the client can set this to true. + private volatile boolean unprocessedByPeer; + Exchange(HttpRequestImpl request, MultiExchange multi) { this.request = request; this.upgrading = false; @@ -110,9 +119,13 @@ final class Exchange { } // Keeps track of the underlying connection when establishing an HTTP/2 - // exchange so that it can be aborted/timed out mid setup. - static final class ConnectionAborter { + // or HTTP/3 exchange so that it can be aborted/timed out mid setup. + final class ConnectionAborter { + // In case of HTTP/3 requests we may have + // two connections in parallel: a regular TCP connection + // and a QUIC connection. private volatile HttpConnection connection; + private volatile HttpQuicConnection quicConnection; private volatile boolean closeRequested; private volatile Throwable cause; @@ -123,10 +136,11 @@ final class Exchange { // closed closeRequested = this.closeRequested; if (!closeRequested) { - this.connection = connection; - } else { - // assert this.connection == null - this.closeRequested = false; + if (connection instanceof HttpQuicConnection quicConnection) { + this.quicConnection = quicConnection; + } else { + this.connection = connection; + } } } if (closeRequested) closeConnection(connection, cause); @@ -134,6 +148,7 @@ final class Exchange { void closeConnection(Throwable error) { HttpConnection connection; + HttpQuicConnection quicConnection; Throwable cause; synchronized (this) { cause = this.cause; @@ -141,39 +156,64 @@ final class Exchange { cause = error; } connection = this.connection; - if (connection == null) { + quicConnection = this.quicConnection; + if (connection == null || quicConnection == null) { closeRequested = true; this.cause = cause; } else { + this.quicConnection = null; this.connection = null; this.cause = null; } } closeConnection(connection, cause); + closeConnection(quicConnection, cause); } + // Called by HTTP/2 after an upgrade. + // There is no upgrade for HTTP/3 HttpConnection disable() { HttpConnection connection; synchronized (this) { connection = this.connection; this.connection = null; + this.quicConnection = null; this.closeRequested = false; this.cause = null; } return connection; } - private static void closeConnection(HttpConnection connection, Throwable cause) { - if (connection != null) { - try { - connection.close(cause); - } catch (Throwable t) { - // ignore + void clear(HttpConnection connection) { + synchronized (this) { + var c = this.connection; + if (connection == c) this.connection = null; + var qc = this.quicConnection; + if (connection == qc) this.quicConnection = null; + } + } + + private void closeConnection(HttpConnection connection, Throwable cause) { + if (connection == null) { + return; + } + try { + connection.close(cause); + } catch (Throwable t) { + // ignore + if (debug.on()) { + debug.log("ignoring exception that occurred during closing of connection: " + + connection, t); } } } } + // true if previous attempt resulted in streamLimitReached + public boolean hasReachedStreamLimit() { return streamLimitReached; } + // can be used to set or clear streamLimitReached (for instance clear it after retrying) + void streamLimitReached(boolean streamLimitReached) { this.streamLimitReached = streamLimitReached; } + // Called for 204 response - when no body is permitted // This is actually only needed for HTTP/1.1 in order // to return the connection to the pool (or close it) @@ -253,7 +293,7 @@ final class Exchange { impl.cancel(cause); } else { // abort/close the connection if setting up the exchange. This can - // be important when setting up HTTP/2 + // be important when setting up HTTP/2 or HTTP/3 closeReason = failed.get(); if (closeReason != null) { connectionAborter.closeConnection(closeReason); @@ -283,6 +323,9 @@ final class Exchange { cf = exchangeCF; } } + if (multi.requestCancelled() && impl != null && cause == null) { + cause = new IOException("Request cancelled"); + } if (cause == null) return; if (impl != null) { // The exception is raised by propagating it to the impl. @@ -314,7 +357,7 @@ final class Exchange { // if upgraded, we don't close the connection. // cancelling will be handled by the HTTP/2 exchange // in its own time. - if (!upgraded) { + if (!upgraded && !(connection instanceof HttpQuicConnection)) { t = getCancelCause(); if (t == null) t = new IOException("Request cancelled"); if (debug.on()) debug.log("exchange cancelled during connect: " + t); @@ -350,8 +393,8 @@ final class Exchange { private CompletableFuture> establishExchange(HttpConnection connection) { if (debug.on()) { - debug.log("establishing exchange for %s,%n\t proxy=%s", - request, request.proxy()); + debug.log("establishing exchange for %s #%s,%n\t proxy=%s", + request, multi.id, request.proxy()); } // check if we have been cancelled first. Throwable t = getCancelCause(); @@ -364,7 +407,17 @@ final class Exchange { } CompletableFuture> cf, res; - cf = ExchangeImpl.get(this, connection); + + cf = ExchangeImpl.get(this, connection) + // set exchImpl and call checkCancelled to make sure exchImpl + // gets cancelled even if the exchangeCf was completed exceptionally + // before the CF returned by ExchangeImpl.get completed. This deals + // with issues when the request is cancelled while the exchange impl + // is being created. + .thenApply((eimpl) -> { + synchronized (Exchange.this) {exchImpl = eimpl;} + checkCancelled(); return eimpl; + }).copy(); // We should probably use a VarHandle to get/set exchangeCF // instead - as we need CAS semantics. synchronized (this) { exchangeCF = cf; }; @@ -390,7 +443,7 @@ final class Exchange { } // Completed HttpResponse will be null if response succeeded - // will be a non null responseAsync if expect continue returns an error + // will be a non-null responseAsync if expect continue returns an error public CompletableFuture responseAsync() { return responseAsyncImpl(null); @@ -715,4 +768,13 @@ final class Exchange { String dbgString() { return dbgTag; } + + final boolean isUnprocessedByPeer() { + return this.unprocessedByPeer; + } + + // Marks the exchange as unprocessed by the peer + final void markUnprocessedByPeer() { + this.unprocessedByPeer = true; + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java index f393b021cd4..74600e78557 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ExchangeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,17 +26,26 @@ package jdk.internal.net.http; import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.ResponseInfo; +import java.net.http.UnsupportedProtocolVersionException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.function.Supplier; +import jdk.internal.net.http.Http2Connection.ALPNException; import jdk.internal.net.http.common.HttpBodySubscriberWrapper; import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.MinimalFuture; import jdk.internal.net.http.common.Utils; import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; /** * Splits request so that headers and body can be sent separately with optional @@ -60,10 +69,6 @@ abstract class ExchangeImpl { private volatile boolean expectTimeoutRaised; - // this will be set to true only when the peer explicitly states (through a GOAWAY frame or - // a relevant error code in reset frame) that the corresponding stream (id) wasn't processed - private volatile boolean unprocessedByPeer; - ExchangeImpl(Exchange e) { // e == null means a http/2 pushed stream this.exchange = e; @@ -98,23 +103,414 @@ abstract class ExchangeImpl { static CompletableFuture> get(Exchange exchange, HttpConnection connection) { - if (exchange.version() == HTTP_1_1) { + HttpRequestImpl request = exchange.request(); + var version = exchange.version(); + if (version == HTTP_1_1 || request.isWebSocket()) { if (debug.on()) debug.log("get: HTTP/1.1: new Http1Exchange"); return createHttp1Exchange(exchange, connection); - } else { - Http2ClientImpl c2 = exchange.client().client2(); // #### improve - HttpRequestImpl request = exchange.request(); - CompletableFuture c2f = c2.getConnectionFor(request, exchange); + } else if (!request.secure() && request.isHttp3Only(version)) { + assert version == HTTP_3; + assert !request.isWebSocket(); if (debug.on()) - debug.log("get: Trying to get HTTP/2 connection"); - // local variable required here; see JDK-8223553 - CompletableFuture>> fxi = - c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection)); - return fxi.thenCompose(x->x); + debug.log("get: HTTP/3: HTTP/3 is not supported on plain connections"); + return MinimalFuture.failedFuture( + new UnsupportedProtocolVersionException( + "HTTP/3 is not supported on plain connections")); + } else if (version == HTTP_2 || isTCP(connection) || !request.secure()) { + assert !request.isWebSocket(); + return attemptHttp2Exchange(exchange, connection); + } else { + assert request.secure(); + assert version == HTTP_3; + assert !request.isWebSocket(); + return attemptHttp3Exchange(exchange, connection); } } + private static boolean isTCP(HttpConnection connection) { + if (connection instanceof HttpQuicConnection) return false; + if (connection == null) return false; + // if it's not an HttpQuicConnection and it's not null it's + // a TCP connection + return true; + } + + private static CompletableFuture> + attemptHttp2Exchange(Exchange exchange, HttpConnection connection) { + HttpRequestImpl request = exchange.request(); + Http2ClientImpl c2 = exchange.client().client2(); // #### improve + CompletableFuture c2f = c2.getConnectionFor(request, exchange); + if (debug.on()) + debug.log("get: Trying to get HTTP/2 connection"); + // local variable required here; see JDK-8223553 + CompletableFuture>> fxi = + c2f.handle((h2c, t) -> createExchangeImpl(h2c, t, exchange, connection)); + return fxi.thenCompose(x -> x); + } + + private static CompletableFuture> + attemptHttp3Exchange(Exchange exchange, HttpConnection connection) { + HttpRequestImpl request = exchange.request(); + var exchvers = exchange.version(); + assert request.secure() : request.uri() + " is not secure"; + assert exchvers == HTTP_3 : "expected HTTP/3, got " + exchvers; + // when we reach here, it's guaranteed that the client supports HTTP3 + assert exchange.client().client3().isPresent() : "HTTP3 isn't supported by the client"; + var client3 = exchange.client().client3().get(); + CompletableFuture c3f; + Supplier> c2fs; + var config = request.http3Discovery(); + + if (debug.on()) { + debug.log("get: Trying to get HTTP/3 connection; config is %s", config); + } + // The algorithm here depends on whether HTTP/3 is specified on + // the request itself, or on the HttpClient. + // In both cases, we may attempt a direct HTTP/3 connection if + // we don't have an H3 endpoint registered in the AltServicesRegistry. + // However, if HTTP/3 is not specified explicitly on the request, + // we will start both an HTTP/2 and an HTTP/3 connection at the + // same time, and use the one that complete first. If HTTP/3 is + // specified on the request, we will give priority to HTTP/3 ond + // only start the HTTP/2 connection if the HTTP/3 connection fails, + // or doesn't succeed in the imparted timeout. The timeout can be + // specified with the property "jdk.httpclient.http3.maxDirectConnectionTimeout". + // If unspecified it defaults to 2750ms. + // + // Because the HTTP/2 connection may start as soon as we create the + // CompletableFuture returned by the Http2Client, + // we are using a Supplier> to + // set up the call chain that would start the HTTP/2 connection. + try { + // first look to see if we already have an HTTP/3 connection in + // the pool. If we find one, we're almost done! We won't need + // to start any HTTP/2 connection. + Http3Connection pooled = client3.findPooledConnectionFor(request, exchange); + if (pooled != null) { + c3f = MinimalFuture.completedFuture(pooled); + c2fs = null; + } else { + if (debug.on()) + debug.log("get: no HTTP/3 pooled connection found"); + // possibly start an HTTP/3 connection + boolean mayAttemptDirectConnection = client3.mayAttemptDirectConnection(request); + c3f = client3.getConnectionFor(request, exchange); + if ((!c3f.isDone() || c3f.isCompletedExceptionally()) && mayAttemptDirectConnection) { + // We don't know if the server supports HTTP/3. + // happy eyeball: prepare to try both HTTP/3 and HTTP/2 and + // to use the first that succeeds + if (config != Http3DiscoveryMode.HTTP_3_URI_ONLY) { + if (debug.on()) { + debug.log("get: trying with both HTTP/3 and HTTP/2"); + } + Http2ClientImpl client2 = exchange.client().client2(); + c2fs = () -> client2.getConnectionFor(request, exchange); + } else { + if (debug.on()) { + debug.log("get: trying with HTTP/3 only"); + } + c2fs = null; + } + } else { + // We have a completed Http3Connection future. + // No need to attempt direct HTTP/3 connection. + c2fs = null; + } + } + } catch (IOException io) { + return MinimalFuture.failedFuture(io); + } + if (c2fs == null) { + // Do not attempt a happy eyeball: go the normal route to + // attempt an HTTP/3 connection + // local variable required here; see JDK-8223553 + if (debug.on()) debug.log("No HTTP/3 eyeball needed"); + CompletableFuture>> fxi = + c3f.handle((h3c, t) -> createExchangeImpl(h3c, t, exchange, connection)); + return fxi.thenCompose(x->x); + } else if (request.version().orElse(null) == HTTP_3) { + // explicit request to use HTTP/3, only use HTTP/2 if HTTP/3 fails, but + // still start both connections in parallel. HttpQuicConnection will + // attempt a direct connection. Because we register + // firstToComplete as a dependent action of c3f we will actually + // only use HTTP/2 (or HTTP/1.1) if HTTP/3 failed + CompletableFuture>> fxi = + c3f.handle((h3c, e) -> firstToComplete(exchange, connection, c2fs, c3f)); + if (debug.on()) { + debug.log("Explicit HTTP/3 request: " + + "attempt HTTP/3 first, then default to HTTP/2"); + } + return fxi.thenCompose(x->x); + } + if (debug.on()) { + debug.log("Attempt HTTP/3 and HTTP/2 in parallel, use the first that connects"); + } + // default client version is HTTP/3 - request version is not set. + // so try HTTP/3 + HTTP/2 in parallel and take the first that completes. + return firstToComplete(exchange, connection, c2fs, c3f); + } + + // Use the first connection that successfully completes. + // This is a bit hairy because HTTP/2 may be downgraded to HTTP/1 if the server + // doesn't support HTTP/2. In which case the connection attempt will succeed but + // c2f will be completed with a ALPNException. + private static CompletableFuture> firstToComplete( + Exchange exchange, + HttpConnection connection, + Supplier> c2fs, + CompletableFuture c3f) { + if (debug.on()) { + debug.log("firstToComplete(connection=%s)", connection); + debug.log("Will use the first connection that succeeds from HTTP/2 or HTTP/3"); + } + assert connection == null : "should not come here if connection is not null: " + connection; + + // Set up a completable future (cf) that will complete + // when the first HTTP/3 or HTTP/2 connection result is + // available. Error cases (when the result is exceptional) + // is handled in a dependent action of cf later below + final CompletableFuture cf; + // c3f is used for HTTP/3, c2f for HTTP/2 + final CompletableFuture c2f; + if (c3f.isDone()) { + // We already have a result for HTTP/3, consider that first; + // There's no need to start HTTP/2 yet if the result is successful. + c2f = null; + cf = c3f; + } else { + // No result for HTTP/3 yet, start HTTP/2 now and wait for the + // first that completes. + c2f = c2fs.get(); + cf = CompletableFuture.anyOf(c2f, c3f); + } + + CompletableFuture>> cfxi = cf.handle((r, t) -> { + if (debug.on()) { + debug.log("Checking which from HTTP/2 or HTTP/3 succeeded first"); + } + CompletableFuture> res; + // first check if c3f is completed successfully + if (c3f.isDone()) { + Http3Connection h3c = c3f.exceptionally((e) -> null).resultNow(); + if (h3c != null) { + // HTTP/3 success! Use HTTP/3 + if (debug.on()) { + debug.log("HTTP/3 connect completed first, using HTTP/3"); + } + res = createExchangeImpl(h3c, null, exchange, connection); + if (c2f != null) c2f.thenApply(c -> { + if (c != null) { + c.abandonStream(); + } + return c; + }); + } else { + // HTTP/3 failed! Use HTTP/2 + if (debug.on()) { + debug.log("HTTP/3 connect completed unsuccessfully," + + " either with null or with exception - waiting for HTTP/2"); + c3f.handle((r3, t3) -> { + debug.log("\tcf3: result=%s, throwable=%s", + r3, Utils.getCompletionCause(t3)); + return r3; + }).exceptionally((e) -> null).join(); + } + // c2f may be null here in the case where c3f was already completed + // when firstToComplete was called. + var h2cf = c2f == null ? c2fs.get() : c2f; + // local variable required here; see JDK-8223553 + CompletableFuture>> fxi = h2cf + .handle((h2c, e) -> createExchangeImpl(h2c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } + } else if (c2f != null && c2f.isDone()) { + Http2Connection h2c = c2f.exceptionally((e) -> null).resultNow(); + if (h2c != null) { + // HTTP/2 succeeded first! Use it. + if (debug.on()) { + debug.log("HTTP/2 connect completed first, using HTTP/2"); + } + res = createExchangeImpl(h2c, null, exchange, connection); + } else if (exchange.multi.requestCancelled()) { + // special case for when the exchange is cancelled + if (debug.on()) { + debug.log("HTTP/2 connect completed unsuccessfully, but request cancelled"); + } + CompletableFuture>> fxi = c2f + .handle((c, e) -> createExchangeImpl(c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } else { + if (debug.on()) { + debug.log("HTTP/2 connect completed unsuccessfully," + + " either with null or with exception"); + c2f.handle((r2, t2) -> { + debug.log("\tcf2: result=%s, throwable=%s", + r2, Utils.getCompletionCause(t2)); + return r2; + }).exceptionally((e) -> null).join(); + } + + // Now is the more complex stuff. + // HTTP/2 could have failed in the ALPN, but we still + // created a valid TLS connection to the server => default + // to HTTP/1.1 over TLS + HttpConnection http1Connection = null; + if (c2f.isCompletedExceptionally() && !c2f.isCancelled()) { + Throwable cause = Utils.getCompletionCause(c2f.exceptionNow()); + if (cause instanceof ALPNException alpn) { + debug.log("HTTP/2 downgraded to HTTP/1.1 - use HTTP/1.1"); + http1Connection = alpn.getConnection(); + } + } + if (http1Connection != null) { + if (debug.on()) { + debug.log("HTTP/1.1 connect completed first, using HTTP/1.1"); + } + // ALPN failed - but we have a valid HTTP/1.1 connection + // to the server: use that. + res = createHttp1Exchange(exchange, http1Connection); + } else { + if (c2f.isCompletedExceptionally()) { + // Wait for HTTP/3 to complete, potentially fallback to + // HTTP/1.1 + // local variable required here; see JDK-8223553 + debug.log("HTTP/2 completed with exception, wait for HTTP/3, " + + "possibly fallback to HTTP/1.1"); + CompletableFuture>> fxi = c3f + .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } else { + // + // r2 == null && t2 == null - which means we know the + // server doesn't support h2, and we probably already + // have an HTTP/1.1 connection to it + // + // If an HTTP/1.1 connection is available use it. + // Otherwise, wait for the HTTP/3 to complete, potentially + // fallback to HTTP/1.1 + HttpRequestImpl request = exchange.request(); + InetSocketAddress proxy = Utils.resolveAddress(request.proxy()); + InetSocketAddress addr = request.getAddress(); + ConnectionPool pool = exchange.client().connectionPool(); + // if we have an HTTP/1.1 connection in the pool, use that. + http1Connection = pool.getConnection(true, addr, proxy); + if (http1Connection != null && http1Connection.isOpen()) { + debug.log("Server doesn't support HTTP/2, " + + "but we have an HTTP/1.1 connection in the pool"); + debug.log("Using HTTP/1.1"); + res = createHttp1Exchange(exchange, http1Connection); + } else { + // we don't have anything ready to use in the pool: + // wait for http/3 to complete, possibly falling back + // to HTTP/1.1 + debug.log("Server doesn't support HTTP/2, " + + "and we do not have an HTTP/1.1 connection"); + debug.log("Waiting for HTTP/3, possibly fallback to HTTP/1.1"); + CompletableFuture>> fxi = c3f + .handle((h3c, e) -> fallbackToHttp1OnTimeout(h3c, e, exchange, connection)); + res = fxi.thenCompose(x -> x); + } + } + } + } + } else { + assert c2f != null; + Throwable failed = t != null ? t : new InternalError("cf1 or cf2 should have completed"); + res = MinimalFuture.failedFuture(failed); + } + return res; + }); + return cfxi.thenCompose(x -> x); + } + + private static CompletableFuture> + fallbackToHttp1OnTimeout(Http3Connection c, + Throwable t, + Exchange exchange, + HttpConnection connection) { + if (t != null) { + Throwable cause = Utils.getCompletionCause(t); + if (cause instanceof HttpConnectTimeoutException) { + // when we reach here we already tried with HTTP/2, + // and we most likely have an HTTP/1.1 connection in + // the idle pool. So fallback to that. + if (debug.on()) { + debug.log("HTTP/3 connection timed out: fall back to HTTP/1.1"); + } + return createHttp1Exchange(exchange, null); + } + } + return createExchangeImpl(c, t, exchange, connection); + } + + + + // Creates an HTTP/3 exchange, possibly downgrading to HTTP/2 + private static CompletableFuture> + createExchangeImpl(Http3Connection c, + Throwable t, + Exchange exchange, + HttpConnection connection) { + if (debug.on()) + debug.log("handling HTTP/3 connection creation result"); + if (t == null && exchange.multi.requestCancelled()) { + return MinimalFuture.failedFuture(new IOException("Request cancelled")); + } + if (c == null && t == null) { + if (debug.on()) + debug.log("downgrading to HTTP/2"); + return attemptHttp2Exchange(exchange, connection); + } else if (t != null) { + t = Utils.getCompletionCause(t); + if (debug.on()) { + if (t instanceof HttpConnectTimeoutException || t instanceof ConnectException) { + debug.log("HTTP/3 connection creation failed: " + t); + } else { + debug.log("HTTP/3 connection creation failed " + + "with unexpected exception:", t); + } + } + return MinimalFuture.failedFuture(t); + } else { + if (debug.on()) + debug.log("creating HTTP/3 exchange"); + try { + if (exchange.hasReachedStreamLimit()) { + // clear the flag before attempting to create a stream again + exchange.streamLimitReached(false); + } + return c.createStream(exchange) + .thenApply(ExchangeImpl::checkCancelled); + } catch (IOException e) { + return MinimalFuture.failedFuture(e); + } + } + } + + private static > T checkCancelled(T exchangeImpl) { + Exchange e = exchangeImpl.getExchange(); + if (debug.on()) { + debug.log("checking cancellation for: " + exchangeImpl); + } + if (e.multi.requestCancelled()) { + if (debug.on()) { + debug.log("request was cancelled"); + } + if (!exchangeImpl.isCanceled()) { + if (debug.on()) { + debug.log("cancelling exchange: " + exchangeImpl); + } + var cause = e.getCancelCause(); + if (cause == null) cause = new IOException("Request cancelled"); + exchangeImpl.cancel(cause); + } + } + return exchangeImpl; + } + + + // Creates an HTTP/2 exchange, possibly downgrading to HTTP/1 private static CompletableFuture> createExchangeImpl(Http2Connection c, Throwable t, @@ -280,12 +676,4 @@ abstract class ExchangeImpl { // an Expect-Continue void expectContinueFailed(int rcode) { } - final boolean isUnprocessedByPeer() { - return this.unprocessedByPeer; - } - - // Marks the exchange as unprocessed by the peer - final void markUnprocessedByPeer() { - this.unprocessedByPeer = true; - } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java new file mode 100644 index 00000000000..3eb4d631c75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/H3FrameOrderVerifier.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; + +/** + * Verifies that when a HTTP3 frame arrives on a stream, then that particular frame type + * is in the expected order as compared to the previous frame type that was received. + * In effect, does what the RFC-9114, section 4.1 and section 6.2.1 specifies. + * Note that the H3FrameOrderVerifier is only responsible for checking the order in which a + * frame type is received on a stream. It isn't responsible for checking if that particular frame + * type is expected to be received on a particular stream type. + */ +abstract class H3FrameOrderVerifier { + long currentProcessingFrameType = -1; // -1 implies no frame being processed currently + long lastCompletedFrameType = -1; // -1 implies no frame processing has completed yet + + /** + * {@return a frame order verifier for HTTP3 request/response stream} + */ + static H3FrameOrderVerifier newForRequestResponseStream() { + return new ResponseStreamVerifier(false); + } + + /** + * {@return a frame order verifier for HTTP3 push promise stream} + */ + static H3FrameOrderVerifier newForPushPromiseStream() { + return new ResponseStreamVerifier(true); + } + + /** + * {@return a frame order verifier for HTTP3 control stream} + */ + static H3FrameOrderVerifier newForControlStream() { + return new ControlStreamVerifier(); + } + + /** + * @param frame The frame that has been received + * {@return true if the {@code frameType} processing can start. false otherwise} + */ + abstract boolean allowsProcessing(final Http3Frame frame); + + /** + * Marks the receipt of complete content of a frame that was currently being processed + * + * @param frame The frame whose content was fully received + * @throws IllegalStateException If the passed frame type wasn't being currently processed + */ + void completed(final Http3Frame frame) { + if (frame instanceof UnknownFrame) { + return; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != frameType) { + throw new IllegalStateException("Unexpected completion of processing " + + "of frame type (" + frameType + "): " + + Http3FrameType.asString(frameType) + ", expected " + + Http3FrameType.asString(currentProcessingFrameType)); + } + currentProcessingFrameType = -1; + lastCompletedFrameType = frameType; + } + + private static final class ControlStreamVerifier extends H3FrameOrderVerifier { + + @Override + boolean allowsProcessing(final Http3Frame frame) { + if (frame instanceof MalformedFrame) { + // a malformed frame can come in any time, so we allow it to be processed + // and we don't "track" it either + return true; + } + if (frame instanceof UnknownFrame) { + // unknown frames can come in any time, we allow them to be processed + // and we don't track their processing/completion. However, if an unknown frame + // is the first frame on a control stream then that's an error and we return "false" + // to prevent processing that frame. + // RFC-9114, section 9, which states - "where a known frame type is required to be + // in a specific location, such as the SETTINGS frame as the first frame of the + // control stream, an unknown frame type does not satisfy that requirement and + // SHOULD be treated as an error" + return lastCompletedFrameType != -1; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != -1) { + // we are in the middle of processing a particular frame type and we + // only expect additional frames of only that type + return frameType == currentProcessingFrameType; + } + // we are not currently processing any frame + if (lastCompletedFrameType == -1) { + // there was no previous frame either, so this is the first frame to have been + // received + if (frameType != SettingsFrame.TYPE) { + // unexpected first frame type + return false; + } + currentProcessingFrameType = frameType; + // expected first frame type + return true; + } + // there's no specific ordering specified on control stream other than expecting + // the SETTINGS frame to be the first received (which we have already verified before + // reaching here) + currentProcessingFrameType = frameType; + return true; + } + } + + private static final class ResponseStreamVerifier extends H3FrameOrderVerifier { + private boolean headerSeen; + private boolean dataSeen; + private boolean trailerCompleted; + private final boolean pushStream; + + private ResponseStreamVerifier(boolean pushStream) { + this.pushStream = pushStream; + } + + @Override + boolean allowsProcessing(final Http3Frame frame) { + if (frame instanceof MalformedFrame) { + // a malformed frame can come in any time, so we allow it to be processed + // and we don't track their processing/completion + return true; + } + if (frame instanceof UnknownFrame) { + // unknown frames can come in any time, we allow them to be processed + // and we don't track their processing/completion + return true; + } + final long frameType = frame.type(); + if (currentProcessingFrameType != -1) { + // we are in the middle of processing a particular frame type and we + // only expect additional frames of only that type + return frameType == currentProcessingFrameType; + } + if (frameType == DataFrame.TYPE) { + if (!headerSeen || trailerCompleted) { + // DATA is not permitted before HEADERS or after trailer + return false; + } + dataSeen = true; + } else if (frameType == HeadersFrame.TYPE) { + if (trailerCompleted) { + // HEADERS is not permitted after trailer + return false; + } + headerSeen = true; + if (dataSeen) { + trailerCompleted = true; + } + } else if (frameType == PushPromiseFrame.TYPE) { + // a push promise is only permitted on a response, + // and not on a push stream + if (pushStream) { + return false; + } + } else { + // no other frames permitted + return false; + } + + currentProcessingFrameType = frameType; + return true; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java index ecc4a63c9d0..02ce63b6314 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Exchange.java @@ -244,7 +244,7 @@ class Http1Exchange extends ExchangeImpl { this.connection = connection; } else { InetSocketAddress addr = request.getAddress(); - this.connection = HttpConnection.getConnection(addr, client, request, HTTP_1_1); + this.connection = HttpConnection.getConnection(addr, client, exchange, request, HTTP_1_1); } this.requestAction = new Http1Request(request, this); this.asyncReceiver = new Http1AsyncReceiver(executor, this); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java index 92a48d901ff..cc8a2a7142b 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java @@ -76,7 +76,7 @@ class Http2ClientImpl { /** * When HTTP/2 requested only. The following describes the aggregate behavior including the - * calling code. In all cases, the HTTP2 connection cache + * calling code. In all cases, the HTTP/2 connection cache * is checked first for a suitable connection and that is returned if available. * If not, a new connection is opened, except in https case when a previous negotiate failed. * In that case, we want to continue using http/1.1. When a connection is to be opened and @@ -144,6 +144,7 @@ class Http2ClientImpl { if (conn != null) { try { conn.reserveStream(true, exchange.pushEnabled()); + exchange.connectionAborter.clear(conn.connection); } catch (IOException e) { throw new UncheckedIOException(e); // shouldn't happen } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java index c33cc93e7dd..63889fa6af2 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java @@ -33,6 +33,7 @@ import java.lang.invoke.VarHandle; import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -70,6 +71,7 @@ import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.common.ValidatingHeadersConsumer; import jdk.internal.net.http.common.ValidatingHeadersConsumer.Context; +import jdk.internal.net.http.frame.AltSvcFrame; import jdk.internal.net.http.frame.ContinuationFrame; import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.ErrorFrame; @@ -90,6 +92,7 @@ import jdk.internal.net.http.hpack.Decoder; import jdk.internal.net.http.hpack.DecodingCallback; import jdk.internal.net.http.hpack.Encoder; import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame; import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH; import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE; import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_CONNECTION_WINDOW_SIZE; @@ -527,6 +530,7 @@ class Http2Connection { AbstractAsyncSSLConnection connection = (AbstractAsyncSSLConnection) HttpConnection.getConnection(request.getAddress(), h2client.client(), + exchange, request, HttpClient.Version.HTTP_2); @@ -635,6 +639,32 @@ class Http2Connection { return true; } + void abandonStream() { + boolean shouldClose = false; + stateLock.lock(); + try { + long reserved = --numReservedClientStreams; + assert reserved >= 0; + if (finalStream && reserved == 0 && streams.isEmpty()) { + shouldClose = true; + } + } catch (Throwable t) { + shutdown(t); // in case the assert fires... + } finally { + stateLock.unlock(); + } + + // We should close the connection here if + // it's not pooled. If it's not pooled it will + // be marked final stream, reserved will be 0 + // after decrementing it by one, and there should + // be no active request-response streams. + if (shouldClose) { + shutdown(new IOException("HTTP/2 connection abandoned")); + } + + } + boolean shouldClose() { stateLock.lock(); try { @@ -1218,6 +1248,8 @@ class Http2Connection { case PingFrame.TYPE -> handlePing((PingFrame) frame); case GoAwayFrame.TYPE -> handleGoAway((GoAwayFrame) frame); case WindowUpdateFrame.TYPE -> handleWindowUpdate((WindowUpdateFrame) frame); + case AltSvcFrame.TYPE -> processAltSvcFrame(0, (AltSvcFrame) frame, + connection, connection.client()); default -> protocolError(ErrorFrame.PROTOCOL_ERROR); } @@ -1323,7 +1355,8 @@ class Http2Connection { try { // idleConnectionTimeoutEvent is always accessed within a lock protected block if (streams.isEmpty() && idleConnectionTimeoutEvent == null) { - idleConnectionTimeoutEvent = client().idleConnectionTimeout() + final HttpClient.Version version = Version.HTTP_2; + idleConnectionTimeoutEvent = client().idleConnectionTimeout(version) .map(IdleConnectionTimeoutEvent::new) .orElse(null); if (idleConnectionTimeoutEvent != null) { @@ -1367,6 +1400,7 @@ class Http2Connection { String protocolError = "protocol error" + (msg == null?"":(": " + msg)); ProtocolException protocolException = new ProtocolException(protocolError); + this.cause.compareAndSet(null, protocolException); if (markHalfClosedLocal()) { framesDecoder.close(protocolError); subscriber.stop(protocolException); @@ -1844,8 +1878,16 @@ class Http2Connection { } finally { Throwable x = errorRef.get(); if (x != null) { - if (debug.on()) debug.log("Stopping scheduler", x); scheduler.stop(); + if (client2.stopping()) { + if (debug.on()) { + debug.log("Stopping scheduler"); + } + } else { + if (debug.on()) { + debug.log("Stopping scheduler", x); + } + } Http2Connection.this.shutdown(x); } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java new file mode 100644 index 00000000000..05b27c3d529 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientImpl.java @@ -0,0 +1,844 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.UnsupportedProtocolVersionException; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.ConnectionExpiredException; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.quic.QuicTLSContext; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static jdk.internal.net.http.Http3ClientProperties.WAIT_FOR_PENDING_CONNECT; +import static jdk.internal.net.http.common.Alpns.H3; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi; + +/** + * Http3 specific aspects of HttpClientImpl + */ +final class Http3ClientImpl implements AutoCloseable { + // Setting this property disables HTTPS hostname verification. Use with care. + private static final boolean disableHostnameVerification = Utils.isHostnameVerificationDisabled(); + // QUIC versions in their descending order of preference + private static final List availableQuicVersions; + static { + // we default to QUIC v1 followed by QUIC v2, if no specific preference cannot be + // determined + final List defaultPref = List.of(QuicVersion.QUIC_V1, QuicVersion.QUIC_V2); + // check user specified preference + final String sysPropVal = Utils.getProperty("jdk.httpclient.quic.available.versions"); + if (sysPropVal == null || sysPropVal.isBlank()) { + // default to supporting both v1 and v2, with v1 given preference + availableQuicVersions = defaultPref; + } else { + final List descendingPref = new ArrayList<>(); + for (final String val : sysPropVal.split(",")) { + final QuicVersion qv; + try { + // parse QUIC version number represented as a hex string + final var vernum = Integer.parseInt(val.trim(), 16); + qv = QuicVersion.of(vernum).orElse(null); + } catch (NumberFormatException nfe) { + // ignore and continue with next + continue; + } + if (qv == null) { + continue; + } + descendingPref.add(qv); + } + availableQuicVersions = descendingPref.isEmpty() ? defaultPref : descendingPref; + } + } + + private final Logger debug = Utils.getDebugLogger(this::dbgString); + + final HttpClientImpl client; + private final Http3ConnectionPool connections = new Http3ConnectionPool(debug); + private final Http3PendingConnections reconnections = new Http3PendingConnections(); + private final Set pendingClose = ConcurrentHashMap.newKeySet(); + private final Set noH3 = ConcurrentHashMap.newKeySet(); + + private final QuicClient quicClient; + private volatile boolean closed; + private final AtomicReference errorRef = new AtomicReference<>(); + private final ReentrantLock lock = new ReentrantLock(); + + Http3ClientImpl(HttpClientImpl client) { + this.client = client; + var executor = client.theExecutor().safeDelegate(); + var context = client.theSSLContext(); + var parameters = client.sslParameters(); + if (!disableHostnameVerification) { + // setting the endpoint identification algo to HTTPS ensures that + // during the TLS handshake, the cert presented by the server is verified + // for hostname checks against the SNI hostname(s) set by the client + // or in its absence the peer's hostname. + // see sun.security.ssl.X509TrustManagerImpl#checkIdentity(...) + parameters.setEndpointIdentificationAlgorithm("HTTPS"); + } + final QuicTLSContext quicTLSContext = new QuicTLSContext(context); + final QuicClient.Builder builder = new QuicClient.Builder(); + builder.availableVersions(availableQuicVersions) + .tlsContext(quicTLSContext) + .sslParameters(parameters) + .executor(executor) + .applicationErrors(Http3Error::stringForCode) + .clientId(client.dbgString()); + if (client.localAddress() != null) { + builder.bindAddress(new InetSocketAddress(client.localAddress(), 0)); + } + final QuicTransportParameters transportParameters = new QuicTransportParameters(); + // HTTP/3 doesn't allow remote bidirectional stream + transportParameters.setIntParameter(initial_max_streams_bidi, 0); + // HTTP/3 doesn't allow remote bidirectional stream: no need to allow data + transportParameters.setIntParameter(initial_max_stream_data_bidi_remote, 0); + builder.transportParameters(transportParameters); + this.quicClient = builder.build(); + } + + // Records an exchange waiting for a connection recovery to complete. + // A connection recovery happens when a connection has maxed out its number + // of streams, and no MAX_STREAM frame has arrived. In that case, the connection + // is abandoned (marked with setFinalStream() and taken out of the pool) and a + // new connection is initiated. Waiters are waiting for the new connection + // handshake to finish and for the connection to be put in the pool. + record Waiter(MinimalFuture cf, HttpRequestImpl request, Exchange exchange) { + void complete(Http3Connection conn, Throwable error) { + if (error != null) cf.completeExceptionally(error); + else cf.complete(conn); + } + static Waiter of(HttpRequestImpl request, Exchange exchange) { + return new Waiter(new MinimalFuture<>(), request, exchange); + } + } + + // Indicates that recovery is needed, or in progress, for a given + // connection + sealed interface ConnectionRecovery permits PendingConnection, StreamLimitReached { + } + + // Indicates that recovery of a connection has been initiated. + // Waiters will be put in wait until the handshake is completed + // and the connection is inserted in the pool + record PendingConnection(AltService altSvc, Exchange exchange, ConcurrentLinkedQueue waiters) + implements ConnectionRecovery { + PendingConnection(AltService altSvc, Exchange exchange, ConcurrentLinkedQueue waiters) { + this.altSvc = altSvc; + this.waiters = Objects.requireNonNull(waiters); + this.exchange = exchange; + } + PendingConnection(AltService altSvc, Exchange exchange) { + this(altSvc, exchange, new ConcurrentLinkedQueue<>()); + } + } + + // Indicates that a connection that was in the pool has maxed out + // its stream limit and will be taken out of the pool. A new connection + // will be created for the first request/response exchange that needs + // it. + record StreamLimitReached(Http3Connection connection) implements ConnectionRecovery {} + + // Called when recovery is needed for a given connection, with + // the request that got the StreamLimitException + public void streamLimitReached(Http3Connection connection, HttpRequestImpl request) { + lock.lock(); + try { + reconnections.streamLimitReached(connectionKey(request), connection); + } finally { + lock.unlock(); + } + } + + HttpClientImpl client() { + return client; + } + + String dbgString() { + return "Http3ClientImpl(" + client.dbgString() + ")"; + } + + QuicClient quicClient() { + return this.quicClient; + } + + String connectionKey(HttpRequestImpl request) { + return connections.connectionKey(request); + } + + Http3Connection findPooledConnectionFor(HttpRequestImpl request, + Exchange exchange) + throws IOException { + if (request.secure() && request.proxy() == null) { + final var pooled = connections.lookupFor(request); + if (pooled == null) { + return null; + } + if (pooled.tryReserveForPoolCheckout() && !pooled.isFinalStream()) { + final var altService = pooled.connection() + .getSourceAltService().orElse(null); + if (altService != null) { + // if this connection was created because it was advertised by some alt-service + // then verify that the alt-service is still valid/active + if (altService.wasAdvertised() && !client.registry().isActive(altService)) { + if (debug.on()) { + debug.log("Alt-Service %s for pooled connection has expired," + + " marking the connection as unusable for new streams", altService); + } + // alt-service that was the reason for this H3 connection to be created (and pooled) + // is no longer valid. We set a state on the connection to disallow any new streams + // and be auto-closed when all current streams are done + pooled.setFinalStreamAndCloseIfIdle(); + return null; + } + } + if (debug.on()) { + debug.log("Found Http3Connection in connection pool"); + } + // found a valid connection in pool, return it + return pooled; + } else { + if (debug.on()) { + debug.log("Pooled connection expired. Removing it."); + } + removeFromPool(pooled); + } + } + return null; + } + + private static String label(Http3Connection conn) { + return Optional.ofNullable(conn) + .map(Http3Connection::connection) + .map(HttpQuicConnection::label) + .orElse("null"); + } + + private static String describe(HttpRequestImpl request, long id) { + return String.format("%s #%s", request, id); + } + + private static String describe(Exchange exchange) { + if (exchange == null) return "null"; + return describe(exchange.request, exchange.multi.id); + } + + private static String describePendingExchange(String prefix, PendingConnection pending) { + return String.format("%s %s", prefix, describe(pending.exchange)); + } + + private static String describeAltSvc(PendingConnection pendingConnection) { + return Optional.ofNullable(pendingConnection) + .map(PendingConnection::altSvc) + .map(AltService::toString) + .map(s -> "altsvc: " + s) + .orElse("no altSvc"); + } + + // Called after a recovered connection has been put back in the pool + // (or when recovery has failed), or when a new connection handshake + // has completed. + // Waiters, if any, will be notified. + private void connectionCompleted(String connectionKey, Exchange origExchange, Http3Connection conn, Throwable error) { + try { + if (Log.http3()) { + Log.logHttp3("Checking waiters on completed connection {0} to {1} created for {2}", + label(conn), connectionKey, describe(origExchange)); + } + connectionCompleted0(connectionKey, origExchange, conn, error); + } catch (Throwable t) { + if (Log.http3() || Log.errors()) { + Log.logError(t); + } + throw t; + } + } + + private void connectionCompleted0(String connectionKey, Exchange origExchange, Http3Connection conn, Throwable error) { + lock.lock(); + // There should be a connection in the pool at this point, + // so we can remove the PendingConnection from the reconnections list; + PendingConnection pendingConnection = null; + try { + var recovery = reconnections.removeCompleted(connectionKey, origExchange, conn); + if (recovery instanceof PendingConnection pending) { + pendingConnection = pending; + } + } finally { + lock.unlock(); + } + if (pendingConnection == null) { + if (Log.http3()) { + Log.logHttp3("No waiters to complete for " + label(conn)); + } + return; + } + + int waitersCount = pendingConnection.waiters.size(); + if (waitersCount != 0 && Log.http3()) { + Log.logHttp3("Completing " + waitersCount + + " waiters on recreated connection " + label(conn) + + describePendingExchange(" - originally created for", pendingConnection)); + } + + // now for each waiter we're going to try to complete it. + // however, there may be more waiters than available streams! + // so it's rinse and repeat at this point + boolean origExchangeCancelled = origExchange == null ? false : origExchange.multi.requestCancelled(); + int completedWaiters = 0; + int errorWaiters = 0; + int retriedWaiters = 0; + try { + while (!pendingConnection.waiters.isEmpty()) { + var waiter = pendingConnection.waiters.poll(); + if (error != null && (!origExchangeCancelled || waiter.exchange == origExchange)) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + error); + } else if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with " + conn + " error=" + error); + } + errorWaiters++; + waiter.complete(conn, error); + } else { + var request = waiter.request; + var exchange = waiter.exchange; + try { + Http3Connection pooled = findPooledConnectionFor(request, exchange); + if (pooled != null && !pooled.isFinalStream() && !waiter.cf.isDone()) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + label(pooled)); + } else if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with pooled conn " + label(pooled)); + } + completedWaiters++; + waiter.cf.complete(pooled); + } else if (!waiter.cf.isDone()) { + // we call getConnectionFor: it should put waiter in the + // new waiting list, or attempt to open a connection again + if (conn != null) { + if (Log.http3()) { + Log.logHttp3("Not enough streams on recreated connection for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + label(conn)); + } else if (debug.on()) { + debug.log("Not enough streams on recreated connection for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with " + label(conn) + + ": retrying on new connection"); + } + retriedWaiters++; + getConnectionFor(request, exchange, waiter); + } else { + if (Log.http3()) { + Log.logHttp3("No HTTP/3 connection for:: " + waiter.request + " #" + + waiter.exchange.multi.id + ": will downgrade or fail"); + } else if (debug.on()) { + debug.log("No HTTP/3 connection for: " + waiter.request + + " #" + waiter.exchange.multi.id + ": will downgrade or fail"); + } + completedWaiters++; + waiter.complete(null, error); + } + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Completing waiter for: " + waiter.request + + " #" + waiter.exchange.multi.id + " with error: " + + Utils.getCompletionCause(t)); + } + var cause = Utils.getCompletionCause(t); + if (cause instanceof ClosedChannelException) { + cause = new ConnectionExpiredException(cause); + } + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + waiter.request + " #" + + waiter.exchange.multi.id + " with " + cause); + } + errorWaiters++; + waiter.cf.completeExceptionally(cause); + } + } + } + } finally { + if (Log.http3()) { + String pendingInfo = describePendingExchange(" - originally created for", pendingConnection); + + if (conn != null) { + Log.logHttp3(("Connection creation completed for requests to %s: " + + "waiters[%s](completed:%s, retried:%s, errors:%s)%s") + .formatted(connectionKey, waitersCount, completedWaiters, + retriedWaiters, errorWaiters, pendingInfo)); + } else { + Log.logHttp3(("No HTTP/3 connection created for requests to %s, will fail or downgrade: " + + "waiters[%s](completed:%s, retried:%s, errors:%s)%s") + .formatted(connectionKey, waitersCount, completedWaiters, + retriedWaiters, errorWaiters, pendingInfo)); + } + } + } + } + + CompletableFuture getConnectionFor(HttpRequestImpl request, Exchange exchange) { + assert request != null; + return getConnectionFor(request, exchange, null); + } + + private void completeWaiter(Logger debug, Waiter pendingWaiter, Http3Connection r, Throwable t) { + // the recovery was done on behalf of a pending waiter. + // this can happen if the new connection has already maxed out, + // and recovery was initiated on behalf of the next waiter. + if (Log.http3()) { + Log.logHttp3("Completing waiter for: " + pendingWaiter.request + " #" + + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")"); + } else if (debug.on()) { + debug.log("Completing pending waiter for " + pendingWaiter.request + " #" + + pendingWaiter.exchange.multi.id + " with (conn: " + label(r) + " error: " + t +")"); + } + pendingWaiter.complete(r, t); + } + + private CompletableFuture wrapForDebug(CompletableFuture h3Cf, + Exchange exchange, + HttpRequestImpl request) { + if (debug.on() || Log.http3()) { + if (Log.http3()) { + Log.logHttp3("Recreating connection for: " + request + " #" + + exchange.multi.id); + } else if (debug.on()) { + debug.log("Recreating connection for: " + request + " #" + + exchange.multi.id); + } + return h3Cf.whenComplete((r, t) -> { + if (Log.http3()) { + if (r != null && t == null) { + Log.logHttp3("Connection recreated for " + request + " #" + + exchange.multi.id + " on " + label(r)); + } else if (t != null) { + Log.logHttp3("Connection creation failed for " + request + " #" + + exchange.multi.id + ": " + t); + } else if (r == null) { + Log.logHttp3("No connection found for " + request + " #" + + exchange.multi.id); + } + } else if (debug.on()) { + debug.log("Connection recreated for " + request + " #" + + exchange.multi.id); + } + }); + } else { + return h3Cf; + } + } + + Optional lookupAltSvc(HttpRequestImpl request) { + return client.registry() + .lookup(request.uri(), H3::equals) + .findFirst(); + } + + CompletableFuture getConnectionFor(HttpRequestImpl request, + Exchange exchange, + Waiter pendingWaiter) { + assert request != null; + if (Log.http3()) { + if (pendingWaiter != null) { + Log.logHttp3("getConnectionFor pendingWaiter {0}", + describe(pendingWaiter.request, pendingWaiter.exchange.multi.id)); + } else { + Log.logHttp3("getConnectionFor exchange {0}", + describe(request, exchange.multi.id)); + } + } + try { + Http3Connection pooled = findPooledConnectionFor(request, exchange); + if (pooled != null) { + if (pendingWaiter != null) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } else if (debug.on()) { + debug.log("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } + pendingWaiter.cf.complete(pooled); + return pendingWaiter.cf; + } else { + return MinimalFuture.completedFuture(pooled); + } + } + if (request.secure() && request.proxy() == null) { + boolean reconnecting, waitForPendingConnect; + PendingConnection pendingConnection = null; + String key; + Waiter waiter = null; + if (reconnecting = exchange.hasReachedStreamLimit()) { + if (debug.on()) { + debug.log("Exchange has reached limit for: " + request + " #" + + exchange.multi.id); + } + } + if (pendingWaiter != null) reconnecting = true; + lock.lock(); + try { + key = connectionKey(request); + + var recovery = reconnections.lookupFor(key, request, client); + if (debug.on()) debug.log("lookup found %s for %s", recovery, request); + if (recovery instanceof PendingConnection pending) { + // Recovery already initiated. Add waiter to the list! + if (debug.on()) { + debug.log("PendingConnection (%s) found for %s", + describePendingExchange("originally created for", pending), + describe(request, exchange.multi.id)); + } + pendingConnection = pending; + waiter = pendingWaiter == null + ? Waiter.of(request, exchange) + : pendingWaiter; + exchange.streamLimitReached(false); + pendingConnection.waiters.add(waiter); + return waiter.cf; + } else if (recovery instanceof StreamLimitReached) { + // A connection to this server has maxed out its allocated + // streams and will be taken out of the pool, but recovery + // has not been initiated yet. Do that now. + reconnecting = waitForPendingConnect = true; + } else waitForPendingConnect = WAIT_FOR_PENDING_CONNECT; + // By default, we allow concurrent attempts to + // create HTTP/3 connections to the same host, except when + // one connection has reached the maximum number of streams + // it is allowed to use. However, + // if waitForPendingConnect is set to `true` above we will + // only allow one connection to attempt handshake at a given + // time, other requests will be added to a pending list so + // that they can go through that connection. + if (waitForPendingConnect) { + // check again + if ((pooled = findPooledConnectionFor(request, exchange)) == null) { + // initiate recovery + var altSvc = lookupAltSvc(request).orElse(null); + // maybe null if ALT_SVC && altSvc == null + pendingConnection = reconnections.addPending(key, request, altSvc, exchange); + } else if (pendingWaiter != null) { + if (Log.http3()) { + Log.logHttp3("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } else if (debug.on()) { + debug.log("Completing pending waiter for: " + request + " #" + + exchange.multi.id + " with " + pooled.dbgTag()); + } + pendingWaiter.cf.complete(pooled); + return pendingWaiter.cf; + } else { + return MinimalFuture.completedFuture(pooled); + } + } + } finally { + lock.unlock(); + if (waiter != null && waiter != pendingWaiter && Log.http3()) { + var altSvc = describeAltSvc(pendingConnection); + var orig = Optional.of(pendingConnection) + .map(PendingConnection::exchange) + .map(e -> " created for #" + e.multi.id) + .orElse(""); + Log.logHttp3("Waiting for connection for: " + describe(request, exchange.multi.id) + + " " + altSvc + orig); + } else if (pendingWaiter != null && Log.http3()) { + var altSvc = describeAltSvc(pendingConnection); + Log.logHttp3("Creating connection for: " + describe(request, exchange.multi.id) + + " " + altSvc); + } else if (debug.on() && waiter != null) { + debug.log("Waiting for connection for: " + describe(request, exchange.multi.id) + + (waiter == pendingWaiter ? " (still pending)" : "")); + } + } + + if (Log.http3()) { + Log.logHttp3("Creating connection for Exchange {0}", describe(exchange)); + } else if (debug.on()) { + debug.log("Creating connection for Exchange %s", describe(exchange)); + } + + CompletableFuture h3Cf = Http3Connection + .createAsync(request, this, exchange); + if (reconnecting) { + // System.err.println("Recreating connection for: " + request + " #" + // + exchange.multi.id); + h3Cf = wrapForDebug(h3Cf, exchange, request); + } + if (pendingWaiter != null) { + // the connection was done on behalf of a pending waiter. + // this can happen if the new connection has already maxed out, + // and recovery was initiated on behalf of the next waiter. + h3Cf = h3Cf.whenComplete((r,t) -> completeWaiter(debug, pendingWaiter, r, t)); + } + h3Cf = h3Cf.thenApply(conn -> { + if (conn != null) { + if (debug.on()) { + debug.log("Offering connection %s created for %s", + label(conn), exchange.multi.id); + } + var offered = offerConnection(conn); + if (debug.on()) { + debug.log("Connection offered %s created for %s", + label(conn), exchange.multi.id); + } + // if we return null here, we will downgrade + // but if we return `conn` we will open a new connection. + return offered == null ? conn : offered; + } else { + if (debug.on()) { + debug.log("No connection for exchange #" + exchange.multi.id); + } + return null; + } + }); + if (pendingConnection != null) { + // need to wake up waiters after successful handshake and recovery + h3Cf = h3Cf.whenComplete((r, t) -> connectionCompleted(key, exchange, r, t)); + } + return h3Cf; + } else { + if (debug.on()) + debug.log("Request is unsecure, or proxy isn't null: can't use HTTP/3"); + if (request.isHttp3Only(exchange.version())) { + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "can't use HTTP/3 with proxied or unsecured connection")); + } + return MinimalFuture.completedFuture(null); + } + } catch (Throwable t) { + if (Log.http3() || Log.errors()) { + Log.logError("Failed to get connection for {0}: {1}", + describe(exchange), t); + } + return MinimalFuture.failedFuture(t); + } + } + + /* + * Cache the given connection, if no connection to the same + * destination exists. If one exists, then we let the initial stream + * complete but allow it to close itself upon completion. + * This situation should not arise with https because the request + * has not been sent as part of the initial alpn negotiation + */ + Http3Connection offerConnection(Http3Connection c) { + if (debug.on()) debug.log("offering to the connection pool: %s", c); + if (!c.isOpen() || c.isFinalStream()) { + if (debug.on()) + debug.log("skipping offered closed or closing connection: %s", c); + return null; + } + + String key = c.key(); + lock.lock(); + try { + if (closed) { + var error = errorRef.get(); + if (error == null) error = new IOException("client closed"); + c.connectionError(error, Http3Error.H3_INTERNAL_ERROR); + return null; + } + Http3Connection c1 = connections.putIfAbsent(key, c); + if (c1 != null) { + // there was a connection in the pool + if (!c1.isFinalStream() || c.isFinalStream()) { + if (!c.isFinalStream()) { + c.allowOnlyOneStream(); + return c; + } else if (c1.isFinalStream()) { + return c; + } + if (debug.on()) + debug.log("existing entry %s in connection pool for %s", c1, key); + // c1 will remain in the pool and we will use c for the given + // request. + if (Log.http3()) { + Log.logHttp3("Existing connection {0} for {1} found in the pool", label(c1), c1.key()); + Log.logHttp3("New connection {0} marked final and not offered to the pool", label(c)); + } + return c1; + } + connections.put(key, c); + } + if (debug.on()) + debug.log("put in the connection pool: %s", c); + return c; + } finally { + lock.unlock(); + } + } + + void removeFromPool(Http3Connection c) { + lock.lock(); + try { + if (connections.remove(c.key(), c)) { + if (debug.on()) + debug.log("removed from the connection pool: %s", c); + } + if (c.isOpen()) { + if (debug.on()) + debug.log("adding to pending close: %s", c); + pendingClose.add(c); + } + } finally { + lock.unlock(); + } + } + + void connectionClosed(Http3Connection c) { + removeFromPool(c); + if (pendingClose.remove(c)) { + if (debug.on()) + debug.log("removed from pending close: %s", c); + } + } + + public Logger debug() { return debug;} + + @Override + public void close() { + try { + lock.lock(); + try { + closed = true; + pendingClose.clear(); + connections.clear(); + } finally { + lock.unlock(); + } + // The client itself is being closed, so we don't individually close the connections + // here and instead just close the QuicClient which then initiates the close of + // the QUIC endpoint. That will silently terminate the underlying QUIC connections + // without exchanging any datagram packets with the peer, since there's no point + // sending/receiving those (including GOAWAY frame) when the endpoint (socket channel) + // itself won't be around after this point. + } finally { + quicClient.close(); + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + if (debug.on()) { + debug.log("HTTP/3 client aborting due to " + t); + } + try { + errorRef.compareAndSet(null, t); + List connectionList; + lock.lock(); + try { + closed = true; + connectionList = new ArrayList<>(connections.values().toList()); + connectionList.addAll(pendingClose); + pendingClose.clear(); + connections.clear(); + } finally { + lock.unlock(); + } + for (var conn : connectionList) { + conn.close(t); + } + } finally { + quicClient.abort(t); + } + } + + public void stop() { + close(); + } + + /** + * After an unsuccessful H3 direct connection attempt, + * mark the authority as not supporting h3. + * @param rawAuthority the raw authority (host:port) + */ + public void noH3(String rawAuthority) { + noH3.add(rawAuthority); + } + + /** + * Tells whether the given authority has been marked as + * not supporting h3 + * @param rawAuthority the raw authority (host:port) + * @return true if the given authority is believed to not support h3 + */ + public boolean hasNoH3(String rawAuthority) { + return noH3.contains(rawAuthority); + } + + /** + * A direct HTTP/3 attempt may be attempted if we don't have an + * AltService h3 endpoint recorded for it, and if the given request + * URI's raw authority hasn't been marked as not supporting HTTP/3, + * and if the request discovery config is not ALT_SVC. + * Note that a URI may be marked has not supporting H3 if it doesn't + * acknowledge the first initial quic packet in the time defined + * by {@systemProperty jdk.httpclient.http3.maxDirectConnectionTimeout}. + * @param request the request that may go through h3 + * @return true if there's no h3 endpoint already registered for the given uri. + */ + public boolean mayAttemptDirectConnection(HttpRequestImpl request) { + var config = request.http3Discovery(); + return switch (config) { + // never attempt direct connection with ALT_SVC + case Http3DiscoveryMode.ALT_SVC -> false; + // always attempt direct connection with HTTP_3_ONLY, unless + // it was attempted before and failed + case Http3DiscoveryMode.HTTP_3_URI_ONLY -> + !hasNoH3(request.uri().getRawAuthority()); + // otherwise, attempt direct connection only if we have no + // alt service and it wasn't attempted and failed before + default -> lookupAltSvc(request).isEmpty() + && !hasNoH3(request.uri().getRawAuthority()); + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java new file mode 100644 index 00000000000..81f8c8109d5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ClientProperties.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import jdk.internal.net.http.common.Utils; + +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY; + +/** + * A class that groups initial values for HTTP/3 client properties. + *

        + * Properties starting with {@code jdk.internal.} are not exposed and + * typically reserved for testing. They could be removed, and their name, + * semantics, or values, could be changed at any time. + *

        + * Properties that are exposed are JDK specifics and typically documented + * in the {@link java.net.http} module API documentation. + *

          + *
        1. + *
        2. + *
        + * + * @apiNote + * Not all properties are exposed. Properties that are not included in + * the {@link java.net.http} module API documentation are subject to + * change, and should be considered internal, though we might also consider + * exposing them in the future if needed. + * + */ +public final class Http3ClientProperties { + + private Http3ClientProperties() { + throw new InternalError("should not come here"); + } + + // The maximum timeout to wait for a reply to the first INITIAL + // packet when attempting a direct connection + public static final long MAX_DIRECT_CONNECTION_TIMEOUT; + + // The maximum timeout to wait for a MAX_STREAM frame + // before throwing StreamLimitException + public static final long MAX_STREAM_LIMIT_WAIT_TIMEOUT; + + // The maximum number of concurrent push streams + // by connection + public static final long MAX_HTTP3_PUSH_STREAMS; + + // Limit for dynamic table capacity that the encoder is allowed + // to set. Its capacity is also limited by the QPACK_MAX_TABLE_CAPACITY + // HTTP/3 setting value received from the peer decoder. + public static final long QPACK_ENCODER_TABLE_CAPACITY_LIMIT; + + // The value of SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long QPACK_DECODER_MAX_TABLE_CAPACITY; + + // The value of SETTINGS_MAX_FIELD_SECTION_SIZE HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long QPACK_DECODER_MAX_FIELD_SECTION_SIZE; + + // Decoder upper bound on the number of streams that can be blocked + public static final long QPACK_DECODER_BLOCKED_STREAMS; + + // of available space in the dynamic table + + // Percentage of occupied space in the dynamic table that controls when + // the draining index starts increasing. This index determines which entries + // are too close to eviction, and can be referenced by the encoder. + public static final int QPACK_ENCODER_DRAINING_THRESHOLD; + + // If set to "true" allows the encoder to insert a header with a dynamic + // name reference and reference it in a field line section without awaiting + // decoder's acknowledgement. + public static final boolean QPACK_ALLOW_BLOCKING_ENCODING = Utils.getBooleanProperty( + "jdk.internal.httpclient.qpack.allowBlockingEncoding", false); + + // whether localhost is acceptable as an alternative service origin + public static final boolean ALTSVC_ALLOW_LOCAL_HOST_ORIGIN = Utils.getBooleanProperty( + "jdk.httpclient.altsvc.allowLocalHostOrigin", true); + + // whether concurrent HTTP/3 requests to the same host should wait for + // first connection to succeed (or fail) instead of attempting concurrent + // connections. Where concurrent connections are attempted, only one of + // them will be offered to the connection pool. The others will serve a + // single request. + public static final boolean WAIT_FOR_PENDING_CONNECT = Utils.getBooleanProperty( + "jdk.httpclient.http3.waitForPendingConnect", true); + + + static { + // 375 is ~ to the initial loss timer + // 1000 is ~ the initial PTO + // We will set a timeout of 2*1375 ms to wait for the reply to our + // first initial packet for a direct connection + long defaultMaxDirectConnectionTimeout = 1375 << 1; // ms + long maxDirectConnectionTimeout = Utils.getLongProperty( + "jdk.httpclient.http3.maxDirectConnectionTimeout", + defaultMaxDirectConnectionTimeout); + long maxStreamLimitTimeout = Utils.getLongProperty( + "jdk.httpclient.http3.maxStreamLimitTimeout", + defaultMaxDirectConnectionTimeout); + int defaultMaxHttp3PushStreams = Utils.getIntegerProperty( + "jdk.httpclient.maxstreams", + 100); + int maxHttp3PushStreams = Utils.getIntegerProperty( + "jdk.httpclient.http3.maxConcurrentPushStreams", + defaultMaxHttp3PushStreams); + long defaultDecoderMaxCapacity = 0; + long decoderMaxTableCapacity = Utils.getLongProperty( + "jdk.httpclient.qpack.decoderMaxTableCapacity", + defaultDecoderMaxCapacity); + long decoderBlockedStreams = Utils.getLongProperty( + "jdk.httpclient.qpack.decoderBlockedStreams", + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS); + long defaultEncoderTableCapacityLimit = 4096; + long encoderTableCapacityLimit = Utils.getLongProperty( + "jdk.httpclient.qpack.encoderTableCapacityLimit", + defaultEncoderTableCapacityLimit); + int defaultDecoderMaxFieldSectionSize = 393216; // 384kB + long decoderMaxFieldSectionSize = Utils.getIntegerNetProperty( + "jdk.http.maxHeaderSize", Integer.MIN_VALUE, Integer.MAX_VALUE, + defaultDecoderMaxFieldSectionSize, true); + // Percentage of occupied space in the dynamic table that when + // exceeded the dynamic table draining index starts increasing + int drainingThreshold = Utils.getIntegerProperty( + "jdk.internal.httpclient.qpack.encoderDrainingThreshold", + 75); + + MAX_DIRECT_CONNECTION_TIMEOUT = maxDirectConnectionTimeout <= 0 + ? defaultMaxDirectConnectionTimeout : maxDirectConnectionTimeout; + MAX_STREAM_LIMIT_WAIT_TIMEOUT = maxStreamLimitTimeout < 0 + ? defaultMaxDirectConnectionTimeout + : maxStreamLimitTimeout; + MAX_HTTP3_PUSH_STREAMS = Math.max(maxHttp3PushStreams, 0); + QPACK_ENCODER_TABLE_CAPACITY_LIMIT = encoderTableCapacityLimit < 0 + ? defaultEncoderTableCapacityLimit : encoderTableCapacityLimit; + QPACK_DECODER_MAX_TABLE_CAPACITY = decoderMaxTableCapacity < 0 ? + DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY : decoderMaxTableCapacity; + QPACK_DECODER_MAX_FIELD_SECTION_SIZE = decoderMaxFieldSectionSize < 0 ? + DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE : decoderMaxFieldSectionSize; + QPACK_DECODER_BLOCKED_STREAMS = decoderBlockedStreams < 0 ? + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS : decoderBlockedStreams; + QPACK_ENCODER_DRAINING_THRESHOLD = Math.clamp(drainingThreshold, 10, 90); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java new file mode 100644 index 00000000000..b97a441881d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Connection.java @@ -0,0 +1,1657 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.net.http.StreamLimitException; +import java.net.http.UnsupportedProtocolVersionException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import jdk.internal.net.http.Http3PushManager.CancelPushReason; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.CancelPushFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.GoAwayFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.MaxPushIdFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.http3.streams.PeerUniStreamDispatcher; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicStreamLimitException; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.http.quic.streams.QuicStreams; +import static java.net.http.HttpClient.Version.HTTP_3; +import static jdk.internal.net.http.Http3ClientProperties.MAX_STREAM_LIMIT_WAIT_TIMEOUT; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_NO_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; + +/** + * An HTTP/3 connection wraps an HttpQuicConnection and implements + * HTTP/3 on top it. + */ +public final class Http3Connection implements AutoCloseable { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3ClientImpl client; + private final HttpQuicConnection connection; + private final QuicConnection quicConnection; + // key by which this connection will be referred to within the connection pool + private final String connectionKey; + private final String dbgTag; + private final UniStreamPair controlStreamPair; + private final UniStreamPair qpackEncoderStreams; + private final UniStreamPair qpackDecoderStreams; + private final Encoder qpackEncoder; + private final Decoder qpackDecoder; + private final FramesDecoder controlFramesDecoder; + private final Predicate remoteStreamListener; + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForControlStream(); + // streams for HTTP3 exchanges + private final ConcurrentMap exchangeStreams = new ConcurrentHashMap<>(); + private final ConcurrentMap> exchanges = new ConcurrentHashMap<>(); + // true when the settings frame has been received on the control stream of this connection + private volatile boolean settingsFrameReceived; + // the settings we received from the peer + private volatile ConnectionSettings peerSettings; + // the settings we send to our peer + private volatile ConnectionSettings ourSettings; + // for tests + private final MinimalFuture peerSettingsCF = new MinimalFuture<>(); + // the (lowest) request stream id received in GOAWAY frames on this connection. + // subsequent request stream id(s) (if any) must always be equal to lesser than this value + // as per spec + // -1 is used to imply no GOAWAY received so far + private final AtomicLong lowestGoAwayReceipt = new AtomicLong(-1); + private volatile IdleConnectionTimeoutEvent idleConnectionTimeoutEvent; + // value of true implies no more streams will be initiated on this connection, + // and the connection will be closed once the in-progress streams complete. + private volatile boolean finalStream; + private volatile boolean allowOnlyOneStream; + // set to true if we decide to open a new connection + // due to stream limit reached + private volatile boolean streamLimitReached; + + private static final int GOAWAY_SENT = 1; // local endpoint sent GOAWAY + private static final int GOAWAY_RECEIVED = 2; // received GOAWAY from remote peer + private static final int CLOSED = 4; // close called on QUIC connection + volatile int closedState; + + private final ReentrantLock lock = new ReentrantLock(); + private final Http3PushManager pushManager; + private final AtomicLong reservedStreamCount = new AtomicLong(); + + // The largest pushId for a remote created stream. + // After GOAWAY has been sent, we will not accept + // any larger pushId. + private final AtomicLong largestPushId = new AtomicLong(); + + // The max pushId for which a frame was scheduled to be sent. + // This should always be less or equal to pushManager.maxPushId + private final AtomicLong maxPushIdSent = new AtomicLong(); + + + /** + * Creates a new HTTP/3 connection over a given {@link HttpQuicConnection}. + * + * @apiNote + * This constructor is invoked upon a successful quic connection establishment, + * typically after a successful Quic handshake. Creating the Http3Connection + * earlier, for instance, after receiving the Server Hello, could also be considered. + * + * @implNote + * Creating an HTTP/3 connection will trigger the creation of the HTTP/3 control + * stream, sending of the HTTP/3 Settings frame, and creation of the QPack + * encoder/decoder streams. + * + * @param request the request which triggered the creation of the connection + * @param client the Http3Client instance this connection belongs to + * @param connection the {@code HttpQuicConnection} that was established + */ + Http3Connection(HttpRequestImpl request, Http3ClientImpl client, HttpQuicConnection connection) { + this.connectionKey = client.connectionKey(request); + this.client = client; + this.connection = connection; + this.quicConnection = connection.quicConnection(); + var qdb = quicConnection.dbgTag(); + this.dbgTag = "H3(" + qdb +")"; + this.pushManager = new Http3PushManager(this); // OK to leak this + controlFramesDecoder = new FramesDecoder("H3-control("+qdb+")", + FramesDecoder::isAllowedOnControlStream); + controlStreamPair = new UniStreamPair( + StreamType.CONTROL, + quicConnection, + this::processPeerControlBytes, + this::lcsWriterLoop, + this::controlStreamFailed, + debug); + + qpackEncoder = new Encoder(Http3Connection::shouldUpdateDynamicTable, + this::createEncoderStreams, this::connectionError); + qpackEncoderStreams = qpackEncoder.encoderStreams(); + qpackDecoder = new Decoder(this::createDecoderStreams, this::connectionError); + qpackDecoderStreams = qpackDecoder.decoderStreams(); + // Register listener to be called when the peer opens a new stream + remoteStreamListener = this::onOpenRemoteStream; + quicConnection.addRemoteStreamListener(remoteStreamListener); + + // Registers dependent actions with the controlStreamPair + // .futureSenderStreamWriter() CF, in order to send + // the SETTINGS and MAX_PUSHID frames. + // These actions will be executed when the stream writer is + // available. + // + // This will schedule the SETTINGS and MAX_PUSHID frames + // for writing, buffering them if necessary until control + // flow credits are available. + // + // If an exception happens the connection will be + // closed abruptly (by closing the underlying quic connection) + // with an error of type Http3Error.H3_INTERNAL_ERROR + controlStreamPair.futureSenderStreamWriter() + // Send SETTINGS first + .thenApply(this::sendSettings) + // Chains to sending MAX_PUSHID after SETTINGS + .thenApply(this::sendMaxPushId) + // arranges for the connection to be closed + // in case of exception. Throws in the dependent + // action after wrapping the exception if needed. + .exceptionally(this::exceptionallyAndClose); + if (Log.http3()) { + Log.logHttp3("HTTP/3 connection created for " + quicConnectionTag() + " - local address: " + + quicConnection.localAddress()); + } + } + + public String quicConnectionTag() { + return quicConnection.logTag(); + } + + private static boolean shouldUpdateDynamicTable(TableEntry tableEntry) { + if (tableEntry.type() == TableEntry.EntryType.NAME_VALUE) { + return false; + } + return switch (tableEntry.name().toString()) { + case ":authority", "user-agent" -> !tableEntry.value().isEmpty(); + default -> false; + }; + } + + private void lock() { + lock.lock(); + } + + private void unlock() { + lock.unlock(); + } + + /** + * Debug tag used to create the debug logger for this + * HTTP/3 connection instance. + * + * @return a debug tag + */ + String dbgTag() { + return dbgTag; + } + + /** + * Asynchronously create an instance of an HTTP/3 connection, if the + * server has a known HTTP/3 endpoint. + * @param request the first request that will go over this connection + * @param h3client the HTTP/3 client + * @param exchange the exchange for which this connection is created + * @return a completable future that will be completed with a new + * HTTP/3 connection, or {@code null} if no usable HTTP/3 endpoint + * was found, or completed exceptionally if an error occurred + */ + static CompletableFuture createAsync(HttpRequestImpl request, + Http3ClientImpl h3client, + Exchange exchange) { + assert request.secure(); + final HttpConnection connection = HttpConnection.getConnection(request.getAddress(), + h3client.client(), + exchange, + request, + HTTP_3); + var debug = h3client.debug(); + var where = "Http3Connection.createAsync"; + if (!(connection instanceof HttpQuicConnection httpQuicConnection)) { + if (Log.http3()) { + Log.logHttp3("{0}: Connection for {1} #{2} is not an HttpQuicConnection: {3}", + where, request, exchange.multi.id, connection); + } + if (debug.on()) + debug.log("%s: Connection is not an HttpQuicConnection: %s", where, connection); + if (request.isHttp3Only(exchange.version())) { + assert connection == null; + // may happen if the client doesn't support HTTP3 + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "cannot establish exchange to requested origin with HTTP/3")); + } + return MinimalFuture.completedFuture(null); + } + if (debug.on()) { + debug.log("%s: Got HttpQuicConnection: %s", where, connection); + } + if (Log.http3()) { + Log.logHttp3("{0}: Got HttpQuicConnection for {1} #{2} is: {3}", + where, request, exchange.multi.id, connection.label()); + } + + // Expose the underlying connection to the exchange's aborter so it can + // be closed if a timeout occurs. + exchange.connectionAborter.connection(httpQuicConnection); + + return httpQuicConnection.connectAsync(exchange) + .thenCompose(unused -> httpQuicConnection.finishConnect()) + .thenCompose(unused -> checkSSLConfig(httpQuicConnection)) + .thenCompose(notused-> { + CompletableFuture cf = new MinimalFuture<>(); + try { + if (debug.on()) + debug.log("creating Http3Connection for %s", httpQuicConnection); + Http3Connection hc = new Http3Connection(request, h3client, httpQuicConnection); + if (!hc.isFinalStream()) { + exchange.connectionAborter.clear(httpQuicConnection); + cf.complete(hc); + } else { + var io = new IOException("can't reserve first stream"); + if (Log.http3()) { + Log.logHttp3(" Unable to use HTTP/3 connection over {0}: {1}", + hc.quicConnectionTag(), + io); + } + hc.protocolError(io); + cf.complete(null); + } + } catch (Exception e) { + cf.completeExceptionally(e); + } + return cf; } ) + .whenComplete(httpQuicConnection::connectionEstablished); + } + + private static CompletableFuture checkSSLConfig(HttpQuicConnection quic) { + // HTTP/2 checks ALPN here; with HTTP/3, we only offer one ALPN, + // and TLS verifies that it's negotiated. + + // We can examine the negotiated parameters here and possibly fail + // if they are not satisfactory. + return MinimalFuture.completedFuture(null); + } + + HttpQuicConnection connection() { + return connection; + } + + String key() { + return connectionKey; + } + + /** + * Whether the final stream (last stream allowed on a connection), has + * been set. + * + * @return true if the final stream has been set. + */ + boolean isFinalStream() { + return this.finalStream; + } + + /** + * Sets the final stream to be the next stream opened on + * the connection. No other stream will be opened after this. + */ + void setFinalStream() { + this.finalStream = true; + } + + void setFinalStreamAndCloseIfIdle() { + boolean closeNow; + lock(); + try { + setFinalStream(); + closeNow = finalStreamClosed(); + } finally { + unlock(); + } + if (closeNow) close(); + } + + void allowOnlyOneStream() { + lock(); + try { + if (isFinalStream()) return; + this.allowOnlyOneStream = true; + this.finalStream = true; + } finally { + unlock(); + } + } + + boolean isOpen() { + return closedState == 0 && quicConnection.isOpen(); + } + + private IOException checkConnectionError() { + final TerminationCause tc = quicConnection.terminationCause(); + return tc == null ? null : tc.getCloseCause(); + } + + // Used only by tests + CompletableFuture peerSettingsCF() { + return peerSettingsCF; + } + + private boolean reserveStream() { + lock(); + try { + boolean allowStream0 = this.allowOnlyOneStream; + this.allowOnlyOneStream = false; + if (finalStream && !allowStream0) { + return false; + } + reservedStreamCount.incrementAndGet(); + return true; + } finally { + unlock(); + } + } + + CompletableFuture> + createStream(final Exchange exchange) throws IOException { + // check if this connection is closing before initiating this new stream + if (!reserveStream()) { + if (Log.http3()) { + Log.logHttp3("Cannot initiate new stream on connection {0} for exchange {1}", + quicConnectionTag(), exchange); + } + // we didn't create the stream and thus the server hasn't yet processed this request. + // mark the request as unprocessed to allow it to be retried on a different connection. + exchange.markUnprocessedByPeer(); + String message = "cannot initiate additional new streams on chosen connection"; + IOException cause = streamLimitReached + ? new StreamLimitException(HTTP_3, message) + : new IOException(message); + return MinimalFuture.failedFuture(cause); + } + // TODO: this duration is currently "computed" from the request timeout duration. + // this computation needs a bit more thought + final Duration streamLimitIncreaseDuration = exchange.request.timeout() + .map((reqTimeout) -> reqTimeout.dividedBy(2)) + .orElse(Duration.ofMillis(MAX_STREAM_LIMIT_WAIT_TIMEOUT)); + final CompletableFuture bidiStream = + quicConnection.openNewLocalBidiStream(streamLimitIncreaseDuration); + // once the bidi stream creation completes: + // - if completed exceptionally, we transform any QuicStreamLimitException into a + // StreamLimitException + // - if completed successfully, we create a Http3 exchange and return that as the result + final CompletableFuture>> h3ExchangeCf = + bidiStream.handle((stream, t) -> { + if (t == null) { + // no exception occurred and a bidi stream was created on the quic + // connection, but check if the connection has been terminated + // in the meantime + final var terminationCause = checkConnectionError(); + if (terminationCause != null) { + // connection already closed and we haven't yet issued the request. + // mark the exchange as unprocessed to allow it to be retried on + // a different connection. + exchange.markUnprocessedByPeer(); + return MinimalFuture.failedFuture(terminationCause); + } + // creation of bidi stream succeeded, now create the H3 exchange impl + // and return it + final Http3ExchangeImpl h3Exchange = createHttp3ExchangeImpl(exchange, stream); + return MinimalFuture.completedFuture(h3Exchange); + } + // failed to open a bidi stream + reservedStreamCount.decrementAndGet(); + final Throwable cause = Utils.getCompletionCause(t); + if (cause instanceof QuicStreamLimitException) { + if (Log.http3()) { + Log.logHttp3("Maximum stream limit reached on {0} for exchange {1}", + quicConnectionTag(), exchange.multi.streamLimitState()); + } + if (debug.on()) { + debug.log("bidi stream creation failed due to stream limit: " + + cause + ", connection will be marked as unusable for subsequent" + + " requests"); + } + // Since we have reached the stream creation limit (which translates to not + // being able to initiate new requests on this connection), we mark the + // connection as "final stream" (i.e. don't consider this (pooled) + // connection for subsequent requests) + this.streamLimitReachedWith(exchange); + return MinimalFuture.failedFuture(new StreamLimitException(HTTP_3, + "No more streams allowed on connection")); + } else if (cause instanceof ClosedChannelException) { + // stream creation failed due to the connection (that was chosen) + // got closed. Thus the request wasn't processed by the server. + // mark the request as unprocessed to allow it to be + // initiated on a different connection + exchange.markUnprocessedByPeer(); + return MinimalFuture.failedFuture(cause); + } + return MinimalFuture.failedFuture(cause); + }); + return h3ExchangeCf.thenCompose(Function.identity()); + } + + private void streamLimitReachedWith(Exchange exchange) { + streamLimitReached = true; + client.streamLimitReached(this, exchange.request); + setFinalStream(); + } + + private Http3ExchangeImpl createHttp3ExchangeImpl(Exchange exchange, QuicBidiStream stream) { + if (debug.on()) { + debug.log("Temporary reference h3 stream: " + stream.streamId()); + } + if (Log.http3()) { + Log.logHttp3("Creating HTTP/3 exchange for {0}/streamId={1}", + quicConnectionTag(), Long.toString(stream.streamId())); + } + client.client.h3StreamReference(); + try { + lock(); + try { + this.exchangeStreams.put(stream.streamId(), stream); + reservedStreamCount.decrementAndGet(); + var te = idleConnectionTimeoutEvent; + if (te != null) { + client.client().cancelTimer(te); + idleConnectionTimeoutEvent = null; + } + } finally { + unlock(); + } + var http3Exchange = new Http3ExchangeImpl<>(this, exchange, stream); + return registerAndStartExchange(http3Exchange); + } finally { + if (debug.on()) { + debug.log("Temporary unreference h3 stream: " + stream.streamId()); + } + client.client.h3StreamUnreference(); + } + } + + private Http3ExchangeImpl registerAndStartExchange(Http3ExchangeImpl exchange) { + var streamId = exchange.streamId(); + if (debug.on()) debug.log("Reference h3 stream: " + streamId); + client.client.h3StreamReference(); + exchanges.put(streamId, exchange); + exchange.start(); + return exchange; + } + + // marks this connection as no longer available for creating additional streams. current + // streams will run to completion. marking the connection as gracefully shutdown + // can involve sending the necessary protocol message(s) to the peer. + private void sendGoAway() throws IOException { + if (markSentGoAway()) { + // already sent (either successfully or an attempt was made) GOAWAY, nothing more to do + return; + } + // RFC-9114, section 5.2: Endpoints initiate the graceful shutdown of an HTTP/3 connection + // by sending a GOAWAY frame. + final QuicStreamWriter writer = controlStreamPair.localWriter(); + if (writer != null && quicConnection.isOpen()) { + try { + // We send here the largest pushId for which the peer has + // opened a stream. We won't process pushIds larger than that, and + // we will later cancel any pending push promises anyway. + final long lastProcessedPushId = largestPushId.get(); + final GoAwayFrame goAwayFrame = new GoAwayFrame(lastProcessedPushId); + final long size = goAwayFrame.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + final var buf = ByteBuffer.allocate((int) size); + goAwayFrame.writeFrame(buf); + buf.flip(); + if (debug.on()) { + debug.log("Sending GOAWAY frame %s from client connection %s", goAwayFrame, this); + } + writer.scheduleForWriting(buf, false); + } catch (Exception e) { + // ignore - we couldn't send a GOAWAY + if (debug.on()) { + debug.log("Failed to send GOAWAY from client " + this, e); + } + Log.logError("Could not send a GOAWAY from client {0}", this); + Log.logError(e); + } + } + } + + @Override + public void close() { + try { + sendGoAway(); + } catch (IOException ioe) { + // log and ignore the failure + // failure to send a GOAWAY shouldn't prevent closing a connection + if (debug.on()) { + debug.log("failed to send a GOAWAY frame before initiating a close: " + ioe); + } + } + // TODO: ideally we should hava flushForClose() which goes all the way to terminator to flush + // streams and increasing the chances of GOAWAY being sent. + // check RFC-9114, section 5.3 which seems to allow including GOAWAY and CONNECTION_CLOSE + // frames in same packet (optionally) + close(Http3Error.H3_NO_ERROR, "H3 connection closed - no error"); + } + + void close(final Throwable throwable) { + close(H3_INTERNAL_ERROR, null, throwable); + } + + void close(final Http3Error error, final String message) { + if (error != H3_NO_ERROR) { + // construct a ProtocolException representing the connection termination cause + final ProtocolException cause = new ProtocolException(message); + close(error, message, cause); + } else { + close(error, message, null); + } + } + + void close(final Http3Error error, final String logMsg, + final Throwable closeCause) { + if (!markClosed()) { + // already closed, nothing to do + return; + } + if (debug.on()) { + debug.log("Closing HTTP/3 connection: %s %s %s", error, logMsg == null ? "" : logMsg, + closeCause == null ? "" : closeCause.toString()); + debug.log("State is: " + describeClosedState(closedState)); + } + exchanges.values().forEach(e -> e.recordError(closeCause)); + // close the underlying QUIC connection + connection.close(error.code(), logMsg, closeCause); + final TerminationCause tc = connection.quicConnection.terminationCause(); + assert tc != null : "termination cause is null"; + // close all HTTP streams + exchanges.values().forEach(exchange -> exchange.cancelImpl(tc.getCloseCause(), error)); + pushManager.cancelAllPromises(tc.getCloseCause(), error); + discardConnectionState(); + // No longer wait for reading HTTP/3 stream types: + // stop waiting on any stream for which we haven't received the stream + // type yet. + try { + var listener = remoteStreamListener; + if (listener != null) { + quicConnection.removeRemoteStreamListener(listener); + } + } finally { + client.connectionClosed(this); + } + if (!peerSettingsCF.isDone()) { + peerSettingsCF.completeExceptionally(tc.getCloseCause()); + } + } + + private void discardConnectionState() { + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + qpackDecoderStreams.stopSchedulers(); + qpackEncoderStreams.stopSchedulers(); + } + + private boolean markClosed() { + return markClosedState(CLOSED); + } + + void protocolError(IOException error) { + connectionError(error, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + void connectionError(Throwable throwable, Http3Error error) { + connectionError(null, throwable, error.code(), null); + } + + void connectionError(Http3Stream exchange, Throwable throwable, long errorCode, + String logMsg) { + final Optional error = Http3Error.fromCode(errorCode); + assert error.isPresent() : "not a HTTP3 error code: " + errorCode; + close(error.get(), logMsg, throwable); + } + + public String toString() { + return String.format("Http3Connection(%s)", connection()); + } + + private boolean finalStreamClosed() { + lock(); + try { + return this.finalStream && this.exchangeStreams.isEmpty() && this.reservedStreamCount.get() == 0; + } finally { + unlock(); + } + } + + /** + * Called by the {@link Http3ExchangeImpl} when the exchange is closed. + * + * @param streamId The request stream id + */ + void onExchangeClose(Http3ExchangeImpl exch, final long streamId) { + // we expect it to be a request/response stream + if (!(QuicStreams.isClientInitiated(streamId) && QuicStreams.isBidirectional(streamId))) { + throw new IllegalArgumentException("Not a client initiated bidirectional stream"); + } + if (this.exchangeStreams.remove(streamId) != null) { + if (connection().quicConnection().isOpen()) { + qpackDecoder.cancelStream(streamId); + } + decrementStreamsCount(exch, streamId); + exchanges.remove(streamId); + } + + if (finalStreamClosed()) { + // no more streams open on this connection. close the connection + if (Log.http3()) { + Log.logHttp3("Closing HTTP/3 connection {0} on final stream (streamId={1})", + quicConnectionTag(), Long.toString(streamId)); + } + // close will take care of canceling all pending push promises + // if any push promises are left pending + close(); + } else { + if (Log.http3()) { + Log.logHttp3("HTTP/3 connection {0} left open: exchanged streamId={1} closed; " + + "finalStream={2}, exchangeStreams={3}, reservedStreamCount={4}", + quicConnectionTag(), Long.toString(streamId), finalStream, + exchangeStreams.size(), reservedStreamCount.get()); + } + lock(); + try { + var te = idleConnectionTimeoutEvent; + if (te == null && exchangeStreams.isEmpty()) { + te = idleConnectionTimeoutEvent = client.client().idleConnectionTimeout(HTTP_3) + .map(IdleConnectionTimeoutEvent::new).orElse(null); + if (te != null) { + client.client().registerTimer(te); + } + } + } finally { + unlock(); + } + } + } + + void decrementStreamsCount(Http3ExchangeImpl exch, long streamid) { + if (exch.deRegister()) { + debug.log("Unreference h3 stream: " + streamid); + client.client.h3StreamUnreference(); + } else { + debug.log("Already unreferenced h3 stream: " + streamid); + } + } + + // Called from Http3PushPromiseStream::start (via Http3ExchangeImpl) + void onPushPromiseStreamStarted(Http3PushPromiseStream http3PushPromiseStream, long streamId) { + // HTTP/3 push promises are not refcounted. + // At the moment an ongoing push promise will not prevent the client + // to exit normally, if all request-response streams are finished. + // Here would be the place to increment ref-counting if we wanted to + } + + // Called by Http3PushPromiseStream::close + void onPushPromiseStreamClosed(Http3PushPromiseStream http3PushPromiseStream, long streamId) { + // HTTP/3 push promises are not refcounted. + // At the moment an ongoing push promise will not prevent the client + // to exit normally, if all request-response streams are finished. + // Here would be the place to decrement ref-counting if we wanted to + if (connection().quicConnection().isOpen()) { + qpackDecoder.cancelStream(streamId); + } + } + + /** + * A class used to dispatch peer initiated unidirectional streams + * according to their HTTP/3 stream type. + * The type of an HTTP/3 unidirectional stream is determined by + * reading a variable length integer code off the stream, which + * indicates the type of stream. + * @see Http3Streams + */ + private final class Http3StreamDispatcher extends PeerUniStreamDispatcher { + Http3StreamDispatcher(QuicReceiverStream stream) { + super(stream); + } + + @Override + protected Logger debug() { return debug; } + + @Override + protected void onStreamAbandoned(QuicReceiverStream stream) { + if (debug.on()) debug.log("Stream " + stream.streamId() + " abandoned!"); + qpackDecoder.cancelStream(stream.streamId()); + } + + @Override + protected void onControlStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, controlStreamPair.futureReceiverStream()); + } + + @Override + protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, qpackDecoderStreams.futureReceiverStream()); + } + + @Override + protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) { + complete(description, stream, qpackEncoderStreams.futureReceiverStream()); + } + + @Override + protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) { + Http3Connection.this.onPushStreamCreated(stream, pushId); + } + + // completes the given completable future with the given stream + private void complete(String description, QuicReceiverStream stream, CompletableFuture cf) { + debug.log("completing CF for %s with stream %s", description, stream.streamId()); + boolean completed = cf.complete(stream); + if (!completed) { + if (!cf.isCompletedExceptionally()) { + debug.log("CF for %s already completed with stream %s!", description, cf.resultNow().streamId()); + close(Http3Error.H3_STREAM_CREATION_ERROR, + "%s already created".formatted(description)); + } else { + debug.log("CF for %s already completed exceptionally!", description); + } + } + } + + /** + * Dispatches the given remote initiated unidirectional stream to the + * given Http3Connection after reading the stream type off the stream. + * + * @param conn the Http3Connection with which the stream is associated + * @param stream a newly opened remote unidirectional stream. + */ + static CompletableFuture dispatch(Http3Connection conn, QuicReceiverStream stream) { + assert stream.isRemoteInitiated(); + assert !stream.isBidirectional(); + var dispatcher = conn.new Http3StreamDispatcher(stream); + dispatcher.start(); + return dispatcher.dispatchCF(); + } + } + + /** + * Attempts to notify the idle connection management that this connection should + * be considered "in use". This way the idle connection management doesn't close + * this connection during the time the connection is handed out from the pool and any + * new stream created on that connection. + * + * @return true if the connection has been successfully reserved and is {@link #isOpen()}. false + * otherwise; in which case the connection must not be handed out from the pool. + */ + boolean tryReserveForPoolCheckout() { + // must be done with "stateLock" held to co-ordinate idle connection management + lock(); + try { + cancelIdleShutdownEvent(); + // co-ordinate with the QUIC connection to prevent it from silently terminating + // a potentially idle transport + if (!quicConnection.connectionTerminator().tryReserveForUse()) { + // QUIC says the connection can't be used + return false; + } + // consider the reservation successful only if the connection's state hasn't moved + // to "being closed" + return isOpen() && finalStream == false; + } finally { + unlock(); + } + } + + /** + * Cancels any event that might have been scheduled to shutdown this connection. Must be called + * with the stateLock held. + */ + private void cancelIdleShutdownEvent() { + assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock; + if (idleConnectionTimeoutEvent == null) return; + idleConnectionTimeoutEvent.cancel(); + idleConnectionTimeoutEvent = null; + } + + // An Idle connection is one that has no active streams + // and has not sent the final stream flag + final class IdleConnectionTimeoutEvent extends TimeoutEvent { + + // both cancelled and idleShutDownInitiated are to be accessed + // when holding the connection's lock + private boolean cancelled; + private boolean idleShutDownInitiated; + + IdleConnectionTimeoutEvent(Duration duration) { + super(duration); + } + + @Override + public void handle() { + boolean okToIdleTimeout; + lock(); + try { + if (cancelled || idleShutDownInitiated) { + return; + } + idleShutDownInitiated = true; + if (debug.on()) { + debug.log("H3 idle shutdown initiated"); + } + setFinalStream(); + okToIdleTimeout = finalStreamClosed(); + } finally { + unlock(); + } + if (okToIdleTimeout) { + if (debug.on()) { + debug.log("closing idle H3 connection"); + } + close(); + } + } + + /** + * Cancels this event. Should be called with stateLock held + */ + void cancel() { + assert lock.isHeldByCurrentThread() : "Current thread doesn't hold " + lock; + // mark as cancelled to prevent potentially already triggered event from actually + // doing the shutdown + this.cancelled = true; + // cancel the timer to prevent the event from being triggered (if it hasn't already) + client.client().cancelTimer(this); + } + + @Override + public String toString() { + return "IdleConnectionTimeoutEvent, " + super.toString(); + } + + } + + /** + * This method is called when the peer opens a new stream. + * The stream can be unidirectional or bidirectional. + * + * @param stream the new stream + * @return always returns true (see {@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + private boolean onOpenRemoteStream(QuicReceiverStream stream) { + debug.log("on open remote stream: " + stream.streamId()); + if (stream instanceof QuicBidiStream bidi) { + // A server will never open a bidirectional stream + // with the client. A client opens a new bidirectional + // stream for each request/response exchange. + return onRemoteBidirectionalStream(bidi); + } else { + // Four types of unidirectional stream are defined: + // control stream, qpack encoder, qpack decoder, push + // promise stream + return onRemoteUnidirectionalStream(stream); + } + } + + /** + * This method is called when the peer opens a unidirectional stream. + * + * @param uni the unidirectional stream opened by the peer + * @return always returns true ({@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + protected boolean onRemoteUnidirectionalStream(QuicReceiverStream uni) { + assert !uni.isBidirectional(); + assert uni.isRemoteInitiated(); + if (!isOpen()) return false; + debug.log("dispatching unidirectional remote stream: " + uni.streamId()); + Http3StreamDispatcher.dispatch(this, uni).whenComplete((r, t)-> { + if (t!=null) this.dispatchingFailed(uni, t); + }); + return true; + } + + /** + * Called when the peer opens a bidirectional stream. + * On the client side, this method should never be called. + * + * @param bidi the new bidirectional stream opened by the + * peer. + * @return always returns false ({@link + * QuicConnection#addRemoteStreamListener(Predicate)} + */ + protected boolean onRemoteBidirectionalStream(QuicBidiStream bidi) { + assert bidi.isRemoteInitiated(); + assert bidi.isBidirectional(); + + // From RFC 9114, Section 6.1: + // Clients MUST treat receipt of a server-initiated bidirectional + // stream as a connection error of type H3_STREAM_CREATION_ERROR + // [ unless such an extension has been negotiated]. + // We don't support any extension, so this is a connection error. + close(Http3Error.H3_STREAM_CREATION_ERROR, + "Bidirectional stream %s opened by server peer" + .formatted(bidi.streamId())); + return false; + } + + /** + * Called if the dispatch failed. + * + * @param reason the reason of the failure + */ + protected void dispatchingFailed(QuicReceiverStream uni, Throwable reason) { + debug.log("dispatching failed for streamId=%s: %s", uni.streamId(), reason); + close(H3_STREAM_CREATION_ERROR, "failed to dispatch remote stream " + uni.streamId(), reason); + } + + + /** + * Schedules sending of client settings. + * + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + QuicStreamWriter sendSettings(QuicStreamWriter writer) { + try { + final SettingsFrame settings = QPACK.updateDecoderSettings(SettingsFrame.defaultRFCSettings()); + this.ourSettings = ConnectionSettings.createFrom(settings); + this.qpackDecoder.configure(ourSettings); + if (debug.on()) { + debug.log("Sending client settings %s for connection %s", this.ourSettings, this); + } + long size = settings.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + var buf = ByteBuffer.allocate((int) size); + settings.writeFrame(buf); + buf.flip(); + writer.scheduleForWriting(buf, false); + return writer; + } catch (IOException io) { + throw new CompletionException(io); + } + } + + /** + * Schedules sending of max push id that this (client) connection allows. + * + * @param writer the control stream writer + * @return the {@link QuicStreamWriter} passed as parameter + */ + private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer) { + try { + long maxPushId = pushManager.getMaxPushId(); + if (maxPushId > 0 && maxPushId > maxPushIdSent.get()) { + return sendMaxPushId(writer, maxPushId); + } else { + return writer; + } + } catch (IOException io) { + // will wrap the io exception in CompletionException, + // close the connection, and throw. + throw new CompletionException(io); + } + } + + // local control stream write loop + void lcsWriterLoop() { + // since we do not write much data on the control stream + // we don't check for credit and always directly buffer + // the data to send in the writer. Therefore, there is + // nothing to do in the control stream writer loop. + // + // When more credit is available, check if we need + // to send maxpushid; + if (maxPushIdSent.get() < pushManager.getMaxPushId()) { + var writer = controlStreamPair.localWriter(); + if (writer != null && writer.connected()) { + sendMaxPushId(writer); + } + } + } + + void controlStreamFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Control stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9114, section 6.2.1: + // If either control stream is closed at any point, + // this MUST be treated as a connection error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "control stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since control stream " + stream.mode() + + " failed", throwable); + } + } + close(throwable); + } + + /** + * This method is called to process bytes received on the peer + * control stream. + * + * @param buffer the bytes received + */ + private void processPeerControlBytes(final ByteBuffer buffer) { + debug.log("received server control: %s bytes", buffer.remaining()); + controlFramesDecoder.submit(buffer); + Http3Frame frame; + while ((frame = controlFramesDecoder.poll()) != null) { + final long frameType = frame.type(); + debug.log("server control frame: %s", Http3FrameType.asString(frameType)); + if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + final Http3Error error = Http3Error.fromCode(malformed.getErrorCode()) + .orElse(H3_INTERNAL_ERROR); + close(error, malformed.getMessage()); + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } + final boolean settingsRcvd = this.settingsFrameReceived; + if ((frameType == SettingsFrame.TYPE && settingsRcvd) + || !this.frameOrderVerifier.allowsProcessing(frame)) { + final String unexpectedFrameType = Http3FrameType.asString(frameType); + // not expected to be arriving now, we either use H3_FRAME_UNEXPECTED + // or H3_MISSING_SETTINGS for the connection error, depending on the context. + // + // RFC-9114, section 4.1: Receipt of an invalid sequence of frames MUST be + // treated as a connection error of type H3_FRAME_UNEXPECTED. + // + // RFC-9114, section 6.2.1: If the first frame of the control stream + // is any other frame type, this MUST be treated as a connection error of + // type H3_MISSING_SETTINGS. + final String logMsg = "unexpected (order of) frame type: " + unexpectedFrameType + + " on control stream"; + if (!settingsRcvd) { + close(Http3Error.H3_MISSING_SETTINGS, logMsg); + } else { + close(Http3Error.H3_FRAME_UNEXPECTED, logMsg); + } + controlStreamPair.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } + if (frame instanceof SettingsFrame settingsFrame) { + this.settingsFrameReceived = true; + this.peerSettings = ConnectionSettings.createFrom(settingsFrame); + if (debug.on()) { + debug.log("Received peer settings %s for connection %s", this.peerSettings, this); + } + peerSettingsCF.completeAsync(() -> peerSettings, + client.client().theExecutor().safeDelegate()); + // We can only initialize encoder's DT only when we get Settings frame with all parameters + qpackEncoder().configure(peerSettings); + } + if (frame instanceof CancelPushFrame cancelPush) { + pushManager.cancelPushPromise(cancelPush.getPushId(), null, CancelPushReason.CANCEL_RECEIVED); + } + if (frame instanceof GoAwayFrame goaway) { + handleIncomingGoAway(goaway); + } + if (frame instanceof PartialFrame partial) { + var payloadBytes = controlFramesDecoder.readPayloadBytes(); + debug.log("added %s bytes to %s", + payloadBytes == null ? 0 : Utils.remaining(payloadBytes), + frame); + if (partial.remaining() == 0) { + this.frameOrderVerifier.completed(frame); + } else if (payloadBytes == null || payloadBytes.isEmpty()) { + break; + } + // only reserved frames reach here; just drop the payload + } else { + this.frameOrderVerifier.completed(frame); + } + if (controlFramesDecoder.eof()) { + break; + } + } + if (controlFramesDecoder.eof()) { + close(H3_CLOSED_CRITICAL_STREAM, "EOF reached while reading server control stream"); + } + } + + /** + * Called when a new push promise stream is created by the peer. + * + * @apiNote this method gives an opportunity to cancel the stream + * before reading the pushId, if it is known that no push + * will be accepted anyway. + * + * @param pushStream the new push promise stream + * @param pushId or -1 if the pushId is not available yet + */ + private void onPushStreamCreated(QuicReceiverStream pushStream, long pushId) { + assert pushStream.isRemoteInitiated(); + assert !pushStream.isBidirectional(); + + onPushPromiseStream(pushStream, pushId); + } + + /** + * Called when a new push promise stream is created by the peer, and + * the pushId has been read. + * + * @param pushStream the new push promise stream + * @param pushId the pushId + */ + void onPushPromiseStream(QuicReceiverStream pushStream, long pushId) { + assert pushId >= 0; + pushManager.onPushPromiseStream(pushStream, pushId); + } + + /** + * This method is called by the {@link Http3PushManager} to figure out whether + * a push stream or a push promise should be processed, with respect to the + * GOAWAY state. Any pushId larger than what was sent in the GOAWAY frame + * should be cancelled /rejected. + * + * @param pushStream a push stream (may be null if not yet materialized) + * @param pushId a pushId, must be > 0 + * @return true if the pushId can be processed + */ + boolean acceptLargerPushPromise(QuicReceiverStream pushStream, long pushId) { + // if GOAWAY has been sent, just cancel the push promise + // otherwise - track this as the maxPushId that will be + // sent in GOAWAY + if (checkMaxPushId(pushId) != null) return false; // connection will be closed + while (true) { + long largestPushId = this.largestPushId.get(); + if ((closedState & GOAWAY_SENT) == GOAWAY_SENT) { + if (pushId >= largestPushId) { + if (pushStream != null) { + pushStream.requestStopSending(H3_NO_ERROR.code()); + } + pushManager.cancelPushPromise(pushId, null, CancelPushReason.PUSH_CANCELLED); + return false; + } + } + if (pushId <= largestPushId) break; + if (!this.largestPushId.compareAndSet(largestPushId, pushId)) continue; + if ((closedState & GOAWAY_SENT) == 0) break; + } + // If we reach here, then either GOAWAY has been sent with a largestPushId >= pushId, + // or GOAWAY has not been sent yet. + return true; + } + + QueuingStreamPair createEncoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_ENCODER, quicConnection, + encoderReceiver, this::onEncoderStreamsFailed, debug); + } + + private void onEncoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Encoder stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "QPACK encoder stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since QPack encoder stream " + stream.streamId() + + " failed", throwable); + } + } + close(throwable); + } + + QueuingStreamPair createDecoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_DECODER, quicConnection, + encoderReceiver, this::onDecoderStreamsFailed, debug); + } + + private void onDecoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + Http3Streams.debugErrorCode(debug, stream, "Decoder stream failed"); + if (stream.state() instanceof QuicReceiverStream.ReceivingStreamState rcvrStrmState) { + if (rcvrStrmState.isReset() && quicConnection.isOpen()) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + final String logMsg = "QPACK decoder stream " + stream.streamId() + + " was reset"; + close(H3_CLOSED_CRITICAL_STREAM, logMsg); + return; + } + } + if (isOpen()) { + if (debug.on()) { + debug.log("closing connection since QPack decoder stream " + stream.streamId() + + " failed", throwable); + } + } + close(throwable); + } + + // This method never returns anything: it always throws + private T exceptionallyAndClose(Throwable t) { + try { + return exceptionally(t); + } finally { + close(t); + } + } + + // This method never returns anything: it always throws + private T exceptionally(Throwable t) { + try { + debug.log(t.getMessage(), t); + throw t; + } catch (RuntimeException | Error r) { + throw r; + } catch (ExecutionException x) { + throw new CompletionException(x.getMessage(), x.getCause()); + } catch (Throwable e) { + throw new CompletionException(e.getMessage(), e); + } + } + + Decoder qpackDecoder() { + return qpackDecoder; + } + + Encoder qpackEncoder() { + return qpackEncoder; + } + + /** + * {@return the settings, sent by the peer, for this connection. If none is present, due to the SETTINGS + * frame not yet arriving from the peer, this method returns {@link Optional#empty()}} + */ + Optional getPeerSettings() { + return Optional.ofNullable(this.peerSettings); + } + + private void handleIncomingGoAway(final GoAwayFrame incomingGoAway) { + final long quicStreamId = incomingGoAway.getTargetId(); + if (debug.on()) { + debug.log("Received GOAWAY %s", incomingGoAway); + } + // ensure request stream id is a bidirectional stream originating from the client. + // RFC-9114, section 7.2.6: A client MUST treat receipt of a GOAWAY frame containing + // a stream ID of any other type as a connection error of type H3_ID_ERROR. + if (!(QuicStreams.isClientInitiated(quicStreamId) + && QuicStreams.isBidirectional(quicStreamId))) { + close(Http3Error.H3_ID_ERROR, "Invalid stream id in GOAWAY frame"); + return; + } + boolean validStreamId = false; + long current = lowestGoAwayReceipt.get(); + while (current == -1 || quicStreamId <= current) { + if (lowestGoAwayReceipt.compareAndSet(current, quicStreamId)) { + validStreamId = true; + break; + } + current = lowestGoAwayReceipt.get(); + } + if (!validStreamId) { + // the request stream id received in the GOAWAY frame is greater than the one received + // in some previous GOAWAY frame. This isn't allowed by spec. + // RFC-9114, section 5.2: An endpoint MAY send multiple GOAWAY frames indicating + // different identifiers, but the identifier in each frame MUST NOT be greater than + // the identifier in any previous frame, ... Receiving a GOAWAY containing a larger + // identifier than previously received MUST be treated as a connection error of + // type H3_ID_ERROR. + close(Http3Error.H3_ID_ERROR, "Invalid stream id in newer GOAWAY frame"); + return; + } + markReceivedGoAway(); + // mark a state on this connection to let it know that no new streams are allowed on this + // connection. + // RFC-9114, section 5.2: Endpoints MUST NOT initiate new requests or promise new pushes on + // the connection after receipt of a GOAWAY frame from the peer. + setFinalStream(); + if (debug.on()) { + debug.log("Connection will no longer allow new streams due to receipt of GOAWAY" + + " from peer"); + } + handlePeerUnprocessedStreams(quicStreamId); + if (finalStreamClosed()) { + close(Http3Error.H3_NO_ERROR, "GOAWAY received"); + } + } + + private void handlePeerUnprocessedStreams(final long leastUnprocessedStreamId) { + this.exchanges.forEach((id, exchange) -> { + if (id >= leastUnprocessedStreamId) { + // close the exchange as unprocessed + client.client().theExecutor().execute(exchange::closeAsUnprocessed); + } + }); + } + + private boolean isMarked(int state, int mask) { + return (state & mask) == mask; + } + + private boolean markSentGoAway() { + return markClosedState(GOAWAY_SENT); + } + + private boolean markReceivedGoAway() { + return markClosedState(GOAWAY_RECEIVED); + } + + private boolean markClosedState(int flag) { + int state, desired; + do { + state = closedState; + if ((state & flag) == flag) return false; + desired = state | flag; + } while (!CLOSED_STATE.compareAndSet(this, state, desired)); + return true; + } + + String describeClosedState(int state) { + if (state == 0) return "active"; + String desc = null; + if (isMarked(state, GOAWAY_SENT)) { + if (desc == null) desc = "goaway-sent"; + else desc += "+goaway-sent"; + } + if (isMarked(state, GOAWAY_RECEIVED)) { + if (desc == null) desc = "goaway-rcvd"; + else desc += "+goaway-rcvd"; + } + if (isMarked(state, CLOSED)) { + if (desc == null) desc = "quic-closed"; + else desc += "+quic-closed"; + } + return desc != null ? desc : "0x" + Integer.toHexString(state); + } + + // PushPromise handling + // ==================== + + /** + * {@return a new PushId for the given pushId} + * @param pushId the pushId + */ + PushId newPushId(long pushId) { + return new Http3PushId(pushId, connection.label()); + } + + /** + * Called when a pushId needs to be cancelled. + * @param pushId the pushId to cancel + * @param cause the cause (may be {@code null}). + */ + void pushCancelled(long pushId, Throwable cause) { + pushManager.cancelPushPromise(pushId, cause, CancelPushReason.PUSH_CANCELLED); + } + + /** + * Called if a PushPromiseFrame is received by an exchange that doesn't have any + * {@link java.net.http.HttpResponse.PushPromiseHandler}. The pushId will be + * cancelled, unless it's already been accepted by another exchange. + * + * @param pushId the pushId + */ + void noPushHandlerFor(long pushId) { + pushManager.cancelPushPromise(pushId, null, CancelPushReason.NO_HANDLER); + } + + boolean acceptPromises() { + return exchanges.values().stream().anyMatch(Http3ExchangeImpl::acceptPushPromise); + } + + /** + * {@return a completable future that will be completed when a pushId has been + * accepted by the exchange in charge of creating the response body} + *

        + * The completable future is complete with {@code true} if the pushId is + * accepted, and with {@code false} if the pushId was rejected or cancelled. + * + * @apiNote + * This method is intended to be called when {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}, returns false, + * indicating that the push promise is being delegated to another request/response + * exchange. + * On completion of the future returned here, if the future is completed + * with {@code true}, the caller is expected to call {@link + * PushGroup#acceptPushPromiseId(PushId)} in order to notify the {@link + * java.net.http.HttpResponse.PushPromiseHandler} of the received {@code pushId}. + *

        + * Callers should not forward the pushId to a {@link + * java.net.http.HttpResponse.PushPromiseHandler} unless the future is completed + * with {@code true} + * + * @param pushId the pushId + */ + CompletableFuture whenPushAccepted(long pushId) { + return pushManager.whenAccepted(pushId); + } + + /** + * Called when a PushPromiseFrame has been decoded. + * + * @param exchange The HTTP/3 exchange that received the frame + * @param pushId The pushId contained in the frame + * @param promiseHeaders The push promise headers contained in the frame + * @return true if the exchange should take care of creating the HttpResponse body, + * false otherwise + */ + boolean onPushPromiseFrame(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) + throws IOException { + return pushManager.onPushPromiseFrame(exchange, pushId, promiseHeaders); + } + + /** + * Checks whether a MAX_PUSH_ID frame should be sent. + */ + void checkSendMaxPushId() { + pushManager.checkSendMaxPushId(); + } + + /** + * Schedules sending of max push id that this (client) connection allows. + * + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + private QuicStreamWriter sendMaxPushId(QuicStreamWriter writer, long maxPushId) throws IOException { + debug.log("Sending max push id frame with max push id set to " + maxPushId); + final MaxPushIdFrame maxPushIdFrame = new MaxPushIdFrame(maxPushId); + final long frameSize = maxPushIdFrame.size(); + assert frameSize >= 0 && frameSize < Integer.MAX_VALUE; + final ByteBuffer buf = ByteBuffer.allocate((int) frameSize); + maxPushIdFrame.writeFrame(buf); + buf.flip(); + if (writer.credit() > buf.remaining()) { + long previous; + do { + previous = maxPushIdSent.get(); + if (previous >= maxPushId) return writer; + } while (!maxPushIdSent.compareAndSet(previous, maxPushId)); + writer.scheduleForWriting(buf, false); + } + return writer; + } + + /** + * Send a MAX_PUSH_ID frame on the control stream with the given {@code maxPushId} + * + * @param maxPushId the new maxPushId + * + * @throws IOException if the pushId could not be sent + */ + void sendMaxPushId(long maxPushId) throws IOException { + sendMaxPushId(controlStreamPair.localWriter(), maxPushId); + } + + /** + * Sends a CANCEL_PUSH frame for the given {@code pushId}. + * If not null, the cause may indicate why the push is cancelled. + * + * @apiNote the cause is only used for logging + * + * @param pushId the pushId to cancel + * @param cause the reason for cancelling, may be {@code null} + */ + void sendCancelPush(long pushId, Throwable cause) { + // send CANCEL_PUSH frame here + if (debug.on()) { + if (cause != null) { + debug.log("Push Promise %s cancelled: %s", pushId, cause.getMessage()); + } else { + debug.log("Push Promise %s cancelled", pushId); + } + } + try { + CancelPushFrame cancelPush = new CancelPushFrame(pushId); + long size = cancelPush.size(); + // frame should contain type, length, pushId + assert size <= 3 * VariableLengthEncoder.MAX_INTEGER_LENGTH; + ByteBuffer buffer = ByteBuffer.allocate((int) size); + cancelPush.writeFrame(buffer); + controlStreamPair.localWriter().scheduleForWriting(buffer, false); + } catch (IOException io) { + debug.log("Failed to cancel pushId: " + pushId); + } + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + IOException checkMaxPushId(long pushId) { + return checkMaxPushId(pushId, maxPushIdSent.get()); + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + private IOException checkMaxPushId(long pushId, long max) { + if (pushId >= max) { + var io = new ProtocolException("Max pushId exceeded (%s >= %s)".formatted(pushId, max)); + connectionError(io, Http3Error.H3_ID_ERROR); + return io; + } + return null; + } + + /** + * {@return the minimum pushId that can be accepted from the peer} + * Any pushId strictly less than this value must be ignored. + * + * @apiNote The minimum pushId represents the smallest pushId that + * was recorded in our history. For smaller pushId, no history has + * been kept, due to history size constraints. Any pushId strictly + * less than this value must be ignored. + */ + public long getMinPushId() { + return pushManager.getMinPushId(); + } + + private static final VarHandle CLOSED_STATE; + static { + try { + CLOSED_STATE = MethodHandles.lookup().findVarHandle(Http3Connection.class, "closedState", int.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java new file mode 100644 index 00000000000..eaacd8213cb --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ConnectionPool.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jdk.internal.net.http.common.Logger; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/** + * This class encapsulate the HTTP/3 connection pool managed + * by an instance of {@link Http3ClientImpl}. + */ +class Http3ConnectionPool { + /* Map key is "scheme:host:port" */ + private final Map advertised = new ConcurrentHashMap<>(); + /* Map key is "scheme:host:port" */ + private final Map unadvertised = new ConcurrentHashMap<>(); + + private final Logger debug; + Http3ConnectionPool(Logger logger) { + this.debug = Objects.requireNonNull(logger); + } + + // https:: + String connectionKey(HttpRequestImpl request) { + var uri = request.uri(); + var scheme = uri.getScheme().toLowerCase(Locale.ROOT); + var host = uri.getHost(); + var port = uri.getPort(); + assert scheme.equals("https"); + if (port < 0) port = 443; // https + return String.format("%s:%s:%d", scheme, host, port); + } + + private Http3Connection lookupUnadvertised(String key, Http3DiscoveryMode discoveryMode) { + var unadvertisedConn = unadvertised.get(key); + if (unadvertisedConn == null) return null; + if (discoveryMode == ANY) return unadvertisedConn; + if (discoveryMode == ALT_SVC) return null; + + assert discoveryMode == HTTP_3_URI_ONLY : String.valueOf(discoveryMode); + + // Double check that if there is an alt service, it has same origin. + final var altService = Optional.ofNullable(unadvertisedConn) + .map(Http3Connection::connection) + .flatMap(HttpQuicConnection::getSourceAltService) + .orElse(null); + + if (altService == null || altService.originHasSameAuthority()) { + return unadvertisedConn; + } + + // We should never come here. + assert false : "unadvertised connection with different origin: %s -> %s" + .formatted(key, altService); + return null; + } + + Http3Connection lookupFor(HttpRequestImpl request) { + var discoveryMode = request.http3Discovery(); + var key = connectionKey(request); + + Http3Connection unadvertisedConn = null; + // If not ALT_SVC, we can use unadvertised connections + if (discoveryMode != ALT_SVC) { + unadvertisedConn = lookupUnadvertised(key, discoveryMode); + if (unadvertisedConn != null && discoveryMode == HTTP_3_URI_ONLY) { + if (debug.on()) { + debug.log("Direct HTTP/3 connection found for %s in connection pool %s", + discoveryMode, unadvertisedConn.connection().label()); + } + return unadvertisedConn; + } + } + + // Then see if we have a connection which was advertised. + var advertisedConn = advertised.get(key); + // We can use it for HTTP3_URI_ONLY too if it has same origin + if (advertisedConn != null) { + final var altService = advertisedConn.connection() + .getSourceAltService().orElse(null); + assert altService != null && altService.wasAdvertised(); + switch (discoveryMode) { + case ANY -> { + return advertisedConn; + } + case ALT_SVC -> { + if (debug.on()) { + debug.log("HTTP/3 connection found for %s in connection pool %s", + discoveryMode, advertisedConn.connection().label()); + } + return advertisedConn; + } + case HTTP_3_URI_ONLY -> { + if (altService != null && altService.originHasSameAuthority()) { + if (debug.on()) { + debug.log("Same authority HTTP/3 connection found for %s in connection pool %s", + discoveryMode, advertisedConn.connection().label()); + } + return advertisedConn; + } + } + } + } + + if (unadvertisedConn != null) { + assert discoveryMode != ALT_SVC; + if (debug.on()) { + debug.log("Direct HTTP/3 connection found for %s in connection pool %s", + discoveryMode, unadvertisedConn.connection().label()); + } + return unadvertisedConn; + } + + // do not log here: this produces confusing logs as this method + // can be called several times when trying to establish a + // connection, when no connection is found in the pool + return null; + } + + Http3Connection putIfAbsent(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()); + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + return advertised.putIfAbsent(key, c); + } + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.putIfAbsent(key, c); + } + + Http3Connection put(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()) : "key mismatch %s -> %s" + .formatted(key, c.key()); + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + return advertised.put(key, c); + } + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.put(key, c); + } + + boolean remove(String key, Http3Connection c) { + Objects.requireNonNull(key); + Objects.requireNonNull(c); + assert key.equals(c.key()) : "key mismatch %s -> %s" + .formatted(key, c.key()); + + var altService = c.connection().getSourceAltService().orElse(null); + if (altService != null && altService.wasAdvertised()) { + boolean remUndavertised = unadvertised.remove(key, c); + assert !remUndavertised + : "advertised connection found in unadvertised pool for " + key; + return advertised.remove(key, c); + } + + assert altService == null || altService.originHasSameAuthority(); + return unadvertised.remove(key, c); + } + + void clear() { + advertised.clear(); + unadvertised.clear(); + } + + java.util.stream.Stream values() { + return java.util.stream.Stream.concat( + advertised.values().stream(), + unadvertised.values().stream()); + } + +} + diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java new file mode 100644 index 00000000000..ff1e024673c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3ExchangeImpl.java @@ -0,0 +1,1795 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.ProtocolException; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiPredicate; + +import jdk.internal.net.http.PushGroup.Acceptor; +import jdk.internal.net.http.common.HttpBodySubscriberWrapper; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SubscriptionBase; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import static jdk.internal.net.http.http3.ConnectionSettings.UNLIMITED_MAX_FIELD_SECTION_SIZE; + +/** + * This class represents an HTTP/3 Request/Response stream. + */ +final class Http3ExchangeImpl extends Http3Stream { + + private static final String COOKIE_HEADER = "Cookie"; + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3Connection connection; + private final HttpRequestImpl request; + private final BodyPublisher requestPublisher; + private final HttpHeadersBuilder responseHeadersBuilder; + private final HeadersConsumer rspHeadersConsumer; + private final HttpHeaders requestPseudoHeaders; + private final HeaderFrameReader headerFrameReader; + private final HeaderFrameWriter headerFrameWriter; + private final Decoder qpackDecoder; + private final Encoder qpackEncoder; + private final AtomicReference errorRef; + private final CompletableFuture requestBodyCF; + + private final FramesDecoder framesDecoder = + new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnRequestStream); + private final SequentialScheduler readScheduler = + SequentialScheduler.lockingScheduler(this::processQuicData); + private final SequentialScheduler writeScheduler = + SequentialScheduler.lockingScheduler(this::sendQuicData); + private final List> response_cfs = new ArrayList<>(5); + private final ReentrantLock stateLock = new ReentrantLock(); + private final ReentrantLock response_cfs_lock = new ReentrantLock(); + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForRequestResponseStream(); + + + final SubscriptionBase userSubscription = + new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError); + + private final QuicBidiStream stream; + private final QuicStreamReader reader; + private final QuicStreamWriter writer; + volatile boolean closed; + volatile RequestSubscriber requestSubscriber; + volatile HttpResponse.BodySubscriber pendingResponseSubscriber; + volatile HttpResponse.BodySubscriber responseSubscriber; + volatile CompletableFuture responseBodyCF; + volatile boolean requestSent; + volatile boolean responseReceived; + volatile long requestContentLen; + volatile int responseCode; + volatile Response response; + volatile boolean stopRequested; + volatile boolean deRegistered; + private String dbgTag = null; + private final AtomicLong sentQuicBytes = new AtomicLong(); + + Http3ExchangeImpl(final Http3Connection connection, final Exchange exchange, + final QuicBidiStream stream) { + super(exchange); + this.errorRef = new AtomicReference<>(); + this.requestBodyCF = new MinimalFuture<>(); + this.connection = connection; + this.request = exchange.request(); + this.requestPublisher = request.requestPublisher; // may be null + this.responseHeadersBuilder = new HttpHeadersBuilder(); + this.rspHeadersConsumer = new HeadersConsumer(ValidatingHeadersConsumer.Context.RESPONSE); + this.qpackDecoder = connection.qpackDecoder(); + this.qpackEncoder = connection.qpackEncoder(); + this.headerFrameReader = qpackDecoder.newHeaderFrameReader(rspHeadersConsumer); + this.headerFrameWriter = qpackEncoder.newHeaderFrameWriter(); + this.requestPseudoHeaders = Utils.createPseudoHeaders(request); + this.stream = stream; + this.reader = stream.connectReader(readScheduler); + this.writer = stream.connectWriter(writeScheduler); + if (debug.on()) debug.log("Http3ExchangeImpl created"); + } + + public void start() { + if (exchange.pushGroup != null) { + connection.checkSendMaxPushId(); + } + if (Log.http3()) { + Log.logHttp3("Starting HTTP/3 exchange for {0}/streamId={1} ({2} #{3})", + connection.quicConnectionTag(), Long.toString(stream.streamId()), + request, Long.toString(exchange.multi.id)); + } + this.reader.start(); + } + + boolean acceptPushPromise() { + return exchange.pushGroup != null; + } + + String dbgTag() { + if (dbgTag != null) return dbgTag; + long streamId = streamId(); + String sid = streamId == -1 ? "?" : String.valueOf(streamId); + String ctag = connection == null ? null : connection.dbgTag(); + String tag = "Http3ExchangeImpl(" + ctag + ", streamId=" + sid + ")"; + if (streamId == -1) return tag; + return dbgTag = tag; + } + + @Override + long streamId() { + var stream = this.stream; + return stream == null ? -1 : stream.streamId(); + } + + Http3Connection http3Connection() { + return connection; + } + + void recordError(Throwable closeCause) { + errorRef.compareAndSet(null, closeCause); + } + + private sealed class HeadersConsumer extends StreamHeadersConsumer permits PushHeadersConsumer { + + private HeadersConsumer(Context context) { + super(context); + } + + @Override + protected HeaderFrameReader headerFrameReader() { + return headerFrameReader; + } + + @Override + protected HttpHeadersBuilder headersBuilder() { + return responseHeadersBuilder; + } + + @Override + protected final Decoder qpackDecoder() { + return qpackDecoder; + } + + void resetDone() { + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + + @Override + String headerFieldType() { + return "RESPONSE HEADER FIELD"; + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed response: " + super.formatMessage(message, header); + } + + @Override + protected void headersCompleted() { + handleResponse(); + } + + @Override + public final long streamId() { + return stream.streamId(); + } + + } + + private final class PushHeadersConsumer extends HeadersConsumer { + volatile PushPromiseState state; + + private PushHeadersConsumer() { + super(Context.REQUEST); + } + + @Override + protected HttpHeadersBuilder headersBuilder() { + return state.headersBuilder(); + } + + @Override + protected HeaderFrameReader headerFrameReader() { + return state.reader(); + } + + @Override + String headerFieldType() { + return "PUSH REQUEST HEADER FIELD"; + } + + void resetDone() { + if (debug.on()) { + debug.log("Push request builder cleared."); + } + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed push request: " + super.formatMessage(message, header); + } + + @Override + protected void headersCompleted() { + try { + if (exchange.pushGroup == null) { + long pushId = state.frame().getPushId(); + connection.noPushHandlerFor(pushId); + reset(); + } else { + handlePromise(this); + } + } catch (IOException io) { + cancelPushPromise(state, io); + } + } + + public void setState(PushPromiseState state) { + this.state = state; + } + } + + // TODO: this is also defined on Stream + // + private static boolean hasProxyAuthorization(HttpHeaders headers) { + return headers.firstValue("proxy-authorization") + .isPresent(); + } + + // TODO: this is also defined on Stream + // + // Determines whether we need to build a new HttpHeader object. + // + // Ideally we should pass the filter to OutgoingHeaders refactor the + // code that creates the HeaderFrame to honor the filter. + // We're not there yet - so depending on the filter we need to + // apply and the content of the header we will try to determine + // whether anything might need to be filtered. + // If nothing needs filtering then we can just use the + // original headers. + private static boolean needsFiltering(HttpHeaders headers, + BiPredicate filter) { + if (filter == Utils.PROXY_TUNNEL_FILTER || filter == Utils.PROXY_FILTER) { + // we're either connecting or proxying + // slight optimization: we only need to filter out + // disabled schemes, so if there are none just + // pass through. + return Utils.proxyHasDisabledSchemes(filter == Utils.PROXY_TUNNEL_FILTER) + && hasProxyAuthorization(headers); + } else { + // we're talking to a server, either directly or through + // a tunnel. + // Slight optimization: we only need to filter out + // proxy authorization headers, so if there are none just + // pass through. + return hasProxyAuthorization(headers); + } + } + + // TODO: this is also defined on Stream + // + private HttpHeaders filterHeaders(HttpHeaders headers) { + HttpConnection conn = connection(); + BiPredicate filter = conn.headerFilter(request); + if (needsFiltering(headers, filter)) { + return HttpHeaders.of(headers.map(), filter); + } + return headers; + } + + @Override + HttpQuicConnection connection() { + return connection.connection(); + } + + @Override + CompletableFuture> sendHeadersAsync() { + final MinimalFuture completable = MinimalFuture.completedFuture(null); + return completable.thenApply(_ -> this.sendHeaders()); + } + + private Http3ExchangeImpl sendHeaders() { + assert stream != null; + assert writer != null; + + if (debug.on()) debug.log("H3 sendHeaders"); + if (Log.requests()) { + Log.logRequest(request.toString()); + } + if (requestPublisher != null) { + requestContentLen = requestPublisher.contentLength(); + } else { + requestContentLen = 0; + } + + Throwable t = errorRef.get(); + if (t != null) { + if (debug.on()) debug.log("H3 stream already cancelled, headers not sent: %s", (Object) t); + if (t instanceof CompletionException ce) throw ce; + throw new CompletionException(t); + } + + HttpHeadersBuilder h = request.getSystemHeadersBuilder(); + if (requestContentLen > 0) { + h.setHeader("content-length", Long.toString(requestContentLen)); + } + HttpHeaders sysh = filterHeaders(h.build()); + HttpHeaders userh = filterHeaders(request.getUserHeaders()); + // Filter context restricted from userHeaders + userh = HttpHeaders.of(userh.map(), Utils.ACCEPT_ALL); + Utils.setUserAuthFlags(request, userh); + + // Don't override Cookie values that have been set by the CookieHandler. + final HttpHeaders uh = userh; + BiPredicate overrides = + (k, v) -> COOKIE_HEADER.equalsIgnoreCase(k) + || uh.firstValue(k).isEmpty(); + + // Filter any headers from systemHeaders that are set in userHeaders + // except for "Cookie:" - user cookies will be appended to system + // cookies + sysh = HttpHeaders.of(sysh.map(), overrides); + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("H3 HEADERS FRAME (stream="); + sb.append(streamId()).append(")\n"); + Log.dumpHeaders(sb, " ", requestPseudoHeaders); + Log.dumpHeaders(sb, " ", sysh); + Log.dumpHeaders(sb, " ", userh); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + final Optional peerSettings = connection.getPeerSettings(); + // It's possible that the peer settings hasn't yet arrived, in which case we use the + // default of "unlimited" header size limit and proceed with sending the request. As per + // RFC-9114, section 7.2.4.2, this is allowed: All settings begin at an initial value. Each + // endpoint SHOULD use these initial values to send messages before the peer's SETTINGS frame + // has arrived, as packets carrying the settings can be lost or delayed. + // When the SETTINGS frame arrives, any settings are changed to their new values. This + // removes the need to wait for the SETTINGS frame before sending messages. + final long headerSizeLimit = peerSettings.isEmpty() ? UNLIMITED_MAX_FIELD_SECTION_SIZE + : peerSettings.get().maxFieldSectionSize(); + if (headerSizeLimit != UNLIMITED_MAX_FIELD_SECTION_SIZE) { + // specific limit has been set on the header size for this connection. + // we compute the header size and ensure that it doesn't exceed that limit + final long computedHeaderSize = computeHeaderSize(requestPseudoHeaders, sysh, userh); + if (computedHeaderSize > headerSizeLimit) { + // RFC-9114, section 4.2.2: An implementation that has received this parameter + // SHOULD NOT send an HTTP message header that exceeds the indicated size. + // we fail the request. + throw new CompletionException(new ProtocolException("Request headers size" + + " exceeds limit set by peer")); + } + } + List buffers = qpackEncoder.encodeHeaders(headerFrameWriter, streamId(), + 1024, requestPseudoHeaders, sysh, userh); + HeadersFrame headersFrame = new HeadersFrame(Utils.remaining(buffers)); + ByteBuffer buffer = ByteBuffer.allocate(headersFrame.headersSize()); + headersFrame.writeHeaders(buffer); + buffer.flip(); + long sentBytes = 0; + try { + boolean hasNoBody = requestContentLen == 0; + int last = buffers.size() - 1; + int toSend = buffer.remaining(); + if (last < 0) { + writer.scheduleForWriting(buffer, hasNoBody); + } else { + writer.queueForWriting(buffer); + } + sentBytes += toSend; + for (int i = 0; i <= last; i++) { + var nextBuffer = buffers.get(i); + toSend = nextBuffer.remaining(); + if (i == last) { + writer.scheduleForWriting(nextBuffer, hasNoBody); + } else { + writer.queueForWriting(nextBuffer); + } + sentBytes += toSend; + } + } catch (QPackException qe) { + if (qe.isConnectionError()) { + // close the connection + connection.close(qe.http3Error(), "QPack error", qe.getCause()); + } + // fail the request + throw new CompletionException(qe.getCause()); + } catch (IOException io) { + throw new CompletionException(io); + } finally { + if (sentBytes != 0) sentQuicBytes.addAndGet(sentBytes); + } + return this; + } + + private static long computeHeaderSize(final HttpHeaders... headers) { + // RFC-9114, section 4.2.2 states: The size of a field list is calculated based on + // the uncompressed size of fields, including the length of the name and value in bytes + // plus an overhead of 32 bytes for each field. + final int OVERHEAD_BYTES_PER_FIELD = 32; + long computedHeaderSize = 0; + for (final HttpHeaders h : headers) { + for (final Map.Entry> entry : h.map().entrySet()) { + try { + computedHeaderSize = Math.addExact(computedHeaderSize, + entry.getKey().getBytes(StandardCharsets.US_ASCII).length); + for (final String v : entry.getValue()) { + computedHeaderSize = Math.addExact(computedHeaderSize, + v.getBytes(StandardCharsets.US_ASCII).length); + } + computedHeaderSize = Math.addExact(computedHeaderSize, OVERHEAD_BYTES_PER_FIELD); + } catch (ArithmeticException ae) { + // overflow, no point trying to compute further, return MAX_VALUE + return Long.MAX_VALUE; + } + } + } + return computedHeaderSize; + } + + + @Override + CompletableFuture> sendBodyAsync() { + return sendBodyImpl().thenApply((e) -> this); + } + + CompletableFuture sendBodyImpl() { + requestBodyCF.whenComplete((v, t) -> requestSent()); + try { + if (debug.on()) debug.log("H3 sendBodyImpl"); + if (requestPublisher != null && requestContentLen != 0) { + final RequestSubscriber subscriber = new RequestSubscriber(requestContentLen); + requestPublisher.subscribe(requestSubscriber = subscriber); + } else { + // there is no request body, therefore the request is complete, + // END_STREAM has already sent with outgoing headers + requestBodyCF.complete(null); + } + } catch (Throwable t) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + requestBodyCF.completeExceptionally(t); + } + return requestBodyCF; + } + + // The Http3StreamResponseSubscriber is registered with the HttpClient + // to ensure that it gets completed if the SelectorManager aborts due + // to unexpected exceptions. + private void registerResponseSubscriber(Http3StreamResponseSubscriber subscriber) { + if (client().registerSubscriber(subscriber)) { + if (debug.on()) { + debug.log("Reference response body for h3 stream: " + streamId()); + } + client().h3StreamReference(); + } + } + + private void unregisterResponseSubscriber(Http3StreamResponseSubscriber subscriber) { + if (client().unregisterSubscriber(subscriber)) { + if (debug.on()) { + debug.log("Unreference response body for h3 stream: " + streamId()); + } + client().h3StreamUnreference(); + } + } + + final class Http3StreamResponseSubscriber extends HttpBodySubscriberWrapper { + Http3StreamResponseSubscriber(BodySubscriber subscriber) { + super(subscriber); + } + + @Override + protected void unregister() { + unregisterResponseSubscriber(this); + } + + @Override + protected void register() { + registerResponseSubscriber(this); + } + + @Override + protected void logComplete(Throwable error) { + if (error == null) { + if (Log.requests()) { + Log.logResponse(() -> "HTTP/3 body successfully completed for: " + request + + " #" + exchange.multi.id); + } + } else { + if (Log.requests()) { + Log.logResponse(() -> "HTTP/3 body exceptionally completed for: " + + request + " (" + error + ")" + + " #" + exchange.multi.id); + } + } + } + } + + + @Override + Http3StreamResponseSubscriber createResponseSubscriber(BodyHandler handler, + ResponseInfo response) { + if (debug.on()) debug.log("Creating body subscriber"); + Http3StreamResponseSubscriber subscriber = + new Http3StreamResponseSubscriber<>(handler.apply(response)); + return subscriber; + } + + @Override + CompletableFuture readBodyAsync(BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) { + try { + if (Log.trace()) { + Log.logTrace("Reading body on stream {0}", streamId()); + } + if (debug.on()) debug.log("Getting BodySubscriber for: " + response); + Http3StreamResponseSubscriber bodySubscriber = + createResponseSubscriber(handler, new ResponseInfoImpl(response)); + CompletableFuture cf = receiveResponseBody(bodySubscriber, executor); + + PushGroup pg = exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(e)); + } + var bodyCF = cf; + return bodyCF; + } catch (Throwable t) { + // may be thrown by handler.apply + // TODO: Is this the right error code? + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + return MinimalFuture.failedFuture(t); + } + } + + @Override + CompletableFuture ignoreBody() { + try { + if (debug.on()) debug.log("Ignoring body"); + reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return MinimalFuture.completedFuture(null); + } catch (Throwable e) { + if (Log.trace()) { + Log.logTrace("Error requesting stop sending for stream {0}: {1}", + streamId(), e.toString()); + } + return MinimalFuture.failedFuture(e); + } + } + + @Override + void cancel() { + if (debug.on()) debug.log("cancel"); + var stream = this.stream; + if ((stream == null)) { + cancel(new IOException("Stream cancelled before streamid assigned")); + } else { + cancel(new IOException("Stream " + stream.streamId() + " cancelled")); + } + } + + @Override + void cancel(IOException cause) { + cancelImpl(cause, Http3Error.H3_REQUEST_CANCELLED); + } + + @Override + void onProtocolError(IOException cause) { + final long streamId = stream.streamId(); + if (debug.on()) { + debug.log("cancelling exchange on stream %d due to protocol error: %s", streamId, cause.getMessage()); + } + Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamId, cause); + cancelImpl(cause, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + @Override + void released() { + long streamid = streamId(); + if (debug.on()) debug.log("Released stream %d", streamid); + // remove this stream from the Http2Connection map. + connection.onExchangeClose(this, streamid); + } + + @Override + void completed() { + } + + @Override + boolean isCanceled() { + return errorRef.get() != null; + } + + @Override + Throwable getCancelCause() { + return errorRef.get(); + } + + @Override + void cancelImpl(Throwable e, Http3Error error) { + try { + var streamid = streamId(); + if (errorRef.compareAndSet(null, e)) { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream", e); + else debug.log("cancelling stream " + streamid + ":", e); + } + if (Log.trace()) { + if (streamid == -1) Log.logTrace("cancelling stream: {0}\n", e); + else Log.logTrace("cancelling stream {0}: {1}\n", streamid, e); + } + } else { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", (Object) e); + else debug.log("cancelling stream %s: %s", streamid, e); + } + } + var firstError = errorRef.get(); + completeResponseExceptionally(firstError); + if (!requestBodyCF.isDone()) { + // complete requestBodyCF before cancelling subscription + requestBodyCF.completeExceptionally(firstError); // we may be sending the body... + var requestSubscriber = this.requestSubscriber; + if (requestSubscriber != null) { + cancel(requestSubscriber.subscription.get()); + } + } + var responseBodyCF = this.responseBodyCF; + if (responseBodyCF != null) { + responseBodyCF.completeExceptionally(firstError); + } + // will send a RST_STREAM frame + var stream = this.stream; + if (connection.isOpen()) { + if (stream != null && stream.sendingState().isSending()) { + // no use reset if already closed. + var cause = Utils.getCompletionCause(firstError); + if (!(cause instanceof EOFException)) { + if (debug.on()) + debug.log("sending reset %s", error); + stream.reset(error.code()); + } + } + if (stream != null) { + if (debug.on()) + debug.log("request stop sending"); + stream.requestStopSending(error.code()); + } + } + } catch (Throwable ex) { + errorRef.compareAndSet(null, ex); + if (debug.on()) + debug.log("failed cancelling request: ", ex); + Log.logError(ex); + } finally { + close(); + } + } + + // cancel subscription and ignore errors in order to continue with + // the cancel/close sequence. + private void cancel(Subscription subscription) { + if (subscription == null) return; + try { subscription.cancel(); } + catch (Throwable t) { + debug.log("Unexpected exception thrown by Subscription::cancel", t); + if (Log.errors()) { + Log.logError("Unexpected exception thrown by Subscription::cancel: " + t); + Log.logError(t); + } + } + } + + @Override + CompletableFuture getResponseAsync(Executor executor) { + CompletableFuture cf; + // The code below deals with race condition that can be caused when + // completeResponse() is being called before getResponseAsync() + response_cfs_lock.lock(); + try { + if (!response_cfs.isEmpty()) { + // This CompletableFuture was created by completeResponse(). + // it will be already completed, unless the expect continue + // timeout fired + cf = response_cfs.get(0); + if (cf.isDone()) { + cf = response_cfs.remove(0); + } + + // if we find a cf here it should be already completed. + // finding a non completed cf should not happen. just assert it. + assert cf.isDone() || request.expectContinue && expectTimeoutRaised() + : "Removing uncompleted response: could cause code to hang!"; + } else { + // getResponseAsync() is called first. Create a CompletableFuture + // that will be completed by completeResponse() when + // completeResponse() is called. + cf = new MinimalFuture<>(); + response_cfs.add(cf); + } + } finally { + response_cfs_lock.unlock(); + } + if (executor != null && !cf.isDone()) { + // protect from executing later chain of CompletableFuture operations from SelectorManager thread + cf = cf.thenApplyAsync(r -> r, executor); + } + if (Log.trace()) { + Log.logTrace("Response future (stream={0}) is: {1}", streamId(), cf); + } + PushGroup pg = exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(Utils.getCompletionCause(e))); + } + if (debug.on()) debug.log("Response future is %s", cf); + return cf; + } + + /** + * Completes the first uncompleted CF on list, and removes it. If there is no + * uncompleted CF then creates one (completes it) and adds to list + */ + void completeResponse(Response resp) { + if (debug.on()) debug.log("completeResponse: %s", resp); + response_cfs_lock.lock(); + try { + CompletableFuture cf; + int cfs_len = response_cfs.size(); + for (int i = 0; i < cfs_len; i++) { + cf = response_cfs.get(i); + if (!cf.isDone() && !expectTimeoutRaised()) { + if (Log.trace()) { + Log.logTrace("Completing response (streamid={0}): {1}", + streamId(), cf); + } + if (debug.on()) + debug.log("Completing responseCF(%d) with response headers", i); + response_cfs.remove(cf); + cf.complete(resp); + return; + } else if (expectTimeoutRaised()) { + Log.logTrace("Completing response (streamid={0}): {1}", + streamId(), cf); + if (debug.on()) + debug.log("Completing responseCF(%d) with response headers", i); + // The Request will be removed in getResponseAsync() + cf.complete(resp); + return; + } // else we found the previous response: just leave it alone. + } + cf = MinimalFuture.completedFuture(resp); + if (Log.trace()) { + Log.logTrace("Created completed future (streamid={0}): {1}", + streamId(), cf); + } + if (debug.on()) + debug.log("Adding completed responseCF(0) with response headers"); + response_cfs.add(cf); + } finally { + response_cfs_lock.unlock(); + } + } + + @Override + void expectContinueFailed(int rcode) { + // Have to mark request as sent, due to no request body being sent in the + // event of a 417 Expectation Failed or some other non 100 response code + requestSent(); + } + + // methods to update state and remove stream when finished + + void requestSent() { + stateLock.lock(); + try { + requestSent0(); + } finally { + stateLock.unlock(); + } + } + + private void requestSent0() { + assert stateLock.isHeldByCurrentThread(); + requestSent = true; + if (responseReceived) { + if (debug.on()) debug.log("requestSent: streamid=%d", streamId()); + close(); + } else { + if (debug.on()) { + debug.log("requestSent: streamid=%d but response not received", streamId()); + } + } + } + + void responseReceived() { + stateLock.lock(); + try { + responseReceived0(); + } finally { + stateLock.unlock(); + } + } + + private void responseReceived0() { + assert stateLock.isHeldByCurrentThread(); + responseReceived = true; + if (requestSent) { + if (debug.on()) debug.log("responseReceived: streamid=%d", streamId()); + close(); + } else { + if (debug.on()) { + debug.log("responseReceived: streamid=%d but request not sent", streamId()); + } + } + } + + /** + * Same as {@link #completeResponse(Response)} above but for errors + */ + void completeResponseExceptionally(Throwable t) { + response_cfs_lock.lock(); + try { + // use index to avoid ConcurrentModificationException + // caused by removing the CF from within the loop. + for (int i = 0; i < response_cfs.size(); i++) { + CompletableFuture cf = response_cfs.get(i); + if (!cf.isDone()) { + response_cfs.remove(i); + cf.completeExceptionally(t); + return; + } + } + response_cfs.add(MinimalFuture.failedFuture(t)); + } finally { + response_cfs_lock.unlock(); + } + } + + @Override + void nullBody(HttpResponse resp, Throwable t) { + if (debug.on()) debug.log("nullBody: streamid=%d", streamId()); + // We should have an END_STREAM data frame waiting in the inputQ. + // We need a subscriber to force the scheduler to process it. + assert pendingResponseSubscriber == null; + pendingResponseSubscriber = HttpResponse.BodySubscribers.replacing(null); + readScheduler.runOrSchedule(); + } + + /** + * An unprocessed exchange is one that hasn't been processed by a peer. The local end of the + * connection would be notified about such exchanges when it receives a GOAWAY frame with + * a stream id that tells which exchanges have been unprocessed. + * This method is called on such unprocessed exchanges and the implementation of this method + * will arrange for the request, corresponding to this exchange, to be retried afresh on a + * new connection. + */ + void closeAsUnprocessed() { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be closed as unprocessed"; + // We arrange for the request to be retried on a new connection as allowed + // by RFC-9114, section 5.2 + this.exchange.markUnprocessedByPeer(); + this.errorRef.compareAndSet(null, new IOException("request not processed by peer")); + // close the exchange and complete the response CF exceptionally + close(); + completeResponseExceptionally(this.errorRef.get()); + if (debug.on()) { + debug.log("request unprocessed by peer " + this.request); + } + } + + // This method doesn't send any frame + void close() { + if (closed) return; + Throwable error; + stateLock.lock(); + try { + if (closed) return; + closed = true; + error = errorRef.get(); + } finally { + stateLock.unlock(); + } + if (Log.http3()) { + if (error == null) { + Log.logHttp3("Closed HTTP/3 exchange for {0}/streamId={1}", + connection.quicConnectionTag(), Long.toString(stream.streamId())); + } else { + Log.logHttp3("Closed HTTP/3 exchange for {0}/streamId={1} with error {2}", + connection.quicConnectionTag(), Long.toString(stream.streamId()), + error); + } + } + if (debug.on()) { + debug.log("stream %d is now closed with %s", + streamId(), + error == null ? "no error" : String.valueOf(error)); + } + if (Log.trace()) { + Log.logTrace("Stream {0} is now closed", streamId()); + } + + BodySubscriber subscriber = responseSubscriber; + if (subscriber == null) subscriber = pendingResponseSubscriber; + if (subscriber instanceof Http3StreamResponseSubscriber h3srs) { + // ensure subscriber is unregistered + h3srs.complete(error); + } + connection.onExchangeClose(this, streamId()); + } + + class RequestSubscriber implements Flow.Subscriber { + // can be < 0 if the actual length is not known. + private final long contentLength; + private volatile long remainingContentLength; + private volatile boolean dataHeaderWritten; + private volatile boolean completed; + private final AtomicReference subscription = new AtomicReference<>(); + + RequestSubscriber(long contentLen) { + this.contentLength = contentLen; + this.remainingContentLength = contentLen; + } + + @Override + public void onSubscribe(Subscription subscription) { + if (!this.subscription.compareAndSet(null, subscription)) { + subscription.cancel(); + throw new IllegalStateException("already subscribed"); + } + if (debug.on()) + debug.log("RequestSubscriber: onSubscribe, request 1"); + subscription.request(1); + } + + @Override + public void onNext(ByteBuffer item) { + if (debug.on()) + debug.log("RequestSubscriber: onNext(%d)", item.remaining()); + var subscription = this.subscription.get(); + if (writer.stopSendingReceived()) { + // whether StopSending contains NO_ERROR or not - we should + // not fail the request and simply stop sending the body. + // The sender should either reset the stream or send a full + // response with an error status code if it wants to fail the request. + Http3Error error = Http3Error.fromCode(writer.stream().sndErrorCode()) + .orElse(Http3Error.H3_NO_ERROR); + if (debug.on()) + debug.log("Stop sending requested by peer (%s): canceling subscription", error); + requestBodyCF.complete(null); + subscription.cancel(); + return; + } + + if (isCanceled() || errorRef.get() != null) { + if (writer.sendingState().isSending()) { + try { + if (debug.on()) { + debug.log("onNext called after stream cancelled: " + + "resetting stream %s", streamId()); + } + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: ", t); + errorRef.compareAndSet(null, t); + requestBodyCF.completeExceptionally(errorRef.get()); + } + } + return; + } + long len = item.remaining(); + try { + writeHeadersIfNeeded(item); + var remaining = remainingContentLength; + if (contentLength >= 0) { + remaining -= len; + remainingContentLength = remaining; + if (remaining < 0) { + lengthMismatch("Too many bytes in request body"); + subscription.cancel(); + } + } + var completed = remaining == 0; + if (completed) this.completed = true; + writer.scheduleForWriting(item, completed); + sentQuicBytes.addAndGet(len); + if (completed) { + requestBodyCF.complete(null); + } + if (writer.credit() > 0) { + if (debug.on()) + debug.log("RequestSubscriber: request 1"); + subscription.request(1); + } else { + if (debug.on()) + debug.log("RequestSubscriber: no more credit"); + } + } catch (Throwable t) { + if (writer.stopSendingReceived()) { + // We can reach here if we continue sending after stop sending + // was received, which may happen since stop sending is + // received asynchronously. In that case, we should + // not fail the request but simply stop sending the body. + // The sender will either reset the stream or send a full + // response with an error status code if it wants to fail + // or complete the request. + if (debug.on()) + debug.log("Stop sending requested by peer: canceling subscription"); + requestBodyCF.complete(null); + subscription.cancel(); + return; + } + // stop sending was not received: cancel the stream + errorRef.compareAndSet(null, t); + if (debug.on()) { + debug.log("Unexpected exception in onNext: " + t); + debug.log("resetting stream %s", streamId()); + } + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable rt) { + if (debug.on()) + debug.log("Failed to reset stream: %s", t); + } + cancelImpl(errorRef.get(), Http3Error.H3_REQUEST_CANCELLED); + } + + } + + private void lengthMismatch(String what) { + if (debug.on()) { + debug.log(what + " (%s/%s)", + contentLength - remainingContentLength, contentLength); + } + try { + var failed = new IOException("stream=" + streamId() + " " + + "[" + Thread.currentThread().getName() + "] " + + what + " (" + + (contentLength - remainingContentLength) + "/" + + contentLength + ")"); + errorRef.compareAndSet(null, failed); + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + requestBodyCF.completeExceptionally(errorRef.get()); + } catch (Throwable t) { + if (debug.on()) + debug.log("Failed to reset stream: %s", t); + } + close(); + } + + private void writeHeadersIfNeeded(ByteBuffer item) throws IOException { + long len = item.remaining(); + if (contentLength >= 0) { + if (!dataHeaderWritten) { + dataHeaderWritten = true; + len = contentLength; + } else { + // headers already written: nothing to do. + return; + } + } + DataFrame df = new DataFrame(len); + ByteBuffer headers = ByteBuffer.allocate(df.headersSize()); + df.writeHeaders(headers); + headers.flip(); + int sent = headers.remaining(); + writer.queueForWriting(headers); + if (sent != 0) sentQuicBytes.addAndGet(sent); + } + + @Override + public void onError(Throwable throwable) { + if (debug.on()) + debug.log(() -> "RequestSubscriber: onError: " + throwable); + // ensure that errors are handled within the flow. + if (errorRef.compareAndSet(null, throwable)) { + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: %s", t); + } + requestBodyCF.completeExceptionally(throwable); + // no need to cancel subscription + close(); + } + } + + @Override + public void onComplete() { + if (debug.on()) debug.log("RequestSubscriber: send request body completed"); + var completed = this.completed; + if (completed || errorRef.get() != null) return; + if (contentLength >= 0 && remainingContentLength != 0) { + if (remainingContentLength < 0) { + lengthMismatch("Too many bytes in request body"); + } else { + lengthMismatch("Too few bytes returned by the publisher"); + } + return; + } + this.completed = true; + try { + writer.scheduleForWriting(QuicStreamReader.EOF, true); + requestBodyCF.complete(null); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to complete stream: " + t, t); + requestBodyCF.completeExceptionally(t); + } + } + + void unblock() { + if (completed || errorRef.get() != null) { + return; + } + var subscription = this.subscription.get(); + try { + if (writer.credit() > 0) { + if (subscription != null) { + subscription.request(1); + } + } + } catch (Throwable throwable) { + if (debug.on()) + debug.log(() -> "RequestSubscriber: unblock: " + throwable); + // ensure that errors are handled within the flow. + if (errorRef.compareAndSet(null, throwable)) { + try { + writer.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to reset stream: %s", t); + } + requestBodyCF.completeExceptionally(throwable); + cancelImpl(throwable, Http3Error.H3_REQUEST_CANCELLED); + subscription.cancel(); + } + } + } + + } + + @Override + Response newResponse(HttpHeaders responseHeaders, int responseCode) { + this.responseCode = responseCode; + return this.response = new Response( + request, exchange, responseHeaders, connection(), + responseCode, Version.HTTP_3); + } + + protected void handleResponse() { + handleResponse(responseHeadersBuilder, rspHeadersConsumer, readScheduler, debug); + } + + protected void handlePromise(PushHeadersConsumer consumer) throws IOException { + PushPromiseState state = consumer.state; + PushPromiseFrame ppf = state.frame(); + promiseMap.remove(ppf); + long pushId = ppf.getPushId(); + + HttpHeaders promiseHeaders = state.headersBuilder().build(); + consumer.reset(); + + if (debug.on()) { + debug.log("received promise headers: %s", + promiseHeaders); + } + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("PUSH_PROMISE HEADERS (pushId: ") + .append(pushId).append("):\n"); + Log.dumpHeaders(sb, " ", promiseHeaders); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + String method = promiseHeaders.firstValue(":method") + .orElseThrow(() -> new ProtocolException("no method in promise request")); + String path = promiseHeaders.firstValue(":path") + .orElseThrow(() -> new ProtocolException("no path in promise request")); + String authority = promiseHeaders.firstValue(":authority") + .orElseThrow(() -> new ProtocolException("no authority in promise request")); + if (Set.of("PUT", "DELETE", "OPTIONS", "TRACE").contains(method)) { + throw new ProtocolException("push method not allowed pushId=" + pushId); + } + long clen = promiseHeaders.firstValueAsLong("Content-Length").orElse(-1); + if (clen > 0) { + throw new ProtocolException("push headers contain non-zero Content-Length for pushId=" + pushId); + } + if (promiseHeaders.firstValue("Transfer-Encoding").isPresent()) { + throw new ProtocolException("push headers contain Transfer-Encoding for pushId=" + pushId); + } + + + // this will clear the response headers + // At this point the push promise stream may not be opened yet + if (connection.onPushPromiseFrame(this, pushId, promiseHeaders)) { + // the promise response will be handled from a child of this exchange + // once the push stream is open, we have nothing more to do here. + if (debug.on()) { + debug.log("handling push promise response for %s with request-response stream %s", + pushId, streamId()); + } + } else { + // the promise response is being handled by another exchange, just accept the id + if (debug.on()) { + debug.log("push promise response for %s is already handled by another stream", + pushId); + } + PushGroup pushGroup = exchange.getPushGroup(); + connection.whenPushAccepted(pushId).thenAccept((accepted) -> { + if (accepted) { + pushGroup.acceptPushPromiseId(connection.newPushId(pushId)); + } + }); + } + } + + private void cancelPushPromise(PushPromiseState state, IOException cause) { + // send CANCEL_PUSH frame here + long pushId = state.frame().getPushId(); + connection.pushCancelled(pushId, cause); + } + + @Override + void onPollException(QuicStreamReader reader, IOException io) { + if (Log.http3()) { + Log.logHttp3("{0}/streamId={1} {2} #{3} (requestSent={4}, responseReceived={5}, " + + "reader={6}, writer={7}, statusCode={8}, finalStream={9}, " + + "receivedQuicBytes={10}, sentQuicBytes={11}): {12}", + connection().quicConnection().logTag(), + String.valueOf(reader.stream().streamId()), request, String.valueOf(exchange.multi.id), + requestSent, responseReceived, reader.receivingState(), writer.sendingState(), + String.valueOf(responseCode), connection.isFinalStream(), String.valueOf(receivedQuicBytes()), + String.valueOf(sentQuicBytes.get()), io); + } + } + + void onReaderReset() { + long errorCode = stream.rcvErrorCode(); + String resetReason = Http3Error.stringForCode(errorCode); + Http3Error resetError = Http3Error.fromCode(errorCode) + .orElse(Http3Error.H3_REQUEST_CANCELLED); + if (!requestSent || !responseReceived) { + cancelImpl(new IOException("Stream %s reset by peer: %s" + .formatted(streamId(), resetReason)), + resetError); + } + if (debug.on()) { + debug.log("Stream %s reset by peer [%s]: Stopping scheduler", + streamId(), resetReason); + } + readScheduler.stop(); + } + + + + // Invoked when some data is received from the request-response + // Quic stream + private void processQuicData() { + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + // + try { + processQuicData(reader, framesDecoder, frameOrderVerifier, readScheduler, debug); + } catch (Throwable t) { + if (debug.on()) + debug.log("processQuicData - Unexpected exception", t); + if (!requestSent) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } else if (!responseReceived) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } + } finally { + if (debug.on()) + debug.log("processQuicData - leaving - eof: %s", framesDecoder.eof()); + } + } + + void connectionError(Throwable throwable, long errorCode, String errMsg) { + if (errorRef.compareAndSet(null, throwable)) { + var streamid = streamId(); + if (debug.on()) { + if (streamid == -1) { + debug.log("cancelling stream due to connection error", throwable); + } else { + debug.log("cancelling stream " + streamid + " due to connection error", throwable); + } + } + if (Log.trace()) { + if (streamid == -1) { + Log.logTrace("connection error: {0}", errMsg); + } else { + var format = "cancelling stream {0} due to connection error: {1}"; + Log.logTrace(format, streamid, errMsg); + } + } + } + connection.connectionError(this, throwable, errorCode, errMsg); + } + + record PushPromiseState(PushPromiseFrame frame, + HeaderFrameReader reader, + HttpHeadersBuilder headersBuilder, + DecodingCallback consumer) {} + final ConcurrentHashMap promiseMap = new ConcurrentHashMap<>(); + + private void ignorePushPromiseData(PushPromiseFrame ppf, List payload) { + boolean completed = ppf.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + buf.limit(buf.position()); + if (buf == QuicStreamReader.EOF) { + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached promise: " + ppf), + Http3Error.H3_FRAME_ERROR); + } + } + + private boolean ignorePushPromiseFrame(PushPromiseFrame ppf, List payload) + throws IOException { + long pushId = ppf.getPushId(); + long minPushId = connection.getMinPushId(); + if (exchange.pushGroup == null) { + IOException checkFailed = connection.checkMaxPushId(pushId); + if (checkFailed != null) { + // connection is closed + throw checkFailed; + } + if (!connection.acceptPromises()) { + // if no stream accept promises, we can ignore the data and + // cancel the promise right away. + if (debug.on()) { + debug.log("ignoring PushPromiseFrame (no promise handler): %s%n", ppf); + } + ignorePushPromiseData(ppf, payload); + if (pushId >= minPushId) { + connection.noPushHandlerFor(pushId); + } + return true; + } + } + if (pushId < minPushId) { + if (debug.on()) { + debug.log("ignoring PushPromiseFrame (pushId=%s < %s): %s%n", + pushId, minPushId, ppf); + } + ignorePushPromiseData(ppf, payload); + return true; + } + return false; + } + + void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) + throws IOException { + var state = promiseMap.get(ppf); + if (state == null) { + if (ignorePushPromiseFrame(ppf, payload)) return; + if (debug.on()) + debug.log("received PushPromiseFrame: " + ppf); + var checkFailed = connection.checkMaxPushId(ppf.getPushId()); + if (checkFailed != null) throw checkFailed; + var builder = new HttpHeadersBuilder(); + var consumer = new PushHeadersConsumer(); + var reader = qpackDecoder.newHeaderFrameReader(consumer); + state = new PushPromiseState(ppf, reader, builder, consumer); + consumer.setState(state); + promiseMap.put(ppf, state); + } + if (debug.on()) + debug.log("receive promise headers: buffer list: " + payload); + HeaderFrameReader headerFrameReader = state.reader(); + boolean completed = ppf.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + headerFrameReader); + if (buf == QuicStreamReader.EOF) { + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached promise: " + ppf), + Http3Error.H3_FRAME_ERROR); + } + } + + /** + * This method is called by the {@link Http3PushManager} in order to + * invoke the {@link Acceptor} that will accept the push + * promise. This method gets the acceptor, invokes its {@link + * Acceptor#accepted()} method, and if {@code true}, returns the + * {@code Acceptor}. + *

        + * If the push request is not accepted this method returns {@code null}. + * + * @apiNote + * This method is called upon reception of a {@link PushPromiseFrame}. + * The quic stream that will carry the body may not be available yet. + * + * @param pushId the pushId + * @param pushRequest the promised push request + * @return an {@link Acceptor} to get the body handler for the + * push request, or {@code null}. + */ + Acceptor acceptPushPromise(long pushId, HttpRequestImpl pushRequest) { + if (Log.requests()) { + Log.logRequest("PUSH_PROMISE: " + pushRequest.toString()); + } + PushGroup pushGroup = exchange.getPushGroup(); + if (pushGroup == null || exchange.multi.requestCancelled()) { + if (Log.trace()) { + Log.logTrace("Rejecting push promise pushId: " + pushId); + } + connection.pushCancelled(pushId, null); + return null; + } + + Acceptor acceptor = null; + boolean accepted = false; + try { + acceptor = pushGroup.acceptPushRequest(pushRequest, connection.newPushId(pushId)); + accepted = acceptor.accepted(); + } catch (Throwable t) { + if (debug.on()) + debug.log("PushPromiseHandler::applyPushPromise threw exception %s", + (Object)t); + } + if (!accepted) { + // cancel / reject + if (Log.trace()) { + Log.logTrace("No body subscriber for {0}: {1}", pushRequest, + "Push " + pushId + " cancelled by users handler"); + } + connection.pushCancelled(pushId, null); + return null; + } + + assert accepted && acceptor != null; + return acceptor; + } + + /** + * This method is called by the {@link Http3PushManager} once the {@link Acceptor#cf() + * responseCF} has been obtained from the acceptor. + * @param pushId the pushId + * @param responseCF the response completable future + */ + void onPushRequestAccepted(long pushId, CompletableFuture> responseCF) { + PushGroup pushGroup = getExchange().getPushGroup(); + assert pushGroup != null; + // setup housekeeping for when the push is received + // TODO: deal with ignoring of CF anti-pattern + CompletableFuture> cf = responseCF; + cf.whenComplete((HttpResponse resp, Throwable t) -> { + t = Utils.getCompletionCause(t); + if (Log.trace()) { + Log.logTrace("Push {0} completed for {1}{2}", pushId, resp, + ((t==null) ? "": " with exception " + t)); + } + if (t != null) { + if (debug.on()) { + debug.log("completing pushResponseCF for" + + ", pushId=" + pushId + " with: " + t); + } + pushGroup.pushError(t); + } else { + if (debug.on()) { + debug.log("completing pushResponseCF for" + + ", pushId=" + pushId + " with: " + resp); + } + } + pushGroup.pushCompleted(); + }); + } + + /** + * This method is called by the {@link Http3PushPromiseStream} when + * starting + * @param pushRequest the pushRequest + * @param pushStream the pushStream + */ + void onHttp3PushStreamStarted(HttpRequestImpl pushRequest, + Http3PushPromiseStream pushStream) { + PushGroup pushGroup = getExchange().getPushGroup(); + assert pushGroup != null; + assert pushStream != null; + connection.onPushPromiseStreamStarted(pushStream, pushStream.streamId()); + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial header frame are received + void receiveHeaders(HeadersFrame headers, List payload) { + if (debug.on()) + debug.log("receive headers: buffer list: " + payload); + boolean completed = headers.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + // if we have finished receiving the header frame, pause reading until + // the status code has been decoded + if (endOfHeaders) switchReadingPaused(true); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + headerFrameReader); + if (buf == QuicStreamReader.EOF) { + eof = true; + // we are at EOF - no need to pause reading + switchReadingPaused(false); + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached: " + headers), + Http3Error.H3_FRAME_ERROR); + } + } + + + // Invoked when data can be pushed to the quic stream; + // Headers may block the stream - but they will be buffered in the stream + // so should not cause this method to be called. + // We should reach here only when sending body bytes. + private void sendQuicData() { + // This method is invoked when the sending part of the + // stream is unblocked. + if (!requestBodyCF.isDone()) { + if (!exchange.multi.requestCancelled()) { + var requestSubscriber = this.requestSubscriber; + // the requestSubscriber will request more data from + // upstream if needed + if (requestSubscriber != null) requestSubscriber.unblock(); + } + } + } + + // pushes entire response body into response subscriber + // blocking when required by local or remote flow control + CompletableFuture receiveResponseBody(BodySubscriber bodySubscriber, Executor executor) { + // ensure that the body subscriber will be subscribed and onError() is + // invoked + pendingResponseSubscriber = bodySubscriber; + + // We want to allow the subscriber's getBody() method to block, so it + // can work with InputStreams. So, we offload execution. + responseBodyCF = ResponseSubscribers.getBodyAsync(executor, bodySubscriber, + new MinimalFuture<>(), (t) -> this.cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED)); + + if (isCanceled()) { + Throwable t = getCancelCause(); + responseBodyCF.completeExceptionally(t); + } + + readScheduler.runOrSchedule(); // in case data waiting already to be processed, or error + + return responseBodyCF; + } + + void onSubscriptionError(Throwable t) { + errorRef.compareAndSet(null, t); + if (debug.on()) debug.log("Got subscription error: %s", (Object) t); + // This is the special case where the subscriber + // has requested an illegal number of items. + // In this case, the error doesn't come from + // upstream, but from downstream, and we need to + // handle the error without waiting for the inputQ + // to be exhausted. + stopRequested = true; + readScheduler.runOrSchedule(); + } + + // This loop is triggered to push response body data into + // the body subscriber. It is called from the processQuicData + // loop. However, we cannot call onNext() if we have no demands. + // So we're using a responseData queue to buffer incoming data. + void pushResponseData(ConcurrentLinkedQueue> responseData) { + if (debug.on()) debug.log("pushResponseData"); + HttpResponse.BodySubscriber subscriber = responseSubscriber; + boolean done = false; + try { + if (subscriber == null) { + subscriber = responseSubscriber = pendingResponseSubscriber; + if (subscriber == null) { + // can't process anything yet + return; + } else { + if (debug.on()) debug.log("subscribing user subscriber"); + subscriber.onSubscribe(userSubscription); + } + } + while (!responseData.isEmpty() && errorRef.get() == null) { + List data = responseData.peek(); + List dsts = Collections.unmodifiableList(data); + long size = Utils.remaining(dsts, Long.MAX_VALUE); + boolean finished = dsts.contains(QuicStreamReader.EOF); + if (size == 0 && finished) { + responseData.remove(); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onComplete"); + } + if (debug.on()) debug.log("pushResponseData: onComplete"); + done = true; + subscriber.onComplete(); + responseReceived(); + return; + } else if (userSubscription.tryDecrement()) { + responseData.remove(); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onNext {0}", size); + } + if (debug.on()) debug.log("pushResponseData: onNext(%d)", size); + subscriber.onNext(dsts); + } else { + if (stopRequested) break; + if (debug.on()) debug.log("no demand"); + return; + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + if (debug.on()) debug.log("pushResponseData: EOF"); + if (Log.trace()) { + Log.logTrace("responseSubscriber.onComplete"); + } + if (debug.on()) debug.log("pushResponseData: onComplete"); + done = true; + subscriber.onComplete(); + responseReceived(); + return; + } + } catch (Throwable throwable) { + if (debug.on()) debug.log("pushResponseData: unexpected exception", throwable); + errorRef.compareAndSet(null, throwable); + } finally { + if (done) responseData.clear(); + } + + Throwable t = errorRef.get(); + if (t != null) { + try { + if (debug.on()) + debug.log("calling subscriber.onError: %s", (Object) t); + subscriber.onError(t); + } catch (Throwable x) { + Log.logError("Subscriber::onError threw exception: {0}", x); + } finally { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + responseData.clear(); + } + } + } + + // This method is called by Http2Connection::decrementStreamCount in order + // to make sure that the stream count is decremented only once for + // a given stream. + boolean deRegister() { + return DEREGISTERED.compareAndSet(this, false, true); + } + + private static final VarHandle DEREGISTERED; + static { + try { + DEREGISTERED = MethodHandles.lookup() + .findVarHandle(Http3ExchangeImpl.class, "deRegistered", boolean.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java new file mode 100644 index 00000000000..92d51b101f7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PendingConnections.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.Http3ClientImpl.ConnectionRecovery; +import jdk.internal.net.http.Http3ClientImpl.PendingConnection; +import jdk.internal.net.http.Http3ClientImpl.StreamLimitReached; +import jdk.internal.net.http.common.Log; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/** + * This class keeps track of pending HTTP/3 connections + * to avoid making two connections to the same server + * in parallel. Methods in this class are not atomic. + * Therefore, it is expected that they will be called + * while holding a lock in order to ensure atomicity. + */ +class Http3PendingConnections { + + private final Map pendingAdvertised = new ConcurrentHashMap<>(); + private final Map pendingUnadvertised = new ConcurrentHashMap<>(); + + Http3PendingConnections() {} + + + // Called when recovery is needed for a given connection, with + // the request that got the StreamLimitException + // Should be called while holding Http3ClientImpl.lock + void streamLimitReached(String key, Http3Connection connection) { + var altSvc = connection.connection().getSourceAltService().orElse(null); + var advertised = altSvc != null && altSvc.wasAdvertised(); + var queue = advertised ? pendingAdvertised : pendingUnadvertised; + queue.computeIfAbsent(key, k -> new StreamLimitReached(connection)); + } + + // Remove a ConnectionRecovery after the connection was established + // Should be called while holding Http3ClientImpl.lock + ConnectionRecovery removeCompleted(String connectionKey, Exchange origExchange, Http3Connection conn) { + var altSvc = Optional.ofNullable(conn) + .map(Http3Connection::connection) + .flatMap(HttpQuicConnection::getSourceAltService) + .orElse(null); + var discovery = Optional.ofNullable(origExchange) + .map(Exchange::request) + .map(HttpRequestImpl::http3Discovery) + .orElse(null); + var advertised = (altSvc != null && altSvc.wasAdvertised()) + || discovery == ALT_SVC; + var sameOrigin = (altSvc != null && altSvc.originHasSameAuthority()); + + ConnectionRecovery recovered = null; + if (advertised) { + recovered = pendingAdvertised.remove(connectionKey); + } + if (discovery == ALT_SVC || recovered != null) return recovered; + if (altSvc == null) { + // for instance, there was an exception, so we don't + // know if there was an altSvc because conn == null + recovered = pendingAdvertised.get(connectionKey); + if (recovered instanceof PendingConnection pending) { + if (pending.exchange() == origExchange) { + pendingAdvertised.remove(connectionKey, recovered); + return recovered; + } + } + } + recovered = pendingUnadvertised.get(connectionKey); + if (recovered instanceof PendingConnection pending) { + if (pending.exchange() == origExchange) { + pendingUnadvertised.remove(connectionKey, recovered); + return pending; + } + } + if (!sameOrigin && advertised) return null; + return pendingUnadvertised.remove(connectionKey); + } + + // Lookup a ConnectionRecovery for the given request with the + // given key. + // Should be called while holding Http3ClientImpl.lock + ConnectionRecovery lookupFor(String key, HttpRequestImpl request, HttpClientImpl client) { + + var discovery = request.http3Discovery(); + + // if ALT_SVC only look in advertised + if (discovery == ALT_SVC) { + return pendingAdvertised.get(key); + } + + // if HTTP_3_ONLY look first in pendingUnadvertised + var unadvertised = pendingUnadvertised.get(key); + if (discovery == HTTP_3_URI_ONLY && unadvertised != null) { + if (unadvertised instanceof PendingConnection) { + return unadvertised; + } + } + + // then look in advertised + var advertised = pendingAdvertised.get(key); + if (advertised instanceof PendingConnection pending) { + var altSvc = pending.altSvc(); + var sameOrigin = altSvc != null && altSvc.originHasSameAuthority(); + assert altSvc != null; // pending advertised should have altSvc + if (discovery == ANY || sameOrigin) return advertised; + } + + // if HTTP_3_ONLY, nothing found, stop here + assert discovery != HTTP_3_URI_ONLY || !(unadvertised instanceof PendingConnection); + if (discovery == HTTP_3_URI_ONLY) { + if (advertised != null && Log.http3()) { + Log.logHttp3("{0} cannot be used for {1}: return null", advertised, request); + } + assert !(unadvertised instanceof PendingConnection); + return unadvertised; + } + + // if ANY return advertised if found, otherwise unadvertised + if (advertised instanceof PendingConnection) return advertised; + if (unadvertised instanceof PendingConnection) { + if (client.client3().isEmpty()) { + return unadvertised; + } + // if ANY and we have an alt service that's eligible for the request + // and is not same origin as the request's URI authority, then don't + // return unadvertised and instead return advertised (which may be null) + final AltService altSvc = client.client3().get().lookupAltSvc(request).orElse(null); + if (altSvc != null && !altSvc.originHasSameAuthority()) { + return advertised; + } else { + return unadvertised; + } + } + if (advertised != null) return advertised; + return unadvertised; + } + + // Adds a pending connection for the given request with the given + // key and altSvc. + // Should be called while holding Http3ClientImpl.lock + PendingConnection addPending(String key, HttpRequestImpl request, AltService altSvc, Exchange exchange) { + var discovery = request.http3Discovery(); + var advertised = altSvc != null && altSvc.wasAdvertised(); + var sameOrigin = altSvc == null || altSvc.originHasSameAuthority(); + // if advertised and same origin, we don't use pendingUnadvertised + // but pendingAdvertised even if discovery is HTTP_3_URI_ONLY + // if we have an advertised altSvc with not same origin, we still + // want to attempt HTTP_3_URI_ONLY at origin, as an unadvertised + // connection. If advertised & same origin, we can use the advertised + // service instead and use pendingAdvertised, even for HTTP_3_URI_ONLY + if (discovery == HTTP_3_URI_ONLY && (!advertised || !sameOrigin)) { + PendingConnection pendingConnection = new PendingConnection(null, exchange); + var previous = pendingUnadvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = "previous unadvertised pending connection found!" + + " (originally created for %s #%s) while adding pending connection for %s" + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + assert discovery != HTTP_3_URI_ONLY || advertised && sameOrigin; + if (advertised) { + PendingConnection pendingConnection = new PendingConnection(altSvc, exchange); + var previous = pendingAdvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = "previous pending advertised connection found!" + + " (originally created for %s #%s) while adding pending connection for %s" + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + if (discovery == ANY) { + assert !advertised; + PendingConnection pendingConnection = new PendingConnection(null, exchange); + var previous = pendingUnadvertised.put(key, pendingConnection); + if (previous instanceof PendingConnection prev) { + String msg = ("previous unadvertised pending connection found for ANY!" + + " (originally created for %s #%s) while adding pending connection for %s") + .formatted(prev.exchange().request, prev.exchange().multi.id, exchange.multi.id); + if (Log.errors()) Log.logError(msg); + assert false : msg; + } + return pendingConnection; + } + // last case - if we reach here we're ALT_SVC but couldn't + // find an advertised alt service. + assert discovery == ALT_SVC; + return null; + } +} + diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java new file mode 100644 index 00000000000..b9cf4dbc0f1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushManager.java @@ -0,0 +1,811 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; + +import static jdk.internal.net.http.Http3ClientProperties.MAX_HTTP3_PUSH_STREAMS; + +/** + * Manages HTTP/3 push promises for an HTTP/3 connection. + *

        + * This class maintains a bounded collection of recent push promises, + * together with the current state of the promise: pending, processed, or + * cancelled. When a new {@link jdk.internal.net.http.http3.frames.PushPromiseFrame} + * is received, and entry is added in the map, and the state of the promise + * is updated as it goes. + * When the map is full, old entries (lowest pushId) are expunged from + * the map. No promise will be accepted if its pushId is lower than the + * lowest pushId in the map. + * + * @apiNote + * When a PushPromiseFrame is received, {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)} + * is called. This arranges for an entry to be added to the map, unless there's + * already one. Also, the first Http3ExchangeImpl for which this method is called + * for a given pushId gets to handle the PushPromise: its {@link + * java.net.http.HttpResponse.PushPromiseHandler} will be invoked to accept the promise + * and handle the body. + *

        + * When a new PushStream is opened, {@link #onPushPromiseStream(QuicReceiverStream, long)} + * is called. When both {@code onPushPromiseFrame} and {@code onPushPromiseStream} have + * been called for a given {@code pushId}, an {@link Http3PushPromiseStream} is created + * and started to receive the body. + *

        + * {@link Http3ExchangeImpl} that receive a push promise frame, but don't get to handle + * the body (because it's already been delegated to another stream) should call + * {@link #whenAccepted(long)} to figure out when it is safe to invoke {@link + * PushGroup#acceptPushPromiseId(PushId)}. + *

        + * {@link #cancelPushPromise(long, Throwable, CancelPushReason)} can be called to cancel + * a push promise. {@link #pushPromiseProcessed(long)} should be called when the body + * has been fully processed. + */ +final class Http3PushManager { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + + private final ReentrantLock promiseLock = new ReentrantLock(); + private final ConcurrentHashMap promises = new ConcurrentHashMap<>(); + private final CompletableFuture DENIED = MinimalFuture.completedFuture(Boolean.FALSE); + private final CompletableFuture ACCEPTED = MinimalFuture.completedFuture(Boolean.TRUE); + + private final AtomicLong maxPushId = new AtomicLong(); + private final AtomicLong maxPushReceived = new AtomicLong(); + private final AtomicLong minPushId = new AtomicLong(); + // the max history we keep in the promiseMap. We start expunging old + // entries from the map when the size of the map exceeds this value + private static final long MAX_PUSH_HISTORY_SIZE = (3*MAX_HTTP3_PUSH_STREAMS)/2; + // the maxPushId increments, we send on MAX_PUSH_ID frame + // with a maxPushId incremented by that amount. + // Ideally should be <= to MAX_PUSH_HISTORY_SIZE, to avoid + // filling up the history right after the first MAX_PUSH_ID + private static final long MAX_PUSH_ID_INCREMENTS = MAX_HTTP3_PUSH_STREAMS; + private final Http3Connection connection; + + // number of pending promises + private final AtomicInteger pendingPromises = new AtomicInteger(); + // push promises are considered blocked if we have failed to send + // the last MAX_PUSH_ID update due to pendingPromises + // count having reached MAX_HTTP3_PUSH_STREAMS + private volatile boolean pushPromisesBlocked; + + + Http3PushManager(Http3Connection connection) { + this.connection = connection; + } + + String dbgTag() { + return connection.dbgTag(); + } + + public void cancelAllPromises(IOException closeCause, Http3Error error) { + for (var promise : promises.entrySet()) { + var pushId = promise.getKey(); + var pp = promise.getValue(); + switch (pp) { + case ProcessedPushPromise ignored -> {} + case CancelledPushPromise ignored -> {} + case PendingPushPromise ppp -> { + cancelPendingPushPromise(ppp, closeCause); + } + } + } + } + + // Different actions needs to be carried out when cancelling a + // push promise, depending on the state of the promise and the + // cancellation reason. + enum CancelPushReason { + NO_HANDLER, // the exchange has no PushGroup + PUSH_CANCELLED, // the PromiseHandler cancelled the push, + // or an error occurred handling the promise + CANCEL_RECEIVED; // received CANCEL_PUSH from server + } + + /** + * A PushPromise can be a PendingPushPromise, until the push + * response is completely received, or a ProcessedPushPromise, + * which replace the PendingPushPromise after the response body + * has been delivered. If the PushPromise is cancelled before + * accepting it or receiving a body, CancelledPushPromise will + * be recorded and replace the PendingPushPromise. + */ + private sealed interface PushPromise + permits PendingPushPromise, ProcessedPushPromise, CancelledPushPromise { + } + + /** + * Represent a PushPromise whose body as already been delivered + */ + private record ProcessedPushPromise(PushId pushId, HttpHeaders promiseHeaders) + implements PushPromise { } + + /** + * Represent a PushPromise that has been cancelled. No body will be delivered. + */ + private record CancelledPushPromise(PushId pushId) implements PushPromise { } + + // difficult to say what will come first - the push promise, + // or the push stream? + // The first push promise frame received will register the + // exchange with this class - and trigger the parsing of + // the request/response when the stream is available. + // The other will trigger a simple call to register the + // push id. + // Probably we also need some timer to clean + // up the map if the stream doesn't manifest after a while. + // We maintain minPushID, where any frame + // containing a push id < to the min will be discarded, + // and any stream with a pushId < will also be discarded. + + /** + * Represents a PushPromise whose body has not been delivered + * yet. + * @param the type of the body + */ + private static final class PendingPushPromise implements PushPromise { + // called when the first push promise frame is received + PendingPushPromise(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) { + this.accepted = new MinimalFuture<>(); + this.exchange = Objects.requireNonNull(exchange); + this.promiseHeaders = Objects.requireNonNull(promiseHeaders); + this.pushId = pushId; + } + + // called when the push promise stream is opened + PendingPushPromise(QuicReceiverStream stream, long pushId) { + this.accepted = new MinimalFuture<>(); + this.stream = Objects.requireNonNull(stream); + this.pushId = pushId; + } + + // volatiles should not be required since we only modify/read + // those within a lock. Final fields should ensure safe publication + final long pushId; // the push id + QuicReceiverStream stream; // the quic promise stream + Http3ExchangeImpl exchange; // the exchange that will create the body subscriber + Http3PushPromiseStream promiseStream; // the HTTP/3 stream to process the quic stream + HttpHeaders promiseHeaders; // the push promise request headers + CompletableFuture> responseCF; + HttpRequestImpl pushReq; + BodyHandler handler; + final CompletableFuture accepted; // whether the push promise was accepted + + public long pushId() { return pushId; } + + public boolean ready() { + if (stream == null) return false; + if (exchange == null) return false; + if (promiseHeaders == null) return false; + if (!accepted.isDone()) return false; + if (responseCF == null) return false; + if (pushReq == null) return false; + if (handler == null) return false; + return true; + } + + @Override + public String toString() { + return "PendingPushPromise{" + + "pushId=" + pushId + + ", stream=" + stream + + ", exchange=" + dbgTag(exchange) + + ", promiseStream=" + dbgTag(promiseStream) + + ", promiseHeaders=" + promiseHeaders + + ", accepted=" + accepted + + '}'; + } + + String dbgTag(Http3ExchangeImpl exchange) { + return exchange == null ? null : exchange.dbgTag(); + } + + String dbgTag(Http3PushPromiseStream promiseStream) { + return promiseStream == null ? null : promiseStream.dbgTag(); + } + } + + /** + * {@return the maximum pushId that can be accepted from the peer} + * This corresponds to the pushId that has been included in the last + * MAX_PUSH_ID frame sent to the peer. A pushId greater than this + * value must be rejected, and cause the connection to close with + * error. + * + * @apiNote due to internal constraints it is possible that the + * MAX_PUSH_ID frame has not been sent yet, but the {@code Http3PushManager} + * will behave as if the peer had received that frame. + * + * @see Http3Connection#checkMaxPushId(long) + * @see #checkMaxPushId(long) + */ + long getMaxPushId() { + return maxPushId.get(); + } + + /** + * {@return the minimum pushId that can be accepted from the peer} + * Any pushId strictly less than this value must be ignored. + * + * @apiNote The minimum pushId represents the smallest pushId that + * was recorded in our history. For smaller pushId, no history has + * been kept, due to history size constraints. Any pushId strictly + * less than this value must be ignored. + */ + long getMinPushId() { + return minPushId.get(); + } + + /** + * Called when a new push promise stream is created by the peer, and + * the pushId has been read. + * @param pushStream the new push promise stream + * @param pushId the pushId + */ + void onPushPromiseStream(QuicReceiverStream pushStream, long pushId) { + assert pushId >= 0; + if (!connection.acceptLargerPushPromise(pushStream, pushId)) return; + PendingPushPromise promise = addPushPromise(pushStream, pushId); + if (promise != null) { + assert promise.stream == pushStream; + // if stream is avoilable start parsing? + tryReceivePromise(promise); + } + } + + /** + * Checks whether a MAX_PUSH_ID frame needs to be sent, + * and send it. + * Called from {@link Http3Connection#checkSendMaxPushId()}. + */ + void checkSendMaxPushId() { + if (MAX_PUSH_ID_INCREMENTS <= 0) return; + long pendingCount = pendingPromises.get(); + long availableSlots = MAX_HTTP3_PUSH_STREAMS - pendingCount; + if (availableSlots <= 0) { + pushPromisesBlocked = true; + if (debug.on()) debug.log("Push promises blocked: availableSlots=%s", pendingCount); + return; + } + long maxPushIdSent = maxPushId.get(); + long maxPushIdReceived = maxPushReceived.get(); + long half = Math.max(1, MAX_PUSH_ID_INCREMENTS /2); + if (maxPushIdSent - maxPushIdReceived < half) { + // do not send a maxPushId that would consume more + // than our available slots + long increment = Math.min(availableSlots, MAX_PUSH_ID_INCREMENTS); + long update = maxPushIdSent + increment; + boolean updated = false; + try { + // let's update the counter before sending the frame, + // otherwise there's a chance we can receive a frame + // before updating the counter. + do { + if (maxPushId.compareAndSet(maxPushIdSent, update)) { + if (debug.on()) { + debug.log("MAX_PUSH_ID updated: %s (%s -> %s), increment %s, pending %s, availableSlots %s", + update, maxPushIdSent, update, increment, + promises.values().stream().filter(PendingPushPromise.class::isInstance) + .map(p -> (PendingPushPromise) p) + .map(PendingPushPromise::pushId).toList(), + availableSlots); + } + updated = true; + break; + } + maxPushIdSent = maxPushId.get(); + } while (maxPushIdSent < update); + if (updated) { + if (pushPromisesBlocked) { + if (debug.on()) debug.log("Push promises unblocked: maxPushIdSent=%s", update); + pushPromisesBlocked = false; + } + connection.sendMaxPushId(update); + } + } catch (IOException io) { + debug.log("Failed to send MAX_PUSH_ID(%s): %s", update, io); + } + } + } + + /** + * Called when a PushPromiseFrame has been decoded. + * + * @apiNote + * This method calls {@link Http3ExchangeImpl#acceptPushPromise(long, HttpRequestImpl)} + * and {@link Http3ExchangeImpl#onPushRequestAccepted(long, CompletableFuture)} + * for the first exchange that receives the {@link + * jdk.internal.net.http.http3.frames.PushPromiseFrame} + * + * @param exchange The HTTP/3 exchange that received the frame + * @param pushId The pushId contained in the frame + * @param promiseHeaders The push promise headers contained in the frame + * + * @return true if the exchange should take care of creating the HttpResponse body, + * false otherwise + * + * @see Http3Connection#onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders) + */ + boolean onPushPromiseFrame(Http3ExchangeImpl exchange, long pushId, HttpHeaders promiseHeaders) + throws IOException { + if (!connection.acceptLargerPushPromise(null, pushId)) return false; + PendingPushPromise promise = addPushPromise(exchange, pushId, promiseHeaders); + if (promise == null) { + return false; + } + // A PendingPushPromise is returned only if there was no + // PushPromise present. If a PendingPushPromise is returned + // it should therefore have its exchange already set to the + // current exchange. + assert promise.exchange == exchange; + HttpRequestImpl pushReq = HttpRequestImpl.createPushRequest( + exchange.getExchange().request(), promiseHeaders); + var acceptor = exchange.acceptPushPromise(pushId, pushReq); + if (acceptor == null) { + // nothing to do: the push should already have been cancelled. + return false; + } + @SuppressWarnings("unchecked") + var pppU = (PendingPushPromise) promise; + var responseCF = pppU.responseCF; + assert responseCF == null; + boolean cancelled = false; + promiseLock.lock(); + try { + promise.pushReq = pushReq; + pppU.responseCF = responseCF = acceptor.cf(); + // recheck to verify the push hasn't been cancelled already + var check = promises.get(pushId); + if (check instanceof CancelledPushPromise || check == null) { + cancelled = true; + } else { + assert promise == check; + pppU.handler = acceptor.bodyHandler(); + } + } finally { + promiseLock.unlock(); + } + if (!cancelled) { + exchange.onPushRequestAccepted(pushId, responseCF); + promise.accepted.complete(true); + // if stream is available start parsing? + tryReceivePromise(promise); + return true; + } else { + cancelPendingPushPromise(promise, null); + // should be a no-op - in theory it should already + // have been completed + promise.accepted.complete(false); + return false; + } + } + + /** + * {@return a completable future that will be completed when a pushId has been + * accepted by the exchange in charge of creating the response body} + * + * The completable future is complete with {@code true} if the pushId is + * accepted, and with {@code false} if the pushId was rejected or cancelled. + * + * This method is intended to be called when {@link + * #onPushPromiseFrame(Http3ExchangeImpl, long, HttpHeaders)}, returns false, + * indicating that the push promise is being delegated to another request/response + * exchange. + * On completion of the future returned here, if the future is completed + * with {@code true}, the caller is expected to call {@link + * PushGroup#acceptPushPromiseId(PushId)} in order to notify the {@link + * java.net.http.HttpResponse.PushPromiseHandler} of the received {@code pushId}. + * + * @see Http3Connection#whenPushAccepted(long) + * @param pushId the pushId + */ + CompletableFuture whenAccepted(long pushId) { + var promise = promises.get(pushId); + if (promise instanceof PendingPushPromise pp) { + return pp.accepted; + } else if (promise instanceof ProcessedPushPromise) { + return ACCEPTED; + } else { // CancelledPushPromise or null + return DENIED; + } + } + + + /** + * Cancel a push promise. In case of concurrent requests receiving the + * same pushId, where one has a PushPromiseHandler and the other doesn't, + * we will cancel the push only if reason != CANCEL_RECEIVED, or no request + * stream has already accepted the push. + * + * @param pushId the promise pushId + * @param cause the cause (can be null) + * @param reason reason for cancelling + */ + void cancelPushPromise(long pushId, Throwable cause, CancelPushReason reason) { + boolean sendCancelPush = false; + PendingPushPromise pending = null; + if (cause != null) { + debug.log("PushPromise cancelled: pushId=" + pushId, cause); + } else { + debug.log("PushPromise cancelled: pushId=%s", pushId); + String msg = "cancelPushPromise(pushId="+pushId+")"; + debug.log(msg); + } + if (reason == CancelPushReason.CANCEL_RECEIVED) { + if (checkMaxPushId(pushId) != null) { + // pushId >= max connection will be closed + return; + } + } + promiseLock.lock(); + try { + var promise = promises.get(pushId); + long min = minPushId.get(); + if (promise == null) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + if (pushId >= min) { + var cancelled = new CancelledPushPromise(connection.newPushId(pushId)); + promises.put(pushId, cancelled); + sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED; + } + } else if (promise instanceof CancelledPushPromise) { + // nothing to do + } else if (promise instanceof ProcessedPushPromise) { + // nothing we can do? + } else if (promise instanceof PendingPushPromise ppp) { + // only cancel if never accepted, or force cancel requested + if (ppp.promiseStream == null || reason != CancelPushReason.NO_HANDLER) { + var cancelled = new CancelledPushPromise(connection.newPushId(pushId)); + promises.put(pushId, cancelled); + long pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + ppp.accepted.complete(false); // NO OP if already completed + pending = ppp; + // send cancel push; do not send if we received + // a CancelPushFrame from the peer + // also do not update MAX_PUSH_ID here - MAX_PUSH_ID will + // be updated when starting the next request/response exchange that accepts + // push promises. + sendCancelPush = reason != CancelPushReason.CANCEL_RECEIVED; + } + } + } finally { + promiseLock.unlock(); + } + if (sendCancelPush) { + connection.sendCancelPush(pushId, cause); + } + if (pending != null) { + cancelPendingPushPromise(pending, cause); + } + } + + private void cancelPendingPushPromise(PendingPushPromise ppp, Throwable cause) { + var ps = ppp.stream; + var http3 = ppp.promiseStream; + var responseCF = ppp.responseCF; + if (ps != null) { + ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + if (http3 != null || responseCF != null) { + IOException io; + if (cause == null) { + io = new IOException("Push promise cancelled: " + ppp.pushId); + } else { + io = Utils.toIOException(cause); + } + if (http3 != null) { + http3.cancel(io); + } else if (responseCF != null) { + responseCF.completeExceptionally(io); + } + } + } + + /** + * Called when a push promise response body has been successfully received. + * @param pushId the pushId + */ + void pushPromiseProcessed(long pushId) { + promiseLock.lock(); + try { + var promise = promises.get(pushId); + if (promise instanceof PendingPushPromise ppp) { + var processed = new ProcessedPushPromise(connection.newPushId(pushId), + ppp.promiseHeaders); + promises.put(pushId, processed); + var pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + // do not update MAX_PUSH_ID here - MAX_PUSH_ID will + // be updated when starting the next request/response exchange that accepts + // push promises. + } + } finally { + promiseLock.unlock(); + } + } + + /** + * Checks whether the given pushId exceed the maximum pushId allowed + * to the peer, and if so, closes the connection. + * @param pushId the pushId + * @return an {@code IOException} that can be used to complete a completable + * future if the maximum pushId is exceeded, {@code null} + * otherwise + */ + IOException checkMaxPushId(long pushId) { + return connection.checkMaxPushId(pushId); + } + + // Checks whether an Http3PushPromiseStream can be created now + private void tryReceivePromise(PendingPushPromise promise) { + debug.log("tryReceivePromise: " + promise); + promiseLock.lock(); + Http3PushPromiseStream http3PushPromiseStream = null; + IOException failed = null; + try { + if (promise.ready() && promise.promiseStream == null) { + promise.promiseStream = http3PushPromiseStream = + createPushExchange(promise); + } else { + debug.log("tryReceivePromise: Can't create Http3PushPromiseStream for pushId=%s yet", + promise.pushId); + } + } catch (IOException io) { + failed = io; + } finally { + promiseLock.unlock(); + } + if (failed != null) { + cancelPushPromise(promise.pushId, failed, CancelPushReason.PUSH_CANCELLED); + return; + } + if (http3PushPromiseStream != null) { + // HTTP/3 push promises are not ref-counted + // If we were to change that it could be necessary to + // temporarly increment ref-counting here, until the stream + // read loop effectively starts. + http3PushPromiseStream.start(); + } + } + + // try to create and start an Http3PushPromiseStream when all bits have + // been received + private Http3PushPromiseStream createPushExchange(PendingPushPromise promise) + throws IOException { + assert promise.ready() : "promise is not ready: " + promise; + Http3ExchangeImpl parent = promise.exchange; + HttpRequestImpl pushReq = promise.pushReq; + QuicReceiverStream quicStream = promise.stream; + Exchange pushExch = new Exchange<>(pushReq, parent.exchange.multi); + Http3PushPromiseStream pushStream = new Http3PushPromiseStream<>(pushExch, + parent.http3Connection(), this, + quicStream, promise.responseCF, promise.handler, parent, promise.pushId); + pushExch.exchImpl = pushStream; + return pushStream; + } + + // The first exchange that gets the PushPromise gets a PushPromise object, + // others get null + // TODO: ideally we should start a timer to cancel a push promise if + // the stream doesn't materialize after a while. + // Note that the callers can always start their own timeouts using + // the CompletableFutures we returned to them. + private PendingPushPromise addPushPromise(Http3ExchangeImpl exchange, + long pushId, + HttpHeaders promiseHeaders) { + PushPromise promise = promises.get(pushId); + boolean cancelStream = false; + if (promise == null) { + promiseLock.lock(); + try { + promise = promises.get(pushId); + if (promise == null) { + if (checkMaxPushId(pushId) == null) { + if (pushId >= minPushId.get()) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + var pp = new PendingPushPromise<>(exchange, pushId, promiseHeaders); + promises.put(pushId, pp); + long pendingCount = pendingPromises.incrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + return pp; + } else { + // pushId < minPushId + cancelStream = true; + } + } else return null; + } + } finally { + promiseLock.unlock(); + } + } + if (cancelStream) { + // we don't have the stream; + // the stream will be canceled if it comes later + // do not send push cancel frame (already cancelled, or abandoned) + return null; + } + if (promise instanceof PendingPushPromise ppp) { + var pe = ppp.exchange; + if (pe == null) { + promiseLock.lock(); + try { + if (ppp.exchange == null) { + assert ppp.promiseHeaders == null; + @SuppressWarnings("unchecked") + var pppU = (PendingPushPromise) ppp; + pppU.exchange = exchange; + pppU.promiseHeaders = promiseHeaders; + return pppU; + } + } finally { + promiseLock.unlock(); + } + } + var previousHeaders = ppp.promiseHeaders; + if (previousHeaders != null && !previousHeaders.equals(promiseHeaders)) { + connection.protocolError( + new ProtocolException("push headers do not match with previous promise for " + pushId)); + } + } else if (promise instanceof ProcessedPushPromise ppp) { + if (!ppp.promiseHeaders().equals(promiseHeaders)) { + connection.protocolError( + new ProtocolException("push headers do not match with previous promise for " + pushId)); + } + } else if (promise instanceof CancelledPushPromise) { + // already cancelled - nothing to do + } + return null; + } + + // TODO: the packet opening the push promise stream might reach us before + // the push promise headers are processed. We could start a timer + // here to cancel the push promise if the PushPromiseFrame doesn't materialize + // after a while. + private PendingPushPromise addPushPromise(QuicReceiverStream stream, long pushId) { + PushPromise promise = promises.get(pushId); + boolean cancelStream = false; + if (promise == null) { + promiseLock.lock(); + try { + promise = promises.get(pushId); + if (promise == null) { + if (checkMaxPushId(pushId) == null) { + if (pushId >= minPushId.get()) { + if (pushId > maxPushReceived.get()) maxPushReceived.set(pushId); + checkExpungePromiseMap(); + var pp = new PendingPushPromise(stream, pushId); + promises.put(pushId, pp); + long pendingCount = pendingPromises.incrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + return pp; + } else { + // pushId < minPushId + cancelStream = true; + } + } else return null; // maxPushId exceeded, connection closed + } + } finally { + promiseLock.unlock(); + } + } + if (cancelStream) { + // do not send push cancel frame (already cancelled, or abandoned) + stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return null; + } + if (promise instanceof PendingPushPromise ppp) { + var ps = ppp.stream; + if (ps == null) { + promiseLock.lock(); + try { + if ((ps = ppp.stream) == null) { + ps = ppp.stream = stream; + } + } finally { + promiseLock.unlock(); + } + } + if (ps == stream) { + @SuppressWarnings("unchecked") + var pp = ((PendingPushPromise) ppp); + return pp; + } else { + // Error! cancel stream... + var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId)); + connection.connectionError(io, Http3Error.H3_ID_ERROR); + } + } else if (promise instanceof ProcessedPushPromise) { + var io = new ProtocolException("HTTP/3 pushId %s already used on this connection".formatted(pushId)); + connection.connectionError(io, Http3Error.H3_ID_ERROR); + } else { + // already cancelled? + // Error! cancel stream... + // connection.sendCancelPush(pushId, null); + stream.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + return null; + } + + // We only keep MAX_PUSH_HISTORY_SIZE entries in the map. + // If the map has more than MAX_PUSH_HISTORY_SIZE entries, we start expunging + // pushIds starting at minPushId. This method makes room for at least + // on push promise in the map + private void checkExpungePromiseMap() { + assert promiseLock.isHeldByCurrentThread(); + while (promises.size() >= MAX_PUSH_HISTORY_SIZE) { + long min = minPushId.getAndIncrement(); + var pp = promises.remove(min); + if (pp instanceof PendingPushPromise ppp) { + var pendingCount = pendingPromises.decrementAndGet(); + long ppc; + assert (ppc = promises.values().stream().filter(PendingPushPromise.class::isInstance).count()) == pendingCount + : "bad pending promise count: expected %s but found %s".formatted(pendingCount, ppc); + var http3 = ppp.promiseStream; + IOException io = null; + if (http3 != null) { + http3.cancel(io = new IOException("PushPromise cancelled")); + } + if (io == null) { + io = new IOException("PushPromise cancelled"); + } + connection.sendCancelPush(ppp.pushId, io); + var ps = ppp.stream; + if (ps != null) { + ps.requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + } + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java new file mode 100644 index 00000000000..27aaa75891c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3PushPromiseStream.java @@ -0,0 +1,746 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.net.ProtocolException; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.Http3PushManager.CancelPushReason; +import jdk.internal.net.http.common.HttpBodySubscriberWrapper; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.SubscriptionBase; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import static jdk.internal.net.http.http3.Http3Error.H3_FRAME_UNEXPECTED; + +/** + * This class represents an HTTP/3 PushPromise stream. + */ +final class Http3PushPromiseStream extends Http3Stream { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final Http3Connection connection; + private final HttpHeadersBuilder respHeadersBuilder; + private final PushRespHeadersConsumer respHeadersConsumer; + private final HeaderFrameReader respHeaderFrameReader; + private final Decoder qpackDecoder; + private final AtomicReference errorRef; + private final CompletableFuture pushCF = new MinimalFuture<>(); + private final CompletableFuture> responseCF; + private final QuicReceiverStream stream; + private final QuicStreamReader reader; + private final Http3ExchangeImpl parent; + private final long pushId; + private final Http3PushManager pushManager; + private final BodyHandler pushHandler; + + private final FramesDecoder framesDecoder = + new FramesDecoder(this::dbgTag, FramesDecoder::isAllowedOnPromiseStream); + private final SequentialScheduler readScheduler = + SequentialScheduler.lockingScheduler(this::processQuicData); + private final ReentrantLock stateLock = new ReentrantLock(); + private final H3FrameOrderVerifier frameOrderVerifier = H3FrameOrderVerifier.newForPushPromiseStream(); + + final SubscriptionBase userSubscription = + new SubscriptionBase(readScheduler, this::cancel, this::onSubscriptionError); + + volatile boolean closed; + volatile BodySubscriber pendingResponseSubscriber; + volatile BodySubscriber responseSubscriber; + volatile CompletableFuture responseBodyCF; + volatile boolean responseReceived; + volatile int responseCode; + volatile Response response; + volatile boolean stopRequested; + private String dbgTag = null; + + Http3PushPromiseStream(Exchange exchange, + final Http3Connection connection, + final Http3PushManager pushManager, + final QuicReceiverStream stream, + final CompletableFuture> responseCF, + final BodyHandler pushHandler, + Http3ExchangeImpl parent, + long pushId) { + super(exchange); + this.responseCF = responseCF; + this.pushHandler = pushHandler; + this.errorRef = new AtomicReference<>(); + this.pushId = pushId; + this.connection = connection; + this.pushManager = pushManager; + this.stream = stream; + this.parent = parent; + this.respHeadersBuilder = new HttpHeadersBuilder(); + this.respHeadersConsumer = new PushRespHeadersConsumer(); + this.qpackDecoder = connection.qpackDecoder(); + this.respHeaderFrameReader = qpackDecoder.newHeaderFrameReader(respHeadersConsumer); + this.reader = stream.connectReader(readScheduler); + debug.log("Http3PushPromiseStream created"); + } + + void start() { + exchange.exchImpl = this; + parent.onHttp3PushStreamStarted(exchange.request(), this); + this.reader.start(); + } + + long pushId() { + return pushId; + } + + String dbgTag() { + if (dbgTag != null) return dbgTag; + long streamId = streamId(); + String sid = streamId == -1 ? "?" : String.valueOf(streamId); + String ctag = connection == null ? null : connection.dbgTag(); + String tag = "Http3PushPromiseStream(" + ctag + ", streamId=" + sid + ", pushId="+ pushId + ")"; + if (streamId == -1) return tag; + return dbgTag = tag; + } + + @Override + long streamId() { + var stream = this.stream; + return stream == null ? -1 : stream.streamId(); + } + + private final class PushRespHeadersConsumer extends StreamHeadersConsumer { + + public PushRespHeadersConsumer() { + super(Context.RESPONSE); + } + + void resetDone() { + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + @Override + String headerFieldType() { + return "PUSH RESPONSE HEADER FIELD"; + } + + @Override + Decoder qpackDecoder() { + return qpackDecoder; + } + + @Override + protected String formatMessage(String message, String header) { + // Malformed requests or responses that are detected MUST be + // treated as a stream error of type H3_MESSAGE_ERROR. + return "malformed push response: " + super.formatMessage(message, header); + } + + + @Override + HeaderFrameReader headerFrameReader() { + return respHeaderFrameReader; + } + + @Override + HttpHeadersBuilder headersBuilder() { + return respHeadersBuilder; + } + + @Override + void headersCompleted() { + handleResponse(); + } + + @Override + public long streamId() { + return stream.streamId(); + } + } + + @Override + HttpQuicConnection connection() { + return connection.connection(); + } + + + // The Http3StreamResponseSubscriber is registered with the HttpClient + // to ensure that it gets completed if the SelectorManager aborts due + // to unexpected exceptions. + private void registerResponseSubscriber(Http3PushStreamResponseSubscriber subscriber) { + if (client().registerSubscriber(subscriber)) { + debug.log("Reference response body for h3 stream: " + streamId()); + client().h3StreamReference(); + } + } + + private void unregisterResponseSubscriber(Http3PushStreamResponseSubscriber subscriber) { + if (client().unregisterSubscriber(subscriber)) { + debug.log("Unreference response body for h3 stream: " + streamId()); + client().h3StreamUnreference(); + } + } + + final class Http3PushStreamResponseSubscriber extends HttpBodySubscriberWrapper { + Http3PushStreamResponseSubscriber(BodySubscriber subscriber) { + super(subscriber); + } + + @Override + protected void unregister() { + unregisterResponseSubscriber(this); + } + + @Override + protected void register() { + registerResponseSubscriber(this); + } + } + + Http3PushStreamResponseSubscriber createResponseSubscriber(BodyHandler handler, + ResponseInfo response) { + debug.log("Creating body subscriber"); + return new Http3PushStreamResponseSubscriber<>(handler.apply(response)); + } + + @Override + CompletableFuture ignoreBody() { + try { + debug.log("Ignoring body"); + reader.stream().requestStopSending(Http3Error.H3_REQUEST_CANCELLED.code()); + return MinimalFuture.completedFuture(null); + } catch (Throwable e) { + Log.logTrace("Error requesting stop sending for stream {0}: {1}", + streamId(), e.toString()); + return MinimalFuture.failedFuture(e); + } + } + + @Override + void cancel() { + debug.log("cancel"); + var stream = this.stream; + if ((stream == null)) { + cancel(new IOException("Stream cancelled before streamid assigned")); + } else { + cancel(new IOException("Stream " + stream.streamId() + " cancelled")); + } + } + + @Override + void cancel(IOException cause) { + cancelImpl(cause, Http3Error.H3_REQUEST_CANCELLED); + } + + @Override + void onProtocolError(IOException cause) { + final long streamId = stream.streamId(); + if (debug.on()) { + debug.log("cancelling exchange on stream %d due to protocol error: %s", streamId, cause.getMessage()); + } + Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamId, cause); + cancelImpl(cause, Http3Error.H3_GENERAL_PROTOCOL_ERROR); + } + + @Override + void released() { + + } + + @Override + void completed() { + + } + + @Override + boolean isCanceled() { + return errorRef.get() != null; + } + + @Override + Throwable getCancelCause() { + return errorRef.get(); + } + + @Override + void cancelImpl(Throwable e, Http3Error error) { + try { + var streamid = streamId(); + if (errorRef.compareAndSet(null, e)) { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", e); + else debug.log("cancelling stream " + streamid + ":", e); + } + if (Log.trace()) { + if (streamid == -1) Log.logTrace("cancelling stream: {0}\n", e); + else Log.logTrace("cancelling stream {0}: {1}\n", streamid, e); + } + } else { + if (debug.on()) { + if (streamid == -1) debug.log("cancelling stream: %s", (Object) e); + else debug.log("cancelling stream %s: %s", streamid, e); + } + } + + var firstError = errorRef.get(); + completeResponseExceptionally(firstError); + if (responseBodyCF != null) { + responseBodyCF.completeExceptionally(firstError); + } + // will send a RST_STREAM frame + var stream = this.stream; + if (connection.isOpen()) { + if (stream != null) { + if (debug.on()) + debug.log("request stop sending"); + stream.requestStopSending(error.code()); + } + } + } catch (Throwable ex) { + debug.log("failed cancelling request: ", ex); + Log.logError(ex); + } finally { + close(); + } + } + + @Override + CompletableFuture getResponseAsync(Executor executor) { + var cf = pushCF; + if (executor != null && !cf.isDone()) { + // protect from executing later chain of CompletableFuture operations from SelectorManager thread + cf = cf.thenApplyAsync(r -> r, executor); + } + Log.logTrace("Response future (stream={0}) is: {1}", streamId(), cf); + if (debug.on()) debug.log("Response future is %s", cf); + return cf; + } + + void completeResponse(Response r) { + debug.log("Response: " + r); + Log.logResponse(r::toString); + pushCF.complete(r); // not strictly required for push API + // start reading the body using the obtained BodySubscriber + CompletableFuture start = new MinimalFuture<>(); + start.thenCompose( v -> readBodyAsync(getPushHandler(), false, getExchange().executor())) + .whenComplete((T body, Throwable t) -> { + if (t != null) { + responseCF.completeExceptionally(t); + debug.log("Cancelling push promise %s (stream %s) due to: %s", pushId, streamId(), t); + pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED); + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } else { + HttpResponseImpl resp = + new HttpResponseImpl<>(r.request, r, null, body, getExchange()); + debug.log("Completing responseCF: " + resp); + pushManager.pushPromiseProcessed(pushId); + responseCF.complete(resp); + } + }); + start.completeAsync(() -> null, getExchange().executor()); + } + + // methods to update state and remove stream when finished + + void responseReceived() { + stateLock.lock(); + try { + responseReceived0(); + } finally { + stateLock.unlock(); + } + } + + private void responseReceived0() { + assert stateLock.isHeldByCurrentThread(); + responseReceived = true; + if (debug.on()) debug.log("responseReceived: streamid=%d", streamId()); + close(); + } + + /** + * same as above but for errors + */ + void completeResponseExceptionally(Throwable t) { + pushManager.cancelPushPromise(pushId, t, CancelPushReason.PUSH_CANCELLED); + responseCF.completeExceptionally(t); + } + + void nullBody(HttpResponse resp, Throwable t) { + if (debug.on()) debug.log("nullBody: streamid=%d", streamId()); + // We should have an END_STREAM data frame waiting in the inputQ. + // We need a subscriber to force the scheduler to process it. + assert pendingResponseSubscriber == null; + pendingResponseSubscriber = HttpResponse.BodySubscribers.replacing(null); + readScheduler.runOrSchedule(); + } + + @Override + CompletableFuture> sendHeadersAsync() { + return MinimalFuture.completedFuture(this); + } + + @Override + CompletableFuture> sendBodyAsync() { + return MinimalFuture.completedFuture(this); + } + + CompletableFuture> responseCF() { + return responseCF; + } + + + BodyHandler getPushHandler() { + // ignored parameters to function can be used as BodyHandler + return this.pushHandler; + } + + @Override + CompletableFuture readBodyAsync(BodyHandler handler, + boolean returnConnectionToPool, + Executor executor) { + try { + Log.logTrace("Reading body on stream {0}", streamId()); + debug.log("Getting BodySubscriber for: " + response); + Http3PushStreamResponseSubscriber bodySubscriber = + createResponseSubscriber(handler, new ResponseInfoImpl(response)); + CompletableFuture cf = receiveResponseBody(bodySubscriber, executor); + + PushGroup pg = parent.exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + cf = cf.whenComplete((t, e) -> pg.pushError(e)); + } + var bodyCF = cf; + return bodyCF; + } catch (Throwable t) { + // may be thrown by handler.apply + // TODO: Is this the right error code? + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + PushGroup pg = parent.exchange.getPushGroup(); + if (pg != null) { + // if an error occurs make sure it is recorded in the PushGroup + pg.pushError(t); + } + return MinimalFuture.failedFuture(t); + } + } + + // This method doesn't send any frame + void close() { + if (closed) return; + stateLock.lock(); + try { + if (closed) return; + closed = true; + } finally { + stateLock.unlock(); + } + if (debug.on()) debug.log("stream %d is now closed", streamId()); + Log.logTrace("Stream {0} is now closed", streamId()); + + BodySubscriber subscriber = responseSubscriber; + if (subscriber == null) subscriber = pendingResponseSubscriber; + if (subscriber instanceof Http3PushStreamResponseSubscriber h3srs) { + // ensure subscriber is unregistered + h3srs.complete(errorRef.get()); + } + connection.onPushPromiseStreamClosed(this, streamId()); + } + + @Override + Response newResponse(HttpHeaders responseHeaders, int responseCode) { + return this.response = new Response( + exchange.request, exchange, responseHeaders, connection(), + responseCode, Version.HTTP_3); + } + + protected void handleResponse() { + handleResponse(respHeadersBuilder, respHeadersConsumer, readScheduler, debug); + } + + @Override + void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) throws IOException { + readScheduler.stop(); + connectionError(new ProtocolException("Unexpected PUSH_PROMISE on push response stream"), H3_FRAME_UNEXPECTED); + } + + @Override + void onPollException(QuicStreamReader reader, IOException io) { + if (Log.http3()) { + Log.logHttp3("{0}/streamId={1} pushId={2} #{3} (responseReceived={4}, " + + "reader={5}, statusCode={6}, finalStream={9}): {10}", + connection().quicConnection().logTag(), + String.valueOf(reader.stream().streamId()), pushId, String.valueOf(exchange.multi.id), + responseReceived, reader.receivingState(), + String.valueOf(responseCode), connection.isFinalStream(), io); + } + } + + @Override + void onReaderReset() { + long errorCode = stream.rcvErrorCode(); + String resetReason = Http3Error.stringForCode(errorCode); + Http3Error resetError = Http3Error.fromCode(errorCode) + .orElse(Http3Error.H3_REQUEST_CANCELLED); + if (!responseReceived) { + cancelImpl(new IOException("Stream %s reset by peer: %s" + .formatted(streamId(), resetReason)), + resetError); + } + if (debug.on()) { + debug.log("Stream %s reset by peer [%s]: Stopping scheduler", + streamId(), resetReason); + } + readScheduler.stop(); + } + + // Invoked when some data is received from the request-response + // Quic stream + private void processQuicData() { + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + try { + processQuicData(reader, framesDecoder, frameOrderVerifier, readScheduler, debug); + } catch (Throwable t) { + debug.log("processQuicData - Unexpected exception", t); + if (!responseReceived) { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + } + } finally { + debug.log("processQuicData - leaving - eof: %s", framesDecoder.eof()); + } + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial header frame are received + void receiveHeaders(HeadersFrame headers, List payload) + throws IOException { + debug.log("receive headers: buffer list: " + payload); + boolean completed = headers.remaining() == 0; + boolean eof = false; + if (payload != null) { + int last = payload.size() - 1; + for (int i = 0; i <= last; i++) { + ByteBuffer buf = payload.get(i); + boolean endOfHeaders = completed && i == last; + if (debug.on()) + debug.log("QPack decoding %s bytes from headers (last: %s)", + buf.remaining(), last); + // if we have finished receiving the header frame, pause reading until + // the status code has been decoded + if (endOfHeaders) switchReadingPaused(true); + qpackDecoder.decodeHeader(buf, + endOfHeaders, + respHeaderFrameReader); + if (buf == QuicStreamReader.EOF) { + // we are at EOF - no need to pause reading + switchReadingPaused(false); + eof = true; + } + } + } + if (!completed && eof) { + cancelImpl(new EOFException("EOF reached: " + headers), + Http3Error.H3_REQUEST_CANCELLED); + } + } + + void connectionError(Throwable throwable, long errorCode, String errMsg) { + if (errorRef.compareAndSet(null, throwable)) { + var streamid = streamId(); + if (debug.on()) { + if (streamid == -1) { + debug.log("cancelling stream due to connection error", throwable); + } else { + debug.log("cancelling stream " + streamid + + " due to connection error", throwable); + } + } + if (Log.trace()) { + if (streamid == -1) { + Log.logTrace( "connection error: {0}", errMsg); + } else { + var format = "cancelling stream {0} due to connection error: {1}"; + Log.logTrace(format, streamid, errMsg); + } + } + } + connection.connectionError(this, throwable, errorCode, errMsg); + } + + + // pushes entire response body into response subscriber + // blocking when required by local or remote flow control + CompletableFuture receiveResponseBody(BodySubscriber bodySubscriber, Executor executor) { + // We want to allow the subscriber's getBody() method to block so it + // can work with InputStreams. So, we offload execution. + responseBodyCF = ResponseSubscribers.getBodyAsync(executor, bodySubscriber, + new MinimalFuture<>(), (t) -> this.cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED)); + + if (isCanceled()) { + Throwable t = getCancelCause(); + responseBodyCF.completeExceptionally(t); + } + + // ensure that the body subscriber will be subsribed and onError() is + // invoked + pendingResponseSubscriber = bodySubscriber; + readScheduler.runOrSchedule(); // in case data waiting already to be processed, or error + + return responseBodyCF; + } + + void onSubscriptionError(Throwable t) { + errorRef.compareAndSet(null, t); + if (debug.on()) debug.log("Got subscription error: %s", (Object) t); + // This is the special case where the subscriber + // has requested an illegal number of items. + // In this case, the error doesn't come from + // upstream, but from downstream, and we need to + // handle the error without waiting for the inputQ + // to be exhausted. + stopRequested = true; + readScheduler.runOrSchedule(); + } + + // This loop is triggered to push response body data into + // the body subscriber. + void pushResponseData(ConcurrentLinkedQueue> responseData) { + debug.log("pushResponseData"); + boolean onCompleteCalled = false; + BodySubscriber subscriber = responseSubscriber; + boolean done = false; + try { + if (subscriber == null) { + subscriber = responseSubscriber = pendingResponseSubscriber; + if (subscriber == null) { + // can't process anything yet + return; + } else { + if (debug.on()) debug.log("subscribing user subscriber"); + subscriber.onSubscribe(userSubscription); + } + } + while (!responseData.isEmpty()) { + List data = responseData.peek(); + List dsts = Collections.unmodifiableList(data); + long size = Utils.remaining(dsts, Long.MAX_VALUE); + boolean finished = dsts.contains(QuicStreamReader.EOF); + if (size == 0 && finished) { + responseData.remove(); + Log.logTrace("responseSubscriber.onComplete"); + if (debug.on()) debug.log("pushResponseData: onComplete"); + subscriber.onComplete(); + done = true; + onCompleteCalled = true; + responseReceived(); + return; + } else if (userSubscription.tryDecrement()) { + responseData.remove(); + Log.logTrace("responseSubscriber.onNext {0}", size); + if (debug.on()) debug.log("pushResponseData: onNext(%d)", size); + subscriber.onNext(dsts); + } else { + if (stopRequested) break; + debug.log("no demand"); + return; + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + debug.log("pushResponseData: EOF"); + if (!onCompleteCalled) { + Log.logTrace("responseSubscriber.onComplete"); + if (debug.on()) debug.log("pushResponseData: onComplete"); + subscriber.onComplete(); + done = true; + onCompleteCalled = true; + responseReceived(); + return; + } + } + } catch (Throwable throwable) { + debug.log("pushResponseData: unexpected exception", throwable); + errorRef.compareAndSet(null, throwable); + } finally { + if (done) responseData.clear(); + } + + Throwable t = errorRef.get(); + if (t != null) { + try { + if (!onCompleteCalled) { + if (debug.on()) + debug.log("calling subscriber.onError: %s", (Object) t); + subscriber.onError(t); + } else { + if (debug.on()) + debug.log("already completed: dropping error %s", (Object) t); + } + } catch (Throwable x) { + Log.logError("Subscriber::onError threw exception: {0}", t); + } finally { + cancelImpl(t, Http3Error.H3_REQUEST_CANCELLED); + responseData.clear(); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java new file mode 100644 index 00000000000..cdac68b47f1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http3Stream.java @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.io.EOFException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.OptionalLong; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.Http3FrameType; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; +import static jdk.internal.net.http.RedirectFilter.HTTP_NOT_MODIFIED; + +/** + * A common super class for the HTTP/3 request/response stream ({@link Http3ExchangeImpl} + * and the HTTP/3 push promises stream ({@link Http3PushPromiseStream}. + * @param the expected type of the response body + */ +sealed abstract class Http3Stream extends ExchangeImpl permits Http3ExchangeImpl, Http3PushPromiseStream { + enum ResponseState { PERMIT_HEADER, PERMIT_TRAILER, PERMIT_NONE } + + // count of bytes read from the Quic stream. This is weakly consistent and + // used for debug only. Must not be updated outside of processQuicData + private volatile long receivedQuicBytes; + // keep track of which HTTP/3 frames have been parsed and whether more header + // frames are permitted + private ResponseState responseState = ResponseState.PERMIT_HEADER; + // value of content-length header in the response header, or null + private Long contentLength; + // number of data bytes delivered to user subscriber + private long consumedDataBytes; + // switched to true if reading from the quic stream should be temporarily + // paused. After switching back to false, readScheduler.runOrSchedule() should + // called. + private volatile boolean readingPaused; + + // A temporary buffer for response body bytes + final ConcurrentLinkedQueue> responseData = new ConcurrentLinkedQueue<>(); + + private final AtomicInteger nonFinalResponseCount = new AtomicInteger(); + + + Http3Stream(Exchange exchange) { + super(exchange); + } + + /** + * Cancel the stream exchange on error + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param error an HTTP/3 error + */ + abstract void cancelImpl(Throwable throwable, Http3Error error); + + /** + * {@return the Quic stream id for this exchange (request/response or push response)} + */ + abstract long streamId(); + + /** + * A base class implementing {@link DecodingCallback} used for receiving + * and building HttpHeaders. Can be used for request headers, response headers, + * push response headers, or trailers. + */ + abstract class StreamHeadersConsumer extends ValidatingHeadersConsumer + implements DecodingCallback { + + private volatile boolean hasError; + + StreamHeadersConsumer(Context context) { + super(context); + } + + abstract Decoder qpackDecoder(); + + abstract HeaderFrameReader headerFrameReader(); + + abstract HttpHeadersBuilder headersBuilder(); + + abstract void resetDone(); + + @Override + public void reset() { + super.reset(); + headerFrameReader().reset(); + headersBuilder().clear(); + hasError = false; + resetDone(); + } + + String headerFieldType() {return "HEADER FIELD";} + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + try { + String n = name.toString(); + String v = value.toString(); + super.onDecoded(n, v); + headersBuilder().addHeader(n, v); + if (Log.headers() && Log.trace()) { + Log.logTrace("RECEIVED {0} (streamid={1}): {2}: {3}", + headerFieldType(), streamId(), n, v); + } + } catch (Throwable throwable) { + if (throwable instanceof UncheckedIOException uio) { + // UncheckedIOException is thrown by ValidatingHeadersConsumer.onDecoded + // for cases with invalid headers or unknown/unsupported pseudo-headers. + // It should be treated as a malformed request. + // RFC-9114 4.1.2. Malformed Requests and Responses: + // Malformed requests or responses that are + // detected MUST be treated as a stream error of + // type H3_MESSAGE_ERROR. + onStreamError(uio.getCause(), Http3Error.H3_MESSAGE_ERROR); + } else { + onConnectionError(throwable, Http3Error.H3_INTERNAL_ERROR); + } + } + } + + @Override + public void onComplete() { + // RFC-9204 2.2.2.1: After the decoder finishes decoding a field + // section encoded using representations containing dynamic table + // references, it MUST emit a Section Acknowledgment instruction + qpackDecoder().ackSection(streamId(), headerFrameReader()); + qpackDecoder().resetInsertionsCounter(); + headersCompleted(); + } + + abstract void headersCompleted(); + + @Override + public void onStreamError(Throwable throwable, Http3Error http3Error) { + hasError = true; + qpackDecoder().resetInsertionsCounter(); + // Stream error + cancelImpl(throwable, http3Error); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + hasError = true; + // Connection error + connectionError(throwable, http3Error); + } + + @Override + public boolean hasError() { + return hasError; + } + + } + + /** + * {@return count of bytes read from the QUIC stream so far} + */ + public long receivedQuicBytes() { + return receivedQuicBytes; + } + + /** + * Notify of a connection error. + * + * The implementation of this method is supposed to close all + * exchanges, cancel all push promises, and close the connection. + * + * @implSpec + * The implementation of this method calls + * {@snippet lang=java : + * connectionError(throwable, error.code(), throwable.getMessage()); + * } + * + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param error an HTTP/3 error + */ + void connectionError(Throwable throwable, Http3Error error) { + connectionError(throwable, error.code(), throwable.getMessage()); + } + + + /** + * Notify of a connection error. + * + * The implementation of this method is supposed to close all + * exchanges, cancel all push promises, and close the connection. + * + * @param throwable an exception to be relayed to the multi exchange + * through the completable future chain + * @param errorCode an HTTP/3 error code + * @param errMsg an error message to be logged when closing the connection + */ + abstract void connectionError(Throwable throwable, long errorCode, String errMsg); + + + /** + * Push response data to the {@linkplain java.net.http.HttpResponse.BodySubscriber + * response body subscriber} if allowed by the subscription state. + * @param responseData a queue of available data to be pushed to the subscriber + */ + abstract void pushResponseData(ConcurrentLinkedQueue> responseData); + + /** + * Called when an exception is thrown by {@link QuicStreamReader#poll() reader::poll} + * when called from {@link #processQuicData(QuicStreamReader, FramesDecoder, + * H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData}. + * This is typically only used for logging purposes. + * @param reader the stream reader + * @param io the exception caught + */ + abstract void onPollException(QuicStreamReader reader, IOException io); + + /** + * Called when new payload data is received by {@link #processQuicData(QuicStreamReader, + * FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData} + * for a given header frame. + *

        + * Any exception thrown here will be rethrown by {@code processQuicData} + * + * @param headers a partially received header frame + * @param payload the payload bytes available for that frame + * @throws IOException if an error is detected + */ + abstract void receiveHeaders(HeadersFrame headers, List payload) throws IOException; + + /** + * Called when new payload data is received by {@link #processQuicData(QuicStreamReader, + * FramesDecoder, H3FrameOrderVerifier, SequentialScheduler, Logger) processQuicData} + * for a given push promise frame. + *

        + * Any exception thrown here will be rethrown by {@code processQuicData} + * + * @param ppf a partially received push promise frame + * @param payload the payload bytes available for that frame + * @throws IOException if an error is detected + */ + abstract void receivePushPromiseFrame(PushPromiseFrame ppf, List payload) throws IOException; + + /** + * {@return whether reading from the quic stream is currently paused} + * Typically reading is paused when waiting for headers to be decoded by QPack. + */ + boolean readingPaused() {return readingPaused;} + + /** + * Switches the value of the {@link #readingPaused() readingPaused} + * flag + *

        + * Subclasses of {@code Http3Stream} can call this method to switch + * the value of this flag if needed, typically in their + * concrete implementation of {@link #receiveHeaders(HeadersFrame, List)}. + * @param value the new value + */ + void switchReadingPaused(boolean value) { + readingPaused = value; + } + + // invoked when ByteBuffers containing the next payload bytes for the + // given partial data frame are received. + private void receiveData(DataFrame data, List payload, Logger debug) { + if (debug.on()) { + debug.log("receiveData: adding %s payload byte", Utils.remaining(payload)); + } + responseData.add(payload); + pushResponseData(responseData); + } + + private ByteBuffer pollIfNotReset(QuicStreamReader reader) throws IOException { + ByteBuffer buffer; + try { + if (reader.isReset()) return null; + buffer = reader.poll(); + } catch (IOException io) { + if (reader.isReset()) return null; + onPollException(reader, io); + throw io; + } + return buffer; + } + + private Throwable toThrowable(MalformedFrame malformedFrame) { + Throwable cause = malformedFrame.getCause(); + if (cause != null) return cause; + return new ProtocolException(malformedFrame.toString()); + } + + /** + * Called when {@code processQuicData} detects that the {@linkplain + * QuicStreamReader reader} has been reset. + * This method should do the appropriate garbage collection, + * possibly closing the exchange or the connection if needed, and + * closing the read scheduler. + */ + abstract void onReaderReset(); + + /** + * Invoked when some data is received from the underlying quic stream. + * This implements the read loop for a request-response stream or a + * push response stream. + */ + void processQuicData(QuicStreamReader reader, + FramesDecoder framesDecoder, + H3FrameOrderVerifier frameOrderVerifier, + SequentialScheduler readScheduler, + Logger debug) throws IOException { + + + // Poll bytes from the request-response stream + // and parses the data to read HTTP/3 frames. + // + // If the frame being read is a header frame, send the + // compacted header field data to QPack. + // + // Otherwise, if it's a data frame, send the bytes + // to the response body subscriber. + // + // Finally, if the frame being read is a PushPromiseFrame, + // sends the compressed field data to the QPack decoder to + // decode the push promise request headers. + // + + // the reader might be null if the loop is triggered before + // the field is assigned + if (reader == null) return; + + // check whether we need to wait until response headers + // have been decoded: in that case readingPaused will be true + if (readingPaused) return; + + if (debug.on()) debug.log("processQuicData"); + ByteBuffer buffer; + Http3Frame frame; + pushResponseData(responseData); + boolean readmore = responseData.isEmpty(); + // do not read more until data has been pulled + while (readmore && (buffer = pollIfNotReset(reader)) != null) { + if (debug.on()) + debug.log("processQuicData - submitting buffer: %s bytes (ByteBuffer@%s)", + buffer.remaining(), System.identityHashCode(buffer)); + // only updated here + var received = receivedQuicBytes; + receivedQuicBytes = received + buffer.remaining(); + framesDecoder.submit(buffer); + while ((frame = framesDecoder.poll()) != null) { + if (debug.on()) debug.log("processQuicData - frame: " + frame); + final long frameType = frame.type(); + // before we start processing, verify that this frame *type* has arrived in the + // allowed order + if (!frameOrderVerifier.allowsProcessing(frame)) { + final String unexpectedFrameType = Http3FrameType.asString(frameType); + // not expected to be arriving now + // RFC-9114, section 4.1 - Receipt of an invalid sequence of frames MUST be + // treated as a connection error of type H3_FRAME_UNEXPECTED. + if (debug.on()) { + debug.log("unexpected (order of) frame type: " + + unexpectedFrameType + " on stream"); + } + Log.logError("Connection error due to unexpected (order of) frame type" + + " {0} on stream", unexpectedFrameType); + readScheduler.stop(); + final String errMsg = "Unexpected frame " + unexpectedFrameType; + connectionError(new ProtocolException(errMsg), Http3Error.H3_FRAME_UNEXPECTED); + return; + } + if (frame instanceof PartialFrame partialFrame) { + final List payload = framesDecoder.readPayloadBytes(); + if (debug.on()) { + debug.log("processQuicData - payload: %s", + payload == null ? null : Utils.remaining(payload)); + } + if (framesDecoder.eof() && !framesDecoder.clean()) { + String msg = "Frame truncated: " + partialFrame; + connectionError(new ProtocolException(msg), + Http3Error.H3_FRAME_ERROR.code(), + msg); + break; + } + if ((payload == null || payload.isEmpty()) && partialFrame.remaining() != 0) { + break; + } + if (partialFrame instanceof HeadersFrame headers) { + receiveHeaders(headers, payload); + // check if we need to wait for the status code to be decoded + // before reading more + readmore = !readingPaused; + } else if (partialFrame instanceof DataFrame data) { + if (responseState != ResponseState.PERMIT_TRAILER) { + cancelImpl(new IOException("DATA frame not expected here"), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (payload != null) { + consumedDataBytes += Utils.remaining(payload); + if (contentLength != null && + consumedDataBytes + data.remaining() > contentLength) { + cancelImpl(new IOException( + String.format("DATA frame (length %d) exceeds content-length (%d) by %d", + data.streamingLength(), contentLength, + consumedDataBytes + data.remaining() - contentLength)), + Http3Error.H3_MESSAGE_ERROR); + return; + } + // don't read more if there is pending data waiting + // to be read from downstream + readmore = responseData.isEmpty(); + receiveData(data, payload, debug); + } + } else if (partialFrame instanceof PushPromiseFrame ppf) { + receivePushPromiseFrame(ppf, payload); + } else if (partialFrame instanceof UnknownFrame) { + if (debug.on()) { + debug.log("ignoring %s bytes for unknown frame type: %s", + Utils.remaining(payload), + Http3FrameType.asString(frameType)); + } + } else { + // should never come here: the only frame that we can + // receive on a request-response stream are + // HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN + // All have already been taken care above. + // So this here should be dead-code. + String msg = "unhandled frame type: " + + Http3FrameType.asString(frameType); + if (debug.on()) debug.log("Warning: %s", msg); + throw new AssertionError(msg); + } + // mark as complete, if all expected data has been read for a frame + if (partialFrame.remaining() == 0) { + frameOrderVerifier.completed(frame); + } + } else if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + readScheduler.stop(); + connectionError(toThrowable(malformed), + malformed.getErrorCode(), + malformed.getMessage()); + return; + } else { + // should never come here: the only frame that we can + // receive on a request-response stream are + // HEADERS, DATA, PUSH_PROMISE, and RESERVED/UNKNOWN + // All should have already been taken care above, + // including malformed frames. So this here should be + // dead-code. + String msg = "unhandled frame type: " + + Http3FrameType.asString(frameType); + if (debug.on()) debug.log("Warning: %s", msg); + throw new AssertionError(msg); + } + if (framesDecoder.eof()) break; + } + if (framesDecoder.eof()) break; + } + if (framesDecoder.eof()) { + if (!framesDecoder.clean()) { + String msg = "EOF reading frame type and length"; + connectionError(new ProtocolException(msg), + Http3Error.H3_FRAME_ERROR.code(), + msg); + } + if (debug.on()) debug.log("processQuicData - EOF"); + if (responseState == ResponseState.PERMIT_HEADER) { + cancelImpl(new EOFException("EOF reached: no header bytes received"), Http3Error.H3_MESSAGE_ERROR); + } else { + if (contentLength != null && + consumedDataBytes != contentLength) { + cancelImpl(new IOException( + String.format("fixed content-length: %d, bytes received: %d", contentLength, consumedDataBytes)), + Http3Error.H3_MESSAGE_ERROR); + return; + } + receiveData(new DataFrame(0), + List.of(QuicStreamReader.EOF), debug); + } + } + if (framesDecoder.eof() && responseData.isEmpty()) { + if (debug.on()) debug.log("EOF: Stopping scheduler"); + readScheduler.stop(); + } + if (reader.isReset() && responseData.isEmpty()) { + onReaderReset(); + } + } + + final String checkInterimResponseCountExceeded() { + // this is also checked by Exchange - but tracking it here too provides + // a more informative message. + int count = nonFinalResponseCount.incrementAndGet(); + if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) { + return String.format( + "Stream %s PROTOCOL_ERROR: too many interim responses received: %s > %s", + streamId(), count, MAX_NON_FINAL_RESPONSES); + } + return null; + } + + /** + * Called to create a new Response object for the newly receive response headers and + * response status code. This method is called from {@link #handleResponse(HttpHeadersBuilder, + * StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, after the status code + * and headers have been validated. + * + * @param responseHeaders response headers + * @param responseCode response code + * @return a new {@code Response} object + */ + abstract Response newResponse(HttpHeaders responseHeaders, int responseCode); + + /** + * Called at the end of {@link #handleResponse(HttpHeadersBuilder, + * StreamHeadersConsumer, SequentialScheduler, Logger) handleResponse}, to propagate + * the response to the multi exchange. + * @param response the {@code Response} that was received. + */ + abstract void completeResponse(Response response); + + /** + * Validate response headers and status code based on the {@link #responseState}. + * If validated, this method will call {@link #newResponse(HttpHeaders, int)} to + * create a {@code Response} object, which it will then pass to + * {@link #completeResponse(Response)}. + * + * @param responseHeadersBuilder the response headers builder + * @param rspHeadersConsumer the response headers consumer + * @param readScheduler the read scheduler + * @param debug the debug logger + */ + void handleResponse(HttpHeadersBuilder responseHeadersBuilder, + StreamHeadersConsumer rspHeadersConsumer, + SequentialScheduler readScheduler, + Logger debug) { + if (responseState == ResponseState.PERMIT_NONE) { + connectionError(new ProtocolException("HEADERS after trailer"), + Http3Error.H3_FRAME_UNEXPECTED.code(), + "HEADERS after trailer"); + return; + } + HttpHeaders responseHeaders = responseHeadersBuilder.build(); + if (responseState == ResponseState.PERMIT_TRAILER) { + if (responseHeaders.firstValue(":status").isPresent()) { + cancelImpl(new IOException("Unexpected :status header in trailer"), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (Log.headers()) { + Log.logHeaders("Ignoring trailers on stream {0}: {1}", streamId(), responseHeaders); + } else if (debug.on()) { + debug.log("Ignoring trailers: %s", responseHeaders); + } + responseState = ResponseState.PERMIT_NONE; + rspHeadersConsumer.reset(); + if (readingPaused) { + readingPaused = false; + readScheduler.runOrSchedule(exchange.executor()); + } + return; + } + + int responseCode; + boolean finalResponse = false; + try { + responseCode = (int) responseHeaders + .firstValueAsLong(":status") + .orElseThrow(() -> new IOException("no statuscode in response")); + } catch (IOException | NumberFormatException exception) { + // RFC-9114: 4.1.2. Malformed Requests and Responses: + // "Malformed requests or responses that are + // detected MUST be treated as a stream error of type H3_MESSAGE_ERROR" + cancelImpl(exception, Http3Error.H3_MESSAGE_ERROR); + return; + } + if (responseCode < 100 || responseCode > 999) { + cancelImpl(new IOException("Unexpected :status header value"), Http3Error.H3_MESSAGE_ERROR); + return; + } + + if (responseCode >= 200) { + responseState = ResponseState.PERMIT_TRAILER; + finalResponse = true; + } else { + assert responseCode >= 100 && responseCode <= 200 : "unexpected responseCode: " + responseCode; + String protocolErrorMsg = checkInterimResponseCountExceeded(); + if (protocolErrorMsg != null) { + if (debug.on()) { + debug.log(protocolErrorMsg); + } + cancelImpl(new ProtocolException(protocolErrorMsg), Http3Error.H3_GENERAL_PROTOCOL_ERROR); + rspHeadersConsumer.reset(); + return; + } + } + + // update readingPaused after having decoded the statusCode and + // switched the responseState. + if (readingPaused) { + readingPaused = false; + readScheduler.runOrSchedule(exchange.executor()); + } + + var response = newResponse(responseHeaders, responseCode); + + if (debug.on()) { + debug.log("received response headers: %s", + responseHeaders); + } + + try { + OptionalLong cl = responseHeaders.firstValueAsLong("content-length"); + if (finalResponse && cl.isPresent()) { + long cll = cl.getAsLong(); + if (cll < 0) { + cancelImpl(new IOException("Invalid content-length value "+cll), Http3Error.H3_MESSAGE_ERROR); + return; + } + if (!(exchange.request().method().equalsIgnoreCase("HEAD") || responseCode == HTTP_NOT_MODIFIED)) { + // HEAD response and 304 response might have a content-length header, + // but it carries no meaning + contentLength = cll; + } + } + } catch (NumberFormatException nfe) { + cancelImpl(nfe, Http3Error.H3_MESSAGE_ERROR); + return; + } + + if (Log.headers() || debug.on()) { + StringBuilder sb = new StringBuilder("H3 RESPONSE HEADERS (stream="); + sb.append(streamId()).append(")\n"); + Log.dumpHeaders(sb, " ", responseHeaders); + if (Log.headers()) { + Log.logHeaders(sb.toString()); + } else if (debug.on()) { + debug.log(sb); + } + } + + // this will clear the response headers + rspHeadersConsumer.reset(); + + completeResponse(response); + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java index c58f0b0c752..b73b92add63 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java @@ -41,6 +41,7 @@ import java.net.ProtocolException; import java.net.ProxySelector; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpTimeoutException; +import java.net.http.UnsupportedProtocolVersionException; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.ClosedChannelException; @@ -93,8 +94,16 @@ import jdk.internal.net.http.common.TimeSource; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.common.OperationTrackers.Trackable; import jdk.internal.net.http.common.OperationTrackers.Tracker; +import jdk.internal.net.http.common.Utils.SafeExecutor; +import jdk.internal.net.http.common.Utils.SafeExecutorService; import jdk.internal.net.http.websocket.BuilderImpl; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.util.Objects.requireNonNullElse; +import static java.util.Objects.requireNonNullElseGet; +import static jdk.internal.net.quic.QuicTLSContext.isQuicCompatible; + /** * Client implementation. Contains all configuration information and also * the selector manager thread which allows async events to be registered @@ -112,7 +121,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { static final int DEFAULT_KEEP_ALIVE_TIMEOUT = 30; static final long KEEP_ALIVE_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout", DEFAULT_KEEP_ALIVE_TIMEOUT); // Defaults to value used for HTTP/1 Keep-Alive Timeout. Can be overridden by jdk.httpclient.keepalive.timeout.h2 property. - static final long IDLE_CONNECTION_TIMEOUT = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); + static final long IDLE_CONNECTION_TIMEOUT_H2 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h2", KEEP_ALIVE_TIMEOUT); + static final long IDLE_CONNECTION_TIMEOUT_H3 = getTimeoutProp("jdk.httpclient.keepalive.timeout.h3", IDLE_CONNECTION_TIMEOUT_H2); // Define the default factory as a static inner class // that embeds all the necessary logic to avoid @@ -145,15 +155,23 @@ final class HttpClientImpl extends HttpClient implements Trackable { static final class DelegatingExecutor implements Executor { private final BooleanSupplier isInSelectorThread; private final Executor delegate; + private final SafeExecutor safeDelegate; private final BiConsumer errorHandler; DelegatingExecutor(BooleanSupplier isInSelectorThread, Executor delegate, BiConsumer errorHandler) { this.isInSelectorThread = isInSelectorThread; this.delegate = delegate; + this.safeDelegate = delegate instanceof ExecutorService svc + ? new SafeExecutorService(svc, ASYNC_POOL, errorHandler) + : new SafeExecutor<>(delegate, ASYNC_POOL, errorHandler); this.errorHandler = errorHandler; } + Executor safeDelegate() { + return safeDelegate; + } + Executor delegate() { return delegate; } @@ -325,6 +343,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { private final SelectorManager selmgr; private final FilterFactory filters; private final Http2ClientImpl client2; + private final Http3ClientImpl client3; + private final AltServicesRegistry registry; private final long id; private final String dbgTag; private final InetAddress localAddr; @@ -386,6 +406,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { private final AtomicLong pendingHttpOperationsCount = new AtomicLong(); private final AtomicLong pendingHttpRequestCount = new AtomicLong(); private final AtomicLong pendingHttp2StreamCount = new AtomicLong(); + private final AtomicLong pendingHttp3StreamCount = new AtomicLong(); private final AtomicLong pendingTCPConnectionCount = new AtomicLong(); private final AtomicLong pendingSubscribersCount = new AtomicLong(); private final AtomicBoolean isAlive = new AtomicBoolean(); @@ -429,14 +450,26 @@ final class HttpClientImpl extends HttpClient implements Trackable { id = CLIENT_IDS.incrementAndGet(); dbgTag = "HttpClientImpl(" + id +")"; localAddr = builder.localAddr; - if (builder.sslContext == null) { + version = requireNonNullElse(builder.version, Version.HTTP_2); + sslContext = requireNonNullElseGet(builder.sslContext, () -> { try { - sslContext = SSLContext.getDefault(); + return SSLContext.getDefault(); } catch (NoSuchAlgorithmException ex) { throw new UncheckedIOException(new IOException(ex)); } - } else { - sslContext = builder.sslContext; + }); + final boolean sslCtxSupportedForH3 = isQuicCompatible(sslContext); + if (version == Version.HTTP_3 && !sslCtxSupportedForH3) { + throw new UncheckedIOException(new UnsupportedProtocolVersionException( + "HTTP3 is not supported")); + } + sslParams = requireNonNullElseGet(builder.sslParams, sslContext::getDefaultSSLParameters); + boolean sslParamsSupportedForH3 = sslParams.getProtocols() == null + || sslParams.getProtocols().length == 0 + || isQuicCompatible(sslParams); + if (version == Version.HTTP_3 && !sslParamsSupportedForH3) { + throw new UncheckedIOException(new UnsupportedProtocolVersionException( + "HTTP3 is not supported - TLSv1.3 isn't configured on SSLParameters")); } Executor ex = builder.executor; if (ex == null) { @@ -450,7 +483,6 @@ final class HttpClientImpl extends HttpClient implements Trackable { this::onSubmitFailure); facadeRef = new WeakReference<>(facadeFactory.createFacade(this)); implRef = new WeakReference<>(this); - client2 = new Http2ClientImpl(this); cookieHandler = builder.cookieHandler; connectTimeout = builder.connectTimeout; followRedirects = builder.followRedirects == null ? @@ -462,17 +494,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { debug.log("proxySelector is %s (user-supplied=%s)", this.proxySelector, userProxySelector != null); authenticator = builder.authenticator; - if (builder.version == null) { - version = HttpClient.Version.HTTP_2; - } else { - version = builder.version; - } - if (builder.sslParams == null) { - sslParams = getDefaultParams(sslContext); - } else { - sslParams = builder.sslParams; - } + boolean h3Supported = sslCtxSupportedForH3 && sslParamsSupportedForH3; + registry = new AltServicesRegistry(id); connections = new ConnectionPool(id); + client2 = new Http2ClientImpl(this); + client3 = h3Supported ? new Http3ClientImpl(this) : null; connections.start(); timeouts = new TreeSet<>(); try { @@ -518,6 +544,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { client2.stop(); // make sure all subscribers are completed closeSubscribers(); + // close client3 + if (client3 != null) { + // close client3 + client3.stop(); + } // close TCP connection if any are still opened openedConnections.forEach(this::closeConnection); // shutdown the executor if needed @@ -610,11 +641,6 @@ final class HttpClientImpl extends HttpClient implements Trackable { return isStarted.get() && !isAlive.get(); } - private static SSLParameters getDefaultParams(SSLContext ctx) { - SSLParameters params = ctx.getDefaultSSLParameters(); - return params; - } - // Returns the facade that was returned to the application code. // May be null if that facade is no longer referenced. final HttpClientFacade facade() { @@ -664,12 +690,14 @@ final class HttpClientImpl extends HttpClient implements Trackable { final long count = pendingOperationCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.decrementAndGet(); final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.get(); final long webSocketCount = pendingWebSocketCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { selmgr.wakeupSelector(); } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -681,10 +709,35 @@ final class HttpClientImpl extends HttpClient implements Trackable { return pendingOperationCount.incrementAndGet(); } + // Increments the pendingHttp3StreamCount and pendingOperationCount. + final long h3StreamReference() { + pendingHttp3StreamCount.incrementAndGet(); + return pendingOperationCount.incrementAndGet(); + } + // Decrements the pendingHttp2StreamCount and pendingOperationCount. final long streamUnreference() { final long count = pendingOperationCount.decrementAndGet(); final long http2Count = pendingHttp2StreamCount.decrementAndGet(); + final long http3Count = pendingHttp3StreamCount.get(); + final long httpCount = pendingHttpOperationsCount.get(); + final long webSocketCount = pendingWebSocketCount.get(); + if (count == 0 && facadeRef.refersTo(null)) { + selmgr.wakeupSelector(); + } + assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; + assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; + assert webSocketCount >= 0 : "count of WS operations < 0"; + assert count >= 0 : "count of pending operations < 0"; + return count; + } + + // Decrements the pendingHttp3StreamCount and pendingOperationCount. + final long h3StreamUnreference() { + final long count = pendingOperationCount.decrementAndGet(); + final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.get(); final long webSocketCount = pendingWebSocketCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { @@ -692,6 +745,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -709,11 +763,13 @@ final class HttpClientImpl extends HttpClient implements Trackable { final long webSocketCount = pendingWebSocketCount.decrementAndGet(); final long httpCount = pendingHttpOperationsCount.get(); final long http2Count = pendingHttp2StreamCount.get(); + final long http3Count = pendingHttp3StreamCount.get(); if (count == 0 && (facadeRef.refersTo(null) || shutdownRequested)) { selmgr.wakeupSelector(); } assert httpCount >= 0 : "count of HTTP/1.1 operations < 0"; assert http2Count >= 0 : "count of HTTP/2 operations < 0"; + assert http3Count >= 0 : "count of HTTP/3 operations < 0"; assert webSocketCount >= 0 : "count of WS operations < 0"; assert count >= 0 : "count of pending operations < 0"; return count; @@ -732,6 +788,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { final AtomicLong requestCount; final AtomicLong httpCount; final AtomicLong http2Count; + final AtomicLong http3Count; final AtomicLong websocketCount; final AtomicLong operationsCount; final AtomicLong connnectionsCount; @@ -744,6 +801,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { HttpClientTracker(AtomicLong request, AtomicLong http, AtomicLong http2, + AtomicLong http3, AtomicLong ws, AtomicLong ops, AtomicLong conns, @@ -756,6 +814,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { this.requestCount = request; this.httpCount = http; this.http2Count = http2; + this.http3Count = http3; this.websocketCount = ws; this.operationsCount = ops; this.connnectionsCount = conns; @@ -787,6 +846,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { @Override public long getOutstandingHttp2Streams() { return http2Count.get(); } @Override + public long getOutstandingHttp3Streams() { return http3Count.get(); } + @Override public long getOutstandingWebSocketOperations() { return websocketCount.get(); } @@ -811,6 +872,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { pendingHttpRequestCount, pendingHttpOperationsCount, pendingHttp2StreamCount, + pendingHttp3StreamCount, pendingWebSocketCount, pendingOperationCount, pendingTCPConnectionCount, @@ -866,6 +928,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { return Thread.currentThread() == selmgr; } + AltServicesRegistry registry() { return registry; } + boolean isSelectorClosed() { return selmgr.isClosed(); } @@ -878,6 +942,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { return client2; } + Optional client3() { + return Optional.ofNullable(client3); + } + private void debugCompleted(String tag, long startNanos, HttpRequest req) { if (debugelapsed.on()) { debugelapsed.log(tag + " elapsed " @@ -917,6 +985,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { HttpConnectTimeoutException hcte = new HttpConnectTimeoutException(msg); hcte.initCause(throwable); throw hcte; + } else if (throwable instanceof UnsupportedProtocolVersionException) { + var upve = new UnsupportedProtocolVersionException(msg); + upve.initCause(throwable); + throw upve; } else if (throwable instanceof HttpTimeoutException) { throw new HttpTimeoutException(msg); } else if (throwable instanceof ConnectException) { @@ -972,6 +1044,13 @@ final class HttpClientImpl extends HttpClient implements Trackable { return MinimalFuture.failedFuture(new IOException("closed")); } + final HttpClient.Version vers = userRequest.version().orElse(this.version()); + if (vers == Version.HTTP_3 && client3 == null + && userRequest.getOption(H3_DISCOVERY).orElse(null) == HTTP_3_URI_ONLY) { + // HTTP3 isn't supported by this client + return MinimalFuture.failedFuture(new UnsupportedProtocolVersionException( + "HTTP3 is not supported")); + } // should not happen, unless the selector manager has // exited abnormally if (selmgr.isClosed()) { @@ -1095,8 +1174,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { } IOException selectorClosedException() { - var io = new IOException("selector manager closed"); - var cause = errorRef.get(); + final var cause = errorRef.get(); + final String msg = cause == null + ? "selector manager closed" + : "selector manager closed due to: " + cause; + final var io = new IOException(msg); if (cause != null) { io.initCause(cause); } @@ -1181,6 +1263,10 @@ final class HttpClientImpl extends HttpClient implements Trackable { } // double check after closing abortPendingRequests(owner, t); + var client3 = owner.client3; + if (client3 != null) { + client3.abort(t); + } IOException io = toAbort.isEmpty() ? null : selectorClosedException(); @@ -1456,8 +1542,8 @@ final class HttpClientImpl extends HttpClient implements Trackable { String keyInterestOps = key.isValid() ? "key.interestOps=" + Utils.interestOps(key) : "invalid key"; return String.format("channel registered with selector, %s, sa.interestOps=%s", - keyInterestOps, - Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps)); + keyInterestOps, + Utils.describeOps(((SelectorAttachment)key.attachment()).interestOps)); } catch (Throwable t) { return String.valueOf(t); } @@ -1627,8 +1713,12 @@ final class HttpClientImpl extends HttpClient implements Trackable { return Optional.ofNullable(connectTimeout); } - Optional idleConnectionTimeout() { - return Optional.ofNullable(getIdleConnectionTimeout()); + Optional idleConnectionTimeout(Version version) { + return switch (version) { + case HTTP_2 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H2); + case HTTP_3 -> timeoutDuration(IDLE_CONNECTION_TIMEOUT_H3); + case HTTP_1_1 -> timeoutDuration(KEEP_ALIVE_TIMEOUT); + }; } @Override @@ -1755,7 +1845,7 @@ final class HttpClientImpl extends HttpClient implements Trackable { // error from here - but in this case there's not much we // could do anyway. Just let it flow... if (failed == null) failed = e; - else failed.addSuppressed(e); + else Utils.addSuppressed(failed, e); Log.logTrace("Failed to handle event {0}: {1}", event, e); } } @@ -1799,10 +1889,11 @@ final class HttpClientImpl extends HttpClient implements Trackable { return sslBufferSupplier; } - private Duration getIdleConnectionTimeout() { - if (IDLE_CONNECTION_TIMEOUT >= 0) - return Duration.ofSeconds(IDLE_CONNECTION_TIMEOUT); - return null; + private Optional timeoutDuration(long seconds) { + if (seconds >= 0) { + return Optional.of(Duration.ofSeconds(seconds)); + } + return Optional.empty(); } private static long getTimeoutProp(String prop, long def) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java index 07cfc4dbdf6..0219b0960d7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.http.HttpResponse; import java.nio.ByteBuffer; +import java.nio.channels.NetworkChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; import java.util.Comparator; @@ -57,7 +58,10 @@ import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.common.SequentialScheduler.DeferredCompleter; import jdk.internal.net.http.common.Log; import jdk.internal.net.http.common.Utils; + +import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; import static jdk.internal.net.http.common.Utils.ProxyHeaders; /** @@ -69,12 +73,13 @@ import static jdk.internal.net.http.common.Utils.ProxyHeaders; * PlainTunnelingConnection: opens plain text (CONNECT) tunnel to server * AsyncSSLConnection: TLS channel direct to server * AsyncSSLTunnelConnection: TLS channel via (CONNECT) proxy tunnel + * HttpQuicConnection: direct QUIC connection to server */ abstract class HttpConnection implements Closeable { final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG); static final Logger DEBUG_LOGGER = Utils.getDebugLogger( - () -> "HttpConnection(SocketTube(?))", Utils.DEBUG); + () -> "HttpConnection", Utils.DEBUG); public static final Comparator COMPARE_BY_ID = Comparator.comparing(HttpConnection::id); @@ -112,8 +117,8 @@ abstract class HttpConnection implements Closeable { this.label = label; } - private static String nextLabel() { - return "" + LABEL_COUNTER.incrementAndGet(); + private static String nextLabel(String prefix) { + return prefix + LABEL_COUNTER.incrementAndGet(); } /** @@ -198,9 +203,17 @@ abstract class HttpConnection implements Closeable { abstract InetSocketAddress proxy(); /** Tells whether, or not, this connection is open. */ - final boolean isOpen() { + boolean isOpen() { return channel().isOpen() && - (connected() ? !getConnectionFlow().isFinished() : true); + (connected() ? !isFlowFinished() : true); + } + + /** + * {@return {@code true} if the {@linkplain #getConnectionFlow() + * connection flow} is {@linkplain FlowTube#isFinished() finished}. + */ + boolean isFlowFinished() { + return getConnectionFlow().isFinished(); } /** @@ -232,13 +245,17 @@ abstract class HttpConnection implements Closeable { * still open, and the method returns true. * @return true if the channel appears to be still open. */ - final boolean checkOpen() { + boolean checkOpen() { if (isOpen()) { try { // channel is non blocking - int read = channel().read(ByteBuffer.allocate(1)); - if (read == 0) return true; - close(); + if (channel() instanceof SocketChannel channel) { + int read = channel.read(ByteBuffer.allocate(1)); + if (read == 0) return true; + close(); + } else { + return channel().isOpen(); + } } catch (IOException x) { debug.log("Pooled connection is no longer operational: %s", x.toString()); @@ -294,6 +311,7 @@ abstract class HttpConnection implements Closeable { * is one of the following: * {@link PlainHttpConnection} * {@link PlainTunnelingConnection} + * {@link HttpQuicConnection} * * The returned connection, if not from the connection pool, must have its, * connect() or connectAsync() method invoked, which ( when it completes @@ -301,6 +319,7 @@ abstract class HttpConnection implements Closeable { */ public static HttpConnection getConnection(InetSocketAddress addr, HttpClientImpl client, + Exchange exchange, HttpRequestImpl request, Version version) { // The default proxy selector may select a proxy whose address is @@ -322,18 +341,27 @@ abstract class HttpConnection implements Closeable { return getPlainConnection(addr, proxy, request, client); } } else { // secure - if (version != HTTP_2) { // only HTTP/1.1 connections are in the pool + if (version == HTTP_1_1) { // only HTTP/1.1 connections are in the pool c = pool.getConnection(true, addr, proxy); } if (c != null && c.isOpen()) { - final HttpConnection conn = c; - if (DEBUG_LOGGER.on()) - DEBUG_LOGGER.log(conn.getConnectionFlow() - + ": SSL connection retrieved from HTTP/1.1 pool"); + if (DEBUG_LOGGER.on()) { + DEBUG_LOGGER.log(c.getConnectionFlow() + + ": SSL connection retrieved from HTTP/1.1 pool"); + } return c; + } else if (version == HTTP_3 && client.client3().isPresent()) { + // We only come here after we have checked the HTTP/3 connection pool, + // and if the client config supports HTTP/3 + if (DEBUG_LOGGER.on()) + DEBUG_LOGGER.log("Attempting to get an HTTP/3 connection"); + return HttpQuicConnection.getHttpQuicConnection(addr, proxy, request, exchange, client); } else { + assert !request.isHttp3Only(version); // should have failed before String[] alpn = null; if (version == HTTP_2 && hasRequiredHTTP2TLSVersion(client)) { + // We only come here after we have checked the HTTP/2 connection pool. + // We will not negotiate HTTP/2 if we don't have the appropriate TLS version alpn = new String[] { Alpns.H2, Alpns.HTTP_1_1 }; } return getSSLConnection(addr, proxy, alpn, request, client); @@ -346,7 +374,7 @@ abstract class HttpConnection implements Closeable { String[] alpn, HttpRequestImpl request, HttpClientImpl client) { - final String label = nextLabel(); + final String label = nextLabel("tls:"); final Origin originServer; try { originServer = Origin.from(request.uri()); @@ -433,7 +461,7 @@ abstract class HttpConnection implements Closeable { InetSocketAddress proxy, HttpRequestImpl request, HttpClientImpl client) { - final String label = nextLabel(); + final String label = nextLabel("tcp:"); final Origin originServer; try { originServer = Origin.from(request.uri()); @@ -483,7 +511,7 @@ abstract class HttpConnection implements Closeable { /* Tells whether or not this connection is a tunnel through a proxy */ boolean isTunnel() { return false; } - abstract SocketChannel channel(); + abstract NetworkChannel channel(); final InetSocketAddress address() { return address; @@ -516,6 +544,19 @@ abstract class HttpConnection implements Closeable { close(); } + /** + * {@return the underlying connection flow, if applicable} + * + * @apiNote + * TCP based protocols like HTTP/1.1 and HTTP/2 are built on + * top of a {@linkplain FlowTube bidirectional connection flow}. + * On the other hand, Quic based protocol like HTTP/3 are + * multiplexed at the Quic level, and therefore do not have + * a connection flow. + * + * @throws IllegalStateException if the underlying transport + * does not expose a single connection flow. + */ abstract FlowTube getConnectionFlow(); /** diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java new file mode 100644 index 00000000000..bbbe1157cdf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpQuicConnection.java @@ -0,0 +1,690 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketOption; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.nio.channels.NetworkChannel; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.ConnectionPool.CacheKey; +import jdk.internal.net.http.AltServicesRegistry.AltService; +import jdk.internal.net.http.common.FlowTube; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnection; + +import static jdk.internal.net.http.Http3ClientProperties.MAX_DIRECT_CONNECTION_TIMEOUT; +import static jdk.internal.net.http.common.Alpns.H3; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.H3_NO_ERROR; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; +import static jdk.internal.net.http.quic.TerminationCause.appLayerException; + +/** + * An {@code HttpQuicConnection} models an HTTP connection over + * QUIC. + * The particulars of the HTTP/3 protocol are handled by the + * Http3Connection class. + */ +abstract class HttpQuicConnection extends HttpConnection { + + final Logger debug = Utils.getDebugLogger(this::quicDbgString); + + final QuicConnection quicConnection; + final ConnectionTerminator quicConnTerminator; + // the alt-service which was advertised, from some origin, for this connection co-ordinates. + // can be null, which indicates this wasn't created because of an alt-service + private final AltService sourceAltService; + // HTTP/2 MUST use TLS version 1.3 or higher for HTTP/3 over TLS + private static final Predicate testRequiredHTTP3TLSVersion = proto -> + proto.equals("TLSv1.3"); + + + HttpQuicConnection(Origin originServer, InetSocketAddress address, HttpClientImpl client, + QuicConnection quicConnection, AltService sourceAltService) { + super(originServer, address, client, "quic:" + quicConnection.uniqueId()); + Objects.requireNonNull(quicConnection); + this.quicConnection = quicConnection; + this.quicConnTerminator = quicConnection.connectionTerminator(); + this.sourceAltService = sourceAltService; + } + + /** + * A HTTP QUIC connection could be created due to an alt-service that was advertised + * from some origin. This method returns that source alt-service if there was one. + * @return The source alt-service if present + */ + Optional getSourceAltService() { + return Optional.ofNullable(this.sourceAltService); + } + + @Override + public List getSNIServerNames() { + final SSLParameters sslParams = this.quicConnection.getTLSEngine().getSSLParameters(); + if (sslParams == null) { + return List.of(); + } + final List sniServerNames = sslParams.getServerNames(); + if (sniServerNames == null) { + return List.of(); + } + return List.copyOf(sniServerNames); + } + + final String quicDbgString() { + String tag = dbgTag; + if (tag == null) tag = dbgTag = "Http" + quicConnection.dbgTag(); + return tag; + } + + /** + * Initiates the connect phase. + * + * Returns a CompletableFuture that completes when the underlying + * TCP connection has been established or an error occurs. + */ + public abstract CompletableFuture connectAsync(Exchange exchange); + + private volatile boolean connected; + /** + * Finishes the connection phase. + * + * Returns a CompletableFuture that completes when any additional, + * type specific, setup has been done. Must be called after connectAsync. + */ + public CompletableFuture finishConnect() { + this.connected = true; + return MinimalFuture.completedFuture(null); + } + + /** Tells whether, or not, this connection is connected to its destination. */ + boolean connected() { + return connected; + } + + /** Tells whether, or not, this connection is secure ( over SSL ) */ + final boolean isSecure() { return true; } // QUIC is secure + + /** + * Tells whether, or not, this connection is proxied. + * Returns true for tunnel connections, or clear connection to + * any host through proxy. + */ + final boolean isProxied() { return false;} // Proxy not supported + + /** + * Returns the address of the proxy used by this connection. + * Returns the proxy address for tunnel connections, or + * clear connection to any host through proxy. + * Returns {@code null} otherwise. + */ + final InetSocketAddress proxy() { return null; } // Proxy not supported + + /** + * This method throws an {@link UnsupportedOperationException} + */ + @Override + final HttpPublisher publisher() { + throw new UnsupportedOperationException("no publisher for a quic connection"); + } + + QuicConnection quicConnection() { + return quicConnection; + } + + /** + * Returns true if the given client's SSL parameter protocols contains at + * least one TLS version that HTTP/3 requires. + */ + private static boolean hasRequiredHTTP3TLSVersion(HttpClient client) { + String[] protos = client.sslParameters().getProtocols(); + if (protos != null) { + return Arrays.stream(protos).anyMatch(testRequiredHTTP3TLSVersion); + } else { + return false; + } + } + + /** + * Called when the HTTP/3 connection is established, either successfully or + * unsuccessfully + * @param connection the HTTP/3 connection, if successful, or null, otherwise + * @param throwable the exception encountered, if unsuccessful + */ + public abstract void connectionEstablished(Http3Connection connection, + Throwable throwable); + + /** + * A functional interface used to update the Alternate Service Registry + * after a direct connection attempt. + */ + @FunctionalInterface + private interface DirectConnectionUpdater { + /** + * This method may update the HttpClient registry, or + * {@linkplain Http3ClientImpl#noH3(String) record the unsuccessful} + * direct connection attempt. + * + * @param conn the connection or null + * @param throwable the exception or null + */ + void onConnectionEstablished( + Http3Connection conn, Throwable throwable); + + /** + * Does nothing + * @param conn the connection + * @param throwable the exception + */ + static void noUpdate( + Http3Connection conn, Throwable throwable) { + } + } + + /** + * This method create and return a new unconnected HttpQuicConnection, + * wrapping a {@link QuicConnection}. May return {@code null} if + * HTTP/3 is not supported with the given parameters. For instance, + * if TLSv1.3 isn't available/enabled in the client's SSLParameters, + * or if ALT_SERVICE is required but no alt service is found. + * + * @param addr the HTTP/3 peer endpoint address, if direct connection + * @param proxy the proxy address, if a proxy is used, in which case this + * method will return {@code null} as proxying is not supported + * with HTTP/3 + * @param request the request for which the connection is being created + * @param exchange the exchange for which the connection is being created + * @param client the HttpClientImpl instance + * @return A new HttpQuicConnection or {@code null} + */ + public static HttpQuicConnection getHttpQuicConnection(final InetSocketAddress addr, + final InetSocketAddress proxy, + final HttpRequestImpl request, + final Exchange exchange, + final HttpClientImpl client) { + if (!client.client3().isPresent()) { + if (Log.http3()) { + Log.logHttp3("HTTP3 isn't supported by the client"); + } + return null; + } + + final Http3ClientImpl h3client = client.client3().get(); + // HTTP_3 with proxy not supported; In this case we will downgrade + // to using HTTP/2 + var debug = h3client.debug(); + var where = "HttpQuicConnection.getHttpQuicConnection"; + if (proxy != null || !hasRequiredHTTP3TLSVersion(client)) { + if (debug.on()) + debug.log("%s: proxy required or SSL version mismatch", where); + return null; + } + + assert request.secure(); + // Question: Do we need this scaffolding? + // I mean - could Http3Connection and HttpQuicConnection be the same + // object? + // Answer: Http3Connection models an established connection which is + // ready to be used. + // HttpQuicConnection serves at establishing a new Http3Connection + // => Http3Connection is pooled, HttpQuicConnection is not. + // => Do we need HttpQuicConnection vs QuicConnection? + // => yes: HttpQuicConnection can access all package protected + // APIs in HttpConnection & al + // QuicConnection is in the quic subpackage. + // HttpQuicConnection makes the necessary adaptation between + // HttpConnection and QuicConnection. + + // find whether we have an alternate service access point for HTTP/3 + // if we do, create a new QuicConnection and a new Http3Connection over it. + var uri = request.uri(); + var config = request.http3Discovery(); + if (debug.on()) { + debug.log("Checking ALT-SVC regardless of H3_DISCOVERY settings"); + } + // we only support H3 right now + var altSvc = client.registry() + .lookup(uri, H3::equals) + .findFirst().orElse(null); + Optional directTimeout = Optional.empty(); + final boolean advertisedAltSvc = altSvc != null && altSvc.wasAdvertised(); + logAltSvcFor(debug, uri, altSvc, where); + switch (config) { + case ALT_SVC: { + if (!advertisedAltSvc) { + // fallback to HTTP/2 + if (altSvc != null) { + if (Log.altsvc()) { + Log.logAltSvc("{0}: Cannot use unadvertised AltService: {1}", + config, altSvc); + } + } + return null; + } + assert altSvc != null && altSvc.wasAdvertised(); + break; + } + // attempt direct connection if HTTP/3 only + case HTTP_3_URI_ONLY: { + if (advertisedAltSvc && !altSvc.originHasSameAuthority()) { + if (Log.altsvc()) { + Log.logAltSvc("{0}: Cannot use advertised AltService: {1}", + config, altSvc); + } + altSvc = null; + } + assert altSvc == null || altSvc.originHasSameAuthority(); + break; + } + default: { + // if direct connection already attempted and failed, + // fallback to HTTP/2 + if (altSvc == null && h3client.hasNoH3(uri.getRawAuthority())) { + return null; + } + if (!advertisedAltSvc) { + // directTimeout is only used for happy eyeball + Duration def = Duration.ofMillis(MAX_DIRECT_CONNECTION_TIMEOUT); + Duration timeout = client.connectTimeout() + .filter(d -> d.compareTo(def) <= 0) + .orElse(def); + directTimeout = Optional.of(timeout); + } + break; + } + } + + if (altSvc != null) { + assert H3.equals(altSvc.alpn()); + Log.logAltSvc("{0}: Using AltService for {1}: {2}", + config, uri.getRawAuthority(), altSvc); + } + if (debug.on()) { + debug.log("%s: creating QuicConnection for: %s", where, uri); + } + final QuicConnection quicConnection = (altSvc != null) ? + h3client.quicClient().createConnectionFor(altSvc) : + h3client.quicClient().createConnectionFor(addr, new String[] {H3}); + if (debug.on()) debug.log("%s: QuicConnection: %s", where, quicConnection); + final DirectConnectionUpdater onConnectFinished = advertisedAltSvc + ? DirectConnectionUpdater::noUpdate + : (c,t) -> registerUnadvertised(client, uri, addr, c, t); + // Note: we could get rid of the updater by introducing + // H3DirectQuicConnectionImpl extends H3QuicConnectionImpl + HttpQuicConnection httpQuicConn = new H3QuicConnectionImpl(Origin.from(request.uri()), addr, client, + quicConnection, onConnectFinished, directTimeout, altSvc); + // if we created a connection and if that connection is to an (advertised) alt service then + // we setup the Exchange's request to include the "alt-used" header to refer to the + // alt service that was used (section 5, RFC-7838) + if (httpQuicConn != null && altSvc != null && advertisedAltSvc) { + exchange.request().setSystemHeader("alt-used", altSvc.authority()); + } + return httpQuicConn; + } + + private static void logAltSvcFor(Logger debug, URI uri, AltService altSvc, String where) { + if (altSvc == null) { + if (Log.altsvc()) { + Log.logAltSvc("No AltService found for {0}", uri.getRawAuthority()); + } else if (debug.on()) { + debug.log("%s: No ALT-SVC for %s", where, uri.getRawAuthority()); + } + } else { + if (debug.on()) debug.log("%s: ALT-SVC: %s", where, altSvc); + } + } + + static void registerUnadvertised(final HttpClientImpl client, + final URI requestURI, + final InetSocketAddress destAddr, + final Http3Connection connection, + final Throwable t) { + if (t == null && connection != null) { + // There is an h3 endpoint at the given origin: update the registry + final Origin origin = connection.connection().getOriginServer(); + assert origin != null : "origin server is null on connection: " + + connection.connection(); + assert origin.port() == destAddr.getPort(); + var id = new AltService.Identity(H3, origin.host(), origin.port()); + client.registry().registerUnadvertised(id, origin, connection.connection()); + return; + } + if (t != null) { + assert client.client3().isPresent() : "HTTP3 isn't supported by the client"; + final URI originURI = requestURI.resolve("/"); + // record that there is no h3 at the given origin + client.client3().get().noH3(originURI.getRawAuthority()); + } + } + + // TODO: we could probably merge H3QuicConnectionImpl with HttpQuicConnection now + static class H3QuicConnectionImpl extends HttpQuicConnection { + private final Optional directTimeout; + private final DirectConnectionUpdater connFinishedAction; + H3QuicConnectionImpl(Origin originServer, + InetSocketAddress address, + HttpClientImpl client, + QuicConnection quic, + DirectConnectionUpdater connFinishedAction, + Optional directTimeout, + AltService sourceAltService) { + super(originServer, address, client, quic, sourceAltService); + this.directTimeout = directTimeout; + this.connFinishedAction = connFinishedAction; + } + + @Override + public CompletableFuture connectAsync(Exchange exchange) { + var request = exchange.request(); + var uri = request.uri(); + // Adapt HandshakeCF to CompletableFuture + CompletableFuture> handshakeCfCf = + quicConnection.startHandshake() + .handle((r, t) -> { + if (t == null) { + // successful handshake + return MinimalFuture.completedFuture(r); + } + final TerminationCause terminationCause = quicConnection.terminationCause(); + final boolean appLayerTermination = terminationCause != null + && terminationCause.isAppLayer(); + // QUIC connection handshake failed. we now decide whether we should + // unregister the alt-service (if any) that was the source of this + // connection attempt. + // + // handshake could have failed for one of several reasons, some of them: + // - something at QUIC layer caused the failure (either some internal + // exception or protocol error or QUIC TLS error) + // - or the app layer, through the HttpClient/HttpConnection + // could have triggered a connection close. + // + // we unregister the alt-service (if any) only if the termination cause + // originated in the QUIC layer. An app layer termination cause doesn't + // necessarily mean that the alt-service isn't valid for subsequent use. + if (!appLayerTermination && this.getSourceAltService().isPresent()) { + final AltService altSvc = this.getSourceAltService().get(); + if (debug.on()) { + debug.log("connection attempt to an alternate service at " + + altSvc.authority() + " failed during handshake: " + t); + } + client().registry().markInvalid(this.getSourceAltService().get()); + // fail with ConnectException to allow the request to potentially + // be retried on a different connection + final ConnectException connectException = new ConnectException( + "QUIC connection handshake to an alternate service failed"); + connectException.initCause(t); + return MinimalFuture.failedFuture(connectException); + } else { + // alt service wasn't the cause of this failed connection attempt. + // return a failed future with the original cause + return MinimalFuture.failedFuture(t); + } + }) + .thenApply((handshakeCompletion) -> { + if (handshakeCompletion.isCompletedExceptionally()) { + return MinimalFuture.failedFuture(handshakeCompletion.exceptionNow()); + } + return MinimalFuture.completedFuture(null); + }); + + // In case of direct connection, set up a timeout on the handshakeReachedPeerCf, + // and arrange for it to complete the handshakeCfCf above with a timeout in + // case that timeout expires... + if (directTimeout.isPresent()) { + debug.log("setting up quic direct connect timeout: " + directTimeout.get().toMillis()); + var handshakeReachedPeerCf = quicConnection.handshakeReachedPeer(); + CompletableFuture> fxi2 = handshakeReachedPeerCf + .thenApply((unused) -> MinimalFuture.completedFuture(null)); + fxi2 = fxi2.completeOnTimeout( + MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic handshake timeout")), + directTimeout.get().toMillis(), TimeUnit.MILLISECONDS); + fxi2.handleAsync((r, t) -> { + if (t != null) { + var cause = Utils.getCompletionCause(t); + // arrange for handshakeCfCf to timeout + handshakeCfCf.completeExceptionally(cause); + } + if (r.isCompletedExceptionally()) { + var cause = Utils.getCompletionCause(r.exceptionNow()); + // arrange for handshakeCfCf to timeout + handshakeCfCf.completeExceptionally(cause); + } + return r; + }, exchange.parentExecutor.safeDelegate()); + } + + Optional timeout = client().connectTimeout(); + CompletableFuture> fxi = handshakeCfCf; + + // In case of connection timeout, set up a timeout on the handshakeCfCf. + // Note: this is a different timeout than the direct connection timeout. + if (timeout.isPresent()) { + // In case of timeout we need to close the quic connection + debug.log("setting up quic connect timeout: " + timeout.get().toMillis()); + fxi = handshakeCfCf.completeOnTimeout( + MinimalFuture.failedFuture(new HttpConnectTimeoutException("quic connect timeout")), + timeout.get().toMillis(), TimeUnit.MILLISECONDS); + } + + // If we have set up any timeout, arrange to close the quicConnection + // if one of the timeout expires + if (timeout.isPresent() || directTimeout.isPresent()) { + fxi = fxi.handleAsync(this::handleTimeout, exchange.parentExecutor.safeDelegate()); + } + return fxi.thenCompose(Function.identity()); + } + + @Override + public void connectionEstablished(Http3Connection connection, + Throwable throwable) { + connFinishedAction.onConnectionEstablished(connection, throwable); + } + + private CompletableFuture handleTimeout(CompletableFuture r, Throwable t) { + if (t != null) { + if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) { + debug.log("Timeout expired: " + te); + close(H3_NO_ERROR.code(), "timeout expired", te); + return MinimalFuture.failedFuture(te); + } + return MinimalFuture.failedFuture(t); + } else if (r.isCompletedExceptionally()) { + t = r.exceptionNow(); + if (Utils.getCompletionCause(t) instanceof HttpConnectTimeoutException te) { + debug.log("Completed in timeout: " + te); + close(H3_NO_ERROR.code(), "timeout expired", te); + } + } + return r; + } + + + @Override + NetworkChannel /* DatagramChannel */ channel() { + // Note: revisit this + // - don't return a new instance each time + // - see if we could avoid exposing + // the channel in the first place + H3QuicConnectionImpl self = this; + return new NetworkChannel() { + @Override + public NetworkChannel bind(SocketAddress local) throws IOException { + throw new UnsupportedOperationException("no bind for a quic connection"); + } + + @Override + public SocketAddress getLocalAddress() throws IOException { + return quicConnection.localAddress(); + } + + @Override + public NetworkChannel setOption(SocketOption name, T value) throws IOException { + return this; + } + + @Override + public T getOption(SocketOption name) throws IOException { + return null; + } + + @Override + public Set> supportedOptions() { + return Set.of(); + } + + @Override + public boolean isOpen() { + return quicConnection.isOpen(); + } + + @Override + public void close() throws IOException { + self.close(); + } + }; + } + + @Override + CacheKey cacheKey() { + return null; + } + + // close with H3_NO_ERROR + @Override + public final void close() { + close(H3_NO_ERROR.code(), "connection closed", null); + } + + @Override + void close(final Throwable cause) { + close(H3_INTERNAL_ERROR.code(), null, cause); + } + } + + /* Tells whether this connection is a tunnel through a proxy */ + boolean isTunnel() { return false; } + + abstract NetworkChannel /* DatagramChannel */ channel(); + + abstract ConnectionPool.CacheKey cacheKey(); + + /** + * Closes the underlying transport connection with + * the given {@code connCloseCode} code. This will be considered a application + * layer close and will generate a {@code ConnectionCloseFrame} + * of type {@code 0x1d} as the cause of the termination. + * + * @param connCloseCode the connection close code + * @param logMsg the message to be included in the logs as + * the cause of the connection termination. can be null. + * @param closeCause the underlying cause of the connection termination. can be null, + * in which case just the {@code error} will be recorded as the + * cause of the connection termination. + */ + final void close(final long connCloseCode, final String logMsg, + final Throwable closeCause) { + final TerminationCause terminationCause; + if (closeCause == null) { + terminationCause = appLayerClose(connCloseCode); + } else { + terminationCause = appLayerException(connCloseCode, closeCause); + } + // set the log message only if non-null, else let it default to internal + // implementation sensible default + if (logMsg != null) { + terminationCause.loggedAs(logMsg); + } + quicConnTerminator.terminate(terminationCause); + } + + abstract void close(final Throwable t); + + /** + * {@inheritDoc} + * + * @implSpec + * Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not + * built on a single connection flow, since multiplexing is + * provided by the lower layer. Therefore, the higher HTTP + * layer should never call {@code getConnectionFlow()} on an + * {@link HttpQuicConnection}. As a consequence, this method + * always throws {@link IllegalStateException} unconditionally. + * + * @return nothing: this method always throw {@link IllegalStateException} + * + * @throws IllegalStateException always + */ + @Override + final FlowTube getConnectionFlow() { + throw new IllegalStateException( + "An HTTP/3 connection does not expose " + + "a single connection flow"); + } + + /** + * Unlike HTTP/1.1 and HTTP/2, an HTTP/3 connection is not + * built on a single connection flow, since multiplexing is + * provided by the lower layer. This method instead will + * return {@code true} if the underlying quic connection + * has been terminated, either exceptionally or normally. + * + * @return {@code true} if the underlying Quic connection + * has been terminated. + */ + @Override + boolean isFlowFinished() { + return !quicConnection().isOpen(); + } + + @Override + public String toString() { + return quicDbgString(); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java index a495fcce1ee..c68965626b7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestBuilderImpl.java @@ -26,12 +26,18 @@ package jdk.internal.net.http; import java.net.URI; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpOption; import java.time.Duration; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; +import java.util.Set; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.common.Utils; @@ -51,6 +57,10 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { private BodyPublisher bodyPublisher; private volatile Optional version; private Duration duration; + private final Map, Object> options = new HashMap<>(); + + private static final Set> supportedOptions = + Set.of(HttpOption.H3_DISCOVERY); public HttpRequestBuilderImpl(URI uri) { requireNonNull(uri, "uri must be non-null"); @@ -100,6 +110,7 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { b.uri = uri; b.duration = duration; b.version = version; + b.options.putAll(Map.copyOf(options)); return b; } @@ -158,6 +169,19 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { return this; } + @Override + public Builder setOption(HttpOption option, T value) { + Objects.requireNonNull(option, "option"); + if (value == null) options.remove(option); + else if (supportedOptions.contains(option)) { + if (!option.type().isInstance(value)) { + throw newIAE("Illegal value type %s for %s", value, option); + } + options.put(option, value); + } // otherwise just ignore the option + return this; + } + HttpHeadersBuilder headersBuilder() { return headersBuilder; } URI uri() { return uri; } @@ -170,6 +194,8 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { Optional version() { return version; } + Map, Object> options() { return options; } + @Override public HttpRequest.Builder GET() { return method0("GET", null); @@ -245,4 +271,30 @@ public class HttpRequestBuilderImpl implements HttpRequest.Builder { Duration timeout() { return duration; } + public static Map, Object> copySupportedOptions(HttpRequest request) { + Objects.requireNonNull(request, "request"); + if (request instanceof ImmutableHttpRequest ihr) { + // already checked and immutable + return ihr.options(); + } + Map, Object> options = new HashMap<>(); + for (HttpOption option : supportedOptions) { + var val = request.getOption(option); + if (!val.isPresent()) continue; + options.put(option, option.type().cast(val.get())); + } + return Map.copyOf(options); + } + + public static Map, Object> copySupportedOptions(Map, Object> options) { + Objects.requireNonNull(options, "option"); + Map, Object> result = new HashMap<>(); + for (HttpOption option : supportedOptions) { + var val = options.get(option); + if (val == null) continue; + result.put(option, option.type().cast(val)); + } + return Map.copyOf(result); + } + } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java index 81c693ea192..00730baa0ce 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java @@ -31,9 +31,13 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpOption; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.time.Duration; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient; @@ -46,6 +50,7 @@ import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.common.Utils; import jdk.internal.net.http.websocket.WebSocketRequest; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.Authenticator.RequestorType.SERVER; import static jdk.internal.net.http.common.Utils.ALLOWED_HEADERS; import static jdk.internal.net.http.common.Utils.ProxyHeaders; @@ -65,6 +70,8 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { private volatile boolean isWebSocket; private final Duration timeout; // may be null private final Optional version; + // An alternative would be to have one field per supported option + private final Map, Object> options; private volatile boolean userSetAuthorization; private volatile boolean userSetProxyAuthorization; @@ -92,6 +99,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.requestPublisher = builder.bodyPublisher(); // may be null this.timeout = builder.timeout(); this.version = builder.version(); + this.options = Map.copyOf(builder.options()); this.authority = null; } @@ -110,12 +118,13 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { "uri must be non null"); Duration timeout = request.timeout().orElse(null); this.method = method == null ? "GET" : method; + this.options = HttpRequestBuilderImpl.copySupportedOptions(request); this.userHeaders = HttpHeaders.of(request.headers().map(), Utils.VALIDATE_USER_HEADER); - if (request instanceof HttpRequestImpl) { + if (request instanceof HttpRequestImpl impl) { // all cases exception WebSocket should have a new system headers - this.isWebSocket = ((HttpRequestImpl) request).isWebSocket; + this.isWebSocket = impl.isWebSocket; if (isWebSocket) { - this.systemHeadersBuilder = ((HttpRequestImpl)request).systemHeadersBuilder; + this.systemHeadersBuilder = impl.systemHeadersBuilder; } else { this.systemHeadersBuilder = new HttpHeadersBuilder(); } @@ -199,6 +208,19 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.timeout = other.timeout; this.version = other.version(); this.authority = null; + this.options = other.optionsFor(this.uri); + } + + private Map, Object> optionsFor(URI uri) { + if (this.uri == uri || Objects.equals(this.uri.getRawAuthority(), uri.getRawAuthority())) { + return options; + } + // preserve config if version is HTTP/3 + if (version.orElse(null) == Version.HTTP_3) { + Http3DiscoveryMode h3DiscoveryMode = (Http3DiscoveryMode)options.get(H3_DISCOVERY); + if (h3DiscoveryMode != null) return Map.of(H3_DISCOVERY, h3DiscoveryMode); + } + return Map.of(); } private BodyPublisher publisher(HttpRequestImpl other) { @@ -234,12 +256,26 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { // What we want to possibly upgrade is the tunneled connection to the // target server (so not the CONNECT request itself) this.version = Optional.of(HttpClient.Version.HTTP_1_1); + this.options = Map.of(); } final boolean isConnect() { return "CONNECT".equalsIgnoreCase(method); } + final boolean isHttp3Only(Version version) { + return version == Version.HTTP_3 && http3Discovery() == HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + } + + final Http3DiscoveryMode http3Discovery() { + // see if discovery mode is set on the request + final var h3Discovery = getOption(H3_DISCOVERY); + // if no explicit discovery mode is set, then default to "ANY" + // irrespective of whether the HTTP/3 version may have been + // set on the HttpClient or the HttpRequest + return h3Discovery.orElse(Http3DiscoveryMode.ANY); + } + /** * Creates a HttpRequestImpl from the given set of Headers and the associated * "parent" request. Fields not taken from the headers are taken from the @@ -276,6 +312,7 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { this.timeout = parent.timeout; this.version = parent.version; this.authority = null; + this.options = parent.options; } @Override @@ -399,6 +436,11 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest { @Override public Optional version() { return version; } + @Override + public Optional getOption(HttpOption option) { + return Optional.ofNullable(option.type().cast(options.get(option))); + } + @Override public void setSystemHeader(String name, String value) { systemHeadersBuilder.setHeader(name, value); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java index 1552cd40ede..ba5bea43078 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpResponseImpl.java @@ -41,14 +41,14 @@ import jdk.internal.net.http.websocket.RawChannel; /** * The implementation class for HttpResponse */ -class HttpResponseImpl implements HttpResponse, RawChannel.Provider { +final class HttpResponseImpl implements HttpResponse, RawChannel.Provider { final int responseCode; private final String connectionLabel; final HttpRequest initialRequest; - final Optional> previousResponse; + final HttpResponse previousResponse; // may be null; final HttpHeaders headers; - final Optional sslSession; + final SSLSession sslSession; // may be null final URI uri; final HttpClient.Version version; final RawChannelProvider rawChannelProvider; @@ -62,10 +62,10 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { this.responseCode = response.statusCode(); this.connectionLabel = connectionLabel(exch).orElse(null); this.initialRequest = initialRequest; - this.previousResponse = Optional.ofNullable(previousResponse); + this.previousResponse = previousResponse; this.headers = response.headers(); //this.trailers = trailers; - this.sslSession = Optional.ofNullable(response.getSSLSession()); + this.sslSession = response.getSSLSession(); this.uri = response.request().uri(); this.version = response.version(); this.rawChannelProvider = RawChannelProvider.create(response, exch); @@ -96,7 +96,7 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { @Override public Optional> previousResponse() { - return previousResponse; + return Optional.ofNullable(previousResponse); } @Override @@ -111,7 +111,7 @@ class HttpResponseImpl implements HttpResponse, RawChannel.Provider { @Override public Optional sslSession() { - return sslSession; + return Optional.ofNullable(sslSession); } @Override diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java index 1fb96944afc..b2c15ac3bef 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ImmutableHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,9 @@ package jdk.internal.net.http; import java.net.URI; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption; import java.time.Duration; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.net.http.HttpClient.Version; @@ -43,6 +45,8 @@ final class ImmutableHttpRequest extends HttpRequest { private final boolean expectContinue; private final Optional timeout; private final Optional version; + // An alternative would be to have one field per supported option + private final Map, Object> options; /** Creates an ImmutableHttpRequest from the given builder. */ ImmutableHttpRequest(HttpRequestBuilderImpl builder) { @@ -53,6 +57,7 @@ final class ImmutableHttpRequest extends HttpRequest { this.expectContinue = builder.expectContinue(); this.timeout = Optional.ofNullable(builder.timeout()); this.version = Objects.requireNonNull(builder.version()); + this.options = Map.copyOf(builder.options()); } @Override @@ -78,8 +83,17 @@ final class ImmutableHttpRequest extends HttpRequest { @Override public Optional version() { return version; } + @Override + public Optional getOption(HttpOption option) { + return Optional.ofNullable(option.type().cast(options.get(option))); + } + @Override public String toString() { return uri.toString() + " " + method; } + + public Map, Object> options() { + return options; + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java index 20120aad7d5..ec621f7f955 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/MultiExchange.java @@ -29,7 +29,9 @@ import java.io.IOError; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.ConnectException; +import java.net.http.HttpClient.Version; import java.net.http.HttpConnectTimeoutException; +import java.net.http.StreamLimitException; import java.time.Duration; import java.util.List; import java.util.ListIterator; @@ -38,8 +40,6 @@ import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; @@ -62,6 +62,8 @@ import jdk.internal.net.http.common.ConnectionExpiredException; import jdk.internal.net.http.common.Utils; import static jdk.internal.net.http.common.MinimalFuture.completedFuture; import static jdk.internal.net.http.common.MinimalFuture.failedFuture; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcHeader; + /** * Encapsulates multiple Exchanges belonging to one HttpRequestImpl. @@ -76,6 +78,16 @@ class MultiExchange implements Cancelable { static final Logger debug = Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG); + private record RetryContext(Throwable requestFailureCause, + boolean shouldRetry, + AtomicInteger reqAttemptCounter, + boolean shouldResetConnectTimer) { + private static RetryContext doNotRetry(Throwable requestFailureCause) { + return new RetryContext(requestFailureCause, false, null, false); + } + } + + private static final AtomicLong IDS = new AtomicLong(); private final HttpRequest userRequest; // the user request private final HttpRequestImpl request; // a copy of the user request private final ConnectTimeoutTracker connectTimeout; // null if no timeout @@ -83,12 +95,11 @@ class MultiExchange implements Cancelable { final HttpResponse.BodyHandler responseHandler; final HttpClientImpl.DelegatingExecutor executor; final AtomicInteger attempts = new AtomicInteger(); + final long id = IDS.incrementAndGet(); HttpRequestImpl currentreq; // used for retries & redirect HttpRequestImpl previousreq; // used for retries & redirect Exchange exchange; // the current exchange Exchange previous; - volatile Throwable retryCause; - volatile boolean retriedOnce; volatile HttpResponse response; // Maximum number of times a request will be retried/redirected @@ -98,6 +109,12 @@ class MultiExchange implements Cancelable { "jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS ); + // Maximum number of times a request should be retried when + // max streams limit is reached + static final int max_stream_limit_attempts = Utils.getIntegerNetProperty( + "jdk.httpclient.retryOnStreamlimit", max_attempts + ); + private final List filters; volatile ResponseTimerEvent responseTimerEvent; volatile boolean cancelled; @@ -113,20 +130,22 @@ class MultiExchange implements Cancelable { volatile AuthenticationFilter.AuthInfo serverauth, proxyauth; // RedirectHandler volatile int numberOfRedirects = 0; + // StreamLimit + private final AtomicInteger streamLimitRetries = new AtomicInteger(); // This class is used to keep track of the connection timeout // across retries, when a ConnectException causes a retry. // In that case - we will retry the connect, but we don't // want to double the timeout by starting a new timer with // the full connectTimeout again. - // Instead we use the ConnectTimeoutTracker to return a new + // Instead, we use the ConnectTimeoutTracker to return a new // duration that takes into account the time spent in the // first connect attempt. // If however, the connection gets connected, but we later // retry the whole operation, then we reset the timer before // retrying (since the connection used for the second request // will not necessarily be the same: it could be a new - // unconnected connection) - see getExceptionalCF(). + // unconnected connection) - see checkRetryEligible(). private static final class ConnectTimeoutTracker { final Duration max; final AtomicLong startTime = new AtomicLong(); @@ -199,8 +218,22 @@ class MultiExchange implements Cancelable { HttpClient.Version version() { HttpClient.Version vers = request.version().orElse(client.version()); - if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null) + if (vers != Version.HTTP_1_1 + && !request.secure() && request.proxy() != null + && !request.isHttp3Only(vers)) { + // downgrade to HTTP_1_1 unless HTTP_3_URI_ONLY. + // if HTTP_3_URI_ONLY and not secure it will fail down the road, so + // we don't downgrade here. vers = HttpClient.Version.HTTP_1_1; + } + if (vers == Version.HTTP_3 && request.secure() && !client.client3().isPresent()) { + if (!request.isHttp3Only(vers)) { + // HTTP/3 not supported with the client config. + // Downgrade to HTTP/2, unless HTTP_3_URI_ONLY is specified + vers = Version.HTTP_2; + if (debug.on()) debug.log("HTTP_3 downgraded to " + vers); + } + } return vers; } @@ -229,28 +262,28 @@ class MultiExchange implements Cancelable { } private void requestFilters(HttpRequestImpl r) throws IOException { - Log.logTrace("Applying request filters"); + if (Log.trace()) Log.logTrace("Applying request filters"); for (HeaderFilter filter : filters) { - Log.logTrace("Applying {0}", filter); + if (Log.trace()) Log.logTrace("Applying {0}", filter); filter.request(r, this); } - Log.logTrace("All filters applied"); + if (Log.trace()) Log.logTrace("All filters applied"); } private HttpRequestImpl responseFilters(Response response) throws IOException { - Log.logTrace("Applying response filters"); + if (Log.trace()) Log.logTrace("Applying response filters"); ListIterator reverseItr = filters.listIterator(filters.size()); while (reverseItr.hasPrevious()) { HeaderFilter filter = reverseItr.previous(); - Log.logTrace("Applying {0}", filter); + if (Log.trace()) Log.logTrace("Applying {0}", filter); HttpRequestImpl newreq = filter.response(response); if (newreq != null) { - Log.logTrace("New request: stopping filters"); + if (Log.trace()) Log.logTrace("New request: stopping filters"); return newreq; } } - Log.logTrace("All filters applied"); + if (Log.trace()) Log.logTrace("All filters applied"); return null; } @@ -293,9 +326,13 @@ class MultiExchange implements Cancelable { return true; } else { if (cancelled) { - debug.log("multi exchange already cancelled: " + interrupted.get()); + if (debug.on()) { + debug.log("multi exchange already cancelled: " + interrupted.get()); + } } else { - debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning); + if (debug.on()) { + debug.log("multi exchange mayInterruptIfRunning=" + mayInterruptIfRunning); + } } } return false; @@ -316,7 +353,7 @@ class MultiExchange implements Cancelable { // and therefore doesn't have to include header information which indicates no // body is present. This is distinct from responses that also do not contain // response bodies (possibly ever) but which are required to have content length - // info in the header (eg 205). Those cases do not have to be handled specially + // info in the header (e.g. 205). Those cases do not have to be handled specially private static boolean bodyNotPermitted(Response r) { return r.statusCode == 204; @@ -344,19 +381,27 @@ class MultiExchange implements Cancelable { if (exception != null) result.completeExceptionally(exception); else { - this.response = - new HttpResponseImpl<>(r.request(), r, this.response, nullBody, exch); - result.complete(this.response); + result.complete(setNewResponse(r.request(), r, nullBody, exch)); } }); // ensure that the connection is closed or returned to the pool. return result.whenComplete(exch::nullBody); } + // creates a new HttpResponseImpl object and assign it to this.response + private HttpResponse setNewResponse(HttpRequest request, Response r, T body, Exchange exch) { + HttpResponse previousResponse = this.response; + return this.response = new HttpResponseImpl<>(request, r, previousResponse, body, exch); + } + private CompletableFuture> responseAsync0(CompletableFuture start) { - return start.thenCompose( v -> responseAsyncImpl()) - .thenCompose((Response r) -> { + return start.thenCompose( _ -> { + // this is the first attempt to have the request processed by the server + attempts.set(1); + return responseAsyncImpl(true); + }).thenCompose((Response r) -> { + processAltSvcHeader(r, client(), currentreq); Exchange exch = getExchange(); if (bodyNotPermitted(r)) { if (bodyIsPresent(r)) { @@ -368,15 +413,11 @@ class MultiExchange implements Cancelable { return handleNoBody(r, exch); } return exch.readBodyAsync(responseHandler) - .thenApply((T body) -> { - this.response = - new HttpResponseImpl<>(r.request(), r, this.response, body, exch); - return this.response; - }); + .thenApply((T body) -> setNewResponse(r.request, r, body, exch)); }).exceptionallyCompose(this::whenCancelled); } - // returns a CancellationExcpetion that wraps the given cause + // returns a CancellationException that wraps the given cause // if cancel(boolean) was called, the given cause otherwise private Throwable wrapIfCancelled(Throwable cause) { CancellationException interrupt = interrupted.get(); @@ -412,79 +453,100 @@ class MultiExchange implements Cancelable { } } - private CompletableFuture responseAsyncImpl() { - CompletableFuture cf; - if (attempts.incrementAndGet() > max_attempts) { - cf = failedFuture(new IOException("Too many retries", retryCause)); - } else { - if (currentreq.timeout().isPresent()) { - responseTimerEvent = ResponseTimerEvent.of(this); - client.registerTimer(responseTimerEvent); - } - try { - // 1. apply request filters - // if currentreq == previousreq the filters have already - // been applied once. Applying them a second time might - // cause some headers values to be added twice: for - // instance, the same cookie might be added again. - if (currentreq != previousreq) { - requestFilters(currentreq); - } - } catch (IOException e) { - return failedFuture(e); - } - Exchange exch = getExchange(); - // 2. get response - cf = exch.responseAsync() - .thenCompose((Response response) -> { - HttpRequestImpl newrequest; - try { - // 3. apply response filters - newrequest = responseFilters(response); - } catch (Throwable t) { - IOException e = t instanceof IOException io ? io : new IOException(t); - exch.exchImpl.cancel(e); - return failedFuture(e); - } - // 4. check filter result and repeat or continue - if (newrequest == null) { - if (attempts.get() > 1) { - Log.logError("Succeeded on attempt: " + attempts); - } - return completedFuture(response); - } else { - cancelTimer(); - this.response = - new HttpResponseImpl<>(currentreq, response, this.response, null, exch); - Exchange oldExch = exch; - if (currentreq.isWebSocket()) { - // need to close the connection and open a new one. - exch.exchImpl.connection().close(); - } - return exch.ignoreBody().handle((r,t) -> { - previousreq = currentreq; - currentreq = newrequest; - retriedOnce = false; - setExchange(new Exchange<>(currentreq, this)); - return responseAsyncImpl(); - }).thenCompose(Function.identity()); - } }) - .handle((response, ex) -> { - // 5. handle errors and cancel any timer set - cancelTimer(); - if (ex == null) { - assert response != null; - return completedFuture(response); - } - // all exceptions thrown are handled here - CompletableFuture errorCF = getExceptionalCF(ex, exch.exchImpl); - if (errorCF == null) { - return responseAsyncImpl(); - } else { - return errorCF; - } }) - .thenCompose(Function.identity()); + // we call this only when a request is being retried + private CompletableFuture retryRequest() { + // maintain state indicating a request being retried + previousreq = currentreq; + // request is being retried, so the filters have already + // been applied once. Applying them a second time might + // cause some headers values to be added twice: for + // instance, the same cookie might be added again. + final boolean applyReqFilters = false; + return responseAsyncImpl(applyReqFilters); + } + + private CompletableFuture responseAsyncImpl(final boolean applyReqFilters) { + if (currentreq.timeout().isPresent()) { + responseTimerEvent = ResponseTimerEvent.of(this); + client.registerTimer(responseTimerEvent); } + try { + // 1. apply request filters + if (applyReqFilters) { + requestFilters(currentreq); + } + } catch (IOException e) { + return failedFuture(e); + } + final Exchange exch = getExchange(); + // 2. get response + final CompletableFuture cf = exch.responseAsync() + .thenCompose((Response response) -> { + HttpRequestImpl newrequest; + try { + // 3. apply response filters + newrequest = responseFilters(response); + } catch (Throwable t) { + IOException e = t instanceof IOException io ? io : new IOException(t); + exch.exchImpl.cancel(e); + return failedFuture(e); + } + // 4. check filter result and repeat or continue + if (newrequest == null) { + if (attempts.get() > 1) { + if (Log.requests()) { + Log.logResponse(() -> String.format( + "%s #%s Succeeded on attempt %s: statusCode=%s", + request, id, attempts, response.statusCode)); + } + } + return completedFuture(response); + } else { + cancelTimer(); + setNewResponse(currentreq, response, null, exch); + if (currentreq.isWebSocket()) { + // need to close the connection and open a new one. + exch.exchImpl.connection().close(); + } + return exch.ignoreBody().handle((r,t) -> { + previousreq = currentreq; + currentreq = newrequest; + // this is the first attempt to have the new request + // processed by the server + attempts.set(1); + setExchange(new Exchange<>(currentreq, this)); + return responseAsyncImpl(true); + }).thenCompose(Function.identity()); + } }) + .handle((response, ex) -> { + // 5. handle errors and cancel any timer set + cancelTimer(); + if (ex == null) { + assert response != null; + return completedFuture(response); + } + // all exceptions thrown are handled here + final RetryContext retryCtx = checkRetryEligible(ex, exch); + assert retryCtx != null : "retry context is null"; + if (retryCtx.shouldRetry()) { + // increment the request attempt counter and retry the request + assert retryCtx.reqAttemptCounter != null : "request attempt counter is null"; + final int numAttempt = retryCtx.reqAttemptCounter.incrementAndGet(); + if (debug.on()) { + debug.log("Retrying request: " + currentreq + " id: " + id + + " attempt: " + numAttempt + " due to: " + + retryCtx.requestFailureCause); + } + // reset the connect timer if necessary + if (retryCtx.shouldResetConnectTimer && this.connectTimeout != null) { + this.connectTimeout.reset(); + } + return retryRequest(); + } else { + assert retryCtx.requestFailureCause != null : "missing request failure cause"; + return MinimalFuture.failedFuture(retryCtx.requestFailureCause); + } }) + .thenCompose(Function.identity()); return cf; } @@ -492,14 +554,14 @@ class MultiExchange implements Cancelable { String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry"); if (s == null) return false; - return s.isEmpty() ? true : Boolean.parseBoolean(s); + return s.isEmpty() || Boolean.parseBoolean(s); } private static boolean disableRetryConnect() { String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect"); if (s == null) return false; - return s.isEmpty() ? true : Boolean.parseBoolean(s); + return s.isEmpty() || Boolean.parseBoolean(s); } /** True if ALL ( even non-idempotent ) requests can be automatic retried. */ @@ -517,7 +579,7 @@ class MultiExchange implements Cancelable { } /** Returns true if the given request can be automatically retried. */ - private static boolean canRetryRequest(HttpRequest request) { + private static boolean isHttpMethodRetriable(HttpRequest request) { if (RETRY_ALWAYS) return true; if (isIdempotentRequest(request)) @@ -534,70 +596,125 @@ class MultiExchange implements Cancelable { return interrupted.get() != null; } - private boolean retryOnFailure(Throwable t) { - if (requestCancelled()) return false; - return t instanceof ConnectionExpiredException - || (RETRY_CONNECT && (t instanceof ConnectException)); - } - - private Throwable retryCause(Throwable t) { - Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t; - return cause == null ? t : cause; + String streamLimitState() { + return id + " attempt:" + streamLimitRetries.get(); } /** - * Takes a Throwable and returns a suitable CompletableFuture that is - * completed exceptionally, or null. + * This method determines if a failed request can be retried. The returned RetryContext + * will contain the {@linkplain RetryContext#shouldRetry() retry decision} and the + * {@linkplain RetryContext#requestFailureCause() underlying + * cause} (computed out of the given {@code requestFailureCause}) of the request failure. + * + * @param requestFailureCause the exception that caused the request to fail + * @param exchg the Exchange + * @return a non-null RetryContext which contains the result of retry eligibility */ - private CompletableFuture getExceptionalCF(Throwable t, ExchangeImpl exchImpl) { - if ((t instanceof CompletionException) || (t instanceof ExecutionException)) { - if (t.getCause() != null) { - t = t.getCause(); + private RetryContext checkRetryEligible(final Throwable requestFailureCause, + final Exchange exchg) { + assert requestFailureCause != null : "request failure cause is missing"; + assert exchg != null : "exchange cannot be null"; + // determine the underlying cause for the request failure + final Throwable t = Utils.getCompletionCause(requestFailureCause); + final Throwable underlyingCause = switch (t) { + case IOException ioe -> { + if (cancelled && !requestCancelled() && !(ioe instanceof HttpTimeoutException)) { + yield toTimeoutException(ioe); + } + yield ioe; } + default -> { + yield t; + } + }; + if (requestCancelled()) { + // request has been cancelled, do not retry + return RetryContext.doNotRetry(underlyingCause); } - final boolean retryAsUnprocessed = exchImpl != null && exchImpl.isUnprocessedByPeer(); - if (cancelled && !requestCancelled() && t instanceof IOException) { - if (!(t instanceof HttpTimeoutException)) { - t = toTimeoutException((IOException)t); + // check if retry limited is reached. if yes then don't retry. + record Limit(int numAttempts, int maxLimit) { + boolean retryLimitReached() { + return Limit.this.numAttempts >= Limit.this.maxLimit; } - } else if (retryAsUnprocessed || retryOnFailure(t)) { - Throwable cause = retryCause(t); - - if (!(t instanceof ConnectException)) { - // we may need to start a new connection, and if so - // we want to start with a fresh connect timeout again. - if (connectTimeout != null) connectTimeout.reset(); - if (!retryAsUnprocessed && !canRetryRequest(currentreq)) { - // a (peer) processed request which cannot be retried, fail with - // the original cause - return failedFuture(cause); - } - } // ConnectException: retry, but don't reset the connectTimeout. - - // allow the retry mechanism to do its work - retryCause = cause; - if (!retriedOnce) { - if (debug.on()) { - debug.log(t.getClass().getSimpleName() - + " (async): retrying " + currentreq + " due to: ", t); - } - retriedOnce = true; - // The connection was abruptly closed. - // We return null to retry the same request a second time. - // The request filters have already been applied to the - // currentreq, so we set previousreq = currentreq to - // prevent them from being applied again. - previousreq = currentreq; - return null; - } else { - if (debug.on()) { - debug.log(t.getClass().getSimpleName() - + " (async): already retried once " + currentreq, t); - } - t = cause; + }; + final Limit limit = switch (underlyingCause) { + case StreamLimitException _ -> { + yield new Limit(streamLimitRetries.get(), max_stream_limit_attempts); } + case ConnectException _ -> { + // for ConnectException (i.e. inability to establish a connection to the server) + // we currently retry the request only once and don't honour the + // "jdk.httpclient.redirects.retrylimit" configuration value. + yield new Limit(attempts.get(), 2); + } + default -> { + yield new Limit(attempts.get(), max_attempts); + } + }; + if (limit.retryLimitReached()) { + if (debug.on()) { + debug.log("request already attempted " + + limit.numAttempts + " times, won't be retried again " + + currentreq + " " + id, underlyingCause); + } + final var x = underlyingCause instanceof ConnectionExpiredException cee + ? cee.getCause() == null ? cee : cee.getCause() + : underlyingCause; + // do not retry anymore + return RetryContext.doNotRetry(x); } - return failedFuture(t); + return switch (underlyingCause) { + case ConnectException _ -> { + // connection attempt itself failed, so the request hasn't reached the server. + // check if retry on connection failure is enabled, if not then we don't retry + // the request. + if (!RETRY_CONNECT) { + // do not retry + yield RetryContext.doNotRetry(underlyingCause); + } + // OK to retry. Since the failure is due to a connection/stream being unavailable + // we mark the retry context to not allow the connect timer to be reset + // when the retry is actually attempted. + yield new RetryContext(underlyingCause, true, attempts, false); + } + case StreamLimitException sle -> { + // make a note that the stream limit was reached for a particular HTTP version + exchg.streamLimitReached(true); + // OK to retry. Since the failure is due to a connection/stream being unavailable + // we mark the retry context to not allow the connect timer to be reset + // when the retry is actually attempted. + yield new RetryContext(underlyingCause, true, streamLimitRetries, false); + } + case ConnectionExpiredException cee -> { + final Throwable cause = cee.getCause() == null ? cee : cee.getCause(); + // check if the request was explicitly marked as unprocessed, in which case + // we retry + if (exchg.isUnprocessedByPeer()) { + // OK to retry and allow for the connect timer to be reset + yield new RetryContext(cause, true, attempts, true); + } + // the request which failed hasn't been marked as unprocessed which implies that + // it could be processed by the server. check if the request's METHOD allows + // for retry. + if (!isHttpMethodRetriable(currentreq)) { + // request METHOD doesn't allow for retry + yield RetryContext.doNotRetry(cause); + } + // OK to retry and allow for the connect timer to be reset + yield new RetryContext(cause, true, attempts, true); + } + default -> { + // some other exception that caused the request to fail. + // we check if the request has been explicitly marked as "unprocessed", + // which implies the server hasn't processed the request and is thus OK to retry. + if (exchg.isUnprocessedByPeer()) { + // OK to retry and allow for resetting the connect timer + yield new RetryContext(underlyingCause, true, attempts, false); + } + // some other cause of failure, do not retry. + yield RetryContext.doNotRetry(underlyingCause); + } + }; } private HttpTimeoutException toTimeoutException(IOException ioe) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java index adbee565297..8aee8ef2230 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java @@ -26,6 +26,7 @@ package jdk.internal.net.http; import java.net.URI; +import java.net.URISyntaxException; import java.util.Locale; import java.util.Objects; @@ -132,6 +133,46 @@ public record Origin(String scheme, String host, int port) { return host + ":" + port; } + /** + * {@return true if the Origin's scheme is considered secure, else returns false} + */ + boolean isSecure() { + // we consider https to be the only secure scheme + return scheme.equals("https"); + } + + /** + * {@return Creates and returns an Origin parsed from the ASCII serialized form as defined + * in section 6.2 of RFC-6454} + * + * @param value The value to be parsed + */ + static Origin fromASCIISerializedForm(final String value) throws IllegalArgumentException { + Objects.requireNonNull(value); + try { + final URI uri = new URI(value); + // the ASCII-serialized form contains scheme://host, optionally followed by :port + if (uri.getScheme() == null || uri.getHost() == null) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin"); + } + // normalize the origin string, check if we get the same result + String normalized = uri.getScheme() + "://" + uri.getHost(); + if (uri.getPort() != -1) { + normalized += ":" + uri.getPort(); + } + if (!value.equals(normalized)) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin"); + } + try { + return Origin.from(uri); + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin", iae); + } + } catch (URISyntaxException use) { + throw new IllegalArgumentException("Invalid ASCII serialized form of origin", use); + } + } + private static boolean isValidScheme(final String scheme) { // only "http" and "https" literals allowed return "http".equals(scheme) || "https".equals(scheme); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java index d0d64312f1f..e705aae72a1 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java @@ -266,24 +266,8 @@ class PlainHttpConnection extends HttpConnection { try { this.chan = SocketChannel.open(); chan.configureBlocking(false); - if (debug.on()) { - int bufsize = getSoReceiveBufferSize(); - debug.log("Initial receive buffer size is: %d", bufsize); - bufsize = getSoSendBufferSize(); - debug.log("Initial send buffer size is: %d", bufsize); - } - if (trySetReceiveBufferSize(client.getReceiveBufferSize())) { - if (debug.on()) { - int bufsize = getSoReceiveBufferSize(); - debug.log("Receive buffer size configured: %d", bufsize); - } - } - if (trySetSendBufferSize(client.getSendBufferSize())) { - if (debug.on()) { - int bufsize = getSoSendBufferSize(); - debug.log("Send buffer size configured: %d", bufsize); - } - } + Utils.configureChannelBuffers(debug::log, chan, + client.getReceiveBufferSize(), client.getSendBufferSize()); chan.setOption(StandardSocketOptions.TCP_NODELAY, true); // wrap the channel in a Tube for async reading and writing tube = new SocketTube(client(), chan, Utils::getBuffer, label); @@ -292,54 +276,6 @@ class PlainHttpConnection extends HttpConnection { } } - private int getSoReceiveBufferSize() { - try { - return chan.getOption(StandardSocketOptions.SO_RCVBUF); - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to get initial receive buffer size on %s", chan); - } - return 0; - } - - private int getSoSendBufferSize() { - try { - return chan.getOption(StandardSocketOptions.SO_SNDBUF); - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to get initial receive buffer size on %s", chan); - } - return 0; - } - - private boolean trySetReceiveBufferSize(int bufsize) { - try { - if (bufsize > 0) { - chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize); - return true; - } - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to set receive buffer size to %d on %s", - bufsize, chan); - } - return false; - } - - private boolean trySetSendBufferSize(int bufsize) { - try { - if (bufsize > 0) { - chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize); - return true; - } - } catch (IOException x) { - if (debug.on()) - debug.log("Failed to set send buffer size to %d on %s", - bufsize, chan); - } - return false; - } - @Override HttpPublisher publisher() { return writePublisher; } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java b/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java index f2c7a61c9b6..6bf03f195a7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PushGroup.java @@ -25,6 +25,7 @@ package jdk.internal.net.http; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.net.http.HttpRequest; @@ -105,9 +106,21 @@ class PushGroup { } Acceptor acceptPushRequest(HttpRequest pushRequest) { + return doAcceptPushRequest(pushRequest, null); + } + + Acceptor acceptPushRequest(HttpRequest pushRequest, PushId pushId) { + return doAcceptPushRequest(pushRequest, Objects.requireNonNull(pushId)); + } + + private Acceptor doAcceptPushRequest(HttpRequest pushRequest, PushId pushId) { AcceptorImpl acceptor = new AcceptorImpl<>(executor); try { - pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept); + if (pushId == null) { + pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, acceptor::accept); + } else { + pushPromiseHandler.applyPushPromise(initiatingRequest, pushRequest, pushId, acceptor::accept); + } } catch (Throwable t) { if (acceptor.accepted()) { CompletableFuture cf = acceptor.cf(); @@ -128,6 +141,10 @@ class PushGroup { } } + void acceptPushPromiseId(PushId pushId) { + pushPromiseHandler.notifyAdditionalPromise(initiatingRequest, pushId); + } + void pushCompleted() { stateLock.lock(); try { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java index 949024453ca..e416773ee61 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,13 +25,14 @@ package jdk.internal.net.http; -import java.net.URI; import java.io.IOException; +import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.InetSocketAddress; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; + import jdk.internal.net.http.common.Utils; /** @@ -71,17 +72,14 @@ class Response { this.statusCode = statusCode; this.isConnectResponse = isConnectResponse; if (connection != null) { - InetSocketAddress a; - try { - a = (InetSocketAddress)connection.channel().getLocalAddress(); - } catch (IOException e) { - a = null; - } - this.localAddress = a; - if (connection instanceof AbstractAsyncSSLConnection) { - AbstractAsyncSSLConnection cc = (AbstractAsyncSSLConnection)connection; + this.localAddress = revealedLocalSocketAddress(connection); + if (connection instanceof AbstractAsyncSSLConnection cc) { SSLEngine engine = cc.getEngine(); sslSession = Utils.immutableSession(engine.getSession()); + } else if (connection instanceof HttpQuicConnection qc) { + // TODO: consider adding Optional getSession() to HttpConnection? + var session = qc.quicConnection().getTLSEngine().getSession(); + sslSession = Utils.immutableSession(session); } else { sslSession = null; } @@ -128,4 +126,12 @@ class Response { sb.append(" Local port: ").append(localAddress.getPort()); return sb.toString(); } + + private static InetSocketAddress revealedLocalSocketAddress(HttpConnection connection) { + try { + return (InetSocketAddress) connection.channel().getLocalAddress(); + } catch (IOException io) { + return null; + } + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java index 04d019e4c81..071c68720ac 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseSubscribers.java @@ -526,7 +526,7 @@ public class ResponseSubscribers { @Override public void onError(Throwable thrwbl) { if (debug.on()) - debug.log("onError called: " + thrwbl); + debug.log("onError called", thrwbl); subscription = null; failed = Objects.requireNonNull(thrwbl); // The client process that reads the input stream might @@ -1086,6 +1086,16 @@ public class ResponseSubscribers { bs.getBody().whenComplete((r, t) -> { if (t != null) { cf.completeExceptionally(t); + // if a user-provided BodySubscriber returns + // a getBody() CF completed exceptionally, it's + // the responsibility of that BodySubscriber to cancel + // its subscription in order to cancel the request, + // if operations are still in progress. + // Calling the errorHandler here would ensure that the + // request gets cancelled, but there me cases where that is + // not what the caller wants. Therefore, it's better to + // not call `errorHandler.accept(t);` here, but leave it + // to the provided BodySubscriber implementation. } else { cf.complete(r); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java index b5dada882b2..4b59a777de2 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java @@ -59,6 +59,8 @@ import jdk.internal.net.http.common.*; import jdk.internal.net.http.frame.*; import jdk.internal.net.http.hpack.DecodingCallback; +import static jdk.internal.net.http.AltSvcProcessor.processAltSvcFrame; + import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; /** @@ -96,8 +98,8 @@ import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES; * placed on the stream's inputQ which is consumed by the stream's * reader thread. * - * PushedStream sub class - * ====================== + * PushedStream subclass + * ===================== * Sending side methods are not used because the request comes from a PUSH_PROMISE * frame sent by the server. When a PUSH_PROMISE is received the PushedStream * is created. PushedStream does not use responseCF list as there can be only @@ -151,7 +153,7 @@ class Stream extends ExchangeImpl { // Indicates the first reason that was invoked when sending a ResetFrame // to the server. A streamState of 0 indicates that no reset was sent. // (see markStream(int code) - private volatile int streamState; // assigned using STREAM_STATE varhandle. + private volatile int streamState; // assigned while holding the sendLock. private volatile boolean deRegistered; // assigned using DEREGISTERED varhandle. // state flags @@ -219,7 +221,7 @@ class Stream extends ExchangeImpl { List buffers = df.getData(); List dsts = Collections.unmodifiableList(buffers); - int size = Utils.remaining(dsts, Integer.MAX_VALUE); + long size = Utils.remaining(dsts, Long.MAX_VALUE); if (size == 0 && finished) { inputQ.remove(); // consumed will not be called @@ -478,7 +480,9 @@ class Stream extends ExchangeImpl { if (code == 0) return streamState; sendLock.lock(); try { - return (int) STREAM_STATE.compareAndExchange(this, 0, code); + var state = streamState; + if (state == 0) streamState = code; + return state; } finally { sendLock.unlock(); } @@ -534,7 +538,7 @@ class Stream extends ExchangeImpl { this.requestPublisher = request.requestPublisher; // may be null this.responseHeadersBuilder = new HttpHeadersBuilder(); this.rspHeadersConsumer = new HeadersConsumer(); - this.requestPseudoHeaders = createPseudoHeaders(request); + this.requestPseudoHeaders = Utils.createPseudoHeaders(request); this.streamWindowUpdater = new StreamWindowUpdateSender(connection); } @@ -587,6 +591,7 @@ class Stream extends ExchangeImpl { case WindowUpdateFrame.TYPE -> incoming_windowUpdate((WindowUpdateFrame) frame); case ResetFrame.TYPE -> incoming_reset((ResetFrame) frame); case PriorityFrame.TYPE -> incoming_priority((PriorityFrame) frame); + case AltSvcFrame.TYPE -> handleAltSvcFrame(streamid, (AltSvcFrame) frame); default -> throw new IOException("Unexpected frame: " + frame); } @@ -745,6 +750,10 @@ class Stream extends ExchangeImpl { } } + void handleAltSvcFrame(int streamid, AltSvcFrame asf) { + processAltSvcFrame(streamid, asf, connection.connection, connection.client()); + } + void handleReset(ResetFrame frame, Flow.Subscriber subscriber) { Log.logTrace("Handling RST_STREAM on stream {0}", streamid); if (!closed) { @@ -763,12 +772,16 @@ class Stream extends ExchangeImpl { // A REFUSED_STREAM error code implies that the stream wasn't processed by the // peer and the client is free to retry the request afresh. if (error == ErrorFrame.REFUSED_STREAM) { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be marked as unprocessed"; // Here we arrange for the request to be retried. Note that we don't call // closeAsUnprocessed() method here because the "closed" state is already set // to true a few lines above and calling close() from within // closeAsUnprocessed() will end up being a no-op. We instead do the additional // bookkeeping here. - markUnprocessedByPeer(); + this.exchange.markUnprocessedByPeer(); errorRef.compareAndSet(null, new IOException("request not processed by peer")); if (debug.on()) { debug.log("request unprocessed by peer (REFUSED_STREAM) " + this.request); @@ -1216,6 +1229,7 @@ class Stream extends ExchangeImpl { assert !endStreamSent : "internal error, send data after END_STREAM flag"; } if ((state = streamState) != 0) { + t = errorRef.get(); if (debug.on()) debug.log("trySend: cancelled: %s", String.valueOf(t)); break; } @@ -1521,7 +1535,7 @@ class Stream extends ExchangeImpl { } else cancelImpl(cause); } - // This method sends a RST_STREAM frame + // This method sends an RST_STREAM frame void cancelImpl(Throwable e) { cancelImpl(e, ResetFrame.CANCEL); } @@ -1856,8 +1870,12 @@ class Stream extends ExchangeImpl { */ void closeAsUnprocessed() { try { + // null exchange implies a PUSH stream and those aren't + // initiated by the client, so we don't expect them to be + // considered unprocessed. + assert this.exchange != null : "PUSH streams aren't expected to be closed as unprocessed"; // We arrange for the request to be retried on a new connection as allowed by the RFC-9113 - markUnprocessedByPeer(); + this.exchange.markUnprocessedByPeer(); this.errorRef.compareAndSet(null, new IOException("request not processed by peer")); if (debug.on()) { debug.log("closing " + this.request + " as unprocessed by peer"); @@ -1905,7 +1923,7 @@ class Stream extends ExchangeImpl { streamid, n, v); } } catch (UncheckedIOException uio) { - // reset stream: From RFC 9113, section 8.1 + // reset stream: From RFC 7540, section-8.1.2.6 // Malformed requests or responses that are detected MUST be // treated as a stream error (Section 5.4.2) of type // PROTOCOL_ERROR. @@ -1953,13 +1971,10 @@ class Stream extends ExchangeImpl { } - private static final VarHandle STREAM_STATE; private static final VarHandle DEREGISTERED; static { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); - STREAM_STATE = lookup - .findVarHandle(Stream.class, "streamState", int.class); DEREGISTERED = lookup .findVarHandle(Stream.class, "deRegistered", boolean.class); } catch (Exception x) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java index 66397d93410..5888e6c2de3 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Alpns.java @@ -34,4 +34,9 @@ public final class Alpns { public static final String HTTP_1_1 = "http/1.1"; public static final String H2 = "h2"; public static final String H2C = "h2c"; + public static final String H3 = "h3"; + + public static boolean isSecureALPNName(final String alpnName) { + return H3.equals(alpnName) || H2.equals(alpnName); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java index 07091886533..164e1e5685d 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/ConnectionExpiredException.java @@ -29,7 +29,9 @@ import java.io.IOException; /** * Signals that an end of file or end of stream has been reached - * unexpectedly before any protocol specific data has been received. + * unexpectedly before any protocol specific data has been received, + * or that a new stream creation was rejected because the underlying + * connection was closed. */ public final class ConnectionExpiredException extends IOException { private static final long serialVersionUID = 0; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java index bc9a992b3bd..3ee334885a3 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Deadline.java @@ -88,6 +88,24 @@ public final class Deadline implements Comparable { return of(deadline.truncatedTo(unit)); } + /** + * Returns a copy of this deadline with the specified amount subtracted. + *

        + * This returns a {@code Deadline}, based on this one, with the specified amount subtracted. + * The amount is typically {@link Duration} but may be any other type implementing + * the {@link TemporalAmount} interface. + *

        + * This instance is immutable and unaffected by this method call. + * + * @param amountToSubtract the amount to subtract, not null + * @return a {@code Deadline} based on this deadline with the subtraction made, not null + * @throws DateTimeException if the subtraction cannot be made + * @throws ArithmeticException if numeric overflow occurs + */ + public Deadline minus(TemporalAmount amountToSubtract) { + return Deadline.of(deadline.minus(amountToSubtract)); + } + /** * Returns a copy of this deadline with the specified amount added. *

        @@ -126,6 +144,21 @@ public final class Deadline implements Comparable { return Deadline.of(deadline.plusSeconds(secondsToAdd)); } + /** + * Returns a copy of this deadline with the specified duration in milliseconds added. + *

        + * This instance is immutable and unaffected by this method call. + * + * @param millisToAdd the milliseconds to add, positive or negative + * @return a {@code Deadline} based on this deadline with the specified milliseconds added, not null + * @throws DateTimeException if the result exceeds the maximum or minimum deadline + * @throws ArithmeticException if numeric overflow occurs + */ + public Deadline plusMillis(long millisToAdd) { + if (millisToAdd == 0) return this; + return Deadline.of(deadline.plusMillis(millisToAdd)); + } + /** * Returns a copy of this deadline with the specified amount added. *

        @@ -183,7 +216,7 @@ public final class Deadline implements Comparable { /** * Checks if this deadline is before the specified deadline. *

        - * The comparison is based on the time-line position of the deadines. + * The comparison is based on the time-line position of the deadlines. * * @param otherDeadline the other deadine to compare to, not null * @return true if this deadline is before the specified deadine @@ -217,7 +250,26 @@ public final class Deadline implements Comparable { return deadline.hashCode(); } + Instant asInstant() { + return deadline; + } + static Deadline of(Instant instant) { return new Deadline(instant); } + + /** + * Obtains a {@code Duration} representing the duration between two deadlines. + *

        + * The result of this method can be a negative period if the end is before the start. + * + * @param startInclusive the start deadline, inclusive, not null + * @param endExclusive the end deadline, exclusive, not null + * @return a {@code Duration}, not null + * @throws DateTimeException if the seconds between the deadline cannot be obtained + * @throws ArithmeticException if the calculation exceeds the capacity of {@code Duration} + */ + public static Duration between(Deadline startInclusive, Deadline endExclusive) { + return Duration.between(startInclusive.deadline, endExclusive.deadline); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java index 6dc79760b0a..1c483ce99f4 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpBodySubscriberWrapper.java @@ -284,6 +284,7 @@ public class HttpBodySubscriberWrapper implements TrustedSubscriber { */ public final void complete(Throwable t) { if (markCompleted()) { + logComplete(t); tryUnregister(); t = withError = Utils.getCompletionCause(t); if (t == null) { @@ -312,6 +313,10 @@ public class HttpBodySubscriberWrapper implements TrustedSubscriber { } } + protected void logComplete(Throwable error) { + + } + /** * {@return true if this subscriber has already completed, either normally * or abnormally} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java index 409a8540b68..7c1d2311ba9 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HttpHeadersBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,6 +41,15 @@ public class HttpHeadersBuilder { headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } + // used in test library (Http3ServerExchange) + public HttpHeadersBuilder(HttpHeaders headers) { + headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry> entry : headers.map().entrySet()) { + List valuesCopy = new ArrayList<>(entry.getValue()); + headersMap.put(entry.getKey(), valuesCopy); + } + } + public HttpHeadersBuilder structuralCopy() { HttpHeadersBuilder builder = new HttpHeadersBuilder(); for (Map.Entry> entry : headersMap.entrySet()) { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java index 48f5a2b06d8..bc89a6e9d8e 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Log.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,14 +28,28 @@ package jdk.internal.net.http.common; import java.net.http.HttpHeaders; import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Stream; + import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.Http2Frame; import jdk.internal.net.http.frame.WindowUpdateFrame; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.PingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLParameters; @@ -43,7 +57,8 @@ import javax.net.ssl.SSLParameters; /** * -Djdk.httpclient.HttpClient.log= * errors,requests,headers, - * frames[:control:data:window:all..],content,ssl,trace,channel + * frames[:control:data:window:all..],content,ssl,trace,channel, + * quic[:control:processed:retransmit:ack:crypto:data:cc:hs:dbb:ping:all] * * Any of errors, requests, headers or content are optional. * @@ -57,15 +72,17 @@ public abstract class Log implements System.Logger { static final String logProp = "jdk.httpclient.HttpClient.log"; - public static final int OFF = 0; - public static final int ERRORS = 0x1; - public static final int REQUESTS = 0x2; - public static final int HEADERS = 0x4; - public static final int CONTENT = 0x8; - public static final int FRAMES = 0x10; - public static final int SSL = 0x20; - public static final int TRACE = 0x40; - public static final int CHANNEL = 0x80; + public static final int OFF = 0x00; + public static final int ERRORS = 0x01; + public static final int REQUESTS = 0x02; + public static final int HEADERS = 0x04; + public static final int CONTENT = 0x08; + public static final int FRAMES = 0x10; + public static final int SSL = 0x20; + public static final int TRACE = 0x40; + public static final int CHANNEL = 0x80; + public static final int QUIC = 0x0100; + public static final int HTTP3 = 0x0200; static int logging; // Frame types: "control", "data", "window", "all" @@ -75,6 +92,27 @@ public abstract class Log implements System.Logger { public static final int ALL = CONTROL| DATA | WINDOW_UPDATES; static int frametypes; + // Quic message types + public static final int QUIC_CONTROL = 1; + public static final int QUIC_PROCESSED = 2; + public static final int QUIC_RETRANSMIT = 4; + public static final int QUIC_DATA = 8; + public static final int QUIC_CRYPTO = 16; + public static final int QUIC_ACK = 32; + public static final int QUIC_PING = 64; + public static final int QUIC_CC = 128; + public static final int QUIC_TIMER = 256; + public static final int QUIC_DIRECT_BUFFER_POOL = 512; + public static final int QUIC_HANDSHAKE = 1024; + public static final int QUIC_ALL = QUIC_CONTROL + | QUIC_PROCESSED | QUIC_RETRANSMIT + | QUIC_DATA | QUIC_CRYPTO + | QUIC_ACK | QUIC_PING | QUIC_CC + | QUIC_TIMER | QUIC_DIRECT_BUFFER_POOL + | QUIC_HANDSHAKE; + static int quictypes; + + static final System.Logger logger; static { @@ -94,6 +132,12 @@ public abstract class Log implements System.Logger { case "headers": logging |= HEADERS; break; + case "quic": + logging |= QUIC; + break; + case "http3": + logging |= HTTP3; + break; case "content": logging |= CONTENT; break; @@ -107,13 +151,14 @@ public abstract class Log implements System.Logger { logging |= TRACE; break; case "all": - logging |= CONTENT|HEADERS|REQUESTS|FRAMES|ERRORS|TRACE|SSL| CHANNEL; + logging |= CONTENT | HEADERS | REQUESTS | FRAMES | ERRORS | TRACE | SSL | CHANNEL | QUIC | HTTP3; frametypes |= ALL; + quictypes |= QUIC_ALL; break; default: // ignore bad values } - if (val.startsWith("frames")) { + if (val.startsWith("frames:") || val.equals("frames")) { logging |= FRAMES; String[] types = val.split(":"); if (types.length == 1) { @@ -139,6 +184,56 @@ public abstract class Log implements System.Logger { } } } + if (val.startsWith("quic:") || val.equals("quic")) { + logging |= QUIC; + String[] types = val.split(":"); + if (types.length == 1) { + quictypes = QUIC_ALL & ~QUIC_TIMER & ~QUIC_DIRECT_BUFFER_POOL; + } else { + for (String type : types) { + switch (type.toLowerCase(Locale.US)) { + case "control": + quictypes |= QUIC_CONTROL; + break; + case "data": + quictypes |= QUIC_DATA; + break; + case "processed": + quictypes |= QUIC_PROCESSED; + break; + case "retransmit": + quictypes |= QUIC_RETRANSMIT; + break; + case "crypto": + quictypes |= QUIC_CRYPTO; + break; + case "cc": + quictypes |= QUIC_CC; + break; + case "hs": + quictypes |= QUIC_HANDSHAKE; + break; + case "ack": + quictypes |= QUIC_ACK; + break; + case "ping": + quictypes |= QUIC_PING; + break; + case "timer": + quictypes |= QUIC_TIMER; + break; + case "dbb": + quictypes |= QUIC_DIRECT_BUFFER_POOL; + break; + case "all": + quictypes = QUIC_ALL; + break; + default: + // ignore bad values + } + } + } + } } } if (logging != OFF) { @@ -175,6 +270,119 @@ public abstract class Log implements System.Logger { return (logging & CHANNEL) != 0; } + public static boolean altsvc() { return headers(); } + + public static boolean quicRetransmit() { + return (logging & QUIC) != 0 && (quictypes & QUIC_RETRANSMIT) != 0; + } + + // not called directly - but impacts isLogging(QuicFrame) + public static boolean quicHandshake() { + return (logging & QUIC) != 0 && (quictypes & QUIC_HANDSHAKE) != 0; + } + + public static boolean quicProcessed() { + return (logging & QUIC) != 0 && (quictypes & QUIC_PROCESSED) != 0; + } + + // not called directly - but impacts isLogging(QuicFrame) + public static boolean quicData() { + return (logging & QUIC) != 0 && (quictypes & QUIC_DATA) != 0; + } + + public static boolean quicCrypto() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CRYPTO) != 0; + } + + public static boolean quicCC() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CC) != 0; + } + + public static boolean quicControl() { + return (logging & QUIC) != 0 && (quictypes & QUIC_CONTROL) != 0; + } + + public static boolean quicTimer() { + return (logging & QUIC) != 0 && (quictypes & QUIC_TIMER) != 0; + } + public static boolean quicDBB() { + return (logging & QUIC) != 0 && (quictypes & QUIC_DIRECT_BUFFER_POOL) != 0; + } + + public static boolean quic() { + return (logging & QUIC) != 0; + } + + public static boolean http3() { + return (logging & HTTP3) != 0; + } + + public static void logHttp3(String s, Object... s1) { + if (http3()) { + logger.log(Level.INFO, "HTTP3: " + s, s1); + } + } + + private static boolean isLogging(QuicFrame frame) { + if (frame instanceof StreamFrame sf) + return (quictypes & QUIC_DATA) != 0 + || (quictypes & QUIC_CONTROL) != 0 && sf.isLast() + || (quictypes & QUIC_CONTROL) != 0 && sf.offset() == 0; + if (frame instanceof AckFrame) + return (quictypes & QUIC_ACK) != 0; + if (frame instanceof CryptoFrame) + return (quictypes & QUIC_CRYPTO) != 0 + || (quictypes & QUIC_HANDSHAKE) != 0; + if (frame instanceof PingFrame) + return (quictypes & QUIC_PING) != 0; + if (frame instanceof PaddingFrame) return false; + if (frame instanceof HandshakeDoneFrame && quicHandshake()) + return true; + return (quictypes & QUIC_CONTROL) != 0; + } + + private static final EnumSet HS_TYPES = EnumSet.complementOf( + EnumSet.of(PacketType.ONERTT)); + + private static boolean quicPacketLoggable(QuicPacket packet) { + return (logging & QUIC) != 0 + && (quictypes == QUIC_ALL + || quicHandshake() && HS_TYPES.contains(packet.packetType()) + || stream(packet.frames()).anyMatch(Log::isLogging)); + } + + public static boolean quicPacketOutLoggable(QuicPacket packet) { + return quicPacketLoggable(packet); + } + + private static Stream stream(Collection list) { + return list == null ? Stream.empty() : list.stream(); + } + + public static boolean quicPacketInLoggable(QuicPacket packet) { + return quicPacketLoggable(packet); + } + + public static void logQuic(String s, Object... s1) { + if (quic()) { + logger.log(Level.INFO, "QUIC: " + s, s1); + } + } + + public static void logQuicPacketOut(String connectionTag, QuicPacket packet) { + if (quicPacketOutLoggable(packet)) { + logger.log(Level.INFO, "QUIC: {0} OUT: {1}", + connectionTag, packet.prettyPrint()); + } + } + + public static void logQuicPacketIn(String connectionTag, QuicPacket packet) { + if (quicPacketInLoggable(packet)) { + logger.log(Level.INFO, "QUIC: {0} IN: {1}", + connectionTag, packet.prettyPrint()); + } + } + public static void logError(String s, Object... s1) { if (errors()) { logger.log(Level.INFO, "ERROR: " + s, s1); @@ -237,6 +445,12 @@ public abstract class Log implements System.Logger { } } + public static void logAltSvc(String s, Object... s1) { + if (altsvc()) { + logger.log(Level.INFO, "ALTSVC: " + s, s1); + } + } + public static boolean loggingFrame(Class clazz) { if (frametypes == ALL) { return true; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java index 3aec13b59ec..ef031eef999 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/OperationTrackers.java @@ -55,6 +55,8 @@ public final class OperationTrackers { long getOutstandingHttpOperations(); // The number of active HTTP/2 streams long getOutstandingHttp2Streams(); + // The number of active HTTP/3 streams + long getOutstandingHttp3Streams(); // The number of active WebSockets long getOutstandingWebSocketOperations(); // number of TCP connections still opened diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java index 489fbe7ffd8..c74c67f7d58 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/TimeSource.java @@ -25,7 +25,6 @@ package jdk.internal.net.http.common; import java.time.Instant; -import java.time.InstantSource; /** * A {@link TimeLine} based on {@link System#nanoTime()} for the @@ -52,7 +51,7 @@ public final class TimeSource implements TimeLine { // The use of Integer.MAX_VALUE is arbitrary. // Any value not too close to Long.MAX_VALUE // would do. - static final int TIME_WINDOW = Integer.MAX_VALUE; + static final long TIME_WINDOW = Integer.MAX_VALUE; final Instant first; final long firstNanos; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index 2916a41e62a..b14d76d8dba 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -37,33 +37,50 @@ import java.io.PrintStream; import java.io.UncheckedIOException; import java.lang.System.Logger.Level; import java.net.ConnectException; +import java.net.Inet6Address; import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardSocketOptions; import java.net.Proxy; import java.net.URI; import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; import java.net.http.HttpTimeoutException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.CancelledKeyException; +import java.nio.channels.NetworkChannel; import java.nio.channels.SelectionKey; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.text.Normalizer; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HexFormat; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CancellationException; +import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; import java.util.function.BiPredicate; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -152,6 +169,10 @@ public final class Utils { return prop.isEmpty() ? true : Boolean.parseBoolean(prop); } + // A threshold to decide whether to slice or copy. + // see sliceOrCopy + public static final int SLICE_THRESHOLD = 32; + /** * Allocated buffer size. Must never be higher than 16K. But can be lower * if smaller allocation units preferred. HTTP/2 mandates that all @@ -169,7 +190,8 @@ public final class Utils { private static Set getDisallowedHeaders() { Set headers = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade")); + headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade", + "alt-used")); String v = getNetProperty("jdk.httpclient.allowRestrictedHeaders"); if (v != null) { @@ -215,6 +237,56 @@ public final class Utils { return true; }; + public static T addSuppressed(T x, Throwable suppressed) { + if (x != suppressed && suppressed != null) { + var sup = x.getSuppressed(); + if (sup != null && sup.length > 0) { + if (Arrays.asList(sup).contains(suppressed)) { + return x; + } + } + sup = suppressed.getSuppressed(); + if (sup != null && sup.length > 0) { + if (Arrays.asList(sup).contains(x)) { + return x; + } + } + x.addSuppressed(suppressed); + } + return x; + } + + /** + * {@return a string comparing the given deadline with now, typically + * something like "due since Nms" or "due in Nms"} + * + * @apiNote + * This method recognize deadlines set to Instant.MIN + * and Instant.MAX as special cases meaning "due" and + * "not scheduled". + * + * @param now now + * @param deadline the deadline + */ + public static String debugDeadline(Deadline now, Deadline deadline) { + boolean isDue = deadline.compareTo(now) <= 0; + try { + if (isDue) { + if (deadline.equals(Deadline.MIN)) { + return "due (Deadline.MIN)"; + } else { + return "due since " + deadline.until(now, ChronoUnit.MILLIS) + "ms"; + } + } else if (deadline.equals(Deadline.MAX)) { + return "not scheduled (Deadline.MAX)"; + } else { + return "due in " + now.until(deadline, ChronoUnit.MILLIS) + "ms"; + } + } catch (ArithmeticException x) { + return isDue ? "due since too long" : "due in the far future"; + } + } + public record ProxyHeaders(HttpHeaders userHeaders, HttpHeaders systemHeaders) {} public static final BiPredicate PROXY_TUNNEL_RESTRICTED() { @@ -346,6 +418,7 @@ public final class Utils { } public static String interestOps(SelectionKey key) { + if (key == null) return "null-key"; try { return describeOps(key.interestOps()); } catch (CancelledKeyException x) { @@ -354,6 +427,7 @@ public final class Utils { } public static String readyOps(SelectionKey key) { + if (key == null) return "null-key"; try { return describeOps(key.readyOps()); } catch (CancelledKeyException x) { @@ -438,6 +512,21 @@ public final class Utils { return cause; } + public static IOException toIOException(Throwable cause) { + if (cause == null) return null; + if (cause instanceof CompletionException ce) { + cause = ce.getCause(); + } else if (cause instanceof ExecutionException ee) { + cause = ee.getCause(); + } + if (cause instanceof IOException io) { + return io; + } else if (cause instanceof UncheckedIOException uio) { + return uio.getCause(); + } + return new IOException(cause.getMessage(), cause); + } + public static IOException getIOException(Throwable t) { if (t instanceof IOException) { return (IOException) t; @@ -575,6 +664,10 @@ public final class Utils { return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); } + public static long getLongProperty(String name, long defaultValue) { + return Long.parseLong(System.getProperty(name, String.valueOf(defaultValue))); + } + public static int getIntegerNetProperty(String property, int min, int max, int defaultValue, boolean log) { int value = Utils.getIntegerNetProperty(property, defaultValue); // use default value if misconfigured @@ -755,6 +848,91 @@ public final class Utils { return remain; } + // + + /** + * Reads as much bytes as possible from the buffer list, and + * write them in the provided {@code data} byte array. + * Returns the number of bytes read and written to the byte array. + * This method advances the position in the byte buffers it reads + * @param bufs A list of byte buffer + * @param data A byte array to write into + * @param offset Where to start writing in the byte array + * @return the amount of bytes read and written to the byte array + */ + public static int read(List bufs, byte[] data, int offset) { + int pos = offset; + for (ByteBuffer buf : bufs) { + if (pos >= data.length) break; + int read = Math.min(buf.remaining(), data.length - pos); + if (read <= 0) continue; + buf.get(data, pos, read); + pos += read; + } + return pos - offset; + } + + /** + * Returns the next buffer that has remaining bytes, or null. + * @param iterator an iterator + * @return the next buffer that has remaining bytes, or null + */ + public static ByteBuffer next(Iterator iterator) { + ByteBuffer next = null; + while (iterator.hasNext() && !(next = iterator.next()).hasRemaining()); + return next == null || !next.hasRemaining() ? null : next; + } + + /** + * Compute the relative consolidated position in bytes at which the two + * input mismatch, or -1 if there is no mismatch. + * @apiNote This method behaves as {@link ByteBuffer#mismatch(ByteBuffer)}. + * @param these a first list of byte buffers + * @param those a second list of byte buffers + * @return the relative consolidated position in bytes at which the two + * input mismatch, or -1L if there is no mismatch. + */ + public static long mismatch(List these, List those) { + if (these.isEmpty()) return those.isEmpty() ? -1 : 0; + if (those.isEmpty()) return 0; + Iterator lefti = these.iterator(), righti = those.iterator(); + ByteBuffer left = next(lefti), right = next(righti); + long parsed = 0; + while (left != null || right != null) { + int m = left == null || right == null ? 0 : left.mismatch(right); + if (m == -1) { + parsed = parsed + left.remaining(); + assert right.remaining() == left.remaining(); + if ((left = next(lefti)) != null) { + if ((right = next(righti)) != null) { + continue; + } + return parsed; + } + return (right = next(righti)) != null ? parsed : -1; + } + if (m == 0) return parsed; + parsed = parsed + m; + if (m < left.remaining()) { + if (m < right.remaining()) { + return parsed; + } + if ((right = next(righti)) != null) { + left = left.slice(m, left.remaining() - m); + continue; + } + return parsed; + } + assert m < right.remaining(); + if ((left = next(lefti)) != null) { + right = right.slice(m, right.remaining() - m); + continue; + } + return parsed; + } + return -1L; + } + public static long synchronizedRemaining(List bufs) { if (bufs == null) return 0L; synchronized (bufs) { @@ -766,12 +944,13 @@ public final class Utils { if (bufs == null) return 0; long remain = 0; for (ByteBuffer buf : bufs) { - remain += buf.remaining(); - if (remain > max) { + int size = buf.remaining(); + if (max - remain < size) { throw new IllegalArgumentException("too many bytes"); } + remain += size; } - return (int) remain; + return remain; } public static int remaining(List bufs, int max) { @@ -783,12 +962,13 @@ public final class Utils { if (refs == null) return 0; long remain = 0; for (ByteBuffer b : refs) { - remain += b.remaining(); - if (remain > max) { + int size = b.remaining(); + if (max - remain < size) { throw new IllegalArgumentException("too many bytes"); } + remain += size; } - return (int) remain; + return remain; } public static int remaining(ByteBuffer[] refs, int max) { @@ -834,6 +1014,50 @@ public final class Utils { return newb; } + /** + * Creates a slice of a buffer, possibly copying the data instead + * of slicing. + * If the buffer capacity is less than the {@linkplain #SLICE_THRESHOLD + * default slice threshold}, or if the capacity minus the length to slice + * is less than the {@linkplain #SLICE_THRESHOLD threshold}, returns a slice. + * Otherwise, copy so as not to retain a reference to a big buffer + * for a small slice. + * @param src the original buffer + * @param start where to start copying/slicing from src + * @param len how many byte to slice/copy + * @return a new ByteBuffer for the given slice + */ + public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len) { + return sliceOrCopy(src, start, len, SLICE_THRESHOLD); + } + + /** + * Creates a slice of a buffer, possibly copying the data instead + * of slicing. + * If the buffer capacity minus the length to slice is less than the threshold, + * returns a slice. + * Otherwise, copy so as not to retain a reference to a buffer + * that contains more bytes than needed. + * @param src the original buffer + * @param start where to start copying/slicing from src + * @param len how many byte to slice/copy + * @param threshold a threshold to decide whether to slice or copy + * @return a new ByteBuffer for the given slice + */ + public static ByteBuffer sliceOrCopy(ByteBuffer src, int start, int len, int threshold) { + assert src.hasArray(); + int cap = src.array().length; + if (cap - len < threshold) { + return src.slice(start, len); + } else { + byte[] b = new byte[len]; + if (len > 0) { + src.get(start, b, 0, len); + } + return ByteBuffer.wrap(b); + } + } + /** * Get the Charset from the Content-encoding header. Defaults to * UTF_8 @@ -849,7 +1073,9 @@ public final class Utils { if (value == null) return StandardCharsets.UTF_8; return Charset.forName(value); } catch (Throwable x) { - Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); + if (Log.trace()) { + Log.logTrace("Can't find charset in \"{0}\" ({1})", type, x); + } return StandardCharsets.UTF_8; } } @@ -1078,6 +1304,40 @@ public final class Utils { } } + /** + * Creates HTTP/2 HTTP/3 pseudo headers for the given request. + * @param request the request + * @return pseudo headers for that request + */ + public static HttpHeaders createPseudoHeaders(HttpRequest request) { + HttpHeadersBuilder hdrs = new HttpHeadersBuilder(); + String method = request.method(); + hdrs.setHeader(":method", method); + URI uri = request.uri(); + hdrs.setHeader(":scheme", uri.getScheme()); + String host = uri.getHost(); + int port = uri.getPort(); + assert host != null; + if (port != -1) { + hdrs.setHeader(":authority", host + ":" + port); + } else { + hdrs.setHeader(":authority", host); + } + String query = uri.getRawQuery(); + String path = uri.getRawPath(); + if (path == null || path.isEmpty()) { + if (method.equalsIgnoreCase("OPTIONS")) { + path = "*"; + } else { + path = "/"; + } + } + if (query != null) { + path += "?" + query; + } + hdrs.setHeader(":path", Utils.encode(path)); + return hdrs.build(); + } // -- toAsciiString-like support to encode path and query URI segments // Encodes all characters >= \u0080 into escaped, normalized UTF-8 octets, @@ -1121,6 +1381,302 @@ public final class Utils { return sb.toString(); } + /** + * {@return the content of the buffer as an hexadecimal string} + * This method doesn't move the buffer position or limit. + * @param buffer a byte buffer + */ + public static String asHexString(ByteBuffer buffer) { + if (!buffer.hasRemaining()) return ""; + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(buffer.position(), bytes); + return HexFormat.of().formatHex(bytes); + } + + /** + * Converts a ByteBuffer containing bytes encoded using + * the given {@linkplain Charset charset} into a + * string. This method does not throw but will replace + * unrecognized sequences with the replacement character. + * The bytes in the buffer are consumed. + * + * @apiNote + * This method is intended for debugging purposes only, + * since buffers are not guaranteed to be split at character + * boundaries. + * + * @param buffer a buffer containing bytes encoded using + * a charset + * @param charset the charset to use to decode the bytes + * into a string + * + * @return a string built from the bytes contained + * in the buffer decoded using the given charset + */ + public static String asString(ByteBuffer buffer, Charset charset) { + var decoded = charset.decode(buffer); + char[] chars = new char[decoded.length()]; + decoded.get(chars); + return new String(chars); + } + + /** + * Converts a ByteBuffer containing UTF-8 bytes into a + * string. This method does not throw but will replace + * unrecognized sequences with the replacement character. + * The bytes in the buffer are consumed. + * + * @apiNote + * This method is intended for debugging purposes only, + * since buffers are not guaranteed to be split at character + * boundaries. + * + * @param buffer a buffer containing UTF-8 bytes + * + * @return a string built from the decoded UTF-8 bytes contained + * in the buffer + */ + public static String asString(ByteBuffer buffer) { + return asString(buffer, StandardCharsets.UTF_8); + } + + public static String millis(Instant now, Instant deadline) { + if (Instant.MAX.equals(deadline)) return "not scheduled"; + try { + long delay = now.until(deadline, ChronoUnit.MILLIS); + return delay + " ms"; + } catch (ArithmeticException a) { + return "too far away"; + } + } + + public static String millis(Deadline now, Deadline deadline) { + return millis(now.asInstant(), deadline.asInstant()); + } + + public static ExecutorService safeExecutor(ExecutorService delegate, + BiConsumer errorHandler) { + Executor overflow = new CompletableFuture().defaultExecutor(); + return new SafeExecutorService(delegate, overflow, errorHandler); + } + + public static sealed class SafeExecutor implements Executor + permits SafeExecutorService { + final E delegate; + final BiConsumer errorHandler; + final Executor overflow; + + public SafeExecutor(E delegate, Executor overflow, BiConsumer errorHandler) { + this.delegate = delegate; + this.overflow = overflow; + this.errorHandler = errorHandler; + } + + @Override + public void execute(Runnable command) { + ensureExecutedAsync(command); + } + + private void ensureExecutedAsync(Runnable command) { + try { + delegate.execute(command); + } catch (RejectedExecutionException t) { + errorHandler.accept(command, t); + overflow.execute(command); + } + } + + } + + public static final class SafeExecutorService extends SafeExecutor + implements ExecutorService { + + public SafeExecutorService(ExecutorService delegate, + Executor overflow, + BiConsumer errorHandler) { + super(delegate, overflow, errorHandler); + } + + @Override + public void shutdown() { + delegate.shutdown(); + } + + @Override + public List shutdownNow() { + return delegate.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return delegate.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return delegate.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return delegate.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return delegate.invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException { + return delegate.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return delegate.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return delegate.invokeAny(tasks); + } + } + + public static T configureChannelBuffers(Consumer logSink, T chan, + int receiveBufSize, int sendBufSize) { + + if (logSink != null) { + int bufsize = getSoReceiveBufferSize(logSink, chan); + logSink.accept("Initial receive buffer size is: %d".formatted(bufsize)); + bufsize = getSoSendBufferSize(logSink, chan); + logSink.accept("Initial send buffer size is: %d".formatted(bufsize)); + } + if (trySetReceiveBufferSize(logSink, chan, receiveBufSize)) { + if (logSink != null) { + int bufsize = getSoReceiveBufferSize(logSink, chan); + logSink.accept("Receive buffer size configured: %d".formatted(bufsize)); + } + } + if (trySetSendBufferSize(logSink, chan, sendBufSize)) { + if (logSink != null) { + int bufsize = getSoSendBufferSize(logSink, chan); + logSink.accept("Send buffer size configured: %d".formatted(bufsize)); + } + } + return chan; + } + + public static boolean trySetReceiveBufferSize(Consumer logSink, NetworkChannel chan, int bufsize) { + try { + if (bufsize > 0) { + chan.setOption(StandardSocketOptions.SO_RCVBUF, bufsize); + return true; + } + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to set receive buffer size to %d on %s" + .formatted(bufsize, chan)); + } + return false; + } + + public static boolean trySetSendBufferSize(Consumer logSink, NetworkChannel chan, int bufsize) { + try { + if (bufsize > 0) { + chan.setOption(StandardSocketOptions.SO_SNDBUF, bufsize); + return true; + } + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to set send buffer size to %d on %s" + .formatted(bufsize, chan)); + } + return false; + } + + public static int getSoReceiveBufferSize(Consumer logSink, NetworkChannel chan) { + try { + return chan.getOption(StandardSocketOptions.SO_RCVBUF); + } catch (IOException x) { + if (logSink != null) + logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan)); + } + return 0; + } + + public static int getSoSendBufferSize(Consumer logSink, NetworkChannel chan) { + try { + return chan.getOption(StandardSocketOptions.SO_SNDBUF); + } catch (IOException x) { + if (logSink!= null) + logSink.accept("Failed to get initial receive buffer size on %s".formatted(chan)); + } + return 0; + } + + + /** + * Try to figure out whether local and remote addresses are compatible. + * Used to diagnose potential communication issues early. + * This is a best effort, and there is no guarantee that all potential + * conflicts will be detected. + * @param local local address + * @param peer peer address + * @return a message describing the conflict, if any, or {@code null} if no + * conflict was detected. + */ + public static String addressConflict(SocketAddress local, SocketAddress peer) { + if (local == null || peer == null) return null; + if (local.equals(peer)) { + return "local endpoint and remote endpoint are bound to the same IP address and port"; + } + if (!(local instanceof InetSocketAddress li) || !(peer instanceof InetSocketAddress pi)) { + return null; + } + var laddr = li.getAddress(); + var paddr = pi.getAddress(); + if (!laddr.isAnyLocalAddress() && !paddr.isAnyLocalAddress()) { + if (laddr.getClass() != paddr.getClass()) { // IPv4 vs IPv6 + if ((laddr instanceof Inet6Address laddr6 && !laddr6.isIPv4CompatibleAddress()) + || (paddr instanceof Inet6Address paddr6 && !paddr6.isIPv4CompatibleAddress())) { + return "local endpoint IP (%s) and remote endpoint IP (%s) don't match" + .formatted(laddr.getClass().getSimpleName(), + paddr.getClass().getSimpleName()); + } + } + } + if (li.getPort() != pi.getPort()) return null; + if (li.getAddress().isAnyLocalAddress() && pi.getAddress().isLoopbackAddress()) { + return "local endpoint (wildcard) and remote endpoint (loopback) ports conflict"; + } + if (pi.getAddress().isAnyLocalAddress() && li.getAddress().isLoopbackAddress()) { + return "local endpoint (loopback) and remote endpoint (wildcard) ports conflict"; + } + return null; + } + /** * {@return the exception the given {@code cf} was completed with, * or a {@link CancellationException} if the given {@code cf} was diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java new file mode 100644 index 00000000000..46d4ebeb772 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/AltSvcFrame.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.frame; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Optional; + +public final class AltSvcFrame extends Http2Frame { + + public static final int TYPE = 0xa; + + private final int length; + private final String origin; + private final String altSvcValue; + + private static final Charset encoding = StandardCharsets.US_ASCII; + + // Strings should be US-ASCII. This is checked by the FrameDecoder. + public AltSvcFrame(int streamid, int flags, Optional originVal, String altValue) { + super(streamid, flags); + this.origin = originVal.orElse(""); + this.altSvcValue = Objects.requireNonNull(altValue); + this.length = 2 + origin.length() + altValue.length(); + assert origin.length() == origin.getBytes(encoding).length; + assert altSvcValue.length() == altSvcValue.getBytes(encoding).length; + } + + @Override + public int type() { + return TYPE; + } + + @Override + int length() { + return length; + } + + public String getOrigin() { + return origin; + } + + public String getAltSvcValue() { + return altSvcValue; + } + + @Override + public String toString() { + return super.toString() + + ", origin=" + this.origin + + ", alt-svc: " + altSvcValue; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java index 7ebfa090830..da05f6392c1 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,11 +26,13 @@ package jdk.internal.net.http.frame; import java.io.IOException; -import java.lang.System.Logger.Level; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + import jdk.internal.net.http.common.Log; import jdk.internal.net.http.common.Logger; import jdk.internal.net.http.common.Utils; @@ -344,6 +346,8 @@ public class FramesDecoder { return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags); case ContinuationFrame.TYPE: return parseContinuationFrame(frameLength, frameStreamid, frameFlags); + case AltSvcFrame.TYPE: + return parseAltSvcFrame(frameLength, frameStreamid, frameFlags); default: // RFC 7540 4.1 // Implementations MUST ignore and discard any frame that has a type that is unknown. @@ -557,4 +561,32 @@ public class FramesDecoder { return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength)); } + private Http2Frame parseAltSvcFrame(int frameLength, int frameStreamid, int frameFlags) { + var len = getShort(); + byte[] origin; + Optional originUri = Optional.empty(); + if (len > 0) { + origin = getBytes(len); + if (!isUSAscii(origin)) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid, + "illegal character in AltSvcFrame"); + } + originUri = Optional.of(new String(origin, StandardCharsets.US_ASCII)); + } + byte[] altbytes = getBytes(frameLength - 2 - len); + if (!isUSAscii(altbytes)) { + return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, frameStreamid, + "illegal character in AltSvcFrame"); + } + String altSvc = new String(altbytes, StandardCharsets.US_ASCII); + return new AltSvcFrame(frameStreamid, 0, originUri, altSvc); + } + + static boolean isUSAscii(byte[] bytes) { + for (int i=0; i < bytes.length; i++) { + if (bytes[i] < 0) return false; + } + return true; + } + } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java index 4fdd4acd661..2ee2083c22c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/FramesEncoder.java @@ -26,6 +26,7 @@ package jdk.internal.net.http.frame; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -70,6 +71,7 @@ public class FramesEncoder { case GoAwayFrame.TYPE -> encodeGoAwayFrame((GoAwayFrame) frame); case WindowUpdateFrame.TYPE -> encodeWindowUpdateFrame((WindowUpdateFrame) frame); case ContinuationFrame.TYPE -> encodeContinuationFrame((ContinuationFrame) frame); + case AltSvcFrame.TYPE -> encodeAltSvcFrame((AltSvcFrame) frame); default -> throw new UnsupportedOperationException("Not supported frame " + frame.type() + " (" + frame.getClass().getName() + ")"); }; @@ -227,6 +229,20 @@ public class FramesEncoder { return join(buf, frame.getHeaderBlock()); } + private List encodeAltSvcFrame(AltSvcFrame frame) { + final int length = frame.length(); + ByteBuffer buf = getBuffer(Http2Frame.FRAME_HEADER_SIZE + length); + putHeader(buf, length, AltSvcFrame.TYPE, NO_FLAGS, frame.streamid); + final String origin = frame.getOrigin(); + assert (origin.length() & 0xffff0000) == 0; + buf.putShort((short)origin.length()); + if (!origin.isEmpty()) + buf.put(frame.getOrigin().getBytes(StandardCharsets.US_ASCII)); + buf.put(frame.getAltSvcValue().getBytes(StandardCharsets.US_ASCII)); + buf.flip(); + return List.of(buf); + } + private List joinWithPadding(ByteBuffer buf, List data, int padLength) { int len = data.size(); if (len == 0) return List.of(buf, getPadding(padLength)); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java index f837645696f..469d06cef0c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/frame/Http2Frame.java @@ -91,8 +91,9 @@ public abstract class Http2Frame { case PingFrame.TYPE -> "PING"; case PushPromiseFrame.TYPE -> "PUSH_PROMISE"; case WindowUpdateFrame.TYPE -> "WINDOW_UPDATE"; + case AltSvcFrame.TYPE -> "ALTSVC"; - default -> "UNKNOWN"; + default -> "UNKNOWN"; }; } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java index 881be12c67c..9cdd604efd6 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java @@ -282,7 +282,7 @@ public final class Decoder { if (endOfHeaderBlock && state != State.READY) { logger.log(NORMAL, () -> format("unexpected end of %s representation", state)); - throw new IOException("Unexpected end of header block"); + throw new ProtocolException("Unexpected end of header block"); } if (endOfHeaderBlock) { size = indexed = 0; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java index a233e0f3a38..979c3ded2bc 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/ISO_8859_1.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -40,9 +40,10 @@ import java.nio.ByteBuffer; // // The encoding is simple and well known: 1 byte <-> 1 char // -final class ISO_8859_1 { +public final class ISO_8859_1 { - private ISO_8859_1() { } + private ISO_8859_1() { + } public static final class Reader { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java index 427c2504de5..c6b4c51761b 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/QuickHuffman.java @@ -619,7 +619,7 @@ public final class QuickHuffman { } } - static final class Reader implements Huffman.Reader { + public static final class Reader implements Huffman.Reader { private final BufferUpdateConsumer UPDATER = (buf, bufLen) -> { @@ -703,7 +703,7 @@ public final class QuickHuffman { } } - static final class Writer implements Huffman.Writer { + public static final class Writer implements Huffman.Writer { private final BufferUpdateConsumer UPDATER = (buf, bufLen) -> { @@ -782,12 +782,26 @@ public final class QuickHuffman { @Override public int lengthOf(CharSequence value, int start, int end) { - int len = 0; - for (int i = start; i < end; i++) { - char c = value.charAt(i); - len += codeLengthOf(c); - } - return bytesForBits(len); + return QuickHuffman.lengthOf(value, start, end); } } + + public static int lengthOf(CharSequence value, int start, int end) { + int len = 0; + for (int i = start; i < end; i++) { + char c = value.charAt(i); + len += codeLengthOf(c); + } + return bytesForBits(len); + } + + public static int lengthOf(CharSequence value) { + return lengthOf(value, 0, value.length()); + } + + /* Used to calculate the number of bytes required for Huffman encoding */ + + public static boolean isHuffmanBetterFor(CharSequence input) { + return lengthOf(input) < input.length(); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java new file mode 100644 index 00000000000..ca734d74ae7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/ConnectionSettings.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3; + +import java.util.Objects; + +import jdk.internal.net.http.http3.frames.SettingsFrame; + +/** + * Represents the settings that are conveyed in a HTTP3 SETTINGS frame for a HTTP3 connection + */ +public record ConnectionSettings( + long maxFieldSectionSize, + long qpackMaxTableCapacity, + long qpackBlockedStreams) { + + // we use -1 (an internal value) to represent unlimited + public static final long UNLIMITED_MAX_FIELD_SECTION_SIZE = -1; + + public static ConnectionSettings createFrom(final SettingsFrame frame) { + Objects.requireNonNull(frame); + // default is unlimited as per RFC-9114 section 7.2.4.1 + final long maxFieldSectionSize = getOrDefault(frame, SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, + UNLIMITED_MAX_FIELD_SECTION_SIZE); + // default is zero as per RFC-9204 section 5 + final long qpackMaxTableCapacity = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 0); + // default is zero as per RFC-9204, section 5 + final long qpackBlockedStreams = getOrDefault(frame, SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS, 0); + return new ConnectionSettings(maxFieldSectionSize, qpackMaxTableCapacity, qpackBlockedStreams); + } + + private static long getOrDefault(final SettingsFrame frame, final int paramId, final long defaultValue) { + final long val = frame.getParameter(paramId); + if (val == -1) { + return defaultValue; + } + return val; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java new file mode 100644 index 00000000000..423fb27f844 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/Http3Error.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3; + +import java.util.HexFormat; +import java.util.Optional; +import java.util.stream.Stream; + +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * This enum models HTTP/3 error codes as specified in + * RFC 9114, Section 8, + * augmented with QPack error codes as specified in + * RFC 9204, Section 6. + */ +public enum Http3Error { + + /** + * No error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * This is used when the connection or stream
        +     * needs to be closed, but there is no error to signal.
        +     * }
        + */ + H3_NO_ERROR (0x0100), // 256 + + /** + * General protocol error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * Peer violated protocol requirements in a way that does
        +     * not match a more specific error code, or endpoint declines
        +     * to use the more specific error code.
        +     * }
        + */ + H3_GENERAL_PROTOCOL_ERROR (0x0101), // 257 + + /** + * Internal error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * An internal error has occurred in the HTTP stack.
        +     * }
        + */ + H3_INTERNAL_ERROR (0x0102), // 258 + + /** + * Stream creation error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * The endpoint detected that its peer created a stream that
        +     * it will not accept.
        +     * }
        + */ + H3_STREAM_CREATION_ERROR (0x0103), // 259 + + /** + * Critical stream closed error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * A stream required by the HTTP/3 connection was closed or reset.
        +     * }
        + */ + H3_CLOSED_CRITICAL_STREAM (0x0104), // 260 + + /** + * Frame unexpected error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * A frame was received that was not permitted in the
        +     * current state or on the current stream.
        +     * }
        + */ + H3_FRAME_UNEXPECTED (0x0105), // 261 + + /** + * Frame error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * A frame that fails to satisfy layout requirements or with
        +     * an invalid size was received.
        +     * }
        + */ + H3_FRAME_ERROR (0x0106), // 262 + + /** + * Excessive load error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * The endpoint detected that its peer is exhibiting a behavior
        +     * that might be generating excessive load.
        +     * }
        + */ + H3_EXCESSIVE_LOAD (0x0107), // 263 + + /** + * Stream ID or Push ID error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * A Stream ID or Push ID was used incorrectly, such as exceeding
        +     * a limit, reducing a limit, or being reused.
        +     * }
        + */ + H3_ID_ERROR (0x0108), // 264 + + /** + * Settings error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * An endpoint detected an error in the payload of a SETTINGS frame.
        +     * }
        + */ + H3_SETTINGS_ERROR (0x0109), // 265 + + /** + * Missing settings error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * No SETTINGS frame was received at the beginning of the control
        +     * stream.
        +     * }
        + */ + H3_MISSING_SETTINGS (0x010a), // 266 + + /** + * Request rejected error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * A server rejected a request without performing any application
        +     * processing.
        +     * }
        + */ + H3_REQUEST_REJECTED (0x010b), // 267 + + /** + * Request cancelled error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * The request or its response (including pushed response) is
        +     * cancelled.
        +     * }
        + */ + H3_REQUEST_CANCELLED (0x010c), // 268 + + /** + * Request incomplete error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * The client's stream terminated without containing a
        +     * fully-formed request.
        +     * }
        + */ + H3_REQUEST_INCOMPLETE (0x010d), //269 + + /** + * Message error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * An HTTP message was malformed and cannot be processed.
        +     * }
        + */ + H3_MESSAGE_ERROR (0x010e), // 270 + + /** + * Connect error. + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * The TCP connection established in response to a CONNECT
        +     * request was reset or abnormally closed.
        +     * }
        + */ + H3_CONNECT_ERROR (0x010f), // 271 + + /** + * Version fallback error + *

        + * From + * RFC 9114, Section 8.1: + *

        {@code
        +     * The requested operation cannot be served over HTTP/3.
        +     * The peer should retry over HTTP/1.1.
        +     * }
        + */ + H3_VERSION_FALLBACK (0x0110), // 272 + + /** + * QPack decompression error + *

        + * From + * RFC 9204, Section 6: + *

        {@code
        +     * The decoder failed to interpret an encoded field section
        +     * and is not able to continue decoding that field section.
        +     * }
        + */ + QPACK_DECOMPRESSION_FAILED (0x0200), // 512 + + /** + * Qpack encoder stream error. + *

        + * From + * RFC 9204, Section 6: + *

        {@code
        +     * The decoder failed to interpret an encoder instruction
        +     * received on the encoder stream.
        +     * }
        + */ + QPACK_ENCODER_STREAM_ERROR (0x0201), // 513 + + /** + * Qpack decoder stream error + *

        + * From + * RFC 9204, Section 6: + *

        {@code
        +     * The encoder failed to interpret a decoder instruction
        +     * received on the decoder stream.
        +     * }
        + */ + QPACK_DECODER_STREAM_ERROR (0x0202); // 514 + + final long errorCode; + Http3Error(long errorCode) { + this.errorCode = errorCode; + } + + public long code() { + return errorCode; + } + + public static Optional fromCode(long code) { + return Stream.of(values()).filter((v) -> v.code() == code) + .findFirst(); + } + + public static String stringForCode(long code) { + return fromCode(code).map(Http3Error::name).orElse(unknown(code)); + } + + private static String unknown(long code) { + return "UnknownError(code=0x" + HexFormat.of().withUpperCase().toHexDigits(code) + ")"; + } + + /** + * {@return true if the given code is {@link Http3Error#H3_NO_ERROR} or equivalent} + * Unknown error codes are treated as equivalent to {@code H3_NO_ERROR} + * @param code an HTTP/3 code error code + */ + public static boolean isNoError(long code) { + return fromCode(code).orElse(H3_NO_ERROR) == Http3Error.H3_NO_ERROR; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java new file mode 100644 index 00000000000..4cb8aa051fd --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/AbstractHttp3Frame.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Random; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.http3.frames.Http3FrameType.asString; + +/** + * Super class for all HTTP/3 frames. + */ +public abstract non-sealed class AbstractHttp3Frame implements Http3Frame { + public static final Random RANDOM = new Random(); + final long type; + public AbstractHttp3Frame(long type) { + this.type = type; + } + + public final String typeAsString() { + return asString(type()); + } + + @Override + public long type() { + return type; + } + + + /** + * Computes the size of this frame. This corresponds to + * the {@linkplain #length()} of the frame's payload, plus the + * size needed to encode this length, plus the size needed to + * encode the frame type. + * + * @return the size of this frame. + */ + public long size() { + var len = length(); + return len + VariableLengthEncoder.getEncodedSize(len) + + VariableLengthEncoder.getEncodedSize(type()); + } + + public int headersSize() { + var len = length(); + return VariableLengthEncoder.getEncodedSize(len) + + VariableLengthEncoder.getEncodedSize(type()); + } + + @Override + public long streamingLength() { + return 0; + } + + protected static long decodeRequiredType(final BuffersReader reader, final long expectedType) { + final long type = VariableLengthEncoder.decode(reader); + if (type < 0) throw new BufferUnderflowException(); + // TODO: throw an exception instead? + assert type == expectedType : "bad frame type: " + type + " expected: " + expectedType; + return type; + } + + protected static MalformedFrame checkPayloadSize(long frameType, + BuffersReader reader, + long start, + long length) { + // check position after reading payload + long read = reader.position() - start; + if (length != read) { + reader.position(start + length); + reader.release(); + return new MalformedFrame(frameType, + Http3Error.H3_FRAME_ERROR.code(), + "payload length mismatch (length=%s, read=%s)" + .formatted(length, start)); + + } + + assert length == reader.position() - start; + return null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(typeAsString()) + .append(": length=") + .append(length()); + return sb.toString(); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java new file mode 100644 index 00000000000..f470efb9b27 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/CancelPushFrame.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents the CANCEL_PUSH HTTP3 frame + */ +public final class CancelPushFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.CANCEL_PUSH_FRAME; + private final long length; + private final long pushId; + + public CancelPushFrame(final long pushId) { + super(Http3FrameType.CANCEL_PUSH.type()); + this.pushId = pushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.pushId); + } + + // only used when constructing the frame during decoding content over a stream + private CancelPushFrame(final long pushId, final long length) { + super(Http3FrameType.CANCEL_PUSH.type()); + this.pushId = pushId; + this.length = length; + } + + @Override + public long length() { + return this.length; + } + + public long getPushId() { + return pushId; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the push id that needs to be cancelled + VariableLengthEncoder.encode(buf, this.pushId); + } + + /** + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * @param reader the reader + * @param debug a logger for debugging purposes + * @return the new frame + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + long position = reader.position(); + decodeRequiredType(reader, TYPE); + long length = VariableLengthEncoder.decode(reader); + if (length > reader.remaining() || length < 0) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed pushId + return new MalformedFrame(TYPE, Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in CANCEL_PUSH frame: " + length); + } + + long pushId = VariableLengthEncoder.decode(reader); + if (pushId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new CancelPushFrame(pushId); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java new file mode 100644 index 00000000000..97b475774b2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/DataFrame.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + + +/** + * This class models an HTTP/3 DATA frame. + * @apiNote + * An instance of {@code DataFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see PartialFrame + */ +public final class DataFrame extends PartialFrame { + + /** + * The DATA frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.DATA_FRAME; + + private final long length; + + /** + * Creates a new HTTP/3 HEADERS frame + */ + public DataFrame(long length) { + super(TYPE, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java new file mode 100644 index 00000000000..a51c71e0a05 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/FramesDecoder.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.LongPredicate; +import java.util.function.Supplier; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader; + +/** + * A FramesDecoder accumulates buffers until a frame can be + * decoded. It also supports decoding {@linkplain PartialFrame + * partial frames} and {@linkplain #readPayloadBytes() reading + * their payload} incrementally. + * @apiNote + * When the frame decoder {@linkplain #poll() returns} a partial + * frame, the same frame will be returned until its payload has been + * {@linkplain PartialFrame#remaining() fully} {@linkplain #readPayloadBytes() + * read}. + * The caller is supposed to call {@link #readPayloadBytes()} until + * {@link #poll()} returns a different frame. At this point there will be no + * {@linkplain PartialFrame#remaining() remaining} payload bytes to read for + * the previous frame. + *
        + * The sequence of calls: {@snippet : + * framesDecoder.submit(buffer); + * while ((frame = framesDecoder.poll()) != null) { + * if (frame instanceof PartialFrame partial) { + * var nextPayloadBytes = framesDecoder.readPayloadBytes(); + * if (nextPayloadBytes == null || nextPayloadBytes.isEmpty()) { + * // no more data is available at this moment + * break; + * } + * // nextPayloadBytes are the next bytes for the payload + * // of the partial frame + * deliverBytes(partial, nextPayloadBytes); + * } else ... + * // got a full frame... + * } + * } + * makes it possible to incrementally deliver payload bytes for + * a frame - since {@code poll()} will always return the same partial + * frame until all its payload has been read. + */ +public class FramesDecoder { + + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final ListBuffersReader framesReader = BuffersReader.list(); + private final ReentrantLock lock = new ReentrantLock(); + + private final Supplier dbgTag; + private final LongPredicate isAllowed; + + // the current partial frame or null + PartialFrame partialFrame; + boolean eof; + + /** + * A new {@code FramesDecoder} that accepts all frames. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(String dbgTag) { + this(dbgTag, FramesDecoder::allAllowed); + } + + /** + * A new {@code FramesDecoder} that accepts only frames + * authorized by the given {@code isAllowed} predicate. + * If a frame is not allowed, a {@link MalformedFrame} is + * returned. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(String dbgTag, LongPredicate isAllowed) { + this(() -> dbgTag, Objects.requireNonNull(isAllowed)); + } + + /** + * A new {@code FramesDecoder} that accepts only frames + * authorized by the given {@code isAllowed} predicate. + * If a frame is not allowed, a {@link MalformedFrame} is + * returned. + * @param dbgTag a debug tag for logging + */ + public FramesDecoder(Supplier dbgTag, LongPredicate isAllowed) { + this.dbgTag = dbgTag; + this.isAllowed = Objects.requireNonNull(isAllowed); + } + + String dbgTag() { return dbgTag.get(); } + + /** + * Submit a new buffer to this frames decoder + * @param buffer a new buffer from the stream + */ + public void submit(ByteBuffer buffer) { + lock.lock(); + try { + if (buffer == QuicStreamReader.EOF) { + eof = true; + } else { + framesReader.add(buffer); + } + } finally { + lock.unlock(); + } + } + + /** + * {@return an {@code Http3Frame}, possibly {@linkplain PartialFrame partial}, + * or {@code null} if not enough bytes have been receive to decode (at least + * partially) a frame} + * If a frame is illegal or not allowed, a {@link MalformedFrame} is + * returned. The caller is supposed to {@linkplain #clear() clear} all data + * and proceed to close the connection in that case. + */ + public Http3Frame poll() { + lock.lock(); + try { + if (partialFrame != null) { + if (partialFrame.remaining() != 0) { + return partialFrame; + } else partialFrame = null; + } + var frame = Http3Frame.decode(framesReader, this::isAllowed, debug); + if (frame instanceof PartialFrame partial) { + partialFrame = partial; + } + return frame; + } finally { + lock.unlock(); + } + } + + /** + * {@return the next payload bytes for the current partial frame, + * or {@code null} if no partial frame} + * If EOF has been reached ({@link QuicStreamReader#EOF EOF} was + * {@linkplain #submit(ByteBuffer) submitted}, and all buffers have + * been read, the returned list will contain {@link QuicStreamReader#EOF + * EOF} + */ + public List readPayloadBytes() { + lock.lock(); + try { + if (partialFrame == null || partialFrame.remaining() == 0) { + partialFrame = null; + return null; + } + if (eof && !framesReader.hasRemaining()) { + return List.of(QuicStreamReader.EOF); + } + return partialFrame.nextPayloadBytes(framesReader); + } finally { + lock.unlock(); + } + } + + /** + * {@return true if EOF has been reached and all buffers have been read} + */ + public boolean eof() { + lock.lock(); + try { + if (!eof) return false; + if (!framesReader.hasRemaining()) return true; + if (partialFrame != null) { + // still some payload data to read... + if (partialFrame.remaining() > 0) return false; + } + var pos = framesReader.position(); + try { + // if there's not enough data to decode a new frame or a new + // partial frame then since no more data will ever come, we do have + // reached EOF. If however, we can read a frame from the remaining + // data in the buffer, then EOF is not reached yet. + // The next call to poll() will return that frame. + var frame = Http3Frame.decode(framesReader, this::isAllowed, debug); + return frame == null; + } finally { + // restore position for the next call to poll. + framesReader.position(pos); + } + } finally { + lock.unlock(); + } + } + + /** + * {@return true if all buffers have been read} + */ + public boolean clean() { + lock.lock(); + try { + if (partialFrame != null) { + // still some payload data to read... + if (partialFrame.remaining() > 0) return false; + } + return !framesReader.hasRemaining(); + } finally { + lock.unlock(); + } + } + + /** + * Clears any unconsumed buffers. + */ + public void clear() { + lock.lock(); + try { + partialFrame = null; + framesReader.clear(); + } finally { + lock.unlock(); + } + } + + /** + * Can be overridden by subclasses to avoid parsing a frame + * fully if the frame is not allowed on this stream, or + * according to the stream state. + * + * @implSpec + * This method delegates to the {@linkplain #FramesDecoder(String, LongPredicate) + * predicate} given at construction time. If {@linkplain #FramesDecoder(String) + * no predicate} was given this method returns true. + * + * @param frameType the frame type + * @return true if the frame is allowed + */ + protected boolean isAllowed(long frameType) { + return isAllowed.test(frameType); + } + + /** + * A predicate that returns true for all frames types allowed + * on the server->client control stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a control stream. + */ + public static boolean isAllowedOnControlStream(long frameType) { + if (frameType == Http3FrameType.DATA.type()) return false; + if (frameType == Http3FrameType.HEADERS.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + /** + * A predicate that returns true for all frames types allowed + * on the client->server control stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a control stream. + */ + public static boolean isAllowedOnClientControlStream(long frameType) { + if (frameType == Http3FrameType.DATA.type()) return false; + if (frameType == Http3FrameType.HEADERS.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + /** + * A predicate that returns true for all frames types allowed + * on a request/response stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a request/response + * stream. + */ + public static boolean isAllowedOnRequestStream(long frameType) { + if (frameType == Http3FrameType.SETTINGS.type()) return false; + if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false; + if (frameType == Http3FrameType.GOAWAY.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + + /** + * A predicate that returns true for all frames types allowed + * on a push promise stream. + * @param frameType a frame type + * @return whether a frame of this type is allowed on a request/response + * stream. + */ + public static boolean isAllowedOnPromiseStream(long frameType) { + if (frameType == Http3FrameType.SETTINGS.type()) return false; + if (frameType == Http3FrameType.CANCEL_PUSH.type()) return false; + if (frameType == Http3FrameType.GOAWAY.type()) return false; + if (frameType == Http3FrameType.MAX_PUSH_ID.type()) return false; + if (frameType == Http3FrameType.PUSH_PROMISE.type()) return false; + if (Http3FrameType.isIllegalType(frameType)) return false; + return true; + } + + private static boolean allAllowed(long frameType) { + return true; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java new file mode 100644 index 00000000000..274a1af3a56 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/GoAwayFrame.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a GOAWAY HTTP3 frame + */ +public final class GoAwayFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.GOAWAY_FRAME; + private final long length; + // represents either a stream id or a push id depending on the context + // of the frame + private final long id; + + public GoAwayFrame(final long id) { + super(TYPE); + this.id = id; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.id); + } + + // only used when constructing the frame during decoding content over a stream + private GoAwayFrame(final long length, final long id) { + super(Http3FrameType.GOAWAY.type()); + this.length = length; + this.id = id; + } + + @Override + public long length() { + return this.length; + } + + /** + * {@return the id of either the stream or a push promise, depending on the context + * of this frame} + */ + public long getTargetId() { + return this.id; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the stream id/push id + VariableLengthEncoder.encode(buf, this.id); + } + + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + final long position = reader.position(); + // read the frame type + decodeRequiredType(reader, Http3FrameType.GOAWAY.type()); + // read length of the payload + final long length = VariableLengthEncoder.decode(reader); + if (length < 0 || length > reader.remaining()) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed targetId + return new MalformedFrame(TYPE, + Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in GOAWAY frame: " + length); + } + + // read stream id / push id + final long targetId = VariableLengthEncoder.decode(reader); + if (targetId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new GoAwayFrame(length, targetId); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(super.toString()).append(" stream/push id: ").append(this.id); + return sb.toString(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java new file mode 100644 index 00000000000..5d7672458a3 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/HeadersFrame.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +/** + * This class models an HTTP/3 HEADERS frame. + * @apiNote + * An instance of {@code HeadersFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see jdk.internal.net.http.http3.frames.PartialFrame + */ +public final class HeadersFrame extends PartialFrame { + + /** + * The HEADERS frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.HEADERS_FRAME; + + + private final long length; + + /** + * Creates a new HTTP/3 HEADERS frame + */ + public HeadersFrame(long length) { + super(TYPE, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java new file mode 100644 index 00000000000..c4b234553ad --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3Frame.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.util.function.LongPredicate; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import static jdk.internal.net.http.http3.frames.Http3FrameType.DATA; +import static jdk.internal.net.http.http3.frames.Http3FrameType.HEADERS; +import static jdk.internal.net.http.http3.frames.Http3FrameType.PUSH_PROMISE; +import static jdk.internal.net.http.http3.frames.Http3FrameType.UNKNOWN; +import static jdk.internal.net.http.http3.frames.Http3FrameType.asString; +import static jdk.internal.net.http.http3.frames.Http3FrameType.isIllegalType; + +/** + * An HTTP/3 frame + */ +public sealed interface Http3Frame permits AbstractHttp3Frame { + + /** + * {@return the type of this frame} + */ + long type(); + + /** + * {@return the length of this frame} + */ + long length(); + + + /** + * {@return the portion of the frame payload that can be read + * after the frame was created, when the current frame + * can be read as a partial frame, otherwise 0, if + * the payload can't be streamed} + */ + default long streamingLength() { return 0;} + + /** + * Attempts to decode an HTTP/3 frame from the bytes accumulated + * in the reader. + * + * @apiNote + * + * If an error is detected while parsing the frame, a {@link MalformedFrame} + * error will be returned + * + * @param reader the reader containing the bytes + * @param isFrameTypeAllowed a predicate to test whether a given + * frame type is allowed in this context + * @param debug a logger to log debug traces + * @return the decoded frame, or {@code null} if some bytes are + * missing to decode the frame + */ + static Http3Frame decode(BuffersReader reader, LongPredicate isFrameTypeAllowed, Logger debug) { + long pos = reader.position(); + long limit = reader.limit(); + long remaining = reader.remaining(); + long type = -1; + long before = reader.read(); + Http3Frame frame; + try { + int tsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos); + if (tsize == -1 || remaining - tsize < 0) return null; + type = VariableLengthEncoder.peekEncodedValue(reader, pos); + if (type == -1) return null; + if (isIllegalType(type) || !isFrameTypeAllowed.test(type)) { + var msg = "H3_FRAME_UNEXPECTED: Frame " + + asString(type) + + " is not allowed on this stream"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_UNEXPECTED.code(), msg); + reader.clear(); + return frame; + } + + int lsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize); + if (lsize == -1 || remaining - tsize - lsize < 0) return null; + final long length = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize); + var frameType = Http3FrameType.forType(type); + if (debug.on()) { + debug.log("Decoding %s(length=%s)", frameType, length); + } + if (frameType == UNKNOWN) { + if (debug.on()) { + debug.log("decode partial unknown frame: " + + "pos:%s, limit:%s, remaining:%s," + + " tsize:%s, lsize:%s, length:%s", + pos, limit, remaining, tsize, lsize, length); + } + reader.position(pos + tsize + lsize); + reader.release(); + return new UnknownFrame(type, length); + } else if (frameType.maxLength() < length) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too long"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + + if (frameType == HEADERS) { + if (length == 0) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + reader.position(pos + tsize + lsize); + reader.release(); + return new HeadersFrame(length); + } + + if (frameType == DATA) { + reader.position(pos + tsize + lsize); + reader.release(); + return new DataFrame(length); + } + + if (frameType == PUSH_PROMISE) { + int pidsize = VariableLengthEncoder.peekEncodedValueSize(reader, pos + tsize + lsize); + if (length == 0 || length < pidsize) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " length too short to fit pushID"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + if (length == pidsize) { + var msg = "H3_FRAME_ERROR: Frame " + asString(type) + " does not contain headers"; + if (debug.on()) debug.log(msg); + frame = new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + reader.clear(); + return frame; + } + if (pidsize == -1 || remaining - tsize - lsize - pidsize < 0) return null; + long pushId = VariableLengthEncoder.peekEncodedValue(reader, pos + tsize + lsize); + reader.position(pos + tsize + lsize + pidsize); + reader.release(); + return new PushPromiseFrame(pushId, length - pidsize); + } + + if (length + tsize + lsize > reader.remaining()) { + // we haven't moved the reader's position. + // we'll be called back when new bytes are available and + // we'll resume reading type + length from the same position + // again, until we have enough to read the frame. + return null; + } + + assert isFrameTypeAllowed.test(type); + + frame = switch(frameType) { + case SETTINGS -> SettingsFrame.decodeFrame(reader, debug); + case GOAWAY -> GoAwayFrame.decodeFrame(reader, debug); + case CANCEL_PUSH -> CancelPushFrame.decodeFrame(reader, debug); + case MAX_PUSH_ID -> MaxPushIdFrame.decodeFrame(reader, debug); + default -> { + reader.position(pos + tsize + lsize); + reader.release(); + yield new UnknownFrame(type, length); + } + }; + + long read; + if (frame instanceof MalformedFrame || frame == null) { + return frame; + } else if ((read = (reader.read() - before - tsize - lsize)) != length) { + String msg = ("H3_FRAME_ERROR: Frame %s payload length does not match" + + " frame length (length=%s, payload=%s)") + .formatted(asString(type), length, read); + if (debug.on()) debug.log(msg); + reader.release(); // mark reader read + reader.position(reader.position() + tsize + lsize + length); + reader.release(); + return new MalformedFrame(type, Http3Error.H3_FRAME_ERROR.code(), msg); + } else { + return frame; + } + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to decode frame", t); + reader.clear(); // mark reader read + return new MalformedFrame(type, Http3Error.H3_INTERNAL_ERROR.code(), t.getMessage(), t); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java new file mode 100644 index 00000000000..858e10fa6a0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/Http3FrameType.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; +import java.util.stream.Stream; + +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_ENCODED_INTEGER; +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_INTEGER_LENGTH; + +/** + * An enum to model HTTP/3 frame types. + */ +public enum Http3FrameType { + + /** + * Used to identify an HTTP/3 frame whose type is unknown + */ + UNKNOWN(-1, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 DATA frame + */ + DATA(TYPE.DATA_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 HEADERS frame + */ + HEADERS(TYPE.HEADERS_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 CANCEL_PUSH frame + */ + CANCEL_PUSH(TYPE.CANCEL_PUSH_FRAME, MAX_INTEGER_LENGTH), + /** + * Used to identify an HTTP/3 SETTINGS frame + */ + SETTINGS(TYPE.SETTINGS_FRAME, TYPE.MAX_SETTINGS_LENGTH), + /** + * Used to identify an HTTP/3 PUSH_PROMISE frame + */ + PUSH_PROMISE(TYPE.PUSH_PROMISE_FRAME, MAX_ENCODED_INTEGER), + /** + * Used to identify an HTTP/3 GOAWAY frame + */ + GOAWAY(TYPE.GOAWAY_FRAME, MAX_INTEGER_LENGTH), + /** + * Used to identify an HTTP/3 MAX_PUSH_ID_FRAME frame + */ + MAX_PUSH_ID(TYPE.MAX_PUSH_ID_FRAME, MAX_INTEGER_LENGTH); + + /** + * A class to hold type constants + */ + static final class TYPE { + private TYPE() { throw new InternalError(); } + + // Frames types + public static final int DATA_FRAME = 0x00; + public static final int HEADERS_FRAME = 0x01; + public static final int CANCEL_PUSH_FRAME = 0x03; + public static final int SETTINGS_FRAME = 0x04; + public static final int PUSH_PROMISE_FRAME = 0x05; + public static final int GOAWAY_FRAME = 0x07; + public static final int MAX_PUSH_ID_FRAME = 0x0d; + + // The maximum size a settings frame can have. + // This is a limit imposed by our implementation. + // There are only 7 settings defined in the current + // specification, but we will allow for a frame to + // contain up to 80. Past that limit, we will consider + // the frame to be malformed: + // 8 x 10 x (max sizeof(id) + max sizeof(value)) = 80 x 16 bytes + public static final long MAX_SETTINGS_LENGTH = + 10L * 8L * MAX_INTEGER_LENGTH * 2L; + } + + + // This is one of the values defined in TYPE above, or + // -1 for the UNKNOWN frame types. + private final int type; + private final long maxLength; + private Http3FrameType(int type, long maxLength) { + this.type = type; + this.maxLength = maxLength; + } + + /** + * {@return the frame type, as defined by HTTP/3} + */ + public long type() { return type;} + + /** + * {@return the maximum length a frame of this type + * can take} + */ + public long maxLength() { + return maxLength; + } + + /** + * {@return the HTTP/3 frame type, as an int} + * + * @apiNote + * HTTP/3 defines frames type as variable length integers + * in the range [0, 2^62-1]. However, the few standard frame + * types registered for HTTP/3 and modeled by this enum + * class can be coded as an int. + * This method provides a convenient way to access the frame + * type as an int, which avoids having to cast when using + * the value in switch statements. + */ + public int intType() { return type;} + + /** + * {@return the {@link Http3FrameType} corresponding to the given + * {@code type}, or {@link #UNKNOWN} if no corresponding + * {@link Http3FrameType} instance is found} + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + */ + public static Http3FrameType forType(long type) { + return Stream.of(values()) + .filter(x -> x.type == type) + .findFirst() + .orElse(UNKNOWN); + } + + /** + * {@return a string representation of the given type, suited for inclusion + * in log messages, exceptions, etc...} + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + */ + public static String asString(long type) { + String str = null; + if (type >= Integer.MIN_VALUE && type <= Integer.MAX_VALUE) { + str = switch ((int)type) { + case TYPE.DATA_FRAME -> DATA.name(); // 0x00 + case TYPE.HEADERS_FRAME -> HEADERS.name(); // 0x01 + case 0x02 -> "RESERVED(0x02)"; + case TYPE.CANCEL_PUSH_FRAME -> CANCEL_PUSH.name(); // 0x03 + case TYPE.SETTINGS_FRAME -> SETTINGS.name(); // 0x04 + case TYPE.PUSH_PROMISE_FRAME -> PUSH_PROMISE.name(); // 0x05 + case 0x06 -> "RESERVED(0x06)"; + case TYPE.GOAWAY_FRAME -> GOAWAY.name(); // 0x07 + case 0x08 -> "RESERVED(0x08)"; + case 0x09 -> "RESERVED(0x09)"; + case TYPE.MAX_PUSH_ID_FRAME -> MAX_PUSH_ID.name(); // 0x0d + default -> null; + }; + } + if (str != null) return str; + if (isReservedType(type)) { + return "RESERVED(type=" + type + ")"; + } + return "UNKNOWN(type=" + type + ")"; + } + + /** + * {@return whether this frame type is illegal} + * This corresponds to HTTP/2 frame types that have no equivalent in + * HTTP/3. + * @param type the frame type + */ + public static boolean isIllegalType(long type) { + return type == 0x02 || type == 0x06 || type == 0x08 || type == 0x09; + } + + /** + * Whether the given type is one of the reserved frame + * types defined by HTTP/3. For any non-negative integer N: + * {@code 0x21 + 0x1f * N } + * is a reserved frame type that has no meaning. + * + * @param type an HTTP/3 frame type identifier read from an HTTP/3 frame + * + * @return true if the given type matches the {@code 0x21 + 0x1f * N} + * pattern + */ + public static boolean isReservedType(long type) { + return type >= 0x21L && type <= MAX_ENCODED_INTEGER + && (type - 0x21L) % 0x1f == 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java new file mode 100644 index 00000000000..0d89666ec26 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MalformedFrame.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.util.function.LongPredicate; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.quic.BuffersReader; + +import jdk.internal.net.http.http3.Http3Error; + +/** + * An instance of MalformedFrame can be returned by + * {@link AbstractHttp3Frame#decode(BuffersReader, LongPredicate, Logger)} + * when a malformed frame is detected. This should cause the caller + * to send an error to its peer, and possibly throw an + * exception to the higher layer. + */ +public class MalformedFrame extends AbstractHttp3Frame { + + private final long errorCode; + private final String msg; + private final Throwable cause; + + /** + * Creates Connection Error malformed frame + * + * @param errorCode - error code + * @param msg - internal debug message + */ + public MalformedFrame(long type, long errorCode, String msg) { + this(type, errorCode, msg, null); + } + + /** + * Creates Connection Error malformed frame + * + * @param errorCode - error code + * @param msg - internal debug message + * @param cause - internal cause for the error, if available + * (can be null) + */ + public MalformedFrame(long type, long errorCode, String msg, Throwable cause) { + super(type); + this.errorCode = errorCode; + this.msg = msg; + this.cause = cause; + } + + @Override + public String toString() { + return super.toString() + " MalformedFrame, Error: " + + Http3Error.stringForCode(errorCode) + + " reason: " + msg; + } + + /** + * {@inheritDoc} + * @implSpec this method always returns 0 + */ + @Override + public long length() { + return 0; // Not Applicable + } + + /** + * {@inheritDoc} + * @implSpec this method always returns 0 + */ + @Override + public long size() { + return 0; // Not applicable + } + + /** + * {@return the {@linkplain Http3Error#code() HTTP/3 error code} that + * should be reported to the peer} + */ + public long getErrorCode() { + return errorCode; + } + + /** + * {@return a message that describe the error} + */ + public String getMessage() { + return msg; + } + + /** + * {@return the cause of the error, if available, {@code null} otherwise} + * + * @apiNote + * This is useful for logging and diagnosis purpose, typically when the + * error is an {@linkplain Http3Error#H3_INTERNAL_ERROR internal error}. + */ + public Throwable getCause() { + return cause; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java new file mode 100644 index 00000000000..b4f35064da1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/MaxPushIdFrame.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a MAX_PUSH_ID HTTP3 frame + */ +public final class MaxPushIdFrame extends AbstractHttp3Frame { + + public static final int TYPE = Http3FrameType.TYPE.MAX_PUSH_ID_FRAME; + + private final long length; + private final long maxPushId; + + public MaxPushIdFrame(final long maxPushId) { + super(Http3FrameType.MAX_PUSH_ID.type()); + this.maxPushId = maxPushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.maxPushId); + } + + // only used when constructing the frame during decoding content over a stream + private MaxPushIdFrame(final long maxPushId, final long length) { + super(Http3FrameType.MAX_PUSH_ID.type()); + this.maxPushId = maxPushId; + this.length = length; + } + + @Override + public long length() { + return this.length; + } + + public long getMaxPushId() { + return this.maxPushId; + } + + public void writeFrame(final ByteBuffer buf) { + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.length); + // write the max push id value + VariableLengthEncoder.encode(buf, this.maxPushId); + } + + /** + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * @param reader the reader + * @param debug a logger for debugging purposes + * @return the new frame + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + static AbstractHttp3Frame decodeFrame(final BuffersReader reader, final Logger debug) { + long position = reader.position(); + decodeRequiredType(reader, TYPE); + long length = VariableLengthEncoder.decode(reader); + if (length > reader.remaining() || length < 0) { + reader.position(position); + throw new BufferUnderflowException(); + } + // position before reading payload + long start = reader.position(); + + if (length == 0 || length != VariableLengthEncoder.peekEncodedValueSize(reader, start)) { + // frame length does not match the enclosed maxPushId + return new MalformedFrame(TYPE, Http3Error.H3_FRAME_ERROR.code(), + "Invalid length in MAX_PUSH_ID frame: " + length); + } + + long maxPushId = VariableLengthEncoder.decode(reader); + if (maxPushId == -1) { + reader.position(position); + throw new BufferUnderflowException(); + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return new MaxPushIdFrame(maxPushId); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java new file mode 100644 index 00000000000..3cbbb814b82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PartialFrame.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; +import java.util.List; + +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.BuffersReader; + +/** + * A PartialFrame helps to read the payload of a frame. + * This class is not multi-thread safe. + */ +public abstract sealed class PartialFrame + extends AbstractHttp3Frame + permits HeadersFrame, + DataFrame, + PushPromiseFrame, + UnknownFrame { + + private static final List NONE = List.of(); + private final long streamingLength; + private long remaining; + PartialFrame(long frameType, long streamingLength) { + super(frameType); + this.remaining = this.streamingLength = streamingLength; + } + + @Override + public final long streamingLength() { + return streamingLength; + } + + /** + * {@return the number of payload bytes that remains to read} + */ + public final long remaining() { + return remaining; + } + + /** + * Reads remaining payload bytes from the given {@link BuffersReader}. + * This method must not run concurrently with any code that submit + * new buffers to the {@link BuffersReader}. + * @param buffers a {@link BuffersReader} that contains payload bytes. + * @return the payload bytes available so far, an empty list if no + * bytes are available or the whole payload has already been + * read + */ + public final List nextPayloadBytes(BuffersReader buffers) { + var remaining = this.remaining; + if (remaining > 0) { + long available = buffers.remaining(); + if (available > 0) { + long read = Math.min(remaining, available); + this.remaining = remaining - read; + return buffers.getAndRelease(read); + } + } + return NONE; + } + + /** + * Reads remaining payload bytes from the given {@link ByteBuffer}. + * @param buffer a {@link ByteBuffer} that contains payload bytes. + * @return the payload bytes available in the given buffer, or + * {@code null} if all payload has been read. + */ + public final ByteBuffer nextPayloadBytes(ByteBuffer buffer) { + var remaining = this.remaining; + if (remaining > 0) { + int available = buffer.remaining(); + if (available > 0) { + long read = Math.min(remaining, available); + remaining -= read; + this.remaining = remaining; + assert read <= available; + int pos = buffer.position(); + int len = (int) read; + // always create a slice, so that we can move the position + // of the original buffer, as if the data had been read. + ByteBuffer next = buffer.slice(pos, len); + buffer.position(pos + len); + return next; + } else return buffer == QuicStreamReader.EOF ? buffer : buffer.slice(); + } + return null; + } + + /** + * Write the frame headers to the given buffer. + * + * @apiNote + * The caller will be responsible for writing the + * remaining {@linkplain #length() length} bytes of + * the frame content after writing the frame headers. + * + * @implSpec + * Usually the header of a frame is assumed to simply + * contain the frame type and frame length. + * Some subclasses of {@code AbstractHttp3Frame} may + * however include some additional information. + * For instance, {@link PushPromiseFrame} may consider + * the {@link PushPromiseFrame#getPushId() pushId} as + * being in part of the headers, and write it along + * in this method after the frame type and length. + * In such a case, a subclass would also need to + * override {@link #headersSize()} in order to add + * the size of the additional information written + * by {@link #writeHeaders(ByteBuffer)}. + * + * @param buf a buffer to write the headers into + */ + public void writeHeaders(ByteBuffer buf) { + long len = length(); + int pos0 = buf.position(); + VariableLengthEncoder.encode(buf, type()); + VariableLengthEncoder.encode(buf, len); + int pos1 = buf.position(); + assert pos1 - pos0 == super.headersSize(); + } + + @Override + public String toString() { + var len = length(); + return "%s (partial: %s/%s)".formatted(this.getClass().getSimpleName(), len - remaining, len); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java new file mode 100644 index 00000000000..f95fa7f964d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/PushPromiseFrame.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.ByteBuffer; + +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * Represents a PUSH_PROMISE HTTP3 frame + */ +public final class PushPromiseFrame extends PartialFrame { + + /** + * The PUSH_PROMISE frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.PUSH_PROMISE_FRAME; + + private final long length; + private final long pushId; + + public PushPromiseFrame(final long pushId, final long fieldLength) { + super(TYPE, fieldLength); + if (pushId < 0 || pushId > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("invalid pushId: " + pushId); + } + this.pushId = pushId; + // the payload length of this frame + this.length = VariableLengthEncoder.getEncodedSize(this.pushId) + fieldLength; + } + + @Override + public long length() { + return this.length; + } + + public long getPushId() { + return this.pushId; + } + + /** + * Write the frame header and the promise {@link #getPushId() + * pushId} to the given buffer. The caller will be responsible + * for writing the remaining {@link #streamingLength()} bytes + * that constitutes the field section length. + * @param buf a buffer to write the headers into + */ + @Override + public void writeHeaders(ByteBuffer buf) { + super.writeHeaders(buf); + VariableLengthEncoder.encode(buf, this.pushId); + } + + /** + * {@return the number of bytes needed to write the headers and + * the promised {@link #getPushId() pushId}}. + */ + @Override + public int headersSize() { + return super.headersSize() + VariableLengthEncoder.getEncodedSize(pushId); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java new file mode 100644 index 00000000000..90fabd47e68 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/SettingsFrame.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.quic.VariableLengthEncoder.MAX_ENCODED_INTEGER; + +/** + * This class models an HTTP/3 SETTINGS frame + */ +public class SettingsFrame extends AbstractHttp3Frame { + + // An array of setting parameters. + // The index is the parameter id, minus 1, the value is the parameter value + private final long[] parameters; + // HTTP/3 specifies some reserved identifier for which the parameter + // has no semantics and the value is undefined and should be ignored. + // It's excepted that at least one such parameter should be included + // in the settings frame to exercise the fact that undefined parameters + // should be ignored + private long undefinedId; + private long undefinedValue; + + /** + * The SETTINGS frame type, as defined by HTTP/3 + */ + public static final int TYPE = Http3FrameType.TYPE.SETTINGS_FRAME; + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(super.toString()) + .append(" Settings: "); + + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + sb.append(name(i+1)) + .append("=") + .append(parameters[i]) + .append(' '); + } + } + if (undefinedId != -1) { + sb.append(name(undefinedId)).append("=") + .append(undefinedValue).append(' '); + } + return sb.toString(); + } + + // TODO: should we use an enum instead? + // HTTP/2 only Parameters - receiving one of those should be + // considered as a protocol error of type SETTINGS_ERROR + public static final int ENABLE_PUSH = 0x2; + public static final int MAX_CONCURRENT_STREAMS = 0x3; + public static final int INITIAL_WINDOW_SIZE = 0x4; + public static final int MAX_FRAME_SIZE = 0x5; + // HTTP/3 Parameters + // This parameter was defined as HEADER_TABLE_SIZE in HTTP/2 + public static final int SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0x1; + public static final int DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0; + // This parameter was defined as MAX_HEADER_LIST_SIZE in HTTP/2 + public static final int SETTINGS_MAX_FIELD_SECTION_SIZE = 0x6; + public static final long DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE = -1; + // Allow compression efficiency by allowing referencing dynamic table entries + // that are still in transit. This parameter specifies the number of streams + // that could become blocked. + public static final int SETTINGS_QPACK_BLOCKED_STREAMS = 0x7; + public static final int DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS = 0; + + public static final int MAX_PARAM = 0x7; + + // maps a parameter id to a parameter name + private String name(long i) { + if (i <= MAX_PARAM) { + return switch ((int)i) { + case SETTINGS_QPACK_MAX_TABLE_CAPACITY -> "SETTINGS_QPACK_MAX_TABLE_CAPACITY"; // 0x01 + case ENABLE_PUSH -> "ENABLE_PUSH"; // 0x02 + case MAX_CONCURRENT_STREAMS -> "MAX_CONCURRENT_STREAMS"; // 0x03 + case INITIAL_WINDOW_SIZE -> "INITIAL_WINDOW_SIZE"; // 0x04 + case MAX_FRAME_SIZE -> "MAX_FRAME_SIZE"; // 0x05 + case SETTINGS_MAX_FIELD_SECTION_SIZE -> "SETTINGS_MAX_FIELD_SECTION_SIZE"; // 0x06 + case SETTINGS_QPACK_BLOCKED_STREAMS -> "SETTINGS_QPACK_BLOCKED_STREAMS"; // 0x07 + default -> "UNKNOWN(0x00)"; // 0x00 ? + }; + } else if (isReservedId(i)) { + return "RESERVED(" + i + ")"; + } else { + return "UNKNOWN(" + i +")"; + } + } + + /** + * Creates a new HTTP/3 SETTINGS frame, including the given + * reserved identifier id and value pair. + * + * @implNote + * We only keep one reserved id/value pair - there's no + * reason to keep more... + * + * @param undefinedId the id of an undefined (reserved) parameter + * @param undefinedValue a random value for the undefined parameter + */ + public SettingsFrame(long undefinedId, long undefinedValue) { + super(TYPE); + parameters = new long [MAX_PARAM]; + Arrays.fill(parameters, -1); + assert undefinedId == -1 || isReservedId(undefinedId); + assert undefinedId != -1 || undefinedValue == -1; + this.undefinedId = undefinedId; + this.undefinedValue = undefinedValue; + } + + /** + * Creates a new empty SETTINGS frame, and allocate a random + * reserved id and value pair. + */ + public SettingsFrame() { + this(nextRandomReservedParameterId(), nextRandomParameterValue()); + } + + /** + * Get the parameter value for the given parameter id + * + * @param paramID the parameter id + * + * @return the value of the given parameter, if present, + * {@code -1}, if absent + * + * @throws IllegalArgumentException if the parameter id is negative or + * {@linkplain #isIllegal(long) illegal} + * + */ + public synchronized long getParameter(int paramID) { + if (isIllegal(paramID)) { + throw new IllegalArgumentException("illegal parameter: " + paramID); + } + if (undefinedId != -1 && paramID == undefinedId) + return undefinedValue; + if (paramID > MAX_PARAM) return -1; + return parameters[paramID - 1]; + } + + /** + * Sets the given parameter to the given value. + * + * @param paramID the parameter id + * @param value the parameter value + * + * @return this + * + * @throws IllegalArgumentException if the parameter id is negative or + * {@linkplain #isIllegal(long) illegal} + */ + public synchronized SettingsFrame setParameter(long paramID, long value) { + // subclasses can override this to actually send + // an illegal parameter + if (isIllegal(paramID) || paramID < 1 || paramID > MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("illegal parameter: " + paramID); + } + if (paramID <= MAX_PARAM) { + parameters[(int)paramID - 1] = value; + } else if (isReservedId(paramID)) { + this.undefinedId = paramID; + this.undefinedValue = value; + } + return this; + } + + @Override + public long length() { + int len = 0; + int i = 0; + for (long p : parameters) { + if (p != -1) { + len += VariableLengthEncoder.getEncodedSize(i+1); + len += VariableLengthEncoder.getEncodedSize(p); + } + } + if (undefinedId != -1) { + assert isReservedId(undefinedId); + len += VariableLengthEncoder.getEncodedSize(undefinedId); + len += VariableLengthEncoder.getEncodedSize(undefinedValue); + } + return len; + } + + /** + * Writes this frame to the given buffer. + * + * @param buf a byte buffer to write this frame into + * + * @throws java.nio.BufferUnderflowException if the buffer + * doesn't have enough space + */ + public void writeFrame(ByteBuffer buf) { + long size = size(); + long len = length(); + int pos0 = buf.position(); + VariableLengthEncoder.encode(buf, TYPE); + VariableLengthEncoder.encode(buf, len); + int pos1 = buf.position(); + for (int i = 0; i < MAX_PARAM; i++) { + if (parameters[i] != -1) { + VariableLengthEncoder.encode(buf, i+1); + VariableLengthEncoder.encode(buf, parameters[i]); + } + } + if (undefinedId != -1) { + // Setting identifiers of the format 0x1f * N + 0x21 for + // non-negative integer values of N are reserved to exercise + // the requirement that unknown identifiers be ignored. + // Such settings have no defined meaning. Endpoints SHOULD + // include at least one such setting in their SETTINGS frame + assert isReservedId(undefinedId); + VariableLengthEncoder.encode(buf, undefinedId); + VariableLengthEncoder.encode(buf, undefinedValue); + } + assert buf.position() - pos1 == len; + assert buf.position() == pos0 + size; + } + + /** + * Decodes a SETTINGS frame from the given reader. + * This method is expected to be called when the reader + * contains enough bytes to decode the frame. + * + * @param reader a reader containing bytes + * + * @return a new SettingsFrame frame, or a MalformedFrame. + * + * @throws BufferUnderflowException if the reader doesn't contain + * enough bytes to decode the frame + */ + public static AbstractHttp3Frame decodeFrame(BuffersReader reader, Logger debug) { + final long pos = reader.position(); + decodeRequiredType(reader, TYPE); + final SettingsFrame frame = new SettingsFrame(-1, -1); + long length = VariableLengthEncoder.decode(reader); + + // is that OK? Find what's the actual limit for + // a frame length... + if (length > reader.remaining()) { + reader.position(pos); + throw new BufferUnderflowException(); + } + + // position before reading payload + long start = reader.position(); + + while (length > reader.position() - start) { + long id = VariableLengthEncoder.decode(reader); + long value = VariableLengthEncoder.decode(reader); + if (id == -1 || value == -1) { + return new MalformedFrame(TYPE, + Http3Error.H3_FRAME_ERROR.code(), + "Invalid SETTINGS frame contents."); + } + try { + frame.setParameter(id, value); + } catch (IllegalArgumentException iae) { + String msg = "H3_SETTINGS_ERROR: " + iae.getMessage(); + if (debug.on()) debug.log(msg, iae); + reader.position(start + length); + reader.release(); + return new MalformedFrame(TYPE, + Http3Error.H3_SETTINGS_ERROR.code(), + iae.getMessage(), + iae); + } + } + + // check position after reading payload + var malformed = checkPayloadSize(TYPE, reader, start, length); + if (malformed != null) return malformed; + + reader.release(); + return frame; + } + + public static SettingsFrame defaultRFCSettings() { + SettingsFrame f = new SettingsFrame() + .setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, + DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE) + .setParameter(SETTINGS_QPACK_MAX_TABLE_CAPACITY, + DEFAULT_SETTINGS_QPACK_MAX_TABLE_CAPACITY) + .setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, + DEFAULT_SETTINGS_QPACK_BLOCKED_STREAMS); + return f; + } + + public boolean isIllegal(long parameterId) { + // Parameters with 0x0, 0x2, 0x3, 0x4 and 0x5 ids are reserved, + // 0x6 is the legal one: + // https://www.rfc-editor.org/rfc/rfc9114.html#name-settings-parameters + // 0x1 and 0x7 defined by QPACK as a legal one: + // https://www.rfc-editor.org/rfc/rfc9204.html#name-configuration + return parameterId < SETTINGS_MAX_FIELD_SECTION_SIZE && + parameterId != SETTINGS_QPACK_MAX_TABLE_CAPACITY; + } + + public static long nextRandomParameterValue() { + long value = RANDOM.nextLong(0, MAX_ENCODED_INTEGER + 1); + assert value >= 0 && value <= MAX_ENCODED_INTEGER; + return value; + } + + private static final long MAX_N = (MAX_ENCODED_INTEGER - 0x21L) / 0x1fL; + public static long nextRandomReservedParameterId() { + long N = RANDOM.nextLong(0, MAX_N + 1); + long id = 0x1fL * N + 0x21L; + assert id <= MAX_ENCODED_INTEGER; + assert id >= 0x21L; + assert isReservedId(id) : "generated id is not undefined: " + id; + return id; + } + + /** + * Tells whether the given id is one of the undefined parameter ids that + * are reserved and have no meaning. + * + * @apiNote + * Setting identifiers of the format 0x1f * N + 0x21 + * for non-negative integer values of N are reserved to + * exercise the requirement that unknown identifiers be + * ignored + * + * @param id the parameter id + * + * @return true if this is one of the reserved identifiers + */ + public static boolean isReservedId(long id) { + return id >= 0x21 && id < MAX_ENCODED_INTEGER && (id - 0x21) % 0x1f == 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java new file mode 100644 index 00000000000..4426493ed7c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/frames/UnknownFrame.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.frames; + +/** + * A class to model an unknown or reserved frame. + * @apiNote + * From RFC 9114: + *
        + * Frame types of the format 0x1f * N + 0x21 for non-negative integer + * values of N are reserved to exercise the requirement that + * unknown types be ignored (Section 9). These frames have no semantics, + * and MAY be sent on any stream where frames are allowed to be sent. + * This enables their use for application-layer padding. Endpoints MUST NOT + * consider these frames to have any meaning upon receipt. + *
        + * + * @apiNote + * An instance of {@code UnknownFrame} is used to read or writes + * the frame's type and length. The payload is supposed to be + * read or written directly to the stream on its own, after having + * read or written the frame type and length. + * @see jdk.internal.net.http.http3.frames.PartialFrame + * */ +public final class UnknownFrame extends PartialFrame { + final long length; + UnknownFrame(long type, long length) { + super(type, length); + this.length = length; + } + + @Override + public long length() { + return length; + } + + /** + * {@return true if this frame type is one of the reserved + * types} + */ + public boolean isReserved() { + return Http3FrameType.isReservedType(type); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java new file mode 100644 index 00000000000..8399a4550ff --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/Http3Streams.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.streams; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; + +public final class Http3Streams { + public static final int CONTROL_STREAM_CODE = 0x00; + public static final int PUSH_STREAM_CODE = 0x01; + public static final int QPACK_ENCODER_STREAM_CODE = 0x02; + public static final int QPACK_DECODER_STREAM_CODE = 0x03; + + private Http3Streams() { throw new InternalError(); } + + public enum StreamType { + CONTROL(CONTROL_STREAM_CODE), + PUSH(PUSH_STREAM_CODE), + QPACK_ENCODER(QPACK_ENCODER_STREAM_CODE), + QPACK_DECODER(QPACK_DECODER_STREAM_CODE); + final int code; + StreamType(int code) { + this.code = code; + } + public final int code() { + return code; + } + public static Optional ofCode(long code) { + return EnumSet.allOf(StreamType.class).stream() + .filter(s -> s.code() == code) + .findFirst(); + } + } + + /** + * {@return an optional string that represents the error state of the + * stream, or {@code Optional.empty()} if no error code + * has been received or sent} + * @param stream a quic stream that may have errors + */ + public static Optional errorCodeAsString(QuicStream stream) { + long sndErrorCode = -1; + long rcvErrorCode = -1; + if (stream instanceof QuicReceiverStream rcv) { + rcvErrorCode = rcv.rcvErrorCode(); + } + if (stream instanceof QuicSenderStream snd) { + sndErrorCode = snd.sndErrorCode(); + } + if (rcvErrorCode >= 0 || sndErrorCode >= 0) { + Stream rcv = rcvErrorCode >= 0 + ? Stream.of("RCV: " + Http3Error.stringForCode(rcvErrorCode)) + : Stream.empty(); + Stream snd = sndErrorCode >= 0 + ? Stream.of("SND: " + Http3Error.stringForCode(sndErrorCode)) + : Stream.empty(); + return Optional.of(Stream.concat(rcv, snd) + .collect(Collectors.joining(",", "errorCode(", ")" ))); + } + return Optional.empty(); + } + + /** + * If the stream has errors, prints a message recording the + * {@linkplain #errorCodeAsString(QuicStream) error state} of the + * stream through the given logger. The message is of the form: + * {@code : }. + * If the given {@code name} is null or empty, {@code "Stream"} is substituted + * to {@code }. + * @param logger the logger to log through + * @param stream a quic stream that may have errors + * @param name a name for the stream, e.g {@code "Control stream"}, or {@code null}. + */ + public static void debugErrorCode(Logger logger, QuicStream stream, String name) { + if (logger.on()) { + var errorCodeStr = errorCodeAsString(stream); + if (errorCodeStr.isPresent()) { + var what = (name == null || name.isEmpty()) ? "Stream" : name; + logger.log("%s %s: %s", what, stream.streamId(), errorCodeStr.get()); + } + } + } + + public static boolean isReserved(long streamType) { + return streamType % 31 == 2 && streamType > 31; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java new file mode 100644 index 00000000000..5db767902f9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/PeerUniStreamDispatcher.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; + +/** + * A class that analyzes the first byte of the stream to figure + * out where to dispatch it. + */ +public abstract class PeerUniStreamDispatcher { + private final QuicStreamIntReader reader; + private final QuicReceiverStream stream; + private final CompletableFuture cf = new MinimalFuture<>(); + + /** + * Creates a {@code PeerUniStreamDispatcher} for the given stream. + * @param stream a new unidirectional stream opened by the peer + */ + protected PeerUniStreamDispatcher(QuicReceiverStream stream) { + this.reader = new QuicStreamIntReader(checkStream(stream), debug()); + this.stream = stream; + } + + private static QuicReceiverStream checkStream(QuicReceiverStream stream) { + if (!stream.isRemoteInitiated()) { + throw new IllegalArgumentException("stream " + stream.streamId() + " is not peer initiated"); + } + if (stream.isBidirectional()) { + throw new IllegalArgumentException("stream " + stream.streamId() + " is not unidirectional"); + } + return stream; + } + + /** + * {@return a completable future that will contain the dispatched stream, + * once dispatched, or a throwable if dispatching the stream failed} + */ + public CompletableFuture dispatchCF() { + return cf; + } + + // The dispatch function. + private void dispatch(Long result, Throwable error) { + if (result != null && result == Http3Streams.PUSH_STREAM_CODE) { + reader.readInt().whenComplete(this::dispatchPushStream); + return; + } + reader.stop(); + if (result != null) { + cf.complete(stream); + if (Http3Streams.isReserved(result)) { + // reserved stream type, 0x1f * N + 0x21 + reservedStreamType(result, stream); + return; + } + if (result < 0) { + debug().log("stream %s EOF, cannot dispatch!", + stream.streamId()); + abandon(); + } + if (result > Integer.MAX_VALUE) { + unknownStreamType(result, stream); + return; + } + int code = (int)(long)result; + switch (code) { + case Http3Streams.CONTROL_STREAM_CODE -> { + controlStream("peer control stream", StreamType.CONTROL); + } + case Http3Streams.QPACK_ENCODER_STREAM_CODE -> { + qpackEncoderStream("peer qpack encoder stream", StreamType.QPACK_ENCODER); + } + case Http3Streams.QPACK_DECODER_STREAM_CODE -> { + qpackDecoderStream("peer qpack decoder stream", StreamType.QPACK_DECODER); + } + default -> { + unknownStreamType(code, stream); + } + } + } else if (error instanceof IOException io) { + if (stream.receivingState().isReset()) { + debug().log("stream %s %s before stream type received, cannot dispatch!", + stream.streamId(), stream.receivingState()); + // RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-10 + // > A receiver MUST tolerate unidirectional streams being closed or reset + // > prior to the reception of the unidirectional stream header + cf.complete(stream); + abandon(); + return; + } + abort(io); + } else { + // We shouldn't come here, so if we do, it's closer to an + // internal error than a stream creation error. + abort(error); + } + } + + private void dispatchPushStream(Long result, Throwable error) { + reader.stop(); + if (result != null) { + cf.complete(stream); + if (result < 0) { + debug().log("stream %s EOF, cannot dispatch!", + stream.streamId()); + abandon(); + } else { + pushStream("push stream", StreamType.PUSH, result); + } + } else if (error instanceof IOException io) { + if (stream.receivingState().isReset()) { + debug().log("stream %s %s before push stream ID received, cannot dispatch!", + stream.streamId(), stream.receivingState()); + // RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-10 + // > A receiver MUST tolerate unidirectional streams being closed or reset + // > prior to the reception of the unidirectional stream header + cf.complete(stream); + abandon(); + return; + } + abort(io); + } else { + // We shouldn't come here, so if we do, it's closer to an + // internal error than a stream creation error. + abort(error); + } + + } + + // dispatches the peer control stream + private void controlStream(String description, StreamType type) { + assert type.code() == Http3Streams.CONTROL_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onControlStreamCreated(description, stream); + } + + // dispatches the peer encoder stream + private void qpackEncoderStream(String description, StreamType type) { + assert type.code() == Http3Streams.QPACK_ENCODER_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onEncoderStreamCreated(description, stream); + } + + // dispatches the peer decoder stream + private void qpackDecoderStream(String description, StreamType type) { + assert type.code() == Http3Streams.QPACK_DECODER_STREAM_CODE; + debug().log("dispatching %s %s(%s)", description, type, type.code()); + onDecoderStreamCreated(description, stream); + } + + // dispatches a push stream initiated by the peer + private void pushStream(String description, StreamType type, long pushId) { + assert type.code() == Http3Streams.PUSH_STREAM_CODE; + debug().log("dispatching %s %s(%s, %s)", description, type, type.code(), pushId); + onPushStreamCreated(description, stream, pushId); + } + + // dispatches a stream whose stream type was recognized as a reserved stream type + private void reservedStreamType(long code, QuicReceiverStream stream) { + onReservedStreamType(code, stream); + } + + // dispatches a stream whose stream type was not recognized + private void unknownStreamType(long code, QuicReceiverStream stream) { + onUnknownStreamType(code, stream); + // if an exception is thrown above, abort will be called. + } + + /** + * {@return the debug logger that should be used} + */ + protected abstract Logger debug(); + + /** + * Starts the dispatcher. + * @apiNote + * The dispatcher should be explicitly started after + * creating the dispatcher. + */ + protected void start() { + reader.readInt().whenComplete(this::dispatch); + } + + /** + * This method disconnects the reader, stops the dispatch, and unless + * the stream type could be decoded and was a {@linkplain Http3Streams#isReserved(long) + * reserved type}, calls {@link #onStreamAbandoned(QuicReceiverStream)} + */ + protected void abandon() { + onStreamAbandoned(stream); + } + + /** + * Aborts the dispatch - for instance, if the stream type + * can't be read, or isn't recognized. + *

        + * This method requests the peer to stop sending this stream, + * and completes the {@link #dispatchCF() dispatchCF} exceptionally + * with the provided throwable. + * + * @param throwable the reason for aborting the dispatch + */ + private void abort(Throwable throwable) { + try { + var debug = debug(); + if (debug.on()) debug.log("aborting dispatch: " + throwable, throwable); + if (!stream.receivingState().isReset() && !stream.isStopSendingRequested()) { + stream.requestStopSending(Http3Error.H3_INTERNAL_ERROR.code()); + } + } finally { + abandon(); + cf.completeExceptionally(throwable); + } + } + + /** + * Called when a reserved stream type is read off the + * stream. + * + * @implSpec + * The default implementation of this method calls + * {@snippet : + * stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + * } + * + * @param code the unrecognized stream type + * @param stream the peer initiated stream + */ + protected void onReservedStreamType(long code, QuicReceiverStream stream) { + debug().log("Ignoring reserved stream type %s", code); + stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + } + + /** + * Called when an unrecognized stream type is read off the + * stream. + * + * @implSpec + * The default implementation of this method calls + * {@snippet : + * stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + * abandon(); + * } + * + * @param code the unrecognized stream type + * @param stream the peer initiated stream + */ + protected void onUnknownStreamType(long code, QuicReceiverStream stream) { + debug().log("Ignoring unknown stream type %s", code); + stream.requestStopSending(Http3Error.H3_STREAM_CREATION_ERROR.code()); + abandon(); + } + + /** + * Called after disconnecting to abandon a peer initiated stream. + * @param stream a peer initiated stream which was abandoned due to having an + * unknown type, or which was abandoned due to being reset + * before being dispatched. + * @apiNote + * A subclass may want to override this method in order to, e.g, emit a + * QPack Stream Cancellation instruction; + * See https://www.rfc-editor.org/rfc/rfc9204.html#name-abandonment-of-a-stream + */ + protected void onStreamAbandoned(QuicReceiverStream stream) {} + + /** + * Called after disconnecting to handle the peer control stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer control stream + */ + protected abstract void onControlStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle the peer encoder stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer encoder stream + */ + protected abstract void onEncoderStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle the peer decoder stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream the peer decoder stream + */ + protected abstract void onDecoderStreamCreated(String description, QuicReceiverStream stream); + + /** + * Called after disconnecting to handle a peer initiated push stream. + * The stream type has already been read off the stream. + * @param description a brief description of the stream for logging purposes + * @param stream a peer initiated push stream + */ + protected abstract void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java new file mode 100644 index 00000000000..bff353a648a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QueuingStreamPair.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +/** + * A class that models a pair of unidirectional streams, where + * data to be written is simply submitted to a queue. + */ +public class QueuingStreamPair extends UniStreamPair { + + // a queue of ByteBuffers submitted for writing. + protected final ConcurrentLinkedQueue writerQueue; + + /** + * Creates a new {@code QueuingStreamPair} for the given HTTP/3 {@code streamType}. + * Valid values for {@code streamType} are {@link StreamType#CONTROL}, + * {@link StreamType#QPACK_ENCODER}, and {@link StreamType#QPACK_DECODER}. + *

        + * This class implements a read loop and a write loop. + *

        + * The read loop will call the given {@code receiver} + * whenever a {@code ByteBuffer} is received. + *

        + * Data can be written to the stream simply by {@linkplain + * #submitData(ByteBuffer) submitting} it to the + * internal unbounded queue managed by this stream. + * When the stream becomes writable, the write loop is invoked and all + * pending data in the queue is written to the stream, until the stream + * is blocked or the queue is empty. + * + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param errorHandler the error handler invoked in case of read errors + * @param logger the debug logger + */ + public QueuingStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + StreamErrorHandler errorHandler, + Logger logger) { + // initialize writer queue before the parent constructor starts the writer loop + writerQueue = new ConcurrentLinkedQueue<>(); + super(streamType, quicConnection, receiver, errorHandler, logger); + } + + /** + * {@return the available credit, taking into account data that has + * not been submitted yet} + * This is only weakly consistent. + */ + public long credit() { + var writer = localWriter(); + long credit = (writer == null) ? 0 : writer.credit(); + if (writerQueue.isEmpty()) return credit; + return credit - writerQueue.stream().mapToLong(Buffer::remaining).sum(); + } + + /** + * Submit data to be written to the sending stream via this + * object's internal queue. + * @param buffer the data to submit + */ + public final void submitData(ByteBuffer buffer) { + writerQueue.offer(buffer); + localWriteScheduler().runOrSchedule(); + } + + // The local control stream write loop + @Override + void localWriterLoop() { + var writer = localWriter(); + if (writer == null) return; + assert !(writer instanceof QueuingWriter); + ByteBuffer buffer; + if (debug.on()) + debug.log("start control writing loop: credit=" + writer.credit()); + while (writer.credit() > 0 && (buffer = writerQueue.poll()) != null) { + try { + if (debug.on()) + debug.log("schedule %s bytes for writing on control stream", buffer.remaining()); + writer.scheduleForWriting(buffer, buffer == QuicStreamReader.EOF); + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to write to control stream", t); + } + errorHandler.onError(writer.stream(), this, t); + } + } + } + + @Override + QuicStreamWriter wrap(QuicStreamWriter writer) { + return new QueuingWriter(writer); + } + + /** + * A class that wraps the actual {@code QuicStreamWriter} + * and redirect everything to the QueuingStreamPair's + * writerQueue - so that data is not sent out of order. + */ + class QueuingWriter extends QuicStreamWriter { + final QuicStreamWriter writer; + QueuingWriter(QuicStreamWriter writer) { + super(QueuingStreamPair.this.localWriteScheduler()); + this.writer = writer; + } + + @Override + public SendingStreamState sendingState() { + return writer.sendingState(); + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + if (!last || buffer.hasRemaining()) submitData(buffer); + if (last) submitData(QuicStreamReader.EOF); + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + QueuingStreamPair.this.writerQueue.offer(buffer); + } + + @Override + public long credit() { + return QueuingStreamPair.this.credit(); + } + + @Override + public void reset(long errorCode) throws IOException { + writer.reset(errorCode); + } + + @Override + public QuicSenderStream stream() { + return writer.stream(); + } + + @Override + public boolean connected() { + return writer.connected(); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java new file mode 100644 index 00000000000..a9101b48ad9 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/QuicStreamIntReader.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.http3.streams; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; + +/** + * A class that reads VL integers from a QUIC stream. + *

        + * After constructing an instance of this class, the application + * is can call {@link #readInt()} to read one VL integer off the stream. + * When the read operation completes, the application can call {@code readInt} + * again, or call {@link #stop()} to disconnect the reader. + */ +final class QuicStreamIntReader { + private final SequentialScheduler scheduler = SequentialScheduler.lockingScheduler(this::dispatch); + private final QuicReceiverStream stream; + private final QuicStreamReader reader; + private final Logger debug; + private CompletableFuture cf; + private ByteBuffer vlongBuf; // accumulate bytes until stream type can be decoded + + /** + * Creates a {@code QuicStreamIntReader} for the given stream. + * @param stream a receiver stream with no connected reader + * @param debug a logger + */ + public QuicStreamIntReader(QuicReceiverStream stream, Logger debug) { + this.stream = stream; + this.reader = stream.connectReader(scheduler); + this.debug = debug; + debug.log("int reader created for stream " + stream.streamId()); + } + + // The read loop. Attempts to read a VL int, and completes the CF when done. + private void dispatch() { + if (cf == null) return; // not reading anything at the moment + try { + ByteBuffer buffer; + while ((buffer = reader.peek()) != null) { + if (buffer == QuicStreamReader.EOF) { + debug.log("stream %s EOF, cannot complete!", + stream.streamId()); + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(-1L); + return; + } + if (buffer.remaining() == 0) { + var polled = reader.poll(); + assert buffer == polled; + continue; + } + if (vlongBuf == null) { + long vlong = VariableLengthEncoder.decode(buffer); + if (vlong >= 0) { + // happy case: we have enough bytes in the buffer + if (buffer.remaining() == 0) { + var polled = reader.poll(); + assert buffer == polled; + } + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(vlong); + return; + } + // we don't have enough bytes: start accumulating them + int vlongSize = VariableLengthEncoder.peekEncodedValueSize(buffer, buffer.position()); + assert vlongSize > 0 && vlongSize <= VariableLengthEncoder.MAX_INTEGER_LENGTH + : vlongSize + " is out of bound for a variable integer size (should be in [1..8]"; + assert buffer.remaining() < vlongSize; + vlongBuf = ByteBuffer.allocate(vlongSize); + vlongBuf.put(buffer); + assert buffer.remaining() == 0; + var polled = reader.poll(); + assert polled == buffer; + // continue and wait for more + } else { + // there wasn't enough bytes the first time around, accumulate + // missing bytes + int missing = vlongBuf.remaining(); + int available = Math.min(missing, buffer.remaining()); + for (int i = 0; i < available; i++) { + vlongBuf.put(buffer.get()); + } + // if we have exhausted the buffer, poll it. + if (!buffer.hasRemaining()) { + var polled = reader.poll(); + assert polled == buffer; + } + // if we have all bytes, we can proceed and decode the stream type + if (!vlongBuf.hasRemaining()) { + vlongBuf.flip(); + long vlong = VariableLengthEncoder.decode(vlongBuf); + assert !vlongBuf.hasRemaining(); + vlongBuf = null; + assert vlong >= 0; + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.complete(vlong); + return; + } // otherwise, wait for more + } + } + } catch (Throwable throwable) { + CompletableFuture cf0; + synchronized (this) { + cf0 = cf; + cf = null; + } + cf0.completeExceptionally(throwable); + } + } + + /** + * Stops and disconnects this reader. This operation must not be done when a read operation + * is in progress. If cancelling a read operation is intended, use + * {@link QuicReceiverStream#requestStopSending(long)}. + * @throws IllegalStateException if a read operation is currently in progress. + */ + public synchronized void stop() { + if (cf != null) { + // if a read is in progress, some bytes might have been read + // off the stream already, and stopping the reader could corrupt the data. + throw new IllegalStateException("Reading in progress"); + } + if (!reader.connected()) return; + stream.disconnectReader(reader); + scheduler.stop(); + } + + /** + * Starts a read operation to decode a single number. + * @return a {@link CompletableFuture} that will be completed + * with the decoded number, or -1 if the stream is terminated before + * the complete number could be read, or an exception + * if the stream is reset or decoding fails. + * @throws IllegalStateException if the reader is stopped, or if a read + * operation is already in progress + */ + public synchronized CompletableFuture readInt() { + if (cf != null) { + throw new IllegalStateException("Read in progress"); + } + if (!reader.connected()) { + throw new IllegalStateException("Reader stopped"); + } + var cf0 = cf = new MinimalFuture<>(); + reader.start(); + scheduler.runOrSchedule(); + return cf0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java new file mode 100644 index 00000000000..403b26f244d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/http3/streams/UniStreamPair.java @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.http3.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +import jdk.internal.net.http.Http3Connection; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; + +/** + * A class that models a pair of HTTP/3 unidirectional streams. + * This class implements a read loop that calls a {@link + * #UniStreamPair(StreamType, QuicConnection, Consumer, Runnable, StreamErrorHandler, Logger) + * receiver} every time a {@code ByteBuffer} is read from + * the receiver part. + * The {@linkplain #futureSenderStreamWriter() sender stream writer}, + * when available, can be used to write to the sender part. + * The {@link #UniStreamPair(StreamType, QuicConnection, Consumer, Runnable,StreamErrorHandler, Logger) + * writerLoop} is invoked whenever the writer part becomes unblocked, and + * writing can be resumed. + *

        + * @apiNote + * The creator of the stream pair (typically {@link Http3Connection}) is expected + * to complete the {@link #futureReceiverStream()} completable future when the remote + * part of the stream pair is created by the remote peer. This class will not + * listen directly for creation of new remote streams. + *

        + * The {@link QueuingStreamPair} class is a subclass of this class which + * implements a writer loop over an unbounded queue of {@code ByteBuffer}, and + * can be used when unlimited buffering of data for writing is not an issue. + */ +public class UniStreamPair { + + // The sequential scheduler for the local control stream (LCS) writer loop + private final SequentialScheduler localWriteScheduler; + // The QuicStreamWriter for the local control stream + private volatile QuicStreamWriter localWriter; + private final CompletableFuture streamWriterCF; + // A completable future that will be completed when the local sender + // stream is opened and the stream type has been queued to the + // writer queue. + private volatile CompletableFuture localSenderStreamCF; + + // The sequential scheduler for the peer receiver stream (PRS) reader loop + final SequentialScheduler peerReadScheduler = + SequentialScheduler.lockingScheduler(this::peerReaderLoop); + // The QuicStreamReader for the peer control stream + volatile QuicStreamReader peerReader; + private final CompletableFuture streamReaderCF; + // A completable future that will be completed when the peer opens + // the receiver part of the stream pair + private final CompletableFuture peerReceiverStreamCF = new MinimalFuture<>(); + private final ReentrantLock lock = new ReentrantLock(); + + + private final StreamType localStreamType; // The HTTP/3 stream type of the sender part + private final StreamType remoteStreamType; // The HTTP/3 stream type of the receiver part + private final QuicConnection quicConnection; // the underlying quic connection + private final Consumer receiver; // called when a ByteBuffer is received + final StreamErrorHandler errorHandler; // used by QueuingStreamPair + final Logger debug; // the debug logger + + /** + * Creates a new {@code UniStreamPair} for the given HTTP/3 {@code streamType}. + * Valid values for {@code streamType} are {@link StreamType#CONTROL}, + * {@link StreamType#QPACK_ENCODER}, and {@link StreamType#QPACK_DECODER}. + *

        + * This class implements a read loop that will call the given {@code receiver} + * whenever a {@code ByteBuffer} is received. + *

        + * Writing to the sender part can be done by interacting directly with + * the writer. If the writer is blocked due to flow control, and becomes + * unblocked again, the {@code writeLoop} is invoked. + * The {@link QueuingStreamPair} subclass provides a convenient implementation + * of a {@code writeLoop} based on an unbounded queue of {@code ByteBuffer}. + * + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param writerLoop the writer loop + * @param errorHandler the error handler invoked in case of read errors + * @param logger the debug logger + */ + public UniStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + Runnable writerLoop, + StreamErrorHandler errorHandler, + Logger logger) { + this(local(streamType), remote(streamType), + Objects.requireNonNull(quicConnection), + Objects.requireNonNull(receiver), + Optional.of(writerLoop), + Objects.requireNonNull(errorHandler), + Objects.requireNonNull(logger)); + } + + /** + * A constructor used by the {@link QueuingStreamPair} subclass + * @param streamType the HTTP/3 stream type + * @param quicConnection the underlying Quic connection + * @param receiver the receiver callback + * @param errorHandler the error handler invoked in case + * of read or write errors + * @param logger + */ + UniStreamPair(StreamType streamType, + QuicConnection quicConnection, + Consumer receiver, + StreamErrorHandler errorHandler, + Logger logger) { + this(local(streamType), remote(streamType), + Objects.requireNonNull(quicConnection), + Objects.requireNonNull(receiver), + Optional.empty(), + errorHandler, + Objects.requireNonNull(logger)); + } + + // all constructors delegate here + private UniStreamPair(StreamType localStreamType, + StreamType remoteStreamType, + QuicConnection quicConnection, + Consumer receiver, + Optional writerLoop, + StreamErrorHandler errorHandler, + Logger logger) { + assert this.getClass() != UniStreamPair.class + || writerLoop.isPresent(); + this.debug = logger; + this.localStreamType = localStreamType; + this.remoteStreamType = remoteStreamType; + this.quicConnection = quicConnection; + this.receiver = receiver; + this.errorHandler = errorHandler; + var localWriterLoop = writerLoop.orElse(this::localWriterLoop); + this.localWriteScheduler = + SequentialScheduler.lockingScheduler(localWriterLoop); + this.streamWriterCF = startSending(); + this.streamReaderCF = startReceiving(); + } + + private static StreamType local(StreamType localStreamType) { + return switch (localStreamType) { + case CONTROL -> localStreamType; + case QPACK_ENCODER -> localStreamType; + case QPACK_DECODER -> localStreamType; + default -> throw new IllegalArgumentException(localStreamType + + " cannot be part of a stream pair"); + }; + } + + private static StreamType remote(StreamType localStreamType) { + return switch (localStreamType) { + case CONTROL -> localStreamType; + case QPACK_ENCODER -> StreamType.QPACK_DECODER; + case QPACK_DECODER -> StreamType.QPACK_ENCODER; + default -> throw new IllegalArgumentException(localStreamType + + " cannot be part of a stream pair"); + }; + } + + /** + * {@return the HTTP/3 stream type of the sender part of the stream pair} + */ + public final StreamType localStreamType() { + return localStreamType; + } + + /** + * {@return the HTTP/3 stream type of the receiver part of the stream pair} + */ + public final StreamType remoteStreamType() { + return remoteStreamType; + } + + /** + * {@return a completable future that will be completed with a writer connected + * to the sender part of this stream pair after the local HTTP/3 stream type + * has been queued for writing on the writing queue} + */ + public final CompletableFuture futureSenderStreamWriter() { + return streamWriterCF; + } + + /** + * {@return a completable future that will be completed with a reader connected + * to the receiver part of this stream pair after the remote HTTP/3 stream + * type has been read off the remote initiated stream} + */ + public final CompletableFuture futureReceiverStreamReader() { + return streamReaderCF; + } + + /** + * {@return a completable future that will be completed with the sender part + * of this stream pair after the local HTTP/3 stream type + * has been queued for writing on the writing queue} + */ + public CompletableFuture futureSenderStream() { + return localSenderStream(); + } + + /** + * {@return a completable future that will be completed with the receiver part + * of this stream pair after the remote HTTP/3 stream type has been read off + * the remote initiated stream} + */ + public CompletableFuture futureReceiverStream() { + return peerReceiverStreamCF; + } + + /** + * {@return the scheduler for the local writer loop} + */ + public SequentialScheduler localWriteScheduler() { + return localWriteScheduler; + } + + /** + * {@return the writer connected to the sender part of this stream or + * {@code null} if no writer is connected yet} + */ + public QuicStreamWriter localWriter() {return localWriter; } + + /** + * Stops schedulers. Can be called when the connection is + * closed to stop the reading and writing loops. + */ + public void stopSchedulers() { + peerReadScheduler.stop(); + localWriteScheduler.stop(); + } + + // Hooks for QueuingStreamPair + // ============================ + + /** + * This method is overridden by {@link QueuingStreamPair} to implement + * a writer loop for this stream. It is only called when the concrete + * subclass is {@link QueuingStreamPair}. + */ + void localWriterLoop() { + if (debug.on()) debug.log("writing loop not implemented"); + } + + + /** + * Used by subclasses to redirect queuing of data to the + * subclass queue. + * @param writer the downstream writer + * @return a writer that can be safely used. + */ + QuicStreamWriter wrap(QuicStreamWriter writer) { + return writer; + } + + // Undidirectional Stream Pair Implementation + // ========================================== + + + /** + * This method is called to process bytes received on the peer + * control stream. + * @param buffer the bytes received + */ + private void processPeerControlBytes(ByteBuffer buffer) { + receiver.accept(buffer); + } + + /** + * Creates the local sender stream and queues the stream + * type code in its writer queue. + * @return a completable future that will be completed with the + * local sender stream + */ + private CompletableFuture localSenderStream() { + CompletableFuture lcs = localSenderStreamCF; + if (lcs != null) return lcs; + StreamType type = localStreamType(); + lock.lock(); + try { + if ((lcs = localSenderStreamCF) != null) return lcs; + if (debug.on()) { + debug.log("Opening local stream: %s(%s)", + type, type.code()); + } + // TODO: review this duration + final Duration streamLimitIncreaseDuration = Duration.ZERO; + localSenderStreamCF = lcs = quicConnection + .openNewLocalUniStream(streamLimitIncreaseDuration) + .thenApply( s -> openLocalStream(s, type.code())); + // TODO: use thenApplyAsync with the executor instead + } finally { + lock.unlock(); + } + return lcs; + } + + + /** + * Schedules sending of client settings. + * @return a completable future that will be completed with the + * {@link QuicStreamWriter} allowing to write to the local control + * stream + */ + private CompletableFuture startSending() { + return localSenderStream().thenApply((stream) -> { + if (debug.on()) { + debug.log("stream %s is ready for sending", stream.streamId()); + } + var controlWriter = stream.connectWriter(localWriteScheduler); + localWriter = controlWriter; + localWriteScheduler.runOrSchedule(); + return wrap(controlWriter); + }); + } + + /** + * Schedules the receiving of server settings + * @return a completable future that will be completed with the + * {@link QuicStreamReader} allowing to read from the remote control + * stream. + */ + private CompletableFuture startReceiving() { + if (debug.on()) { + debug.log("prepare to receive"); + } + return peerReceiverStreamCF.thenApply(this::connectReceiverStream); + } + + /** + * Connects the peer control stream reader and + * schedules the receiving of the peer settings from the given + * {@code peerControlStream}. + * @param peerControlStream the peer control stream + * @return the peer control stream reader + */ + private QuicStreamReader connectReceiverStream(QuicReceiverStream peerControlStream) { + var reader = peerControlStream.connectReader(peerReadScheduler); + var streamType = remoteStreamType(); + if (debug.on()) { + debug.log("peer %s stream reader connected (stream %s)", + streamType, peerControlStream.streamId()); + } + peerReader = reader; + reader.start(); + return reader; + } + + // The peer receiver stream reader loop + private void peerReaderLoop() { + var reader = peerReader; + if (reader == null) return; + ByteBuffer buffer; + long bytes = 0; + var streamType = remoteStreamType(); + try { + // TODO: Revisit: if the underlying quic connection is closed + // by the peer, we might get a ClosedChannelException from poll() + // here before the upper layer connection (HTTP/3 connection) is + // marked closed. + if (debug.on()) { + debug.log("start reading from peer %s stream", streamType); + } + while ((buffer = reader.poll()) != null) { + final int remaining = buffer.remaining(); + if (remaining == 0 && buffer != QuicStreamReader.EOF) { + continue; // not yet EOF, so poll more + } + bytes += remaining; + processPeerControlBytes(buffer); + if (buffer == QuicStreamReader.EOF) { + // a EOF was processed, don't poll anymore + break; + } + } + if (debug.on()) { + debug.log("stop reading peer %s stream after %s bytes", + streamType, bytes); + } + } catch (IOException | RuntimeException | Error throwable) { + if (debug.on()) { + debug.log("Reading peer %s stream failed: %s", streamType, throwable); + } + // call the error handler and pass it the stream on which the error happened + errorHandler.onError(reader.stream(), this, throwable); + } + } + + /** + * Queues the given HTTP/3 stream type code on the given local unidirectional + * stream writer queue. + * @param stream a new local unidirectional stream + * @param code the code to queue up on the stream writer queue + * @return the given {@code stream} + */ + private QuicSenderStream openLocalStream(QuicSenderStream stream, int code) { + var streamType = localStreamType(); + if (debug.on()) { + debug.log("Opening local stream: %s %s(code=%s)", + stream.streamId(), streamType, code); + } + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = stream.connectWriter(scheduler); + try { + if (debug.on()) { + debug.log("Writing local stream type: stream %s %s(code=%s)", + stream.streamId(), streamType, code); + } + var buffer = ByteBuffer.allocate(VariableLengthEncoder.getEncodedSize(code)); + VariableLengthEncoder.encode(buffer, code); + buffer.flip(); + writer.queueForWriting(buffer); + scheduler.stop(); + stream.disconnectWriter(writer); + } catch (Throwable t) { + if (debug.on()) { + debug.log("failed to create stream %s %s(code=%s): %s", + stream.streamId(), streamType, code, t); + } + try { + switch (streamType) { + case CONTROL, QPACK_ENCODER, QPACK_DECODER -> { + final String logMsg = "stream %s %s(code=%s)" + .formatted(stream.streamId(), streamType, code); + // TODO: revisit - we should probably invoke a method + // on the HttpQuicConnection or H3Connection instead of + // dealing directly with QuicConnection here. + final TerminationCause terminationCause = + appLayerClose(H3_STREAM_CREATION_ERROR.code()).loggedAs(logMsg); + quicConnection.connectionTerminator().terminate(terminationCause); + } + default -> writer.reset(H3_STREAM_CREATION_ERROR.code()); + } + } catch (Throwable suppressed) { + if (debug.on()) { + debug.log("couldn't close connection or reset stream: " + suppressed); + } + Utils.addSuppressed(t, suppressed); + throw new CompletionException(t); + } + } + return stream; + } + + public static interface StreamErrorHandler { + + /** + * Will be invoked when there is an error on a {@code QuicStream} handled by + * the {@code UniStreamPair} + * + * @param stream the stream on which the error occurred + * @param uniStreamPair the UniStreamPair to which the stream belongs + * @param error the error that occurred + */ + void onError(QuicStream stream, UniStreamPair uniStreamPair, Throwable error); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java new file mode 100644 index 00000000000..8487100c8ca --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Decoder.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPACK.StreamPairSupplier; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.DecoderInstructionsWriter; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.http3.frames.SettingsFrame.DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.qpack.DynamicTable.ENTRY_SIZE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Decodes headers from their binary representation. + * + *

        Typical lifecycle looks like this: + * + *

        {@link #Decoder(StreamPairSupplier, QPACKErrorHandler) new Decoder} + * ({@link #configure(ConnectionSettings)} called once from our HTTP/3 settings + * {@link #decodeHeader(ByteBuffer, boolean, HeaderFrameReader) decodeHeader} + * + *

        {@code Decoder} does not require a complete header block in a single + * {@code ByteBuffer}. The header block can be spread across many buffers of any + * size and decoded one-by-one the way it makes most sense for the user. This + * way also allows not to limit the size of the header block. + * + *

        Headers are delivered to the {@linkplain DecodingCallback callback} as + * soon as they become decoded. Using the callback also gives the user freedom + * to decide how headers are processed. The callback does not limit the number + * of headers decoded during single decoding operation. + */ + +public final class Decoder { + + private final QPACK.Logger logger; + private final DynamicTable dynamicTable; + private final EncoderInstructionsReader encoderInstructionsReader; + private final QueuingStreamPair decoderStreamPair; + private static final AtomicLong DECODERS_IDS = new AtomicLong(); + // ID of last acknowledged entry acked by Insert Count Increment + // or section acknowledgement instruction + private long acknowledgedInsertsCount; + private final ReentrantLock ackInsertCountLock = new ReentrantLock(); + private final AtomicLong blockedStreamsCounter = new AtomicLong(); + private volatile long maxBlockedStreams; + private final QPACKErrorHandler qpackErrorHandler; + private volatile long maxFieldSectionSize = DEFAULT_SETTINGS_MAX_FIELD_SECTION_SIZE; + private final AtomicLong concurrentDynamicTableInsertions = + new AtomicLong(); + private static final long MAX_LITERAL_WITH_INDEXING = + Utils.getIntegerNetProperty("jdk.httpclient.maxLiteralWithIndexing", 512); + + /** + * Constructs a {@code Decoder} with zero initial capacity of the dynamic table. + * + *

        Dynamic table capacity values has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK. + *

        Maximum dynamic table capacity is determined by the value of SETTINGS_QPACK_MAX_TABLE_CAPACITY + * HTTP/3 setting sent by the decoder side (see + * + * 3.2.3. Maximum Dynamic Table Capacity). + *

        An encoder informs the decoder of a change to the dynamic table capacity using the + * "Set Dynamic Table Capacity" instruction + * (see + * 4.3.1. Set Dynamic Table Capacity) + * + * @see Decoder#configure(ConnectionSettings) + */ + public Decoder(StreamPairSupplier streams, QPACKErrorHandler errorHandler) { + long id = DECODERS_IDS.incrementAndGet(); + logger = QPACK.getLogger().subLogger("Decoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> "New decoder"); + } + dynamicTable = new DynamicTable(logger.subLogger("DynamicTable"), false); + decoderStreamPair = streams.create(this::processEncoderInstruction); + qpackErrorHandler = errorHandler; + encoderInstructionsReader = new EncoderInstructionsReader(new DecoderTableCallback(), logger); + } + + public QueuingStreamPair decoderStreams() { + return decoderStreamPair; + } + + /** + * {@return a new {@link HeaderFrameReader} that will hold the decoding + * state for a new request/response stream} + */ + public HeaderFrameReader newHeaderFrameReader(DecodingCallback decodingCallback) { + return new HeaderFrameReader(dynamicTable, decodingCallback, + blockedStreamsCounter, maxBlockedStreams, + maxFieldSectionSize, logger); + } + + public void ackTableInsertions() { + ackInsertCountLock.lock(); + try { + long insertCount = dynamicTable.insertCount(); + assert acknowledgedInsertsCount <= insertCount; + long incrementValue = insertCount - acknowledgedInsertsCount; + if (incrementValue > 0) { + // Write "Insert Count Increment" to the decoder stream + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instructionSize = decoderInstructionsWriter.configureForInsertCountInc(incrementValue); + submitDecoderInstruction(decoderInstructionsWriter, instructionSize); + } + // Update lastAck value + acknowledgedInsertsCount = insertCount; + } finally { + ackInsertCountLock.unlock(); + } + } + + /** + * Submit "Section Acknowledgment" instruction to the decoder stream. + * A field line section needs to be acknowledged after completion of + * section decoding. + * @param streamId stream ID associated with the field section's + * @param headerFrameReader header frame reader used to read + * the field line section + */ + public void ackSection(long streamId, HeaderFrameReader headerFrameReader) { + + FieldSectionPrefix prefix = headerFrameReader.decodedSectionPrefix(); + + // 4.4.1. Section Acknowledgment: If an encoder receives a Section Acknowledgment instruction + // referring to a stream on which every encoded field section with a non-zero Required Insert + // Count has already been acknowledged, this MUST be treated as a connection error of type + // QPACK_DECODER_STREAM_ERROR. + long prefixInsertCount = prefix.requiredInsertCount(); + if (prefixInsertCount == 0) return; + ackInsertCountLock.lock(); + try { + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instrSize = decoderInstructionsWriter.configureForSectionAck(streamId); + submitDecoderInstruction(decoderInstructionsWriter, instrSize); + if (prefixInsertCount > acknowledgedInsertsCount) { + acknowledgedInsertsCount = prefixInsertCount; + } + } finally { + ackInsertCountLock.unlock(); + } + } + + public void cancelStream(long streamId) { + var decoderInstructionsWriter = new DecoderInstructionsWriter(); + int instrSize = decoderInstructionsWriter.configureForStreamCancel(streamId); + submitDecoderInstruction(decoderInstructionsWriter, instrSize); + dynamicTable.cleanupStreamInsertCountNotifications(streamId); + } + + /** + * Configures maximum capacity of the decoder's dynamic table based on connection settings of + * the HTTP client, also configures the number of allowed blocked streams. + * The decoder's dynamic table capacity can only be changed via + * {@linkplain EncoderInstructionsReader.Callback encoder instructions callback}. + * + * @param ourSettings connection settings + */ + public void configure(ConnectionSettings ourSettings) { + long maxCapacity = ourSettings.qpackMaxTableCapacity(); + dynamicTable.setMaxTableCapacity(maxCapacity); + maxBlockedStreams = ourSettings.qpackBlockedStreams(); + long maxFieldSS = ourSettings.maxFieldSectionSize(); + if (maxFieldSS > 0) { + maxFieldSectionSize = maxFieldSS; + } else { + // Unlimited field section size + maxFieldSectionSize = -1L; + } + } + + /** + * Decodes a header block from the given buffer to the given callback. + * + *

        Suppose a header block is represented by a sequence of + * {@code ByteBuffer}s in the form of {@code Iterator}. And the + * consumer of decoded headers is represented by {@linkplain DecodingCallback the callback} + * registered within the provided {@code headerFrameReader}. + * Then to decode the header block, the following approach might be used: + * {@snippet : + * HeaderFrameReader headerFrameReader = + * newHeaderFrameReader(decodingCallback); + * while (buffers.hasNext()) { + * ByteBuffer input = buffers.next(); + * decoder.decodeHeader(input, !buffers.hasNext(), headerFrameReader); + * } + * } + * + *

        The decoder reads as much as possible of the header block from the + * given buffer, starting at the buffer's position, and increments its + * position to reflect the bytes read. The buffer's mark and limit will not + * be modified. + * + *

        Once the method is invoked with {@code endOfHeaderBlock == true}, the + * current header block is deemed ended, and inconsistencies, if any, are + * reported immediately via a callback registered within the {@code + * headerFrameReader} instance. + * + *

        Each callback method is called only after the implementation has + * processed the corresponding bytes. If the bytes revealed a decoding + * error it is reported via a callback registered within the {@code + * headerFrameReader} instance. + * + * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of + * returning it for two reasons. The first one is that the user of the + * decoder always knows which chunk is the last. The second one is to throw + * the most detailed exception possible, which might be useful for + * diagnosing issues. + * + * @implNote This implementation is not atomic in respect to decoding + * errors. In other words, if the decoding operation has thrown a decoding + * error, the decoder is no longer usable. + * + * @param headerBlock + * the chunk of the header block, may be empty + * @param endOfHeaderBlock + * true if the chunk is the final (or the only one) in the sequence + * @param headerFrameReader the stateful header frame reader + * @throws NullPointerException + * if either {@code headerBlock} or {@code headerFrameReader} are null + */ + public void decodeHeader(ByteBuffer headerBlock, boolean endOfHeaderBlock, + HeaderFrameReader headerFrameReader) { + requireNonNull(headerFrameReader, "headerFrameReader"); + headerFrameReader.read(headerBlock, endOfHeaderBlock); + } + + /** + * This method is invoked when the {@linkplain + * UniStreamPair#futureReceiverStreamReader() decoder's stream reader} + * has data available for reading. + */ + private void processEncoderInstruction(ByteBuffer buffer) { + if (buffer == QuicStreamReader.EOF) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + qpackErrorHandler.closeOnError( + new ProtocolException("QPACK " + decoderStreamPair.remoteStreamType() + + " remote stream was unexpectedly closed"), H3_CLOSED_CRITICAL_STREAM); + return; + } + try { + int stringLengthLimit = Math.clamp(dynamicTable.capacity() - ENTRY_SIZE, + 0, Integer.MAX_VALUE - (int) ENTRY_SIZE); + encoderInstructionsReader.read(buffer, stringLengthLimit); + } catch (QPackException qPackException) { + qpackErrorHandler.closeOnError(qPackException.getCause(), qPackException.http3Error()); + } + } + + private void submitDecoderInstruction(DecoderInstructionsWriter decoderInstructionsWriter, + int size) { + if (size > decoderStreamPair.credit()) { + qpackErrorHandler.closeOnError( + new IOException("QPACK not enough credit on a decoder stream " + + decoderStreamPair.remoteStreamType()), H3_CLOSED_CRITICAL_STREAM); + return; + } + // All decoder instructions contain only one variable length integer. + // Which could take up to 9 bytes max. + ByteBuffer buffer = ByteBuffer.allocate(size); + boolean done = decoderInstructionsWriter.write(buffer); + // Assert that instruction is fully written, ie the correct + // instruction size estimation was supplied. + assert done; + buffer.flip(); + decoderStreamPair.submitData(buffer); + } + + void incrementAndCheckDynamicTableInsertsCount() { + if (MAX_LITERAL_WITH_INDEXING > 0) { + long concurrentNumberOfInserts = concurrentDynamicTableInsertions.incrementAndGet(); + if (concurrentNumberOfInserts > MAX_LITERAL_WITH_INDEXING) { + String exceptionMessage = "Too many literal with indexing: %s > %s" + .formatted(concurrentNumberOfInserts, MAX_LITERAL_WITH_INDEXING); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> exceptionMessage); + } + throw QPackException.encoderStreamError(new ProtocolException(exceptionMessage)); + } + } + } + + public void resetInsertionsCounter() { + if (MAX_LITERAL_WITH_INDEXING > 0) { + concurrentDynamicTableInsertions.set(0); + } + } + + private class DecoderTableCallback implements EncoderInstructionsReader.Callback { + + private void ensureInstructionsAllowed() { + // RFC9204 3.2.3. Maximum Dynamic Table Capacity: + // "When the maximum table capacity is zero, the encoder MUST NOT + // insert entries into the dynamic table and MUST NOT send any encoder + // instructions on the encoder stream." + if (dynamicTable.maxCapacity() == 0) { + throw new IllegalStateException("Unexpected encoder instruction"); + } + } + + @Override + public void onCapacityUpdate(long capacity) { + if (capacity == 0 && dynamicTable.maxCapacity() == 0) { + return; + } + ensureInstructionsAllowed(); + dynamicTable.setCapacity(capacity); + } + + @Override + public void onInsert(String name, String value) { + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (dynamicTable.insert(name, value) != DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough evictable space in dynamic table to insert entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + + @Override + public void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString) { + // RFC9204 7.4. Implementation Limits: + // "If an implementation encounters a value larger than it is able to decode, this MUST be + // treated as a stream error of type QPACK_DECOMPRESSION_FAILED if on a request stream or + // a connection error of the appropriate type if on the encoder or decoder stream." + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (dynamicTable.insert(nameIndex, indexInStaticTable, valueString) != + DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough space in dynamic table to insert entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + + @Override + public void onDuplicate(long l) { + // RFC9204 7.4. Implementation Limits: + // "If an implementation encounters a value larger than it is able to decode, this + // MUST be treated as a stream error of type QPACK_DECOMPRESSION_FAILED" + ensureInstructionsAllowed(); + incrementAndCheckDynamicTableInsertsCount(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, + () -> format("Processing duplicate instruction (%d)", l)); + } + if (dynamicTable.duplicate(l) != DynamicTable.ENTRY_NOT_INSERTED) { + ackTableInsertions(); + } else { + // Not enough space in dynamic table to duplicate entry + throw new IllegalStateException("Not enough space in dynamic table"); + } + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java new file mode 100644 index 00000000000..bb9dbdadf59 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DecodingCallback.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; + +import java.nio.ByteBuffer; + +/** + * Delivers results of the {@link Decoder#decodeHeader(ByteBuffer, boolean, HeaderFrameReader)} + * decoding operation. + * + *

        Methods of the callback are never called by a decoder with any of the + * arguments being {@code null}. + * + * @apiNote + * + *

        The callback provides methods for all possible + * + * field line representations. + * + *

        Names and values are {@link CharSequence}s rather than {@link String}s in + * order to allow users to decide whether they need to create objects. A + * {@code CharSequence} might be used in-place, for example, to be appended to + * an {@link Appendable} (e.g. {@link StringBuilder}) and then discarded. + * + *

        That said, if a passed {@code CharSequence} needs to outlast the method + * call, it needs to be copied. + * + */ +public interface DecodingCallback { + + /** + * A method the more specific methods of the callback forward their calls + * to. + * + * @param name + * header name + * @param value + * header value + */ + void onDecoded(CharSequence name, CharSequence value); + + /** + * A header fields decoding is completed. + */ + void onComplete(); + + /** + * A connection-level error observed during the decoding process. + * + * @param throwable a {@code Throwable} instance + * @param http3Error a HTTP3 error code + */ + void onConnectionError(Throwable throwable, Http3Error http3Error); + + /** + * A stream-level error observed during the decoding process. + * + * @param throwable a {@code Throwable} instance + * @param http3Error a HTTP3 error code + */ + default void onStreamError(Throwable throwable, Http3Error http3Error) { + onConnectionError(throwable, http3Error); + } + + /** + * Reports if {@linkplain #onConnectionError(Throwable, Http3Error) a connection} + * or {@linkplain #onStreamError(Throwable, Http3Error) a stream} error has been + * observed during the decoding process + * @return true - if error was observed; false - otherwise + */ + default boolean hasError() { + return false; + } + + /** + * Returns request/response stream id or push stream id associated with a decoding callback. + */ + long streamId(); + + /** + * A more finer-grained version of {@link #onDecoded(CharSequence, + * CharSequence)} that also reports on value sensitivity. + * + *

        Value sensitivity must be considered, for example, when implementing + * an intermediary. A {@code value} is sensitive if it was represented as Literal Header + * Field Never Indexed. + * + * @implSpec + * + *

        The default implementation invokes {@code onDecoded(name, value)}. + * + * @param name + * header name + * @param value + * header value + * @param sensitive + * whether the value is sensitive + */ + default void onDecoded(CharSequence name, + CharSequence value, + boolean sensitive) { + onDecoded(name, value); + } + + /** + * An Indexed + * Field Line decoded. + * + * @implSpec + * + *

        The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of a name/value pair in static or dynamic table + * @param name + * header name + * @param value + * header value + */ + default void onIndexed(long index, CharSequence name, CharSequence value) { + onDecoded(name, value, false); + } + + /** + * A Literal + * Field Line with Name Reference decoded, where a {@code name} was + * referred by an {@code index}. + * + * @implSpec + * + *

        The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + * @param hideIntermediary + * if the header field should be written to intermediary nodes + */ + default void onLiteralWithNameReference(long index, + CharSequence name, + CharSequence value, + boolean valueHuffman, + boolean hideIntermediary) { + onDecoded(name, value, hideIntermediary); + } + + /** + * A Literal Field + * Line with Literal Name decoded, where both a {@code name} and a {@code value} + * were literal. + * + * @implSpec + * + *

        The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralWithLiteralName(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman, + boolean hideIntermediary) { + onDecoded(name, value, hideIntermediary); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java new file mode 100644 index 00000000000..6f5e567e182 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/DynamicTable.java @@ -0,0 +1,1069 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.Encoder.EncodingContext; +import jdk.internal.net.http.qpack.Encoder.SectionReference; +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; + +/* + * The dynamic table to store header fields. Implements dynamic table described + * in "QPACK: Header Compression for HTTP/3" RFC. + * The size of the table is the sum of the sizes of its entries. + */ +public final class DynamicTable implements HeadersTable { + + // QPACK Section 3.2.1: + // The size of an entry is the sum of its name's length in bytes, + // its value's length in bytes, and 32 additional bytes. + public static final long ENTRY_SIZE = 32L; + + // Initial length of the elements array + // It is required for this value to be a power of 2 integer + private static final int INITIAL_HOLDER_ARRAY_LENGTH = 64; + + final Logger logger; + + // Capacity (Maximum size) in bytes (or capacity in RFC 9204) of the dynamic table + private long capacity; + + // RFC-9204: 3.2.3. Maximum Dynamic Table Capacity + private long maxCapacity; + + // Max entries is required to implement encoding of Required Insert Count + // in Field Lines Prefix: + // RFC-9204: 4.5.1.1. Required Insert Count: + // "This encoding limits the length of the prefix on long-lived connections." + private long maxEntries; + + // Size of the dynamic table in bytes - calculated as the sum of the sizes of its entries. + private long size; + + // Table elements holder and its state variables + // Absolute ID of tail and head elements. + // tail id - is an id of the oldest element in the table + // head id - is an id of the next element that will be added to the table. + // head element id is head - 1. + // drain id - is the lowest element id that encoder can reference + private long tail, head, drain = -1; + + // Used space percentage threshold when to start increasing the drain index + private final int drainUsedSpaceThreshold = QPACK.ENCODER_DRAINING_THRESHOLD; + + // true - table is used by the QPack encoder, otherwise used by the + // QPack decoder + private final boolean encoderTable; + + // Array that holds dynamic table entries + private HeaderField[] elements; + + // name -> (value -> [index]) + private final Map>> indicesMap; + + private record TableInsertCountNotification(long streamId, long minimumRIC, + CompletableFuture completion) { + public boolean isStreamId(long streamId) { + return this.streamId == streamId; + } + public boolean isFulfilled(long insertionCount) { + return insertionCount >= minimumRIC; + } + } + + private final Queue insertCountNotifications = + new PriorityQueue<>( + Comparator.comparingLong(TableInsertCountNotification::minimumRIC) + ); + + public CompletableFuture awaitFutureInsertCount(long streamId, + long valueToAwait) { + if (encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var completion = new CompletableFuture(); + long insertCount = insertCount(); + if (insertCount >= valueToAwait) { + completion.complete(null); + } else { + insertCountNotifications + .add(new TableInsertCountNotification( + streamId, valueToAwait, completion)); + } + return completion; + } finally { + writeLock.unlock(); + } + } + + private void notifyInsertCountChange() { + assert lock.isWriteLockedByCurrentThread(); + if (insertCountNotifications.isEmpty()) { + return; + } + long insertCount = insertCount(); + Predicate isFulfilled = + icn -> icn.isFulfilled(insertCount); + insertCountNotifications.removeIf(icn -> completeIf(isFulfilled, icn)); + } + + public boolean cleanupStreamInsertCountNotifications(long streamId) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + Predicate isSameStreamId = + icn -> icn.isStreamId(streamId); + return insertCountNotifications.removeIf(icn -> completeIf(isSameStreamId, icn)); + } finally { + writeLock.unlock(); + } + } + + private static boolean completeIf(Predicate predicate, + TableInsertCountNotification insertCountNotification) { + if (predicate.test(insertCountNotification)) { + insertCountNotification.completion.complete(null); + return true; + } + return false; + } + + // Read-Write lock to manage access to table entries + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + public DynamicTable(Logger logger) { + this(logger, true); + } + + public DynamicTable(Logger logger, boolean encoderTable) { + this.logger = logger; + this.encoderTable = encoderTable; + elements = new HeaderField[INITIAL_HOLDER_ARRAY_LENGTH]; + indicesMap = new HashMap<>(); + // -1 signifies that max table capacity was not yet initialized + maxCapacity = -1L; + maxEntries = 0L; + } + + /** + * Returns size of the dynamic table in bytes + * @return size of the dynamic table + */ + public long size() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return size; + } finally { + readLock.unlock(); + } + } + + /** + * Returns current capacity of the dynamic table + * @return current capacity + */ + public long capacity() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return capacity; + } finally { + readLock.unlock(); + } + } + + /** + * Returns a maximum capacity in bytes of the dynamic table. + * @return maximum capacity + */ + public long maxCapacity() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return maxCapacity; + } finally { + readLock.unlock(); + } + } + + /** + * Sets a maximum capacity in bytes of the dynamic table. + * + *

        The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK + * (see + * 3.2.3 Maximum Dynamic Table Capacity). + * + *

        May be called only once to set maximum dynamic table capacity. + *

        This method doesn't change the actual capacity of the dynamic table. + * + * @see #setCapacity(long) + * @param maxCapacity a non-negative long + * @throws IllegalArgumentException if max capacity is negative + * @throws IllegalStateException if max capacity was already set + */ + public void setMaxTableCapacity(long maxCapacity) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (maxCapacity < 0) { + throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity); + } + if (this.maxCapacity != -1L) { + // Max table capacity is initialized from SETTINGS frame which can be only received once: + // "If an endpoint receives a second SETTINGS frame on the control stream, + // the endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED" + // [RFC 9114 https://www.rfc-editor.org/rfc/rfc9114.html#name-settings] + throw new IllegalStateException("Max Table Capacity can only be set once"); + } + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("setting maximum allowed dynamic table capacity to %s", + maxCapacity)); + } + this.maxCapacity = maxCapacity; + this.maxEntries = maxCapacity / ENTRY_SIZE; + } finally { + writeLock.unlock(); + } + } + + /** + * Returns maximum possible number of entries that could be stored in the dynamic table + * with respect to MAX_CAPACITY setting. + * @return max entries + */ + public long maxEntries() { + var readLock = lock.readLock(); + readLock.lock(); + try { + return maxEntries; + } finally { + readLock.unlock(); + } + } + + /** + * Retrieves a header field by its absolute index. Entry referenced by an absolute + * index does not depend on the state of the dynamic table. + * @param uniqueID an entry unique index + * @return retrieved header field + * @throws IllegalArgumentException if entry is not received yet, + * already evicted or invalid entry index is specified. + */ + @Override + public HeaderField get(long uniqueID) { + var readLock = lock.readLock(); + readLock.lock(); + try { + if (uniqueID < 0) { + throw new IllegalArgumentException("Entry index invalid"); + } + // Not yet received entry + if (uniqueID >= head) { + throw new IllegalArgumentException("Entry not received yet"); + } + // Already evicted entry + if (uniqueID < tail) { + throw new IllegalArgumentException("Entry already evicted"); + } + return elements[(int) (uniqueID & (elements.length - 1))]; + } finally { + readLock.unlock(); + } + } + + /** + * Retrieves a header field by its relative index. Entry referenced by a relative index depends + * on the state of the dynamic table. + * @param relativeId index relative to the most recently inserted entry + * @return retrieved header field + */ + public HeaderField getRelative(long relativeId) { + // RFC 9204: 3.2.5. Relative Indexing + // "Relative indices begin at zero and increase in the opposite direction from the absolute index. + // Determining which entry has a relative index of 0 depends on the context of the reference. + // In encoder instructions (Section 4.3), a relative index of 0 refers to the most recently inserted + // value in the dynamic table." + var readLock = lock.readLock(); + readLock.lock(); + try { + return get(insertCount() - 1 - relativeId); + } finally { + readLock.unlock(); + } + } + + /** + * Converts absolute entry index to relative index that can be used + * in the encoder instructions. + * Relative index of 0 refers to the most recently inserted entry. + * + * @param absoluteId absolute index of an entry + * @return relative entry index + */ + public long toRelative(long absoluteId) { + var readLock = lock.readLock(); + readLock.lock(); + try { + assert absoluteId < head; + return head - 1 - absoluteId; + } finally { + readLock.unlock(); + } + } + + /** + * Search an absolute id of a name:value pair in the dynamic table. + * @param name a name to search for + * @param value a value to search for + * @return positive index if name:value match found, + * negative index if only name match found, + * 0 if no match found + */ + @Override + public long search(String name, String value) { + // This method is only designated for encoder use + if (!encoderTable) { + return 0; + } + var readLock = lock.readLock(); + readLock.lock(); + try { + Map> values = indicesMap.get(name); + if (values == null) { + return 0; + } + Deque indexes = values.get(value); + if (indexes != null) { + // "+1" since the index range [0..id] is mapped to [1..id+1] + return indexes.peekLast() + 1; + } else { + assert !values.isEmpty(); + Long any = values.values().iterator().next().peekLast(); // Iterator allocation + // Use last entry in found values with matching name, and use its index for + // encoding with name reference. + // Negation and "-1" since name-only matches are mapped from [0..id] to + // [-1..-id-1] region + return -any - 1; + } + } finally { + readLock.unlock(); + } + } + + /** + * Add an entry to the dynamic table. + * Entries could be evicted from the dynamic table. + * Unacknowledged section references are not checked by this method, therefore + * this method is intended to be used by the decoder only. The encoder should use + * overloaded method that takes global unacknowledged section reference. + * + * @param name header name + * @param value header value + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + */ + @Override + public long insert(String name, String value) { + // Invoking toString() will possibly allocate Strings. But that's + // unavoidable at this stage. If a CharSequence is going to be stored in + // the table, it must not be mutable (e.g. for the sake of hashing). + return insert(new HeaderField(name, value), SectionReference.noReferences()); + } + + + /** + * Add entry to the dynamic table with name specified as index in static + * or dynamic table. + * Entries could be evicted from the dynamic table. + * Unacknowledged section references are not checked by this method, therefore + * this method is intended to be used by the decoder only. The encoder should use + * overloaded method that takes global unacknowledged section reference. + * + * @param nameIndex index of the header name to add + * @param isStaticIndex if name index references static table header name + * @param value header value + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + * @throws IllegalStateException if table memory reclamation error observed + */ + public long insert(long nameIndex, boolean isStaticIndex, String value) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Inserting with name index (nameIndex='%s' isStaticIndex='%s' value=%s)", + nameIndex, isStaticIndex, value)); + } + String name = isStaticIndex ? + StaticTable.HTTP3.get(nameIndex).name() : + getRelative(nameIndex).name(); + return insert(name, value); + } finally { + writeLock.unlock(); + } + } + + /** + * Add an entry to the dynamic table. + * Entries could be evicted from the dynamic table. + * The supplied unacknowledged section references are checked by this method to check + * if entries are evictable. + * Such checks are performed when there is not enough space in the dynamic table to insert + * the requested header. + * This method is intended to be used by the encoder only. + * + * @param name header name + * @param value header value + * @param sectionReference unacknowledged section references + * @return unique index of an entry added to the table. + * If element cannot be added {@code -1} is returned. + * @throws IllegalStateException if table memory reclamation error observed + */ + public long insert(String name, String value, SectionReference sectionReference) { + return insert(new HeaderField(name, value), sectionReference); + } + + /** + * Inserts an entry to the dynamic table and sends encoder insert instruction bytes + * to the peer decoder. + * This method is designated to be used by the {@link Encoder} class only. + * If an entry with matching name:value is available, its index is returned + * and no insert instruction is generated on encoder stream. If duplicate entry is required + * due to entry being non-referencable then {@link DynamicTable#duplicateWithEncoderStreamUpdate( + * EncoderInstructionsWriter, long, QueuingStreamPair, EncodingContext)} is used. + * + * @param entry table entry to add + * @param writer non-configured encoder instruction writer for generating encoder + * instruction + * @param encoderStreams encoder stream pair + * @param encodingContext encoder encoding context + * @return absolute id of inserted entry OR already available entry, -1L if entry cannot + * be added + */ + public long insertWithEncoderStreamUpdate(TableEntry entry, + EncoderInstructionsWriter writer, + QueuingStreamPair encoderStreams, + EncodingContext encodingContext) { + if (!encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + String name = entry.name().toString(); + String value = entry.value().toString(); + // Entry with name only match in dynamic table + boolean nameOnlyDynamicEntry = !entry.isStaticTable() && entry.type() == NAME; + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + // First, check if entry is in the table already - + // no need to add a new one. + long index = search(name, value); + if (index > 0) { + long absIndex = index - 1; + // Check if found entry can be referenced, + // if not issue duplicate instruction + if (!canReferenceEntry(absIndex)) { + return duplicateWithEncoderStreamUpdate(writer, + absIndex, encoderStreams, encodingContext); + } + return absIndex; + } + SectionReference evictionLimitSR = encodingContext.evictionLimit(); + if (nameOnlyDynamicEntry) { + long nameIndex = entry.index(); + if (!canReferenceEntry(nameIndex)) { + return ENTRY_NOT_INSERTED; + } + evictionLimitSR = evictionLimitSR.reduce(nameIndex); + encodingContext.registerSessionReference(nameIndex); + } + // Relative index calculation should precede the insertion + // due to dependency on insert count value + long relativeNameIndex = + nameOnlyDynamicEntry ? toRelative(entry.index()) : -1; + + // Insert new entry to the table with respect to entry + // references range provided by the encoding context + long idx = insert(name, value, evictionLimitSR); + if (idx == ENTRY_NOT_INSERTED) { + // Insertion requires eviction of entries from unacknowledged + // sections therefore entry is not added + return ENTRY_NOT_INSERTED; + } + // Entry was successfully inserted + if (nameOnlyDynamicEntry) { + // Absolute index only needs to be replaced with the relative one + // when it references a name in the dynamic table. + entry = entry.relativizeDynamicTableEntry(relativeNameIndex); + } + int instructionSize = writer.configureForEntryInsertion(entry); + writeEncoderInstruction(writer, instructionSize, encoderStreams); + return idx; + } finally { + writeLock.unlock(); + } + } + + private long insert(HeaderField h, SectionReference sectionReference) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("adding ('%s', '%s')", h.name(), h.value())); + } + long entrySize = headerSize(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s", h.name(), h.value(), entrySize)); + } + + long availableEvictableSpace = availableEvictableSpace(sectionReference); + if (availableEvictableSpace < entrySize) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Header size exceeds available evictable space=%s." + + " Combined section reference=%s", + availableEvictableSpace, sectionReference)); + } + // Evicting entries won't help to gather enough space to insert the requested one + return ENTRY_NOT_INSERTED; + } + while (entrySize > capacity - size && size != 0) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("insufficient space %s, must evict entry", (capacity - size))); + } + // Only Encoder will supply section with referenced + // entries + if (sectionReference.referencesEntries()) { + // Check if tail element is evictable + if (tail < sectionReference.min()) { + if (!evictEntry()) { + return ENTRY_NOT_INSERTED; + } + } else { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Cannot evict entry: sectionRef=%s tail=%s", + sectionReference, tail)); + } + // For now -1 is returned to notify the Encoder that entry + // cannot be inserted to the dynamic table + return ENTRY_NOT_INSERTED; + } + } else { + // This call can be called by both Encoder and Decoder: + // - Encoder when add new entry with no unacked section references + // - Decoder when processing insert entry instructions. + // Entries are evicted until there is enough space OR until table + // is empty. + if (!evictEntry()) { + return ENTRY_NOT_INSERTED; + } + } + } + size += entrySize; + // At this stage it is clear that there are enough bytes (max capacity is not exceeded) in the dynamic + // table to add new header field + addWithInverseMapping(h); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("('%s, '%s') added", h.name(), h.value())); + logger.log(EXTRA, this::toString); + } + notifyInsertCountChange(); + return head - 1; + } finally { + writeLock.unlock(); + } + } + + public long duplicate(long relativeId) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var entry = getRelative(relativeId); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Duplicate entry with absId=%s" + + " insertCount=%s ('%s', '%s')", + insertCount() - 1 - relativeId, insertCount(), + entry.name(), entry.value())); + } + return insert(entry.name(), entry.value()); + } finally { + writeLock.unlock(); + } + } + + public long duplicateWithEncoderStreamUpdate(EncoderInstructionsWriter writer, + long absoluteEntryId, + QueuingStreamPair encoderStreams, + EncodingContext encodingContext) { + if (!encoderTable) { + throw new IllegalStateException("Misconfigured table"); + } + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + var entry = get(absoluteEntryId); + // Relative index calculation should precede the insertion + // due to dependency on insert count value + long relativeEntryId = toRelative(absoluteEntryId); + + // Make entry id that needs to be duplicated non-evictable + SectionReference evictionLimit = encodingContext.evictionLimit() + .reduce(absoluteEntryId); + + // Put duplicated entry to our dynamic table first + long idx = insert(entry.name(), entry.value(), + evictionLimit); + if (idx == ENTRY_NOT_INSERTED) { + return ENTRY_NOT_INSERTED; + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Issuing entry duplication instruction" + + " for absId=%s relId=%s ('%s', '%s')", + absoluteEntryId, relativeEntryId, entry.name(), entry.value())); + } + + // Configure writer for entry duplication + int instructionSize = + writer.configureForEntryDuplication(relativeEntryId); + + // Write instruction to the encoder stream + writeEncoderInstruction(writer, instructionSize, encoderStreams); + return idx; + } finally { + writeLock.unlock(); + } + } + + private HeaderField remove() { + assert lock.isWriteLockedByCurrentThread(); + // Remove element from the holder array first + if (getElementsCount() == 0) { + throw new IllegalStateException("Empty table"); + } + + int tailIdx = (int) (tail++ & (elements.length - 1)); + HeaderField f = elements[tailIdx]; + elements[tailIdx] = null; + + // Log the removal event + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("removing ('%s', '%s')", f.name(), f.value())); + } + + // Update indices map on the encoder table only + if (encoderTable) { + Map> values = indicesMap.get(f.name()); + Deque indexes = values.get(f.value()); + // Remove the oldest index of the name:value pair + Long index = indexes.pollFirst(); + // Clean-up indexes associated with a value from values map + if (indexes.isEmpty()) { + values.remove(f.value()); + } + assert index != null; + // If indexes map associated with name is empty remove name + // entry from indices map + if (values.isEmpty()) { + indicesMap.remove(f.name()); + } + } + return f; + } + + /** + * Sets the dynamic table capacity in bytes. + * The new capacity must be lower than or equal to the limit defined by + * SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 settings parameter. This limit is + * enforced by {@linkplain DynamicTable#setMaxTableCapacity(long)}. + * + * @param capacity dynamic table capacity to set + */ + public void setCapacity(long capacity) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + if (capacity > maxCapacity) { + // Calling code catches IllegalArgumentException and generates the connection error: + // 4.3.1. Set Dynamic Table Capacity: + // "The decoder MUST treat a new dynamic table capacity value that exceeds + // this limit as a connection error of type QPACK_ENCODER_STREAM_ERROR." + throw new IllegalArgumentException("Illegal dynamic table capacity"); + } + if (capacity < 0) { + throw new IllegalArgumentException("capacity >= 0: capacity=" + capacity); + } + while (capacity < size && size != 0) { + // Evict entries until existing elements fit into + // new table capacity + boolean entryEvicted = evictEntry(); + assert entryEvicted; + } + this.capacity = capacity; + if (usedSpace() < drainUsedSpaceThreshold) { + if (drain != -1) { + drain = -1; + } + } else if (drain == -1 || tail > drain) { + drain = tail; + } + } finally { + writeLock.unlock(); + } + } + + /** + * Updates the capacity of the dynamic table and sends encoder capacity update instruction + * bytes to the peer decoder. + * This method is designated to be used by the {@link Encoder} class only. + * @param writer non-configured encoder instruction writer for generating encoder instruction + * @param capacity new capacity value + * @param encoderStreams encoder stream pair + */ + public void setCapacityWithEncoderStreamUpdate(EncoderInstructionsWriter writer, long capacity, + QueuingStreamPair encoderStreams) { + var writeLock = lock.writeLock(); + writeLock.lock(); + try { + // Configure writer for capacity update + int instructionSize = writer.configureForTableCapacityUpdate(capacity); + // Check and set our capacity + setCapacity(capacity); + // Write instruction + writeEncoderInstruction(writer, instructionSize, encoderStreams); + } finally { + writeLock.unlock(); + } + } + + + /** + * Evicts one entry from the table tail. + * @return {@code true} if entry was evicted, + * {@code false} if nothing to remove + */ + private boolean evictEntry() { + assert lock.isWriteLockedByCurrentThread(); + try { + HeaderField f = remove(); + long s = headerSize(f); + this.size -= s; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> format("evicted entry ('%s', '%s') of size %s with absId=%s", + f.name(), f.value(), s, tail - 1)); + } + } catch (IllegalStateException ise) { + // Entry cannot be evicted from empty table + return false; + } + return true; + } + + public long availableEvictableSpace(SectionReference sectionReference) { + var readLock = lock.readLock(); + readLock.lock(); + try { + if (!sectionReference.referencesEntries()) { + return capacity; + } + // Size that can be reclaimed in the dynamic table by evicting + // non-referenced entries + long availableEvictableCapacity = 0; + for (long absId = tail; absId < sectionReference.min(); absId++) { + HeaderField field = get(absId); + availableEvictableCapacity += headerSize(field); + } + // (capacity - size) - free space in the dynamic table + return availableEvictableCapacity + (capacity - size); + } finally { + readLock.unlock(); + } + } + + public boolean tryReferenceEntry(TableEntry tableEntry, EncodingContext context) { + var readLock = lock.readLock(); + readLock.lock(); + try { + long absId = tableEntry.index(); + if (canReferenceEntry(absId)) { + context.registerSessionReference(absId); + context.referenceEntry(tableEntry); + return true; + } else { + return false; + } + } finally { + readLock.unlock(); + } + } + + @Override + public String toString() { + var readLock = lock.readLock(); + readLock.lock(); + try { + double used = usedSpace(); + return format("full length: %s, used space: %s/%s (%.1f%%)", + getElementsCount(), size, capacity, used); + } finally { + readLock.unlock(); + } + } + + private boolean canReferenceEntry(long absId) { + // The dynamic table lock is acquired by the calling methods + return absId > drain; + } + + private double usedSpace() { + return capacity == 0 ? 0 : 100 * (((double) size) / capacity); + } + + public static long headerSize(HeaderField f) { + return headerSize(f.name(), f.value()); + } + + public static long headerSize(String name, String value) { + return name.length() + value.length() + ENTRY_SIZE; + } + + // To quickly find an index of an entry in the dynamic table with the + // given contents an effective inverse mapping is needed. + private void addWithInverseMapping(HeaderField field) { + assert lock.isWriteLockedByCurrentThread(); + // Check if holder array has at least one free slot to add header field + // The method below can increase elements.length if no free slot found + ensureElementsArrayLength(); + long counterSnapshot = head++; + elements[(int) (counterSnapshot & (elements.length - 1))] = field; + if (encoderTable) { + // Allocate unique index and use it to store in indicesMap + Map> values = indicesMap.computeIfAbsent( + field.name(), _ -> new HashMap<>()); + Deque indexes = values.computeIfAbsent( + field.value(), _ -> new LinkedList<>()); + indexes.add(counterSnapshot); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> format("added '%s' header field with '%s' unique id", + field, counterSnapshot)); + } + assert indexesUniqueAndOrdered(indexes); + // Draining index is only used by the Encoder + updateDrainIndex(); + } + } + + private void updateDrainIndex() { + if (!encoderTable) { + return; + } + assert lock.isWriteLockedByCurrentThread(); + if (usedSpace() > drainUsedSpaceThreshold) { + if (drain == -1L) { + drain = tail; + } else { + drain++; + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Draining index changed: %d", drain)); + } + } + } + + private void ensureElementsArrayLength() { + assert lock.isWriteLockedByCurrentThread(); + + int currentArrayLength = elements.length; + if (getElementsCount() == currentArrayLength) { + if (currentArrayLength == (1 << 30)) { + throw new IllegalStateException("No room for more elements"); + } + // Increase elements array by factor of 2 + resize(currentArrayLength << 1); + } + } + + private boolean indexesUniqueAndOrdered(Deque indexes) { + long maxIndexSoFar = -1L; + for (long l : indexes) { + if (l <= maxIndexSoFar) { + return false; + } else { + maxIndexSoFar = l; + } + } + return true; + } + + private int getElementsCount() { + // head and tail are unique and monotonic indexes - therefore we can just use their + // difference to determine number of header:value pairs stored in the dynamic table. + // Since head points to the next unused element head == 0 means that there is + // no elements in the dynamic table + return head > 0 ? (int) (head - tail) : 0; + } + + private void resize(int newSize) { + // newSize is always a power of 2: + // - its initial size is a power of 2 + // - it is shifted 1 bit left every + // time when there is not enough space in the 'elements' array + assert lock.isWriteLockedByCurrentThread(); + int elementsCnt = getElementsCount(); + final int oldSize = elements.length; + + if (newSize < elementsCnt) { + throw new IllegalArgumentException("New size is too low to hold existing elements"); + } + + HeaderField[] newElements = new HeaderField[newSize]; + if (elementsCnt == 0) { + elements = newElements; + return; + } + long headID = head - 1; + final int oldTailIdx = (int) (tail & (oldSize - 1)); + final int oldHeadIdx = (int) (headID & (oldSize - 1)); + final int newTailIdx = (int) (tail & (newSize - 1)); + final int newHeadIdx = (int) (headID & (newSize - 1)); + + if (oldTailIdx <= oldHeadIdx) { + // Elements in an old array are stored in a continuous segment + if (newTailIdx <= newHeadIdx) { + // Elements in a new array will be stored in a continuous segment + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, elementsCnt); + } else { + // Elements in a new array will split in two segments due to wrapping around + // the end of a new array. + int sizeFromNewTailToEnd = newSize - newTailIdx; + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, sizeFromNewTailToEnd); + System.arraycopy(elements, oldTailIdx + sizeFromNewTailToEnd, + newElements, 0, newHeadIdx + 1); + } + } else { + // Elements in an old array are split in two segments + if (newTailIdx <= newHeadIdx) { + // Elements in a new array will be stored in a continuous segment + int firstSegmentSize = oldSize - oldTailIdx; + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, firstSegmentSize); + System.arraycopy(elements, 0, + newElements, newTailIdx + firstSegmentSize, oldHeadIdx + 1); + } else { + // Elements in a new array will be stored in two segments + // Size from the tail to the end in an old array + int oldPart1Size = oldSize - oldTailIdx; + // Size from the tail to the end in a new array + int newPart1Size = newSize - newTailIdx; + if (oldPart1Size <= newPart1Size) { + // Segment from tail to the end of an old array + // fits into the corresponding segment in a new array + System.arraycopy(elements, oldTailIdx, newElements, newTailIdx, oldPart1Size); + int leftToCopyToNewPart1 = newPart1Size - oldPart1Size; + System.arraycopy(elements, 0, newElements, + newTailIdx + oldPart1Size, leftToCopyToNewPart1); + System.arraycopy(elements, leftToCopyToNewPart1, + newElements, 0, newHeadIdx + 1); + } else { // oldPart1Size > newPart1Size + // Not possible given two restrictions: + // - we do not allow rewriting of entries if size is not enough, + // IAE is thrown above. + // - the size of elements holder array can only be a power of 2 + throw new AssertionError("Not possible dynamic table indexes configuration"); + } + } + } + elements = newElements; + } + + /** + * Method returns number of elements inserted to the dynamic table. + * Since element ids start from 0 the returned value is equal + * to the id of the head element plus one. + * @return number of elements in the dynamic table + */ + public long insertCount() { + var rl = lock.readLock(); + rl.lock(); + try { + // head points to the next unallocated element + return head; + } finally { + rl.unlock(); + } + } + + // Writes an encoder instruction to the encoder stream associated with dynamic table. + // This method is kept in DynamicTable class since most instructions depend on + // and/or update the dynamic table state. + // Also, we want to send encoder instruction and update the dynamic table state while + // holding the write-lock. + private void writeEncoderInstruction(EncoderInstructionsWriter writer, int instructionSize, + QueuingStreamPair encoderStreams) { + if (instructionSize > encoderStreams.credit()) { + throw new QPackException(H3_CLOSED_CRITICAL_STREAM, + new IOException("QPACK not enough credit on an encoder stream " + + encoderStreams.remoteStreamType()), true); + } + boolean done; + ByteBuffer buffer; + do { + if (instructionSize > MAX_BUFFER_SIZE) { + buffer = ByteBuffer.allocate(MAX_BUFFER_SIZE); + instructionSize -= MAX_BUFFER_SIZE; + } else { + buffer = ByteBuffer.allocate(instructionSize); + } + done = writer.write(buffer); + buffer.flip(); + encoderStreams.submitData(buffer); + } while (!done); + } + private static final int MAX_BUFFER_SIZE = 1024 * 16; + static final long ENTRY_NOT_INSERTED = -1L; +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java new file mode 100644 index 00000000000..26da27a702a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/Encoder.java @@ -0,0 +1,672 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPACK.StreamPairSupplier; +import jdk.internal.net.http.qpack.TableEntry.EntryType; +import jdk.internal.net.http.qpack.readers.DecoderInstructionsReader; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; +import jdk.internal.net.http.qpack.writers.FieldLineSectionPrefixWriter; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.net.ProtocolException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_CLOSED_CRITICAL_STREAM; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Encodes headers to their binary representation. + */ +public class Encoder { + private static final AtomicLong ENCODERS_IDS = new AtomicLong(); + + // RFC 9204 7.1.3. Never-Indexed Literals: + // "Implementations can also choose to protect sensitive fields by not + // compressing them and instead encoding their value as literals" + private static final Set SENSITIVE_HEADER_NAMES = + Set.of("cookie", "authorization", "proxy-authorization"); + + private final Logger logger; + private final InsertionPolicy policy; + private final TablesIndexer tablesIndexer; + private final DynamicTable dynamicTable; + private final QueuingStreamPair encoderStreams; + private final DecoderInstructionsReader decoderInstructionsReader; + // RFC-9204: 2.1.4. Known Received Count + private long knownReceivedCount; + + // Lock for Known Received Count variable + private final ReentrantReadWriteLock krcLock = new ReentrantReadWriteLock(); + + // Max blocked streams setting value received from the peer decoder + // can be set only once + private long maxBlockedStreams = -1L; + + // Number of streams in process of headers encoding that expected to be blocked + // but their unacknowledged section is not registered yet + private long blockedStreamsInFlight; + + private final ReentrantLock blockedStreamsCounterLock = new ReentrantLock(); + + // stream id -> fifo list of max and min ids referenced from field sections for each stream id + private final ConcurrentMap> unacknowledgedSections = + new ConcurrentHashMap<>(); + + // stream id -> set of referenced entry absolute indexes from a field line section that currently + // are in process of encoding and not added to the unacknowledged field sections map yet. + private final ConcurrentMap> liveContextReferences = + new ConcurrentHashMap<>(); + + private final QPACKErrorHandler qpackErrorHandler; + + public HeaderFrameWriter newHeaderFrameWriter() { + return new HeaderFrameWriter(logger); + } + + /** + * Constructs an {@code Encoder} with zero initial capacity of the dynamic table. + * Maximum dynamic table capacity is not initialized until peer (decoder) HTTP/3 settings frame is + * received (see {@link Encoder#configure(ConnectionSettings)}). + * + *

        Dynamic table capacity values has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK. + *

        Maximum dynamic table capacity is determined by the value of SETTINGS_QPACK_MAX_TABLE_CAPACITY + * HTTP/3 setting sent by the decoder side (see + * + * 3.2.3. Maximum Dynamic Table Capacity). + *

        An encoder informs the decoder of a change to the dynamic table capacity using the + * "Set Dynamic Table Capacity" instruction + * (see + * 4.3.1. Set Dynamic Table Capacity) + * + * @param streamPairs supplier of the encoder unidirectional stream pair + * @throws IllegalArgumentException if maxCapacity is negative + * @see Encoder#configure(ConnectionSettings) + */ + public Encoder(InsertionPolicy policy, StreamPairSupplier streamPairs, QPACKErrorHandler codingError) { + this.policy = policy; + long id = ENCODERS_IDS.incrementAndGet(); + this.logger = QPACK.getLogger().subLogger("Encoder#" + id); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> "New encoder"); + } + if (logger.isLoggable(EXTRA)) { + /* To correlate with logging outside QPACK, knowing + hashCode/toString is important */ + logger.log(EXTRA, () -> { + String hashCode = Integer.toHexString( + System.identityHashCode(this)); + /* Since Encoder can be subclassed hashCode AND identity + hashCode might be different. So let's print both. */ + return format("toString='%s', hashCode=%s, identityHashCode=%s", + this, hashCode(), hashCode); + }); + } + // Set maximum dynamic table to 0, postpone setting of max capacity until peer + // settings frame is received + dynamicTable = new DynamicTable(logger.subLogger("DynamicTable"), true); + tablesIndexer = new TablesIndexer(StaticTable.HTTP3, dynamicTable); + encoderStreams = streamPairs.create(this::processDecoderAcks); + decoderInstructionsReader = new DecoderInstructionsReader(new TableUpdatesCallback(), + logger); + qpackErrorHandler = codingError; + } + + /** + * Configures encoder according to the settings received from the peer. + * + * @param peerSettings the peer settings + */ + public void configure(ConnectionSettings peerSettings) { + blockedStreamsCounterLock.lock(); + try { + if (maxBlockedStreams == -1) { + maxBlockedStreams = peerSettings.qpackBlockedStreams(); + } else { + throw new IllegalStateException("Encoder already configured"); + } + } finally { + blockedStreamsCounterLock.unlock(); + } + // Set max dynamic table capacity + long maxCapacity = peerSettings.qpackMaxTableCapacity(); + dynamicTable.setMaxTableCapacity(maxCapacity); + // Send DT capacity update instruction if the peer negotiated non-zero + // max table capacity, and limit the value with encoder's table capacity + // limit system property value + if (QPACK.ENCODER_TABLE_CAPACITY_LIMIT > 0 && maxCapacity > 0) { + long encoderCapacity = Math.min(maxCapacity, QPACK.ENCODER_TABLE_CAPACITY_LIMIT); + setTableCapacity(encoderCapacity); + } + } + + public QueuingStreamPair encoderStreams() { + return encoderStreams; + } + + public void header(EncodingContext context, CharSequence name, CharSequence value, + boolean sensitive) throws IllegalStateException { + header(context, name, value, sensitive, knownReceivedCount()); + } + + /** + * Sets up the given header {@code (name, value)} with possibly sensitive + * value. + * + *

        If the {@code value} is sensitive (think security, secrecy, etc.) + * this encoder will compress it using a special representation + * (see + * 7.1.3. Never-Indexed Literals). + * + *

        Fixates {@code name} and {@code value} for the duration of encoding. + * + * @param context the encoding context + * @param name the name + * @param value the value + * @param sensitive whether the value is sensitive + * @param knownReceivedCount the count of received entries known to a peer decoder or + * {@code -1} to skip the dynamic table entry index check during header encoding. + * @throws NullPointerException if any of the arguments are {@code null} + * @throws IllegalStateException if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) + */ + public void header(EncodingContext context, CharSequence name, CharSequence value, + boolean sensitive, long knownReceivedCount) throws IllegalStateException { + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s", + name, value, sensitive)); + } + requireNonNull(name, "name"); + requireNonNull(value, "value"); + + // TablesIndexer.entryOf checks if the found entry is a dynamic table entry, + // and if its insertion was already ACKed. If not - use literal or name index encoding. + var tableEntry = tablesIndexer.entryOf(name, value, knownReceivedCount); + + // NAME_VALUE table entry type means that one of dynamic or static tables contain + // exact name:value pair. + if (dynamicTable.capacity() > 0L + && tableEntry.type() != EntryType.NAME_VALUE + && !sensitive && policy.shouldUpdateDynamicTable(tableEntry)) { + // We should check if we have an entry in dynamic table: + // - If we have it - do nothing + // - if we do not have it - insert it and use the index straight-away + // when blocking encoding is allowed + tableEntry = context.tryInsertEntry(tableEntry); + } + + // First, check that found/newly inserted entry is in the dynamic table + // and can be referenced + if (!tableEntry.isStaticTable() && tableEntry.index() >= 0 && + tableEntry.type() != EntryType.NEITHER) { + if (!dynamicTable.tryReferenceEntry(tableEntry, context)) { + // If entry cannot be referenced - use literal encoding instead + tableEntry = tableEntry.toLiteralsEntry(); + } + } + + // Configure header frame writer to write header field to the headers frame. One of the following + // writers is selected based on entry type, the base value and the referenced table (static or dynamic): + // - static table and name:value match - "Indexed Field Line" + // - static table and name match - "Literal Field Line with Name Reference" + // - dynamic table, name:value match and index < base - "Indexed Field Line" + // - dynamic table, name match and index < base - "Literal Field Line with Name Reference" + // - dynamic table, name:value match and index >= base - "Indexed Field Line with Post-Base Index" + // - dynamic table, name match and index >= base - "Literal Field Line with Post-Base Name Reference" + // - not in dynamic or static tables - "Literal Field Line with Literal Name" + context.writer.configure(tableEntry, sensitive, context.base); + } + + /** + * Sets the capacity of the encoder's dynamic table and notifies the decoder by + * issuing "Set Dynamic Table Capacity" instruction. + * + *

        The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses QPACK + * (see + * 4.3.1. Set Dynamic Table Capacity). + * + * @param capacity a non-negative long + * @throws IllegalArgumentException if capacity is negative or exceeds the negotiated max capacity HTTP/3 setting + */ + public void setTableCapacity(long capacity) { + dynamicTable.setCapacityWithEncoderStreamUpdate(new EncoderInstructionsWriter(logger), + capacity, encoderStreams); + } + + /** + * This method is called when the peer decoder sends + * data on the peer's decoder stream + * + * @param buffer data sent by the peer's decoder + */ + private void processDecoderAcks(ByteBuffer buffer) { + if (buffer == QuicStreamReader.EOF) { + // RFC-9204, section 4.2: + // Closure of either unidirectional stream type MUST be treated as a connection + // error of type H3_CLOSED_CRITICAL_STREAM. + qpackErrorHandler.closeOnError( + new ProtocolException("QPACK " + encoderStreams.remoteStreamType() + + " remote stream was unexpectedly closed"), H3_CLOSED_CRITICAL_STREAM); + return; + } + try { + decoderInstructionsReader.read(buffer); + } catch (QPackException e) { + qpackErrorHandler.closeOnError(e.getCause(), e.http3Error()); + } + } + + public List encodeHeaders(HeaderFrameWriter writer, long streamId, + int bufferSize, HttpHeaders... headers) { + List buffers = new ArrayList<>(); + ByteBuffer buffer = getByteBuffer(bufferSize); + + try (EncodingContext encodingContext = newEncodingContext(streamId, + dynamicTable.insertCount(), writer)) { + for (HttpHeaders header : headers) { + for (Map.Entry> e : header.map().entrySet()) { + // RFC-9114, section 4.2: Field names are strings containing a subset of + // ASCII characters. .... Characters in field names MUST be converted to + // lowercase prior to their encoding. + final String lKey = e.getKey().toLowerCase(Locale.ROOT); + final List values = e.getValue(); + // An encoder might also choose not to index values for fields that are + // considered to be highly valuable or sensitive to recovery, such as the + // Cookie or Authorization header fields + final boolean sensitive = SENSITIVE_HEADER_NAMES.contains(lKey); + for (String value : values) { + header(encodingContext, lKey, value, sensitive); + while (!writer.write(buffer)) { + buffer.flip(); + buffers.add(buffer); + buffer = getByteBuffer(bufferSize); + } + } + } + } + buffer.flip(); + buffers.add(buffer); + + // Put field line section prefix as the first byte buffer + generateFieldLineSectionPrefix(encodingContext, buffers); + + // Register field line section as unacked if it uses references to the + // dynamic table entries + registerUnackedFieldLineSection(streamId, SectionReference.of(encodingContext)); + } + return buffers; + } + + public void generateFieldLineSectionPrefix(EncodingContext encodingContext, List buffers) { + // Write field section prefix according to RFC 9204: "4.5.1. Encoded Field Section Prefix" + FieldLineSectionPrefixWriter prefixWriter = new FieldLineSectionPrefixWriter(); + FieldSectionPrefix fsp = encodingContext.sectionPrefix(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Encoding Field Section Prefix - required insert" + + " count: %d base: %d", + fsp.requiredInsertCount(), fsp.base())); + } + int requiredSize = prefixWriter.configure(fsp, dynamicTable.maxEntries()); + var fspBuffer = getByteBuffer(requiredSize); + if (!prefixWriter.write(fspBuffer)) { + throw new IllegalStateException("Field Line Section Prefix"); + } + fspBuffer.flip(); + buffers.addFirst(fspBuffer); + } + + public void registerUnackedFieldLineSection(long streamId, SectionReference sectionReference) { + if (sectionReference.referencesEntries()) { + unacknowledgedSections + .computeIfAbsent(streamId, k -> new ConcurrentLinkedQueue<>()) + .add(sectionReference); + } + } + + // This one is for tracking evict-ability of dynamic table entries + public SectionReference unackedFieldLineSectionsRange(EncodingContext context) { + SectionReference referenceNotRegisteredYet = SectionReference.of(context); + return unackedFieldLineSectionsRange(referenceNotRegisteredYet); + } + + private SectionReference unackedFieldLineSectionsRange(SectionReference initial) { + return unacknowledgedSections.values().stream() + .flatMap(Queue::stream) + .reduce(initial, SectionReference::reduce); + } + + long blockedStreamsCount() { + long blockedStreams = 0; + long krc = knownReceivedCount(); + for (var streamSections : unacknowledgedSections.values()) { + boolean hasBlockedSection = streamSections.stream() + .anyMatch(sectionReference -> !sectionReference.fullyAcked(krc)); + blockedStreams = hasBlockedSection ? blockedStreams + 1 : blockedStreams; + } + return blockedStreams; + } + + + public long knownReceivedCount() { + krcLock.readLock().lock(); + try { + return knownReceivedCount; + } finally { + krcLock.readLock().unlock(); + } + } + + private void updateKrcSectionAck(long streamId) { + krcLock.writeLock().lock(); + try { + var queue = unacknowledgedSections.get(streamId); + // max() + 1 - since it is "Required Insert Count" not entry ID + SectionReference oldestSectionRef = queue != null ? queue.poll() : null; + long oldestNonAckedRic = oldestSectionRef != null ? oldestSectionRef.max() + 1 : -1L; + if (oldestNonAckedRic == -1L) { + // RFC 9204 4.4.1. Section Acknowledgment: + // If an encoder receives a Section Acknowledgment instruction referring + // to a stream on which every encoded field section with a non-zero + // Required Insert Count has already been acknowledged, this MUST be treated + // as a connection error of type QPACK_DECODER_STREAM_ERROR. + var qPackException = QPackException.decoderStreamError( + new IllegalStateException("No unacknowledged sections found" + + " for stream id = " + streamId)); + throw qPackException; + } + // "2.1.4. Known Received Count": + // If the Required Insert Count of the acknowledged field section is greater + // than the current Known Received Count, the Known Received Count is updated + // to that Required Insert Count value. + if (oldestNonAckedRic != -1 && knownReceivedCount < oldestNonAckedRic) { + knownReceivedCount = oldestNonAckedRic; + } + } finally { + krcLock.writeLock().unlock(); + } + } + + private void updateKrcInsertCountIncrement(long increment) { + long insertCount = dynamicTable.insertCount(); + krcLock.writeLock().lock(); + try { + // An encoder that receives an Increment field equal to zero, or one that increases + // the Known Received Count beyond what the encoder has sent, MUST treat this as + // a connection error of type QPACK_DECODER_STREAM_ERROR. + if (increment == 0 || knownReceivedCount > insertCount - increment) { + var qpackException = QPackException.decoderStreamError( + new IllegalStateException("Invalid increment field value: " + increment)); + throw qpackException; + } + knownReceivedCount += increment; + } finally { + krcLock.writeLock().unlock(); + } + } + + private void cleanupStreamData(long streamId) { + liveContextReferences.remove(streamId); + unacknowledgedSections.remove(streamId); + } + + private class TableUpdatesCallback implements DecoderInstructionsReader.Callback { + @Override + public void onSectionAck(long streamId) { + updateKrcSectionAck(streamId); + } + + @Override + public void onInsertCountIncrement(long increment) { + updateKrcInsertCountIncrement(increment); + } + + @Override + public void onStreamCancel(long streamId) { + cleanupStreamData(streamId); + } + } + + public class EncodingContext implements AutoCloseable { + final long base; + final long streamId; + final ConcurrentSkipListSet referencedIndexes; + long maxIndex; + long minIndex; + boolean blockedDecoderExpected; + final HeaderFrameWriter writer; + final EncoderInstructionsWriter encoderInstructionsWriter; + + public EncodingContext(long streamId, long base, HeaderFrameWriter writer) { + this.base = base; + this.encoderInstructionsWriter = new EncoderInstructionsWriter(logger); + this.writer = writer; + this.maxIndex = -1L; + this.minIndex = Long.MAX_VALUE; + this.streamId = streamId; + this.referencedIndexes = liveContextReferences.computeIfAbsent(streamId, + _ -> new ConcurrentSkipListSet<>()); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Begin encoding session with base = %s stream-id = %s", base, streamId)); + } + } + + public void registerSessionReference(long absoluteEntryId) { + referencedIndexes.add(absoluteEntryId); + } + + @Override + public void close() { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Closing encoding context for stream-id=%s" + + " session references:%s", + streamId, referencedIndexes)); + } + liveContextReferences.remove(streamId); + // Deregister if this stream was marked as in-flight blocked + blockedStreamsCounterLock.lock(); + try { + if (blockedDecoderExpected) { + blockedStreamsInFlight--; + } + } finally { + blockedStreamsCounterLock.unlock(); + } + } + + public FieldSectionPrefix sectionPrefix() { + // RFC 9204: 2.1.2. Blocked Streams + // "the Required Insert Count is one larger than the largest absolute index + // of all referenced dynamic table entries" + // largestAbsoluteIndex is initialized to -1, and if there is no dynamic + // table entry references - RIC will be set to 0. + return new FieldSectionPrefix(maxIndex + 1, base); + } + + public SectionReference evictionLimit() { + // In-flight references - a set with entry ids referenced from all + // active header encoding sessions not fully encoded yet + SectionReference inFlightReferences = SectionReference.singleReference( + liveContextReferences.values().stream() + .filter(Predicate.not(ConcurrentSkipListSet::isEmpty)) + .map(ConcurrentSkipListSet::first) + .min(Long::compare) + .orElse(-1L)); + + // Calculate the eviction limit with respect to: + // - in-flight references + // - acknowledged dynamic table insertions + // - range of unacknowledged sections which already fully encoded + // and sent as part of other request/response streams + return inFlightReferences + .reduce(knownReceivedCount()) + .reduce(unackedFieldLineSectionsRange(this)); + } + + public TableEntry tryInsertEntry(TableEntry entry) { + long idx = dynamicTable.insertWithEncoderStreamUpdate(entry, + encoderInstructionsWriter, encoderStreams, + this); + if (idx == DynamicTable.ENTRY_NOT_INSERTED) { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Not adding entry '%s' to the dynamic " + + "table - not enough space, or unacknowledged entry needs to be evicted", + entry)); + } + // Return what we previously found in the dynamic or static table + return entry; + } + + if (QPACK.ALLOW_BLOCKING_ENCODING && canReferenceNewEntry()) { + // Create a new TableEntry that describes newly added header field + return entry.toNewDynamicTableEntry(idx); + } else { + return entry; + } + } + + private boolean canReferenceNewEntry() { + blockedStreamsCounterLock.lock(); + try { + // If current encoding context is already marked as blocked we can + // reference new entries without analyzing number of blocked streams + if (blockedDecoderExpected) { + return true; + } + // Number of streams with unacknowledged field line section + long alreadyBlocked = blockedStreamsCount(); + // Other streams might be in progress of headers encoding + boolean canReferenceNewEntry = maxBlockedStreams - alreadyBlocked - blockedStreamsInFlight > 0; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("%s reference to newly added header. " + + "Number of blocked streams based on unAcked sections: %d " + + "Number of blocked streams in progress of encoding: %d " + + "Max allowed by HTTP/3 settings: %d", + canReferenceNewEntry ? "Allowing" : "Restricting", + alreadyBlocked, blockedStreamsInFlight, maxBlockedStreams)); + } + if (canReferenceNewEntry && !blockedDecoderExpected) { + blockedStreamsInFlight++; + blockedDecoderExpected = true; + } + return canReferenceNewEntry; + } finally { + blockedStreamsCounterLock.unlock(); + } + } + + public void referenceEntry(TableEntry tableEntry) { + assert tableEntry.index() >= 0; + if (!tableEntry.isStaticTable()) { + long index = tableEntry.index(); + maxIndex = Long.max(maxIndex, index); + minIndex = Long.min(minIndex, index); + } + } + } + + /** + * Descriptor of entries range referenced from a field lines section. + * + * @param min minimum entry id referenced from a field lines section + * @param max maximum entry id referenced from a field lines section + */ + public record SectionReference(long min, long max) { + public static SectionReference of(EncodingContext context) { + if (context.maxIndex == -1L) { + return SectionReference.noReferences(); + } + return new SectionReference(context.minIndex, context.maxIndex); + } + + public SectionReference reduce(SectionReference other) { + if (!referencesEntries()) { + return other; + } else if (!other.referencesEntries()) { + return this; + } + long newMin = Long.min(this.min, other.min); + long newMax = Long.max(this.max, other.max); + return new SectionReference(newMin, newMax); + } + + public SectionReference reduce(long entryId) { + return reduce(singleReference(entryId)); + } + + public static SectionReference singleReference(long entryId) { + return new SectionReference(entryId, entryId); + } + + public boolean fullyAcked(long knownReceiveCount) { + return max < knownReceiveCount; + } + + public static SectionReference noReferences() { + return new SectionReference(-1L, -1L); + } + + public boolean referencesEntries() { + return max != -1L; + } + } + + public EncodingContext newEncodingContext(long streamId, long base, HeaderFrameWriter writer) { + assert streamId >= 0; + assert base >= 0; + return new EncodingContext(streamId, base, writer); + } + + private ByteBuffer getByteBuffer(int size) { + ByteBuffer buf = ByteBuffer.allocate(size); + buf.limit(size); + return buf; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java new file mode 100644 index 00000000000..cbf2037af6a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/FieldSectionPrefix.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +public record FieldSectionPrefix(long requiredInsertCount, long base) { + + public static FieldSectionPrefix decode(long encodedRIC, long deltaBase, + int baseSign, DynamicTable dynamicTable) { + long decodedRIC = decodeRIC(encodedRIC, dynamicTable); + long decodedBase = decodeBase(decodedRIC, deltaBase, baseSign); + return new FieldSectionPrefix(decodedRIC, decodedBase); + } + + private static long decodeRIC(long encodedRIC, DynamicTable dynamicTable) { + if (encodedRIC == 0) { + return 0; + } + long maxEntries = dynamicTable.maxEntries(); + long insertCount = dynamicTable.insertCount(); + long fullRange = 2 * maxEntries; + if (encodedRIC > fullRange) { + throw decompressionFailed(); + } + long maxValue = insertCount + maxEntries; + long maxWrapped = (maxValue/fullRange) * fullRange; + long ric = maxWrapped + encodedRIC - 1; + if (ric > maxValue) { + if (ric <= fullRange) { + throw decompressionFailed(); + } + ric -= fullRange; + } + + if (ric == 0) { + throw decompressionFailed(); + } + return ric; + } + + private static long decodeBase(long decodedRic, long deltaBase, int signBit) { + if (signBit == 0) { + return decodedRic + deltaBase; + } else { + return decodedRic - deltaBase - 1; + } + } + + private static QPackException decompressionFailed() { + var decompressionFailed = new IllegalStateException("QPACK decompression failed"); + return QPackException.decompressionFailed(decompressionFailed, true); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java new file mode 100644 index 00000000000..83ee21eda30 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeaderField.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +public record HeaderField(String name, String value) { + + public HeaderField(String name) { + this(name, ""); + } + + @Override + public String toString() { + return value.isEmpty() ? name : name + ":" + value; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java new file mode 100644 index 00000000000..4b70777401d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/HeadersTable.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +public sealed interface HeadersTable permits StaticTable, DynamicTable { + + /** + * Add an entry to the table. + * + * @param name header name + * @param value header value + * @return unique index of entry added to the table. + * If element cannot be added {@code -1} is returned. + */ + long insert(String name, String value); + + /** + * Get a table entry with specified unique index. + * + * @param index an entry unique index + * @return table entry + */ + HeaderField get(long index); + + /** + * Returns an index for name:value pair, or just name in a headers table. + * The contract for return values is the following: + * - a positive integer {@code i} where {@code i - 1} is an index of an + * entry with a header (n, v), where {@code n.equals(name) && v.equals(value)}. + *

        + * - a negative integer {@code j} where {@code -j - 1} is an index of an entry with + * a header (n, v), where {@code n.equals(name)}. + *

        + * - {@code 0} if there's no entry 'e' found such that {@code e.getName().equals(name)} + * + * @param name a name to search for + * @param value a value to search for + * @return a non-zero value if a matching entry is found, 0 otherwise + */ + long search(String name, String value); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java new file mode 100644 index 00000000000..1bdf84304ca --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/InsertionPolicy.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +public interface InsertionPolicy { + boolean shouldUpdateDynamicTable(TableEntry tableEntry); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java new file mode 100644 index 00000000000..44282e5ad53 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPACK.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.qpack.QPACK.Logger.Level; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.lang.String.format; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ALLOW_BLOCKING_ENCODING; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_BLOCKED_STREAMS; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_DECODER_MAX_TABLE_CAPACITY; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ENCODER_DRAINING_THRESHOLD; +import static jdk.internal.net.http.Http3ClientProperties.QPACK_ENCODER_TABLE_CAPACITY_LIMIT; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NONE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +/** + * Internal utilities and stuff. + */ +public final class QPACK { + + + // A dynamic table capacity that the encoder is allowed to set given that it doesn't + // exceed the max capacity value negotiated by the decoder. If the max capacity + // less than this limit the encoder's dynamic table capacity is set to the max capacity + // value. + public static final long ENCODER_TABLE_CAPACITY_LIMIT = QPACK_ENCODER_TABLE_CAPACITY_LIMIT; + + // The value of SETTINGS_QPACK_MAX_TABLE_CAPACITY HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long DECODER_MAX_TABLE_CAPACITY = QPACK_DECODER_MAX_TABLE_CAPACITY; + + // The value of SETTINGS_MAX_FIELD_SECTION_SIZE HTTP/3 setting that is + // negotiated by HTTP client's decoder + public static final long DECODER_MAX_FIELD_SECTION_SIZE = QPACK_DECODER_MAX_FIELD_SECTION_SIZE; + + // Decoder upper bound on the number of streams that can be blocked + public static final long DECODER_BLOCKED_STREAMS = QPACK_DECODER_BLOCKED_STREAMS; + + // If set to "true" allows the encoder to insert a header with a dynamic + // name reference and reference it in a field line section without awaiting + // decoder's acknowledgement. + public static final boolean ALLOW_BLOCKING_ENCODING = QPACK_ALLOW_BLOCKING_ENCODING; + + // Threshold of available dynamic table space after which the draining + // index starts increasing. This index determines which entries are + // too close to eviction, and can be referenced by the encoder + public static final int ENCODER_DRAINING_THRESHOLD = QPACK_ENCODER_DRAINING_THRESHOLD; + + private static final RootLogger LOGGER; + private static final Map logLevels = + Map.of("NORMAL", NORMAL, "EXTRA", EXTRA); + + static { + String PROPERTY = "jdk.internal.httpclient.qpack.log.level"; + String value = Utils.getProperty(PROPERTY); + + if (value == null) { + LOGGER = new RootLogger(NONE); + } else { + String upperCasedValue = value.toUpperCase(); + Level l = logLevels.get(upperCasedValue); + if (l == null) { + LOGGER = new RootLogger(NONE); + LOGGER.log(System.Logger.Level.INFO, + () -> format("%s value '%s' not recognized (use %s); logging disabled", + PROPERTY, value, String.join(", ", logLevels.keySet()))); + } else { + LOGGER = new RootLogger(l); + LOGGER.log(System.Logger.Level.DEBUG, + () -> format("logging level %s", l)); + } + } + } + + public static Logger getLogger() { + return LOGGER; + } + + public static SettingsFrame updateDecoderSettings(SettingsFrame defaultSettingsFrame) { + SettingsFrame settingsFrame = defaultSettingsFrame; + settingsFrame.setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, DECODER_BLOCKED_STREAMS); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DECODER_MAX_TABLE_CAPACITY); + settingsFrame.setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, DECODER_MAX_FIELD_SECTION_SIZE); + return settingsFrame; + } + + private QPACK() { } + + /** + * The purpose of this logger is to provide means of diagnosing issues _in + * the QPACK implementation_. It's not a general purpose logger. + */ + // implements System.Logger to make it possible to skip this class + // when looking for the Caller. + public static class Logger implements System.Logger { + + /** + * Log detail level. + */ + public enum Level { + + NONE(0, System.Logger.Level.OFF), + NORMAL(1, System.Logger.Level.DEBUG), + EXTRA(2, System.Logger.Level.TRACE); + + private final int level; + final System.Logger.Level systemLevel; + + Level(int i, System.Logger.Level system) { + level = i; + systemLevel = system; + } + + public final boolean implies(Level other) { + return this.level >= other.level; + } + } + + private final String name; + private final Level level; + private final String path; + private final System.Logger logger; + + private Logger(String path, String name, Level level) { + this.path = path; + this.name = name; + this.level = level; + this.logger = Utils.getHpackLogger(path::toString, level.systemLevel); + } + + public final String getName() { + return name; + } + + @Override + public boolean isLoggable(System.Logger.Level level) { + return logger.isLoggable(level); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String msg, Throwable thrown) { + logger.log(level, bundle, msg,thrown); + } + + @Override + public void log(System.Logger.Level level, ResourceBundle bundle, String format, Object... params) { + logger.log(level, bundle, format, params); + } + + /* + * Usual performance trick for logging, reducing performance overhead in + * the case where logging with the specified level is a NOP. + */ + + public boolean isLoggable(Level level) { + return this.level.implies(level); + } + + public void log(Level level, Supplier s) { + if (this.level.implies(level)) { + logger.log(level.systemLevel, s); + } + } + + public Logger subLogger(String name) { + return new Logger(path + "/" + name, name, level); + } + + } + + private static final class RootLogger extends Logger { + + protected RootLogger(Level level) { + super("qpack", "qpack", level); + } + + } + + // -- low-level utilities -- + + /** + * An interface used to obtain the encoder or decoder stream pair + * from the enclosing HTTP/3 connection. + */ + @FunctionalInterface + public interface StreamPairSupplier { + QueuingStreamPair create(Consumer receiver); + } + + public interface QPACKErrorHandler { + void closeOnError(Throwable throwable, Http3Error error); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java new file mode 100644 index 00000000000..6252a1b9549 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/QPackException.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.http3.Http3Error; + +/** + * Represents a QPack related failure as a failure cause and + * an HTTP/3 error code. + */ +public final class QPackException extends RuntimeException { + + @java.io.Serial + private static final long serialVersionUID = 8443631555257118370L; + + private final boolean isConnectionError; + private final Http3Error http3Error; + + public QPackException(Http3Error http3Error, Throwable cause, boolean isConnectionError) { + super(cause); + this.isConnectionError = isConnectionError; + this.http3Error = http3Error; + } + + public static QPackException encoderStreamError(Throwable cause) { + throw new QPackException(Http3Error.QPACK_ENCODER_STREAM_ERROR, cause, true); + } + + public static QPackException decoderStreamError(Throwable cause) { + throw new QPackException(Http3Error.QPACK_DECODER_STREAM_ERROR, cause, true); + } + + public static QPackException decompressionFailed(Throwable cause, boolean isConnectionError) { + throw new QPackException(Http3Error.QPACK_DECOMPRESSION_FAILED, cause, isConnectionError); + } + + + public Http3Error http3Error() { + return http3Error; + } + + public boolean isConnectionError() { + return isConnectionError; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java new file mode 100644 index 00000000000..52900a2d1c3 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/StaticTable.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/* + * A header table with most common header fields. + * This table was generated by analyzing actual Internet traffic in 2018 + * and is a part of "QPACK: Header Compression for HTTP/3" RFC. + */ +public final class StaticTable implements HeadersTable { + + /* An immutable list of static header fields */ + public static final List HTTP3_HEADER_FIELDS = List.of( + new HeaderField(":authority"), + new HeaderField(":path", "/"), + new HeaderField("age", "0"), + new HeaderField("content-disposition"), + new HeaderField("content-length", "0"), + new HeaderField("cookie"), + new HeaderField("date"), + new HeaderField("etag"), + new HeaderField("if-modified-since"), + new HeaderField("if-none-match"), + new HeaderField("last-modified"), + new HeaderField("link"), + new HeaderField("location"), + new HeaderField("referer"), + new HeaderField("set-cookie"), + new HeaderField(":method", "CONNECT"), + new HeaderField(":method", "DELETE"), + new HeaderField(":method", "GET"), + new HeaderField(":method", "HEAD"), + new HeaderField(":method", "OPTIONS"), + new HeaderField(":method", "POST"), + new HeaderField(":method", "PUT"), + new HeaderField(":scheme", "http"), + new HeaderField(":scheme", "https"), + new HeaderField(":status", "103"), + new HeaderField(":status", "200"), + new HeaderField(":status", "304"), + new HeaderField(":status", "404"), + new HeaderField(":status", "503"), + new HeaderField("accept", "*/*"), + new HeaderField("accept", "application/dns-message"), + new HeaderField("accept-encoding", "gzip, deflate, br"), + new HeaderField("accept-ranges", "bytes"), + new HeaderField("access-control-allow-headers", "cache-control"), + new HeaderField("access-control-allow-headers", "content-type"), + new HeaderField("access-control-allow-origin", "*"), + new HeaderField("cache-control", "max-age=0"), + new HeaderField("cache-control", "max-age=2592000"), + new HeaderField("cache-control", "max-age=604800"), + new HeaderField("cache-control", "no-cache"), + new HeaderField("cache-control", "no-store"), + new HeaderField("cache-control", "public, max-age=31536000"), + new HeaderField("content-encoding", "br"), + new HeaderField("content-encoding", "gzip"), + new HeaderField("content-type", "application/dns-message"), + new HeaderField("content-type", "application/javascript"), + new HeaderField("content-type", "application/json"), + new HeaderField("content-type", "application/x-www-form-urlencoded"), + new HeaderField("content-type", "image/gif"), + new HeaderField("content-type", "image/jpeg"), + new HeaderField("content-type", "image/png"), + new HeaderField("content-type", "text/css"), + new HeaderField("content-type", "text/html; charset=utf-8"), + new HeaderField("content-type", "text/plain"), + new HeaderField("content-type", "text/plain;charset=utf-8"), + new HeaderField("range", "bytes=0-"), + new HeaderField("strict-transport-security", "max-age=31536000"), + new HeaderField("strict-transport-security", "max-age=31536000; includesubdomains"), + new HeaderField("strict-transport-security", "max-age=31536000; includesubdomains; preload"), + new HeaderField("vary", "accept-encoding"), + new HeaderField("vary", "origin"), + new HeaderField("x-content-type-options", "nosniff"), + new HeaderField("x-xss-protection", "1; mode=block"), + new HeaderField(":status", "100"), + new HeaderField(":status", "204"), + new HeaderField(":status", "206"), + new HeaderField(":status", "302"), + new HeaderField(":status", "400"), + new HeaderField(":status", "403"), + new HeaderField(":status", "421"), + new HeaderField(":status", "425"), + new HeaderField(":status", "500"), + new HeaderField("accept-language"), + new HeaderField("access-control-allow-credentials", "FALSE"), + new HeaderField("access-control-allow-credentials", "TRUE"), + new HeaderField("access-control-allow-headers", "*"), + new HeaderField("access-control-allow-methods", "get"), + new HeaderField("access-control-allow-methods", "get, post, options"), + new HeaderField("access-control-allow-methods", "options"), + new HeaderField("access-control-expose-headers", "content-length"), + new HeaderField("access-control-request-headers", "content-type"), + new HeaderField("access-control-request-method", "get"), + new HeaderField("access-control-request-method", "post"), + new HeaderField("alt-svc", "clear"), + new HeaderField("authorization"), + new HeaderField("content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"), + new HeaderField("early-data", "1"), + new HeaderField("expect-ct"), + new HeaderField("forwarded"), + new HeaderField("if-range"), + new HeaderField("origin"), + new HeaderField("purpose", "prefetch"), + new HeaderField("server"), + new HeaderField("timing-allow-origin", "*"), + new HeaderField("upgrade-insecure-requests", "1"), + new HeaderField("user-agent"), + new HeaderField("x-forwarded-for"), + new HeaderField("x-frame-options", "deny"), + new HeaderField("x-frame-options", "sameorigin") + ); + + public static final StaticTable HTTP3 = new StaticTable(HTTP3_HEADER_FIELDS); + + private final List headerFields; + private final Map> indicesMap; + + private StaticTable(List headerFields) { + this.headerFields = headerFields; + this.indicesMap = buildIndicesMap(headerFields); + } + + @Override + public HeaderField get(long index) { + if (index >= headerFields.size()) { + throw new IllegalArgumentException("Invalid static table entry index"); + } + return headerFields.get((int)index); + } + + @Override + public long insert(String name, String value) { + throw new UnsupportedOperationException("Operation not supported by static tables"); + } + + @Override + public long search(String name, String value) { + Map values = indicesMap.get(name); + // 0 return value if no match is found in the static table + int searchResult = 0; + if (values != null) { + Integer idx = values.get(value); + if (idx != null) { + searchResult = idx + 1; + } else { + // Only name is found - return first id from indices for the name provided + searchResult = -values.values().iterator().next() - 1; + } + } + return searchResult; + } + + private static Map> buildIndicesMap(List fields) { + int numEntries = fields.size(); + Map> map = new HashMap<>(numEntries); + for (int i = 0; i < numEntries; i++) { + HeaderField f = fields.get(i); + Map values = map.computeIfAbsent(f.name(), _ -> new HashMap<>()); + values.put(f.value(), i); + } + return map; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java new file mode 100644 index 00000000000..6603c1d4c44 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TableEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack; + +import jdk.internal.net.http.hpack.QuickHuffman; + +// Record containing table information for entry +public record TableEntry(boolean isStaticTable, long index, CharSequence name, CharSequence value, + EntryType type, boolean huffmanName, boolean huffmanValue) { + + public TableEntry(boolean isStaticTable, long index, CharSequence name, CharSequence value, EntryType type) { + this(isStaticTable, index, name, value, type, + isHuffmanBetterFor(name, true, type), + isHuffmanBetterFor(value, false, type)); + } + + public TableEntry toNewDynamicTableEntry(long index) { + return new TableEntry(false, index, name, value, EntryType.NAME_VALUE); + } + + public TableEntry relativizeDynamicTableEntry(long relativeIndex) { + assert !isStaticTable; + assert relativeIndex >= 0; + return new TableEntry(false, relativeIndex, name, value, type); + } + + public TableEntry(CharSequence name, CharSequence value) { + this(false, -1L, name, value, EntryType.NEITHER, + isHuffmanBetterFor(name, true, EntryType.NEITHER), + isHuffmanBetterFor(value, false, EntryType.NEITHER)); + } + + public TableEntry toLiteralsEntry() { + return new TableEntry(name, value); + } + + /** + * EntryType describes the type of TableEntry as either: + *

        + * - NAME_VALUE: a table entry where both name and value exist in table + * - NAME: a table entry where only name is present in table + * - NEITHER: a table entry where neither name nor value have been found + */ + public enum EntryType {NAME_VALUE, NAME, NEITHER} + + static boolean isHuffmanBetterFor(CharSequence str, boolean isName, EntryType type) { + return switch (type) { + case NEITHER -> QuickHuffman.isHuffmanBetterFor(str); + case NAME_VALUE -> false; + case NAME -> !isName && QuickHuffman.isHuffmanBetterFor(str); + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java new file mode 100644 index 00000000000..5afe25d9781 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/TablesIndexer.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack; + +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME_VALUE; + +/* + * Adds reverse lookup to dynamic and static tables. + * Decoder does not need this functionality. On the other hand, + * Encoder does. + */ +public final class TablesIndexer { + + private final DynamicTable dynamicTable; + private final StaticTable staticTable; + + public TablesIndexer(StaticTable staticTable, DynamicTable dynamicTable) { + this.dynamicTable = dynamicTable; + this.staticTable = staticTable; + } + + /** + * Searches in dynamic and static tables for an entry that has matching name + * or name:value. + * Found dynamic table entry ids are matched against provided + * known receive count value if it is non-negative. + * If known receive count value is negative the entry id check is + * not performed. + * + * @param name entry name to search + * @param value entry value to search + * @param knownReceivedCount known received count to match dynamic table + * entries, if negative - id check is not performed. + * @return a table entry that matches provided parameters + */ + public TableEntry entryOf(CharSequence name, CharSequence value, + long knownReceivedCount) { + // Invoking toString() will possibly allocate Strings for the sake of + // the searchDynamic, which doesn't feel right. + String n = name.toString(); + String v = value.toString(); + + // Tests can use -1 known receive count value to filter dynamic table + // entry ids. + boolean limitDynamicTableEntryIds = knownReceivedCount >= 0; + + // 1. Try exact match in the static table + var staticSearchResult = staticTable.search(n, v); + if (staticSearchResult > 0) { + // name:value pair is found in static table + return new TableEntry(true, staticSearchResult - 1, + name, value, NAME_VALUE); + } + // 2. Try exact match in the dynamic table + var dynamicSearchResult = dynamicTable.search(n, v); + if (dynamicSearchResult == 0 && staticSearchResult == 0) { + // dynamic and static tables do not contain name or name:value entries + // - use literal table entry + return new TableEntry(name, value); + } + long dtEntryId; + // name:value hit in dynamic table + if (dynamicSearchResult > 0) { + dtEntryId = dynamicSearchResult - 1; + if (!limitDynamicTableEntryIds || dtEntryId < knownReceivedCount) { + return new TableEntry(false, dtEntryId, name, value, + NAME_VALUE); + } + } + // Name only hit in the static table + if (staticSearchResult < 0) { + return new TableEntry(true, -staticSearchResult - 1, name, + value, NAME); + } + + // Name only hit in the dynamic table + if (dynamicSearchResult < 0) { + dtEntryId = -dynamicSearchResult - 1; + if (!limitDynamicTableEntryIds || dtEntryId < knownReceivedCount) { + return new TableEntry(false, dtEntryId, name, value, NAME); + } + } + + // No match found in the tables, or there is a dynamic table entry that has + // name or 'name:value' match but its index is greater than max allowed dynamic + // table index, ie the entry is not acknowledged by the decoder. + return new TableEntry(name, value); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java new file mode 100644 index 00000000000..d171e81dfbf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +/** + * QPACK (Header Compression for HTTP/3) implementation conforming to + * RFC 9204. + * + *

        Headers can be decoded and encoded by {@link jdk.internal.net.http.qpack.Decoder} + * and {@link jdk.internal.net.http.qpack.Encoder} respectively. + * + *

        Instances of these classes are not safe for use by multiple threads. + */ +package jdk.internal.net.http.qpack; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java new file mode 100644 index 00000000000..70ddace72ce --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/DecoderInstructionsReader.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.QPACK.Logger; +import jdk.internal.net.http.qpack.QPackException; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECODER_STREAM_ERROR; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +/* + * Reader for decoder instructions described in RFC9204 + * "4.4 Encoder Instructions" section. + * Read instructions are passed to the consumer via the DecoderInstructionsReader.Callback + * instance supplied to the reader constructor. + */ +public class DecoderInstructionsReader { + enum State { + INIT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | Stream ID (7+) | + +---+---------------------------+ + */ + SECTION_ACKNOWLEDGMENT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | Stream ID (6+) | + +---+---+-----------------------+ + */ + STREAM_CANCELLATION, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | Increment (6+) | + +---+---+-----------------------+ + */ + INSERT_COUNT_INCREMENT + } + + private State state; + private final IntegerReader integerReader; + private final Callback callback; + private final Logger logger; + + public DecoderInstructionsReader(Callback callback, Logger logger) { + this.integerReader = new IntegerReader( + new ReaderError(QPACK_DECODER_STREAM_ERROR, true)); + this.callback = callback; + this.state = State.INIT; + this.logger = logger.subLogger("DecoderInstructionsReader"); + } + + public void read(ByteBuffer buffer) { + requireNonNull(buffer, "buffer"); + while (buffer.hasRemaining()) { + switch (state) { + case INIT: + integerReader.reset(); + state = identifyDecoderInstruction(buffer); + break; + case INSERT_COUNT_INCREMENT, SECTION_ACKNOWLEDGMENT, STREAM_CANCELLATION: + // All decoder instructions consists of only one variable + // length integer field, therefore we fully read integer and + // then call the callback method depending on the state value + if (integerReader.read(buffer)) { + long value = integerReader.get(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Instruction: %s value: %s", + state.name(), value)); + } + // dispatch instruction to the consumer via the callback + dispatchParsedInstruction(value); + state = State.INIT; + } + break; + } + } + } + + private State identifyDecoderInstruction(ByteBuffer buffer) { + int b = buffer.get(buffer.position()) & 0xFF; // absolute read + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + case 0 -> { + integerReader.configure(7); + yield State.SECTION_ACKNOWLEDGMENT; + } + case 1 -> { + integerReader.configure(6); + yield State.STREAM_CANCELLATION; + } + default -> { + if ((b & 0b1100_0000) == 0) { + integerReader.configure(6); + yield State.INSERT_COUNT_INCREMENT; + } else { + throw QPackException.decoderStreamError( + new IOException("Unexpected decoder instruction: " + b)); + } + } + }; + } + + private void dispatchParsedInstruction(long value) { + switch (state) { + case INSERT_COUNT_INCREMENT: + callback.onInsertCountIncrement(value); + break; + case SECTION_ACKNOWLEDGMENT: + callback.onSectionAck(value); + break; + case STREAM_CANCELLATION: + callback.onStreamCancel(value); + break; + default: + throw QPackException.decoderStreamError( + new IOException("Unknown decoder instruction")); + } + } + + public interface Callback { + void onSectionAck(long streamId); + + void onStreamCancel(long streamId); + + void onInsertCountIncrement(long increment); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java new file mode 100644 index 00000000000..ff8b2868639 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/EncoderInstructionsReader.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static java.lang.System.Logger.Level.TRACE; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.QPACK_ENCODER_STREAM_ERROR; + +/* + * Reader for encoder instructions defined in RFC9204 + * "4.3 Encoder Instructions" section. + * Read instruction is passed to the consumer via Callback + * interface supplied to the EncoderInstructionsReader constructor. + */ +public class EncoderInstructionsReader { + + enum State { + INIT, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | Capacity (5+) | + +---+---+---+-------------------+ + */ + DT_CAPACITY, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Name Index (6+) | + +---+---+-----------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + INSERT_NAME_REF_NAME, + INSERT_NAME_REF_VALUE, + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | H | Name Length (5+) | + +---+---+---+-------------------+ + | Name String (Length bytes) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + INSERT_NAME_LIT_NAME, + INSERT_NAME_LIT_VALUE, + + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | Index (5+) | + +---+---+---+-------------------+ + */ + DUPLICATE + } + + private final QPACK.Logger logger; + private final Callback updateCallback; + private State state; + private final IntegerReader integerReader; + private final StringReader stringReader; + private int bitT = -1; + private long nameIndex = -1L; + private boolean huffmanValue; + private final StringBuilder valueString = new StringBuilder(); + + private boolean huffmanName; + private final StringBuilder nameString = new StringBuilder(); + + public EncoderInstructionsReader(Callback dtUpdateCallback, QPACK.Logger logger) { + this.logger = logger; + this.updateCallback = dtUpdateCallback; + this.state = State.INIT; + var errorToReport = new ReaderError(QPACK_ENCODER_STREAM_ERROR, true); + this.integerReader = new IntegerReader(errorToReport); + this.stringReader = new StringReader(errorToReport); + } + + public void read(ByteBuffer buffer, int maxStringLength) { + try { + read0(buffer, maxStringLength); + } catch (IllegalArgumentException | IllegalStateException exception) { + // "Duplicate" and "Insert With Name Reference" instructions can reference + // non-existing entries in the dynamic table. + // Such errors are treated as encoder stream errors. + throw QPackException.encoderStreamError(exception); + } + } + + private void read0(ByteBuffer buffer, int maxStringLength) { + requireNonNull(buffer, "buffer"); + while (buffer.hasRemaining()) { + switch (state) { + case INIT: + state = identifyEncoderInstruction(buffer); + break; + case DT_CAPACITY: + if (integerReader.read(buffer)) { + long capacity = integerReader.get(); + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Dynamic Table Capacity update: %d", + capacity)); + } + updateCallback.onCapacityUpdate(integerReader.get()); + reset(); + } + break; + case INSERT_NAME_LIT_NAME: + if (stringReader.read(5, buffer, nameString, maxStringLength)) { + huffmanName = stringReader.isHuffmanEncoded(); + stringReader.reset(); + state = State.INSERT_NAME_LIT_VALUE; + } + break; + case INSERT_NAME_LIT_VALUE: + int stringReaderLimit = maxStringLength > 0 ? + Math.max(maxStringLength - nameString.length(), 0) : -1; + if (stringReader.read(buffer, valueString, stringReaderLimit)) { + huffmanValue = stringReader.isHuffmanEncoded(); + // Insert with literal name instruction completely parsed + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Insert with Literal Name ('%s','%s'," + + " huffmanName='%s', huffmanValue='%s')", nameString, + valueString, huffmanName, huffmanValue)); + } + updateCallback.onInsert(nameString.toString(), valueString.toString()); + reset(); + } + break; + case INSERT_NAME_REF_NAME: + if (integerReader.read(buffer)) { + nameIndex = integerReader.get(); + state = State.INSERT_NAME_REF_VALUE; + } + break; + case INSERT_NAME_REF_VALUE: + if (stringReader.read(buffer, valueString, maxStringLength)) { + // Insert with name reference instruction completely parsed + if (logger.isLoggable(TRACE)) { + logger.log(TRACE, () -> format("Insert With Name Reference (T=%d, nameIdx=%d," + + " value='%s', valueHuffman='%s')", + bitT, nameIndex, valueString, stringReader.isHuffmanEncoded())); + } + updateCallback.onInsertIndexedName(bitT == 1, nameIndex, valueString.toString()); + reset(); + } + break; + case DUPLICATE: + if (integerReader.read(buffer)) { + updateCallback.onDuplicate(integerReader.get()); + reset(); + } + break; + } + } + } + + private State identifyEncoderInstruction(ByteBuffer buffer) { + int b = buffer.get(buffer.position()) & 0xFF; // absolute read + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + case 0 -> { + // Configure integer reader to read out name index and read the T bit + integerReader.configure(6); + bitT = (b & 0b0100_0000) == 0 ? 0 : 1; + yield State.INSERT_NAME_REF_NAME; + } + case 1 -> State.INSERT_NAME_LIT_NAME; + case 2 -> { + integerReader.configure(5); + yield State.DT_CAPACITY; + } + default -> { + boolean isDuplicateInstruction = (b & 0b1110_0000) == 0; + if (isDuplicateInstruction) { + integerReader.configure(5); + yield State.DUPLICATE; + } else { + throw QPackException.encoderStreamError( + new InternalError("Unexpected encoder instruction: " + b)); + } + } + }; + } + + public void reset() { + state = State.INIT; + bitT = -1; + nameIndex = -1L; + huffmanName = false; + huffmanValue = false; + resetBuffersAndReaders(); + } + + private void resetBuffersAndReaders() { + integerReader.reset(); + stringReader.reset(); + nameString.setLength(0); + valueString.setLength(0); + } + + public interface Callback { + void onCapacityUpdate(long capacity); + + void onInsert(String name, String value); + + void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString); + + void onDuplicate(long l); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java new file mode 100644 index 00000000000..ce2da3552fa --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedPostBaseReader.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineIndexedPostBaseReader extends FieldLineReader { + private final IntegerReader integerReader; + private final QPACK.Logger logger; + + public FieldLineIndexedPostBaseReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.integerReader = new IntegerReader( + new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + this.logger = logger; + } + + public void configure(int b) { + integerReader.configure(4); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+---+---+---------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!integerReader.read(input)) { + return false; + } + long relativeIndex = integerReader.get(); + long absoluteIndex = prefix.base() + relativeIndex; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Post-Base Indexed Field Line: base=%s index=%s[%s]", + prefix.base(), relativeIndex, absoluteIndex)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + checkSectionSize(DynamicTable.headerSize(f)); + action.onIndexed(absoluteIndex, f.name(), f.value()); + reset(); + return true; + } + + public void reset() { + integerReader.reset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java new file mode 100644 index 00000000000..321a640bb5f --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineIndexedReader.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineIndexedReader extends FieldLineReader { + private final IntegerReader integerReader; + private final QPACK.Logger logger; + + public FieldLineIndexedReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + integerReader = new IntegerReader( + new ReaderError(Http3Error.QPACK_DECOMPRESSION_FAILED, false)); + } + + public void configure(int b) { + integerReader.configure(6); + fromStaticTable = (b & 0b0100_0000) != 0; + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!integerReader.read(input)) { + return false; + } + long intValue = integerReader.get(); + // "In a field line representation, a relative index of 0 refers to the + // entry with absolute index equal to Base - 1." + long absoluteIndex = fromStaticTable ? intValue : prefix.base() - 1 - intValue; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("%s index %s", fromStaticTable ? "Static" : "Dynamic", + absoluteIndex)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + checkSectionSize(DynamicTable.headerSize(f)); + action.onIndexed(absoluteIndex, f.name(), f.value()); + reset(); + return true; + } + + public void reset() { + integerReader.reset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java new file mode 100644 index 00000000000..1dfa81b5631 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineLiteralsReader.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.DynamicTable.ENTRY_SIZE; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineLiteralsReader extends FieldLineReader { + private boolean hideIntermediary; + private boolean huffmanName, huffmanValue; + private final StringBuilder name, value; + private final StringReader stringReader; + private final QPACK.Logger logger; + private boolean firstValueRead = false; + + public FieldLineLiteralsReader(long maxSectionSize, AtomicLong sectionSizeTracker, + QPACK.Logger logger) { + // Dynamic table is not needed for literals reader + super(null, maxSectionSize, sectionSizeTracker); + this.logger = logger; + stringReader = new StringReader(new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + name = new StringBuilder(512); + value = new StringBuilder(1024); + } + + public void configure(int b) { + hideIntermediary = (b & 0b0001_0000) != 0; + } + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | N | H |NameLen(3+)| + // +---+---+-----------------------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + long readPart = ENTRY_SIZE + name.length() + value.length(); + checkPartialSize(readPart); + return false; + } + String n = name.toString(); + String v = value.toString(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with literal name ('%s', huffman=%b, '%s', huffman=%b)", + n, huffmanName, v, huffmanValue)); + } + checkSectionSize(DynamicTable.headerSize(n, v)); + action.onLiteralWithLiteralName(n, huffmanName, v, huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!stringReader.read(3, input, name, getMaxFieldLineLimit(name.length()))) { + return false; + } + huffmanName = stringReader.isHuffmanEncoded(); + stringReader.reset(); + firstValueRead = true; + return false; + } else { + int maxLength = getMaxFieldLineLimit(name.length() + value.length()); + if (!stringReader.read(input, value, maxLength)) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + return true; + } + + public void reset() { + name.setLength(0); + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java new file mode 100644 index 00000000000..420844ad72d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameRefPostBaseReader.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineNameRefPostBaseReader extends FieldLineReader { + private long intValue; + private boolean hideIntermediary; + private boolean huffmanValue; + private final StringBuilder value; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final QPACK.Logger logger; + + private boolean firstValueRead = false; + + FieldLineNameRefPostBaseReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + var errorToReport = new ReaderError(QPACK_DECOMPRESSION_FAILED, false); + integerReader = new IntegerReader(errorToReport); + stringReader = new StringReader(errorToReport); + value = new StringBuilder(1024); + } + + public void configure(int b) { + hideIntermediary = (b & 0b0000_1000) != 0; + integerReader.configure(3); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---+-----------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + if (firstValueRead) { + long readPart = DynamicTable.ENTRY_SIZE + value.length(); + checkPartialSize(readPart); + } + return false; + } + + long absoluteIndex = prefix.base() + intValue; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with post-base name reference (%s, %s, '%s', huffman=%b)", + absoluteIndex, prefix.base(), value, huffmanValue)); + } + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + String valueStr = value.toString(); + checkSectionSize(DynamicTable.headerSize(f.name(), valueStr)); + action.onLiteralWithNameReference(absoluteIndex, + f.name(), valueStr, huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value, getMaxFieldLineLimit())) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + + return true; + } + + public void reset() { + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java new file mode 100644 index 00000000000..f7e3d300628 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineNameReferenceReader.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +final class FieldLineNameReferenceReader extends FieldLineReader { + private long intValue; + private boolean hideIntermediary; + private boolean huffmanValue; + private final StringBuilder value; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final QPACK.Logger logger; + + private boolean firstValueRead = false; + + FieldLineNameReferenceReader(DynamicTable dynamicTable, long maxSectionSize, + AtomicLong sectionSizeTracker, QPACK.Logger logger) { + super(dynamicTable, maxSectionSize, sectionSizeTracker); + this.logger = logger; + var errorToReport = new ReaderError(QPACK_DECOMPRESSION_FAILED, false); + integerReader = new IntegerReader(errorToReport); + stringReader = new StringReader(errorToReport); + value = new StringBuilder(1024); + } + + public void configure(int b) { + fromStaticTable = (b & 0b0001_0000) != 0; + hideIntermediary = (b & 0b0010_0000) != 0; + integerReader.configure(4); + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T | NameIndex (4+)| + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + public boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action) { + if (!completeReading(input)) { + if (firstValueRead) { + long readPart = DynamicTable.ENTRY_SIZE + value.length(); + checkPartialSize(readPart); + } + return false; + } + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format( + "literal with name reference (%s, %s, '%s', huffman=%b)", + fromStaticTable ? "static" : "dynamic", intValue, value, huffmanValue)); + } + long absoluteIndex = fromStaticTable ? intValue : prefix.base() - 1 - intValue; + checkEntryIndex(absoluteIndex, prefix); + HeaderField f = entryAtIndex(absoluteIndex); + String valueStr = value.toString(); + checkSectionSize(DynamicTable.headerSize(f.name(), valueStr)); + action.onLiteralWithNameReference(absoluteIndex, f.name(), valueStr, + huffmanValue, hideIntermediary); + reset(); + return true; + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value, getMaxFieldLineLimit())) { + return false; + } + } + huffmanValue = stringReader.isHuffmanEncoded(); + stringReader.reset(); + + return true; + } + + public void reset() { + value.setLength(0); + firstValueRead = false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java new file mode 100644 index 00000000000..da67f92bf19 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/FieldLineReader.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.StaticTable; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +sealed abstract class FieldLineReader permits FieldLineIndexedPostBaseReader, + FieldLineIndexedReader, FieldLineLiteralsReader, FieldLineNameRefPostBaseReader, + FieldLineNameReferenceReader { + + final long maxSectionSize; + boolean fromStaticTable; + private final AtomicLong sectionSizeTracker; + private final DynamicTable dynamicTable; + + FieldLineReader(DynamicTable dynamicTable, long maxSectionSize, AtomicLong sectionSizeTracker) { + this.maxSectionSize = maxSectionSize; + this.sectionSizeTracker = sectionSizeTracker; + this.dynamicTable = dynamicTable; + } + + abstract void reset(); + abstract void configure(int b); + abstract boolean read(ByteBuffer input, FieldSectionPrefix prefix, + DecodingCallback action); + + final void checkSectionSize(long fieldSize) { + long sectionSize = sectionSizeTracker.addAndGet(fieldSize); + if (maxSectionSize > 0 && sectionSize > maxSectionSize) { + throw maxFieldSectionExceeded(sectionSize, maxSectionSize); + } + } + + final void checkPartialSize(long partialFieldSize) { + long sectionSize = sectionSizeTracker.get() + partialFieldSize; + if (maxSectionSize > 0 && sectionSize > maxSectionSize) { + throw maxFieldSectionExceeded(sectionSize, maxSectionSize); + } + } + + final int getMaxFieldLineLimit(int partiallyRead) { + int maxLimit = -1; + if (maxSectionSize > 0) { + maxLimit = Math.clamp(maxSectionSize - partiallyRead - 32 - + sectionSizeTracker.get(), 0, Integer.MAX_VALUE); + } + return maxLimit; + } + + final int getMaxFieldLineLimit() { + return getMaxFieldLineLimit(0); + } + + private static QPackException maxFieldSectionExceeded(long sectionSize, long maxSize) { + throw QPackException.decompressionFailed( + new ProtocolException("Size exceeds MAX_FIELD_SECTION_SIZE: %s > %s" + .formatted(sectionSize, maxSize)), false); + } + + /** + * Checks if the decoder encounters a reference in a field line representation to + * a dynamic table entry that has already been evicted or that has an absolute index + * greater than or equal to the declared Required Insert Count (Section 4.5.1), + * it MUST treat this as a connection error of type QPACK_DECOMPRESSION_FAILED. + * @param absoluteIndex dynamic table absolute index + * @param prefix field line section prefix + */ + void checkEntryIndex(long absoluteIndex, FieldSectionPrefix prefix) { + if (!fromStaticTable && absoluteIndex >= prefix.requiredInsertCount()) { + throw QPackException.decompressionFailed( + new IOException("header index is greater than RIC"), true); + } + } + + /** + * Return a header field entry for the specified entry index. The table type + * is selected according to the {@code fromStaticTable} value. + * @param index absolute index of the table entry. + * @return a header field corresponding to the specified entry + */ + final HeaderField entryAtIndex(long index) { + HeaderField f; + try { + if (fromStaticTable) { + f = StaticTable.HTTP3.get(index); + } else { + assert dynamicTable != null; + f = dynamicTable.get(index); + } + } catch (IndexOutOfBoundsException | IllegalStateException | IllegalArgumentException e) { + throw QPackException.decompressionFailed( + new IOException("header fields table index", e), true); + } + return f; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java new file mode 100644 index 00000000000..a4ea55661fe --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/HeaderFrameReader.java @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.quic.streams.QuicStreamReader; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static jdk.internal.net.http.http3.Http3Error.H3_INTERNAL_ERROR; +import static jdk.internal.net.http.http3.Http3Error.QPACK_DECOMPRESSION_FAILED; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.NORMAL; + +public class HeaderFrameReader { + + private enum State { + // Nothing has been read so-far, "Required Insert Count" (RIC) will be read next + INITIAL, + // "Required Insert Count" read is done, "S" and "Delta Base" are next + DELTA_BASE, + // Encoded Field Section Prefix read is done, ready to start reading header fields. + // In this state we only select a proper reader based on the field line encoding type, + // ie the first byte is analysed to select a proper reader. + SELECT_FIELD_READER, + INDEXED, + INDEX_WITH_POST_BASE, + LITERAL_WITH_LITERAL_NAME, + LITERAL_WITH_NAME_REF, + LITERAL_WITH_POST_BASE, + AWAITING_DT_INSERT_COUNT + } + + /* + 4.5.1. Encoded Field Section Prefix + Each encoded field section is prefixed with two integers. The Required Insert Count + is encoded as an integer with an 8-bit prefix using the encoding described in Section 4.5.1.1. + The Base is encoded as a Sign bit ('S') and a Delta Base value with a 7-bit prefix; + see Section 4.5.1.2. + + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | Required Insert Count (8+) | + +---+---------------------------+ + | S | Delta Base (7+) | + +---+---------------------------+ + */ + long requiredInsertCount; + long deltaBase; + int signBit; + volatile FieldSectionPrefix fieldSectionPrefix; + private final IntegerReader integerReader; + private FieldLineReader reader; + private final QPACK.Logger logger; + private final FieldLineIndexedReader indexedReader; + private final FieldLineIndexedPostBaseReader indexedPostBaseReader; + private final FieldLineNameReferenceReader literalWithNameReferenceReader; + private final FieldLineNameRefPostBaseReader literalWithNameRefPostBaseReader; + + private final FieldLineLiteralsReader literalWithLiteralNameReader; + // Need dynamic table reference for decoding field line section prefix + private final DynamicTable dynamicTable; + private final DecodingCallback decodingCallback; + + private volatile State state = State.INITIAL; + + private final SequentialScheduler headersScheduler = SequentialScheduler.lockingScheduler(this::readLoop); + private final ConcurrentLinkedQueue headersData = new ConcurrentLinkedQueue<>(); + + private final AtomicLong blockedStreamsCounter; + private final long maxBlockedStreams; + + // A tracker of header data received by the decoder, to check that the peer encoder + // honours the SETTINGS_MAX_FIELD_SECTION_SIZE value: + // RFC-9114: 4.2.2. Header Size Constraints + // "If an implementation wishes to advise its peer of this limit, it can + // be conveyed as a number of bytes in the SETTINGS_MAX_FIELD_SECTION_SIZE parameter. + // An implementation that has received this parameter SHOULD NOT send an HTTP message + // header that exceeds the indicated size" + // "A client can discard responses that it cannot process." + // + // Maximum allowed value is passed to FieldLineReader's implementations and not stored in + // HeaderFrameReader instance. + private final AtomicLong fieldSectionSizeTracker; + + private static final AtomicLong HEADER_FRAME_READER_IDS = new AtomicLong(); + + private void readLoop() { + try { + readLoop0(); + } catch (QPackException qPackException) { + Throwable cause = qPackException.getCause(); + if (qPackException.isConnectionError()) { + decodingCallback.onConnectionError(cause, qPackException.http3Error()); + } else { + decodingCallback.onStreamError(cause, qPackException.http3Error()); + } + } catch (Throwable throwable) { + decodingCallback.onConnectionError(throwable, H3_INTERNAL_ERROR); + } finally { + // Stop the scheduler, clear the reader's queue and + // remove all insert count notification events associated + // with current stream. + if (decodingCallback.hasError()) { + headersScheduler.stop(); + headersData.clear(); + dynamicTable.cleanupStreamInsertCountNotifications(decodingCallback.streamId()); + } + } + } + + private void readLoop0() { + ByteBuffer headerBlock; + OUTER: + while (!decodingCallback.hasError() && (headerBlock = headersData.peek()) != null) { + boolean endOfHeaderBlock = headerBlock == QuicStreamReader.EOF; + State state = this.state; + FieldSectionPrefix sectionPrefix = this.fieldSectionPrefix; + while (!decodingCallback.hasError() && headerBlock.hasRemaining()) { + if (state == State.SELECT_FIELD_READER) { + int b = headerBlock.get(headerBlock.position()) & 0xff; // absolute read + state = this.state = selectHeaderReaderState(b); + if (logger.isLoggable(EXTRA)) { + String message = format("next binary representation %s (first byte 0x%02x)", state, b); + logger.log(EXTRA, () -> message); + } + reader = switch (state) { + case INDEXED -> indexedReader; + case LITERAL_WITH_NAME_REF -> literalWithNameReferenceReader; + case LITERAL_WITH_LITERAL_NAME -> literalWithLiteralNameReader; + case INDEX_WITH_POST_BASE -> indexedPostBaseReader; + case LITERAL_WITH_POST_BASE -> literalWithNameRefPostBaseReader; + default -> throw QPackException.decompressionFailed( + new InternalError("Unexpected decoder state: " + state), false); + }; + reader.configure(b); + } else if (state == State.INITIAL) { + if (!integerReader.read(headerBlock)) { + continue; + } + // Required Insert Count was fully read + requiredInsertCount = integerReader.get(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Encoded Required Insert Count = %d", requiredInsertCount)); + } + // Continue reading S and Delta Base values + state = this.state = State.DELTA_BASE; + // Reset integer reader + integerReader.reset(); + // Prepare it for reading S and Delta Base (7+) + integerReader.configure(7); + continue; + } else if (state == State.DELTA_BASE) { + if (signBit == -1) { + int b = headerBlock.get(headerBlock.position()) & 0xff; // absolute read + signBit = (b & 0b1000_0000) == 0b1000_0000 ? 1 : 0; + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Base Sign = %d", signBit)); + } + } + if (!integerReader.read(headerBlock)) { + continue; + } + deltaBase = integerReader.get(); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("Delta Base = %d", deltaBase)); + } + // Construct field section prefix from the parsed fields + sectionPrefix = this.fieldSectionPrefix = + FieldSectionPrefix.decode(requiredInsertCount, deltaBase, + signBit, dynamicTable); + + // Check if decoding of field section is blocked due to not yet received + // dynamic table entries + long insertCount = dynamicTable.insertCount(); + if (sectionPrefix.requiredInsertCount() > insertCount) { + long blocked = blockedStreamsCounter.incrementAndGet(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, + () -> "Blocked stream observed. Total blocked: " + blocked + + " Max allowed: " + maxBlockedStreams); + } + // System property value is checked here instead of the HTTP3 settings because decoder uses its + // value to update connection settings. HTTP client's encoder implementation won't block the streams - + // only acknowledged entry references is used, therefore this connection setting is not consulted + // on encoder side. + if (blocked > maxBlockedStreams) { + var ioException = new IOException(("too many blocked streams: current=%d; max=%d; " + + "prefixCount=%d; tableCount=%d").formatted(blocked, maxBlockedStreams, + sectionPrefix.requiredInsertCount(), insertCount)); + // If a decoder encounters more blocked streams than it promised to support, + // it MUST treat this as a connection error of type QPACK_DECOMPRESSION_FAILED. + throw QPackException.decompressionFailed(ioException, true); + } else { + CompletableFuture future = + dynamicTable.awaitFutureInsertCount(decodingCallback.streamId(), + sectionPrefix.requiredInsertCount()); + state = this.state = State.AWAITING_DT_INSERT_COUNT; + future.thenRun(this::onInsertCountUpdate); + } + break OUTER; + } + // The stream is unblocked - field lines can be decoded now + state = this.state = State.SELECT_FIELD_READER; + continue; + } else if (state == State.AWAITING_DT_INSERT_COUNT) { + // If we're waiting for a specific dynamic table update + return; + } + if (reader.read(headerBlock, sectionPrefix, decodingCallback)) { + // Finished reading of one header field line + state = this.state = State.SELECT_FIELD_READER; + } + } + if (!headerBlock.hasRemaining()) { + var head = headersData.poll(); + assert head == headerBlock; + } + if (endOfHeaderBlock) { + if (state == State.SELECT_FIELD_READER) { + decodingCallback.onComplete(); + } else { + logger.log(NORMAL, () -> "unexpected end of representation"); + throw QPackException.decompressionFailed( + new ProtocolException("Unexpected end of header block"), true); + } + } + } + } + + private void onInsertCountUpdate() { + long blocked = blockedStreamsCounter.decrementAndGet(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> "Stream Unblocked - number of blocked streams: " + blocked); + } + state = State.SELECT_FIELD_READER; + headersScheduler.runOrSchedule(); + } + + public HeaderFrameReader(DynamicTable dynamicTable, DecodingCallback callback, + AtomicLong blockedStreamsCounter, long maxBlockedStreams, + long maxFieldSectionSize, QPACK.Logger logger) { + this.blockedStreamsCounter = blockedStreamsCounter; + this.logger = logger.subLogger("HeaderFrameReader#" + + HEADER_FRAME_READER_IDS.incrementAndGet()); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("New HeaderFrameReader, dynamic table capacity = %s", + dynamicTable.capacity())); + /* To correlate with logging outside QPACK, knowing + hashCode/toString is important */ + logger.log(NORMAL, () -> { + String hashCode = Integer.toHexString(System.identityHashCode(this)); + return format("toString='%s', identityHashCode=%s", this, hashCode); + }); + } + this.fieldSectionSizeTracker = new AtomicLong(); + indexedReader = new FieldLineIndexedReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineIndexedReader")); + indexedPostBaseReader = new FieldLineIndexedPostBaseReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineIndexedPostBaseReader")); + literalWithNameReferenceReader = new FieldLineNameReferenceReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineNameReferenceReader")); + literalWithNameRefPostBaseReader = new FieldLineNameRefPostBaseReader(dynamicTable, + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineNameRefPostBaseReader")); + literalWithLiteralNameReader = new FieldLineLiteralsReader( + maxFieldSectionSize, fieldSectionSizeTracker, + this.logger.subLogger("FieldLineLiteralsReader")); + integerReader = new IntegerReader(new ReaderError(QPACK_DECOMPRESSION_FAILED, false)); + resetPrefixVars(); + // Since reader is constructed in Initial state - it means that the + // "Required Insert Count" will be read first. + integerReader.configure(8); + decodingCallback = callback; + this.dynamicTable = dynamicTable; + this.maxBlockedStreams = maxBlockedStreams; + } + + private void resetPrefixVars() { + requiredInsertCount = -1L; + deltaBase = -1L; + signBit = -1; + fieldSectionPrefix = null; + fieldSectionSizeTracker.set(0); + } + + public FieldSectionPrefix decodedSectionPrefix() { + if (deltaBase == -1L) { + throw new IllegalStateException("Field Section Prefix not parsed yet"); + } + return fieldSectionPrefix; + } + + public void read(ByteBuffer headerBlock, boolean endOfHeaderBlock) { + requireNonNull(headerBlock, "headerBlock"); + if (logger.isLoggable(NORMAL)) { + logger.log(NORMAL, () -> format("reading %s, end of header block? %s", + headerBlock, endOfHeaderBlock)); + } + headersData.add(headerBlock); + if (endOfHeaderBlock) { + headersData.add(QuicStreamReader.EOF); + } + headersScheduler.runOrSchedule(); + } + + private State selectHeaderReaderState(int b) { + // First non-zero bit in lower 8 bits (see the caller) + int pos = Integer.numberOfLeadingZeros(b) - 24; + return switch (pos) { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | T | Index (6+) | + +---+---+-----------------------+ + */ + case 0 -> State.INDEXED; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | N | T |Name Index (4+)| + +---+---+---+---+---------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + case 1 -> State.LITERAL_WITH_NAME_REF; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 1 | N | H |NameLen(3+)| + +---+---+---+---+---+-----------+ + | Name String (Length bytes) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + case 2 -> State.LITERAL_WITH_LITERAL_NAME; + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 1 | Index (4+) | + +---+---+---+---+---------------+ + */ + case 3 -> State.INDEX_WITH_POST_BASE; + // "Literal Field Line with Post-Base Name Reference": + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---------------+ + default -> { + if ((b & 0xF0) == 0) { + yield State.LITERAL_WITH_POST_BASE; + } + throw QPackException.decompressionFailed( + new IOException("Unknown frame reader line prefix: " + b), + false); + } + }; + } + + /** + * Reset the state of the HeaderFrameReader so that it's ready + * to parse a new HeaderFrame. + */ + public void reset() { + state = State.INITIAL; + reader = null; + resetPrefixVars(); + integerReader.reset(); + integerReader.configure(8); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java new file mode 100644 index 00000000000..35025c3f06a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/IntegerReader.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static java.lang.String.format; + +/** + * This class able to decode integers up to and including 62 bits long values. + * https://www.rfc-editor.org/rfc/rfc9204.html#name-prefixed-integers + */ +public final class IntegerReader { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_READ = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int N; + private long maxValue; + private long value; + private long r; + private long b = 1; + private final ReaderError readError; + + public IntegerReader(ReaderError readError) { + this.readError = readError; + } + + public IntegerReader() { + this(new ReaderError(Http3Error.H3_INTERNAL_ERROR, true)); + } + + // "QPACK implementations MUST be able to decode integers up to and including 62 bits long." + // https://www.rfc-editor.org/rfc/rfc9204.html#name-prefixed-integers + public static final long QPACK_MAX_INTEGER_VALUE = (1L << 62) - 1; + + public IntegerReader configure(int N) { + return configure(N, QPACK_MAX_INTEGER_VALUE); + } + + // + // Why is it important to configure 'maxValue' here. After all we can wait + // for the integer to be fully read and then check it. Can't we? + // + // Two reasons. + // + // 1. Value wraps around long won't be unnoticed. + // 2. It can spit out an exception as soon as it becomes clear there's + // an overflow. Therefore, no need to wait for the value to be fully read. + // + public IntegerReader configure(int N, long maxValue) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + checkPrefix(N); + if (maxValue < 0) { + throw new IllegalArgumentException( + "maxValue >= 0: maxValue=" + maxValue); + } + this.maxValue = maxValue; + this.N = N; + state = CONFIGURED; + return this; + } + + public boolean read(ByteBuffer input) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + int n = input.get() & max; + if (n != max) { + value = n; + state = DONE; + return true; + } else { + r = max; + } + state = FIRST_BYTE_READ; + } + if (state == FIRST_BYTE_READ) { + try { + // variable-length quantity (VLQ) + byte i; + boolean continuationFlag; + do { + if (!input.hasRemaining()) { + return false; + } + i = input.get(); + // RFC 7541: 5.1. Integer Representation + // "The most significant bit of each octet is used + // as a continuation flag: its value is set to 1 except + // for the last octet in the list" + continuationFlag = (i & 0b10000000) != 0; + long increment = Math.multiplyExact(b, i & 127); + if (continuationFlag) { + b = Math.multiplyExact(b, 128); + } + if (r > maxValue - increment) { + throw readError.toQPackException( + new IOException(format( + "Integer overflow: maxValue=%,d, value=%,d", + maxValue, r + increment))); + } + r += increment; + } while (continuationFlag); + value = r; + state = DONE; + return true; + } catch (ArithmeticException arithmeticException) { + // Sequence of bytes encodes value greater + // than QPACK_MAX_INTEGER_VALUE + throw readError.toQPackException(new IOException("Integer overflow", + arithmeticException)); + } + } + throw new InternalError(Arrays.toString( + new Object[]{state, N, maxValue, value, r, b})); + } + + public long get() throws IllegalStateException { + if (state != DONE) { + throw new IllegalStateException("Has not been fully read yet"); + } + return value; + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public IntegerReader reset() { + b = 1; + state = NEW; + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java new file mode 100644 index 00000000000..3be6033f827 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/ReaderError.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.readers; + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.QPackException; + +/** + * QPack readers configuration record to be used by the readers to + * report errors. + * @param http3Error corresponding HTTP/3 error code. + * @param isConnectionError if the reader error should be treated + * as connection error. + */ +record ReaderError(Http3Error http3Error, boolean isConnectionError) { + + /** + * Construct a {@link QPackException} from on {@code http3Error}, + * {@code isConnectionError} and provided {@code "cause"} values. + * @param cause cause of the constructed {@link QPackException} + * @return a {@code QPackException} instance. + */ + QPackException toQPackException(Throwable cause) { + return new QPackException(http3Error, cause, isConnectionError); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java new file mode 100644 index 00000000000..0cd0c92e91a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/readers/StringReader.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.readers; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.hpack.ISO_8859_1; +import jdk.internal.net.http.hpack.Huffman; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.http3.Http3Error; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +public final class StringReader { + + private static final int NEW = 0; + private static final int FIRST_BYTE_READ = 1; + private static final int LENGTH_READ = 2; + private static final int DONE = 4; + + private final ReaderError readError; + private final IntegerReader intReader; + private final Huffman.Reader huffmanReader = new QuickHuffman.Reader(); + private final ISO_8859_1.Reader plainReader = new ISO_8859_1.Reader(); + + private int state = NEW; + private boolean huffman; + private int remainingLength; + + public StringReader() { + this(new ReaderError(Http3Error.H3_INTERNAL_ERROR, true)); + } + + public StringReader(ReaderError readError) { + this.readError = readError; + this.intReader = new IntegerReader(readError); + } + + public boolean read(ByteBuffer input, Appendable output, int maxLength) { + return read(7, input, output, maxLength); + } + + boolean read(int N, ByteBuffer input, Appendable output, int maxLength) { + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == NEW) { + int huffmanBit = switch (N) { + case 7 -> 0b1000_0000; // for all value strings + case 5 -> 0b0010_0000; // in name string for insert literal + case 3 -> 0b0000_1000; // in name string for literal + default -> throw new IllegalStateException("Unexpected value: " + N); + }; + int p = input.position(); + huffman = (input.get(p) & huffmanBit) != 0; + state = FIRST_BYTE_READ; + intReader.configure(N); + } + if (state == FIRST_BYTE_READ) { + boolean lengthRead = intReader.read(input); + if (!lengthRead) { + return false; + } + long remainingLengthLong = intReader.get(); + if (maxLength >= 0) { + long huffmanEstimate = huffman ? + remainingLengthLong / 4 : remainingLengthLong; + if (huffmanEstimate > maxLength) { + throw readError.toQPackException(new ProtocolException( + "Size exceeds MAX_FIELD_SECTION_SIZE or dynamic table capacity.")); + } + } + remainingLength = (int) remainingLengthLong; + state = LENGTH_READ; + } + if (state == LENGTH_READ) { + boolean isLast = input.remaining() >= remainingLength; + int oldLimit = input.limit(); + if (isLast) { + input.limit(input.position() + remainingLength); + } + remainingLength -= Math.min(input.remaining(), remainingLength); + try { + if (huffman) { + huffmanReader.read(input, output, isLast); + } else { + plainReader.read(input, output); + } + } catch (IOException ioe) { + throw readError.toQPackException(ioe); + } + if (isLast) { + input.limit(oldLimit); + state = DONE; + } + return isLast; + } + throw new InternalError(Arrays.toString( + new Object[]{state, huffman, remainingLength})); + } + + public boolean isHuffmanEncoded() { + if (state < FIRST_BYTE_READ) { + throw new IllegalStateException("Has not been fully read yet"); + } + return huffman; + } + + public void reset() { + if (huffman) { + huffmanReader.reset(); + } else { + plainReader.reset(); + } + intReader.reset(); + state = NEW; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java new file mode 100644 index 00000000000..b028b994df2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/BinaryRepresentationWriter.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; + +interface BinaryRepresentationWriter { + boolean write(ByteBuffer destination); + + BinaryRepresentationWriter reset(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java new file mode 100644 index 00000000000..c27f46e2761 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/DecoderInstructionsWriter.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class DecoderInstructionsWriter { + private final QPACK.Logger logger; + private boolean encoding; + private static final AtomicLong IDS = new AtomicLong(); + + private final IntegerWriter integerWriter = new IntegerWriter(); + + public DecoderInstructionsWriter() { + long id = IDS.incrementAndGet(); + this.logger = QPACK.getLogger().subLogger("DecoderInstructionsWriter#" + id); + } + + /* + * Configure the writer for encoding "Section Acknowledgment" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 1 | Stream ID (7+) | + * +---+---------------------------+ + */ + public int configureForSectionAck(long streamId) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(streamId, 7, 0b1000_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Section Acknowledgment for stream id=%s", + streamId)); + } + return IntegerWriter.requiredBufferSize(7, streamId); + } + + /* + * Configure the writer for encoding "Stream Cancellation" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 1 | Stream ID (6+) | + * +---+---+-----------------------+ + */ + public int configureForStreamCancel(long streamId) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(streamId, 6, 0b0100_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Stream Cancellation for stream id=%s", + streamId)); + } + return IntegerWriter.requiredBufferSize(6, streamId); + } + + /* + * Configure the writer for encoding "Insert Count Increment" decoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | Increment (6+) | + * +---+---+-----------------------+ + */ + public int configureForInsertCountInc(long increment) { + checkIfEncodingInProgress(); + encoding = true; + integerWriter.configure(increment, 6, 0b0000_0000); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Insert Count Increment value=%s", + increment)); + } + return IntegerWriter.requiredBufferSize(6, increment); + } + + public boolean write(ByteBuffer byteBuffer) { + if (!encoding) { + throw new IllegalStateException("Writer hasn't been configured"); + } + boolean done = integerWriter.write(byteBuffer); + if (done) { + integerWriter.reset(); + encoding = false; + } + return done; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java new file mode 100644 index 00000000000..29f6cac9ed8 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDuplicateEntryWriter.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; + +final class EncoderDuplicateEntryWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter; + + public EncoderDuplicateEntryWriter() { + this.intWriter = new IntegerWriter(); + } + + public EncoderDuplicateEntryWriter configure(long relativeId) { + // IntegerWriter.configure checks if the relative id value is not negative + intWriter.configure(relativeId, 5, 0b0000_0000); + // Need to store entry id for adding a duplicate to the dynamic table + // once write operation is completed + return this; + } + + @Override + public boolean write(ByteBuffer destination) { + // IntegerWriter.write checks if it was properly configured + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java new file mode 100644 index 00000000000..1a920ff5b7b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderDynamicTableCapacityWriter.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; + +final class EncoderDynamicTableCapacityWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter; + + public EncoderDynamicTableCapacityWriter() { + this.intWriter = new IntegerWriter(); + } + + public EncoderDynamicTableCapacityWriter configure(long capacity) { + // IntegerWriter.configure checks if the capacity value is not negative + intWriter.configure(capacity, 5, 0b0010_0000); + return this; + } + + @Override + public boolean write(ByteBuffer destination) { + // IntegerWriter.write checks if it was properly configured + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java new file mode 100644 index 00000000000..d1851a0c204 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertIndexedNameWriter.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +class EncoderInsertIndexedNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + public EncoderInsertIndexedNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Encoder Insert With %s Table Name Reference (%s, '%s', huffman=%b)", + e.isStaticTable() ? "Static" : "Dynamic", e.index(), e.value(), e.huffmanValue())); + } + return this.index(e).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private EncoderInsertIndexedNameWriter index(TableEntry e) { + int N = 6; + int payload = 0b1000_0000; + long index = e.index(); + if (e.isStaticTable()) { + payload |= 0b0100_0000; + } + intWriter.configure(index, N, payload); + return this; + } + + private EncoderInsertIndexedNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java new file mode 100644 index 00000000000..1783f60b062 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInsertLiteralNameWriter.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class EncoderInsertLiteralNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + EncoderInsertLiteralNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Insert With Literal Name (%s, '%s', huffmanName=%b, huffmanValue=%b)", + e.name(), e.value(), e.huffmanName(), e.huffmanValue())); + } + return this.name(e).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!nameWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + nameWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private EncoderInsertLiteralNameWriter name(TableEntry e) { + int N = 5; + int payload = 0b0100_0000; + if (e.huffmanName()) { + payload |= 0b0010_0000; + } + nameWriter.configure(e.name(), N, payload, e.huffmanName()); + return this; + } + + private EncoderInsertLiteralNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java new file mode 100644 index 00000000000..c4a22436083 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/EncoderInstructionsWriter.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class EncoderInstructionsWriter { + private BinaryRepresentationWriter writer; + private final QPACK.Logger logger; + private final EncoderInsertIndexedNameWriter insertIndexedNameWriter; + private final EncoderInsertLiteralNameWriter insertLiteralNameWriter; + private final EncoderDuplicateEntryWriter duplicateWriter; + private final EncoderDynamicTableCapacityWriter capacityWriter; + private boolean encoding; + private static final AtomicLong ENCODERS_IDS = new AtomicLong(); + + public EncoderInstructionsWriter() { + this(QPACK.getLogger()); + } + + public EncoderInstructionsWriter(QPACK.Logger parentLogger) { + long id = ENCODERS_IDS.incrementAndGet(); + this.logger = parentLogger.subLogger("EncoderInstructionsWriter#" + id); + // Writer for "Insert with Name Reference" encoder instruction + insertIndexedNameWriter = new EncoderInsertIndexedNameWriter( + logger.subLogger("EncoderInsertIndexedNameWriter")); + // Writer for "Insert with Literal Name" encoder instruction + insertLiteralNameWriter = new EncoderInsertLiteralNameWriter( + logger.subLogger("EncoderInsertLiteralNameWriter")); + // Writer for "Set Dynamic Table Capacity" encoder instruction + capacityWriter = new EncoderDynamicTableCapacityWriter(); + // Writer for "Duplicate" encoder instruction + duplicateWriter = new EncoderDuplicateEntryWriter(); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Insert with Name Reference" or "Insert with Literal Name" + * encoder instruction. The instruction is selected based on TableEntry.type() value: + * "Insert with Name Reference" is selected for TableEntry.EntryType.NAME: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 1 | T | Name Index (6+) | + * +---+---+-----------------------+ + * | H | Value Length (7+) | + * +---+---------------------------+ + * | Value String (Length bytes) | + * +-------------------------------+ + * + * "Insert with Literal Name" is selected for TableEntry.EntryType.NEITHER: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 1 | H | Name Length (5+) | + * +---+---+---+-------------------+ + * | Name String (Length bytes) | + * +---+---------------------------+ + * | H | Value Length (7+) | + * +---+---------------------------+ + * | Value String (Length bytes) | + * +-------------------------------+ + */ + public int configureForEntryInsertion(TableEntry e) { + checkIfEncodingInProgress(); + encoding = true; + writer = switch (e.type()) { + case NAME -> insertIndexedNameWriter.configure(e); + case NEITHER -> insertLiteralNameWriter.configure(e); + default -> throw new IllegalArgumentException("Unsupported table entry insertion type: " + e.type()); + }; + return calculateEntryInsertionSize(e); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Duplicate" encoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | 0 | Index (5+) | + * +---+---+---+-------------------+ + */ + public int configureForEntryDuplication(long entryIndexToDuplicate) { + checkIfEncodingInProgress(); + encoding = true; + duplicateWriter.configure(entryIndexToDuplicate); + writer = duplicateWriter; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("duplicate entry with id=%s", entryIndexToDuplicate)); + } + return IntegerWriter.requiredBufferSize(5, entryIndexToDuplicate); + } + + /* + * Configure EncoderInstructionsWriter for encoding "Set Dynamic Table Capacity" encoder instruction: + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | 0 | 0 | 1 | Capacity (5+) | + * +---+---+---+-------------------+ + */ + public int configureForTableCapacityUpdate(long tableCapacity) { + checkIfEncodingInProgress(); + encoding = true; + capacityWriter.configure(tableCapacity); + writer = capacityWriter; + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("set dynamic table capacity to %s", tableCapacity)); + } + return IntegerWriter.requiredBufferSize(5, tableCapacity); + } + + + public boolean write(ByteBuffer byteBuffer) { + if (!encoding) { + throw new IllegalStateException("Writer hasn't been configured"); + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("writing to %s", byteBuffer)); + } + boolean done = writer.write(byteBuffer); + if (done) { + writer.reset(); + encoding = false; + } + return done; + } + + private int calculateEntryInsertionSize(TableEntry e) { + int vlen = Math.min(QuickHuffman.lengthOf(e.value()), e.value().length()); + int integerValuesSize; + return switch (e.type()) { + case NAME -> { + // Calculate how many bytes are needed to encode the index part: + // | 1 | T | Name Index (6+) | + integerValuesSize = IntegerWriter.requiredBufferSize(6, e.index()); + // Calculate how many bytes are needed to encode the value length part: + // | H | Value Length (7+) | + integerValuesSize += IntegerWriter.requiredBufferSize(7, vlen); + // We also need vlen bytes for the value string content + yield integerValuesSize + vlen; + } + case NEITHER -> { + int nlen = Math.min(QuickHuffman.lengthOf(e.name()), e.name().length()); + // Calculate how many bytes are needed to encode the name length part: + // | 0 | 1 | H | Name Length (5+) | + integerValuesSize = IntegerWriter.requiredBufferSize(5, nlen); + // Calculate how many bytes are needed to encode the value length part: + // | H | Value Length (7+) | + integerValuesSize += IntegerWriter.requiredBufferSize(7, vlen); + // We also need nlen + vlen bytes for the name and the value strings + // content + yield integerValuesSize + nlen + vlen; + } + default -> throw new IllegalArgumentException("Unsupported table entry type: " + e.type()); + }; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java new file mode 100644 index 00000000000..6e268f32acc --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedNameWriter.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class FieldLineIndexedNameWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + FieldLineIndexedNameWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, boolean hideIntermediary, long base) + throws IndexOutOfBoundsException { + return e.isStaticTable() ? configureStatic(e, hideIntermediary) : + configureDynamic(e, hideIntermediary, base); + } + + private BinaryRepresentationWriter configureStatic(TableEntry e, boolean hideIntermediary) + throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With Static Table Name Reference" + + " (%s, '%s', huffman=%b, hideIntermediary=%b)", + e.index(), e.value(), e.huffmanValue(), hideIntermediary)); + } + return this.staticIndex(e.index(), hideIntermediary).value(e); + } + + private BinaryRepresentationWriter configureDynamic(TableEntry e, boolean hideIntermediary, long base) + throws IndexOutOfBoundsException { + boolean usePostBase = e.index() >= base; + long index = usePostBase ? e.index() - base : base - 1 - e.index(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With %s Dynamic Table Name Reference" + + " (%s, '%s', huffman=%b, hideIntermediary=%b)", + usePostBase ? "Post-Base" : "", index, e.value(), e.huffmanValue(), + hideIntermediary)); + } + if (usePostBase) { + return this.dynamicPostBaseIndex(index, hideIntermediary).value(e); + } else { + return this.dynamicIndex(index, hideIntermediary).value(e); + } + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public FieldLineIndexedNameWriter reset() { + intWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private FieldLineIndexedNameWriter staticIndex(long absoluteIndex, boolean hideIntermediary) { + int payload = 0b0101_0000; + if (hideIntermediary) { + payload |= 0b0010_0000; + } + intWriter.configure(absoluteIndex, 4, payload); + return this; + } + + private FieldLineIndexedNameWriter dynamicIndex(long relativeIndex, boolean hideIntermediary) { + int payload = 0b0100_0000; + if (hideIntermediary) { + payload |= 0b0010_0000; + } + intWriter.configure(relativeIndex, 4, payload); + return this; + } + + private FieldLineIndexedNameWriter dynamicPostBaseIndex(long relativeIndex, boolean hideIntermediary) { + int payload = 0b0000_0000; + if (hideIntermediary) { + payload |= 0b0000_1000; + } + intWriter.configure(relativeIndex, 3, payload); + return this; + } + + private FieldLineIndexedNameWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java new file mode 100644 index 00000000000..c829966980a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineIndexedWriter.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +final class FieldLineIndexedWriter implements BinaryRepresentationWriter { + private final QPACK.Logger logger; + private final IntegerWriter intWriter = new IntegerWriter(); + + public FieldLineIndexedWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, long base) { + return e.isStaticTable() ? configureStatic(e) : configureDynamic(e, base); + } + + private BinaryRepresentationWriter configureStatic(TableEntry e) { + assert e.isStaticTable(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Indexed Field Line Static Table reference" + + " (%s, '%s', '%s')", e.index(), e.name(), e.value())); + } + return this.staticIndex(e.index()); + } + + private BinaryRepresentationWriter configureDynamic(TableEntry e, long base) { + assert !e.isStaticTable(); + // RFC-9204: 3.2.6. Post-Base Indexing + // Post-Base indices are used in field line representations for entries with absolute + // indices greater than or equal to Base, starting at 0 for the entry with absolute index + // equal to Base and increasing in the same direction as the absolute index. + boolean usePostBase = e.index() >= base; + long index = usePostBase ? e.index() - base : base - 1 - e.index(); + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("Indexed Field Line Dynamic Table reference %s (%s[%s], '%s', '%s')", + usePostBase ? "with Post-Base Index" : "", index, e.index(), e.name(), e.value())); + } + if (usePostBase) { + return dynamicPostBaseIndex(index); + } else { + return dynamicIndex(index); + } + } + + @Override + public boolean write(ByteBuffer destination) { + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } + + private FieldLineIndexedWriter staticIndex(long absoluteIndex) { + int N = 6; + intWriter.configure(absoluteIndex, N, 0b1100_0000); + return this; + } + + private FieldLineIndexedWriter dynamicIndex(long relativeIndex) { + assert relativeIndex >= 0; + int N = 6; + intWriter.configure(relativeIndex, N, 0b1000_0000); + return this; + } + + private FieldLineIndexedWriter dynamicPostBaseIndex(long relativeIndex) { + assert relativeIndex >= 0; + int N = 4; + intWriter.configure(relativeIndex, N, 0b0001_0000); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java new file mode 100644 index 00000000000..42187c256d7 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineLiteralsWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +class FieldLineLiteralsWriter implements BinaryRepresentationWriter { + private int state = NEW; + private final QPACK.Logger logger; + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + public FieldLineLiteralsWriter(QPACK.Logger logger) { + this.logger = logger; + } + + public BinaryRepresentationWriter configure(TableEntry e, boolean hideIntermediary) throws IndexOutOfBoundsException { + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format( + "Field Line With Name and Value Literals ('%s', '%s', huffmanName=%b, huffmanValue=%b, hideIntermediary=%b)", + e.name(), e.value(), e.huffmanName(), e.huffmanValue(), hideIntermediary)); + } + return this.name(e, hideIntermediary).value(e); + } + + @Override + public boolean write(ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (!nameWriter.write(destination)) { + return false; + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public BinaryRepresentationWriter reset() { + nameWriter.reset(); + valueWriter.reset(); + state = NEW; + return this; + } + + private FieldLineLiteralsWriter name(TableEntry e, boolean hideIntermediary) { + int N = 3; + int payload = 0b0010_0000; + if (hideIntermediary) { + payload |= 0b0001_0000; + } + if (e.huffmanName()) { + payload |= 0b0000_1000; + } + nameWriter.configure(e.name(), N, payload, e.huffmanName()); + return this; + } + + private FieldLineLiteralsWriter value(TableEntry e) { + int N = 7; + int payload = 0b0000_0000; + if (e.huffmanValue()) { + payload |= 0b1000_0000; + } + valueWriter.configure(e.value(), N, payload, e.huffmanValue()); + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java new file mode 100644 index 00000000000..18e1d1e2676 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/FieldLineSectionPrefixWriter.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.FieldSectionPrefix; + +import java.nio.ByteBuffer; + +public class FieldLineSectionPrefixWriter { + enum State {NEW, CONFIGURED, RIC_WRITTEN, DONE} + + private final IntegerWriter intWriter; + private State state = State.NEW; + private long encodedRic; + private int signBit; + private long deltaBase; + + public FieldLineSectionPrefixWriter() { + this.intWriter = new IntegerWriter(); + } + + private void encodeFieldSectionPrefixFields(FieldSectionPrefix fsp, long maxEntries) { + // Required Insert Count encoded according to RFC-9204 "4.5.1.1: Required Insert Count" + // Base and Sign encoded according to RFC-9204: "4.5.1.2. Base" + long ric = fsp.requiredInsertCount(); + long base = fsp.base(); + + if (ric == 0) { + encodedRic = 0; + deltaBase = 0; + signBit = 0; + } else { + encodedRic = (ric % (2 * maxEntries)) + 1; + signBit = base >= ric ? 0 : 1; + deltaBase = base >= ric ? base - ric : ric - base - 1; + } + } + + public int configure(FieldSectionPrefix sectionPrefix, long maxEntries) { + intWriter.reset(); + encodeFieldSectionPrefixFields(sectionPrefix, maxEntries); + intWriter.configure(encodedRic, 8, 0); + state = State.CONFIGURED; + return IntegerWriter.requiredBufferSize(8, encodedRic) + + IntegerWriter.requiredBufferSize(7, deltaBase); + } + + public boolean write(ByteBuffer destination) { + if (state == State.NEW) { + throw new IllegalStateException("Configure first"); + } + + if (state == State.CONFIGURED) { + if (!intWriter.write(destination)) { + return false; + } + // Required Insert Count part is written, + // prepare integer writer for delta base and + // base sign write + intWriter.reset(); + int signPayload = signBit == 1 ? 0b1000_0000 : 0b0000_0000; + intWriter.configure(deltaBase, 7, signPayload); + state = State.RIC_WRITTEN; + } + + if (state == State.RIC_WRITTEN) { + if (!intWriter.write(destination)) { + return false; + } + state = State.DONE; + } + return state == State.DONE; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java new file mode 100644 index 00000000000..3841ca7c5ff --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/HeaderFrameWriter.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.TableEntry; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.String.format; +import static jdk.internal.net.http.qpack.QPACK.Logger.Level.EXTRA; + +public class HeaderFrameWriter { + private BinaryRepresentationWriter writer; + private final QPACK.Logger logger; + private final FieldLineIndexedWriter indexedWriter; + private final FieldLineIndexedNameWriter literalWithNameReferenceWriter; + private final FieldLineLiteralsWriter literalWithLiteralNameWriter; + private boolean encoding; + private static final AtomicLong HEADER_FRAME_WRITER_IDS = new AtomicLong(); + + public HeaderFrameWriter() { + this(QPACK.getLogger()); + } + + public HeaderFrameWriter(QPACK.Logger parentLogger) { + long id = HEADER_FRAME_WRITER_IDS.incrementAndGet(); + this.logger = parentLogger.subLogger("HeaderFrameWriter#" + id); + + indexedWriter = new FieldLineIndexedWriter(logger.subLogger("FieldLineIndexedWriter")); + literalWithNameReferenceWriter = new FieldLineIndexedNameWriter( + logger.subLogger("FieldLineIndexedNameWriter")); + literalWithLiteralNameWriter = new FieldLineLiteralsWriter( + logger.subLogger("FieldLineLiteralsWriter")); + } + + public void configure(TableEntry e, boolean sensitive, long base) { + checkIfEncodingInProgress(); + encoding = true; + writer = switch (e.type()) { + case NAME_VALUE -> indexedWriter.configure(e, base); + case NAME -> literalWithNameReferenceWriter.configure(e, sensitive, base); + case NEITHER -> literalWithLiteralNameWriter.configure(e, sensitive); + }; + } + + /** + * Writes the {@linkplain #configure(TableEntry, boolean, long) + * set up} header into the given buffer. + * + *

        The method writes as much as possible of the header's binary + * representation into the given buffer, starting at the buffer's position, + * and increments its position to reflect the bytes written. The buffer's + * mark and limit will not be modified. + * + *

        Once the method has returned {@code true}, the configured header is + * deemed encoded. A new header may be set up. + * + * @param headerFrame the buffer to encode the header into, may be empty + * @return {@code true} if the current header has been fully encoded, + * {@code false} otherwise + * @throws NullPointerException if the buffer is {@code null} + * @throws IllegalStateException if there is no set up header + */ + public boolean write(ByteBuffer headerFrame) { + if (!encoding) { + throw new IllegalStateException("A header hasn't been set up"); + } + if (logger.isLoggable(EXTRA)) { + logger.log(EXTRA, () -> format("writing to %s", headerFrame)); + } + boolean done = writer.write(headerFrame); + if (done) { + writer.reset(); + encoding = false; + } + return done; + } + + private void checkIfEncodingInProgress() { + if (encoding) { + throw new IllegalStateException("Previous encoding operation hasn't finished yet"); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java new file mode 100644 index 00000000000..f042135b4a1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/IntegerWriter.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public final class IntegerWriter { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_WRITTEN = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int payload; + private int N; + private long value; + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | | | | | | | | | + // +---+---+---+-------------------+ + // |<--------->|<----------------->| + // payload N=5 + // + // payload is the contents of the left-hand side part of the first octet; + // it is truncated to fit into 8-N bits, where 1 <= N <= 8; + // + public IntegerWriter configure(long value, int N, int payload) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + if (value < 0) { + throw new IllegalArgumentException("value >= 0: value=" + value); + } + checkPrefix(N); + this.value = value; + this.N = N; + this.payload = payload & 0xFF & (0xFFFFFFFF << N); + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + if (value < max) { + output.put((byte) (payload | value)); + state = DONE; + return true; + } + output.put((byte) (payload | max)); + value -= max; + state = FIRST_BYTE_WRITTEN; + } + if (state == FIRST_BYTE_WRITTEN) { + while (value >= 128 && output.hasRemaining()) { + output.put((byte) ((value & 127) + 128)); + value /= 128; + } + if (!output.hasRemaining()) { + return false; + } + output.put((byte) value); + state = DONE; + return true; + } + throw new InternalError(Arrays.toString( + new Object[]{state, payload, N, value})); + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public static int requiredBufferSize(int N, long value) { + checkPrefix(N); + int size = 1; + int max = (2 << (N - 1)) - 1; + if (value < max) { + return size; + } + size++; + value -= max; + while (value >= 128) { + value /= 128; + size++; + } + return size; + } + + public IntegerWriter reset() { + state = NEW; + return this; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java new file mode 100644 index 00000000000..298c3d8f9c1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/qpack/writers/StringWriter.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.qpack.writers; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import jdk.internal.net.http.hpack.ISO_8859_1; +import jdk.internal.net.http.hpack.Huffman; +import jdk.internal.net.http.hpack.QuickHuffman; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +// StringWriter does not require a notion of endOfInput (isLast) in 'write' +// methods due to the nature of string representation in HPACK. Namely, the +// length of the string is put before string's contents. Therefore the length is +// always known beforehand. +// +// Expected use: +// +// configure write* (reset configure write*)* +// +public final class StringWriter { + private static final int DEFAULT_PREFIX = 7; + private static final int DEFAULT_PAYLOAD = 0b0000_0000; + private static final int HUFFMAN_PAYLOAD = 0b1000_0000; + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int LENGTH_WRITTEN = 2; + private static final int DONE = 4; + + private final IntegerWriter intWriter = new IntegerWriter(); + private final Huffman.Writer huffmanWriter = new QuickHuffman.Writer(); + private final ISO_8859_1.Writer plainWriter = new ISO_8859_1.Writer(); + + private int state = NEW; + private boolean huffman; + + public StringWriter configure(CharSequence input, boolean huffman) { + return configure(input, 0, input.length(), DEFAULT_PREFIX, huffman ? HUFFMAN_PAYLOAD : DEFAULT_PAYLOAD, huffman); + } + + public StringWriter configure(CharSequence input, int N, int payload, boolean huffman) { + return configure(input, 0, input.length(), N, payload, huffman); + } + + StringWriter configure(CharSequence input, + int start, + int end, + int N, + int payload, + boolean huffman) { + if (start < 0 || end < 0 || end > input.length() || start > end) { + throw new IndexOutOfBoundsException( + String.format("input.length()=%s, start=%s, end=%s", + input.length(), start, end)); + } + if (!huffman) { + plainWriter.configure(input, start, end); + intWriter.configure(end - start, N, payload); + } else { + huffmanWriter.from(input, start, end); + intWriter.configure(huffmanWriter.lengthOf(input, start, end), N, payload); + } + + this.huffman = huffman; + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == DONE) { + return true; + } + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + if (intWriter.write(output)) { + state = LENGTH_WRITTEN; + } else { + return false; + } + } + if (state == LENGTH_WRITTEN) { + boolean written = huffman + ? huffmanWriter.write(output) + : plainWriter.write(output); + if (written) { + state = DONE; + return true; + } else { + return false; + } + } + throw new InternalError(Arrays.toString(new Object[]{state, huffman})); + } + + public void reset() { + intWriter.reset(); + if (huffman) { + huffmanWriter.reset(); + } else { + plainWriter.reset(); + } + state = NEW; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java new file mode 100644 index 00000000000..9f2ebf17264 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/BuffersReader.java @@ -0,0 +1,707 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A class that allows to read data from an aggregation of {@code ByteBuffer}. + * This is mostly geared to reading Quic or HTTP/3 frames that are composed + * of an aggregation of {@linkplain VariableLengthEncoder Variable Length Integers}. + * This class is not multi-thread safe. + *

        + * The {@code BuffersReader} class is an abstract class with two concrete + * implementations: {@link SingleBufferReader} and {@link ListBuffersReader}. + *

        + * The {@link SingleBufferReader} presents a simple lightweight view of a single + * {@link ByteBuffer}. Instances of {@code SingleBufferReader} can be created by + * calling {@link BuffersReader#single(ByteBuffer) BuffersReader.single(buffer)}; + *

        + * The {@link ListBuffersReader} view can be created from a (possibly empty) + * list of byte buffers. New byte buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} to the {@link ListBuffersReader} instance + * as they become available. Once a frame has been fully received, + * {@link BuffersReader#release()} or {@link BuffersReader#getAndRelease(long)} should + * be called to forget and relinquish all bytes buffers up to the current + * {@linkplain #position() position} of the {@code BuffersReader}. + * Released buffers are removed from {@code BuffersReader} list, and the position + * of the reader is reset to 0, allowing to read the next frame from the remaining + * data. + */ +public abstract sealed class BuffersReader { + + /** + * Release all buffers held by this {@code BuffersReader}, whether + * consumed or unconsumed. Released buffer are all set to their + * limit. + */ + public abstract void clear(); + + // Used to store the original position and limit of a + // buffer at the time it's added to the reader's list + // It is not possible to beyond that position or limit when + // using the reader + private record Buffer(ByteBuffer buffer, int offset, int limit) { + Buffer { + assert offset <= limit; + assert offset >= 0; + assert limit == buffer.limit(); + } + Buffer(ByteBuffer buffer) { + this(buffer, buffer.position(), buffer.limit()); + } + } + + /** + * {@return the current position of the reader} + * The semantic is similar to {@link ByteBuffer#position()}. + */ + public abstract long position(); + + /** + * {@return the limit of the reader} + * The semantic is similar to {@link ByteBuffer#limit()}. + */ + public abstract long limit(); + + /** + * Reads one byte from the reader. This method increase + * the position by one. The semantic is similar to + * {@link ByteBuffer#get()}. + * @return the byte at the current position + * @throws BufferUnderflowException if trying to read past + * the limit. + */ + public abstract byte get(); + + /** + * Reads the byte located at the given position in the + * reader. The semantic is similar to {@link ByteBuffer#get(int)}. + * This method doesn't change the position of the reader. + * + * @param position the position of the byte + * @return the byte at the given position in the reader + * + * @throws IndexOutOfBoundsException if trying to read before + * the reader's position or after the reader's limit + */ + public abstract byte get(long position); + + /** + * Sets the position of the reader. + * The semantic is similar to {@link ByteBuffer#position(int)}. + * + * @param newPosition the new position + * + * @throws IllegalArgumentException if trying to set + * the position to a negative value, or to a value + * past the limit + */ + public abstract void position(long newPosition); + + /** + * Releases all the data that has been read, sets the + * reader's position to 0 and its limit to the amount + * of data remaining. + */ + public abstract void release(); + + /** + * Returns a list of {@code ByteBuffer} containing the + * requested amount of bytes, starting at the current + * position, then release all the data up to the new + * position, and reset the reader's position to 0 and + * the reader's limit to the amount of remaining data. + * . + * @param bytes the amount of bytes to read and move + * to the returned list. + * + * @return a list of {@code ByteBuffer} containing the next + * {@code bytes} of data, starting at the current position. + * + * @throws BufferUnderflowException if attempting to read past + * the limit + */ + public abstract List getAndRelease(long bytes); + + /** + * {@return true if the reader has remaining bytes to the read} + * The semantic is similar to {@link ByteBuffer#hasRemaining()}. + */ + public boolean hasRemaining() { + return position() < limit(); + } + + /** + * {@return the number of bytes that remain to read} + * The semantic is similar to {@link ByteBuffer#remaining()}. + */ + public long remaining() { + long rem = limit() - position(); + return rem > 0 ? rem : 0; + } + + /** + * {@return the cumulated amount of data that has been read in this + * {@code BuffersReader} since its creation} + * This number is not reset when calling {@link #release()}. + */ + public abstract long read(); + + /** + * {@return The offset of this {@code BuffersReader}} + * This is the position in the first {@code ByteBuffer} that + * was set on the reader. The {@code BuffersReader} will not + * allow to get or set a position lower than the offset. + */ + public abstract long offset(); + + /** + * {@return true if this {@code BuffersReader} is empty} + * A {@code BuffersReader} is empty if it has been {@linkplain + * #list() created empty, or if it has been {@linkplain #release() + * released} after all data has been read. + */ + public abstract boolean isEmpty(); + + /** + * A lightweight view allowing to see a {@link ByteBuffer} as a + * {@link BuffersReader}. This class wrap a single {@link ByteBuffer} + * and cannot be reused after {@link #release()}. + */ + public static final class SingleBufferReader extends BuffersReader { + ByteBuffer single; + long read = 0; + long start; + SingleBufferReader(ByteBuffer single) { + this.single = single; + start = single.position(); + } + + @Override + public void release() { + single = null; + } + + @Override + public List getAndRelease(long bytes) { + return List.of(getAndReleaseBuffer(bytes)); + } + + @Override + public byte get() { + if (single == null) throw new BufferUnderflowException(); + return single.get(); + } + + @Override + public byte get(long position) { + if (single == null || position < start || position >= single.limit()) + throw new IndexOutOfBoundsException(); + return single.get((int) position); + } + + @Override + public long limit() { + return single == null ? 0 : single.limit(); + } + + @Override + public long position() { + return single == null ? 0 : single.position(); + } + + @Override + public boolean hasRemaining() { + return single != null && single.hasRemaining(); + } + + @Override + public void position(long pos) { + if (single == null || pos < start || pos > single.limit()) + throw new BufferUnderflowException(); + single.position((int) pos); + } + + /** + * This method has the same semantics than {@link #getAndRelease(long)} + * except that it avoids creating a list. + * @return a buffer containing the next {@code bytes}. + */ + public ByteBuffer getAndReleaseBuffer(long bytes) { + var released = single; + int remaining = released.remaining(); + if (bytes > remaining) + throw new BufferUnderflowException(); + if (bytes == remaining) { + read = single.limit() - start; + single = null; + } else { + read = single.position() - start; + single = released.slice(released.position() + (int)bytes, released.limit()); + start = 0; + released = released.slice(released.position(), (int) bytes); + } + return released; + } + + @Override + public long read() { + return single == null ? read : (read + single.position() - start); + } + + @Override + public long offset() { + return start; + } + + @Override + public boolean isEmpty() { + return single == null; + } + + @Override + public void clear() { + if (single == null) return; + single.position(single.limit()); + single = null; + } + } + + /** + * A {@code BuffersReader} that iterates over a list of {@code ByteBuffers}. + * New {@code ByteBuffers} can be added at the end of list by calling + * {@link #add(ByteBuffer)} or {@link #addAll(List)}, which increases + * the {@linkplain #limit() limit} accordingly. + *

        + * When {@link #release() released}, the data prior to the current + * {@linkplain #position()} is discarded, the {@linkplain #position() position} + * and {@linkplain #offset() offset} are reset to {@code 0}, and the + * {@linkplain #limit() limit} is set to the amount of remaining data. + *

        + * A {@code ListBuffersReader} can be reused after being released. + * If it still contains data, the {@linkplain #offset() offset} will + * be {@code 0}. Otherwise, the offset will be set to the position + * of the first buffer {@linkplain #add(ByteBuffer) added} to the + * {@code ListBuffersReader}. + */ + public static final class ListBuffersReader extends BuffersReader { + private final List buffers = new ArrayList<>(); + private Buffer current; + private int nextIndex; + private long currentOffset; + private long position; + private long limit; + private long start; + private long readAndReleased = 0; + + ListBuffersReader() { + } + + /** + * Adds a new {@code ByteBuffer} to this {@code BuffersReader}. + * If the reader is {@linkplain #isEmpty() empty}, the reader's + * {@linkplain #offset() offset} and {@linkplain #position() position} + * is set to the buffer's position, and the reader {@linkplain #limit() + * limit} is set to the buffer's limit. + * Otherwise, the reader's limit is simply increased by the buffer's + * remaining bytes. The reader will only allow to read those bytes + * between the current position and limit of the buffer. + * + * @apiNote + * This class doesn't make defensive copies of the provided buffers, + * so the caller must not modify the buffer's position or limit + * after it's been added to the reader. + * + * @param buffer a byte buffer + * @return this reader + */ + public ListBuffersReader add(ByteBuffer buffer) { + if (buffers.isEmpty()) { + int lim = buffer.limit(); + buffers.add(new Buffer(buffer, 0, lim)); + start = buffer.position(); + position = limit = start; + currentOffset = 0; + } else { + buffers.add(new Buffer(buffer)); + } + limit += buffer.remaining(); + return this; + } + + /** + * Adds a list of byte buffers to this reader. + * This is equivalent to calling: + * {@snippet : + * ListBuffersReader reader = ...; + * for (var buffer : buffers) { + * reader.add(buffer); // @link substring="add" target="#add(ByteBuffer)" + * } + * } + * @param buffers a list of {@link ByteBuffer ByteBuffers} + * @return this reader + */ + public ListBuffersReader addAll(List buffers) { + for (var buffer : buffers) { + if (isEmpty()) { + add(buffer); + continue; + } + this.buffers.add(new Buffer(buffer)); + limit += buffer.remaining(); + } + return this; + } + + @Override + public boolean isEmpty() { + return buffers.isEmpty(); + } + + @Override + public byte get() { + ByteBuffer buffer = current(true); + byte res = buffer.get(); + position++; + return res; + } + + @Override + public byte get(long pos) { + if (pos >= limit || pos < start) + throw new IndexOutOfBoundsException(); + ByteBuffer buffer = current(false); + if (position == limit && current != null) { + // let the current buffer throw + buffer = current.buffer; + } + assert buffer != null : "limit check failed"; + if (pos == position) { + return buffer.get(buffer.position()); + } + long offset = currentOffset; + int index = nextIndex; + Buffer cur = current; + while (pos >= offset) { + int bpos = buffer.position(); + int boffset = cur.offset; + int blimit = buffer.limit(); + assert index == nextIndex || bpos == boffset; + if (pos - offset < blimit - boffset) { + return buffer.get((int) (pos - offset + boffset)); + } + if (index >= buffers.size()) { + assert false : "buffers exhausted"; + throw new IndexOutOfBoundsException(); + } + int skipped = cur.limit - cur.offset; + offset += skipped; + cur = buffers.get(index++); + buffer = cur.buffer; + } + assert pos <= offset; + int blimit = cur.offset; + int boffset = cur.offset; + while (pos < offset) { + assert blimit == cur.limit || index == nextIndex && blimit == boffset; + if (index <= 1) { + assert false : "buffers exhausted"; + throw new IndexOutOfBoundsException(); + } + cur = buffers.get(--index - 1); + buffer = cur.buffer; + int bpos = buffer.position(); + blimit = buffer.limit(); + boffset = cur.offset; + int skipped = blimit - boffset; + offset -= skipped; + assert index == nextIndex || bpos == blimit; + if (pos - offset >= 0 && pos - offset < blimit - boffset) { + return buffer.get((int) (pos - offset + boffset)); + } + } + assert false : "buffer not found"; + throw new IndexOutOfBoundsException(); // should not reach here + } + + /** + * {@return the current {@code ByteBuffer} in which to find + * the byte at the current {@link #position()}} + * + * @param throwIfUnderflow if true, calling this method + * will throw {@link BufferUnderflowException} if + * the position is past the limit. + * + * @throws BufferUnderflowException if attempting to read past + * the limit and {@code throwIfUnderflow == true} + */ + private ByteBuffer current(boolean throwIfUnderflow) { + while (current == null || !current.buffer.hasRemaining()) { + if (buffers.size() > nextIndex) { + if (nextIndex != 0) { + currentOffset = position; + } else { + currentOffset = 0; + } + current = buffers.get(nextIndex++); + } else if (throwIfUnderflow) { + throw new BufferUnderflowException(); + } else { + return null; + } + } + return current.buffer; + } + + @Override + public List getAndRelease(long bytes) { + release(); + if (bytes > limit - position) { + throw new BufferUnderflowException(); + } + ByteBuffer buf = current(false); + if (buf == null || bytes == 0) return List.of(); + List list = null; + assert position == 0; + assert currentOffset == 0; + while (bytes > 0) { + buf = current(false); + assert nextIndex == 1; + assert buf != null; + assert buf.position() == current.offset; + int remaining = buf.remaining(); + if (remaining <= bytes) { + var b = buffers.remove(--nextIndex); + assert b == current; + long relased = buf.remaining(); + assert b.buffer.limit() == b.limit; + bytes -= relased; + limit -= relased; + readAndReleased += relased; + current = null; + + // if a buffer has no remaining bytes it + // may be EOF. Let's not skip it here + // if (!buf.hasRemaining()) continue; + + if (bytes == 0 && list == null) { + list = List.of(buf); + } else { + if (list == null) { + list = new ArrayList<>(); + } + list.add(buf); + } + } else { + var b = current; + long relased = bytes; + bytes = 0; + limit -= relased; + var pos = buf.position(); + assert b.limit == buf.limit(); + assert pos == b.offset; + var slice = buf.slice(pos, (int)relased); + buf.position(pos + (int) relased); + buffers.set(nextIndex - 1, current = new Buffer(buf)); + readAndReleased += relased; + if (list != null) { + list.add(slice); + } else { + list = List.of(slice); + } + assert bytes == 0; + } + } + return list; + } + + @Override + public long position() { + return position; + } + + @Override + public long limit() { + return limit; + } + + @Override + public void release() { + long released = - start; + for (var it = buffers.listIterator(); it.hasNext(); ) { + var b = it.next(); + var buf = b.buffer; + released += (buf.position() - b.offset); + if (buf.hasRemaining()) { + it.set(new Buffer(buf)); + break; + } + it.remove(); + } + assert released == position - start + : "start=%s, position=%s, released=%s" + .formatted(start, position, released); + readAndReleased += released; + limit -= position; + current = null; + position = 0; + currentOffset = 0; + nextIndex = 0; + start = 0; + } + + @Override + public void position(long pos) { + if (pos > limit) throw new IllegalArgumentException(pos + " > " + limit); + if (pos < start) throw new IllegalArgumentException(pos + " < " + start); + if (pos == position) return; // happy case! + // look forward, starting from the current position: + // - identify the ByteBuffer that contains the requested position + // - set the local position in that ByteBuffer to + // match the requested position + if (pos > position) { + long skip = pos - position; + assert skip > 0; + while (skip > 0) { + var buffer = current(true); + int remaining = buffer.remaining(); + if (remaining == 0) continue; + if (skip > remaining) { + // somewhere after the current buffer + buffer.position(buffer.limit()); + position += remaining; + skip -= remaining; + } else { + // somewhere in the current buffer + buffer.position(buffer.position() + (int) skip); + position += skip; + skip = 0; + } + } + } else { + // look backward, starting from the current position: + // - identify the ByteBuffer that contains the requested position + // - set the local position in that ByteBuffer to + // match the requested position + long skip = pos - position; + assert skip < 0; + if (current == null) { + current(false); + if (current == null) + throw new IllegalArgumentException(); + } + while (skip < 0) { + var buffer = current.buffer; + assert buffer.limit() == current.limit; + var remaining = buffer.position() - current.offset; + var rest = skip + remaining; + if (rest >= 0) { + // somewhere in this byte buffer, between the + // buffer offset and the buffer position + buffer.position(buffer.position() + (int)skip); + position += skip; + assert position >= start; + skip = 0; + } else { + // in some buffer prior to the current byte buffer + buffer.position(current.offset); + skip += remaining; + position -= remaining; + assert skip < 0; + assert position >= start; + assert nextIndex > 1; + current = buffers.get(--nextIndex - 1); + currentOffset -= current.limit - current.offset; + assert currentOffset >= 0; + assert current.buffer.position() == current.limit; + } + } + } + } + + @Override + public long read() { + return readAndReleased + (position - start); + } + + @Override + public long offset() { + return start; + } + + @Override + public void clear() { + release(); + position(limit()); + release(); + } + } + + /** + * Creates a lightweight {@link SingleBufferReader} view over + * a single {@link ByteBuffer}. + * @param buffer a byte buffer + * @return a lightweight {@link SingleBufferReader} view over + * a single {@link ByteBuffer} + */ + public static SingleBufferReader single(ByteBuffer buffer) { + return new SingleBufferReader(Objects.requireNonNull(buffer)); + } + + /** + * Creates an {@linkplain #isEmpty() empty} {@link ListBuffersReader}. + * @return an empty {@code ListBuffersReader} + */ + public static ListBuffersReader list() { + return new ListBuffersReader(); + } + + /** + * Creates a {@link ListBuffersReader} with the given + * {@code buffer}. More buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} as they become + * available. + * @return a {@code ListBuffersReader} + */ + public static ListBuffersReader list(ByteBuffer buffer) { + return new ListBuffersReader().add(buffer); + } + + /** + * Creates a {@link ListBuffersReader} with the given + * {@code buffers} list. More buffers can be later {@linkplain + * ListBuffersReader#add(ByteBuffer) added} as they become + * available. + * @return a {@code ListBuffersReader} + */ + public static ListBuffersReader list(List buffers) { + return new ListBuffersReader().addAll(buffers); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java new file mode 100644 index 00000000000..d2daa6fadaf --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/CodingContext.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public interface CodingContext { + + /** + * {@return the largest incoming packet number successfully processed + * in the given packet number space} + * + * @apiNote + * This method is used when decoding the packet number of an incoming packet. + * + * @param packetSpace the packet number space + */ + long largestProcessedPN(QuicPacket.PacketNumberSpace packetSpace); + + /** + * {@return the largest outgoing packet number acknowledged by the peer + * in the given packet number space} + * + * @apiNote + * This method is used when encoding the packet number of an outgoing packet. + * + * @param packetSpace the packet number space + */ + long largestAckedPN(QuicPacket.PacketNumberSpace packetSpace); + + /** + * {@return the length of the local connection ids expected + * to be found in incoming short header packets} + */ + int connectionIdLength(); + + /** + * {@return the largest incoming packet number successfully processed + * in the packet number space corresponding to the given packet type} + *

        + * This is equivalent to calling:

        +     *     {@code largestProcessedPN(QuicPacket.PacketNumberSpace.of(packetType));}
        +     * 
        + * + * @apiNote + * This method is used when decoding the packet number of an incoming packet. + * + * @param packetType the packet type + */ + default long largestProcessedPN(QuicPacket.PacketType packetType) { + return largestProcessedPN(QuicPacket.PacketNumberSpace.of(packetType)); + } + + /** + * {@return the largest outgoing packet number acknowledged by the peer + * in the packet number space corresponding to the given packet type} + *

        + * This is equivalent to calling:

        +     *     {@code largestAckedPN(QuicPacket.PacketNumberSpace.of(packetType));}
        +     * 
        + * + * @apiNote + * This method is used when encoding the packet number of an outgoing packet. + * + * @param packetType the packet type + */ + default long largestAckedPN(QuicPacket.PacketType packetType) { + return largestAckedPN(QuicPacket.PacketNumberSpace.of(packetType)); + } + + /** + * Writes the given outgoing packet in the given byte buffer. + * This method moves the position of the byte buffer. + * @param packet the outgoing packet to write + * @param buffer the byte buffer to write the packet into + * @return the number of bytes written + * @throws java.nio.BufferOverflowException if the buffer doesn't have + * enough space to write the packet + */ + int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Reads an encrypted packet from the given byte buffer. + * This method moves the position of the byte buffer. + * @param src a byte buffer containing a non encrypted packet + * @return the packet read + * @throws IOException if the packet couldn't be read + * @throws QuicTransportException if packet is correctly signed but malformed + */ + QuicPacket parsePacket(ByteBuffer src) throws IOException, QuicKeyUnavailableException, QuicTransportException; + + /** + * Returns the original destination connection id, required for + * calculating the retry integrity tag. + *

        + * This is only of interest when protecting/unprotecting a {@linkplain + * QuicPacket.PacketType#RETRY Retry Packet}. + * + * @return the original destination connection id, required for calculating + * the retry integrity tag + */ + QuicConnectionId originalServerConnId(); + + /** + * Returns the TLS engine associated with this context + * @return the TLS engine associated with this context + */ + QuicTLSEngine getTLSEngine(); + + /** + * Checks if the provided token is valid for the given context and connection ID. + * @param destinationID destination connection ID found in the packet + * @param token token to verify + * @return true if token is valid, false otherwise + */ + boolean verifyToken(QuicConnectionId destinationID, byte[] token); + + /** + * {@return The minimum payload size for short packet payloads}. + * Padding will be added to match that size if needed. + * @param destConnectionIdLength the length of the destination + * connectionId included in the packet + */ + default int minShortPacketPayloadSize(int destConnectionIdLength) { + // See RFC 9000, Section 10.3 + // https://www.rfc-editor.org/rfc/rfc9000#section-10.3 + // [..] the endpoint SHOULD ensure that all packets it sends + // are at least 22 bytes longer than the minimum connection + // ID length that it requests the peer to include in its + // packets [...] + // + // A 1-RTT packet contains the peer connection id + // (whose length is destConnectionIdLength), therefore the + // payload should be at least 5 - (destConnectionIdLength + // - connectionIdLength()) - where connectionIdLength is the + // length of the local connection ID. + return 5 - (destConnectionIdLength - connectionIdLength()); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java new file mode 100644 index 00000000000..24230a0883d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +// responsible for managing the connection termination of a QUIC connection +public sealed interface ConnectionTerminator permits ConnectionTerminatorImpl { + + // lets the terminator know that the connection is still alive and should not be + // idle timed out + void keepAlive(); + + void terminate(TerminationCause cause); + + boolean tryReserveForUse(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java new file mode 100644 index 00000000000..150d6233953 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/ConnectionTerminatorImpl.java @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicConnectionImpl.HandshakeFlow; +import jdk.internal.net.http.quic.QuicConnectionImpl.ProtectionRecord; +import jdk.internal.net.http.quic.TerminationCause.AppLayerClose; +import jdk.internal.net.http.quic.TerminationCause.SilentTermination; +import jdk.internal.net.http.quic.TerminationCause.TransportError; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.CLOSED; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.CLOSING; +import static jdk.internal.net.http.quic.QuicConnectionImpl.QuicConnectionState.DRAINING; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.INTERNAL_ERROR; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +final class ConnectionTerminatorImpl implements ConnectionTerminator { + + private final QuicConnectionImpl connection; + private final Logger debug; + private final String logTag; + private final AtomicReference terminationCause = new AtomicReference<>(); + private final CompletableFuture futureTC = new MinimalFuture<>(); + + ConnectionTerminatorImpl(final QuicConnectionImpl connection) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.debug = connection.debug; + this.logTag = connection.logTag(); + } + + @Override + public void keepAlive() { + this.connection.idleTimeoutManager.keepAlive(); + } + + @Override + public boolean tryReserveForUse() { + return this.connection.idleTimeoutManager.tryReserveForUse(); + } + + @Override + public void terminate(final TerminationCause cause) { + Objects.requireNonNull(cause); + try { + doTerminate(cause); + } catch (Throwable t) { + // make sure we do fail the handshake CompletableFuture(s) + // even when the connection termination itself failed. that way + // the dependent CompletableFuture(s) tasks don't keep waiting forever + failHandshakeCFs(t); + } + } + + TerminationCause getTerminationCause() { + return this.terminationCause.get(); + } + + private void doTerminate(final TerminationCause cause) { + final ConnectionCloseFrame frame; + KeySpace keySpace; + switch (cause) { + case SilentTermination st -> { + silentTerminate(st); + return; + } + case TransportError te -> { + frame = new ConnectionCloseFrame(te.getCloseCode(), te.frameType, + te.getPeerVisibleReason()); // 0x1c + keySpace = te.keySpace; + } + case TerminationCause.InternalError ie -> { + frame = new ConnectionCloseFrame(ie.getCloseCode(), 0, + ie.getPeerVisibleReason()); // 0x1c + keySpace = null; + } + case AppLayerClose alc -> { + // application layer triggered connection close + frame = new ConnectionCloseFrame(alc.getCloseCode(), + alc.getPeerVisibleReason()); // 0x1d + keySpace = null; + } + } + if (keySpace == null) { + // TODO: review this + keySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + } + immediateClose(frame, keySpace, cause); + } + + void incomingConnectionCloseFrame(final ConnectionCloseFrame frame) { + Objects.requireNonNull(frame); + if (debug.on()) { + debug.log("Received close frame: %s", frame); + } + drain(frame); + } + + void incomingStatelessReset() { + // if local endpoint is a client, then our peer is a server + final boolean peerIsServer = connection.isClientConnection(); + if (Log.errors()) { + Log.logError("{0}: stateless reset from peer ({1})", connection.logTag(), + (peerIsServer ? "server" : "client")); + } + final SilentTermination st = forSilentTermination("stateless reset from peer (" + + (peerIsServer ? "server" : "client") + ")"); + terminate(st); + } + + /** + * Called only when the connection is expected to be discarded without being required + * to inform the peer. + * Discards all state, no CONNECTION_CLOSE is sent, nor does the connection enter closing + * or discarding state. + */ + private void silentTerminate(final SilentTermination terminationCause) { + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for this connection + connection.idleTimeoutManager.shutdown(); + // mark the connection state as closed (we don't enter closing or draining state + // during silent termination) + if (!markClosed(terminationCause)) { + // previously already closed + return; + } + if (Log.quic()) { + Log.logQuic("{0} silently terminating connection due to: {1}", + logTag, terminationCause.getLogMsg()); + } else if (debug.on()) { + debug.log("silently terminating connection due to: " + terminationCause.getLogMsg()); + } + if (debug.on() || Log.quic()) { + String message = connection.loggableState(); + if (message != null) { + Log.logQuic("{0} connection state: {1}", logTag, message); + debug.log("connection state: %s", message); + } + } + failHandshakeCFs(); + // remove from the endpoint + unregisterConnFromEndpoint(); + discardConnectionState(); + // terminate the streams + connection.streams.terminate(terminationCause); + } + + CompletableFuture futureTerminationCause() { + return this.futureTC; + } + + private void unregisterConnFromEndpoint() { + final QuicEndpoint endpoint = this.connection.endpoint(); + if (endpoint == null) { + // this can happen if the connection is being terminated before + // an endpoint has been established (which is OK) + return; + } + endpoint.removeConnection(this.connection); + } + + private void immediateClose(final ConnectionCloseFrame closeFrame, + final KeySpace keySpace, + final TerminationCause terminationCause) { + assert closeFrame != null : "connection close frame is null"; + assert keySpace != null : "keyspace is null"; + final String logMsg = terminationCause.getLogMsg(); + // if the connection has already been closed (for example: through silent termination) + // then the local state of the connection is already discarded and thus + // there's nothing more we can do with the connection. + if (connection.stateHandle().isMarked(CLOSED)) { + return; + } + // switch to closing state + if (!markClosing(terminationCause)) { + // has previously already gone into closing state + return; + } + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for a closing connection + connection.idleTimeoutManager.shutdown(); + + if (connection.stateHandle().draining()) { + if (Log.quic()) { + Log.logQuic("{0} skipping immediate close, since connection is already" + + " in draining state", logTag, logMsg); + } else if (debug.on()) { + debug.log("skipping immediate close, since connection is already" + + " in draining state"); + } + // we are already (in the subsequent) draining state, no need to anything more + return; + } + try { + final String closeCodeHex = (terminationCause.isAppLayer() ? "(app layer) " : "") + + "0x" + Long.toHexString(closeFrame.errorCode()); + if (Log.quic()) { + Log.logQuic("{0} entering closing state, code {1} - {2}", logTag, closeCodeHex, logMsg); + } else if (debug.on()) { + debug.log("entering closing state, code " + closeCodeHex + " - " + logMsg); + } + pushConnectionCloseFrame(keySpace, closeFrame); + } catch (Exception e) { + if (Log.errors()) { + Log.logError("{0} removing connection from endpoint after failure to send" + + " CLOSE_CONNECTION: {1}", logTag, e); + } else if (debug.on()) { + debug.log("removing connection from endpoint after failure to send" + + " CLOSE_CONNECTION"); + } + // we failed to send a CONNECTION_CLOSE frame. this implies that the QuicEndpoint + // won't detect that the QuicConnectionImpl has transitioned to closing connection + // and thus won't remap it to closing. we thus discard such connection from the + // endpoint. + unregisterConnFromEndpoint(); + } + failHandshakeCFs(); + discardConnectionState(); + connection.streams.terminate(terminationCause); + if (Log.quic()) { + Log.logQuic("{0} connection has now transitioned to closing state", logTag); + } else if (debug.on()) { + debug.log("connection has now transitioned to closing state"); + } + } + + private void drain(final ConnectionCloseFrame incomingFrame) { + // if the connection has already been closed (for example: through silent termination) + // then the local state of the connection is already discarded and thus + // there's nothing more we can do with the connection. + if (connection.stateHandle().isMarked(CLOSED)) { + return; + } + final boolean isAppLayerClose = incomingFrame.variant(); + final String closeCodeString = isAppLayerClose ? + "[app]" + connection.quicInstance().appErrorToString(incomingFrame.errorCode()) : + QuicTransportErrors.toString(incomingFrame.errorCode()); + final String reason = incomingFrame.reasonString(); + final String peer = connection.isClientConnection() ? "server" : "client"; + final String msg = "Connection closed by " + peer + " peer: " + + closeCodeString + + (reason == null || reason.isEmpty() ? "" : (" " + reason)); + final TerminationCause terminationCause; + if (isAppLayerClose) { + terminationCause = appLayerClose(incomingFrame.errorCode(), msg) + .peerVisibleReason(reason); + } else { + terminationCause = forTransportError(incomingFrame.errorCode(), msg, + incomingFrame.errorFrameType()) + .peerVisibleReason(reason); + } + // switch to draining state + if (!markDraining(terminationCause)) { + // has previously already gone into draining state + return; + } + // shutdown the idle timeout manager since we no longer bother with idle timeout + // management for a closing connection + connection.idleTimeoutManager.shutdown(); + + if (Log.quic()) { + Log.logQuic("{0} entering draining state, {1}", logTag, + terminationCause.getLogMsg()); + } else if (debug.on()) { + debug.log("entering draining state, " + + terminationCause.getLogMsg()); + } + // RFC-9000, section 10.2.2: + // An endpoint that receives a CONNECTION_CLOSE frame MAY send a single packet containing + // a CONNECTION_CLOSE frame before entering the draining state, using a NO_ERROR code if + // appropriate. An endpoint MUST NOT send further packets. + // if we had previously marked our state as closing, then that implies + // we would have already sent a connection close frame. we won't send + // another when draining in such a case. + if (markClosing(terminationCause)) { + try { + if (Log.quic()) { + Log.logQuic("{0} sending CONNECTION_CLOSE frame before entering draining state", + logTag); + } else if (debug.on()) { + debug.log("sending CONNECTION_CLOSE frame before entering draining state"); + } + final ConnectionCloseFrame outgoingFrame = + new ConnectionCloseFrame(NO_ERROR.code(), incomingFrame.getTypeField(), null); + final KeySpace currentKeySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + pushConnectionCloseFrame(currentKeySpace, outgoingFrame); + } catch (Exception e) { + // just log and ignore, since sending the CONNECTION_CLOSE when entering + // draining state is optional + if (Log.errors()) { + Log.logError(logTag + " Failed to send CONNECTION_CLOSE frame," + + " when entering draining state: {0}", e); + } else if (debug.on()) { + debug.log("failed to send CONNECTION_CLOSE frame, when entering" + + " draining state: " + e); + } + } + } + failHandshakeCFs(); + // remap the connection to a draining connection + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + endpoint.draining(connection); + discardConnectionState(); + connection.streams.terminate(terminationCause); + if (Log.quic()) { + Log.logQuic("{0} connection has now transitioned to draining state", logTag); + } else if (debug.on()) { + debug.log("connection has now transitioned to draining state"); + } + } + + private void discardConnectionState() { + // close packet spaces + connection.packetNumberSpaces().close(); + // close the incoming packets buffered queue + connection.closeIncoming(); + } + + private void failHandshakeCFs() { + final TerminationCause tc = this.terminationCause.get(); + assert tc != null : "termination cause is null"; + failHandshakeCFs(tc.getCloseCause()); + } + + private void failHandshakeCFs(final Throwable cause) { + final HandshakeFlow handshakeFlow = connection.handshakeFlow(); + handshakeFlow.failHandshakeCFs(cause); + } + + private boolean markClosing(final TerminationCause terminationCause) { + return mark(CLOSING, terminationCause); + } + + private boolean markDraining(final TerminationCause terminationCause) { + return mark(DRAINING, terminationCause); + } + + private boolean markClosed(final TerminationCause terminationCause) { + return mark(CLOSED, terminationCause); + } + + private boolean mark(final int mask, final TerminationCause cause) { + assert cause != null : "termination cause is null"; + final boolean causeSet = this.terminationCause.compareAndSet(null, cause); + // first mark the state appropriately, before completing the futureTerminationCause + // completable future, so that any dependent actions on the completable future + // will see the right state + final boolean marked = this.connection.stateHandle().mark(mask); + if (causeSet) { + this.futureTC.completeAsync(() -> cause, connection.quicInstance().executor()); + } + return marked; + } + + /** + * CONNECTION_CLOSE frame is not congestion controlled (RFC-9002 section 3 + * and RFC-9000 section 12.4, table 3), nor is it queued or scheduled for sending. + * This method constructs a {@link QuicPacket} containing the {@code frame} and immediately + * {@link QuicConnectionImpl#pushDatagram(ProtectionRecord) pushes the datagram} through + * the connection. + * + * @param keySpace the KeySpace to use for sending the packet + * @param frame the CONNECTION_CLOSE frame + * @throws QuicKeyUnavailableException if the keys for the KeySpace aren't available + * @throws QuicTransportException for any QUIC transport exception when sending the packet + */ + private void pushConnectionCloseFrame(final KeySpace keySpace, + final ConnectionCloseFrame frame) + throws QuicKeyUnavailableException, QuicTransportException { + // ConnectionClose frame is allowed in Initial, Handshake, 0-RTT, 1-RTT spaces. + // for Initial and Handshake space, the frame is expected to be of type 0x1c. + // see RFC-9000, section 12.4, Table 3 for additional details + final ConnectionCloseFrame toSend = switch (keySpace) { + case ONE_RTT, ZERO_RTT -> frame; + case INITIAL, HANDSHAKE -> { + // RFC 9000 - section 10.2.3: + // A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE + // of type 0x1c when sending the frame in Initial or Handshake packets. + // Otherwise, information about the application state might be revealed. + // Endpoints MUST clear the value of the Reason Phrase field and SHOULD + // use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE + // of type 0x1c. + yield frame.clearApplicationState(); + } + default -> { + throw new IllegalStateException("cannot send a connection close frame" + + " in keyspace: " + keySpace); + } + }; + final QuicPacket packet = connection.newQuicPacket(keySpace, List.of(toSend)); + final ProtectionRecord protectionRecord = ProtectionRecord.single(packet, + connection::allocateDatagramForEncryption); + // while sending the packet containing the CONNECTION_CLOSE frame, the pushDatagram will + // remap (or remove) the QuicConnectionImpl in QuicEndpoint. + connection.pushDatagram(protectionRecord); + } + + /** + * Returns a {@link ByteBuffer} which contains an encrypted QUIC packet containing + * a {@linkplain ConnectionCloseFrame CONNECTION_CLOSE frame}. The CONNECTION_CLOSE + * frame will have a frame type of {@code 0x1c} and error code of {@code NO_ERROR}. + *

        + * This method should only be invoked when the {@link QuicEndpoint} is being closed + * and the endpoint wants to send out a {@code CONNECTION_CLOSE} frame on a best-effort + * basis (in a fire and forget manner). + * + * @return the datagram containing the QUIC packet with a CONNECTION_CLOSE frame + * @throws QuicKeyUnavailableException + * @throws QuicTransportException + */ + ByteBuffer makeConnectionCloseDatagram() + throws QuicKeyUnavailableException, QuicTransportException { + // in theory we don't need this assert, but given the knowledge that this method + // should only be invoked by a closing QuicEndpoint, we have this assert here to + // prevent misuse of this makeConnectionCloseDatagram() method + assert connection.endpoint().isClosed() : "QUIC endpoint isn't closed"; + final ConnectionCloseFrame connCloseFrame = new ConnectionCloseFrame(NO_ERROR.code(), + QuicFrame.CONNECTION_CLOSE, null); + final KeySpace keySpace = connection.getTLSEngine().getCurrentSendKeySpace(); + // we don't want the connection's ByteBuffer pooling infrastructure + // (through the QuicConnectionImpl::allocateDatagramForEncryption) for + // this packet, so we use a simple custom allocator. + final Function allocator = (pkt) -> ByteBuffer.allocate(pkt.size()); + final QuicPacket packet = connection.newQuicPacket(keySpace, List.of(connCloseFrame)); + final ProtectionRecord encrypted = ProtectionRecord.single(packet, allocator) + .encrypt(connection.codingContext()); + final ByteBuffer datagram = encrypted.datagram(); + final int firstPacketOffset = encrypted.firstPacketOffset(); + // flip the datagram + datagram.limit(datagram.position()); + datagram.position(firstPacketOffset); + return datagram; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java new file mode 100644 index 00000000000..a7469f18ed8 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/IdleTimeoutManager.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.quic.QuicTLSEngine; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; + +/** + * Keeps track of activity on a {@code QuicConnectionImpl} and manages + * the idle timeout of the QUIC connection + */ +final class IdleTimeoutManager { + + private static final long NO_IDLE_TIMEOUT = 0; + + private final QuicConnectionImpl connection; + private final Logger debug; + private final AtomicBoolean shutdown = new AtomicBoolean(); + private final AtomicLong idleTimeoutDurationMs = new AtomicLong(); + private final ReentrantLock stateLock = new ReentrantLock(); + // must be accessed only when holding stateLock + private IdleTimeoutEvent idleTimeoutEvent; + // must be accessed only when holding stateLock + private StreamDataBlockedEvent streamDataBlockedEvent; + // the time at which the last outgoing packet was sent or an + // incoming packet processed on the connection + private volatile long lastPacketActivityAt; + + private final ReentrantLock idleTerminationLock = new ReentrantLock(); + // true if it has been decided to terminate the connection due to being idle, + // false otherwise. should be accessed only when holding the idleTerminationLock + private boolean chosenForIdleTermination; + // the time at which the connection was last reserved for use. + // should be accessed only when holding the idleTerminationLock + private long lastUsageReservationAt; + + IdleTimeoutManager(final QuicConnectionImpl connection) { + this.connection = Objects.requireNonNull(connection, "connection"); + this.debug = connection.debug; + } + + /** + * Starts the idle timeout management for the connection. This should be called + * after the handshake is complete for the connection. + * + * @throw IllegalStateException if handshake hasn't yet completed or if the handshake + * has failed for the connection + */ + void start() { + final CompletableFuture handshakeCF = + this.connection.handshakeFlow().handshakeCF(); + // start idle management only for successfully completed handshake + if (!handshakeCF.isDone()) { + throw new IllegalStateException("handshake isn't yet complete," + + " cannot start idle connection management"); + } + if (handshakeCF.isCompletedExceptionally()) { + throw new IllegalStateException("cannot start idle connection management for a failed" + + " connection"); + } + startTimers(); + } + + /** + * Starts the idle timeout timer of the QUIC connection, if not already started. + */ + private void startTimers() { + if (shutdown.get()) { + return; + } + this.stateLock.lock(); + try { + if (shutdown.get()) { + return; + } + startIdleTerminationTimer(); + startStreamDataBlockedTimer(); + } finally { + this.stateLock.unlock(); + } + } + + private void startIdleTerminationTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + final Optional idleTimeoutMillis = getIdleTimeout(); + if (idleTimeoutMillis.isEmpty()) { + if (debug.on()) { + debug.log("idle connection management disabled for connection"); + } else { + Log.logQuic("{0} idle connection management disabled for connection", + connection.logTag()); + } + return; + } + final QuicTimerQueue timerQueue = connection.endpoint().timer(); + final Deadline deadline = timeLine().instant().plusMillis(idleTimeoutMillis.get()); + // we don't expect idle timeout management to be started more than once + assert this.idleTimeoutEvent == null : "idle timeout management" + + " already started for connection"; + // create the idle timeout event and register with the QuicTimerQueue. + this.idleTimeoutEvent = new IdleTimeoutEvent(deadline); + timerQueue.offer(this.idleTimeoutEvent); + if (debug.on()) { + debug.log("started QUIC idle timeout management for connection," + + " idle timeout event: " + this.idleTimeoutEvent + + " deadline: " + deadline); + } else { + Log.logQuic("{0} started QUIC idle timeout management for connection," + + " idle timeout event: {1} deadline: {2}", + connection.logTag(), this.idleTimeoutEvent, deadline); + } + } + + private void stopIdleTerminationTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + if (this.idleTimeoutEvent == null) { + return; + } + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + // disable the event (refreshDeadline() of IdleTimeoutEvent will return Deadline.MAX) + final Deadline nextDeadline = this.idleTimeoutEvent.nextDeadline; + if (!nextDeadline.equals(Deadline.MAX)) { + this.idleTimeoutEvent.nextDeadline = Deadline.MAX; + endpoint.timer().reschedule(this.idleTimeoutEvent, Deadline.MIN); + } + this.idleTimeoutEvent = null; + } + + private void startStreamDataBlockedTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + // 75% of idle timeout or if idle timeout is not configured, then 30 seconds + final long timeoutMillis = getIdleTimeout() + .map((v) -> (long) (0.75 * v)) + .orElse(30000L); + final QuicTimerQueue timerQueue = connection.endpoint().timer(); + final Deadline deadline = timeLine().instant().plusMillis(timeoutMillis); + // we don't expect the timer to be started more than once + assert this.streamDataBlockedEvent == null : "STREAM_DATA_BLOCKED timer already started"; + // create the timeout event and register with the QuicTimerQueue. + this.streamDataBlockedEvent = new StreamDataBlockedEvent(deadline, timeoutMillis); + timerQueue.offer(this.streamDataBlockedEvent); + if (debug.on()) { + debug.log("started STREAM_DATA_BLOCKED timer for connection," + + " event: " + this.streamDataBlockedEvent + + " deadline: " + deadline); + } else { + Log.logQuic("{0} started STREAM_DATA_BLOCKED timer for connection," + + " event: {1} deadline: {2}", + connection.logTag(), this.streamDataBlockedEvent, deadline); + } + } + + private void stopStreamDataBlockedTimer() { + assert stateLock.isHeldByCurrentThread() : "not holding state lock"; + if (this.streamDataBlockedEvent == null) { + return; + } + final QuicEndpoint endpoint = this.connection.endpoint(); + assert endpoint != null : "QUIC endpoint is null"; + // disable the event (refreshDeadline() of StreamDataBlockedEvent will return Deadline.MAX) + final Deadline nextDeadline = this.streamDataBlockedEvent.nextDeadline; + if (!nextDeadline.equals(Deadline.MAX)) { + this.streamDataBlockedEvent.nextDeadline = Deadline.MAX; + endpoint.timer().reschedule(this.streamDataBlockedEvent, Deadline.MIN); + } + this.streamDataBlockedEvent = null; + } + + /** + * Attempts to notify the idle connection management that this connection should + * be considered "in use". This way the idle connection management doesn't close + * this connection during the time the connection is handed out from the pool and any + * new stream created on that connection. + * + * @return true if the connection has been successfully reserved and is {@link #isOpen()}. false + * otherwise; in which case the connection must not be handed out from the pool. + */ + boolean tryReserveForUse() { + this.idleTerminationLock.lock(); + try { + if (chosenForIdleTermination) { + // idle termination has been decided for this connection, don't use it + return false; + } + // if the connection is nearing idle timeout due to lack of traffic then + // don't use it + final long lastPktActivity = lastPacketActivityAt; + final long currentNanos = System.nanoTime(); + final long inactivityMs = MILLISECONDS.convert((currentNanos - lastPktActivity), + NANOSECONDS); + final boolean nearingIdleTimeout = getIdleTimeout() + .map((timeoutMillis) -> inactivityMs >= (0.8 * timeoutMillis)) // 80% of idle timeout + .orElse(false); + if (nearingIdleTimeout) { + return false; + } + // express interest in using the connection + this.lastUsageReservationAt = System.nanoTime(); + return true; + } finally { + this.idleTerminationLock.unlock(); + } + } + + + /** + * Returns the idle timeout duration, in milliseconds, negotiated for the connection represented + * by this {@code IdleTimeoutManager}. The negotiated idle timeout of a connection + * is the minimum of the idle connection timeout that is advertised by the + * endpoint represented by this {@code IdleTimeoutManager} and the idle + * connection timeout advertised by the peer. If neither endpoints have advertised + * any idle connection timeout then this method returns an + * {@linkplain Optional#empty() empty} value. + * + * @return the idle timeout in milliseconds or {@linkplain Optional#empty() empty} + */ + Optional getIdleTimeout() { + final long val = this.idleTimeoutDurationMs.get(); + return val == NO_IDLE_TIMEOUT ? Optional.empty() : Optional.of(val); + } + + void keepAlive() { + lastPacketActivityAt = System.nanoTime(); // TODO: timeline().instant()? + } + + void shutdown() { + if (!shutdown.compareAndSet(false, true)) { + // already shutdown + return; + } + this.stateLock.lock(); + try { + // unregister the timeout events from the QuicTimerQueue + stopIdleTerminationTimer(); + stopStreamDataBlockedTimer(); + } finally { + this.stateLock.unlock(); + } + if (debug.on()) { + debug.log("idle timeout manager shutdown"); + } + } + + void localIdleTimeout(final long timeoutMillis) { + checkUpdateIdleTimeout(timeoutMillis); + } + + void peerIdleTimeout(final long timeoutMillis) { + checkUpdateIdleTimeout(timeoutMillis); + } + + private void checkUpdateIdleTimeout(final long newIdleTimeoutMillis) { + if (newIdleTimeoutMillis <= 0) { + // idle timeout should be non-zero value, we disregard other values + return; + } + long current; + boolean updated = false; + // update the idle timeout if the new timeout is lesser + // than the previously set value + while ((current = this.idleTimeoutDurationMs.get()) == NO_IDLE_TIMEOUT + || current > newIdleTimeoutMillis) { + updated = this.idleTimeoutDurationMs.compareAndSet(current, newIdleTimeoutMillis); + if (updated) { + break; + } + } + if (!updated) { + return; + } + if (debug.on()) { + debug.log("idle connection timeout updated to " + + newIdleTimeoutMillis + " milli seconds"); + } else { + Log.logQuic("{0} idle connection timeout updated to {1} milli seconds", + connection.logTag(), newIdleTimeoutMillis); + } + } + + private TimeLine timeLine() { + return this.connection.endpoint().timeSource(); + } + + // called when the connection has been idle past its idle timeout duration + private void idleTimedOut() { + if (shutdown.get()) { + return; // nothing to do - the idle timeout manager has been shutdown + } + final Optional timeoutVal = getIdleTimeout(); + assert timeoutVal.isPresent() : "unexpectedly idle timing" + + " out connection, when no idle timeout is configured"; + final long timeoutMillis = timeoutVal.get(); + if (Log.quic() || debug.on()) { + // log idle timeout, with packet space statistics + final String msg = "silently terminating connection due to idle timeout (" + + timeoutMillis + " milli seconds)"; + StringBuilder sb = new StringBuilder(); + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (connection.packetNumberSpaces().get(sp) instanceof PacketSpaceManager m) { + sb.append("\n PacketSpace: ").append(sp).append('\n'); + m.debugState(" ", sb); + } + } + if (Log.quic()) { + Log.logQuic("{0} {1}: {2}", connection.logTag(), msg, sb.toString()); + } else if (debug.on()) { + debug.log("%s: %s", msg, sb); + } + } + // silently close the connection and discard all its state + final TerminationCause cause = forSilentTermination("connection idle timed out (" + + timeoutMillis + " milli seconds)"); + connection.terminator.terminate(cause); + } + + private long computeInactivityMillis() { + final long currentNanos = System.nanoTime(); + final long lastActiveNanos = Math.max(lastPacketActivityAt, lastUsageReservationAt); + return MILLISECONDS.convert((currentNanos - lastActiveNanos), NANOSECONDS); + } + + final class IdleTimeoutEvent implements QuicTimedEvent { + private final long eventId; + private volatile Deadline deadline; + private volatile Deadline nextDeadline; + + private IdleTimeoutEvent(final Deadline deadline) { + assert deadline != null : "timeout deadline is null"; + this.deadline = this.nextDeadline = deadline; + this.eventId = QuicTimerQueue.newEventId(); + } + + @Override + public Deadline deadline() { + return this.deadline; + } + + @Override + public Deadline refreshDeadline() { + if (shutdown.get()) { + return this.deadline = this.nextDeadline = Deadline.MAX; + } + return this.deadline = this.nextDeadline; + } + + @Override + public Deadline handle() { + if (shutdown.get()) { + // timeout manager is shutdown, nothing more to do + return this.nextDeadline = Deadline.MAX; + } + final Optional idleTimeout = getIdleTimeout(); + if (idleTimeout.isEmpty()) { + // nothing to do, don't reschedule + return Deadline.MAX; + } + final long idleTimeoutMillis = idleTimeout.get(); + // check whether the connection has indeed been idle for the idle timeout duration + idleTerminationLock.lock(); + try { + Deadline postponed = maybePostponeDeadline(idleTimeoutMillis); + if (postponed != null) { + // not idle long enough, reschedule + this.nextDeadline = postponed; + return postponed; + } + chosenForIdleTermination = true; + } finally { + idleTerminationLock.unlock(); + } + // the connection has been idle for the idle timeout duration, go + // ahead and terminate it. + terminateNow(); + assert shutdown.get() : "idle timeout manager was expected to be shutdown"; + this.nextDeadline = Deadline.MAX; + return Deadline.MAX; + } + + private Deadline maybePostponeDeadline(final long expectedIdleDurationMs) { + assert idleTerminationLock.isHeldByCurrentThread() : "not holding idle termination lock"; + final long inactivityMs = computeInactivityMillis(); + if (inactivityMs >= expectedIdleDurationMs) { + // the connection has been idle long enough, don't postpone the timeout. + return null; + } + // not idle long enough, compute the deadline when it's expected to reach + // idle timeout + final long remainingMs = expectedIdleDurationMs - inactivityMs; + final Deadline next = timeLine().instant().plusMillis(remainingMs); + if (debug.on()) { + debug.log("postponing timeout event: " + this + " to fire" + + " in " + remainingMs + " milli seconds, deadline: " + next); + } + return next; + } + + private void terminateNow() { + try { + idleTimedOut(); + } finally { + shutdown(); + } + } + + @Override + public long eventId() { + return this.eventId; + } + + @Override + public String toString() { + return "QuicIdleTimeoutEvent-" + this.eventId; + } + } + + final class StreamDataBlockedEvent implements QuicTimedEvent { + private final long eventId; + private final long timeoutMillis; + private volatile Deadline deadline; + private volatile Deadline nextDeadline; + + private StreamDataBlockedEvent(final Deadline deadline, final long timeoutMillis) { + assert deadline != null : "timeout deadline is null"; + this.deadline = this.nextDeadline = deadline; + this.timeoutMillis = timeoutMillis; + this.eventId = QuicTimerQueue.newEventId(); + } + + @Override + public Deadline deadline() { + return this.deadline; + } + + @Override + public Deadline refreshDeadline() { + if (shutdown.get()) { + return this.deadline = this.nextDeadline = Deadline.MAX; + } + return this.deadline = this.nextDeadline; + } + + @Override + public Deadline handle() { + if (shutdown.get()) { + // timeout manager is shutdown, nothing more to do + return this.nextDeadline = Deadline.MAX; + } + // check whether the connection has indeed been idle for the idle timeout duration + idleTerminationLock.lock(); + try { + if (chosenForIdleTermination) { + // connection is already chosen for termination, no need to send + // a STREAM_DATA_BLOCKED + this.nextDeadline = Deadline.MAX; + return this.nextDeadline; + } + final long inactivityMs = computeInactivityMillis(); + if (inactivityMs >= timeoutMillis && connection.streams.hasBlockedStreams()) { + // has been idle long enough, but there are streams that are blocked due to + // flow control limits and that could have lead to the idleness. + // trigger sending a STREAM_DATA_BLOCKED frame for the streams + // to try and have their limits increased by the peer. + connection.streams.enqueueStreamDataBlocked(); + if (debug.on()) { + debug.log("enqueued a STREAM_DATA_BLOCKED frame since connection" + + " has been idle due to blocked stream(s)"); + } else { + Log.logQuic("{0} enqueued a STREAM_DATA_BLOCKED frame" + + " since connection has been idle due to" + + " blocked stream(s)", connection.logTag()); + } + } + this.nextDeadline = timeLine().instant().plusMillis(timeoutMillis); + return this.nextDeadline; + } finally { + idleTerminationLock.unlock(); + } + } + + @Override + public long eventId() { + return this.eventId; + } + + @Override + public String toString() { + return "StreamDataBlockedEvent-" + this.eventId; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java new file mode 100644 index 00000000000..76a1f251e75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/LocalConnIdManager.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantLock; + +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * Manages the connection ids advertised by the local endpoint of a connection. + * - Produces outgoing NEW_CONNECTION_ID frames, + * - handles incoming RETIRE_CONNECTION_ID frames, + * - registers produced connection IDs with the QuicEndpoint + * Handshake connection ID is created and registered by QuicConnection. + */ +final class LocalConnIdManager { + private final Logger debug; + private final QuicConnectionImpl connection; + private long nextConnectionIdSequence; + private final ReentrantLock lock = new ReentrantLock(); + private boolean closed; // when true, no more connection IDs are registered + + // the connection ids (there can be more than one) with which the endpoint identifies this connection. + // the key of this Map is a (RFC defined) sequence number for the connection id + private final NavigableMap localConnectionIds = + Collections.synchronizedNavigableMap(new TreeMap<>()); + + LocalConnIdManager(final QuicConnectionImpl connection, final String dbTag, + QuicConnectionId handshakeConnectionId) { + this.debug = Utils.getDebugLogger(() -> dbTag); + this.connection = connection; + this.localConnectionIds.put(nextConnectionIdSequence++, handshakeConnectionId); + } + + private QuicConnectionId newConnectionId() { + return connection.endpoint().idFactory().newConnectionId(); + + } + + private byte[] statelessTokenFor(QuicConnectionId cid) { + return connection.endpoint().idFactory().statelessTokenFor(cid); + } + + void handleRetireConnectionIdFrame(final QuicConnectionId incomingPacketDestConnId, + final QuicPacket.PacketType packetType, + final RetireConnectionIDFrame retireFrame) + throws QuicTransportException { + if (debug.on()) { + debug.log("Received RETIRE_CONNECTION_ID frame: %s", retireFrame); + } + final QuicConnectionId toRetire; + lock.lock(); + try { + final long seqNumber = retireFrame.sequenceNumber(); + if (seqNumber >= nextConnectionIdSequence) { + // RFC-9000, section 19.16: Receipt of a RETIRE_CONNECTION_ID frame containing a + // sequence number greater than any previously sent to the peer MUST be treated + // as a connection error of type PROTOCOL_VIOLATION + throw new QuicTransportException("Invalid sequence number " + seqNumber + + " in RETIRE_CONNECTION_ID frame", + packetType.keySpace().orElse(null), + retireFrame.getTypeField(), PROTOCOL_VIOLATION); + } + toRetire = this.localConnectionIds.get(seqNumber); + if (toRetire == null) { + return; + } + if (toRetire.equals(incomingPacketDestConnId)) { + // RFC-9000, section 19.16: The sequence number specified in a RETIRE_CONNECTION_ID + // frame MUST NOT refer to the Destination Connection ID field of the packet in which + // the frame is contained. The peer MAY treat this as a connection error of type + // PROTOCOL_VIOLATION. + throw new QuicTransportException("Invalid connection id in RETIRE_CONNECTION_ID frame", + packetType.keySpace().orElse(null), + retireFrame.getTypeField(), PROTOCOL_VIOLATION); + } + // forget this id from our local store + this.localConnectionIds.remove(seqNumber); + this.connection.endpoint().removeConnectionId(toRetire, connection); + } finally { + lock.unlock(); + } + if (debug.on()) { + debug.log("retired connection id " + toRetire); + } + } + + public QuicFrame nextFrame(int remaining) { + if (localConnectionIds.size() >= 2) { + return null; + } + int cidlen = connection.endpoint().idFactory().connectionIdLength(); + if (cidlen == 0) { + return null; + } + // frame: + // type - 1 byte + // sequence number - var int + // retire prior to - 1 byte (always zero) + // connection id: + 1 byte + // stateless reset token - 16 bytes + int len = 19 + cidlen + VariableLengthEncoder.getEncodedSize(nextConnectionIdSequence); + if (len > remaining) { + return null; + } + NewConnectionIDFrame newCidFrame; + QuicConnectionId cid = newConnectionId(); + byte[] token = statelessTokenFor(cid); + lock.lock(); + try { + if (closed) return null; + newCidFrame = new NewConnectionIDFrame(nextConnectionIdSequence++, 0, + cid.asReadOnlyBuffer(), ByteBuffer.wrap(token)); + this.localConnectionIds.put(newCidFrame.sequenceNumber(), cid); + this.connection.endpoint().addConnectionId(cid, connection); + if (debug.on()) { + debug.log("Sending NEW_CONNECTION_ID frame"); + } + return newCidFrame; + } finally { + lock.unlock(); + } + } + + public List connectionIds() { + lock.lock(); + try { + // copy to avoid ConcurrentModificationException + return List.copyOf(localConnectionIds.values()); + } finally { + lock.unlock(); + } + } + + public void close() { + lock.lock(); + closed = true; + lock.unlock(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java new file mode 100644 index 00000000000..52821a0fac2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/OrderedFlow.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; + +/** + * A class to take care of frames reordering in an ordered flow. + * + * Frames that are {@linkplain #receive(QuicFrame) received} out of order + * will be either buffered or dropped, depending on their {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) position} + * with respect to the current ordered flow {@linkplain #offset() offset}. + * The buffered frames are returned by later calls to {@linkplain #poll()} + * when the flow offset matches the frame offset. + * + * Frames that are {@linkplain #receive(QuicFrame) received} in order + * are immediately returned. + * + * This class is not thread-safe and concurrent access needs to be synchronized + * externally. + * @param A frame type that defines an offset and a {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) + * length}. The offset of the frame + * indicates its {@linkplain + * #OrderedFlow(Comparator, ToLongFunction, ToIntFunction) + * position} in the ordered flow. + */ +public sealed abstract class OrderedFlow { + + /** + * A subclass of {@link OrderedFlow} used to reorder instances of + * {@link CryptoFrame}. + */ + public static final class CryptoDataFlow extends OrderedFlow { + /** + * Constructs a new instance of {@code CryptoDataFlow} to reorder + * a flow of {@code CryptoFrame} instances. + */ + public CryptoDataFlow() { + super(CryptoFrame::compareOffsets, + CryptoFrame::offset, + CryptoFrame::length); + } + + @Override + protected CryptoFrame slice(CryptoFrame frame, long offset, int length) { + if (length == 0) return null; + return frame.slice(offset, length); + } + } + + /** + * A subclass of {@link OrderedFlow} used to reorder instances of + * {@link StreamFrame}. + */ + public static final class StreamDataFlow extends OrderedFlow { + /** + * Constructs a new instance of {@code StreamDataFlow} to reorder + * a flow of {@code StreamFrame} instances. + */ + public StreamDataFlow() { + super(StreamFrame::compareOffsets, + StreamFrame::offset, + StreamFrame::dataLength); + } + + @Override + protected StreamFrame slice(StreamFrame frame, long offset, int length) { + if (length == 0) return null; + return frame.slice(offset, length); + } + } + + private final ConcurrentSkipListSet queue; + private final ToLongFunction position; + private final ToIntFunction length; + long offset; + long buffered; + + /** + * Constructs a new instance of ordered flow to reorder frames in a given + * flow. + * @param comparator A comparator to order the frames according to their position in + * the ordered flow. Typically, this will compare the + * frame's offset: the frame with the smaller offset will be sorted + * before the frame with the greater offset + * @param position A method reference that returns the position of the frame in the + * flow. For instance, this would be {@link CryptoFrame#offset() + * CryptoFrame::offset} if {@code } is {@code CryptoFrame}, or + * {@link StreamFrame#offset() StreamFrame::offset} if {@code } + * is {@code StreamFrame} + * @param length A method reference that returns the number of bytes in the frame data. + * This is used to compute the expected position of the next + * frame in the flow. For instance, this would be {@link CryptoFrame#length() + * CryptoFrame::length} if {@code } is {@code CryptoFrame}, or + * {@link StreamFrame#dataLength() StreamFrame::dataLength} if {@code } + * is {@code StreamFrame} + */ + public OrderedFlow(Comparator comparator, ToLongFunction position, + ToIntFunction length) { + queue = new ConcurrentSkipListSet<>(comparator); + this.position = position; + this.length = length; + } + + /** + * {@return a slice of the given frame} + * @param frame the frame to slice + * @param offset the new frame offset + * @param length the new frame length + * @throws IndexOutOfBoundsException if the new offset or length + * fall outside of the frame's bounds + */ + protected abstract T slice(T frame, long offset, int length); + + /** + * Receives a new frame. If the frame is below the current + * offset the frame is dropped. If it is above the current offset, + * it is queued. + * If the frame is exactly at the current offset, it is + * returned. + * + * @param frame a frame that was received + * @return the next frame in the flow, or {@code null} if it is not + * available yet. + */ + public T receive(T frame) { + if (frame == null) return null; + + long start = this.position.applyAsLong(frame); + int length = this.length.applyAsInt(frame); + long end = start + length; + assert length >= 0; + assert start >= 0; + long offset = this.offset; + if (end <= offset || length == 0) { + // late arrival or empty frame. Just drop it; No overlap + // if we reach here! + return null; + } else if (start > offset) { + // the frame is after the offset. + // insert or slice it, depending on what we + // have already received. + enqueue(frame, start, length, offset); + return null; + } else { + // case where the frame is either at offset, or is below + // offset but has a length that provides bytes that + // overlap with the current offset. In the later case + // we will return a slice. + int todeliver = (int)(end - offset); + + assert end == offset + todeliver; + // update the offset with the new position + this.offset = end; + // cleanup the queue + dropuntil(end); + if (start == offset) return frame; + return slice(frame, offset, todeliver); + } + } + + private T peekFirst() { + if (queue.isEmpty()) return null; + // why is there no peekFirst? + try { + return queue.first(); + } catch (NoSuchElementException nse) { + return null; + } + } + + private void enqueue(T frame, long pos, int length, long after) { + assert pos == position.applyAsLong(frame); + assert length == this.length.applyAsInt(frame); + assert pos > after; + long offset = this.offset; + assert offset >= after; + long newpos = pos; + int newlen = length; + long limit = Math.addExact(pos, length); + + // look at the closest frame, if any, whose offset is <= to + // the new frame offset. Try to see if the new frame overlaps + // with that frame, and if so, drops the part that overlaps + // in the new frame. + T floor = queue.floor(frame); + if (floor != null) { + long foffset = position.applyAsLong(floor); + long flen = this.length.applyAsInt(floor); + if (limit <= foffset + flen) { + // bytes already all buffered! + // just drop the frame + return; + } + assert foffset <= pos; + // foffset == pos case handled as ceiling below + if (foffset < pos && pos - foffset < flen) { + // reduce the frame if it overlaps with the + // one that sits just before in the queue + newpos = foffset + flen; + newlen = length - (int) (newpos - pos); + } + } + assert limit == newpos + newlen; + + // Look at the frames that have an offset higher or equal to + // the new frame offset, and see if any overlap with the new + // frame. Remove frames that are entirely contained in the new one, + // slice the current frame if the frames overlap. + while (true) { + T ceil = queue.ceiling(frame); + if (ceil != null) { + long coffset = position.applyAsLong(ceil); + assert coffset >= newpos : "overlapping frames in queue"; + if (coffset < limit) { + long clen = this.length.applyAsInt(ceil); + if (clen <= limit - coffset) { + // ceiling frame completely contained in the new frame: + // remove the ceiling frame + queue.remove(ceil); + buffered -= clen; + continue; + } + // safe cast, since newlen <= len + newlen = (int) (coffset - newpos); + } + } + break; + } + assert newlen >= 0; + if (newlen == length) { + assert newpos == pos; + queue.add(frame); + } else if (newlen > 0) { + queue.add(slice(frame, newpos, newlen)); + } + buffered += newlen; + } + + /** + * Removes and return the head of the queue if it is at the + * current offset. Otherwise, returns null. + * @return the head of the queue if it is at the current offset, + * or {@code null} + */ + public T poll() { + return poll(offset); + } + + /** + * {@return the number of buffered frames} + */ + public int size() { + return queue.size(); + } + + /** + * {@return the number of bytes buffered} + */ + public long buffered() { + return buffered; + } + + /** + * {@return true if there are no buffered frames} + */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** + * {@return the current offset of this buffer} + */ + public long offset() { + return offset; + } + + /** + * Drops all buffered frames + */ + public void clear() { + queue.clear(); + } + + /** + * Drop all frames in the buffer whose position is strictly + * below offset. + * + * @param offset the offset below which frames should be dropped + * @return the amount of dropped data + */ + private long dropuntil(long offset) { + T head; + long pos; + long dropped = 0; + do { + head = peekFirst(); + if (head == null) break; + pos = position.applyAsLong(head); + if (pos < offset) { + var length = this.length.applyAsInt(head); + var consumed = offset - pos; + if (length <= consumed) { + // drop it + if (head == queue.pollFirst()) { + buffered -= length; + dropped += length; + } else { + throw new AssertionError("Concurrent modification"); + } + } else { + // safe cast: consumed < length if we reach here + int newlen = length - (int)consumed; + var newhead = slice(head, offset, newlen); + if (head == queue.pollFirst()) { + queue.add(newhead); + buffered -= consumed; + dropped += consumed; + } else { + throw new AssertionError("Concurrent modification"); + } + } + } + } while (pos < offset); + return dropped; + } + + /** + * Pretends to {@linkplain #receive(QuicFrame) receive} the head of the queue, + * if it is at the provided offset + * + * @param offset the minimal offset + * + * @return a received frame at the current flow offset, or {@code null} + */ + private T poll(long offset) { + long current = this.offset; + assert offset <= current; + dropuntil(offset); + T head = peekFirst(); + if (head != null) { + long pos = position.applyAsLong(head); + if (pos == offset) { + // the frame we wanted was in the queue! + // well, let's handle it... + if (head == queue.pollFirst()) { + long length = this.length.applyAsInt(head); + buffered -= length; + } else { + throw new AssertionError("Concurrent modification"); + } + return receive(head); + } + } + return null; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java new file mode 100644 index 00000000000..35b42333a1e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketEmitter.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.concurrent.Executor; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTransportException; + +/** + * This interface is a useful abstraction used to tie + * {@link PacketSpaceManager} and {@link QuicConnectionImpl}. + * The {@link PacketSpaceManager} uses functionalities provided + * by a {@link PacketEmitter} when it deems that a packet needs + * to be retransmitted, or that an acknowledgement is due. + * It also uses the emitter's {@linkplain #timer() timer facility} + * when it needs to register a {@link QuicTimedEvent}. + * + * @apiNote + * All these methods are actually implemented by {@link QuicConnectionImpl} + * but the {@code PacketEmitter} interface makes it possible to write + * unit tests against a {@link PacketSpaceManager} without involving + * any {@code QuicConnection} instance. + * + */ +public interface PacketEmitter { + /** + * {@return the timer queue used by this packet emitter} + */ + QuicTimerQueue timer(); + + /** + * Retransmit the given packet on behalf of the given packet space + * manager. + * @param packetSpaceManager the packet space manager on behalf of + * which the packet is being retransmitted + * @param packet the unacknowledged packet which should be retransmitted + * @param attempts the number of previous retransmission of this packet. + * A value of 0 indicates the first retransmission. + */ + void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Emit a possibly non ACK-eliciting packet containing the given ACK frame. + * @param packetSpaceManager the packet space manager on behalf + * of which the acknowledgement should + * be sent. + * @param ackFrame the ACK frame to be sent. + * @param sendPing whether a PING frame should be sent. + * @return the emitted packet number, or -1L if not applicable or not emitted + */ + long emitAckPacket(PacketSpace packetSpaceManager, AckFrame ackFrame, boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * Called when a packet has been acknowledged. + * @param packet the acknowledged packet + */ + void acknowledged(QuicPacket packet); + + /** + * Called when congestion controller allows sending one packet + * @param packetNumberSpace current packet number space + * @return true if a packet was sent, false otherwise + */ + boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException; + + /** + * {@return an executor to use when {@linkplain + * jdk.internal.net.http.common.SequentialScheduler#runOrSchedule(Executor) + * offloading loops to another thread} is required} + */ + Executor executor(); + + /** + * Reschedule the given event on the {@link #timer() timer} + * @param event the event to reschedule + */ + default void reschedule(QuicTimedEvent event) { + timer().reschedule(event); + } + + /** + * Reschedule the given event on the {@link #timer() timer} + * @param event the event to reschedule + */ + default void reschedule(QuicTimedEvent event, Deadline deadline) { + timer().reschedule(event, deadline); + } + + /** + * Abort the connection if needed, for example if the peer is not responding + * or max idle time was reached + */ + void checkAbort(PacketNumberSpace packetNumberSpace); + + /** + * {@return true if this emitter is open for transmitting packets, else returns false} + */ + boolean isOpen(); + + default void ptoBackoffIncreased(PacketSpaceManager space, long backoff) { }; + + default String logTag() { return toString(); } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java new file mode 100644 index 00000000000..494af85447e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java @@ -0,0 +1,2370 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.AckFrame.AckFrameBuilder; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A {@code PacketSpaceManager} takes care of acknowledgement and + * retransmission of packets for a given {@link PacketNumberSpace}. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ + +// See also: RFC 9000, https://www.rfc-editor.org/rfc/rfc9000#name-sending-ack-frames +// Every packet SHOULD be acknowledged at least once, and +// ack-eliciting packets MUST be acknowledged at least once within +// the maximum delay an endpoint communicated using the max_ack_delay +// transport parameter [...]; +// [...] +// In order to assist loss detection at the sender, an endpoint +// SHOULD generate and send an ACK frame without delay when it +// receives an ack-eliciting packet either: +// - when the received packet has a packet number less +// than another ack-eliciting packet that has been received, or +// - when the packet has a packet number larger than the +// highest-numbered ack-eliciting packet that has been received +// and there are missing packets between that packet and this +// packet. [...] +public sealed class PacketSpaceManager implements PacketSpace + permits PacketSpaceManager.OneRttPacketSpaceManager, + PacketSpaceManager.HandshakePacketSpaceManager { + + private final QuicCongestionController congestionController; + private volatile boolean blockedByCC; + // packet threshold for loss detection; RFC 9002 suggests 3 + private static final long kPacketThreshold = 3; + // Multiplier for persistent congestion; RFC 9002 suggests 3 + private static final int kPersistentCongestionThreshold = 3; + + /** + * A record that stores the next AckFrame that should be sent + * within this packet number space. + * + * @param ackFrame the ACK frame to send. + * @param deadline the deadline by which to send this ACK frame. + * @param lastUpdated the time at which the {@link AckFrame}'s + * {@link AckFrame#largestAcknowledged()} was + * last updated. Used for calculating ack delay. + * @param sent the time at which the {@link AckFrame} was sent, + * or {@code null} if it has not been sent yet. + */ + record NextAckFrame(AckFrame ackFrame, + Deadline deadline, + Deadline lastUpdated, + Deadline sent) { + /** + * {@return an identical {@code NextAckFrame} record, with an updated + * {@code deadline}} + * @param deadline the new deadline + * @param sent the point in time at which the ack frame was sent, or null. + */ + public NextAckFrame withDeadline(Deadline deadline, Deadline sent) { + return new NextAckFrame(ackFrame, deadline, lastUpdated, sent); + } + } + + // true if transmit timer should fire now + private volatile boolean transmitNow; + + // These two numbers control whether an PING frame will be + // sent with the next ACK frame, to turn the packet that + // contains the ACK frame into an ACK-eliciting packet. + // These numbers are *not* defined in RFC 9000, but are used + // to implement a strategy for sending occasional PING frames + // in order to prevent ACK frames from growing too big. + // See RFC 9000 section 13.2.4 + // https://www.rfc-editor.org/rfc/rfc9000#name-limiting-ranges-by-tracking + public static final int MAX_ACKRANGE_COUNT_BEFORE_PING = 10; + + protected final Logger debug; + private final Supplier debugStrSupplier; + private final PacketNumberSpace packetNumberSpace; + private final PacketEmitter packetEmitter; + private final ReentrantLock transferLock = new ReentrantLock(); + // The next packet number to use in this space + private final AtomicLong nextPN = new AtomicLong(); + private final TimeLine instantSource; + private final QuicRttEstimator rttEstimator; + // first packet number sent after handshake confirmed + private long handshakeConfirmedPN; + + // A priority queue containing a record for each unacknowledged PingRequest. + // PingRequest are removed from this queue when they are acknowledged, that + // is when any packet whose number is greater than the request packet + // is acknowledged. + // Note: this is used to implement {@link #requestSendPing()} which is + // used to implement out of band ping requests triggered by the + // application. + private final ConcurrentLinkedQueue pendingPingRequests = + new ConcurrentLinkedQueue<>(); + + // A priority queue containing a record for each unacknowledged packet. + // Packets are removed from this queue when they are acknowledged, or when they + // are being retransmitted. In which case, they will be in the pendingRetransmission + // queue + private final ConcurrentLinkedQueue pendingAcknowledgements = + new ConcurrentLinkedQueue<>(); + + // A map containing send times of ack-eliciting packets. + // Packets are removed from this map when they can't contribute to RTT sample, + // i.e. when they are acknowledged, or when a higher-numbered packet is acknowledged. + private final ConcurrentSkipListMap sendTimes = + new ConcurrentSkipListMap<>(); + + // A priority queue containing a record for each unacknowledged packet whose deadline + // is due, and which is currently being retransmitted. + // Packets are removed from this queue when they have been scheduled for retransmission + // with the quic endpoint + private final ConcurrentLinkedQueue pendingRetransmission = + new ConcurrentLinkedQueue<>(); + + // A priority queue containing a record for each unacknowledged packet whose deadline + // is due, and which should be retransmitted. + // Packets are removed from this queue when they have been scheduled for encryption. + private final ConcurrentLinkedQueue triggeredForRetransmission = + new ConcurrentLinkedQueue<>(); + + // lost packets + private final ConcurrentLinkedQueue lostPackets = + new ConcurrentLinkedQueue<>(); + + // A task invoked by the QuicTimerQueue when some packet retransmission are + // due. This task will move packets from the pendingAcknowledgement queue + // into the triggeredForRetransmission queue (and pendingRetransmission queue) + private final PacketTransmissionTask packetTransmissionTask; + // Used to synchronize transmission with handshake restarts + private final ReentrantLock transmitLock = new ReentrantLock(); + private volatile boolean fastRetransmitDone; + private volatile boolean fastRetransmit; + + /** + * A record to store previous numbers with which a packet has been + * retransmitted. If such a packet is acknowledged, we can stop + * retransmission. + * + * @param number A packet number with which the content of this + * packet was previously sent. + * @param largestAcknowledged the largest packet number acknowledged by this + * previous packet, or {@code -1L} if no packet was + * acknowledged by this packet. + * @param previous Further previous packet numbers, or {@code null}. + */ + private record PreviousNumbers(long number, + long largestAcknowledged, PreviousNumbers previous) {} + + /** + * A record used to implement {@link #requestSendPing()}. + * @param sent when the ping frame was sent + * @param packetNumber the packet number of the packet containing the pingframe + * @param response the response, which will be complete as soon as a packet whose number is + * >= to {@code packetNumber} is received. + */ + private record PingRequest(Deadline sent, long packetNumber, CompletableFuture response) {} + + /** + * A record to store a packet that hasn't been acknowledged, and should + * be scheduled for retransmission if not acknowledged when the deadline + * is reached. + * + * @param packet the unacknowledged quic packet + * @param sent the instant when the packet was sent. + * @param packetNumber the packet number of the {@code packet} + * @param largestAcknowledged the largest packet number acknowledged by this + * packet, or {@code -1L} if no packet is acknowledged + * by this packet. + * @param previousNumbers previous packet numbers with which the packet was + * transmitted, if any, {@code null} otherwise. + */ + private record PendingAcknowledgement(QuicPacket packet, Deadline sent, + long packetNumber, long largestAcknowledged, + PreviousNumbers previousNumbers) { + + PendingAcknowledgement(QuicPacket packet, Deadline sent, + long packetNumber, PreviousNumbers previousNumbers) { + this(packet, sent, packetNumber, + AckFrame.largestAcknowledgedInPacket(packet), previousNumbers); + } + + boolean hasPreviousNumber(long packetNumber) { + if (this.packetNumber <= packetNumber) return false; + var pn = previousNumbers; + while (pn != null) { + if (pn.number == packetNumber) { + return true; + } + pn = pn.previous; + } + return false; + } + boolean hasExactNumber(long packetNumber) { + return this.packetNumber == packetNumber; + } + boolean hasNumber(long packetNumber) { + return this.packetNumber == packetNumber || hasPreviousNumber(packetNumber); + } + PreviousNumbers findPreviousAcknowledged(AckFrame frame) { + var pn = previousNumbers; + while (pn != null) { + if (frame.isAcknowledging(pn.number)) return pn; + pn = pn.previous; + } + return null; + } + boolean isAcknowledgedBy(AckFrame frame) { + if (frame.isAcknowledging(packetNumber)) return true; + else return findPreviousAcknowledged(frame) != null; + } + + public int attempts() { + var pn = previousNumbers; + int count = 0; + while (pn != null) { + count++; + pn = pn.previous; + } + return count; + } + + String prettyPrint() { + StringBuilder b = new StringBuilder(); + b.append("pn:").append(packetNumber); + var ppn = previousNumbers; + if (ppn != null) { + var sep = " ["; + while (ppn != null) { + b.append(sep).append(ppn.number); + ppn = ppn.previous; + sep = ", "; + } + b.append("]"); + } + return b.toString(); + } + } + + /** + * A task that sends packets to the peer. + * + * Packets are sent after a delay when: + * - ack delay timer expires + * - PTO timer expires + * They can also be sent without delay when: + * - we are unblocked by the peer + * - new data is available for sending, and we are not blocked + * - need to send ack without delay + */ + final class PacketTransmissionTask implements QuicTimedEvent { + private final SequentialScheduler handleScheduler = + SequentialScheduler.lockingScheduler(this::handleLoop); + private final long id = QuicTimerQueue.newEventId(); + private volatile Deadline nextDeadline; // updated through VarHandle + private PacketTransmissionTask() { + nextDeadline = Deadline.MAX; + } + + @Override + public long eventId() { return id; } + + @Override + public Deadline deadline() { + return nextDeadline; + } + + @Override + public Deadline handle() { + if (closed) { + if (debug.on()) { + debug.log("packet space already closed, PacketTransmissionTask will" + + " no longer be scheduled"); + } + return Deadline.MAX; + } + handleScheduler.runOrSchedule(packetEmitter.executor()); + return Deadline.MAX; + } + + /** + * The handle loop takes care of sending ACKs, packaging stream data + * (if applicable), and retransmitting on PTO. It is never invoked + * directly - but can be triggered by {@link #handle()} or {@link + * #runTransmitter()} + */ + private void handleLoop() { + transmitLock.lock(); + try { + handleLoop0(); + } catch (Throwable t) { + if (Log.errors()) { + Log.logError("{0}: {1} handleLoop failed: {2}", + packetEmitter.logTag(), packetNumberSpace, t); + Log.logError(t); + } else if (debug.on()) { + debug.log("handleLoop failed", t); + } + } finally { + transmitLock.unlock(); + } + } + + private void handleLoop0() throws IOException, QuicTransportException { + // while congestion control allows, or if PTO expired: + // - send lost packet or new packet + // if PTO still expired (== nothing was sent) + // - resend oldest packet, if available + // - otherwise send ping (+ack, if available) + // if ACK still not sent, send ack + if (debug.on()) { + debug.log("PacketTransmissionTask::handle"); + } + packetEmitter.checkAbort(PacketSpaceManager.this.packetNumberSpace); + // Handle is called from within the executor + var nextDeadline = this.nextDeadline; + Deadline now = now(); + do { + transmitNow = false; + var closed = !isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("PacketTransmissionTask::handle: %s closed", + PacketSpaceManager.this.packetNumberSpace); + } + return; + } + if (debug.on()) debug.log("PacketTransmissionTask::handle"); + // this may update congestion controller + int lost = detectAndRemoveLostPackets(now); + if (lost > 0 && debug.on()) debug.log("handle: found %s lost packets", lost); + // if we're sending on PTO, we need to double backoff afterwards + boolean needBackoff = isPTO(now); + int packetsSent = 0; + boolean cwndAvailable; + while ((cwndAvailable = congestionController.canSendPacket()) || + (needBackoff && packetsSent < 2)) { // if PTO, try to send 2 packets + if (!isOpenForTransmission()) { + break; + } + final boolean retransmitted; + try { + retransmitted = retransmit(); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not re-transmitting any more data"); + } + clearAll(); + return; + } + throw new IOException("failed to retransmit data, reason: " + + qkue.getMessage()); + } + if (retransmitted) { + packetsSent++; + continue; + } + final boolean sentNew; + // nothing was retransmitted - check for new data + try { + sentNew = sendNewData(); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not transmitting any more data"); + } + return; + } + throw new IOException("failed to send new data, reason: " + + qkue.getMessage()); + } + if (!sentNew) { + break; + } else { + if (needBackoff && packetsSent == 0 && Log.quicRetransmit()) { + Log.logQuic("%s OUT: transmitted new packet on PTO".formatted( + packetEmitter.logTag())); + } + } + packetsSent++; + } + blockedByCC = !cwndAvailable; + if (!cwndAvailable && isOpenForTransmission()) { + if (debug.on()) debug.log("handle: blocked by CC"); + // CC might be available already + if (congestionController.canSendPacket()) { + if (debug.on()) debug.log("handle: unblocked immediately"); + transmitNow = true; + } + } + try { + if (isPTO(now) && isOpenForTransmission()) { + if (debug.on()) debug.log("handle: retransmit on PTO"); + // nothing was sent by the above loop - try to resend the oldest packet + retransmitPTO(); + } else if (fastRetransmit) { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + fastRetransmitDone = true; + fastRetransmit = false; + if (debug.on()) debug.log("handle: fast retransmit"); + // try to resend the oldest packet + retransmitPTO(); + } + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not re-transmitting any more data"); + } + return; + } + throw new IOException("failed to retransmitPTO data, key space, reason: " + + qkue.getMessage()); + } + boolean stillPTO = isPTO(now); + // if the ack frame is not sent yet, send it now + var ackFrame = getNextAckFrame(!stillPTO); + var pingRequested = PacketSpaceManager.this.pingRequested; + boolean sendPing = pingRequested != null || stillPTO + || shouldSendPing(now, ackFrame); + if (sendPing || ackFrame != null) { + if (debug.on()) debug.log("handle: generate ACK packet or PING ack:%s ping:%s", + ackFrame != null, sendPing); + final long emitted; + try { + emitted = emitAckPacket(ackFrame, sendPing); + } catch (QuicKeyUnavailableException qkue) { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; not sending ack/ping packet"); + } + return; + } + throw new IOException("failed to send ack/ping data, reason: " + + qkue.getMessage()); + } + if (sendPing && pingRequested != null) { + if (emitted < 0) pingRequested.complete(-1L); + else registerPingRequest(new PingRequest(now, emitted, pingRequested)); + synchronized (PacketSpaceManager.this) { + PacketSpaceManager.this.pingRequested = null; + } + } + } + if (needBackoff) { + long backoff = rttEstimator.increasePtoBackoff(); + if (debug.on()) { + debug.log("handle: %s increase backoff to %s", + PacketSpaceManager.this.packetNumberSpace, + backoff); + } + packetEmitter.ptoBackoffIncreased(PacketSpaceManager.this, backoff); + } + + // if nextDeadline is not Deadline.MAX the task will be + // automatically rescheduled. + if (debug.on()) debug.log("handle: refreshing deadline"); + nextDeadline = computeNextDeadline(); + } while(!nextDeadline.isAfter(now)); + + logNoDeadline(nextDeadline, true); + if (Deadline.MAX.equals(nextDeadline)) return; + // we have a new deadline + packetEmitter.reschedule(this, nextDeadline); + } + + /** + * Create and send a new packet + * @return true if packet was sent, false if there is no more data to send + */ + private boolean sendNewData() throws QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) debug.log("handle: sending data..."); + boolean sent = packetEmitter.sendData(packetNumberSpace); + if (!sent) { + if (debug.on()) debug.log("handle: no more data to send"); + } + return sent; + } + + @Override + public Deadline refreshDeadline() { + Deadline previousDeadline, newDeadline; + do { + previousDeadline = this.nextDeadline; + newDeadline = computeNextDeadline(); + } while (!Handles.DEADLINE.compareAndSet(this, previousDeadline, newDeadline)); + + if (!newDeadline.equals(previousDeadline)) { + if (debug.on()) { + var now = now(); + if (newDeadline.equals(Deadline.MAX)) { + debug.log("Deadline refreshed: no new deadline"); + } else if (newDeadline.equals(Deadline.MIN)) { + debug.log("Deadline refreshed: run immediately"); + } else if (previousDeadline.equals(Deadline.MAX) || previousDeadline.equals(Deadline.MIN)) { + var delay = now.until(newDeadline, ChronoUnit.MILLIS); + if (delay < 0) { + debug.log("Deadline refreshed: new deadline passed by %dms", delay); + } else { + debug.log("Deadline refreshed: new deadline in %dms", delay); + } + } else { + var delay = now.until(newDeadline, ChronoUnit.MILLIS); + if (delay < 0) { + debug.log("Deadline refreshed: new deadline passed by %dms (diff: %dms)", + delay, previousDeadline.until(newDeadline, ChronoUnit.MILLIS)); + } else { + debug.log("Deadline refreshed: new deadline in %dms (diff: %dms)", + instantSource.instant().until(newDeadline, ChronoUnit.MILLIS), + previousDeadline.until(newDeadline, ChronoUnit.MILLIS)); + } + } + } + } else { + debug.log("Deadline not refreshed: no change"); + } + logNoDeadline(newDeadline, false); + return newDeadline; + } + + void logNoDeadline(Deadline newDeadline, boolean onlyNoDeadline) { + if (Log.quicRetransmit()) { + if (Deadline.MAX.equals(newDeadline)) { + if (shouldLogWhenNoDeadline()) { + Log.logQuic("{0}: {1} no deadline, task unscheduled", + packetEmitter.logTag(), packetNumberSpace); + } // else: no changes... + } else if (!onlyNoDeadline && shouldLogWhenNewDeadline()) { + if (Deadline.MIN.equals(newDeadline)) { + Log.logQuic("{0}: {1} Deadline.MIN, task will be rescheduled immediately", + packetEmitter.logTag(), packetNumberSpace); + } else { + try { + Log.logQuic("{0}: {1} new deadline computed, deadline in {2}ms", + packetEmitter.logTag(), packetNumberSpace, + Long.toString(now().until(newDeadline, ChronoUnit.MILLIS))); + } catch (ArithmeticException ae) { + Log.logError("Unexpected exception while logging deadline " + + newDeadline + ": " + ae); + Log.logError(ae); + assert false : "Unexpected ArithmeticException: " + ae; + } + } + } + } + } + + private boolean hadNoDeadline; + private synchronized boolean shouldLogWhenNoDeadline() { + if (!hadNoDeadline) { + hadNoDeadline = true; + return true; + } + return false; + } + + private synchronized boolean shouldLogWhenNewDeadline() { + if (hadNoDeadline) { + hadNoDeadline = false; + return true; + } + return false; + } + + boolean hasNoDeadline() { + return Deadline.MAX.equals(nextDeadline); + } + + // reschedule this task + void reschedule() { + Deadline deadline = computeNextDeadline(); + Deadline nextDeadline = this.nextDeadline; + if (Deadline.MAX.equals(deadline)) { + debug.log("no deadline, don't reschedule"); + } else if (deadline.equals(nextDeadline)) { + debug.log("deadline unchanged, don't reschedule"); + } else { + packetEmitter.reschedule(this, deadline); + debug.log("retransmission task: rescheduled"); + } + } + + @Override + public String toString() { + return "PacketTransmissionTask(" + debugStrSupplier.get() + ")"; + } + } + + Deadline deadline() { + return packetTransmissionTask.deadline(); + } + + Deadline prospectiveDeadline() { + return computeNextDeadline(false); + } + + // remove all pending acknowledgements and retransmissions. + private void clearAll() { + transferLock.lock(); + try { + pendingAcknowledgements.forEach(ack -> congestionController.packetDiscarded(List.of(ack.packet))); + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + pendingAcknowledgements.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting pending acks: " + sb); + } + } + pendingAcknowledgements.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + pendingRetransmission.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting pending retransmissions: " + sb); + } + } + pendingRetransmission.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + triggeredForRetransmission.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting triggered-for-retransmissions: " + sb.toString()); + } + } + triggeredForRetransmission.clear(); + + if (debug.on()) { + final StringBuilder sb = new StringBuilder(); + lostPackets.forEach((p) -> sb.append(" ").append(p)); + if (!sb.isEmpty()) { + debug.log("forgetting lost-packets: " + sb.toString()); + } + } + lostPackets.clear(); + } finally { + transferLock.unlock(); + } + } + + private void retransmitPTO() throws QuicKeyUnavailableException, QuicTransportException { + if (!isOpenForTransmission()) { + if (debug.on()) { + debug.log("already closed; retransmission on PTO dropped", packetNumberSpace); + } + clearAll(); + return; + } + + PendingAcknowledgement pending; + transferLock.lock(); + try { + if ((pending = pendingAcknowledgements.poll()) != null) { + if (debug.on()) debug.log("Retransmit on PTO: looking for candidate"); + // TODO should keep this packet on the list until it's either acked or lost + congestionController.packetDiscarded(List.of(pending.packet)); + pendingRetransmission.add(pending); + } + } finally { + transferLock.unlock(); + } + if (pending != null) { + packetEmitter.retransmit(this, pending.packet(), pending.attempts()); + } + } + + /** + * {@return true if this packet space isn't closed and if the underlying packet emitter + * is open, else returns false} + */ + private boolean isOpenForTransmission() { + return !this.closed && this.packetEmitter.isOpen(); + } + + /** + * A class to keep track of the largest packet that was acknowledged by + * a packet that is being acknowledged. + * This information is used to implement the algorithm described in + * RFC 9000 13.2.4. Limiting Ranges by Tracking ACK Frames + */ + private final class EmittedAckTracker { + volatile long ignoreAllPacketsBefore = -1; + /** + * Record the {@link AckFrame#largestAcknowledged() + * largest acknowledged} packet that was sent in an + * {@link AckFrame} that the peer has acknowledged. + * @param largestAcknowledged the packet number to record + * @return the largest {@code largestAcknowledged} + * packet number that was recorded. + * This is necessarily smaller than (or equal to) the + * {@link #getLargestSentAckedPN()}. + */ + private long record(long largestAcknowledged) { + long witness; + long largestSentAckedPN = PacketSpaceManager.this.largestSentAckedPN; + do { + witness = largestAckedPNReceivedByPeer; + if (witness >= largestAcknowledged) { + largestSentAckedPN = PacketSpaceManager.this.largestSentAckedPN; + assert witness <= largestSentAckedPN || ignoreAllPacketsBefore > largestSentAckedPN + : "largestAckedPNReceivedByPeer: %s, ignoreAllPacketsBefore: %s, largestSentAckedPN: %s" + .formatted(witness, ignoreAllPacketsBefore, largestSentAckedPN); + return witness; + } + } while (!Handles.LARGEST_ACK_ACKED_PN.compareAndSet( + PacketSpaceManager.this, witness, largestAcknowledged)); + assert witness <= largestAcknowledged; + assert largestAcknowledged <= largestSentAckedPN || ignoreAllPacketsBefore > largestSentAckedPN + : "largestAcknowledged: %s, ignoreAllPacketsBefore: %s, largestSentAckedPN: %s" + .formatted(largestSentAckedPN, ignoreAllPacketsBefore, largestSentAckedPN); + return largestAcknowledged; + } + + private boolean ignoreAllPacketsBefore(long packetNumber) { + long ignoreAllPacketsBefore; + do { + ignoreAllPacketsBefore = this.ignoreAllPacketsBefore; + if (packetNumber <= ignoreAllPacketsBefore) return false; + } while (!Handles.IGNORE_ALL_PN_BEFORE.compareAndSet( + this, ignoreAllPacketsBefore, packetNumber)); + return true; + } + + /** + * Tracks the largest packet acknowledged by the packets acknowledged in the + * given AckFrame. This helps to implement the algorithm described in + * RFC 9000, 13.2.4. Limiting Ranges by Tracking ACK Frames. + * @param pending a yet unacknowledged packet that may be acknowledged + * by the given{@link AckFrame}. + * @param frame a received {@code AckFrame} + * @return whether the given pending unacknowledged packet is being + * acknowledged by this ack frame. + */ + public boolean trackAcknowlegment(PendingAcknowledgement pending, AckFrame frame) { + if (frame.isAcknowledging(pending.packetNumber)) { + record(pending.largestAcknowledged); + packetEmitter.acknowledged(pending.packet()); + return true; + } + // There is a potential for a never ending retransmission + // loop here if we don't treat the ack of a previous packet just + // as the ack of the tip of the chain. + // So we call packetEmitter.acknowledged(pending.packet()) here too, + // and return `true` in this case as well. + var previous = pending.findPreviousAcknowledged(frame); + if (previous != null) { + record(previous.largestAcknowledged); + packetEmitter.acknowledged(pending.packet()); + return true; + } + return false; + } + + public long largestAckAcked() { + return largestAckedPNReceivedByPeer; + } + + public void dropPacketNumbersSmallerThan(long newLargestIgnored) { + // this method is called after arbitrarily reducing the ack range + // to this value; This mean we will drop packets whose packet + // number is smaller than the given packet number. + if (ignoreAllPacketsBefore(newLargestIgnored)) { + record(newLargestIgnored); + } + } + } + + private final QuicTLSEngine quicTLSEngine; + private final EmittedAckTracker emittedAckTracker; + private volatile NextAckFrame nextAckFrame; // assigned through VarHandle + // exponent for outgoing packets; defaults to 3 + public static final int ACK_DELAY_EXPONENT = 3; + // max ack delay sent in quic transport parameters, in millis + public static final int ADVERTISED_MAX_ACK_DELAY = 25; + // max timer delay, i.e. how late selector.select returns; 15.6 millis on Windows + public static final int TIMER_DELAY = 16; + // effective max ack delay for outgoing application packets + public static final int MAX_ACK_DELAY = ADVERTISED_MAX_ACK_DELAY - TIMER_DELAY; + + // exponent for incoming packets + private volatile long peerAckDelayExponent; + // max peer ack delay; zero on initial and handshake, + // initialized from transport parameters on application + private volatile long peerMaxAckDelayMillis; // ms + // max ack delay; zero on initial and handshake, MAX_ACK_DELAY on application + private final long maxAckDelay; // ms + volatile boolean closed; + + // The last time an ACK eliciting packet was sent. + // May be null before any such packet is sent... + private volatile Deadline lastAckElicitingTime; + + // not null if sending ping has been requested. + private volatile CompletableFuture pingRequested; + + // The largest packet number successfully processed in this space. + // Needed to decode received packet numbers, see RFC 9000 appendix A.3 + private volatile long largestProcessedPN; // assigned through VarHandle + + // The largest ACK-eliciting packet number received in this space. + // Needed to determine if we should send ACK without delay, see RFC 9000 section 13.2.1 + private volatile long largestAckElicitingReceivedPN; // assigned through VarHandle + + // The largest ACK-eliciting packet number sent in this space. + // Needed to determine if we should arm PTO timer + private volatile long largestAckElicitingSentPN; + + // The largest packet number acknowledged by peer. + // Needed to determine packet number length, see RFC 9000 appendix A.2 + private volatile long largestReceivedAckedPN; // assigned through VarHandle + + // The largest packet number acknowledged in this space + // This is the largest packet number we have acknowledged. + // This should be less or equal to the largestProcessedPN always. + // Not used. + private volatile long largestSentAckedPN; // assigned through VarHandle + + // The largest packet number that this instance has included + // in an AckFrame sent to the peer, and of which the peer has + // acknowledged reception. + // Used to limit ack ranges, see RFC 9000 section 13.2.4 + private volatile long largestAckedPNReceivedByPeer; // assigned through VarHandle + + /** + * Creates a new {@code PacketSpaceManager} for the given + * packet number space. + * @param connection The connection for which this manager + * is created. + * @param packetNumberSpace The packet number space. + */ + public PacketSpaceManager(final QuicConnectionImpl connection, + final PacketNumberSpace packetNumberSpace) { + this(packetNumberSpace, connection.emitter(), TimeSource.source(), + connection.rttEstimator, connection.congestionController, connection.getTLSEngine(), + () -> connection.dbgTag() + "[" + packetNumberSpace.name() + "]"); + } + + /** + * Creates a new {@code PacketSpaceManager} for the given + * packet number space. + * + * @param packetNumberSpace the packet number space. + * @param packetEmitter the packet emitter + * @param congestionController the congestion controller + * @param debugStrSupplier a supplier for a debug tag to use for logging purposes + */ + public PacketSpaceManager(PacketNumberSpace packetNumberSpace, + PacketEmitter packetEmitter, + TimeLine instantSource, + QuicRttEstimator rttEstimator, + QuicCongestionController congestionController, + QuicTLSEngine quicTLSEngine, + Supplier debugStrSupplier) { + largestProcessedPN = largestReceivedAckedPN = largestAckElicitingReceivedPN + = largestAckElicitingSentPN = largestSentAckedPN = largestAckedPNReceivedByPeer = -1L; + this.debugStrSupplier = debugStrSupplier; + this.debug = Utils.getDebugLogger(debugStrSupplier); + this.instantSource = instantSource; + this.rttEstimator = rttEstimator; + this.congestionController = congestionController; + this.packetNumberSpace = packetNumberSpace; + this.packetEmitter = packetEmitter; + this.emittedAckTracker = new EmittedAckTracker(); + this.packetTransmissionTask = new PacketTransmissionTask(); + this.quicTLSEngine = quicTLSEngine; + maxAckDelay = (packetNumberSpace == PacketNumberSpace.APPLICATION) + ? MAX_ACK_DELAY : 0; + } + + /** + * {@return the max delay before emitting a non ACK-eliciting packet to + * acknowledge a received ACK-eliciting packet, in milliseconds} + */ + public long getMaxAckDelay() { + return maxAckDelay; + } + + /** + * {@return the max ACK delay of the peer, in milliseconds} + */ + public long getPeerMaxAckDelayMillis() { + return peerMaxAckDelayMillis; + } + + /** + * Changes the value of the {@linkplain #getPeerMaxAckDelayMillis() + * peer max ACK delay} and ack delay exponent + * + * @param peerDelay the new delay, in milliseconds + * @param ackDelayExponent the new ack delay exponent + */ + @Override + public void updatePeerTransportParameters(long peerDelay, long ackDelayExponent) { + this.peerAckDelayExponent = ackDelayExponent; + this.peerMaxAckDelayMillis = peerDelay; + } + + @Override + public PacketNumberSpace packetNumberSpace() { + return packetNumberSpace; + } + + @Override + public long allocateNextPN() { + return nextPN.getAndIncrement(); + } + + @Override + public long getLargestPeerAckedPN() { + return largestReceivedAckedPN; + } + + @Override + public long getLargestProcessedPN() { + return largestProcessedPN; + } + + @Override + public long getMinPNThreshold() { + return largestAckedPNReceivedByPeer; + } + + @Override + public long getLargestSentAckedPN() { + return largestSentAckedPN; + } + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful negotiation of a new version. + * In that case we should stop retransmitting packet that have the + * "wrong" version: they will never be acknowledged. + */ + public void versionChanged() { + // don't retransmit packet with "bad" version + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (debug.on()) { + debug.log("version changed - clearing pending acks"); + } + clearAll(); + } + + public void retry() { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (debug.on()) { + debug.log("retry received - clearing pending acks"); + } + clearAll(); + } + + @Override + public ReentrantLock getTransmitLock() { + return transmitLock; + } + + // adds the PingRequest to the pendingPingRequests queue so + // that it can be completed when the packet is ACK'ed. + private void registerPingRequest(PingRequest pingRequest) { + if (closed) { + pingRequest.response().completeExceptionally(new IOException("closed")); + return; + } + pendingPingRequests.add(pingRequest); + // could be acknowledged already! + processPingResponses(largestReceivedAckedPN); + } + + @Override + public void close() { + if (closed) { + return; + } + synchronized (this) { + if (closed) return; + closed = true; + } + if (Log.quicControl() || Log.quicRetransmit()) { + Log.logQuic("{0} closing packet space {1}", + packetEmitter.logTag(), packetNumberSpace); + } + if (debug.on()) { + debug.log("closing packet space"); + } + // stop the internal scheduler + packetTransmissionTask.handleScheduler.stop(); + // make sure the task gets eventually removed from the timer + packetEmitter.reschedule(packetTransmissionTask); + // clear pending acks, retransmissions + transferLock.lock(); + try { + clearAll(); + // discard the (TLS) keys + if (debug.on()) { + debug.log("discarding TLS keys"); + } + this.quicTLSEngine.discardKeys(tlsEncryptionLevel()); + } finally { + transferLock.unlock(); + } + rttEstimator.resetPtoBackoff(); + // complete any ping request that hasn't been completed + IOException io = null; + try { + for (var pr : pendingPingRequests) { + if (io == null) { + io = new IOException("Not sending ping because " + + this.packetNumberSpace + " packet space is being closed"); + } + // TODO: is it necessary for this to be an exceptional completion? + pr.response().completeExceptionally(io); + } + } finally { + pendingPingRequests.clear(); + } + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void runTransmitter() { + transmitNow = true; + // run the handle loop + packetTransmissionTask.handle(); + } + + @Override + public void packetReceived(PacketType packet, long packetNumber, boolean isAckEliciting) { + assert PacketNumberSpace.of(packet) == packetNumberSpace; + assert packetNumber > largestAckedPNReceivedByPeer; + + if (closed) { + if (debug.on()) { + debug.log("%s closed, ignoring %s(pn: %s)", packetNumberSpace, packet, packetNumber); + } + return; + } + + if (debug.on()) { + debug.log("packetReceived %s(pn:%d, needsAck:%s)", + packet, packetNumber, isAckEliciting); + } + + // whether the packet is ack eliciting or not, we need to add its packet + // number to the ack frame. + packetProcessed(packetNumber); + addToAckFrame(packetNumber, isAckEliciting); + } + + // used in tests + public T triggeredForRetransmission(Function walker) { + return walker.apply(triggeredForRetransmission.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + public T pendingRetransmission(Function walker) { + return walker.apply(pendingRetransmission.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + // used in tests + public T pendingAcknowledgements(Function walker) { + return walker.apply(pendingAcknowledgements.stream() + .mapToLong(PendingAcknowledgement::packetNumber)); + } + + // used in tests + public AtomicLong getNextPN() { return nextPN; } + + // Called by the retransmitLoop scheduler. + // Retransmit one packet for which retransmission has been triggered by + // the PacketTransmissionTask. + // return true if something was retransmitted, or false if there was nothing to retransmit + private boolean retransmit() throws QuicKeyUnavailableException, QuicTransportException { + PendingAcknowledgement pending; + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("already closed; retransmission dropped"); + } + clearAll(); + return false; + } + transferLock.lock(); + try { + pending = triggeredForRetransmission.poll(); + } finally { + transferLock.unlock(); + } + + if (pending != null) { + // allocate new packet number + // create new packet + // encrypt packet + // send packet + if (debug.on()) debug.log("handle: retransmitting..."); + packetEmitter.retransmit(this, pending.packet(), pending.attempts()); + return true; + } + return false; + } + + /** + * Called by the {@link PacketTransmissionTask} to + * generate a non ACK eliciting packet containing only the given + * ACK frame. + * + *

        If a received packet is ACK-eliciting, then it will be either + * directly acknowledged by {@link QuicConnectionImpl} - which will + * call {@link #getNextAckFrame(boolean)} to embed the {@link AckFrame} + * in a packet, or by a non-eliciting ACK packet which will be + * triggered {@link #getMaxAckDelay() maxAckDelay} after the reception + * of the ACK-eliciting packet (this method, triggered by the {@link + * PacketTransmissionTask}). + * + *

        This method doesn't reset the {@linkplain #getNextAckFrame(boolean) + * next ack frame} to be sent, but reset its delay so that only + * one non ACK-eliciting packet is emitted to acknowledge a given + * packet. + * + * @param ackFrame The ACK frame to send. + * @return the packet number of the emitted packet + */ + private long emitAckPacket(AckFrame ackFrame, boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + final boolean closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("Packet space closed, ack/ping won't be sent" + + (ackFrame != null ? ": " + ackFrame : "")); + } + return -1L; + } + try { + return packetEmitter.emitAckPacket(this, ackFrame, sendPing); + } catch (QuicKeyUnavailableException | QuicTransportException e) { + if (!this.isOpenForTransmission()) { + // possible race condition where the packet space was closed (and keys discarded) + // while there was an attempt to send an ACK/PING frame. + // Ignore such cases, since it's OK to not send those frames when the packet space + // is already closed + if (debug.on()) { + debug.log("ack/ping wasn't sent since packet space was closed" + + (ackFrame != null ? ": " + ackFrame : "")); + } + return -1L; + } + throw e; + } + } + + boolean isClosing(QuicPacket packet) { + var frames = packet.frames(); + if (frames == null || frames.isEmpty()) return false; + return frames.stream() + .anyMatch(ConnectionCloseFrame.class::isInstance); + } + + private synchronized void lastAckElicitingSent(long packetNumber) { + if (largestAckElicitingSentPN < packetNumber) { + largestAckElicitingSentPN = packetNumber; + } + } + + @Override + public void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber) { + if (packetNumber < 0) { + throw new IllegalArgumentException("Invalid packet number: " + packetNumber); + } + largestAckSent(AckFrame.largestAcknowledgedInPacket(packet)); + if (previousPacketNumber >= 0) { + if (debug.on()) { + debug.log("retransmitted packet %s(%d) as %d", + packet.packetType(), previousPacketNumber, packetNumber); + } + + boolean found = false; + transferLock.lock(); + try { + // check for close and addAcknowledgement in the same lock + // to avoid races with close / clearAll + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("%s already closed: ignoring packet pn:%s", + packetNumberSpace, packet.packetNumber()); + } + return; + } + // TODO: should use a tail set here to skip all pending acks + // whose packet number is < previousPacketNumber? + var iterator = pendingRetransmission.iterator(); + PendingAcknowledgement replacement; + while (iterator.hasNext()) { + PendingAcknowledgement pending = iterator.next(); + if (pending.hasPreviousNumber(previousPacketNumber)) { + // no need to retransmit twice, but can this happen? + iterator.remove(); + } else if (!found && pending.hasExactNumber(previousPacketNumber)) { + PreviousNumbers previous = new PreviousNumbers( + previousPacketNumber, + pending.largestAcknowledged, pending.previousNumbers); + replacement = + new PendingAcknowledgement(packet, now(), packetNumber, previous); + if (debug.on()) { + debug.log("Packet %s(pn:%s) previous %s(pn:%s) is pending acknowledgement", + packet.packetType(), packetNumber, packet.packetType(), previousPacketNumber); + } + var rep = replacement; + if (lostPackets.removeIf(p -> rep.hasPreviousNumber(p.packetNumber))) { + lostPackets.add(rep); + } + addAcknowledgement(replacement); + iterator.remove(); + found = true; + } + } + } finally { + transferLock.unlock(); + } + if (found && packetTransmissionTask.hasNoDeadline()) { + packetTransmissionTask.reschedule(); + } + if (!found) { + if (debug.on()) { + debug.log("packetRetransmitted: packet not found - previous: %s for %s(%s)", + previousPacketNumber, packet.packetType(), packetNumber); + } + } + } else { + if (packet.isAckEliciting()) { + // This method works with the following assumption: + // - Non ACK eliciting packet do not need to be retransmitted because: + // - they only contain ack frames - which may/will we be retransmitted + // anyway with the next ack eliciting packet + // - they will not be acknowledged directly - we don't want to + // resend them constantly + if (debug.on()) { + debug.log("Packet %s(pn:%s) is pending acknowledgement", + packet.packetType(), packetNumber); + } + PendingAcknowledgement pending = new PendingAcknowledgement(packet, + now(), packetNumber, null); + transferLock.lock(); + try { + // check for close and addAcknowledgement in the same lock + // to avoid races with close / clearAll + final var closed = !this.isOpenForTransmission(); + if (closed) { + if (debug.on()) { + debug.log("%s already closed: ignoring packet pn:%s", + packetNumberSpace, packet.packetNumber()); + } + return; + } + addAcknowledgement(pending); + if (packetTransmissionTask.hasNoDeadline()) { + packetTransmissionTask.reschedule(); + } + } finally { + transferLock.unlock(); + } + } + } + + + } + + private void addAcknowledgement(PendingAcknowledgement ack) { + lastAckElicitingSent(ack.sent); + lastAckElicitingSent(ack.packetNumber); + pendingAcknowledgements.add(ack); + sendTimes.put(ack.packetNumber, ack.sent); + congestionController.packetSent(ack.packet().size()); + } + + /** + * Computes the next deadline for generating a non ACK eliciting + * packet containing the next ACK frame, or for retransmitting + * unacknowledged packets for which retransmission is due. + * This may be different to the {@link #nextScheduledDeadline()} + * if newer changes have not been taken into account yet. + * @return the deadline at which the scheduler's task for this packet + * space should be scheduled to wake up + */ + public Deadline computeNextDeadline() { + return computeNextDeadline(true); + } + + public Deadline computeNextDeadline(boolean verbose) { + + if (closed) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] closed - no deadline", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MAX; + } + if (transmitNow) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] transmit now", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MIN; + } + if (pingRequested != null) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] ping requested", + packetEmitter.logTag(), packetNumberSpace)); + } + return Deadline.MIN; + } + var ack = nextAckFrame; + + Deadline ackDeadline = (ack == null || ack.sent() != null) + ? Deadline.MAX // if the ack frame has already been sent, getNextAck() returns null + : ack.deadline(); + Deadline lossDeadline = getLossTimer(); + // TODO: consider removing the debug traces in this method when integrating + // if both loss deadline and PTO timer are set, loss deadline is always earlier + if (verbose && debug.on() && lossDeadline != Deadline.MIN) debug.log("lossDeadline is: " + lossDeadline); + if (lossDeadline != null) { + if (verbose && debug.on()) { + if (lossDeadline == Deadline.MIN) { + debug.log("lossDeadline is immediate"); + } else if (!ackDeadline.isBefore(lossDeadline)) { + debug.log("lossDeadline in %s ms", + Deadline.between(now(), lossDeadline).toMillis()); + } else { + debug.log("ackDeadline before lossDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] loss deadline: %s, ackDeadline: %s, deadline in %s", + packetEmitter.logTag(), packetNumberSpace, lossDeadline, ackDeadline, + Utils.debugDeadline(now(), min(ackDeadline, lossDeadline)))); + } + return min(ackDeadline, lossDeadline); + } + Deadline ptoDeadline = getPtoDeadline(); + if (verbose && debug.on()) debug.log("ptoDeadline is: " + ptoDeadline); + if (ptoDeadline != null) { + if (verbose && debug.on()) { + if (!ackDeadline.isBefore(ptoDeadline)) { + debug.log("ptoDeadline in %s ms", + Deadline.between(now(), ptoDeadline).toMillis()); + } else { + debug.log("ackDeadline before ptoDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] PTO deadline: %s, ackDeadline: %s, deadline in %s", + packetEmitter.logTag(), packetNumberSpace, ptoDeadline, ackDeadline, + Utils.debugDeadline(now(), min(ackDeadline, ptoDeadline)))); + } + return min(ackDeadline, ptoDeadline); + } + if (verbose && debug.on()) { + if (ackDeadline == Deadline.MAX) { + debug.log("ackDeadline is: Deadline.MAX"); + } else { + debug.log("ackDeadline in %s ms", + Deadline.between(now(), ackDeadline).toMillis()); + } + } + if (ackDeadline.equals(Deadline.MAX)) { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] no deadline: " + + "pendingAcks: %s, triggered: %s, pendingRetransmit: %s", + packetEmitter.logTag(), packetNumberSpace, pendingAcknowledgements.size(), + triggeredForRetransmission.size(), pendingRetransmission.size())); + } + } else { + if (verbose && Log.quicTimer()) { + Log.logQuic(String.format("%s: [%s] deadline is %s", + packetEmitter.logTag(), packetNumberSpace(), + Utils.debugDeadline(now(), ackDeadline))); + } + } + return ackDeadline; + } + + /** + * {@return the next deadline at which the scheduler's task for this packet + * space is currently scheduled to wake up} + */ + public Deadline nextScheduledDeadline() { + return packetTransmissionTask.nextDeadline; + } + + private Deadline now() { + return instantSource.instant(); + } + + /** + * Tracks the largest packet acknowledged by the packets acknowledged in the + * given AckFrame. This helps to implement the algorithm described in + * RFC 9000, 13.2.4. Limiting Ranges by Tracking ACK Frames. + * @param pending a yet unacknowledged packet that may be acknowledged + * by the given{@link AckFrame}. + * @param frame a received {@code AckFrame} + * @return whether the given pending unacknowledged packet is being + * acknowledged by this ack frame. + */ + private boolean trackAcknowlegment(PendingAcknowledgement pending, AckFrame frame) { + return emittedAckTracker.trackAcknowlegment(pending, frame); + } + + private boolean isAcknowledgingLostPacket(PendingAcknowledgement pending, AckFrame frame, + List[] recovered) { + if (frame.isAcknowledging(pending.packetNumber)) { + if (recovered != null) { + if (recovered[0] == null) { + recovered[0] = new ArrayList<>(); + } + recovered[0].add(pending); + } + return true; + } + // There is a potential for a never ending retransmission + // loop here if we don't treat the ack of a previous packet just + // as the ack of the tip of the chain. + // So we call packetEmitter.acknowledged(pending.packet()) here too, + // and return `true` in this case as well. + var previous = pending.findPreviousAcknowledged(frame); + if (previous != null) { + if (recovered != null) { + if (recovered[0] == null) { + recovered[0] = new ArrayList<>(); + } + recovered[0].add(pending); + } + return true; + } + return false; + + } + + @Override + public void processAckFrame(AckFrame frame) throws QuicTransportException { + // for each acknowledged packet, remove it from the + // list of packets pending acknowledgement, or from the + // list of packets pending retransmission + long largestAckAckedBefore = emittedAckTracker.largestAckAcked(); + long largestAcknowledged = frame.largestAcknowledged(); + Deadline now = now(); + if (largestAcknowledged >= nextPN.get()) { + throw new QuicTransportException("Acknowledgement for a nonexistent packet", + null, frame.getTypeField(), QuicTransportErrors.PROTOCOL_VIOLATION); + } + + int lostCount; + transferLock.lock(); + try { + if (largestAckReceived(largestAcknowledged)) { + // if the largest acknowledged PN is newly acknowledged + // and at least one of the newly acked packets is ack-eliciting + // -> use the new RTT sample + // the below code only checks if largest acknowledged is ack-eliciting + Deadline sentTime = sendTimes.get(largestAcknowledged); + if (sentTime != null) { + long ackDelayMicros; + if (isApplicationSpace()) { + confirmHandshake(); + long baseAckDelay = peerAckDelayToMicros(frame.ackDelay()); + // if packet was sent after handshake confirmed, use max ack delay + if (largestAcknowledged >= handshakeConfirmedPN) { + ackDelayMicros = Math.min( + baseAckDelay, + TimeUnit.MILLISECONDS.toMicros(peerMaxAckDelayMillis)); + } else { + ackDelayMicros = baseAckDelay; + } + } else { + // acks are not delayed during handshake + ackDelayMicros = 0; + } + long rttSample = sentTime.until(now, ChronoUnit.MICROS); + if (debug.on()) { + debug.log("New RTT sample on packet %s: %s us (delay %s us)", + largestAcknowledged, rttSample, + ackDelayMicros); + } + rttEstimator.consumeRttSample( + rttSample, + ackDelayMicros, + now + ); + } else { + if (debug.on()) { + debug.log("RTT sample on packet %s ignored: not ack eliciting", + largestAcknowledged); + } + } + if (packetNumberSpace != PacketNumberSpace.INITIAL) { + rttEstimator.resetPtoBackoff(); + } + purgeSendTimes(largestAcknowledged); + // complete PingRequests if needed + processPingResponses(largestAcknowledged); + } else { + if (debug.on()) { + debug.log("RTT sample on packet %s ignored: not largest", + largestAcknowledged); + } + } + + pendingRetransmission.removeIf((p) -> trackAcknowlegment(p, frame)); + triggeredForRetransmission.removeIf((p) -> trackAcknowlegment(p, frame)); + for (Iterator iterator = pendingAcknowledgements.iterator(); iterator.hasNext(); ) { + PendingAcknowledgement p = iterator.next(); + if (trackAcknowlegment(p, frame)) { + iterator.remove(); + congestionController.packetAcked(p.packet.size(), p.sent); + } + } + lostCount = detectAndRemoveLostPackets(now); + @SuppressWarnings({"unchecked","rawtypes"}) + List[] recovered= Log.quicRetransmit() ? new List[1] : null; + lostPackets.removeIf((p) -> isAcknowledgingLostPacket(p, frame, recovered)); + if (recovered != null && recovered[0] != null) { + Log.logQuic("{0} lost packets recovered: {1}({2}) total unrecovered {3}, unacknowledged {4}", + packetEmitter.logTag(), packetType(), + recovered[0].stream().map(PendingAcknowledgement::packetNumber).toList(), + lostPackets.size(), pendingAcknowledgements.size() + pendingRetransmission.size()); + } + } finally { + transferLock.unlock(); + } + + long largestAckAcked = emittedAckTracker.largestAckAcked(); + if (largestAckAcked > largestAckAckedBefore) { + if (debug.on()) { + debug.log("%s: largestAckAcked=%d - cleaning up AckFrame", + packetNumberSpace, largestAckAcked); + } + // remove ack ranges that we no longer need to acknowledge. + // this implements the algorithm described in RFC 9000, + // 13.2.4. Limiting Ranges by Tracking ACK Frames + cleanupAcks(); + } + + if (lostCount > 0) { + if (debug.on()) + debug.log("Found %s lost packets", lostCount); + // retransmit if possible + runTransmitter(); + } else if (blockedByCC && congestionController.canSendPacket()) { + // CC just got unblocked... send more data + blockedByCC = false; + runTransmitter(); + } else { + // RTT was updated, some packets might be lost, recompute timers + packetTransmissionTask.reschedule(); + } + } + + @Override + public void confirmHandshake() { + assert isApplicationSpace(); + if (handshakeConfirmedPN == 0) { + handshakeConfirmedPN = nextPN.get(); + } + } + + private void purgeSendTimes(long largestAcknowledged) { + sendTimes.headMap(largestAcknowledged, true).clear(); + } + + private long peerAckDelayToMicros(long ackDelay) { + return ackDelay << peerAckDelayExponent; + } + + private NextAckFrame getNextAck(boolean onlyOverdue, int maxSize) { + Deadline now = now(); + // This method is called to retrieve the AckFrame that will + // be embedded in the next packet sent to the peer. + // We therefore need to disarm the timer that will send a + // non-ACK eliciting packet with that AckFrame (if any) before + // returning the AckFrame. This is the purpose of the loop + // below... + while (true) { + NextAckFrame ack = nextAckFrame; + if (ack == null + || ack.deadline() == Deadline.MAX + || (onlyOverdue && ack.deadline().isAfter(now)) + || ack.sent() != null) { + return null; + } + // also reserve 3 bytes for the ack delay + if (ack.ackFrame().size() > maxSize - 3) return null; + NextAckFrame newAck = ack.withDeadline(Deadline.MAX, now); + boolean respin = !Handles.NEXTACK.compareAndSet(this, ack, newAck); + if (!respin) { + return ack; + } + } + } + + @Override + public AckFrame getNextAckFrame(boolean onlyOverdue) { + return getNextAckFrame(onlyOverdue, Integer.MAX_VALUE); + } + + @Override + public AckFrame getNextAckFrame(boolean onlyOverdue, int maxSize) { + if (closed) { + return null; + } + NextAckFrame ack = getNextAck(onlyOverdue, maxSize); + if (ack == null) { + return null; + } + long delay = ack.lastUpdated() + .until(now(), ChronoUnit.MICROS) >> ACK_DELAY_EXPONENT; + return ack.ackFrame().withAckDelay(delay); + } + + /** + * Returns the count of unacknowledged packets that were declared lost. + * The lost packets are moved from the pendingAcknowledgements + * into the pendingRetransmission. + * + * @param now current time, used for time-based loss detection. + */ + private int detectAndRemoveLostPackets(Deadline now) { + Deadline lossSendTime = now.minus(rttEstimator.getLossThreshold()); + int count = 0; + // debug.log("preparing for retransmission"); + transferLock.lock(); + try { + List lost = Log.quicRetransmit() ? new ArrayList<>() : null; + List packets = new ArrayList<>(); + Deadline firstSendTime = null, lastSendTime = null; + for (PendingAcknowledgement head = pendingAcknowledgements.peek(); + head != null && head.packetNumber < largestReceivedAckedPN; + head = pendingAcknowledgements.peek()) { + if (head.packetNumber < largestReceivedAckedPN - kPacketThreshold || + !lossSendTime.isBefore(head.sent)) { + if (debug.on()) { + debug.log("retransmit:head pn:" + head.packetNumber + + ",largest acked PN:" + largestReceivedAckedPN + + ",sent:" + head.sent + + ",lossSendTime:" + lossSendTime + ); + } + if (pendingAcknowledgements.remove(head)) { + pendingRetransmission.add(head); + triggeredForRetransmission.add(head); + packets.add(head.packet); + if (firstSendTime == null) { + firstSendTime = head.sent; + } + lastSendTime = head.sent; + var lp = head; + lostPackets.removeIf(p -> lp.hasPreviousNumber(p.packetNumber)); + lostPackets.add(head); + count++; + if (lost != null) lost.add(head); + } + } else { + if (debug.on()) { + debug.log("no retransmit:head pn:" + head.packetNumber + + ",largest acked PN:" + largestReceivedAckedPN + + ",sent:" + head.sent + + ",lossSendTime:" + lossSendTime + ); + } + break; + } + } + if (!packets.isEmpty()) { + // Persistent congestion is detected more aggressively than mandated by RFC 9002: + // - may be reported even if there's no prior RTT sample + // - may be reported even if there are acknowledged packets between the lost ones + boolean persistent = Deadline.between(firstSendTime, lastSendTime) + .compareTo(getPersistentCongestionDuration()) > 0; + congestionController.packetLost(packets, lastSendTime, persistent); + } + if (lost != null && !lost.isEmpty()) { + Log.logQuic("{0} lost packet {1}({2}) total unrecovered {3}, unacknowledged {4}", + packetEmitter.logTag(), + packetType(), lost.stream().map(PendingAcknowledgement::packetNumber).toList(), + lostPackets.size(), pendingAcknowledgements.size() + pendingRetransmission.size()); + } + } finally { + transferLock.unlock(); + } + return count; + } + + PacketType packetType() { + return switch (packetNumberSpace) { + case INITIAL -> PacketType.INITIAL; + case HANDSHAKE -> PacketType.HANDSHAKE; + case APPLICATION -> PacketType.ONERTT; + case NONE -> PacketType.NONE; + }; + } + + /** + * {@return true if PTO timer expired, false otherwise} + */ + private boolean isPTO(Deadline now) { + Deadline ptoDeadline = getPtoDeadline(); + return ptoDeadline != null && !ptoDeadline.isAfter(now); + } + + // returns true if this space is the APPLICATION space + private boolean isApplicationSpace() { + return packetNumberSpace == PacketNumberSpace.APPLICATION; + } + + // returns the PTO duration + Duration getPtoDuration() { + var pto = rttEstimator.getBasePtoDuration() + .plusMillis(peerMaxAckDelayMillis) + .multipliedBy(rttEstimator.getPtoBackoff()); + var max = QuicRttEstimator.MAX_PTO_BACKOFF_TIMEOUT; + // don't allow PTO > 240s + return pto.compareTo(max) > 0 ? max : pto; + } + + // returns the persistent congestion duration + Duration getPersistentCongestionDuration() { + return rttEstimator.getBasePtoDuration() + .plusMillis(peerMaxAckDelayMillis) + .multipliedBy(kPersistentCongestionThreshold); + } + + private Deadline getPtoDeadline() { + if (packetNumberSpace == PacketNumberSpace.INITIAL && lastAckElicitingTime != null) { + if (!quicTLSEngine.keysAvailable(QuicTLSEngine.KeySpace.HANDSHAKE)) { + // if handshake keys are not available, initial PTO must be set + return lastAckElicitingTime.plus(getPtoDuration()); + } + } + if (packetNumberSpace == PacketNumberSpace.HANDSHAKE) { + // set anti-deadlock timer + if (lastAckElicitingTime == null) { + lastAckElicitingTime = now(); + } + if (largestAckElicitingSentPN == -1) { + return lastAckElicitingTime.plus(getPtoDuration()); + } + } + if (largestAckElicitingSentPN <= largestReceivedAckedPN) { + return null; + } + // Application space deadline can only be set when handshake is confirmed + if (isApplicationSpace() && quicTLSEngine.getHandshakeState() != QuicTLSEngine.HandshakeState.HANDSHAKE_CONFIRMED) { + return null; + } + return lastAckElicitingTime.plus(getPtoDuration()); + } + + private Deadline getLossTimer() { + PendingAcknowledgement head = pendingAcknowledgements.peek(); + if (head == null || head.packetNumber >= largestReceivedAckedPN) { + return null; + } + if (head.packetNumber < largestReceivedAckedPN - kPacketThreshold) { + return Deadline.MIN; + } + return head.sent.plus(rttEstimator.getLossThreshold()); + } + + // Compute the new deadline when adding an ack-eliciting packet number + // to an ack frame which is not empty. + private Deadline computeNewDeadlineFor(AckFrame frame, Deadline now, Deadline deadline, + long packetNumber, long previousLargest, + long ackDelay) { + + boolean previousEliciting = !deadline.equals(Deadline.MAX); + + if (closed) return Deadline.MAX; + + if (previousEliciting) { + // RFC 9000 #13.2.2: + // We should send an ACK immediately after receiving two + // ACK-eliciting packets + if (debug.on()) { + debug.log("two ACK-Eliciting packets received: " + + "next ack deadline now"); + } + return now; + } else if (packetNumber < previousLargest) { + // RFC 9000 #13.2.1: + // if the packet has PN less than another ack-eliciting packet, + // send ACK frame as soon as possible + if (debug.on()) { + debug.log("ACK-Eliciting packet received out of order: " + + "next ack deadline now"); + } + return now; + } else if (packetNumber - 1 > previousLargest && previousLargest > -1) { + // RFC 9000 #13.2.1: + // Check whether there are gaps between this packet and the + // previous ACK-eliciting packet that was received: + // if we find any gap we should send an ACK frame as soon + // as possible + if (!frame.isRangeAcknowledged(previousLargest + 1, packetNumber)) { + if (debug.on()) { + debug.log("gaps detected between this packet" + + " and the previous ACK eliciting packet: " + + "next ack deadline now"); + } + return now; + } + } + // send ACK within max delay + return now.plusMillis(ackDelay); + } + + /** + * Used to request sending of a ping frame, for instance, to verify that + * the connection is alive. + * @return a completable future that will be completed with the time it + * took, in milliseconds, for the peer to acknowledge the packet that + * contained the PingFrame (or any packet that was sent after) + */ + @Override + public CompletableFuture requestSendPing() { + CompletableFuture pingRequested; + synchronized (this) { + if ((pingRequested = this.pingRequested) == null) { + pingRequested = this.pingRequested = new MinimalFuture<>(); + } + } + runTransmitter(); + return pingRequested; + } + + // Look at whether a ping frame should be sent with the + // next ACK frame... + // If a PING frame should be sent, return the new deadline (now) + // Otherwise, return Deadline.MAX; + // A PING frame will be sent if: + // - the AckFrame contains more than (10) ACK Ranges + // - and no ACK eliciting packet was sent, or the last ACK-eliciting was + // sent long enough ago - typically 1 PTO delay + // These numbers are implementation dependent and not defined in the RFC, but + // help implement a strategy that sends occasional PING frames to limit the size + // of the ACK frames - as described in RFC 9000. + // + // See RFC 9000 Section 13.2.4 + private boolean shouldSendPing(Deadline now, AckFrame frame) { + Deadline last = lastAckElicitingTime; + if (frame != null && + (last == null || + last.isBefore(now.minus(rttEstimator.getBasePtoDuration()))) + && frame.ackRanges().size() > MAX_ACKRANGE_COUNT_BEFORE_PING) { + return true; + } + return false; + } + + // TODO: store the builder instead of storing the AckFrame? + // storing a builder would mean locking - so it might not be a good + // idea. But creating a new builder and AckFrame each time means + // producing more garbage for the GC to collect. + // This method is called when a new packet is received, and it adds the + // received packet number to the next ACK frame to send out. + // If the packet is ACK eliciting it also arms a timeout (if needed) + // to make sure the packet will be acknowledged within the committed + // time frame. + private void addToAckFrame(long packetNumber, boolean isAckEliciting) { + + long largestAckEliciting = largestAckElicitingReceivedPN; + if (isAckEliciting) ackElicitingPacketProcessed(packetNumber); + + if (debug.on()) { + if (packetNumber < largestAckEliciting) { + debug.log("already received a larger ACK eliciting packet"); + } + } + + // compute a new AckFrame that includes the + // provided packet number + NextAckFrame nextAckFrame, ack = null; + boolean reschedule; + long largestAckAcked; + long newLargestAckAcked = -1; + do { + Deadline now = now(); + nextAckFrame = this.nextAckFrame; + var frame = nextAckFrame == null ? null : nextAckFrame.ackFrame(); + largestAckAcked = emittedAckTracker.largestAckAcked(); + boolean needNewFrame = (frame == null || !frame.isAcknowledging(packetNumber)) + && packetNumber > largestAckAcked; + if (needNewFrame) { + if (debug.on()) { + debug.log("Adding %s(%d) to ackFrame %s (ackEliciting %s)", + packetNumberSpace, packetNumber, nextAckFrame, isAckEliciting); + } + var builder = AckFrameBuilder + .ofNullable(frame) + .dropAcksBefore(largestAckAcked) + .addAck(packetNumber); + assert !builder.isEmpty(); + frame = builder.build(); + + // Note: we could optimize this if needed by simply using a max number of + // ranges: we could pre-compute the approximate size of a frame that has N ranges + // and use that. + final int maxFrameSize = QuicConnectionImpl.SMALLEST_MAXIMUM_DATAGRAM_SIZE - 100; + if (frame.size() > maxFrameSize) { + // frame is too big. We will drop some ranges + int ranges = frame.ackRanges().size(); + int index = ranges/3; + builder.dropAckRangesAfter(index); + newLargestAckAcked = builder.getLargestAckAcked(); + var newFrame = builder.build(); + if (Log.quicCC() || Log.quicRetransmit()) { + Log.logQuic("{0}: frame too big ({1} bytes) dropping ack ranges after {2}, " + + "will ignore packets smaller than {3} (new frame: {4} bytes)", + debugStrSupplier.get(), Integer.toString(frame.size()), + Integer.toString(index), Long.toString(newLargestAckAcked), + Integer.toString(newFrame.size())); + } + frame = newFrame; + assert frame.size() <= maxFrameSize; + } + assert frame.isAcknowledging(packetNumber); + if (nextAckFrame == null) { + if (debug.on()) debug.log("no previous ackframe"); + Deadline deadline = isAckEliciting + ? now.plusMillis(maxAckDelay) + : Deadline.MAX; + ack = new NextAckFrame(frame, deadline, now, null); + reschedule = isAckEliciting; + if (debug.on()) debug.log("next deadline: " + maxAckDelay); + } else { + Deadline deadline = nextAckFrame.deadline(); + Deadline nextDeadline = deadline; + boolean deadlineNotExpired = now.isBefore(deadline); + if (isAckEliciting && deadlineNotExpired) { + if (debug.on()) debug.log("computing new deadline for ackframe"); + nextDeadline = computeNewDeadlineFor(frame, now, deadline, + packetNumber, largestAckEliciting, maxAckDelay); + } + long millisToNext = nextDeadline.equals(Deadline.MAX) + ? Long.MAX_VALUE + : now.until(nextDeadline, ChronoUnit.MILLIS); + if (debug.on()) { + if (nextDeadline == Deadline.MAX) { + debug.log("next deadline is: Deadline.MAX"); + } else { + debug.log("next deadline is: " + millisToNext); + } + } + ack = new NextAckFrame(frame, nextDeadline, now, null); + reschedule = !nextDeadline.equals(deadline) + || millisToNext <= 0; + } + if (debug.on()) { + String delay = reschedule ? Utils.millis(now(), ack.deadline()) + : "not rescheduled"; + debug.log("%s: new ackFrame composed: %s - reschedule=%s", + packetNumberSpace, ack.ackFrame(), delay); + } + } else { + reschedule = false; + if (debug.on()) { + debug.log("packet %s(%d) is already in ackFrame %s", + packetNumberSpace, packetNumber, nextAckFrame); + } + break; + } + } while (!Handles.NEXTACK.compareAndSet(this, nextAckFrame, ack)); + + if (newLargestAckAcked >= 0) { + // we reduced the frame because it was too big: we need to ignore + // packets that are larger than the new largest ignored packet. + // this is now our new de-facto 'largestAckAcked' even if it wasn't + // really acked by the peer + emittedAckTracker.dropPacketNumbersSmallerThan(newLargestAckAcked); + } + + var ackFrame = ack == null ? null : ack.ackFrame(); + assert packetNumber <= largestAckAcked + || ackFrame != null && ackFrame.isAcknowledging(packetNumber) + || nextAckFrame != null && nextAckFrame.ackFrame() != null + && nextAckFrame.ackFrame.isAcknowledging(packetNumber) + : "packet %s(%s) should be in ackFrame" + .formatted(packetNumberSpace, packetNumber); + + if (reschedule) { + runTransmitter(); + } + } + + void debugState() { + if (debug.on()) { + debug.log("state: %s", isClosed() ? "closed" : "opened" ); + debug.log("AckFrame: " + nextAckFrame); + String pendingAcks = pendingAcknowledgements.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + String pendingRetransmit = pendingRetransmission.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + debug.log("Pending acks: %s", pendingAcks); + debug.log("Pending retransmit: %s", pendingRetransmit); + } + } + + void debugState(String prefix, StringBuilder sb) { + String state = isClosed() ? "closed" : "opened"; + sb.append(prefix).append("State: ").append(state).append('\n'); + sb.append(prefix).append("AckFrame: ").append(nextAckFrame).append('\n'); + String pendingAcks = pendingAcknowledgements.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + String pendingRetransmit = pendingRetransmission.stream() + .map(PendingAcknowledgement::prettyPrint) + .collect(Collectors.joining(", ", "(", ")")); + sb.append(prefix).append("Pending acks: ").append(pendingAcks).append('\n'); + sb.append(prefix).append("Pending retransmit: ").append(pendingRetransmit); + } + + @Override + public boolean isAcknowledged(long packetNumber) { + var ack = nextAckFrame; + var ackFrame = ack == null ? null : ack.ackFrame(); + var largestProcessed = largestProcessedPN; + // if ackFrame is null it means all packets <= largestProcessedPN + // have been acked. + if (ackFrame == null) return packetNumber <= largestProcessed; + if (packetNumber > largestProcessed) return false; + var largestAckedPNReceivedByPeer = this.largestAckedPNReceivedByPeer; + if (packetNumber <= largestAckedPNReceivedByPeer) return true; + return ackFrame.isAcknowledging(packetNumber); + } + + @Override + public void fastRetransmit() { + assert packetNumberSpace == PacketNumberSpace.INITIAL; + if (closed || fastRetransmitDone) { + return; + } + fastRetransmit = true; + if (Log.quicControl() || Log.quicRetransmit()) { + Log.logQuic("Scheduling fast retransmit"); + } else if (debug.on()) { + debug.log("Scheduling fast retransmit"); + } + runTransmitter(); + + } + + private static Deadline min(Deadline one, Deadline two) { + return two.isAfter(one) ? one : two; + } + + // This implements the algorithm described in RFC 9000: + // 13.2.4. Limiting Ranges by Tracking ACK Frames + private void cleanupAcks() { + // clean up the next ACK frame, removing all packets <= largestAckAcked + NextAckFrame nextAckFrame, ack = null; + long largestAckAcked; + do { + nextAckFrame = this.nextAckFrame; + if (nextAckFrame == null) return; // nothing to do! + var frame = nextAckFrame.ackFrame(); + largestAckAcked = emittedAckTracker.largestAckAcked(); + boolean needNewFrame = frame != null + && frame.smallestAcknowledged() <= largestAckAcked; + if (needNewFrame) { + if (debug.on()) { + debug.log("Dropping all acks below %s(%d) in ackFrame %s", + packetNumberSpace, largestAckAcked, nextAckFrame); + } + var builder = AckFrameBuilder + .ofNullable(frame) + .dropAcksBefore(largestAckAcked); + frame = builder.isEmpty() ? null : builder.build(); + if (frame == null) { + ack = null; + if (debug.on()) { + debug.log("%s: ackFrame cleared - nothing to acknowledge", + packetNumberSpace); + } + } else { + Deadline deadline = nextAckFrame.deadline(); + ack = new NextAckFrame(frame, deadline, + nextAckFrame.lastUpdated(), nextAckFrame.sent()); + if (debug.on()) { + debug.log("%s: ackFrame cleaned up: %s", + packetNumberSpace, ack.ackFrame()); + } + } + } else { + if (debug.on()) { + debug.log("%s: no packet smaller than %d in ackFrame %s", + packetNumberSpace, largestAckAcked, nextAckFrame); + } + break; + } + } while (!Handles.NEXTACK.compareAndSet(this, nextAckFrame, ack)); + + var ackFrame = ack == null ? null : ack.ackFrame(); + assert ackFrame == null || ackFrame.smallestAcknowledged() > largestAckAcked + : "%s(pn > %s) should not acknowledge packet <= %s" + .formatted(packetNumberSpace, ackFrame.smallestAcknowledged(), largestAckAcked); + } + + private long ackElicitingPacketProcessed(long packetNumber) { + long largestPN; + do { + largestPN = largestAckElicitingReceivedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_ACK_ELICITING_RECEIVED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + private long packetProcessed(long packetNumber) { + long largestPN; + do { + largestPN = largestProcessedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_PROCESSED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + /** + * Theoretically we should wait for the packet that contains the + * ping frame to be acknowledged, but if we receive the ack of a + * packet with a larger number, we can assume that the connection + * is still alive, and therefore complete the ping response. + * @param packetNumber the acknowledged packet number + */ + private void processPingResponses(long packetNumber) { + if (pendingPingRequests.isEmpty()) return; + var iterator = pendingPingRequests.iterator(); + while (iterator.hasNext()) { + var pr = iterator.next(); + if (pr.packetNumber() <= packetNumber) { + iterator.remove(); + pr.response().complete(pr.sent().until(now(), ChronoUnit.MILLIS)); + } else { + // this is a queue, so the PingRequest with the smaller + // packet number will be at the head. We can stop iterating + // as soon as we find a PingRequest that has a packet + // number larger than the one acknowledged. + break; + } + } + } + + private long largestAckSent(long packetNumber) { + long largestPN; + do { + largestPN = largestSentAckedPN; + if (largestPN >= packetNumber) return largestPN; + } while (!Handles.LARGEST_SENT_ACKED_PN + .compareAndSet(this, largestPN, packetNumber)); + return packetNumber; + } + + private boolean largestAckReceived(long packetNumber) { + long largestPN; + do { + largestPN = largestReceivedAckedPN; + if (largestPN >= packetNumber) return false; // already up to date + } while (!Handles.LARGEST_RECEIVED_ACKED_PN + .compareAndSet(this, largestPN, packetNumber)); + return true; // updated + } + + // records the time at which the last ACK-eliciting packet was sent. + // This has the side effect of resetting the nextPingTime to Deadline.MAX + // The logic is that a PING frame only need to be sent if no ACK-eliciting + // packet has been sent for some time (and the AckFrame has grown big enough). + // See RFC 9000 - Section 13.2.4 + private Deadline lastAckElicitingSent(Deadline now) { + Deadline max; + if (debug.on()) + debug.log("Updating last send time to %s", now); + do { + max = lastAckElicitingTime; + if (max != null && !now.isAfter(max)) return max; + } while (!Handles.LAST_ACK_ELICITING_TIME + .compareAndSet(this, max, now)); + return now; + } + + /** + * returns the TLS encryption level of this packet space as specified + * in RFC-9001, section 4, table 1. + */ + private QuicTLSEngine.KeySpace tlsEncryptionLevel() { + return switch (this.packetNumberSpace) { + case INITIAL -> QuicTLSEngine.KeySpace.INITIAL; + // APPLICATION packet space could even mean 0-RTT, but currently we don't support 0-RTT + case APPLICATION -> QuicTLSEngine.KeySpace.ONE_RTT; + case HANDSHAKE -> QuicTLSEngine.KeySpace.HANDSHAKE; + default -> throw new IllegalStateException("No known TLS encryption level" + + " for packet space: " + this.packetNumberSpace); + }; + } + + // VarHandle provide the same atomic compareAndSet functionality + // than AtomicXXXXX classes, but without the additional cost in + // footprint. + private static final class Handles { + private Handles() {throw new InternalError();} + static final VarHandle DEADLINE; + static final VarHandle NEXTACK; + static final VarHandle LARGEST_PROCESSED_PN; + static final VarHandle LARGEST_ACK_ELICITING_RECEIVED_PN; + static final VarHandle LARGEST_RECEIVED_ACKED_PN; + static final VarHandle LARGEST_SENT_ACKED_PN; + static final VarHandle LARGEST_ACK_ACKED_PN; + static final VarHandle LAST_ACK_ELICITING_TIME; + static final VarHandle IGNORE_ALL_PN_BEFORE; + static { + Lookup lookup = MethodHandles.lookup(); + try { + Class srt = PacketTransmissionTask.class; + DEADLINE = lookup.findVarHandle(srt, "nextDeadline", Deadline.class); + + Class pmc = PacketSpaceManager.class; + LAST_ACK_ELICITING_TIME = lookup.findVarHandle(pmc, + "lastAckElicitingTime", Deadline.class); + NEXTACK = lookup.findVarHandle(pmc, "nextAckFrame", NextAckFrame.class); + LARGEST_RECEIVED_ACKED_PN = lookup + .findVarHandle(pmc, "largestReceivedAckedPN", long.class); + LARGEST_SENT_ACKED_PN = lookup + .findVarHandle(pmc, "largestSentAckedPN", long.class); + LARGEST_PROCESSED_PN = lookup + .findVarHandle(pmc, "largestProcessedPN", long.class); + LARGEST_ACK_ELICITING_RECEIVED_PN = lookup + .findVarHandle(pmc, "largestAckElicitingReceivedPN", long.class); + LARGEST_ACK_ACKED_PN = lookup + .findVarHandle(pmc, "largestAckedPNReceivedByPeer", long.class); + + Class eat = EmittedAckTracker.class; + IGNORE_ALL_PN_BEFORE = lookup + .findVarHandle(eat, "ignoreAllPacketsBefore", long.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + + } + } + } + + static final class OneRttPacketSpaceManager extends PacketSpaceManager + implements QuicOneRttContext { + + OneRttPacketSpaceManager(final QuicConnectionImpl connection) { + super(connection, PacketNumberSpace.APPLICATION); + } + } + + static final class HandshakePacketSpaceManager extends PacketSpaceManager { + private final PacketSpaceManager initialPktSpaceMgr; + private final boolean isClientConnection; + private final AtomicBoolean firstPktSent = new AtomicBoolean(); + + HandshakePacketSpaceManager(final QuicConnectionImpl connection, + final PacketSpaceManager initialPktSpaceManager) { + super(connection, PacketNumberSpace.HANDSHAKE); + this.isClientConnection = connection.isClientConnection(); + this.initialPktSpaceMgr = initialPktSpaceManager; + } + + @Override + public void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber) { + super.packetSent(packet, previousPacketNumber, packetNumber); + if (!isClientConnection) { + // nothing additional to be done for server connections + return; + } + if (firstPktSent.compareAndSet(false, true)) { + // if this is the first packet we sent in the HANDSHAKE keyspace + // then we close the INITIAL space discard the INITIAL keys. + // RFC-9000, section 17.2.2.1: + // A client stops both sending and processing Initial packets when it sends + // its first Handshake packet. ... Though packets might still be in flight or + // awaiting acknowledgment, no further Initial packets need to be exchanged + // beyond this point. Initial packet protection keys are discarded along with + // any loss recovery and congestion control state + if (debug.on()) { + debug.log("first handshake packet sent by client, initiating close of" + + " INITIAL packet space"); + } + this.initialPktSpaceMgr.close(); + } + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java new file mode 100644 index 00000000000..065d045b57c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnIdManager.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * Manages the connection ids advertised by a peer of a connection. + * - Handles incoming NEW_CONNECTION_ID frames, + * - produces outgoing RETIRE_CONNECTION_ID frames, + * - registers received stateless reset tokens with the QuicEndpoint + * Additionally on the client side: + * - handles incoming transport parameters (preferred_address, stateless_reset_token) + * - stores original and retry peer IDs + */ +// TODO implement voluntary switching of connection IDs +final class PeerConnIdManager { + private final Logger debug; + private final QuicConnectionImpl connection; + private final String logTag; + private final boolean isClient; + + private enum State { + INITIAL_PKT_NOT_RECEIVED_FROM_PEER, + RETRY_PKT_RECEIVED_FROM_PEER, + PEER_CONN_ID_FINALIZED + } + + private volatile State state = State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER; + + private QuicConnectionId clientSelectedDestConnId; + private QuicConnectionId peerDecidedRetryConnId; + // sequence number of active connection ID + private long activeConnIdSeq = -1; + private QuicConnectionId activeConnId; + + // the connection ids (there can be more than one) with which the peer identifies this connection. + // the key of this Map is a (RFC defined) sequence number for the connection id + private final NavigableMap peerConnectionIds = + Collections.synchronizedNavigableMap(new TreeMap<>()); + // the connection id sequence numbers that we haven't received yet. + // We need to know which sequence numbers are retired, and which are not assigned yet + private final NavigableSet gaps = + Collections.synchronizedNavigableSet(new TreeSet<>()); + // the connection id sequence numbers that are awaiting retirement. + private final Queue toRetire = new ArrayDeque<>(); + // the largest retirePriorTo value received across NEW_CONNECTION_ID frames + private volatile long largestReceivedRetirePriorTo = -1; // -1 implies none received so far + // the largest sequenceNumber value received across NEW_CONNECTION_ID frames + private volatile long largestReceivedSequenceNumber; + private final ReentrantLock lock = new ReentrantLock(); + + PeerConnIdManager(final QuicConnectionImpl connection, final String dbTag) { + this.isClient = connection.isClientConnection(); + this.debug = Utils.getDebugLogger(() -> dbTag); + this.logTag = connection.logTag(); + this.connection = connection; + } + + /** + * Save the client-selected original server connection ID + * + * @param peerConnId the client-selected original server connection ID + */ + void originalServerConnId(final QuicConnectionId peerConnId) { + lock.lock(); + try { + final var st = this.state; + if (st != State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER) { + throw new IllegalStateException("Cannot associate a client selected peer id" + + " in current state " + st); + } + this.clientSelectedDestConnId = peerConnId; + this.activeConnId = peerConnId; + } finally { + lock.unlock(); + } + } + + /** + * {@return the client-selected original server connection ID} + */ + QuicConnectionId originalServerConnId() { + lock.lock(); + try { + final var id = this.clientSelectedDestConnId; + if (id == null) { + throw new IllegalArgumentException("Original (peer) connection id not yet set"); + } + return id; + } finally { + lock.unlock(); + } + } + + /** + * Save the server-selected retry connection ID + * + * @param peerConnId the server-selected retry connection ID + */ + void retryConnId(final QuicConnectionId peerConnId) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final var st = this.state; + if (st != State.INITIAL_PKT_NOT_RECEIVED_FROM_PEER) { + throw new IllegalStateException("Cannot associate a peer id, from retry packet," + + " in current state " + st); + } + this.peerDecidedRetryConnId = peerConnId; + this.activeConnId = peerConnId; + this.state = State.RETRY_PKT_RECEIVED_FROM_PEER; + } finally { + lock.unlock(); + } + } + + /** + * Returns the connectionId the server included in the Source Connection ID field of a + * Retry packet. May be null. + * + * @return the connection id sent in the server's retry packet + */ + QuicConnectionId retryConnId() { + lock.lock(); + try { + return this.peerDecidedRetryConnId; + } finally { + lock.unlock(); + } + } + + /** + * The peer in its INITIAL packet would have sent a connection id representing itself. That + * connection id may not be the same that we might have sent in the INITIAL packet. If it isn't + * the same, then we switch the peer connection id, that we keep track off, to the one that + * the peer has chosen. + * + * @param initialPacket the INITIAL packet from the peer + */ + void finalizeHandshakePeerConnId(final InitialPacket initialPacket) throws QuicTransportException { + lock.lock(); + try { + final QuicConnectionId sourceId = initialPacket.sourceId(); + final var st = this.state; + if (st == State.PEER_CONN_ID_FINALIZED) { + // we have already finalized the peer connection id, through a previous INITIAL + // packet receipt (there can be more than one INITIAL packets). + // now we just verify that this INITIAL packet too has the finalized peer connection + // id and if it doesn't then we throw an exception + final QuicConnectionId handshakePeerConnId = this.peerConnectionIds.get(0L); + assert handshakePeerConnId != null : "Handshake peer connection id is unavailable"; + if (!handshakePeerConnId.equals(sourceId)) { + throw new QuicTransportException("Invalid source connection id in INITIAL packet", + QuicTLSEngine.KeySpace.INITIAL, 0, PROTOCOL_VIOLATION); + } + return; + } + // this is the first INITIAL packet from the peer, so we finalize the peer connection id + final PeerConnectionId handshakePeerConnId = new PeerConnectionId(sourceId.getBytes()); + // at this point we have either switched to a new peer connection id (chosen by the peer) + // or have agreed to use the one we chose for the peer. In either case, we register this + // as the handshake peer connection id with sequence number 0. + // RFC-9000, section 5.1.1: The initial connection ID issued by an endpoint is sent in + // the Source Connection ID field of the long packet header during the handshake. + // The sequence number of the initial connection ID is 0. + this.peerConnectionIds.put(0L, handshakePeerConnId); + this.state = State.PEER_CONN_ID_FINALIZED; + this.activeConnIdSeq = 0; + this.activeConnId = handshakePeerConnId; + if (debug.on()) { + debug.log("scid: %s finalized handshake peerConnectionId as: %s", + connection.localConnectionId().toHexString(), + handshakePeerConnId.toHexString()); + } + } finally { + lock.unlock(); + } + } + + /** + * Save the connection ID from the preferred address QUIC transport parameter + * + * @param preferredConnId preferred connection ID + * @param preferredStatelessResetToken preferred stateless reset token + */ + void handlePreferredAddress(final ByteBuffer preferredConnId, + final byte[] preferredStatelessResetToken) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final PeerConnectionId peerConnId = new PeerConnectionId(preferredConnId, + preferredStatelessResetToken); + // keep track of this peer connection id + // RFC-9000, section 5.1.1: If the preferred_address transport parameter is sent, + // the sequence number of the supplied connection ID is 1 + assert largestReceivedSequenceNumber == 0; + this.peerConnectionIds.put(1L, peerConnId); + largestReceivedSequenceNumber = 1; + } finally { + lock.unlock(); + } + } + + /** + * Save the stateless reset token QUIC transport parameter + * + * @param statelessResetToken stateless reset token + */ + void handshakeStatelessResetToken(final byte[] statelessResetToken) { + if (!isClient) { + throw new IllegalStateException("Should not be used on the server"); + } + lock.lock(); + try { + final QuicConnectionId handshakeConnId = this.peerConnectionIds.get(0L); + if (handshakeConnId == null) { + throw new IllegalStateException("No handshake peer connection available"); + } + // recreate the conn id with the stateless token + this.peerConnectionIds.put(0L, new PeerConnectionId(handshakeConnId.asReadOnlyBuffer(), + statelessResetToken)); + // register with the endpoint + connection.endpoint().associateStatelessResetToken(statelessResetToken, connection); + } finally { + lock.unlock(); + } + } + + /** + * {@return the active peer connection ID} + */ + QuicConnectionId getPeerConnId() { + lock.lock(); + try { + if (activeConnIdSeq < largestReceivedRetirePriorTo) { + // stop using the old connection ID + switchConnectionId(); + } + return activeConnId; + } finally { + lock.unlock(); + } + } + + private QuicConnectionId getPeerConnId(final long sequenceNum) { + assert lock.isHeldByCurrentThread(); + return this.peerConnectionIds.get(sequenceNum); + } + + /** + * Process the incoming NEW_CONNECTION_ID frame. + * + * @param newCid the NEW_CONNECTION_ID frame + * @throws QuicTransportException if the frame violates the protocol + */ + void handleNewConnectionIdFrame(final NewConnectionIDFrame newCid) + throws QuicTransportException { + if (debug.on()) { + debug.log("Received NEW_CONNECTION_ID frame: %s", newCid); + } + // pre-checks + final long sequenceNumber = newCid.sequenceNumber(); + assert sequenceNumber >= 0 : "negative sequence number disallowed in new connection id frame"; + final long retirePriorTo = newCid.retirePriorTo(); + if (retirePriorTo > sequenceNumber) { + // RFC 9000, section 19.15: Receiving a value in the Retire Prior To field that is greater + // than that in the Sequence Number field MUST be treated as a connection error of + // type FRAME_ENCODING_ERROR + throw new QuicTransportException("Invalid retirePriorTo " + retirePriorTo, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final ByteBuffer connectionId = newCid.connectionId(); + final int connIdLength = connectionId.remaining(); + if (connIdLength < 1 || connIdLength > MAX_CONNECTION_ID_LENGTH) { + // RFC-9000, section 19.15: Values less than 1 and greater than 20 are invalid and + // MUST be treated as a connection error of type FRAME_ENCODING_ERROR + throw new QuicTransportException("Invalid connection id length " + connIdLength, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final ByteBuffer statelessResetToken = newCid.statelessResetToken(); + assert statelessResetToken.remaining() == QuicConnectionImpl.RESET_TOKEN_LENGTH; + lock.lock(); + try { + // see if we have received any connection ids for this same sequence number. + // this is possible if the packet containing the new connection id frame was retransmitted. + // the connection id for such a (duplicate) sequence number is expected to be the same. + // RFC-9000, section 19.15: if a sequence number is used for different connection IDs, + // the endpoint MAY treat that receipt as a connection error of type PROTOCOL_VIOLATION + final QuicConnectionId previousConnIdForSeqNum = getPeerConnId(sequenceNumber); + if (previousConnIdForSeqNum != null) { + if (previousConnIdForSeqNum.matches(connectionId)) { + // frame with same sequence number and connection id, probably a retransmission. + // ignore this frame + if (Log.trace()) { + Log.logTrace("{0} Ignoring (duplicate) new connection id frame with" + + " sequence number {1}", logTag, sequenceNumber); + } + if (debug.on()) { + debug.log("Ignoring (duplicate) new connection id frame with" + + " sequence number %d", sequenceNumber); + } + return; + } + // mismatch, throw protocol violation error + throw new QuicTransportException("Invalid connection id in (duplicated)" + + " new connection id frame with sequence number " + sequenceNumber, + QuicTLSEngine.KeySpace.ONE_RTT, + newCid.getTypeField(), PROTOCOL_VIOLATION); + } + if ((sequenceNumber <= largestReceivedSequenceNumber && !gaps.contains(sequenceNumber)) + || sequenceNumber < largestReceivedRetirePriorTo) { + if (Log.trace()) { + Log.logTrace("{0} Ignoring (retired) new connection id frame with" + + " sequence number {1}", logTag, sequenceNumber); + } + if (debug.on()) { + debug.log("Ignoring (retired) new connection id frame with" + + " sequence number %d", sequenceNumber); + } + return; + } + long numConnIdsToAdd = Math.max(sequenceNumber - largestReceivedSequenceNumber, 0); + final long numCurrentActivePeerConnIds = this.peerConnectionIds.size() + this.gaps.size(); + // we can temporarily store up to 3x the active connection ID limit, + // including active and retired IDs. + if (numCurrentActivePeerConnIds + numConnIdsToAdd + toRetire.size() + > 3 * this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.1: After processing a NEW_CONNECTION_ID frame and adding and + // retiring active connection IDs, if the number of active connection IDs exceeds + // the value advertised in its active_connection_id_limit transport parameter, + // an endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR + throw new QuicTransportException("Connection id limit reached", + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + // end pre-checks + // if we reached here, the number of connection IDs is less than twice the active limit. + // Insert gaps for the sequence numbers we haven't seen yet + insertGaps(sequenceNumber); + // Update the list of sequence numbers to retire + retirePriorTo(retirePriorTo); + // insert the new connection ID + final byte[] statelessResetTokenBytes = new byte[QuicConnectionImpl.RESET_TOKEN_LENGTH]; + statelessResetToken.get(statelessResetTokenBytes); + final PeerConnectionId newPeerConnId = new PeerConnectionId(connectionId, statelessResetTokenBytes); + final var previous = this.peerConnectionIds.putIfAbsent(sequenceNumber, newPeerConnId); + assert previous == null : "A peer connection id already exists for sequence number " + + sequenceNumber; + // post-checks + // now we can accurately check the number of active and retired connection IDs + if (peerConnectionIds.size() + gaps.size() + > this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.1: After processing a NEW_CONNECTION_ID frame and adding and + // retiring active connection IDs, if the number of active connection IDs exceeds + // the value advertised in its active_connection_id_limit transport parameter, + // an endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR + throw new QuicTransportException("Active connection id limit reached", + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + if (toRetire.size() > 2 * this.connection.getLocalActiveConnIDLimit()) { + // RFC-9000, section 5.1.2: + // An endpoint SHOULD limit the number of connection IDs it has retired locally for + // which RETIRE_CONNECTION_ID frames have not yet been acknowledged. + // An endpoint SHOULD allow for sending and tracking a number + // of RETIRE_CONNECTION_ID frames of at least twice the value + // of the active_connection_id_limit transport parameter + throw new QuicTransportException("Retired connection id limit reached: " + toRetire, + QuicTLSEngine.KeySpace.ONE_RTT, newCid.getTypeField(), + QuicTransportErrors.CONNECTION_ID_LIMIT_ERROR); + } + if (this.largestReceivedRetirePriorTo < retirePriorTo) { + this.largestReceivedRetirePriorTo = retirePriorTo; + } + if (this.largestReceivedSequenceNumber < sequenceNumber) { + this.largestReceivedSequenceNumber = sequenceNumber; + } + } finally { + lock.unlock(); + } + } + + private void switchConnectionId() { + assert lock.isHeldByCurrentThread(); + // the caller is expected to retire the active connection id prior to calling this + assert !peerConnectionIds.containsKey(activeConnIdSeq); + Map.Entry entry = peerConnectionIds.ceilingEntry(largestReceivedRetirePriorTo); + activeConnIdSeq = entry.getKey(); + activeConnId = entry.getValue(); + // link the peer issued stateless reset token to this connection + final QuicEndpoint endpoint = this.connection.endpoint(); + endpoint.associateStatelessResetToken(entry.getValue().getStatelessResetToken(), this.connection); + + if (Log.trace()) { + Log.logTrace("{0} Switching to connection ID {1}", logTag, activeConnIdSeq); + } + if (debug.on()) { + debug.log("Switching to connection ID %d", activeConnIdSeq); + } + } + + private void insertGaps(long sequenceNumber) { + assert lock.isHeldByCurrentThread(); + for (long i = largestReceivedSequenceNumber + 1; i < sequenceNumber; i++) { + gaps.add(i); + } + } + + private void retirePriorTo(final long priorTo) { + assert lock.isHeldByCurrentThread(); + // remove/retire (in preparation of sending a RETIRE_CONNECTION_ID frames) + for (Iterator> iterator = peerConnectionIds.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + final long seqNumToRetire = entry.getKey(); + if (seqNumToRetire >= priorTo) { + break; + } + iterator.remove(); + toRetire.add(seqNumToRetire); + // Note that the QuicEndpoint only stores local connection ids and doesn't store peer + // connection ids. It does however store the peer-issued stateless reset token of a + // peer connection id, so we let the endpoint know that the stateless reset token needs + // to be forgotten since the corresponding peer connection id is being retired + final byte[] resetTokenToForget = entry.getValue().getStatelessResetToken(); + if (resetTokenToForget != null) { + this.connection.endpoint().forgetStatelessResetToken(resetTokenToForget); + } + } + for (Iterator iterator = gaps.iterator(); iterator.hasNext(); ) { + Long gap = iterator.next(); + if (gap >= priorTo) { + return; + } + iterator.remove(); + toRetire.add(gap); + } + } + + /** + * Produce a queued RETIRE_CONNECTION_ID frame, if it fits in the packet + * + * @param remaining bytes remaining in the packet + * @return a RetireConnectionIdFrame, or null if none is queued or remaining is too low + */ + public QuicFrame nextFrame(int remaining) { + // retire connection id: + // type - 1 byte + // sequence number - var int + if (remaining < 9) { + return null; + } + lock.lock(); + try { + final Long seqNumToRetire = toRetire.poll(); + if (seqNumToRetire != null) { + if (seqNumToRetire == activeConnIdSeq) { + // can't send this connection ID yet, we will send it in the next packet + toRetire.add(seqNumToRetire); + return null; + } + return new RetireConnectionIDFrame(seqNumToRetire); + } + return null; + } finally { + lock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java new file mode 100644 index 00000000000..0c6f946d1a2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PeerConnectionId.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + + +/** + * A free-form connection ID to wrap the connection ID bytes + * sent by the peer. + * Client and server might impose some structure on the + * connection ID bytes. For instance, they might choose to + * encode the connection ID length in the connection ID bytes. + * This class makes no assumption on the structure of the + * connection id bytes. + */ +public final class PeerConnectionId extends QuicConnectionId { + private final byte[] statelessResetToken; + + /** + * A new {@link QuicConnectionId} represented by the given bytes. + * @param connId The connection ID bytes. + */ + public PeerConnectionId(final byte[] connId) { + super(ByteBuffer.wrap(connId.clone())); + this.statelessResetToken = null; + } + + /** + * A new {@link QuicConnectionId} represented by the given bytes. + * @param connId The connection ID bytes. + * @param statelessResetToken The stateless reset token to be associated with this connection id. + * Can be null. + * @throws IllegalArgumentException If the {@code statelessResetToken} is non-null and if its + * length isn't 16 bytes + * + */ + public PeerConnectionId(final ByteBuffer connId, final byte[] statelessResetToken) { + super(cloneBuffer(connId)); + if (statelessResetToken != null) { + if (statelessResetToken.length != 16) { + throw new IllegalArgumentException("Invalid stateless reset token length " + + statelessResetToken.length); + } + this.statelessResetToken = statelessResetToken.clone(); + } else { + this.statelessResetToken = null; + } + } + + private static ByteBuffer cloneBuffer(ByteBuffer src) { + final byte[] idBytes = new byte[src.remaining()]; + src.get(idBytes); + return ByteBuffer.wrap(idBytes); + } + + /** + * {@return the stateless reset token associated with this connection id. returns null if no + * token exists} + */ + public byte[] getStatelessResetToken() { + return this.statelessResetToken == null ? null : this.statelessResetToken.clone(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "(length:" + length() + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java new file mode 100644 index 00000000000..58a07c22f66 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicClient.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.LongFunction; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.AltServicesRegistry; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint.QuicEndpointFactory; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +/** + * This class represents a QuicClient. + * The QuicClient is responsible for creating/returning instances + * of QuicConnection for a given AltService, and for linking them + * with an instance of QuicEndpoint and QuicSelector for reading + * and writing Datagrams off the network. + * A QuicClient is also a factory for QuicConnectionIds. + * There is a 1-1 relationship between a QuicClient and an Http3Client. + * A QuicClient can be closed: closing a QuicClient will close all + * quic connections opened on that client. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public final class QuicClient implements QuicInstance, AutoCloseable { + private static final AtomicLong IDS = new AtomicLong(); + private static final AtomicLong CONNECTIONS = new AtomicLong(); + + private final Logger debug = Utils.getDebugLogger(this::name); + + // See RFC 9000 section 14 + static final int SMALLEST_MAXIMUM_DATAGRAM_SIZE = 1200; + static final int INITIAL_SERVER_CONNECTION_ID_LENGTH = 17; + static final int MAX_ENDPOINTS_LIMIT = 16; + static final int DEFAULT_MAX_ENDPOINTS = Utils.getIntegerNetProperty( + "jdk.httpclient.quic.maxEndpoints", 1); + + private final String clientId; + private final String name; + private final Executor executor; + private final QuicTLSContext quicTLSContext; + private final SSLParameters sslParameters; + // QUIC versions in their descending order of preference + private final List availableVersions; + private final InetSocketAddress bindAddress; + private final QuicTransportParameters transportParams; + private final ReentrantLock lock = new ReentrantLock(); + private final QuicEndpoint[] endpoints = new QuicEndpoint[computeMaxEndpoints()]; + private int insertionPoint; + private volatile QuicSelector selector; + private volatile boolean closed; + // keep track of any initial tokens that a server has advertised for use. The key in this + // map is the server's host and port representation and the value is the token to use. + private final Map initialTokens = new ConcurrentHashMap<>(); + private final QuicEndpointFactory endpointFactory = new QuicEndpointFactory(); + private final LongFunction appErrorCodeToString; + + private QuicClient(final QuicClient.Builder builder) { + Objects.requireNonNull(builder, "Quic client builder"); + if (builder.availableVersions == null) { + throw new IllegalStateException("Need at least one available Quic version"); + } + if (builder.tlsContext == null) { + throw new IllegalStateException("No QuicTLSContext set"); + } + this.clientId = builder.clientId == null ? nextName() : builder.clientId; + this.name = "QuicClient(%s)".formatted(clientId); + this.appErrorCodeToString = builder.appErrorCodeToString == null + ? QuicInstance.super::appErrorToString + : builder.appErrorCodeToString; + // verify that QUIC TLS supports all requested QUIC versions + var test = new ArrayList<>(builder.availableVersions); + test.removeAll(builder.tlsContext.createEngine().getSupportedQuicVersions()); + if (!test.isEmpty()) { + throw new IllegalArgumentException( + "Requested QUIC versions not supported by TLS: " + test); + } + this.availableVersions = builder.availableVersions; + this.quicTLSContext = builder.tlsContext; + this.bindAddress = builder.bindAddr == null ? new InetSocketAddress(0) : builder.bindAddr; + this.executor = builder.executor; + this.sslParameters = builder.sslParams == null + ? new SSLParameters() + : requireTLS13(builder.sslParams); + this.transportParams = builder.transportParams; + if (debug.on()) debug.log("created"); + } + + + private static int computeMaxEndpoints() { + // available processors may change according to the API doc, + // so recompute this for each new client... + int availableProcessors = Runtime.getRuntime().availableProcessors(); + int max = DEFAULT_MAX_ENDPOINTS <= 0 ? availableProcessors >> 1 : DEFAULT_MAX_ENDPOINTS; + return Math.clamp(max, 1, MAX_ENDPOINTS_LIMIT); + } + + // verifies that the TLS protocol(s) configured in SSLParameters, if any, + // allows TLSv1.3 + private static SSLParameters requireTLS13(final SSLParameters parameters) { + final String[] protos = parameters.getProtocols(); + if (protos == null || protos.length == 0) { + // no specific protocols specified, so it's OK + return parameters; + } + for (final String proto : protos) { + if ("TLSv1.3".equals(proto)) { + // TLSv1.3 is allowed, that's good + return parameters; + } + } + // explicit TLS protocols have been configured in SSLParameters and it doesn't + // include TLSv1.3. QUIC mandates TLSv1.3, so we can't use this SSLParameters + throw new IllegalArgumentException("TLSv1.3 is required for QUIC," + + " but SSLParameters is configured with " + Arrays.toString(protos)); + } + + @Override + public String appErrorToString(long code) { + return appErrorCodeToString.apply(code); + } + + @Override + public QuicTransportParameters getTransportParameters() { + if (this.transportParams == null) { + return null; + } + // return a copy + return new QuicTransportParameters(this.transportParams); + } + + private static String nextName() { + return "quic-client-" + IDS.incrementAndGet(); + } + + /** + * The address that the QuicEndpoint will bind to. + * @implNote By default, this is wildcard:0 + * @return the address that the QuicEndpoint will bind to. + */ + public InetSocketAddress bindAddress() { + return bindAddress; + } + + @Override + public boolean isVersionAvailable(final QuicVersion quicVersion) { + return this.availableVersions.contains(quicVersion); + } + + /** + * {@return the versions that are available for use on this instance, in the descending order + * of their preference} + */ + @Override + public List getAvailableVersions() { + return this.availableVersions; + } + + /** + * Creates a new unconnected {@code QuicConnection} to the given + * {@code service}. + * + * @param service the alternate service for which to create the connection for + * @return a new unconnected {@code QuicConnection} + * @throws IllegalArgumentException if the ALPN of this transport isn't the same as that of the + * passed alternate service + * @apiNote The caller is expected to call {@link QuicConnectionImpl#startHandshake()} to + * initiate the handshaking. The connection is considered "connected" when + * the handshake is successfully completed. + */ + public QuicConnectionImpl createConnectionFor(final AltServicesRegistry.AltService service) { + final InetSocketAddress peerAddress = new InetSocketAddress(service.identity().host(), + service.identity().port()); + final String alpn = service.alpn(); + if (alpn == null) { + throw new IllegalArgumentException("missing ALPN on alt service"); + } + final SSLParameters sslParameters = createSSLParameters(new String[]{alpn}); + return new QuicConnectionImpl(null, this, peerAddress, + service.origin().host(), service.origin().port(), sslParameters, "QuicClientConnection(%s)", + CONNECTIONS.incrementAndGet()); + } + + /** + * Creates a new unconnected {@code QuicConnection} to the given + * {@code peerAddress}. + * + * @param peerAddress the address of the peer + * @return a new unconnected {@code QuicConnection} + * @apiNote The caller is expected to call {@link QuicConnectionImpl#startHandshake()} to + * initiate the handshaking. The connection is considered "connected" when + * the handshake is successfully completed. + */ + public QuicConnectionImpl createConnectionFor(final InetSocketAddress peerAddress, + final String[] alpns) { + Objects.requireNonNull(peerAddress); + Objects.requireNonNull(alpns); + if (alpns.length == 0) { + throw new IllegalArgumentException("at least one ALPN is needed"); + } + final SSLParameters sslParameters = createSSLParameters(alpns); + return new QuicConnectionImpl(null, this, peerAddress, peerAddress.getHostString(), + peerAddress.getPort(), sslParameters, "QuicClientConnection(%s)", CONNECTIONS.incrementAndGet()); + } + + private SSLParameters createSSLParameters(final String[] alpns) { + final SSLParameters sslParameters = Utils.copySSLParameters(this.getSSLParameters()); + sslParameters.setApplicationProtocols(alpns); + // section 4.2, RFC-9001 (QUIC) Clients MUST NOT offer TLS versions older than 1.3 + sslParameters.setProtocols(new String[] {"TLSv1.3"}); + return sslParameters; + } + + @Override + public String instanceId() { + return clientId; + } + + @Override + public QuicTLSContext getQuicTLSContext() { + return quicTLSContext; + } + + @Override + public SSLParameters getSSLParameters() { + return Utils.copySSLParameters(sslParameters); + } + + /** + * The name identifying this QuicClient, used in debug traces. + * @implNote This is {@code "QuicClient()"}. + * @return the name identifying this QuicClient. + */ + public String name() { + return name; + } + + /** + * The HttpClientImpl Id. used to identify the client in + * debug traces. + * @return A string identifying the HttpClientImpl instance. + */ + public String clientId() { + return clientId; + } + + /** + * The executor used by this QuicClient when a task needs to + * be offloaded to a separate thread. + * @implNote This is the HttpClientImpl internal executor. + * @return the executor used by this QuicClient. + */ + @Override + public Executor executor() { + return executor; + } + + @Override + public QuicEndpoint getEndpoint() throws IOException { + return chooseEndpoint(); + } + + private QuicEndpoint chooseEndpoint() throws IOException { + QuicEndpoint endpoint; + lock.lock(); + try { + if (closed) throw new IllegalStateException("QuicClient is closed"); + int index = insertionPoint; + if (index >= endpoints.length) index = 0; + endpoint = endpoints[index]; + if (endpoint != null) { + if (endpoints.length == 1) return endpoint; + if (endpoint.connectionCount() < 2) return endpoint; + for (int i = 1; i < endpoints.length - 1; i++) { + var nexti = (index + i) % endpoints.length; + var next = endpoints[nexti]; + if (next == null) continue; + if (next.connectionCount() < endpoint.connectionCount()) { + endpoint = next; + index = nexti; + } + } + if (++index >= endpoints.length) index = 0; + insertionPoint = index; + + if (Log.quicControl()) { + Log.logQuic("Selecting endpoint: " + endpoint.name()); + } else if (debug.on()) { + debug.log("Selecting endpoint: " + endpoint.name()); + } + + return endpoint; + } + + final var endpointName = "QuicEndpoint(" + clientId + "-" + index + ")"; + if (Log.quicControl()) { + Log.logQuic("Adding new endpoint: " + endpointName); + } else if (debug.on()) { + debug.log("Adding new endpoint: " + endpointName); + } + endpoint = createEndpoint(endpointName); + assert endpoints[index] == null; + endpoints[index] = endpoint; + insertionPoint = index + 1; + } finally { + lock.unlock(); + } + // register the newly created endpoint with the selector + QuicEndpoint.registerWithSelector(endpoint, selector, debug); + return endpoint; + } + + /** + * Creates an endpoint with the given name, and register it with a selector. + * @return the new QuicEndpoint + * @throws IOException if an error occurs when setting up the selector + * or linking the transport with the selector. + * @throws IllegalStateException if the client is closed. + */ + private QuicEndpoint createEndpoint(final String endpointName) throws IOException { + var selector = this.selector; + boolean newSelector = false; + final QuicEndpoint.ChannelType configuredChannelType = QuicEndpoint.CONFIGURED_CHANNEL_TYPE; + if (selector == null) { + // create a selector first + lock.lock(); + try { + if (closed) { + throw new IllegalStateException("QuicClient is closed"); + } + selector = this.selector; + if (selector == null) { + final String selectorName = "QuicSelector(" + clientId + ")"; + selector = this.selector = switch (configuredChannelType) { + case NON_BLOCKING_WITH_SELECTOR -> + QuicSelector.createQuicNioSelector(this, selectorName); + case BLOCKING_WITH_VIRTUAL_THREADS -> + QuicSelector.createQuicVirtualThreadPoller(this, selectorName); + }; + newSelector = true; + } + } finally { + lock.unlock(); + } + } + if (newSelector) { + // we may be closed when we reach here. It doesn't matter though. + // if the selector is closed before it's started the thread will + // immediately exit (or exit after the first wakeup) + selector.start(); + } + final QuicEndpoint endpoint = switch (configuredChannelType) { + case NON_BLOCKING_WITH_SELECTOR -> + endpointFactory.createSelectableEndpoint(this, endpointName, + bindAddress(), selector.timer()); + case BLOCKING_WITH_VIRTUAL_THREADS -> + endpointFactory.createVirtualThreadedEndpoint(this, endpointName, + bindAddress(), selector.timer()); + }; + assert endpoint.channelType() == configuredChannelType + : "bad endpoint for " + configuredChannelType + ": " + endpoint.getClass(); + return endpoint; + } + + @Override + public void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer) { + if (debug.on()) { + debug.log("dropping unmatched packet in buffer [%s, %d bytes, %s]", + type, buffer.remaining(), source); + } + } + + /** + * @param peerAddress The address of the server + * @return the initial token to use in INITIAL packets during connection establishment + * against a server represented by the {@code peerAddress}. Returns null if no token exists for + * the server. + */ + byte[] initialTokenFor(final InetSocketAddress peerAddress) { + if (peerAddress == null) { + return null; + } + final InitialTokenRecipient recipient = new InitialTokenRecipient(peerAddress.getHostString(), + peerAddress.getPort()); + // an initial token (obtained through NEW_TOKEN frame) can be used only once against the + // peer which advertised it. Hence, we remove it. + return this.initialTokens.remove(recipient); + } + + /** + * Registers a token to use in INITIAL packets during connection establishment against a server + * represented by the {@code peerAddress}. + * + * @param peerAddress The address of the server + * @param token The token to use + * @throws NullPointerException If either of {@code peerAddress} or {@code token} is null + * @throws IllegalArgumentException If the token is of zero length + */ + void registerInitialToken(final InetSocketAddress peerAddress, final byte[] token) { + Objects.requireNonNull(peerAddress); + Objects.requireNonNull(token); + if (token.length == 0) { + throw new IllegalArgumentException("Empty token"); + } + final InitialTokenRecipient recipient = new InitialTokenRecipient(peerAddress.getHostString(), + peerAddress.getPort()); + // multiple initial tokens (through NEW_TOKEN frame) can be sent by the same peer, but as + // per RFC-9000, section 8.1.3, it's OK for clients to just use the last received token, + // since the rest are less likely to be useful + this.initialTokens.put(recipient, token); + } + + @Override + public void close() { + // TODO: ignore exceptions while closing? + lock.lock(); + try { + if (closed) return; + closed = true; + } finally { + lock.unlock(); + } + for (int i = 0 ; i < endpoints.length ; i++) { + var endpoint = endpoints[i]; + if (endpoint != null) closeEndpoint(endpoint); + } + var selector = this.selector; + if (selector != null) selector.close(); + } + + private void closeEndpoint(QuicEndpoint endpoint) { + try { endpoint.close(); } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close endpoint: %s: %s", endpoint.name(), t); + } + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + lock.lock(); + try { + if (closed) return; + closed = true; + } finally { + lock.unlock(); + } + for (int i = 0 ; i < endpoints.length ; i++) { + var endpoint = endpoints[i]; + if (endpoint != null) abortEndpoint(endpoint, t); + } + var selector = this.selector; + if (selector != null) selector.abort(t); + } + + private void abortEndpoint(QuicEndpoint endpoint, Throwable cause) { + try { endpoint.abort(cause); } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to abort endpoint: %s: %s", endpoint.name(), t); + } + } + } + + private record InitialTokenRecipient (String host, int port) { + } + + public static final class Builder { + private String clientId; + private List availableVersions; + private Executor executor; + private SSLParameters sslParams; + private QuicTLSContext tlsContext; + private QuicTransportParameters transportParams; + private InetSocketAddress bindAddr; + private LongFunction appErrorCodeToString; + + public Builder availableVersions(final List versions) { + Objects.requireNonNull(versions, "Quic versions"); + if (versions.isEmpty()) { + throw new IllegalArgumentException("Need at least one available Quic version"); + } + this.availableVersions = List.copyOf(versions); + return this; + } + + public Builder applicationErrors(LongFunction errorCodeToString) { + this.appErrorCodeToString = errorCodeToString; + return this; + } + + public Builder availableVersions(final QuicVersion version, final QuicVersion... more) { + Objects.requireNonNull(version, "Quic version"); + if (more == null) { + this.availableVersions = List.of(version); + return this; + } + final List versions = new ArrayList<>(); + versions.add(version); + for (final QuicVersion v : more) { + Objects.requireNonNull(v, "Quic version"); + versions.add(v); + } + this.availableVersions = List.copyOf(versions); + return this; + } + + public Builder clientId(final String clientId) { + this.clientId = clientId; + return this; + } + + public Builder tlsContext(final QuicTLSContext tlsContext) { + this.tlsContext = tlsContext; + return this; + } + + public Builder sslParameters(final SSLParameters sslParameters) { + this.sslParams = sslParameters; + return this; + } + + public Builder bindAddress(final InetSocketAddress bindAddr) { + this.bindAddr = bindAddr; + return this; + } + + public Builder executor(final Executor executor) { + this.executor = executor; + return this; + } + + public Builder transportParameters(final QuicTransportParameters transportParams) { + this.transportParams = transportParams; + return this; + } + + public QuicClient build() { + return new QuicClient(this); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java new file mode 100644 index 00000000000..4bfad2c5560 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.util.Collection; + +public interface QuicCongestionController { + + /** + * {@return true if a new non-ACK packet can be sent at this time} + */ + boolean canSendPacket(); + + /** + * Update the maximum datagram size + * @param newSize new maximum datagram size. + */ + void updateMaxDatagramSize(int newSize); + + /** + * Update CC with a non-ACK packet + * @param packetBytes packet size in bytes + */ + void packetSent(int packetBytes); + + /** + * Update CC after a non-ACK packet is acked + * + * @param packetBytes acked packet size in bytes + * @param sentTime time when packet was sent + */ + void packetAcked(int packetBytes, Deadline sentTime); + + /** + * Update CC after packets are declared lost + * + * @param lostPackets collection of lost packets + * @param sentTime time when the most recent lost packet was sent + * @param persistent true if persistent congestion detected, false otherwise + */ + void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent); + + /** + * Update CC after packets are discarded + * @param discardedPackets collection of discarded packets + */ + void packetDiscarded(Collection discardedPackets); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java new file mode 100644 index 00000000000..05bfa1adc8c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnection.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreams; +import jdk.internal.net.quic.QuicTLSEngine; + +/** + * This class implements a QUIC connection. + * A QUIC connection is established between a client and a server + * over a QuicEndpoint endpoint. + * A QUIC connection can then multiplex multiple QUIC streams to the + * same server. + * This abstract class exposes public methods used by the higher level + * protocol. + * + *

        A typical call flow to establish a connection would be: + * {@snippet : + * AltService service = ...; + * QuicClient client = ...; + * QuicConnection connection = client.createConnectionFor(service); + * connection.startHandshake() + * .thenApply((r) -> { ... }) + * ...; + * } + * + */ +public abstract class QuicConnection { + + /** + * Starts the Quic Handshake. + * @return A completable future which will be completed when the + * handshake is completed. + * @throws UnsupportedOperationException If this connection isn't a client connection + */ + public abstract CompletableFuture startHandshake(); + + /** + * Creates a new locally initiated bidirectional stream. + *

        + * Creation of streams is limited to the maximum limit advertised by the peer. If a new stream + * cannot be created due to this limitation, then this method will use the + * {@code limitIncreaseDuration} to decide how long to wait for a potential increase in the + * limit. + *

        + * If the limit has been reached and the {@code limitIncreaseDuration} is not + * {@link Duration#isPositive() positive} then this method returns a {@code CompletableFuture} + * which has been completed exceptionally with {@link QuicStreamLimitException}. Else, this + * method returns a {@code CompletableFuture} which waits for that duration for a potential + * increase in the limit. If, during this period, the stream creation limit does increase and + * stream creation succeeds then the returned {@code CompletableFuture} will be completed + * successfully, else it will complete exceptionally with {@link QuicStreamLimitException}. + * + * @param limitIncreaseDuration Amount of time to wait for the bidirectional stream creation + * limit to be increased by the peer, if this connection has + * currently reached its limit + * @return a CompletableFuture which completes either with a new locally initiated + * bidirectional stream or exceptionally if the stream creation failed + */ + public abstract CompletableFuture openNewLocalBidiStream( + Duration limitIncreaseDuration); + + /** + * Creates a new locally initiated unidirectional stream. Locally created unidirectional streams + * are write-only streams. + *

        + * Creation of streams is limited to the maximum limit advertised by the peer. If a new stream + * cannot be created due to this limitation, then this method will use the + * {@code limitIncreaseDuration} to decide how long to wait for a potential increase in the + * limit. + *

        + * If the limit has been reached and the {@code limitIncreaseDuration} is not + * {@link Duration#isPositive() positive} then this method returns a {@code CompletableFuture} + * which has been completed exceptionally with {@link QuicStreamLimitException}. Else, this + * method returns a {@code CompletableFuture} which waits for that duration for a potential + * increase in the limit. If, during this period, the stream creation limit does increase and + * stream creation succeeds then the returned {@code CompletableFuture} will be completed + * successfully, else it will complete exceptionally with {@link QuicStreamLimitException}. + * + * @param limitIncreaseDuration Amount of time to wait for the unidirectional stream creation + * limit to be increased by the peer, if this connection has + * currently reached its limit + * @return a CompletableFuture which completes either with a new locally initiated + * unidirectional stream or exceptionally if the stream creation failed + */ + public abstract CompletableFuture openNewLocalUniStream( + Duration limitIncreaseDuration); + + /** + * Adds a listener that will be invoked when a remote stream is + * created. + * + * @apiNote + * The listener will be invoked with any remote streams + * already opened, and not yet acquired by another listener. + * Any stream passed to the listener is either a {@link QuicBidiStream} + * or a {@link QuicReceiverStream} depending on the + * {@linkplain QuicStreams#streamType(long) stream type} of the given + * streamId. + * The listener should return {@code true} if it wishes to acquire + * the stream. + * + * @param streamConsumer the listener + */ + public abstract void addRemoteStreamListener(Predicate streamConsumer); + + /** + * Removes a listener previously added with {@link #addRemoteStreamListener(Predicate)} + * @return {@code true} if the listener was found and removed, {@code false} otherwise + */ + public abstract boolean removeRemoteStreamListener(Predicate streamConsumer); + + /** + * {@return a stream of all currently opened {@link QuicStream} in the connection} + * + * @apiNote + * All current quic streams are included, whether local or remote, and whether they + * have been acquired or not. + * + * @see #addRemoteStreamListener(Predicate) + */ + public abstract Stream quicStreams(); + + /** + * {@return true if this connection is open} + */ + public abstract boolean isOpen(); + + /** + * {@return a long identifier that can be used to uniquely + * identify a quic connection in the context of the + * {@link QuicInstance} that created it} + */ + public long uniqueId() { return 0; } + + /** + * {@return a debug tag to be used with {@linkplain + * jdk.internal.net.http.common.Logger lower level logging}} + * This typically includes both the connection {@link #uniqueId()} + * and the {@link QuicInstance#instanceId()}. + */ + public abstract String dbgTag(); + + /** + * {@return a debug tag} + * Typically used with {@linkplain jdk.internal.net.http.common.Log + * higher level logging} + */ + public abstract String logTag(); + + /** + * {@return the {@link TerminationCause} if the connection has + * closed or is being closed, otherwise returns null} + */ + public abstract TerminationCause terminationCause(); + + public abstract QuicTLSEngine getTLSEngine(); + + public abstract InetSocketAddress peerAddress(); + + public abstract SocketAddress localAddress(); + + /** + * {@return a {@code CompletableFuture} that gets completed when + * the peer has acknowledged, or replied to the first {@link + * QuicPacket.PacketType#INITIAL INITIAL} + * packet + */ + public abstract CompletableFuture handshakeReachedPeer(); + + /** + * Requests to send a PING frame to the peer. + * An implementation may decide to support sending of out-of-band ping + * frames (triggered by the application layer) only for a subset of the + * {@linkplain jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace + * packet number spaces}. It may complete with -1 if it doesn't want to request + * sending of a ping frame at the time {@code requestSendPing()} is called. + * @return A completable future that will be completed with the number of + * milliseconds it took to get a valid response. It may also complete + * exceptionally, or with {@code -1L} if the ping was not sent. + */ + public abstract CompletableFuture requestSendPing(); + + /** + * {@return this connection {@code QuicConnectionId} or null} + * @implSpec + * The default implementation of this method returns null + */ + public QuicConnectionId localConnectionId() { return null; } + + /** + * {@return the {@link ConnectionTerminator} for this connection} + */ + public abstract ConnectionTerminator connectionTerminator(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java new file mode 100644 index 00000000000..21c40a69588 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionId.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + +/** + * Models a Quic Connection id. + * QuicConnectionId instance are typically created by a Quic client or server. + */ +// Connection IDs are used as keys in an ID to connection map. +// They implement Comparable to mitigate the penalty of hash collisions. +public abstract class QuicConnectionId implements Comparable { + + /** + * The maximum length, in bytes, of a connection id. + * This is supposed to be version-specific, but for now, we + * are going to treat that as a universal constant. + */ + public static final int MAX_CONNECTION_ID_LENGTH = 20; + protected final int hashCode; + protected final ByteBuffer buf; + + protected QuicConnectionId(ByteBuffer buf) { + this.buf = buf.asReadOnlyBuffer(); + hashCode = this.buf.hashCode(); + } + + /** + * Returns the length of this connection id, in bytes; + * @return the length of this connection id + */ + public int length() { + return buf.remaining(); + } + + /** + * Returns this connection id bytes as a read-only buffer. + * @return A new read only buffer containing this connection id bytes. + */ + public ByteBuffer asReadOnlyBuffer() { + return buf.asReadOnlyBuffer(); + } + + /** + * Returns this connection id bytes as a byte array. + * @return A new byte array containing this connection id bytes. + */ + public byte[] getBytes() { + var length = length(); + byte[] bytes = new byte[length]; + buf.get(buf.position(), bytes, 0, length); + return bytes; + } + + /** + * Compare this connection id bytes with the bytes in the + * given byte buffer. + *

        The given byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

        This method does not advance the buffer position. + * + * @implSpec This is equivalent to:

        {@code
        +     *  this.asReadOnlyBuffer().comparesTo(idbytes)
        +     *  }
        + * + * @param idbytes A byte buffer containing the id bytes of another + * connection id. + * @return {@code -1}, {@code 0}, or {@code 1} if this connection's id + * bytes are less, equal, or greater than the provided bytes. + */ + public int compareBytes(ByteBuffer idbytes) { + return buf.compareTo(idbytes); + } + + /** + * Tells whether the given byte buffer matches this connection id. + * The given byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

        This method does not advance the buffer position. + * + * @implSpec + * This is equivalent to:

        {@code
        +     *  this.asReadOnlyBuffer().mismatch(idbytes) == -1
        +     *  }
        + * + * @param idbytes A buffer that delimits a connection id. + * @return true if the bytes in the given buffer match this + * connection id bytes. + */ + public boolean matches(ByteBuffer idbytes) { + return buf.equals(idbytes); + } + + @Override + public int compareTo(QuicConnectionId o) { + return buf.compareTo(o.buf); + } + + + @Override + public final boolean equals(Object o) { + if (o instanceof QuicConnectionId that) { + return buf.equals(that.buf); + } + return false; + } + + @Override + public final int hashCode() { + return hashCode; + } + + /** + * {@return an hexadecimal string representing this connection id bytes, + * as returned by {@code HexFormat.of().formatHex(getBytes())}} + */ + public String toHexString() { + return HexFormat.of().formatHex(getBytes()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java new file mode 100644 index 00000000000..04cb2e6c263 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionIdFactory.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + +/** + * A class to generate connection ids bytes. + * This algorithm is specific to our implementation - it's not defined + * in any RFC (connection id bytes are free form). + * For the purpose of validation we encode the length of + * the connection id into the connection id bytes. + * For the purpose of uniqueness we encode a unique id. + * The rest of the connection id are random bytes. + */ +public class QuicConnectionIdFactory { + private static final Random RANDOM = new SecureRandom(); + private static final String CLIENT_DESC = "QuicClientConnectionId"; + private static final String SERVER_DESC = "QuicServerConnectionId"; + + private static final int MIN_CONNECTION_ID_LENGTH = 9; + + private final AtomicLong tokens = new AtomicLong(); + private volatile boolean wrapped; + private final byte[] scrambler; + private final Key statelessTokenKey; + private final String simpleDesc; + private final int connectionIdLength = RANDOM.nextInt(MIN_CONNECTION_ID_LENGTH, MAX_CONNECTION_ID_LENGTH+1); + + public static QuicConnectionIdFactory getClient() { + return new QuicConnectionIdFactory(CLIENT_DESC); + } + + public static QuicConnectionIdFactory getServer() { + return new QuicConnectionIdFactory(SERVER_DESC); + } + + private QuicConnectionIdFactory(String simpleDesc) { + this.simpleDesc = simpleDesc; + byte[] temp = new byte[MAX_CONNECTION_ID_LENGTH]; + RANDOM.nextBytes(temp); + scrambler = temp; + try { + KeyGenerator kg = KeyGenerator.getInstance("HmacSHA256"); + statelessTokenKey = kg.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("HmacSHA256 key generator not available", e); + } + } + + /** + * The connection ID length used by this Quic instance. + * This is the source connection id length for outgoing packets, + * and the destination connection id length for incoming packets. + * @return the connection ID length used by this instance + */ + public int connectionIdLength() { + return connectionIdLength; + } + + /** + * Creates a new connection ID for a connection. + * @return a new connection ID + */ + public QuicConnectionId newConnectionId() { + long token = newToken(); + return new QuicLocalConnectionId(token, simpleDesc, + newConnectionId(connectionIdLength, token)); + } + + /** + * Quick validation to see if the buffer can contain a connection + * id generated by this instance. The byte buffer is expected to have + * its {@linkplain ByteBuffer#position() position} set at the start + * of the connection id, and its {@linkplain ByteBuffer#limit() limit} + * at the end. In other words, {@code Buffer.remaining()} should + * indicate the connection id length. + *

        This method does not advance the buffer position, and + * returns a connection id that wraps the given buffer. + * The returned connection id is only safe to use as long as + * the buffer is not modified. + *

        It is usually only used temporarily as a lookup key + * to locate an existing {@code QuicConnection}. + * + * @param buffer A buffer that delimits a connection id. + * @return a new QuicConnectionId if the buffer can contain + * a connection id generated by this instance, {@code null} + * otherwise. + */ + public QuicConnectionId unsafeConnectionIdFor(ByteBuffer buffer) { + int expectedLength = connectionIdLength; + + int remaining = buffer.remaining(); + if (remaining < MIN_CONNECTION_ID_LENGTH) return null; + if (remaining != expectedLength) return null; + + byte first = buffer.get(0); + int len = extractConnectionIdLength(first); + if (len < MIN_CONNECTION_ID_LENGTH) return null; + if (len > MAX_CONNECTION_ID_LENGTH) return null; + if (len != expectedLength) return null; + + long token = peekConnectionIdToken(buffer); + if (!isValidToken(token)) return null; + var cid = new QuicLocalConnectionId(buffer, token, simpleDesc); + assert cid.length() == expectedLength; + return cid; + } + + /** + * Returns a stateless reset token for the given connection ID + * @param connectionId connection ID + * @return stateless reset token for the given connection ID + * @throws IllegalArgumentException if the connection ID was not generated by this factory + */ + public byte[] statelessTokenFor(QuicConnectionId connectionId) { + if (!(connectionId instanceof QuicLocalConnectionId)) { + throw new IllegalArgumentException("Not a locally-generated connection ID"); + } + Mac mac; + try { + mac = Mac.getInstance("HmacSHA256"); + mac.init(statelessTokenKey); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("HmacSHA256 is not available", e); + } + byte[] result = mac.doFinal(connectionId.getBytes()); + return Arrays.copyOf(result, 16); + } + + // visible for testing + public long newToken() { + var token = tokens.incrementAndGet(); + if (token < 0) { + token = -token - 1; + wrapped = true; + } + return token; + } + + // visible for testing + public byte[] newConnectionId(int length, long token) { + length = Math.clamp(length, MIN_CONNECTION_ID_LENGTH, MAX_CONNECTION_ID_LENGTH); + assert length <= MAX_CONNECTION_ID_LENGTH; + assert length >= MIN_CONNECTION_ID_LENGTH; + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + + if (token < 0) token = -token - 1; + assert token >= 0; + int len = variableLengthLength(token); + assert len < 8; + + bytes[0] = (byte) ((length << 3) & 0xF8); + bytes[0] = (byte) (bytes[0] | len); + assert (bytes[0] & 0x07) == len; + assert ((bytes[0] & 0xFF) >> 3) == length : + "%s != %s".formatted(bytes[0] & 0xFF, length); + int shift = 8 * len; + for (int i = 0; i <= len; i++) { + assert shift <= 56; + bytes[i + 1] = (byte) ((token >> shift) & 0xFF); + shift -= 8; + } + for (int i = 0; i < length; i++) { + bytes[i] = (byte) ((bytes[i] & 0xFF) ^ (scrambler[i] & 0xFF)); + } + + assert length == getConnectionIdLength(bytes); + assert token == getConnectionIdToken(bytes); + return bytes; + } + + // visible for testing + public int getConnectionIdLength(byte[] bytes) { + assert bytes.length >= MIN_CONNECTION_ID_LENGTH; + var length = extractConnectionIdLength(bytes[0]); + assert length <= MAX_CONNECTION_ID_LENGTH; + return length; + } + + // visible for testing + public long getConnectionIdToken(byte[] bytes) { + assert bytes.length >= MIN_CONNECTION_ID_LENGTH; + int len = extractTokenLength(bytes[0]); + long token = 0; + int shift = len * 8; + for (int i = 0; i <= len; i++) { + assert shift >= 0; + assert shift <= 56; + int j = i + 1; + long l = ((bytes[j] & 0xFF) ^ (scrambler[j] & 0xFF)) & 0xFF; + l = l << shift; + token += l; + shift -= 8; + } + assert token >= 0; + return token; + } + + private long peekConnectionIdToken(ByteBuffer bytes) { + assert bytes.remaining() >= MIN_CONNECTION_ID_LENGTH; + int len = extractTokenLength(bytes.get(0)); + long token = 0; + int shift = len * 8; + for (int i = 0; i <= len; i++) { + assert shift >= 0; + assert shift <= 56; + int j = i + 1; + long l = ((bytes.get(j) & 0xFF) ^ (scrambler[j] & 0xFF)) & 0xFF; + l = l << shift; + token += l; + shift -= 8; + } + return token; + } + + private boolean isValidToken(long token) { + if (token < 0) return false; + long prevToken = tokens.get(); + boolean wrapped = prevToken < 0 || this.wrapped; + // if `tokens` has wrapped, we can say nothing... + // otherwise, we can say it should not be coded on more bytes than + // the previous token that was distributed + if (!wrapped) { + return token <= prevToken; + } + return true; + } + + private int extractConnectionIdLength(byte b) { + var bits = ((b & 0xFF) ^ (scrambler[0] & 0xFF)) & 0xFF; + bits = bits >> 3; + return bits; + } + + private int extractTokenLength(byte b) { + var bits = ((b & 0xFF) ^ (scrambler[0] & 0xFF)) & 0xFF; + return bits & 0x07; + } + + private static int variableLengthLength(long token) { + assert token >= 0; + int len = 0; + int shift = 0; + for (int i = 1; i < 8; i++) { + shift += 8; + if ((token >> shift) == 0) break; + len++; + } + assert len < 8; + return len; + } + + /** + * Checks if {@code connId} looks like a connection ID we could possibly generate. + * If it does, returns a stateless reset datagram. + * @param connId the destination connection id that was received on the packet + * @param length maximum length of the stateless reset packet + * @return stateless reset datagram payload, or null + */ + public ByteBuffer statelessReset(ByteBuffer connId, int length) { + // 43 bytes max: + // first byte bits 01xx xxxx + // followed by random bytes + // terminated by 16 bytes reset token + length = Math.min(length, 43); + if (length < 21) { // minimum QUIC short datagram length + return null; + } + + var cid = (QuicLocalConnectionId)unsafeConnectionIdFor(connId); + if (cid != null) { + var localToken = statelessTokenFor(cid); + assert localToken != null; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.put((byte)(0x40 + RANDOM.nextInt(0x40))); + byte[] random = new byte[length - 17]; + RANDOM.nextBytes(random); + buf.put(random); + buf.put(localToken); + assert !buf.hasRemaining() : buf.remaining(); + buf.flip(); + return buf; + } + return null; + } + + // A connection id generated by this instance. + private static final class QuicLocalConnectionId extends QuicConnectionId { + private final long token; + private final String simpleDesc; + + // Connection Ids created with this constructor are safer + // to use in maps as the buffer wraps a safe byte array in + // this constructor. + private QuicLocalConnectionId(long token, String simpleDesc, byte[] bytes) { + super(ByteBuffer.wrap(bytes)); + this.token = token; + this.simpleDesc = simpleDesc; + } + + // Connection Ids created with this constructor are only + // safe to use as long as the caller abstain from mutating + // the provided byte buffer. + // Typically, they will be transiently used to look up some + // connection in a map indexed by a connection id. + private QuicLocalConnectionId(ByteBuffer buffer, long token, String simpleDesc) { + super(buffer); + assert token >= 0; + this.token = token; + this.simpleDesc = simpleDesc; + } + + @Override + public String toString() { + return "%s(length=%s, token=%s, hash=%s)" + .formatted(simpleDesc, length(), token, hashCode); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java new file mode 100644 index 00000000000..f05519d339b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java @@ -0,0 +1,4353 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.net.ConnectException; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NetworkChannel; +import java.nio.channels.UnresolvedAddressException; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.OrderedFlow.CryptoDataFlow; +import jdk.internal.net.http.quic.QuicEndpoint.QuicDatagram; +import jdk.internal.net.http.quic.QuicTransportParameters.VersionInformation; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.DataBlockedFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.MaxDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamsFrame; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.NewTokenFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.PathChallengeFrame; +import jdk.internal.net.http.quic.frames.PathResponseFrame; +import jdk.internal.net.http.quic.frames.PingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.frames.StopSendingFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.frames.StreamsBlockedFrame; +import jdk.internal.net.http.quic.packets.HandshakePacket; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.http.quic.packets.LongHeader; +import jdk.internal.net.http.quic.packets.OneRttPacket; +import jdk.internal.net.http.quic.packets.PacketSpace; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder.OutgoingQuicPacket; +import jdk.internal.net.http.quic.packets.RetryPacket; +import jdk.internal.net.http.quic.packets.VersionNegotiationPacket; +import jdk.internal.net.http.quic.streams.CryptoWriterQueue; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicBidiStreamImpl; +import jdk.internal.net.http.quic.streams.QuicConnectionStreams; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStream.StreamState; +import jdk.internal.net.http.quic.streams.QuicStreams; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.HandshakeState; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.quic.QuicVersion; + +import static jdk.internal.net.http.quic.QuicClient.INITIAL_SERVER_CONNECTION_ID_LENGTH; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.active_connection_id_limit; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_data; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_local; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_source_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.max_idle_timeout; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.max_udp_payload_size; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.version_information; +import static jdk.internal.net.http.quic.TerminationCause.forException; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; +import static jdk.internal.net.http.quic.QuicRttEstimator.MAX_PTO_BACKOFF_TIMEOUT; +import static jdk.internal.net.http.quic.QuicRttEstimator.MIN_PTO_BACKOFF_TIMEOUT; +import static jdk.internal.net.http.quic.frames.QuicFrame.MAX_VL_INTEGER; +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static jdk.internal.net.http.quic.streams.QuicStreams.isUnidirectional; +import static jdk.internal.net.http.quic.streams.QuicStreams.streamType; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +/** + * This class implements a QUIC connection. + * A QUIC connection is established between a client and a server over a + * QuicEndpoint endpoint. + * A QUIC connection can then multiplex multiple QUIC streams to the same server. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +public class QuicConnectionImpl extends QuicConnection implements QuicPacketReceiver { + + private static final int MAX_IPV6_MTU = 65527; + private static final int MAX_IPV4_MTU = 65507; + + // Quic assumes a minimum packet size of 1200 + // See https://www.rfc-editor.org/rfc/rfc9000#name-datagram-size + public static final int SMALLEST_MAXIMUM_DATAGRAM_SIZE = + QuicClient.SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + public static final int DEFAULT_MAX_INITIAL_TIMEOUT = Math.clamp( + Utils.getIntegerProperty("jdk.httpclient.quic.maxInitialTimeout", 30), + 1, Integer.MAX_VALUE); + public static final long DEFAULT_INITIAL_MAX_DATA = Math.clamp( + Utils.getLongProperty("jdk.httpclient.quic.maxInitialData", 15 << 20), + 0, 1L << 60); + public static final long DEFAULT_INITIAL_STREAM_MAX_DATA = Math.clamp( + Utils.getIntegerProperty("jdk.httpclient.quic.maxStreamInitialData", 6 << 20), + 0, 1L << 60); + public static final int DEFAULT_MAX_BIDI_STREAMS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxBidiStreams", 100); + public static final int DEFAULT_MAX_UNI_STREAMS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxUniStreams", 100); + public static final boolean USE_DIRECT_BUFFER_POOL = Utils.getBooleanProperty( + "jdk.internal.httpclient.quic.poolDirectByteBuffers", !QuicEndpoint.DGRAM_SEND_ASYNC); + + public static final int RESET_TOKEN_LENGTH = 16; // RFC states 16 bytes for stateless token + public static final long MAX_STREAMS_VALUE_LIMIT = 1L << 60; // cannot exceed 2^60 as per RFC + + // VarHandle provide the same atomic compareAndSet functionality + // than AtomicXXXXX classes, but without the additional cost in + // footprint. + private static final VarHandle VERSION_NEGOTIATED; + private static final VarHandle STATE; + private static final VarHandle MAX_SND_DATA; + private static final VarHandle MAX_RCV_DATA; + public static final int DEFAULT_DATAGRAM_SIZE; + private static final int MAX_INCOMING_CRYPTO_CAPACITY = 64 << 10; + + static { + try { + Lookup lookup = MethodHandles.lookup(); + VERSION_NEGOTIATED = lookup + .findVarHandle(QuicConnectionImpl.class, "versionNegotiated", boolean.class); + STATE = lookup.findVarHandle(QuicConnectionImpl.class, "state", int.class); + MAX_SND_DATA = lookup.findVarHandle(OneRttFlowControlledSendingQueue.class, "maxData", long.class); + MAX_RCV_DATA = lookup.findVarHandle(OneRttFlowControlledReceivingQueue.class, "maxData", long.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + int size = Utils.getIntegerProperty("jdk.httpclient.quic.defaultMTU", + SMALLEST_MAXIMUM_DATAGRAM_SIZE); + // don't allow the value to be below 1200 and above 65527, to conform with RFC-9000, + // section 18.2: + // The default for this parameter is the maximum permitted UDP payload of 65527. + // Values below 1200 are invalid. + if (size < SMALLEST_MAXIMUM_DATAGRAM_SIZE || size > MAX_IPV6_MTU) { + // fallback to SMALLEST_MAXIMUM_DATAGRAM_SIZE + size = SMALLEST_MAXIMUM_DATAGRAM_SIZE; + } + DEFAULT_DATAGRAM_SIZE = size; + } + + protected final Logger debug = Utils.getDebugLogger(this::dbgTag); + + final QuicRttEstimator rttEstimator = new QuicRttEstimator(); + final QuicCongestionController congestionController; + /** + * The state of the quic connection. + * The handshake is confirmed when HANDSHAKE_DONE has been received, + * or when the first 1-RTT packet has been successfully decrypted. + * See RFC 9001 section 4.1.2 + * https://www.rfc-editor.org/rfc/rfc9001#name-handshake-confirmed + */ + private final StateHandle stateHandle = new StateHandle(); + private final AtomicBoolean startHandshakeCalled = new AtomicBoolean(); + private final InetSocketAddress peerAddress; + private final QuicInstance quicInstance; + private final String dbgTag; + private final QuicTLSEngine quicTLSEngine; + private final CodingContext codingContext; + private final PacketSpaces packetSpaces; + private final OneRttFlowControlledSendingQueue oneRttSndQueue = + new OneRttFlowControlledSendingQueue(); + private final OneRttFlowControlledReceivingQueue oneRttRcvQueue = + new OneRttFlowControlledReceivingQueue(this::logTag); + protected final QuicConnectionStreams streams; + protected final Queue outgoing1RTTFrames = new ConcurrentLinkedQueue<>(); + // for one-rtt crypto data (session tickets) + private final CryptoDataFlow peerCryptoFlow = new CryptoDataFlow(); + private final CryptoWriterQueue localCryptoFlow = new CryptoWriterQueue(); + private final HandshakeFlow handshakeFlow = new HandshakeFlow(); + final ConnectionTerminatorImpl terminator; + protected final IdleTimeoutManager idleTimeoutManager; + protected final QuicTransportParameters transportParams; + // the initial (local) connection ID + private final QuicConnectionId connectionId; + private final PeerConnIdManager peerConnIdManager; + private final LocalConnIdManager localConnIdManager; + private volatile QuicConnectionId incomingInitialPacketSourceId; + protected final QuicEndpoint endpoint; + private volatile QuicTransportParameters localTransportParameters; + private volatile QuicTransportParameters peerTransportParameters; + private volatile byte[] initialToken; + // the number of (active) connection ids the peer is willing to accept for a given connection + private volatile long peerActiveConnIdsLimit = 2; // default is 2 as per RFC + + private volatile int state; + // the quic version currently in use + private volatile QuicVersion quicVersion; + // the quic version from the first packet + private final QuicVersion originalVersion; + private volatile QuicPacketDecoder decoder; + private volatile QuicPacketEncoder encoder; + // (client-only) if true, we no longer accept VERSIONS packets + private volatile boolean versionCompatible; + // if true, we no longer accept version changes + private volatile boolean versionNegotiated; + // true if we changed version in response to VERSIONS packet + private volatile boolean processedVersionsPacket; + // start off with 1200 or whatever is configured through + // jdk.net.httpclient.quic.defaultPDU system property + private int maxPeerAdvertisedPayloadSize = DEFAULT_DATAGRAM_SIZE; + // max MTU size on the connection: either MAX_IPV4_MTU or MAX_IPV6_MTU, + // depending on whether the peer address is IPv6 or IPv4 + private final int maxConnectionMTU; + // we start with a pathMTU that is 1200 or whatever is configured through + // jdk.net.httpclient.quic.defaultPDU system property + private int pathMTU = DEFAULT_DATAGRAM_SIZE; + private final SequentialScheduler handshakeScheduler = + SequentialScheduler.lockingScheduler(this::continueHandshake0); + private final ReentrantLock handshakeLock = new ReentrantLock(); + private final String cachedToString; + private final String logTag; + private final long labelId; + // incoming PATH_CHALLENGE frames waiting for PATH_RESPONSE + private final Queue pathChallengeFrameQueue = new ConcurrentLinkedQueue<>(); + + private volatile MaxInitialTimer maxInitialTimer; + + static String dbgTag(QuicInstance quicInstance, String logTag) { + return String.format("QuicConnection(%s, %s)", + quicInstance.instanceId(), logTag); + } + + protected QuicConnectionImpl(final QuicVersion firstFlightVersion, + final QuicInstance quicInstance, + final InetSocketAddress peerAddress, + final String peerName, + final int peerPort, + final SSLParameters sslParameters, + final String logTagFormat, + final long labelId) { + this.labelId = labelId; + this.quicInstance = Objects.requireNonNull(quicInstance, "quicInstance"); + try { + this.endpoint = quicInstance.getEndpoint(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + this.peerAddress = peerAddress; + this.maxConnectionMTU = peerAddress.getAddress() instanceof Inet6Address + ? MAX_IPV6_MTU + : MAX_IPV4_MTU; + this.pathMTU = Math.clamp(DEFAULT_DATAGRAM_SIZE, SMALLEST_MAXIMUM_DATAGRAM_SIZE, maxConnectionMTU); + this.cachedToString = String.format(logTagFormat.formatted("quic:%s:%s:%s"), labelId, + Arrays.toString(sslParameters.getApplicationProtocols()), peerAddress); + this.connectionId = this.endpoint.idFactory().newConnectionId(); + this.logTag = logTagFormat.formatted(labelId); + this.dbgTag = dbgTag(quicInstance, logTag); + this.congestionController = new QuicRenoCongestionController(dbgTag); + this.originalVersion = this.quicVersion = firstFlightVersion == null + ? QuicVersion.firstFlightVersion(quicInstance.getAvailableVersions()) + : firstFlightVersion; + final boolean isClientConn = isClientConnection(); + this.peerConnIdManager = new PeerConnIdManager(this, dbgTag); + this.localConnIdManager = new LocalConnIdManager(this, dbgTag, connectionId); + this.decoder = QuicPacketDecoder.of(this.quicVersion); + this.encoder = QuicPacketEncoder.of(this.quicVersion); + this.codingContext = new QuicCodingContext(); + final QuicTLSEngine engine = this.quicInstance.getQuicTLSContext() + .createEngine(peerName, peerPort); + engine.setUseClientMode(isClientConn); + engine.setSSLParameters(sslParameters); + this.quicTLSEngine = engine; + quicTLSEngine.setRemoteQuicTransportParametersConsumer(this::consumeQuicParameters); + packetSpaces = PacketSpaces.forConnection(this); + quicTLSEngine.setOneRttContext(packetSpaces.getOneRttContext()); + streams = new QuicConnectionStreams(this, debug); + if (quicInstance instanceof QuicClient quicClient) { + // use the (INITIAL) token that a server might have sent to this client (through + // NEW_TOKEN frame) on a previous connection against that server + this.initialToken = quicClient.initialTokenFor(this.peerAddress); + } + terminator = new ConnectionTerminatorImpl(this); + idleTimeoutManager = new IdleTimeoutManager(this); + transportParams = quicInstance.getTransportParameters() == null + ? new QuicTransportParameters() + : quicInstance.getTransportParameters(); + if (debug.on()) debug.log("Quic Connection Created"); + } + + @Override + public final long uniqueId() { + return labelId; + } + + /** + * An abstraction to represent the connection state as a bit mask. + * This is not an enum as some stages can overlap. + */ + public abstract static class QuicConnectionState { + public static final int + NEW = 0, // the connection is new + HISENT = 1, // first initial hello packet sent + HSCOMPLETE = 16, // handshake completed + CLOSING = 128, // connection has entered "Closing" state as defined in RFC-9000 + DRAINING = 256, // connection has entered "Draining" state as defined in RFC-9000 + CLOSED = 512; // CONNECTION_CLOSE ACK sent or received + public abstract int state(); + public boolean helloSent() {return isMarked(HISENT);} + public boolean handshakeComplete() { return isMarked(HSCOMPLETE);} + public boolean closing() { return isMarked(CLOSING);} + public boolean draining() { return isMarked(DRAINING);} + public boolean opened() { return (state() & (CLOSED | DRAINING | CLOSING)) == 0; } + public boolean isMarked(int mask) { return isMarked(state(), mask); } + public String toString() { return toString(state()); } + public static boolean isMarked(int state, int mask) { + return mask == 0 ? state == 0 : (state & mask) == mask; + } + public static String toString(int state) { + if (state == NEW) return "new"; + if (isMarked(state, CLOSED)) return "closed"; + if (isMarked(state, DRAINING)) return "draining"; + if (isMarked(state, CLOSING)) return "closing"; + if (isMarked(state, HSCOMPLETE)) return "handshakeComplete"; + if (isMarked(state, HISENT)) return "helloSent"; + return "Unknown(" + state + ")"; + } + } + + /** + * A {link QuicTimedEvent} used to interrupt the handshake + * if no response to the first initial packet is received within + * a reasonable delay (default is ~ 30s). + * This avoids waiting more than 30s for ConnectionException + * to be raised if no server is available at the peer address. + * This class is only used on the client side. + */ + final class MaxInitialTimer implements QuicTimedEvent { + private final Deadline maxInitialDeadline; + private final QuicTimerQueue timerQueue; + private final long eventId; + private volatile Deadline deadline; + private volatile boolean initialPacketReceived; + private volatile boolean connectionClosed; + + // optimization: if done is true it avoids volatile read + // of initialPacketReceived and/or connectionClosed + // from initialPacketReceived() + private boolean done; + private MaxInitialTimer(QuicTimerQueue timerQueue, Deadline maxDeadline) { + this.eventId = QuicTimerQueue.newEventId(); + this.timerQueue = timerQueue; + maxInitialDeadline = deadline = maxDeadline; + assert isClientConnection() : "MaxInitialTimer should only be used on QuicClients"; + } + + /** + * Called when an initial packet is received from the + * peer. At this point the MaxInitialTimer is disarmed, + * and further calls to this method are no-op. + */ + void initialPacketReceived() { + if (done) return; // races are OK - avoids volatile read + boolean firsPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + if (done = (firsPacketReceived || closed)) return; + initialPacketReceived = true; + if (debug.on()) { + debug.log("Quic initial timer disarmed after %s seconds", + DEFAULT_MAX_INITIAL_TIMEOUT - + Deadline.between(now(), maxInitialDeadline).toSeconds()); + } + if (!closed) { + // rescheduling with Deadline.MAX will take the + // MaxInitialTimer out of the timer queue. + timerQueue.reschedule(this, Deadline.MAX); + } + } + + @Override + public Deadline deadline() { + return deadline; + } + + /** + * This method is called if the timer expires. + * If no initial packet has been received ( + * {@link #initialPacketReceived()} was never called), + * the connection's handshakeCF is completed with a + * {@link ConnectException}. + * Calling this method a second time is a no-op. + * @return {@link Deadline#MAX}, always. + */ + @Override + public Deadline handle() { + if (done) return Deadline.MAX; + boolean firsPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + if (!firsPacketReceived && !closed) { + assert !now().isBefore(maxInitialDeadline); + var connectException = new ConnectException("No response from peer for %s seconds" + .formatted(DEFAULT_MAX_INITIAL_TIMEOUT)); + if (QuicConnectionImpl.this.handshakeFlow.handshakeCF() + .completeExceptionally(connectException)) { + // abandon the connection, but sends ConnectionCloseFrame + TerminationCause cause = TerminationCause.forException( + new QuicTransportException(connectException.getMessage(), + KeySpace.INITIAL, 0, QuicTransportErrors.APPLICATION_ERROR)); + terminator.terminate(cause); + } + connectionClosed = done = closed = true; + } + assert firsPacketReceived || closed; + return Deadline.MAX; + } + + @Override + public long eventId() { + return eventId; + } + + @Override + public Deadline refreshDeadline() { + boolean firstPacketReceived = initialPacketReceived; + boolean closed = connectionClosed; + Deadline newDeadlne = deadline; + if (closed || firstPacketReceived) newDeadlne = deadline = Deadline.MAX; + return newDeadlne; + } + + private Deadline now() { + return QuicConnectionImpl.this.endpoint().timeSource().instant(); + } + } + + /** + * A state handle is a mutable implementation of {@link QuicConnectionState} + * that allows to view the volatile connection int variable {@code state} as + * a {@code QuicConnectionState}, and provides methods to mutate it in + * a thread safe atomic way. + */ + protected final class StateHandle extends QuicConnectionState { + public int state() { return state;} + + /** + * Updates the state to a new state value with the passed bit {@code mask} set. + * + * @param mask The state mask + * @return true if previously the state value didn't have the {@code mask} set and this + * method successfully updated the state value to set the {@code mask} + */ + final boolean mark(final int mask) { + int state, desired; + do { + state = desired = state(); + if ((state & mask) == mask) return false; // already set + desired = state | mask; + } while (!STATE.compareAndSet(QuicConnectionImpl.this, state, desired)); + return true; // compareAndSet switched the old state to the desired state + } + public boolean markHelloSent() { return mark(HISENT); } + public boolean markHandshakeComplete() { return mark(HSCOMPLETE); } + } + + /** + * Keeps track of: + * - handshakeCF the handshake completable future + * - localInitial the local initial crypto writer queue + * - peerInitial the peer initial crypto flow + * - localHandshake the local handshake crypto queue + * - peerHandshake the peer handshake crypto flow + */ + protected final class HandshakeFlow { + + private final CompletableFuture handshakeCF; + // a CompletableFuture which will get completed when the handshake initiated locally, + // has "reached" the peer i.e. when the peer acknowledges or replies to the first + // INITIAL packet sent by an endpoint + final CompletableFuture handshakeReachedPeerCF; + private final CryptoWriterQueue localInitial = new CryptoWriterQueue(); + private final CryptoDataFlow peerInitial = new CryptoDataFlow(); + private final CryptoWriterQueue localHandshake = new CryptoWriterQueue(); + private final CryptoDataFlow peerHandshake = new CryptoDataFlow(); + private final AtomicBoolean handshakeStarted = new AtomicBoolean(); + + private HandshakeFlow() { + this.handshakeCF = new MinimalFuture<>(); + this.handshakeReachedPeerCF = new MinimalFuture<>(); + // ensure that the handshakeReachedPeerCF gets completed exceptionally + // if an exception is raised before the first INITIAL packet is + // acked by the peer. + handshakeCF.whenComplete((r, t) -> { + if (Log.quicHandshake()) { + Log.logQuic("{0} handshake completed {1}", + logTag(), + t == null ? "successfully" : ("exceptionally: " + t)); + } + if (t != null) { + handshakeReachedPeerCF.completeExceptionally(t); + } + }); + } + + /** + * {@return the CompletableFuture representing a handshake} + */ + public CompletableFuture handshakeCF() { + return this.handshakeCF; + } + + public void failHandshakeCFs(final Throwable cause) { + assert cause != null : "missing cause when failing handshake CFs"; + SSLHandshakeException sslHandshakeException = null; + if (!handshakeCF.isDone()) { + sslHandshakeException = sslHandshakeException(cause); + handshakeCF.completeExceptionally(sslHandshakeException); + } + if (!handshakeReachedPeerCF.isDone()) { + if (sslHandshakeException == null) { + sslHandshakeException = sslHandshakeException(cause); + } + handshakeReachedPeerCF.completeExceptionally(sslHandshakeException); + } + } + + private SSLHandshakeException sslHandshakeException(final Throwable cause) { + if (cause instanceof SSLHandshakeException ssl) { + return ssl; + } + return new SSLHandshakeException("QUIC connection establishment failed", cause); + } + + /** + * Marks the start of a handshake. + * @throws IllegalStateException If handshake has already started + */ + private void markHandshakeStart() { + if (!handshakeStarted.compareAndSet(false, true)) { + throw new IllegalStateException("Handshake has already started on " + + QuicConnectionImpl.this); + } + } + } + + public record PacketSpaces(PacketSpace initial, PacketSpace handshake, PacketSpace app) { + public PacketSpace get(PacketNumberSpace pnspace) { + return switch (pnspace) { + case INITIAL -> initial(); + case HANDSHAKE -> handshake(); + case APPLICATION -> app(); + default -> throw new IllegalArgumentException(String.valueOf(pnspace)); + }; + } + + private QuicOneRttContext getOneRttContext() { + final var appPacketSpaceMgr = app(); + assert appPacketSpaceMgr instanceof QuicOneRttContext + : "unexpected 1-RTT packet space manager"; + return (QuicOneRttContext) appPacketSpaceMgr; + } + + public static PacketSpaces forConnection(final QuicConnectionImpl connection) { + final var initialPktSpaceMgr = new PacketSpaceManager(connection, PacketNumberSpace.INITIAL); + return new PacketSpaces(initialPktSpaceMgr, + new PacketSpaceManager.HandshakePacketSpaceManager(connection, initialPktSpaceMgr), + new PacketSpaceManager.OneRttPacketSpaceManager(connection)); + } + + public void close() { + initial.close(); + handshake.close(); + app.close(); + } + } + + private final ConcurrentLinkedQueue incoming = new ConcurrentLinkedQueue<>(); + private final SequentialScheduler incomingLoopScheduler = + SequentialScheduler.lockingScheduler(this::incoming); + + + /* + * delegate handling of the datagrams to the executor to free up + * the endpoint readLoop. Helps with processing ACKs in a more + * timely fashion, which avoids too many retransmission. + * The endpoint readLoop runs on a single thread, while this loop + * will have one thread per connection which helps with a better + * utilization of the system resources. + */ + private void scheduleForDecryption(IncomingDatagram datagram) { + // Processes an incoming encrypted packet that has just been + // read off the network. + var received = datagram.buffer.remaining(); + if (incomingLoopScheduler.isStopped()) { + if (debug.on()) { + debug.log("scheduleForDecryption closed: dropping datagram (%d bytes)", + received); + } + return; + } + if (debug.on()) { + debug.log("scheduleForDecryption: %d bytes", received); + } + endpoint.buffer(received); + incoming.add(datagram); + + incomingLoopScheduler.runOrSchedule(quicInstance().executor()); + } + + private void incoming() { + try { + IncomingDatagram datagram; + while ((datagram = incoming.poll()) != null) { + ByteBuffer buffer = datagram.buffer; + int remaining = buffer.remaining(); + try { + if (incomingLoopScheduler.isStopped()) { + // we still need to unbuffer, continue here will + // ensure we skip directly to the finally-block + // below. + continue; + } + + internalProcessIncoming(datagram.source(), + datagram.destConnId(), + datagram.headersType(), + datagram.buffer()); + } catch (Throwable t) { + if (Log.errors() || debug.on()) { + String msg = "Failed to process datagram: " + t; + Log.logError(logTag() + " " + msg); + debug.log(msg, t); + } + } finally { + endpoint.unbuffer(remaining); + } + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * Schedule an incoming quic packet for decryption. + * The ByteBuffer should contain a single packet, and its + * limit should be set at the end of the packet. + * + * @param buffer a byte buffer containing the incoming packet + */ + private void decrypt(ByteBuffer buffer) { + // Processes an incoming encrypted packet that has just been + // read off the network. + PacketType packetType = decoder.peekPacketType(buffer); + var received = buffer.remaining(); + var pos = buffer.position(); + if (debug.on()) { + debug.log("decrypt %s(pos=%d, remaining=%d)", + packetType, pos, received); + } + try { + assert packetType != PacketType.VERSIONS; + var packet = codingContext.parsePacket(buffer); + if (packet != null) { + processDecrypted(packet); + } else { + if (packetType == PacketType.HANDSHAKE) { + packetSpaces.initial.fastRetransmit(); + } + } + } catch (QuicTransportException qte) { + // close the connection on this fatal error + if (Log.errors() || debug.on()) { + final String msg = "closing connection due to error while decoding" + + " packet (type=" + packetType + "): " + qte; + Log.logError(logTag() + " " + msg); + debug.log(msg, qte); + } + terminator.terminate(TerminationCause.forException(qte)); + } catch (Throwable t) { + if (Log.errors() || debug.on()) { + String msg = "Failed to decode packet (type=" + packetType + "): " + t; + Log.logError(logTag() + " " + msg); + debug.log(msg, t); + } + } + } + + public void closeIncoming() { + incomingLoopScheduler.stop(); + IncomingDatagram icd; + // we still need to unbuffer all datagrams in the queue + while ((icd = incoming.poll()) != null ) { + endpoint.unbuffer(icd.buffer().remaining()); + } + } + + /** + * A protection record contains a packet to encrypt, and a datagram that may already + * contain encrypted packets. The firstPacketOffset indicates the position of the + * first encrypted packet in the datagram. The packetOffset indicates the position + * at which this packet will be - or has been - written in the datagram. + * Before the packet is encrypted and written to the datagram, the packetOffset + * should be the same as the datagram buffer position. + * After the packet has been written, the packetOffset should indicate + * at which position the packet has been written. The datagram position + * indicates where to write the next packet. + *

        + * Additionally, a {@code ProtectionRecord} may carry some flags indicating the + * intended usage of the datagram. The following flags are supported: + *

          + *
        • {@link #SINGLE_PACKET}: the default - it is not expected that the + * datagram will contain more packets
        • + *
        • {@link #COALESCED}: should be used if it is expected that the + * datagram will contain more than one packet
        • + *
        • {@link #LAST_PACKET}: should be used in conjunction with {@link #COALESCED} + * to indicate that the packet being protected is the last that will be + * added to the datagram
        • + *
        + * + * @apiNote + * Flag values can be combined, but some combinations + * may not make sense. A single packet can also be identified as any + * packet that doesn't have the {@code COALESCED} bit on. + * The flag is used to convey information that may be used to figure + * out whether to send the datagram right away, or whether to wait for + * more packet to be coalesced inside it. + * + * @param packet the packet to encrypt + * @param datagram the datagram in which the encrypted packet should be written + * @param firstPacketOffset the position of the first encrypted packet in the datagram + * @param packetOffset the offset at which the packet should be / has been written in the datagram + * @param flags a bit mask containing some details about the datagram being sent out + */ + public record ProtectionRecord(QuicPacket packet, ByteBuffer datagram, + int firstPacketOffset, int packetOffset, + long retransmittedPacketNumber, int flags) { + /** + * This is the default. + * This protection record is adding a single packet to be sent into + * the datagram and the datagram can be sent as soon as the packet + * has been encrypted. + */ + public static final int SINGLE_PACKET = 0; + /** + * This can be used when it is expected that more than one packet + * will be added to this datagram. We should wait until the last packet + * has been added before sending the datagram out. + */ + public static final int COALESCED = 1; + /** + * This protection record is adding the last packet to be sent into + * the datagram and the datagram can be sent as soon as the packet + * has been encrypted. + */ + public static final int LAST_PACKET = 2; + + // indicate that the packet is not retransmitted + private static final long NOT_RETRANSMITTED = -1L; + + ProtectionRecord withOffset(int packetOffset) { + if (this.packetOffset == packetOffset) { + return this; + } + return new ProtectionRecord(packet, datagram, firstPacketOffset, + packetOffset, retransmittedPacketNumber, flags); + } + + public ProtectionRecord encrypt(final CodingContext codingContext) + throws QuicKeyUnavailableException, QuicTransportException { + final PacketType packetType = packet.packetType(); + assert packetType != PacketType.VERSIONS; + // keep track of position before encryption + final int preEncryptPos = datagram.position(); + codingContext.writePacket(packet, datagram); + final ProtectionRecord encrypted = withOffset(preEncryptPos); + return encrypted; + } + + /** + * Records the intent of protecting a packet that will be sent as soon + * as it has been encrypted, without waiting for more packets to be + * coalesced into the datagram. + * + * @param packet the packet to protect + * @param allocator an allocator to allocate the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord single(QuicPacket packet, + Function allocator) { + ByteBuffer datagram = allocator.apply(packet); + int offset = datagram.position(); + return new ProtectionRecord(packet, datagram, + offset, offset, NOT_RETRANSMITTED, 0); + } + + /** + * Records the intent of protecting a packet that retransmits + * a previously transmitted packet. The packet will be sent as soon + * as it has been encrypted, without waiting for more packets to be + * coalesced into the datagram. + * + * @param packet the packet to protect + * @param retransmittedPacketNumber the packet number of the original + * packet that was considered lost + * @param allocator an allocator to allocate the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord retransmitting(QuicPacket packet, + long retransmittedPacketNumber, + Function allocator) { + ByteBuffer datagram = allocator.apply(packet); + int offset = datagram.position(); + return new ProtectionRecord(packet, datagram, offset, offset, + retransmittedPacketNumber, 0); + } + + /** + * Records the intent of protecting a packet that will be followed by + * more packets to be coalesced in the same datagram. The datagram + * should not be sent until the last packet has been coalesced. + * + * @param packet the packet to protect + * @param datagram the datagram in which packet will be coalesced + * @param firstPacketOffset the offset of the first packet in the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord more(QuicPacket packet, ByteBuffer datagram, int firstPacketOffset) { + return new ProtectionRecord(packet, datagram, firstPacketOffset, + datagram.position(), NOT_RETRANSMITTED, COALESCED); + } + + /** + * Records the intent of protecting the last packet that will be + * coalesced in the given datagram. The datagram can be sent as soon + * as the packet has been encrypted and coalesced into the given + * datagram. + * + * @param packet the packet to protect + * @param datagram the datagram in which packet will be coalesced + * @param firstPacketOffset the offset of the first packet in the datagram + * @return a protection record to submit for packet protection + */ + public static ProtectionRecord last(QuicPacket packet, ByteBuffer datagram, int firstPacketOffset) { + return new ProtectionRecord(packet, datagram, firstPacketOffset, + datagram.position(), NOT_RETRANSMITTED, LAST_PACKET | COALESCED); + } + } + + final QuicPacket newQuicPacket(final KeySpace keySpace, final List frames) { + final PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.of(keySpace)); + return encoder.newOutgoingPacket(keySpace, packetSpace, + localConnectionId(), peerConnectionId(), initialToken(), + frames, + codingContext); + } + + /** + * Encrypt an outgoing quic packet. + * The ProtectionRecord indicates the position at which the encrypted packet + * should be written in the datagram, as well as the position of the + * first packet in the datagram. After encrypting the packet, this method calls + * {@link #pushEncryptedDatagram(ProtectionRecord)} + * + * @param protectionRecord a record containing a quic packet to encrypt, + * a destination byte buffer, and various offset information. + */ + final void pushDatagram(final ProtectionRecord protectionRecord) + throws QuicKeyUnavailableException, QuicTransportException { + final QuicPacket packet = protectionRecord.packet(); + if (debug.on()) { + debug.log("encrypting packet into datagram %s(pn:%s, %s)", packet.packetType(), + packet.packetNumber(), packet.frames()); + } + // Processes an outgoing unencrypted packet that needs to be + // encrypted before being packaged in a datagram. + final ProtectionRecord encrypted; + try { + encrypted = protectionRecord.encrypt(codingContext); + } catch (Throwable e) { + // release the datagram ByteBuffer on failure to encrypt + datagramDiscarded(new QuicDatagram(this, peerAddress, protectionRecord.datagram())); + if (Log.errors()) { + Log.logError("Failed to encrypt packet: " + e); + // certain failures like key not being available are OK + // in some situations. log the stacktrace only if this + // was an unexpected failure. + boolean skipStackTrace = false; + if (e instanceof QuicKeyUnavailableException) { + final PacketSpace packetSpace = packetSpace(protectionRecord.packet().numberSpace()); + skipStackTrace = packetSpace.isClosed(); + } + if (!skipStackTrace) { + Log.logError(e); + } + } + throw e; + } + // we currently don't support a ProtectionRecord with more than one QuicPacket + assert (encrypted.flags & ProtectionRecord.COALESCED) == 0 : "coalesced packets not supported"; + // encryption of the datagram is complete, now push the encrypted + // datagram through the endpoint + if (Log.quicPacketOutLoggable(packet)) { + Log.logQuicPacketOut(logTag(), packet); + } + pushEncryptedDatagram(encrypted); + } + + protected void completeHandshakeCF() { + // This can be called from the decrypt loop, and can trigger + // sending of 1-RTT application data from within the same + // thread: we use an executor here to avoid running the application + // sending loop from within the Quic decrypt loop. + completeHandshakeCF(quicInstance().executor()); + } + + protected final void completeHandshakeCF(Executor executor) { + final var handshakeCF = handshakeFlow.handshakeCF(); + if (handshakeCF.isDone()) { + return; + } + var handshakeState = quicTLSEngine.getHandshakeState(); + if (executor != null) { + handshakeCF.completeAsync(() -> handshakeState, executor); + } else { + handshakeCF.complete(handshakeState); + } + } + + /** + * A class used to check that 1-RTT received data doesn't exceed + * the MAX_DATA of the connection + */ + class OneRttFlowControlledReceivingQueue { + private static final long MIN_BUFFER_SIZE = 16L << 10; // 16k + private volatile long receivedData; + private volatile long maxData; + private volatile long processedData; + // Desired buffer size; used when updating maxStreamData + private final long desiredBufferSize = Math.clamp(DEFAULT_INITIAL_MAX_DATA, MIN_BUFFER_SIZE, MAX_VL_INTEGER); + private final Supplier logTag; + + OneRttFlowControlledReceivingQueue(Supplier logTag) { + this.logTag = Objects.requireNonNull(logTag); + } + + /** + * Called when new local parameters are available + * @param localParameters the new local paramaters + */ + void newLocalParameters(QuicTransportParameters localParameters) { + if (localParameters.isPresent(ParameterId.initial_max_data)) { + long maxData = this.maxData; + long newMaxData = localParameters.getIntParameter(ParameterId.initial_max_data); + while (maxData < newMaxData) { + if (MAX_RCV_DATA.compareAndSet(this, maxData, newMaxData)) break; + maxData = this.maxData; + } + } + } + + /** + * Checks whether the give frame would cause the connection max data + * to be exceeded. If no, increase the amount of data processed by + * this connection by the length of the frame. If yes, sends a + * ConnectionCloseFrame with FLOW_CONTROL_ERROR. + * + * @param diff number of bytes newly received + * @param frameType type of frame received + * @throws QuicTransportException if processing this frame would cause the connection + * max data to be exceeded + */ + void checkAndIncreaseReceivedData(long diff, long frameType) throws QuicTransportException { + assert diff > 0; + long max, processed; + boolean exceeded; + synchronized (this) { + max = maxData; + processed = receivedData; + if (max - processed < diff) { + exceeded = true; + } else { + try { + receivedData = processed = Math.addExact(processed, diff); + exceeded = false; + } catch (ArithmeticException x ) { + // should not happen - flow control should have + // caught that + receivedData = processed = Long.MAX_VALUE; + exceeded = true; + } + } + } + if (exceeded) { + String reason = "Connection max data exceeded: max data processed=%s, max connection data=%s" + .formatted(processed, max); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, frameType, QuicTransportErrors.FLOW_CONTROL_ERROR); + } + } + + public void increaseProcessedData(long diff) { + long processed, received, max; + synchronized (this) { + processed = processedData += diff; + received = receivedData; + max = maxData; + } + if (Log.quicProcessed()) { + Log.logQuic(logTag()+ " Processed: " + processed + + ", received: " + received + + ", max:" + max); + } + if (needSendMaxData()) { + runAppPacketSpaceTransmitter(); + } + } + + private long bumpMaxData() { + long newMaxData = processedData + desiredBufferSize; + long maxData = this.maxData; + if (newMaxData - maxData < (desiredBufferSize / 5)) { + return 0; + } + while (maxData < newMaxData) { + if (MAX_RCV_DATA.compareAndSet(this, maxData, newMaxData)) + return newMaxData; + maxData = this.maxData; + } + return 0; + } + + public boolean needSendMaxData() { + return maxData - processedData < desiredBufferSize/2; + } + + String logTag() { return logTag.get(); } + } + + /** + * An event loop triggered when stream data is available for sending. + * We use a sequential scheduler here to make sure we don't send + * more data than allowed by the connection's flow control. + * This guarantee that only one thread composes flow controlled + * OneRTT packets at a given time, which in turn guarantees that the + * credit computed at the beginning of the loop will still be + * available after the packet has been composed. + */ + class OneRttFlowControlledSendingQueue { + private volatile long dataProcessed; + private volatile long maxData; + + /** + * Called when a MAX_DATA frame is received. + * This method is a no-op if the given value is less than the + * current max stream data for the connection. + * + * @param maxData the maximum data offset that the peer is prepared + * to accept for the whole connection + * @param isInitial true when processing transport parameters, + * false when processing MaxDataFrame + * @return the actual max data after taking the given value into account + */ + public long setMaxData(long maxData, boolean isInitial) { + long max; + long processed; + boolean wasblocked, unblocked = false; + do { + synchronized (this) { + max = this.maxData; + processed = dataProcessed; + } + wasblocked = max <= processed; + if (max < maxData) { + if (MAX_SND_DATA.compareAndSet(this, max, maxData)) { + max = maxData; + unblocked = (wasblocked && max > processed); + } + } + } while (max < maxData); + if (unblocked && !isInitial) { + packetSpaces.app.runTransmitter(); + } + return max; + } + + /** + * {@return the remaining credit for this connection} + */ + public long credit() { + synchronized (this) { + return maxData - dataProcessed; + } + } + + // We can continue sending if we have credit and data is available to send + private boolean canSend() { + return credit() > 0 && streams.hasAvailableData() + || streams.hasControlFrames() + || hasQueuedFrames() + || oneRttRcvQueue.needSendMaxData(); + } + + // implementation of the sending loop. + private boolean send1RTTData() { + Throwable failure; + try { + return doSend1RTTData(); + } catch (Throwable t) { + failure = t; + } + if (failure instanceof QuicKeyUnavailableException qkue) { + if (!QuicConnectionImpl.this.stateHandle().opened()) { + // connection is already being closed and that explains the + // key unavailability (they might have been discarded). just log + // and return + if (debug.on()) { + debug.log("failed to send stream data, reason: " + qkue.getMessage()); + } + return false; + } + // connection is still open but a key unavailability exception was raised. + // close the connection and use an IOException instead of the internal + // QuicKeyUnavailableException as the cause for the connection close. + failure = new IOException(qkue.getMessage()); + } + if (debug.on()) { + debug.log("failed to send stream data", failure); + } + // close the connection to make sure it's not just ignored + terminator.terminate(TerminationCause.forException(failure)); + return false; + } + + private boolean doSend1RTTData() throws QuicKeyUnavailableException, QuicTransportException { + // Loop over all sending streams to see if data is available - include + // as much data as possible in the quic packet before sending it. + // The QuicConnectionStreams make sure that streams are polled in a fair + // manner (using round-robin?) + // This loop is called through a sequential scheduler to make + // sure we only have one thread emitting flow control data for + // this connection + final PacketSpace space = packetSpace(PacketNumberSpace.APPLICATION); + final int maxDatagramSize = getMaxDatagramSize(); + final QuicConnectionId peerConnectionId = peerConnectionId(); + final int dstIdLength = peerConnectionId().length(); + if (!canSend()) { + return false; + } + final long packetNumber = space.allocateNextPN(); + final long largestPeerAckedPN = space.getLargestPeerAckedPN(); + int remaining = QuicPacketEncoder.computeMaxOneRTTPayloadSize( + codingContext, packetNumber, dstIdLength, maxDatagramSize, largestPeerAckedPN); + if (remaining == 0) { + // not enough space to send available data + return false; + } + final List frames = new ArrayList<>(); + remaining -= addConnectionControlFrames(remaining, frames); + assert remaining >= 0 : remaining; + long produced = streams.produceFramesToSend(encoder, remaining, credit(), frames); + if (frames.isEmpty()) { + // produced cannot be > 0 unless there are some frames to send + assert produced == 0; + return false; + } + // non-atomic operation should be OK since sendStreamData0 is called + // only from the sending loop, and this is the only place where we + // mutate dataProcessed. + dataProcessed += produced; + final OneRttPacket packet = encoder.newOneRttPacket(peerConnectionId, + packetNumber, largestPeerAckedPN, frames, codingContext); + QuicConnectionImpl.this.send1RTTPacket(packet); + return true; + } + + /** + * Produces connection-level control frames for sending in the next one-rtt + * packet. The frames are added to the provided list. + * + * @param maxAllowedBytes maximum number of bytes the method is allowed to add + * @param frames list where the frames are added + * @return number of bytes added + */ + private int addConnectionControlFrames(final int maxAllowedBytes, + final List frames) { + assert maxAllowedBytes > 0 : "unexpected max allowed bytes: " + maxAllowedBytes; + int added = 0; + int remaining = maxAllowedBytes; + QuicFrame f; + while ((f = outgoing1RTTFrames.peek()) != null) { + final int frameSize = f.size(); + if (frameSize <= remaining) { + outgoing1RTTFrames.remove(); + frames.add(f); + added += frameSize; + remaining -= frameSize; + } else { + break; + } + } + PathChallengeFrame pcf; + while (remaining >= 9 && (pcf = pathChallengeFrameQueue.poll()) != null) { + f = new PathResponseFrame(pcf.data()); + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + + // NEW_CONNECTION_ID + while ((f = localConnIdManager.nextFrame(remaining)) != null) { + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + // RETIRE_CONNECTION_ID + while ((f = peerConnIdManager.nextFrame(remaining)) != null) { + final int frameSize = f.size(); + assert frameSize <= remaining : "Frame too large"; + frames.add(f); + added += frameSize; + remaining -= frameSize; + } + + if (remaining == 0) { + return added; + } + final PacketSpace space = packetSpace(PacketNumberSpace.APPLICATION); + final AckFrame ack = space.getNextAckFrame(false, remaining); + if (ack != null) { + final int ackFrameSize = ack.size(); + assert ackFrameSize <= remaining; + if (debug.on()) { + debug.log("Adding AckFrame"); + } + frames.add(ack); + added += ackFrameSize; + remaining -= ackFrameSize; + } + final long credit = credit(); + if (credit < remaining && remaining > 10) { + if (debug.on()) { + debug.log("Adding DataBlockedFrame"); + } + DataBlockedFrame dbf = new DataBlockedFrame(maxData); + frames.add(dbf); + added += dbf.size(); + remaining -= dbf.size(); + } + // max data + if (remaining > 10) { + long maxData = oneRttRcvQueue.bumpMaxData(); + if (maxData != 0) { + if (debug.on()) { + debug.log("Adding MaxDataFrame (processed: %s)", + oneRttRcvQueue.processedData); + } + MaxDataFrame mdf = new MaxDataFrame(maxData); + frames.add(mdf); + added += mdf.size(); + remaining -= mdf.size(); + } + } + // session ticket + if (quicTLSEngine.getCurrentSendKeySpace() == KeySpace.ONE_RTT) { + try { + ByteBuffer payloadBuffer = quicTLSEngine.getHandshakeBytes(KeySpace.ONE_RTT); + if (payloadBuffer != null) { + localCryptoFlow.enqueue(payloadBuffer); + } + } catch (IOException e) { + throw new AssertionError("Should not happen!", e); + } + if (localCryptoFlow.remaining() > 0 && remaining > 3) { + CryptoFrame frame = localCryptoFlow.produceFrame(remaining); + if (frame != null) { + if (debug.on()) { + debug.log("Adding CryptoFrame"); + } + frames.add(frame); + added += frame.size(); + remaining -= frame.size(); + assert remaining >= 0; + } + } + } + return added; + } + } + + /** + * Invoked to send a ONERTT packet containing stream data or + * control frames. + * + * @apiNote + * This method can be overridden if some action needs to be + * performed after sending a packet containing certain type + * of frames. Typically, a server side connection may want + * to close the HANDSHAKE space only after sending the + * HANDSHAKE_DONE frame. + * + * @param packet The ONERTT packet to send. + */ + protected void send1RTTPacket(final OneRttPacket packet) + throws QuicKeyUnavailableException, QuicTransportException { + pushDatagram(ProtectionRecord.single(packet, + QuicConnectionImpl.this::allocateDatagramForEncryption)); + } + + /** + * Schedule a frame for sending in a 1-RTT packet. + *

        + * For use with frames that do not change with time + * (like MAX_* / *_BLOCKED / ACK), + * or with remaining datagram capacity (like STREAM or CRYPTO), + * and do not require certain path (PATH_CHALLENGE / RESPONSE). + *

        + * Use with frames like HANDSHAKE_DONE, NEW_TOKEN, + * NEW_CONNECTION_ID, RETIRE_CONNECTION_ID. + *

        + * Maximum accepted frame size is 1000 bytes to ensure that the frame + * will fit in a 1-RTT datagram in the foreseeable future. + * @param frame frame to send + * @throws IllegalArgumentException if frame is larger than 1000 bytes + */ + protected void enqueue1RTTFrame(final QuicFrame frame) { + if (frame.size() > 1000) { + throw new IllegalArgumentException("Frame too big"); + } + assert frame.isValidIn(PacketType.ONERTT) : "frame " + frame + " is not" + + " eligible in 1-RTT space"; + outgoing1RTTFrames.add(frame); + } + + /** + * {@return true if queued frames are available for sending} + */ + private boolean hasQueuedFrames() { + return !outgoing1RTTFrames.isEmpty(); + } + + protected QuicPacketEncoder encoder() { return encoder;} + protected QuicPacketDecoder decoder() { return decoder; } + public QuicEndpoint endpoint() { return endpoint; } + protected final StateHandle stateHandle() { return stateHandle; } + protected CodingContext codingContext() { + return codingContext; + } + + public long largestAckedPN(PacketNumberSpace packetSpace) { + var space = packetSpaces.get(packetSpace); + return space.getLargestPeerAckedPN(); + } + + public long largestProcessedPN(PacketNumberSpace packetSpace) { + var space = packetSpaces.get(packetSpace); + return space.getLargestProcessedPN(); + } + + public int connectionIdLength() { + return localConnectionId().length(); + } + + public QuicInstance quicInstance() { + return this.quicInstance; + } + + public QuicVersion quicVersion() { + return this.quicVersion; + } + + protected class QuicCodingContext implements CodingContext { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return QuicConnectionImpl.this.largestProcessedPN(packetSpace); + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return QuicConnectionImpl.this.largestAckedPN(packetSpace); + } + @Override public int connectionIdLength() { + return QuicConnectionImpl.this.connectionIdLength(); + } + @Override public int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException { + int start = buffer.position(); + encoder.encode(packet, buffer, this); + return buffer.position() - start; + } + @Override public QuicPacket parsePacket(ByteBuffer src) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + return decoder.decode(src, this); + } + @Override + public QuicConnectionId originalServerConnId() { + return QuicConnectionImpl.this.originalServerConnId(); + } + + @Override + public QuicTLSEngine getTLSEngine() { + return quicTLSEngine; + } + + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return QuicConnectionImpl.this.verifyToken(destinationID, token); + } + } + + protected boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + // server must send zero-length token + return token == null; + } + + protected PacketEmitter emitter() { + return new PacketEmitter() { + @Override + public QuicTimerQueue timer() { + return QuicConnectionImpl.this.endpoint().timer(); + } + + @Override + public void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException { + QuicConnectionImpl.this.retransmit(packetSpaceManager, packet, attempts); + } + + @Override + public long emitAckPacket(PacketSpace packetSpaceManager, + AckFrame frame, + boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + return QuicConnectionImpl.this.emitAckPacket(packetSpaceManager, frame, sendPing); + } + + @Override + public void acknowledged(QuicPacket packet) { + QuicConnectionImpl.this.packetAcknowledged(packet); + } + + @Override + public boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + return QuicConnectionImpl.this.sendData(packetNumberSpace); + } + + @Override + public Executor executor() { + return quicInstance().executor(); + } + + @Override + public void reschedule(QuicTimedEvent task) { + var endpoint = QuicConnectionImpl.this.endpoint(); + if (endpoint == null) return; + endpoint.timer().reschedule(task); + } + + @Override + public void reschedule(QuicTimedEvent task, Deadline deadline) { + var endpoint = QuicConnectionImpl.this.endpoint(); + if (endpoint == null) return; + endpoint.timer().reschedule(task, deadline); + } + + @Override + public void checkAbort(PacketNumberSpace packetNumberSpace) { + QuicConnectionImpl.this.checkAbort(packetNumberSpace); + } + + @Override + public void ptoBackoffIncreased(PacketSpaceManager space, long backoff) { + if (Log.quicRetransmit()) { + Log.logQuic("%s OUT: [%s] increase backoff to %s, duration %s ms: %s" + .formatted(QuicConnectionImpl.this.logTag(), + space.packetNumberSpace(), backoff, + space.getPtoDuration().toMillis(), + rttEstimator.state())); + } + } + + @Override + public String logTag() { + return QuicConnectionImpl.this.logTag(); + } + + @Override + public boolean isOpen() { + return QuicConnectionImpl.this.stateHandle.opened(); + } + }; + } + + private void checkAbort(PacketNumberSpace packetNumberSpace) { + // if pto backoff > 32 (i.e. PTO expired 5 times in a row), abort, + // unless we haven't reached MIN_PTO_BACKOFF_TIMEOUT + var backoff = rttEstimator.getPtoBackoff(); + if (backoff > QuicRttEstimator.MAX_PTO_BACKOFF) { + // If the maximum backoff is exceeded, we close the connection + // only if the associated backoff timeout exceeds the + // MIN_PTO_BACKOFF_TIMEOUT. Otherwise, we allow the backoff + // factor to grow again past the MAX_PTO_BACKOFF + if (rttEstimator.isMinBackoffTimeoutExceeded()) { + if (debug.on()) { + debug.log("%s Too many probe time outs: %s", packetNumberSpace, backoff); + debug.log(String.valueOf(rttEstimator.state())); + debug.log("State: %s", stateHandle().toString()); + } + if (Log.quicRetransmit() || Log.quicCC()) { + Log.logQuic("%s OUT: %s: Too many probe timeouts %s" + .formatted(logTag(), packetNumberSpace, + rttEstimator.state())); + StringBuilder sb = new StringBuilder(logTag()); + sb.append(" State: ").append(stateHandle().toString()); + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (packetSpaces.get(sp) instanceof PacketSpaceManager m) { + sb.append("\nPacketSpace: ").append(sp).append('\n'); + m.debugState(" ", sb); + } + } + Log.logQuic(sb.toString()); + } else if (debug.on()) { + for (PacketNumberSpace sp : PacketNumberSpace.values()) { + if (sp == PacketNumberSpace.NONE) continue; + if (packetSpaces.get(sp) instanceof PacketSpaceManager m) { + m.debugState(); + } + } + } + var pto = rttEstimator.getBasePtoDuration(); + var to = pto.multipliedBy(backoff); + if (to.compareTo(MAX_PTO_BACKOFF_TIMEOUT) > 0) to = MAX_PTO_BACKOFF_TIMEOUT; + String msg = "%s: Too many probe time outs (%s: backoff %s, duration %s, %s)" + .formatted(logTag(), packetNumberSpace, backoff, + to, rttEstimator.state()); + final TerminationCause terminationCause; + if (packetNumberSpace == PacketNumberSpace.HANDSHAKE) { + terminationCause = TerminationCause.forException(new SSLHandshakeException(msg)); + } else if (packetNumberSpace == PacketNumberSpace.INITIAL) { + terminationCause = TerminationCause.forException(new ConnectException(msg)); + } else { + terminationCause = TerminationCause.forException(new IOException(msg)); + } + terminator.terminate(terminationCause); + } else { + if (debug.on()) { + debug.log("%s: Max PTO backoff reached (%s) before min probe timeout exceeded (%s)," + + " allow more backoff %s", + packetNumberSpace, backoff, MIN_PTO_BACKOFF_TIMEOUT, rttEstimator.state()); + } + if (Log.quicRetransmit() || Log.quicCC()) { + Log.logQuic("%s OUT: %s: Max PTO backoff reached (%s) before min probe timeout exceeded (%s) - %s" + .formatted(QuicConnectionImpl.this.logTag(), packetNumberSpace, backoff, + MIN_PTO_BACKOFF_TIMEOUT, rttEstimator.state())); + } + } + } + } + + // this method is called when a packet has been acknowledged + private void packetAcknowledged(QuicPacket packet) { + // process packet frames to track acknowledgement + // of RESET_STREAM frames etc... + if (debug.on()) { + debug.log("Packet %s(pn:%s) is acknowledged by peer", + packet.packetType(), + packet.packetNumber()); + } + packet.frames().forEach(this::frameAcknowledged); + } + + // this method is called when a frame has been acknowledged + private void frameAcknowledged(QuicFrame frame) { + if (frame instanceof ResetStreamFrame reset) { + long streamId = reset.streamId(); + if (streams.isSendingStream(streamId)) { + streams.streamResetAcknowledged(reset); + } + } else if (frame instanceof StreamFrame streamFrame) { + if (streamFrame.isLast()) { + streams.streamDataSentAcknowledged(streamFrame); + } + } + } + + protected PacketSpaces packetNumberSpaces() { + return packetSpaces; + } + protected PacketSpace packetSpace(PacketNumberSpace packetNumberSpace) { + return packetSpaces.get(packetNumberSpace); + } + + public String dbgTag() { return dbgTag; } + + public String streamDbgTag(long streamId, String direction) { + String dir = direction == null || direction.isEmpty() + ? "" : ("(" + direction + ")"); + return dbgTag + "[streamId" + dir + "=" + streamId + "]"; + } + + + @Override + public CompletableFuture openNewLocalBidiStream(final Duration limitIncreaseDuration) { + if (!stateHandle.opened()) { + return MinimalFuture.failedFuture(new ClosedChannelException()); + } + final CompletableFuture> streamCF = + this.handshakeFlow.handshakeCF().thenApply((ignored) -> + streams.createNewLocalBidiStream(limitIncreaseDuration)); + return streamCF.thenCompose(Function.identity()); + } + + @Override + public CompletableFuture openNewLocalUniStream(final Duration limitIncreaseDuration) { + if (!stateHandle.opened()) { + return MinimalFuture.failedFuture(new ClosedChannelException()); + } + final CompletableFuture> streamCF = + this.handshakeFlow.handshakeCF().thenApply((ignored) + -> streams.createNewLocalUniStream(limitIncreaseDuration)); + return streamCF.thenCompose(Function.identity()); + } + + @Override + public void addRemoteStreamListener(Predicate streamConsumer) { + streams.addRemoteStreamListener(streamConsumer); + } + + @Override + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + return streams.removeRemoteStreamListener(streamConsumer); + } + + @Override + public Stream quicStreams() { + return streams.quicStreams(); + } + + @Override + public List connectionIds() { + return localConnIdManager.connectionIds(); + } + + LocalConnIdManager localConnectionIdManager() { + return localConnIdManager; + } + + /** + * {@return the local connection id} + */ + public QuicConnectionId localConnectionId() { + return connectionId; + } + + /** + * {@return the peer connection id} + */ + public QuicConnectionId peerConnectionId() { + return this.peerConnIdManager.getPeerConnId(); + } + + /** + * Returns the original connection id. + * This is the original destination connection id that + * the client generated when connecting to the server for + * the first time. + * @return the original connection id + */ + protected QuicConnectionId originalServerConnId() { + return this.peerConnIdManager.originalServerConnId(); + } + + private record IncomingDatagram(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) {} + + @Override + public boolean accepts(SocketAddress source) { + // The client ever accepts packets from two sources: + // => the original peer address + // => the preferred peer address (not implemented) + if (!source.equals(peerAddress)) { + // We only accept packets from the endpoint to + // which we send them. + if (debug.on()) { + debug.log("unexpected sender %s, skipping packet", source); + } + return false; + } + return true; + } + + public void processIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) { + // Processes an incoming datagram that has just been + // read off the network. + if (debug.on()) { + debug.log("processIncoming %s(pos=%d, remaining=%d)", + headersType, buffer.position(), buffer.remaining()); + } + if (!stateHandle.opened()) { + if (debug.on()) { + debug.log("connection closed, skipping packet"); + } + return; + } + + assert accepts(source); + + scheduleForDecryption(new IncomingDatagram(source, destConnId, headersType, buffer)); + } + + public void internalProcessIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer) { + try { + int packetIndex = 0; + while(buffer.hasRemaining()) { + int startPos = buffer.position(); + packetIndex++; + boolean isLongHeader = QuicPacketDecoder.peekHeaderType(buffer, startPos) == QuicPacket.HeadersType.LONG; + // It's only safe to check version here if versionNegotiated is true. + // We might be receiving an INITIAL packet before the version negotiation + // has been handled. + if (isLongHeader) { + LongHeader header = QuicPacketDecoder.peekLongHeader(buffer); + if (header == null) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram): too short", packetIndex); + } + return; + } + if (!header.destinationId().matches(destConnId)) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram):" + + " wrong connection id (%s vs %s)", + packetIndex, + header.destinationId().toHexString(), + Utils.asHexString(destConnId)); + } + return; + } + var peekedVersion = header.version(); + final var version = this.quicVersion.versionNumber(); + if (version != peekedVersion) { + if (peekedVersion == 0) { + if (!versionCompatible) { + VersionNegotiationPacket packet = (VersionNegotiationPacket) codingContext.parsePacket(buffer); + processDecrypted(packet); + } else { + if (debug.on()) { + debug.log("Versions packet (%s in datagram) ignored", packetIndex); + } + } + return; + } + QuicVersion packetVersion = QuicVersion.of(peekedVersion).orElse(null); + if (packetVersion == null) { + if (debug.on()) { + debug.log("Unknown Quic version in long header packet" + + " (%s in datagram) %s: 0x%x", + packetIndex, headersType, peekedVersion); + } + return; + } else if (versionNegotiated) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram)" + + " with version %s, already negotiated %s", + packetIndex, packetVersion, quicVersion); + } + return; + } else if (!quicInstance().isVersionAvailable(packetVersion)) { + if (debug.on()) { + debug.log("Dropping long header packet (%s in datagram)" + + " with disabled version %s", + packetIndex, packetVersion); + } + return; + } else { + // do we need to be less trusting here? + if (debug.on()) { + debug.log("Switching version to %s, previous: %s", + packetVersion, quicVersion); + } + switchVersion(packetVersion); + } + } + if (decoder.peekPacketType(buffer) == PacketType.INITIAL && + !quicTLSEngine.keysAvailable(KeySpace.INITIAL)) { + if (debug.on()) { + debug.log("Dropping INITIAL packet (%s in datagram): %s", + packetIndex, "keys discarded"); + } + decoder.skipPacket(buffer, startPos); + continue; + } + } else { + var cid = QuicPacketDecoder.peekShortConnectionId(buffer, destConnId.remaining()); + if (cid == null) { + if (debug.on()) { + debug.log("Dropping short header packet (%s in datagram):" + + " too short", packetIndex); + } + return; + } + if (cid.mismatch(destConnId) != -1) { + if (debug.on()) { + debug.log("Dropping short header packet (%s in datagram):" + + " wrong connection id (%s vs %s)", + packetIndex, Utils.asHexString(cid), Utils.asHexString(destConnId)); + } + return; + } + + } + ByteBuffer packet = decoder.nextPacketSlice(buffer, buffer.position()); + PacketType packetType = decoder.peekPacketType(packet); + if (debug.on()) { + debug.log("unprotecting packet (%s in datagram) %s(%s bytes)", + packetIndex, packetType, packet.remaining()); + } + decrypt(packet); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to process incoming packet", t); + } + } + } + + /** + * Called when an incoming packet has been decrypted. + *

        + * @param quicPacket the decrypted quic packet + */ + public void processDecrypted(QuicPacket quicPacket) { + PacketType packetType = quicPacket.packetType(); + long packetNumber = quicPacket.packetNumber(); + if (debug.on()) { + debug.log("processDecrypted %s(%d)", packetType, packetNumber); + } + if (Log.quicPacketInLoggable(quicPacket)) { + Log.logQuicPacketIn(logTag(), quicPacket); + } + if (packetType != PacketType.VERSIONS) { + versionCompatible = true; + // versions will also set versionCompatible later + } + if (isClientConnection() + && quicPacket instanceof InitialPacket longPacket + && quicPacket.frames().stream().anyMatch(CryptoFrame.class::isInstance)) { + markVersionNegotiated(longPacket.version()); + } + PacketSpace packetSpace = null; + if (packetNumber >= 0) { + packetSpace = packetSpace(quicPacket.numberSpace()); + + // From RFC 9000, Section 13.2.3: + // A receiver MUST retain an ACK Range unless it can ensure that + // it will not subsequently accept packets with numbers in + // that range. Maintaining a minimum packet number that increases + // as ranges are discarded is one way to achieve this with minimal + // state. + long threshold = packetSpace.getMinPNThreshold(); + if (packetNumber <= threshold) { + // discard the packet, as we are no longer acknowledging + // packets in this range. + if (debug.on()) + debug.log("discarding packet %s(%d) - threshold: %d", + packetType, packetNumber, threshold); + return; + } + if (packetSpace.isAcknowledged(packetNumber)) { + if (debug.on()) + debug.log("discarding packet %s(%d) - duplicated", + packetType, packetNumber, threshold); + } + + if (debug.on()) { + debug.log("receiving packet %s(pn:%s, %s)", packetType, + packetNumber, quicPacket.frames()); + } + } + switch (packetType) { + case VERSIONS -> processVersionNegotiationPacket(quicPacket); + case INITIAL -> processInitialPacket(quicPacket); + case ONERTT -> processOneRTTPacket(quicPacket); + case HANDSHAKE -> processHandshakePacket(quicPacket); + case RETRY -> processRetryPacket(quicPacket); + case ZERORTT -> { + if (debug.on()) { + debug.log("Dropping unhandled quic packet %s", packetType); + } + } + case NONE -> throw new InternalError("Unrecognized packet type"); + } + // packet has been processed successfully - connection isn't idle (RFC-9000, section 10.1) + this.terminator.keepAlive(); + if (packetSpace != null) { + packetSpace.packetReceived( + packetType, + packetNumber, + quicPacket.isAckEliciting()); + } + } + + /** + * {@return true if this is a stream initiated locally, and false if + * this is a stream initiated by the peer}. + * @param streamId a stream ID. + */ + protected final boolean isLocalStream(long streamId) { + return isClientConnection() == QuicStreams.isClientInitiated(streamId); + } + + /** + * If a stream with this streamId was already created, returns it. + * @param streamId the stream ID + * @return the stream identified by the given {@code streamId}, or {@code null}. + */ + protected QuicStream findStream(long streamId) { + return streams.findStream(streamId); + } + + /** + * @return true if this stream ID identifies a stream that was + * already opened + * @param streamId the stream id + */ + protected boolean isExistingStreamId(long streamId) { + long next = streams.peekNextStreamId(streamType(streamId)); + return streamId < next; + } + + /** + * Get or open a peer initiated stream with the given stream ID + * @param streamId the id of the remote stream + * @param frameType type of the frame received, used in exceptions + * @return the remote initiated stream identified by the given + * stream ID, or null + * @throws QuicTransportException if the streamID is higher than allowed + */ + protected QuicStream openOrGetRemoteStream(long streamId, long frameType) throws QuicTransportException { + assert !isLocalStream(streamId); + return streams.getOrCreateRemoteStream(streamId, frameType); + } + + /** + * Called to process a {@link OneRttPacket} after it has been successfully decrypted + * @param quicPacket the Quic packet + * @throws IllegalArgumentException if the {@code quicPacket} isn't a 1-RTT packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processOneRTTPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.ONERTT) { + throw new IllegalArgumentException("Not a ONERTT packet: " + quicPacket.packetType()); + } + assert quicPacket instanceof OneRttPacket : "Unexpected ONERTT packet class type: " + + quicPacket.getClass(); + final OneRttPacket oneRTT = (OneRttPacket) quicPacket; + try { + if (debug.on()) { + debug.log("processing packet ONERTT(%s)", quicPacket.packetNumber()); + } + final var frames = oneRTT.frames(); + if (debug.on()) { + debug.log("processing frames: " + frames.stream() + .map(Object::getClass).map(Class::getSimpleName) + .collect(Collectors.joining(", ", "[", "]"))); + } + for (var frame : oneRTT.frames()) { + if (!frame.isValidIn(PacketType.ONERTT)) { + throw new QuicTransportException("Invalid frame in ONERTT packet", + KeySpace.ONE_RTT, frame.getTypeField(), + PROTOCOL_VIOLATION); + } + if (debug.on()) { + debug.log("received 1-RTT frame %s", frame); + } + switch (frame) { + case AckFrame ackFrame -> { + incoming1RTTFrame(ackFrame); + } + case StreamFrame streamFrame -> { + incoming1RTTFrame(streamFrame); + } + case CryptoFrame crypto -> { + incoming1RTTFrame(crypto); + } + case ResetStreamFrame resetStreamFrame -> { + incoming1RTTFrame(resetStreamFrame); + } + case DataBlockedFrame dataBlockedFrame -> { + incoming1RTTFrame(dataBlockedFrame); + } + case StreamDataBlockedFrame streamDataBlockedFrame -> { + incoming1RTTFrame(streamDataBlockedFrame); + } + case StreamsBlockedFrame streamsBlockedFrame -> { + incoming1RTTFrame(streamsBlockedFrame); + } + case PaddingFrame paddingFrame -> { + incoming1RTTFrame(paddingFrame); + } + case MaxDataFrame maxData -> { + incoming1RTTFrame(maxData); + } + case MaxStreamDataFrame maxStreamData -> { + incoming1RTTFrame(maxStreamData); + } + case MaxStreamsFrame maxStreamsFrame -> { + incoming1RTTFrame(maxStreamsFrame); + } + case StopSendingFrame stopSendingFrame -> { + incoming1RTTFrame(stopSendingFrame); + } + case PingFrame ping -> { + incoming1RTTFrame(ping); + } + case ConnectionCloseFrame close -> { + incoming1RTTFrame(close); + } + case HandshakeDoneFrame handshakeDoneFrame -> { + incoming1RTTFrame(handshakeDoneFrame); + } + case NewConnectionIDFrame newCid -> { + incoming1RTTFrame(newCid); + } + case RetireConnectionIDFrame retireCid -> { + incoming1RTTFrame(oneRTT, retireCid); + } + case NewTokenFrame newTokenFrame -> { + incoming1RTTFrame(newTokenFrame); + } + case PathResponseFrame pathResponseFrame -> { + incoming1RTTFrame(pathResponseFrame); + } + case PathChallengeFrame pathChallengeFrame -> { + incoming1RTTFrame(pathChallengeFrame); + } + default -> { + if (debug.on()) { + debug.log("Frame type: %s not supported yet", frame.getClass()); + } + } + } + } + } catch (Throwable t) { + onProcessingError(quicPacket, t); + } + } + + /** + * Gets a receiving stream instance for the given ID, used for processing + * incoming STREAM, RESET_STREAM and STREAM_DATA_BLOCKED frames. + * Returns null if the instance is gone already. Throws an exception if the stream ID is incorrect. + * @param streamId stream ID + * @param frameType received frame type. Used in QuicTransportException + * @return receiver stream, or null if stream is already gone + * @throws QuicTransportException if the stream ID is not a valid receiving stream + */ + private QuicReceiverStream getReceivingStream(long streamId, long frameType) throws QuicTransportException { + var stream = findStream(streamId); + boolean isLocalStream = isLocalStream(streamId); + boolean isUnidirectional = isUnidirectional(streamId); + if (isLocalStream && isUnidirectional) { + // stream is write-only + throw new QuicTransportException("Stream %s (type %s) is unidirectional" + .formatted(streamId, streamType(streamId)), + KeySpace.ONE_RTT, frameType, QuicTransportErrors.STREAM_STATE_ERROR); + } + if (stream == null && isLocalStream) { + // the stream is either closed or bad stream + if (!isExistingStreamId(streamId)) { + throw new QuicTransportException("No such stream %s (type %s)" + .formatted(streamId, streamType(streamId)), + KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_STATE_ERROR); + } + return null; + } + + if (stream == null) { + assert !isLocalStream; + // Note: The quic protocol allows any peer to open + // a bidirectional remote stream. + // The HTTP/3 protocol does not allow a server to open a + // bidirectional stream on the client. If this is a client + // connection and the stream type is bidirectional and + // remote, the connection will be closed by the HTTP/3 + // higher level protocol but not here, since this is + // not a Quic protocol error. + stream = openOrGetRemoteStream(streamId, frameType); + if (stream == null) { + return null; + } + } + return (QuicReceiverStream)stream; + } + + /** + * Gets a sending stream instance for the given ID, used for processing + * incoming MAX_STREAM_DATA and STOP_SENDING frames. + * Returns null if the instance is gone already. Throws an exception if the stream ID is incorrect. + * @param streamId stream ID + * @param frameType received frame type. Used in QuicTransportException + * @return sender stream, or null if stream is already gone + * @throws QuicTransportException if the stream ID is not a valid sending stream + */ + private QuicSenderStream getSendingStream(long streamId, long frameType) throws QuicTransportException { + var stream = findStream(streamId); + boolean isLocalStream = isLocalStream(streamId); + boolean isUnidirectional = isUnidirectional(streamId); + if (!isLocalStream && isUnidirectional) { + // stream is read-only + throw new QuicTransportException("Stream %s (type %s) is unidirectional" + .formatted(streamId, streamType(streamId)), + QuicTLSEngine.KeySpace.ONE_RTT, frameType, QuicTransportErrors.STREAM_STATE_ERROR); + } + if (stream == null && isLocalStream) { + // the stream is either closed or bad stream + if (!isExistingStreamId(streamId)) { + throw new QuicTransportException("No such stream %s (type %s)" + .formatted(streamId, streamType(streamId)), + QuicTLSEngine.KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_STATE_ERROR); + } + return null; + } + + if (stream == null) { + assert !isLocalStream; + stream = openOrGetRemoteStream(streamId, frameType); + if (stream == null) { + return null; + } + } + return (QuicSenderStream)stream; + } + + /** + * Called to process an {@link InitialPacket} after it has been decrypted. + * @param quicPacket the Quic packet + * @throws IllegalArgumentException if {@code quicPacket} isn't a INITIAL packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processInitialPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.INITIAL) { + throw new IllegalArgumentException("Not a INITIAL packet: " + quicPacket.packetType()); + } + try { + if (quicPacket instanceof InitialPacket initial) { + MaxInitialTimer initialTimer = this.maxInitialTimer; + if (initialTimer != null) { + // will be a no-op after the first call; + initialTimer.initialPacketReceived(); + // we no longer need the timer + this.maxInitialTimer = null; + } + int total; + updatePeerConnectionId(initial); + total = processInitialPacketPayload(initial); + assert total == initial.payloadSize(); + // received initial packet from server - we won't need to replay anything now + handshakeFlow.localInitial.discardReplayData(); + continueHandshake(); + if (quicTLSEngine.getHandshakeState() == HandshakeState.NEED_RECV_CRYPTO && + quicTLSEngine.keysAvailable(KeySpace.HANDSHAKE)) { + // arm the anti-deadlock PTO timer + packetSpaces.handshake.runTransmitter(); + } + } else { + throw new InternalError("Bad packet type: " + quicPacket); + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + protected void updatePeerConnectionId(InitialPacket initial) throws QuicTransportException { + this.incomingInitialPacketSourceId = initial.sourceId(); + this.peerConnIdManager.finalizeHandshakePeerConnId(initial); + } + + public QuicConnectionId getIncomingInitialPacketSourceId() { + return incomingInitialPacketSourceId; + } + + @Override + public CompletableFuture handshakeReachedPeer() { + return this.handshakeFlow.handshakeReachedPeerCF; + } + + /** + * Process the payload of an incoming initial packet + * @param packet the incoming packet + * @return the total number of bytes consumed + * @throws SSLHandshakeException if the handshake failed + * @throws IOException if a frame couldn't be decoded, or the payload + * wasn't entirely consumed. + */ + protected int processInitialPacketPayload(final InitialPacket packet) + throws IOException, QuicTransportException { + int provided=0, total=0; + int initialPayloadSize = packet.payloadSize(); + if (debug.on()) { + debug.log("Processing initial packet pn:%s payload:%s", + packet.packetNumber(), initialPayloadSize); + } + for (final var frame: packet.frames()) { + if (debug.on()) { + debug.log("received INITIAL frame %s", frame); + } + int size = frame.size(); + total += size; + switch (frame) { + case AckFrame ack -> { + incomingInitialFrame(ack); + } + case CryptoFrame crypto -> { + provided = incomingInitialFrame(crypto); + } + case PaddingFrame paddingFrame -> { + incomingInitialFrame(paddingFrame); + } + case PingFrame ping -> { + incomingInitialFrame(ping); + } + case ConnectionCloseFrame close -> { + incomingInitialFrame(close); + } + default -> { + if (debug.on()) { + debug.log("Received invalid frame: " + frame); + } + assert !frame.isValidIn(packet.packetType()) : frame.getClass(); + throw new QuicTransportException("Invalid frame in this packet type", + packet.packetType().keySpace().orElse(null), frame.getTypeField(), + PROTOCOL_VIOLATION); + } + } + } + if (total != initialPayloadSize) { + throw new IOException("Initial payload wasn't fully consumed: %s read, of which %s crypto, from %s size" + .formatted(total, provided, initialPayloadSize)); + } + return total; + } + /** + * Process the payload of an incoming handshake packet + * @param packet the incoming packet + * @return the total number of bytes consumed + * @throws SSLHandshakeException if the handshake failed + * @throws IOException if a frame couldn't be decoded, or the payload + * wasn't entirely consumed. + */ + protected int processHandshakePacketPayload(final HandshakePacket packet) + throws IOException, QuicTransportException { + int provided=0, total=0; + int payloadSize = packet.payloadSize(); + for (final var frame: packet.frames()) { + if (debug.on()) { + debug.log("received HANDSHAKE frame %s", frame); + } + int size = frame.size(); + total += size; + switch (frame) { + case AckFrame ack -> { + incomingHandshakeFrame(ack); + } + case CryptoFrame crypto -> { + provided = incomingHandshakeFrame(crypto); + } + case PaddingFrame paddingFrame -> { + incomingHandshakeFrame(paddingFrame); + } + case PingFrame ping -> { + incomingHandshakeFrame(ping); + } + case ConnectionCloseFrame close -> { + incomingHandshakeFrame(close); + } + default -> { + assert !frame.isValidIn(packet.packetType()) : frame.getClass(); + throw new QuicTransportException("Invalid frame in this packet type", + packet.packetType().keySpace().orElse(null), frame.getTypeField(), + PROTOCOL_VIOLATION); + } + } + } + if (total != payloadSize) { + throw new IOException("Handshake payload wasn't fully consumed: %s read, of which %s crypto, from %s size" + .formatted(total, provided, payloadSize)); + } + return total; + } + + /** + * Called to process an {@link HandshakePacket} after it has been decrypted. + * @param quicPacket the handshake quic packet + * @throws IllegalArgumentException if {@code quicPacket} is not a HANDSHAKE packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processHandshakePacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.HANDSHAKE) { + throw new IllegalArgumentException("Not a HANDSHAKE packet: " + quicPacket.packetType()); + } + final var handshake = this.handshakeFlow.handshakeCF(); + if (handshake.isDone() && debug.on()) { + debug.log("Receiving HandshakePacket(%s) after handshake is done: %s", + quicPacket.packetNumber(), quicPacket.frames()); + } + try { + if (quicPacket instanceof HandshakePacket hs) { + int total; + total = processHandshakePacketPayload(hs); + assert total == hs.payloadSize(); + continueHandshake(); + } else { + throw new InternalError("Bad packet type: " + quicPacket); + } + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * Called to process a {@link RetryPacket} after it has been decrypted. + * @param quicPacket the retry quic packet + * @throws IllegalArgumentException if {@code quicPacket} is not a RETRY packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processRetryPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.RETRY) { + throw new IllegalArgumentException("Not a RETRY packet: " + quicPacket.packetType()); + } + try { + if (!(quicPacket instanceof RetryPacket rt)) { + throw new InternalError("Bad packet type: " + quicPacket); + } + assert stateHandle.helloSent() : "unexpected message"; + if (rt.retryToken().length == 0) { + if (debug.on()) { + debug.log("Invalid retry, empty token"); + } + return; + } + final QuicConnectionId currentPeerConnId = this.peerConnIdManager.getPeerConnId(); + if (rt.sourceId().equals(currentPeerConnId)) { + if (debug.on()) { + debug.log("Invalid retry, same connection ID"); + } + return; + } + if (this.peerConnIdManager.retryConnId() != null) { + if (debug.on()) { + debug.log("Ignoring retry, already got one"); + } + return; + } + // ignore retry if we already received initial packets + if (incomingInitialPacketSourceId != null) { + if (debug.on()) { + debug.log("Already received initial, ignoring retry"); + } + return; + } + final int version = rt.version(); + final QuicVersion retryVersion = QuicVersion.of(version).orElse(null); + if (retryVersion == null) { + if (debug.on()) { + debug.log("Ignoring retry packet with unknown version 0x" + + Integer.toHexString(version)); + } + // ignore the packet + return; + } + final QuicVersion originalVersion = this.quicVersion; // the original version used to establish the connection + if (originalVersion != retryVersion) { + if (debug.on()) { + debug.log("Ignoring retry packet with version 0x" + + Integer.toHexString(version) + + " since it doesn't match the original version 0x" + + Integer.toHexString(originalVersion.versionNumber())); + } + // ignore the packet + return; + } + ReentrantLock tl = packetSpaces.initial.getTransmitLock(); + tl.lock(); + try { + initialToken = rt.retryToken(); + final QuicConnectionId retryConnId = rt.sourceId(); + this.peerConnIdManager.retryConnId(retryConnId); + quicTLSEngine.deriveInitialKeys(originalVersion, retryConnId.asReadOnlyBuffer()); + this.packetSpace(PacketNumberSpace.INITIAL).retry(); + handshakeFlow.localInitial.replayData(); + } finally { + tl.unlock(); + } + packetSpaces.initial.runTransmitter(); + } catch (Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + } + + /** + * {@return the next (higher) max streams limit that should be advertised to the remote peer. + * Returns {@code 0} if the limit should not be increased} + * + * @param bidi true if bidirectional stream, false otherwise + */ + public long nextMaxStreamsLimit(final boolean bidi) { + if (isClientConnection() && bidi) return 0; // server does not open bidi streams + return streams.nextMaxStreamsLimit(bidi); + } + + /** + * Called when a stateless reset token is received. + */ + @Override + public void processStatelessReset() { + terminator.incomingStatelessReset(); + } + + /** + * Called to process a received {@link VersionNegotiationPacket} + * @param quicPacket the {@link VersionNegotiationPacket} + * @throws IllegalArgumentException if {@code quicPacket} is not a {@link PacketType#VERSIONS} + * packet + * @throws NullPointerException if {@code quicPacket} is null + */ + protected void processVersionNegotiationPacket(final QuicPacket quicPacket) { + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.VERSIONS) { + throw new IllegalArgumentException("Not a VERSIONS packet type: " + quicPacket.packetType()); + } + // servers aren't expected to receive version negotiation packet + if (!this.isClientConnection()) { + if (debug.on()) { + debug.log("(server) ignoring version negotiation packet"); + } + return; + } + try { + final var handshakeCF = this.handshakeFlow.handshakeCF(); + // we must ignore version negotiation if we already had a successful exchange + var versionCompatible = this.versionCompatible; + if (versionCompatible || handshakeCF.isDone()) { + if (debug.on()) { + debug.log("ignoring version negotiation packet (neg: %s, state: %s, hs: %s)", + versionCompatible, stateHandle, handshakeCF); + } + return; + } + // we shouldn't receive unsolicited version negotiation packets + assert stateHandle.helloSent(); + if (!(quicPacket instanceof VersionNegotiationPacket negotiate)) { + if (debug.on()) { + debug.log("Bad packet type %s for %s", + quicPacket.getClass().getName(), quicPacket); + } + return; + } + if (!negotiate.sourceId().equals(originalServerConnId())) { + if (debug.on()) { + debug.log("Received version negotiation packet with wrong connection id"); + debug.log("expected source id: %s, received source id: %s", + originalServerConnId(), negotiate.sourceId()); + debug.log("ignoring version negotiation packet (wrong id)"); + } + return; + } + final int[] serverSupportedVersions = negotiate.supportedVersions(); + if (debug.on()) { + debug.log("Received version negotiation packet with supported=%s", + Arrays.toString(serverSupportedVersions)); + } + assert this.quicInstance() instanceof QuicClient : "Not a quic client"; + final QuicClient client = (QuicClient) this.quicInstance(); + QuicVersion negotiatedVersion = null; + for (final int v : serverSupportedVersions) { + final QuicVersion serverVersion = QuicVersion.of(v).orElse(null); + if (serverVersion == null) { + if (debug.on()) { + debug.log("Ignoring unrecognized server supported version %d", v); + } + continue; + } + if (serverVersion == this.quicVersion) { + // RFC-9000, section 6.2: + // A client MUST discard a Version Negotiation packet that lists + // the QUIC version selected by the client. + if (debug.on()) { + debug.log("ignoring version negotiation packet since the version" + + " %d matches the current quic version selected by the client", v); + } + return; + } + // check if the current quic client is enabled for this version + if (!client.isVersionAvailable(serverVersion)) { + if (debug.on()) { + debug.log("Ignoring server supported version %d because the " + + "client isn't enabled for it", v); + } + continue; + } + if (debug.on()) { + if (negotiatedVersion == null) { + debug.log("Accepting server supported version %d", + serverVersion.versionNumber()); + negotiatedVersion = serverVersion; + } else { + // currently all versions are equal + debug.log("Skipping server supported version %d", + serverVersion.versionNumber()); + } + } + } + // at this point if negotiatedVersion is null, then it implies that none of the server + // supported versions are supported by the client. The spec expects us to abandon the + // current connection attempt in such cases (RFC-9000, section 6.2) + if (negotiatedVersion == null) { + final String msg = "No support for any of the QUIC versions being negotiated: " + + Arrays.toString(serverSupportedVersions); + if (debug.on()) { + debug.log("No version could be negotiated: %s", msg); + } + terminator.terminate(forException(new IOException(msg))); + return; + } + // a different version than the current client chosen version has been negotiated, + // switch the client connection to use this negotiated version + ReentrantLock tl = packetSpaces.initial.getTransmitLock(); + tl.lock(); + try { + if (switchVersion(negotiatedVersion)) { + final ByteBuffer quicInitialParameters = buildInitialParameters(); + quicTLSEngine.setLocalQuicTransportParameters(quicInitialParameters); + quicTLSEngine.restartHandshake(); + handshakeFlow.localInitial.reset(); + continueHandshake(); + packetSpaces.initial.runTransmitter(); + this.versionCompatible = true; + processedVersionsPacket = true; + } + } finally { + tl.unlock(); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to handle packet", t); + } + } + + } + + /** + * Switch to a new version after receiving a version negotiation + * packet. This method checks that no version was previously + * negotiated, in which case it switches the connection to the + * new version and returns true. + * Otherwise, it returns false. + * + * @param negotiated the new version that was negotiated + * @return true if switching to the new version was successful + */ + protected boolean switchVersion(QuicVersion negotiated) { + try { + assert !versionNegotiated; + if (debug.on()) + debug.log("switch to negotiated version %s", negotiated); + this.quicVersion = negotiated; + this.decoder = QuicPacketDecoder.of(negotiated); + this.encoder = QuicPacketEncoder.of(negotiated); + this.packetSpace(PacketNumberSpace.INITIAL).versionChanged(); + // regenerate the INITIAL keys using the new negotiated Quic version + this.quicTLSEngine.deriveInitialKeys(negotiated, originalServerConnId().asReadOnlyBuffer()); + return true; + } catch (Throwable t) { + terminator.terminate(forException(t)); + throw new RuntimeException("failed to switch to version", t); + } + } + + /** + * Mark the version as negotiated. No further version changes are possible. + * + * @param packetVersion the packet version + */ + protected void markVersionNegotiated(int packetVersion) { + int version = this.quicVersion.versionNumber(); + assert packetVersion == version; + if (!versionNegotiated) { + if (VERSION_NEGOTIATED.compareAndSet(this, false, true)) { + // negotiated version finalized + quicTLSEngine.versionNegotiated(QuicVersion.of(version).get()); + } + } + } + + /** + * {@return a boolean value telling whether the datagram in the + * protection record is complete} + * The datagram is complete when no other packet need to be coalesced + * in the datagram. + * If a datagram is complete, it is ready to be sent. + * + * @param protectionRecord the protection record + */ + protected boolean isDatagramComplete(ProtectionRecord protectionRecord) { + return protectionRecord.datagram.remaining() == 0 + || protectionRecord.flags == ProtectionRecord.SINGLE_PACKET + || (protectionRecord.flags & ProtectionRecord.LAST_PACKET) != 0 + || (protectionRecord.flags & ProtectionRecord.COALESCED) == 0; + } + + /** + * {@return the peer address that should be used when sending datagram + * to the peer} + */ + public InetSocketAddress peerAddress() { + return peerAddress; + } + + /** + * {@return the local address of the quic endpoint} + * @throws UncheckedIOException if the address is not available + */ + public SocketAddress localAddress() { + try { + var endpoint = this.endpoint; + if (endpoint == null) { + throw new IOException("no endpoint defined"); + } + return endpoint.getLocalAddress(); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + /** + * Pushes the {@linkplain ProtectionRecord#datagram() datagram} contained in + * the {@code protectionRecord}, through the {@linkplain QuicEndpoint endpoint}. + * + * @param protectionRecord the ProtectionRecord containing the datagram + */ + private void pushEncryptedDatagram(final ProtectionRecord protectionRecord) { + final long packetNumber = protectionRecord.packet().packetNumber(); + assert packetNumber >= 0 : "unexpected packet number: " + packetNumber; + final long retransmittedPacketNumber = protectionRecord.retransmittedPacketNumber(); + assert packetNumber > retransmittedPacketNumber : "packet number: " + packetNumber + + " was expected to be greater than packet the packet being retransmitted: " + + retransmittedPacketNumber; + final boolean pktContainsConnClose = containsConnectionClose(protectionRecord.packet()); + // if the connection isn't open then except for the packet containing a CONNECTION_CLOSE + // frame, we don't push any other packets. + if (!isOpen() && !pktContainsConnClose) { + if (debug.on()) { + debug.log("connection isn't open - ignoring %s(pn:%s): frames:%s", + protectionRecord.packet.packetType(), + protectionRecord.packet.packetNumber(), + protectionRecord.packet.frames()); + } + datagramDropped(new QuicDatagram(this, peerAddress, protectionRecord.datagram)); + return; + } + // TODO: revisit this: we need to figure out how best to emit coalesced packet, + // and having one protection record per packet may not be the the best. + // Maybe a protection record should have a list of coalesced packets + // instead of a single packet? + final ByteBuffer datagram = protectionRecord.datagram(); + final int firstPacketOffset = protectionRecord.firstPacketOffset(); + // flip the datagram + datagram.limit(datagram.position()); + datagram.position(firstPacketOffset); + if (debug.on()) { + final PacketType packetType = protectionRecord.packet().packetType(); + final int packetOffset = protectionRecord.packetOffset(); + if (packetOffset == firstPacketOffset) { + debug.log("Pushing datagram([%s(%d)], %d)", packetType, packetNumber, + datagram.remaining()); + } else { + debug.log("Pushing coalesced datagram([%s(%d)], %d)", + packetType, packetNumber, datagram.remaining()); + } + } + + // upon successful sending of the datagram, notify that the packet was sent + // we call packetSent just before sending the packet here, to make sure + // that the PendingAcknowledgement will be present in the queue before + // we receive the ACK frame from the server. Not doing this would create + // a race where the peer might be able to send the ack, and we might process + // it, before the PendingAcknowledgement is added. + final QuicPacket packet = protectionRecord.packet(); + final PacketSpace packetSpace = packetSpace(packet.numberSpace()); + packetSpace.packetSent(packet, retransmittedPacketNumber, packetNumber); + + // if we are sending a packet containing a CONNECTION_CLOSE frame, then we + // also switch/remove the current connection instance in the endpoint. + if (pktContainsConnClose) { + if (stateHandle.isMarked(QuicConnectionState.DRAINING)) { + // a CONNECTION_CLOSE frame is being sent to the peer when the local + // connection state is in DRAINING. This implies that the local endpoint + // is responding to an incoming CONNECTION_CLOSE frame from the peer. + // we remove the connection from the endpoint for such cases. + endpoint.pushClosedDatagram(this, peerAddress(), datagram); + } else if (stateHandle.isMarked(QuicConnectionState.CLOSING)) { + // a CONNECTION_CLOSE frame is being sent to the peer when the local + // connection state is in CLOSING. For such cases, we switch this + // connection in the endpoint to one which responds with + // CONNECTION_CLOSE frame for any subsequent incoming packets + // from the peer. + endpoint.pushClosingDatagram(this, peerAddress(), datagram); + } else { + // should not happen + throw new IllegalStateException("connection is neither draining nor closing," + + " cannot send a connection close frame"); + } + } else { + pushDatagram(peerAddress(), datagram); + } + // RFC-9000, section 10.1: An endpoint also restarts its idle timer when sending + // an ack-eliciting packet ... + if (packet.isAckEliciting()) { + this.terminator.keepAlive(); + } + } + + /** + * Calls the {@link QuicEndpoint#pushDatagram(QuicPacketReceiver, SocketAddress, ByteBuffer)} + * + * @param destination The destination of this datagram + * @param datagram The datagram + */ + protected void pushDatagram(final SocketAddress destination, final ByteBuffer datagram) { + endpoint.pushDatagram(this, destination, datagram); + } + + /** + * Called when a datagram scheduled for writing by this connection + * could not be written to the network. + * @param t the error that occurred + */ + @Override + public void onWriteError(Throwable t) { + // log exception if still opened + if (stateHandle.opened()) { + if (Log.errors()) { + Log.logError("%s: Failed to write datagram: %s", dbgTag(), t ); + Log.logError(t); + } else if (debug.on()) { + debug.log("Failed to write datagram", t); + } + } + } + + /** + * Called when a packet couldn't be processed + * @param t the error that occurred + */ + public void onProcessingError(QuicPacket packet, Throwable t) { + terminator.terminate(TerminationCause.forException(t)); + } + + /** + * Starts the Quic Handshake. + * @return A completable future which will be completed when the + * handshake is completed. + * @throws UnsupportedOperationException If this connection isn't a client connection + */ + public final CompletableFuture startHandshake() { + if (!isClientConnection()) { + throw new UnsupportedOperationException("Not a client connection, cannot start handshake"); + } + if (!this.startHandshakeCalled.compareAndSet(false, true)) { + throw new IllegalStateException("handshake has already been started on connection"); + } + if (this.peerAddress.isUnresolved()) { + // fail if address is unresolved + return MinimalFuture.failedFuture( + Utils.toConnectException(new UnresolvedAddressException())); + } + CompletableFuture cf; + try { + // register the connection with an endpoint + assert this.quicInstance instanceof QuicClient : "Not a QuicClient"; + endpoint.registerNewConnection(this); + cf = MinimalFuture.completedFuture(null); + } catch (Throwable t) { + cf = MinimalFuture.failedFuture(t); + } + return cf.thenApply(this::sendFirstInitialPacket) + .exceptionally((t) -> { + // complete the handshake CFs with the failure + handshakeFlow.failHandshakeCFs(t); + return handshakeFlow; + }) + .thenCompose(HandshakeFlow::handshakeCF) + .thenApply(this::onHandshakeCompletion); + } + + /** + * This method is called when the handshake is successfully completed. + * @param result the result of the handshake + */ + protected QuicEndpoint onHandshakeCompletion(final HandshakeState result) { + if (debug.on()) { + debug.log("Quic handshake successfully completed with %s(%s)", + quicTLSEngine.getApplicationProtocol(), peerAddress()); + } + // now that the handshake has successfully completed, start the + // idle timeout management for this connection + this.idleTimeoutManager.start(); + return this.endpoint; + } + + protected HandshakeFlow handshakeFlow() { + return handshakeFlow; + } + + protected void startInitialTimer() { + if (!isClientConnection()) return; + MaxInitialTimer initialTimer = maxInitialTimer; + if (initialTimer == null && DEFAULT_MAX_INITIAL_TIMEOUT < Integer.MAX_VALUE) { + Deadline maxInitialDeadline = null; + synchronized (this) { + initialTimer = maxInitialTimer; + if (initialTimer == null) { + Deadline now = this.endpoint().timeSource().instant(); + maxInitialDeadline = now.plusSeconds(DEFAULT_MAX_INITIAL_TIMEOUT); + initialTimer = maxInitialTimer = new MaxInitialTimer(this.endpoint().timer(), maxInitialDeadline); + } + } + if (maxInitialDeadline != null) { + if (Log.quic()) { + Log.logQuic("{0}: Arming quic initial timer for {1}", logTag(), + Deadline.between(this.endpoint().timeSource().instant(), maxInitialDeadline)); + } + if (debug.on()) { + debug.log("Arming quic initial timer for %s seconds", + Deadline.between(this.endpoint().timeSource().instant(), maxInitialDeadline).toSeconds()); + } + initialTimer.timerQueue.reschedule(initialTimer, maxInitialDeadline); + } + } + } + + // adaptation to Function + private HandshakeFlow sendFirstInitialPacket(Void unused) { + // may happen if connection cancelled before endpoint is + // created + final TerminationCause tc = terminationCause(); + if (tc != null) { + throw new CompletionException(tc.getCloseCause()); + } + try { + startInitialTimer(); + if (Log.quic()) { + Log.logQuic(logTag() + ": connectionId: " + + connectionId.toHexString() + + ", " + endpoint + ": " + endpoint.name() + + " - " + endpoint.getLocalAddressString()); + } else if (debug.on()) { + debug.log(logTag() + ": connectionId: " + + connectionId.toHexString() + + ", " + endpoint + ": " + endpoint.name() + + " - " + endpoint.getLocalAddressString()); + } + var localAddress = endpoint.getLocalAddress(); + var conflict = Utils.addressConflict(localAddress, peerAddress); + if (conflict != null) { + String msg = conflict; + if (debug.on()) { + debug.log("%s (local: %s, remote: %s)", msg, localAddress, peerAddress); + } + Log.logError("{0} {1} (local: {2}, remote: {3})", logTag(), + msg, localAddress, peerAddress); + throw new SSLHandshakeException(msg); + } + final QuicConnectionId clientSelectedPeerId = initialServerConnectionId(); + this.peerConnIdManager.originalServerConnId(clientSelectedPeerId); + handshakeFlow.markHandshakeStart(); + stateHandle.markHelloSent(); + // the "original version" used to establish the connection + final QuicVersion originalVersion = this.quicVersion; + quicTLSEngine.deriveInitialKeys(originalVersion, clientSelectedPeerId.asReadOnlyBuffer()); + final ByteBuffer quicInitialParameters = buildInitialParameters(); + quicTLSEngine.setLocalQuicTransportParameters(quicInitialParameters); + handshakeFlow.localInitial.keepReplayData(); + continueHandshake(); + packetSpaces.initial.runTransmitter(); + } catch (Throwable t) { + terminator.terminate(forException(t)); + throw new CompletionException(terminationCause().getCloseCause()); + } + return handshakeFlow; + } + + private static final Random RANDOM = new SecureRandom(); + + private QuicConnectionId initialServerConnectionId() { + byte[] bytes = new byte[INITIAL_SERVER_CONNECTION_ID_LENGTH]; + RANDOM.nextBytes(bytes); + return new PeerConnectionId(bytes); + } + + /** + * Compose a list of Quic frames containing a crypto frame and an ack frame, + * omitting null frames. + * @param crypto the crypto frame + * @param ack the ack frame + * @return A list of {@link QuicFrame}. + */ + private List makeList(CryptoFrame crypto, AckFrame ack) { + List frames = new ArrayList<>(2); + if (crypto != null) { + frames.add(crypto); + } + if (ack != null) { + frames.add(ack); + } + return frames; + } + + /** + * Allocate a {@link ByteBuffer} that can be used to encrypt the + * given packet. + * @param packet the packet to encrypt + * @return a new {@link ByteBuffer} with sufficient space to encrypt + * the given packet. + */ + protected ByteBuffer allocateDatagramForEncryption(QuicPacket packet) { + int size = packet.size(); + if (packet.hasLength()) { // packet can be coalesced + size = Math.max(size, getMaxDatagramSize()); + } + if (size > getMaxDatagramSize()) { + + if (Log.errors()) { + var error = new AssertionError("%s: Size too big: %s > %s".formatted( + logTag(), + size, getMaxDatagramSize())); + Log.logError(logTag() + ": Packet too big: " + packet.prettyPrint()); + Log.logError(error); + } else if (debug.on()) { + var error = new AssertionError("%s: Size too big: %s > %s".formatted( + logTag(), + size, getMaxDatagramSize())); + debug.log("Packet too big: " + packet.prettyPrint()); + debug.log(error); + } + // Revisit: if we implement Path MTU detection, then the max datagram size + // may evolve, increasing or decreasing as the path change. + // In which case - we may want to tune this, down and only + // log an error or warning? + final String errMsg = "Failed to encode packet, too big: " + size; + terminator.terminate(forTransportError(PROTOCOL_VIOLATION).loggedAs(errMsg)); + throw new UncheckedIOException(terminator.getTerminationCause().getCloseCause()); + } + return getOutgoingByteBuffer(size); + } + + /** + * {@return the maximum datagram size that can be used on the + * connection path} + * @implSpec + * Initially this is {@link #DEFAULT_DATAGRAM_SIZE}, but the + * value will then be decided if the peer sends a specific size + * in the transport parameters and the value can further evolve based + * on path MTU. + */ + // this is public for use in tests + public int getMaxDatagramSize() { + // TODO: we should implement path MTU detection, or maybe let + // this be configurable. Sizes of 32256 or 64512 seem to + // be giving much better throughput when downloading. + // large files + return Math.min(maxPeerAdvertisedPayloadSize, pathMTU); + } + + /** + * Retrieves cryptographic messages from TLS engine, enqueues them for sending + * and starts the transmitter. + */ + protected void continueHandshake() { + handshakeScheduler.runOrSchedule(); + } + + protected void continueHandshake0() { + try { + continueHandshake1(); + } catch (Throwable t) { + var flow = handshakeFlow; + flow.handshakeReachedPeerCF.completeExceptionally(t); + flow.handshakeCF.completeExceptionally(t); + } + } + + private void continueHandshake1() throws IOException { + HandshakeFlow flow = handshakeFlow; + // make sure the localInitialQueue is not modified concurrently + // while we are in this loop + boolean handshakeDataAvailable = false; + boolean initialDataAvailable = false; + for (;;) { + var handshakeState = quicTLSEngine.getHandshakeState(); + if (debug.on()) { + debug.log("continueHandshake: state: %s", handshakeState); + } + if (handshakeState == QuicTLSEngine.HandshakeState.NEED_SEND_CRYPTO) { + // buffer next TLS message + KeySpace keySpace = quicTLSEngine.getCurrentSendKeySpace(); + ByteBuffer payloadBuffer; + handshakeLock.lock(); + try { + payloadBuffer = quicTLSEngine.getHandshakeBytes(keySpace); + assert payloadBuffer != null; + assert payloadBuffer.hasRemaining(); + if (keySpace == KeySpace.INITIAL) { + flow.localInitial.enqueue(payloadBuffer); + initialDataAvailable = true; + } else if (keySpace == KeySpace.HANDSHAKE) { + flow.localHandshake.enqueue(payloadBuffer); + handshakeDataAvailable = true; + } + } finally { + handshakeLock.unlock(); + } + + assert payloadBuffer != null; + if (debug.on()) { + debug.log("continueHandshake: buffered %s bytes in %s keyspace", + payloadBuffer.remaining(), keySpace); + } + } else if (handshakeState == QuicTLSEngine.HandshakeState.NEED_TASK) { + quicTLSEngine.getDelegatedTask().run(); + } else { + if (debug.on()) { + debug.log("continueHandshake: nothing to do (state: %s)", handshakeState); + } + if (initialDataAvailable) { + packetSpaces.initial.runTransmitter(); + } + if (handshakeDataAvailable && flow.localInitial.remaining() == 0) { + packetSpaces.handshake.runTransmitter(); + } + return; + } + } + } + + private boolean sendData(PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + if (packetNumberSpace != PacketNumberSpace.APPLICATION) { + // This method can be called by two packet spaces: INITIAL and HANDSHAKE. + // We need to lock to make sure that the method is not run concurrently. + handshakeLock.lock(); + try { + return sendInitialOrHandshakeData(packetNumberSpace); + } finally { + handshakeLock.unlock(); + } + } else { + return oneRttSndQueue.send1RTTData(); + } + } + + private boolean sendInitialOrHandshakeData(final PacketNumberSpace packetNumberSpace) + throws QuicKeyUnavailableException, QuicTransportException { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: Send %s data", logTag(), packetNumberSpace)); + } + final HandshakeFlow flow = handshakeFlow; + final QuicConnectionId peerConnId = peerConnectionId(); + if (packetNumberSpace == PacketNumberSpace.INITIAL && flow.localInitial.remaining() > 0) { + // process buffered initial data + byte[] token = initialToken(); + int tksize = token == null ? 0 : token.length; + PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.INITIAL); + int maxDstIdLength = isClientConnection() ? + MAX_CONNECTION_ID_LENGTH : // reserve space for the id to grow + peerConnId.length(); + int maxSrcIdLength = connectionId.length(); + // compute maxPayloadSize given maxSizeBeforeEncryption + var largestAckedPN = packetSpace.getLargestPeerAckedPN(); + var packetNumber = packetSpace.allocateNextPN(); + int maxPayloadSize = QuicPacketEncoder.computeMaxInitialPayloadSize(codingContext, 4, tksize, + maxSrcIdLength, maxDstIdLength, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + // compute how many bytes were reserved to allow smooth retransmission + // of packets + int reserved = QuicPacketEncoder.computeMaxInitialPayloadSize(codingContext, + computePacketNumberLength(packetNumber, + codingContext.largestAckedPN(PacketNumberSpace.INITIAL)), + tksize, connectionId.length(), peerConnId.length(), + SMALLEST_MAXIMUM_DATAGRAM_SIZE) - maxPayloadSize; + assert reserved >= 0 : "reserved is negative: " + reserved; + if (debug.on()) { + debug.log("reserved %s byte in initial packet", reserved); + } + if (maxPayloadSize < 5) { + // token too long, can't fit a crypto frame in this packet. Abort. + final String msg = "Initial token too large, maxPayload: " + maxPayloadSize; + terminator.terminate(TerminationCause.forException(new IOException(msg))); + return false; + } + AckFrame ackFrame = packetSpace.getNextAckFrame(false, maxPayloadSize); + int ackSize = ackFrame == null ? 0 : ackFrame.size(); + if (debug.on()) { + debug.log("ack frame size: %d", ackSize); + } + + CryptoFrame crypto = flow.localInitial.produceFrame(maxPayloadSize - ackSize); + int cryptoSize = crypto == null ? 0 : crypto.size(); + assert cryptoSize <= maxPayloadSize : cryptoSize - maxPayloadSize; + List frames = makeList(crypto, ackFrame); + + if (debug.on()) { + debug.log("building initial packet: source=%s, dest=%s", + connectionId, peerConnId); + } + OutgoingQuicPacket packet = encoder.newInitialPacket( + connectionId, peerConnId, token, + packetNumber, largestAckedPN, frames, codingContext); + int size = packet.size(); + if (debug.on()) { + debug.log("initial packet size is %d, max is %d", + size, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + } + assert size == SMALLEST_MAXIMUM_DATAGRAM_SIZE : size - SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + stateHandle.markHelloSent(); + if (debug.on()) { + debug.log("protecting initial quic hello packet for %s(%s) - %d bytes", + Arrays.toString(quicTLSEngine.getSSLParameters().getApplicationProtocols()), + peerAddress(), packet.size()); + } + pushDatagram(ProtectionRecord.single(packet, this::allocateDatagramForEncryption)); + if (flow.localHandshake.remaining() > 0) { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: local handshake has remaining, starting HANDSHAKE transmitter", logTag())); + } + packetSpaces.handshake.runTransmitter(); + } + } else if (packetNumberSpace == PacketNumberSpace.HANDSHAKE && flow.localHandshake.remaining() > 0) { + // process buffered handshake data + PacketSpace packetSpace = packetSpaces.get(PacketNumberSpace.HANDSHAKE); + AckFrame ackFrame = packetSpace.getNextAckFrame(false); + int ackSize = ackFrame == null ? 0 : ackFrame.size(); + if (debug.on()) { + debug.log("ack frame size: %d", ackSize); + } + // compute maxPayloadSize given maxSizeBeforeEncryption + var largestAckedPN = packetSpace.getLargestPeerAckedPN(); + var packetNumber = packetSpace.allocateNextPN(); + int maxPayloadSize = QuicPacketEncoder.computeMaxHandshakePayloadSize(codingContext, + packetNumber, connectionId.length(), peerConnId.length(), + SMALLEST_MAXIMUM_DATAGRAM_SIZE); + maxPayloadSize = maxPayloadSize - ackSize; + + final CryptoFrame crypto = flow.localHandshake.produceFrame(maxPayloadSize); + assert crypto != null : "Handshake data was available (" + + flow.localHandshake.remaining() + " bytes) for sending, but no CRYPTO" + + " frame was produced, for max frame size: " + maxPayloadSize; + int cryptoSize = crypto.size(); + assert cryptoSize <= maxPayloadSize : cryptoSize - maxPayloadSize; + List frames = makeList(crypto, ackFrame); + + if (debug.on()) { + debug.log("building handshake packet: source=%s, dest=%s", + connectionId, peerConnId); + } + OutgoingQuicPacket packet = encoder.newHandshakePacket( + connectionId, peerConnId, + packetNumber, largestAckedPN, frames, codingContext); + int size = packet.size(); + if (debug.on()) { + debug.log("handshake packet size is %d, max is %d", + size, SMALLEST_MAXIMUM_DATAGRAM_SIZE); + } + assert size <= SMALLEST_MAXIMUM_DATAGRAM_SIZE : size - SMALLEST_MAXIMUM_DATAGRAM_SIZE; + + if (debug.on()) { + debug.log("protecting handshake quic hello packet for %s(%s) - %d bytes", + Arrays.toString(quicTLSEngine.getSSLParameters().getApplicationProtocols()), + peerAddress(), packet.size()); + } + pushDatagram(ProtectionRecord.single(packet, this::allocateDatagramForEncryption)); + var handshakeState = quicTLSEngine.getHandshakeState(); + if (debug.on()) { + debug.log("Handshake state is now: %s", handshakeState); + } + if (flow.localHandshake.remaining() == 0 + && quicTLSEngine.isTLSHandshakeComplete() + && !flow.handshakeCF.isDone()) { + if (stateHandle.markHandshakeComplete()) { + if (debug.on()) { + debug.log("Handshake completed"); + } + completeHandshakeCF(); + } + } + if (!packetSpaces.initial().isClosed() && flow.localInitial.remaining() > 0) { + if (Log.quicCrypto()) { + Log.logQuic(String.format("%s: local initial has remaining, starting INITIAL transmitter", logTag())); + } + packetSpaces.initial.runTransmitter(); + } + } else { + return false; + } + return true; + } + + public QuicTransportParameters peerTransportParameters() { + return peerTransportParameters; + } + + public QuicTransportParameters localTransportParameters() { + return localTransportParameters; + } + + protected void consumeQuicParameters(final ByteBuffer byteBuffer) throws QuicTransportException { + final QuicTransportParameters params = QuicTransportParameters.decode(byteBuffer); + if (debug.on()) { + debug.log("Received peer Quic transport params: %s", params.toStringWithValues()); + } + final QuicConnectionId retryConnId = this.peerConnIdManager.retryConnId(); + if (params.isPresent(ParameterId.retry_source_connection_id)) { + if (retryConnId == null) { + throw new QuicTransportException("Retry connection ID was set even though no retry was performed", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } else if (!params.matches(ParameterId.retry_source_connection_id, retryConnId)) { + throw new QuicTransportException("Retry connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } else { + if (retryConnId != null) { + throw new QuicTransportException("Retry connection ID was expected but absent", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + if (!params.isPresent(ParameterId.original_destination_connection_id)) { + throw new QuicTransportException( + "Original connection ID transport parameter missing", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + + } + if (!params.isPresent(initial_source_connection_id)) { + throw new QuicTransportException( + "Initial source connection ID transport parameter missing", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + + } + final QuicConnectionId clientSelectedPeerConnId = this.peerConnIdManager.originalServerConnId(); + if (!params.matches(ParameterId.original_destination_connection_id, clientSelectedPeerConnId)) { + throw new QuicTransportException( + "Original connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (!params.matches(initial_source_connection_id, incomingInitialPacketSourceId)) { + throw new QuicTransportException( + "Initial source connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + // RFC-9000, section 18.2: A server that chooses a zero-length connection ID MUST NOT + // provide a preferred address. + if (peerConnectionId().length() == 0 && + params.isPresent(ParameterId.preferred_address)) { + throw new QuicTransportException( + "Preferred address present but connection ID has zero length", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(active_connection_id_limit)) { + final long limit = params.getIntParameter(active_connection_id_limit); + if (limit < 2) { + throw new QuicTransportException( + "Invalid active_connection_id_limit " + limit, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + if (params.isPresent(ParameterId.stateless_reset_token)) { + final byte[] statelessResetToken = params.getParameter(ParameterId.stateless_reset_token); + if (statelessResetToken.length != RESET_TOKEN_LENGTH) { + // RFC states 16 bytes for stateless token + throw new QuicTransportException( + "Invalid stateless reset token length " + statelessResetToken.length, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + VersionInformation vi = + params.getVersionInformationParameter(version_information); + if (vi != null) { + if (vi.chosenVersion() != quicVersion().versionNumber()) { + throw new QuicTransportException( + "[version_information] Chosen Version does not match version in use", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + if (processedVersionsPacket) { + if (vi.availableVersions().length == 0) { + throw new QuicTransportException( + "[version_information] available versions empty", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + if (Arrays.stream(vi.availableVersions()) + .anyMatch(i -> i == originalVersion.versionNumber())) { + throw new QuicTransportException( + "[version_information] original version was available", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + } else { + if (processedVersionsPacket && quicVersion != QuicVersion.QUIC_V1) { + throw new QuicTransportException( + "version_information parameter absent", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + handleIncomingPeerTransportParams(params); + + // params.setIntParameter(ParameterId.max_idle_timeout, TimeUnit.SECONDS.toMillis(30)); + // params.setParameter(ParameterId.stateless_reset_token, ...); // no token + // params.setIntParameter(ParameterId.initial_max_data, DEFAULT_INITIAL_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_bidi_local, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_bidi_remote, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_stream_data_uni, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(ParameterId.initial_max_streams_bidi, DEFAULT_MAX_STREAMS); + // params.setIntParameter(ParameterId.initial_max_streams_uni, DEFAULT_MAX_STREAMS); + // params.setIntParameter(ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(ParameterId.disable_active_migration, false); + // params.setPreferredAddressParameter(ParameterId.preferred_address, ...); + // params.setIntParameter(ParameterId.active_connection_id_limit, 2); + } + + /** + * {@return the number of (active) connection ids that this endpoint is willing + * to accept from the peer for a given connection} + */ + protected long getLocalActiveConnIDLimit() { + // currently we don't accept anything more than 2 (the RFC defined default minimum) + return 2; + } + + /** + * {@return the number of (active) connection ids that the peer is willing to accept + * for a given connection} + */ + protected long getPeerActiveConnIDLimit() { + return this.peerActiveConnIdsLimit; + } + + protected ByteBuffer buildInitialParameters() { + final QuicTransportParameters params = new QuicTransportParameters(this.transportParams); + setIntParamIfNotSet(params, active_connection_id_limit, this::getLocalActiveConnIDLimit); + final long idleTimeoutMillis = TimeUnit.SECONDS.toMillis( + Utils.getLongProperty("jdk.httpclient.quic.idleTimeout", 30)); + setIntParamIfNotSet(params, max_idle_timeout, () -> idleTimeoutMillis); + setIntParamIfNotSet(params, max_udp_payload_size, () -> { + assert this.endpoint != null : "Endpoint hasn't been set"; + return (long) this.endpoint.getMaxUdpPayloadSize(); + }); + setIntParamIfNotSet(params, initial_max_data, () -> DEFAULT_INITIAL_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_local, + () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_uni, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_remote, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_streams_uni, () -> (long) DEFAULT_MAX_UNI_STREAMS); + setIntParamIfNotSet(params, initial_max_streams_bidi, () -> (long) DEFAULT_MAX_BIDI_STREAMS); + if (!params.isPresent(initial_source_connection_id)) { + params.setParameter(initial_source_connection_id, connectionId.getBytes()); + } + if (!params.isPresent(version_information)) { + final VersionInformation vi = + QuicTransportParameters.buildVersionInformation(quicVersion, + quicInstance().getAvailableVersions()); + params.setVersionInformationParameter(version_information, vi); + } + // params.setIntParameter(ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(ParameterId.disable_active_migration, false); + final ByteBuffer buf = ByteBuffer.allocate(params.size()); + params.encode(buf); + buf.flip(); + if (debug.on()) { + debug.log("local transport params: %s", params.toStringWithValues()); + } + newLocalTransportParameters(params); + return buf; + } + + protected static void setIntParamIfNotSet(final QuicTransportParameters params, + final ParameterId paramId, + final Supplier valueSupplier) { + if (params.isPresent(paramId)) { + return; + } + params.setIntParameter(paramId, valueSupplier.get()); + } + + // the token to be included in initial packets, if any. + private byte[] initialToken() { + return initialToken; + } + + protected void newLocalTransportParameters(QuicTransportParameters params) { + localTransportParameters = params; + oneRttRcvQueue.newLocalParameters(params); + streams.newLocalTransportParameters(params); + final long idleTimeout = params.getIntParameter(max_idle_timeout, 0); + this.idleTimeoutManager.localIdleTimeout(idleTimeout); + } + + private List ackOrPing(AckFrame ack, boolean sendPing) { + if (sendPing) { + return ack == null ? List.of(new PingFrame()) : List.of(new PingFrame(), ack); + } + assert ack != null; + return List.of(ack); + } + + /** + * Emit a possibly non ACK-eliciting packet containing the given ACK frame. + * @param packetSpaceManager the packet space manager on behalf + * of which the acknowledgement should + * be sent. + * @param ackFrame the ACK frame to be sent. + * @param sendPing whether a PING frame should be sent. + * @return the emitted packet number, or -1L if not applicable or not emitted + */ + private long emitAckPacket(final PacketSpace packetSpaceManager, final AckFrame ackFrame, + final boolean sendPing) + throws QuicKeyUnavailableException, QuicTransportException { + if (ackFrame == null && !sendPing) { + return -1L; + } + if (debug.on()) { + if (sendPing) { + debug.log("Sending PING packet %s ack", + ackFrame == null ? "without" : "with"); + } else { + debug.log("sending ACK packet"); + } + } + final List frames = ackOrPing(ackFrame, sendPing); + final PacketNumberSpace packetNumberSpace = packetSpaceManager.packetNumberSpace(); + if (debug.on()) { + debug.log("Sending packet for %s, frame=%s", packetNumberSpace, frames); + } + final KeySpace keySpace = switch (packetNumberSpace) { + case APPLICATION -> KeySpace.ONE_RTT; + case HANDSHAKE -> KeySpace.HANDSHAKE; + case INITIAL -> KeySpace.INITIAL; + default -> throw new UnsupportedOperationException( + "Invalid packet number space: " + packetNumberSpace); + }; + if (sendPing && Log.quicRetransmit()) { + Log.logQuic("{0} {1}: sending PingFrame", logTag(), keySpace); + } + final QuicPacket ackpacket = encoder.newOutgoingPacket(keySpace, + packetSpaceManager, localConnectionId(), + peerConnectionId(), initialToken(), frames, codingContext); + pushDatagram(ProtectionRecord.single(ackpacket, this::allocateDatagramForEncryption)); + return ackpacket.packetNumber(); + } + + private LinkedList removeOutdatedFrames(List frames) { + // Remove frames that should not be retransmitted + LinkedList result = new LinkedList<>(); + for (QuicFrame f : frames) { + if (!(f instanceof PaddingFrame) && + !(f instanceof AckFrame) && + !(f instanceof PathChallengeFrame) && + !(f instanceof PathResponseFrame)) { + result.add(f); + } + } + return result; + } + + /** + * Retransmit the given packet on behalf of the given packet space + * manager. + * @param packetSpaceManager the packet space manager on behalf of + * which the packet is being retransmitted + * @param packet the unacknowledged packet which should be retransmitted + */ + private void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) + throws QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) { + debug.log("Retransmitting packet [type=%s, pn=%d, attempts:%d]: %s", + packet.packetType(), packet.packetNumber(), attempts, packet); + } + + assert packetSpaceManager.packetNumberSpace() == packet.numberSpace(); + long oldPacketNumber = packet.packetNumber(); + assert oldPacketNumber >= 0; + + long largestAckedPN = packetSpaceManager.getLargestPeerAckedPN(); + long newPacketNumber = packetSpaceManager.allocateNextPN(); + final int maxDatagramSize = getMaxDatagramSize(); + final QuicConnectionId peerConnectionId = peerConnectionId(); + final int dstIdLength = peerConnectionId.length(); + final PacketNumberSpace packetNumberSpace = packetSpaceManager.packetNumberSpace(); + final int initialDstIdLength = MAX_CONNECTION_ID_LENGTH; // reserve space for the ID to grow + + int maxPayloadSize = switch (packetNumberSpace) { + case APPLICATION -> QuicPacketEncoder.computeMaxOneRTTPayloadSize( + codingContext, newPacketNumber, dstIdLength, maxDatagramSize, largestAckedPN); + case INITIAL -> QuicPacketEncoder.computeMaxInitialPayloadSize( + codingContext, computePacketNumberLength(newPacketNumber, + codingContext.largestAckedPN(PacketNumberSpace.INITIAL)), + ((InitialPacket) packet).tokenLength(), + localConnectionId().length(), initialDstIdLength, maxDatagramSize); + case HANDSHAKE -> QuicPacketEncoder.computeMaxHandshakePayloadSize( + codingContext, newPacketNumber, localConnectionId().length(), + dstIdLength, maxDatagramSize); + default -> throw new IllegalArgumentException( + "Invalid packet number space: " + packetNumberSpace); + }; + + // The new packet may have larger size(), which might no longer fit inside + // the maximum datagram size supported on the path. To avoid that, we + // strip the padding and old ack frame from the original packet, and + // include the new ack frame only if it fits in the available size. + LinkedList frames = removeOutdatedFrames(packet.frames()); + int size = frames.stream().mapToInt(QuicFrame::size).sum(); + int remaining = maxPayloadSize - size; + AckFrame ack = packetSpaceManager.getNextAckFrame(false, remaining); + if (ack != null) { + assert ack.size() <= remaining : "AckFrame size %s is bigger than %s" + .formatted(ack.size(), remaining); + frames.addFirst(ack); + } + QuicPacket retransmitted = + switch (packet.packetType()) { + case INITIAL -> encoder.newInitialPacket(localConnectionId(), + peerConnectionId, ((InitialPacket) packet).token(), + newPacketNumber, largestAckedPN, frames, + codingContext); + case HANDSHAKE -> encoder.newHandshakePacket(localConnectionId(), + peerConnectionId, newPacketNumber, largestAckedPN, + frames, codingContext); + case ONERTT, ZERORTT -> encoder.newOneRttPacket( + peerConnectionId, newPacketNumber, largestAckedPN, + frames, codingContext); + default -> throw new IllegalArgumentException("packetType: %s, packet: %s" + .formatted(packet.packetType(), packet.packetNumber())); + }; + + if (Log.quicRetransmit()) { + Log.logQuic("%s OUT: retransmitting packet [%s] pn:%s as pn:%s".formatted( + logTag(), packet.packetType(), oldPacketNumber, newPacketNumber)); + } + pushDatagram(ProtectionRecord.retransmitting(retransmitted, + oldPacketNumber, + this::allocateDatagramForEncryption)); + } + + @Override + public CompletableFuture requestSendPing() { + final KeySpace space = quicTLSEngine.getCurrentSendKeySpace(); + final PacketSpace spaceManager = packetSpaces.get(PacketNumberSpace.of(space)); + return spaceManager.requestSendPing(); + } + + /** + * {@return the underlying {@code NetworkChannel} used by this connection, + * which may be {@code null} if the endpoint has not been configured yet} + */ + public NetworkChannel channel() { + QuicEndpoint endpoint = this.endpoint; + return endpoint == null ? null : endpoint.channel(); + } + + @Override + public String toString() { + return cachedToString; + } + + @Override + public boolean isOpen() { + return stateHandle.opened(); + } + + @Override + public TerminationCause terminationCause() { + return terminator.getTerminationCause(); + } + + public final CompletableFuture futureTerminationCause() { + return terminator.futureTerminationCause(); + } + + @Override + public ConnectionTerminator connectionTerminator() { + return this.terminator; + } + + /** + * {@return true if this connection is a client connection} + * Server side connections will return false. + */ + public boolean isClientConnection() { + return true; + } + + /** + * Called when new quic transport parameters are available from the peer. + * @param params the peer's new quic transport parameter + */ + protected void handleIncomingPeerTransportParams(final QuicTransportParameters params) { + peerTransportParameters = params; + this.idleTimeoutManager.peerIdleTimeout(params.getIntParameter(max_idle_timeout)); + // when we reach here, the value for max_udp_payload_size has already been + // asserted that it isn't outside the allowed range of 1200 to 65527. That has + // happened in QuicTransportParameters.checkParameter(). + // intentional cast to int since the value will be within int range + maxPeerAdvertisedPayloadSize = (int) params.getIntParameter(max_udp_payload_size); + congestionController.updateMaxDatagramSize(getMaxDatagramSize()); + if (params.isPresent(ParameterId.initial_max_data)) { + oneRttSndQueue.setMaxData(params.getIntParameter(ParameterId.initial_max_data), true); + } + streams.newPeerTransportParameters(params); + packetSpaces.app().updatePeerTransportParameters( + params.getIntParameter(ParameterId.max_ack_delay), + params.getIntParameter(ParameterId.ack_delay_exponent)); + // param value for this param is already validated outside of this method, so we just + // set the value without any validations + this.peerActiveConnIdsLimit = params.getIntParameter(active_connection_id_limit); + if (params.isPresent(ParameterId.stateless_reset_token)) { + // the stateless reset token for the handshake connection id + final byte[] statelessResetToken = params.getParameter(ParameterId.stateless_reset_token); + // register with peer connid manager + this.peerConnIdManager.handshakeStatelessResetToken(statelessResetToken); + } + if (params.isPresent(ParameterId.preferred_address)) { + final byte[] val = params.getParameter(ParameterId.preferred_address); + final ByteBuffer preferredConnId = QuicTransportParameters.getPreferredConnectionId(val); + final byte[] preferredStatelessResetToken = QuicTransportParameters + .getPreferredStatelessResetToken(val); + this.peerConnIdManager.handlePreferredAddress(preferredConnId, preferredStatelessResetToken); + } + if (debug.on()) { + debug.log("incoming peer parameters handled"); + } + } + + protected void incomingInitialFrame(final AckFrame frame) throws QuicTransportException { + packetSpaces.initial.processAckFrame(frame); + if (!handshakeFlow.handshakeReachedPeerCF.isDone()) { + if (debug.on()) debug.log("completing handshakeStartedCF normally"); + handshakeFlow.handshakeReachedPeerCF.complete(null); + } + } + + protected int incomingInitialFrame(final CryptoFrame frame) throws QuicTransportException { + // make sure to provide the frames in order, and + // buffer them if at the wrong offset + if (!handshakeFlow.handshakeReachedPeerCF.isDone()) { + if (debug.on()) debug.log("completing handshakeStartedCF normally"); + handshakeFlow.handshakeReachedPeerCF.complete(null); + } + final CryptoDataFlow peerInitial = handshakeFlow.peerInitial; + final long buffer = frame.offset() + frame.length() - peerInitial.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.INITIAL, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + int provided = 0; + CryptoFrame nextFrame = peerInitial.receive(frame); + while (nextFrame != null) { + if (debug.on()) { + debug.log("Provide crypto frame to engine: %s", nextFrame); + } + quicTLSEngine.consumeHandshakeBytes(KeySpace.INITIAL, nextFrame.payload()); + provided += nextFrame.length(); + nextFrame = peerInitial.poll(); + if (debug.on()) { + debug.log("Provided: " + provided); + } + } + return provided; + } + + protected void incomingInitialFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingInitialFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingInitialFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incomingHandshakeFrame(final AckFrame frame) throws QuicTransportException { + packetSpaces.handshake.processAckFrame(frame); + } + + protected int incomingHandshakeFrame(final CryptoFrame frame) throws QuicTransportException { + final CryptoDataFlow peerHandshake = handshakeFlow.peerHandshake; + // make sure to provide the frames in order, and + // buffer them if at the wrong offset + final long buffer = frame.offset() + frame.length() - peerHandshake.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.HANDSHAKE, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + int provided = 0; + CryptoFrame nextFrame = peerHandshake.receive(frame); + while (nextFrame != null) { + quicTLSEngine.consumeHandshakeBytes(KeySpace.HANDSHAKE, nextFrame.payload()); + provided += nextFrame.length(); + nextFrame = peerHandshake.poll(); + } + return provided; + } + + protected void incomingHandshakeFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingHandshakeFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incomingHandshakeFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incoming1RTTFrame(final AckFrame ackFrame) throws QuicTransportException { + packetSpaces.app.processAckFrame(ackFrame); + } + + protected void incoming1RTTFrame(final StreamFrame frame) throws QuicTransportException { + final long streamId = frame.streamId(); + final QuicReceiverStream stream = getReceivingStream(streamId, frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final CryptoFrame frame) throws QuicTransportException { + final long buffer = frame.offset() + frame.length() - peerCryptoFlow.offset(); + if (buffer > MAX_INCOMING_CRYPTO_CAPACITY) { + throw new QuicTransportException( + "Crypto buffer exceeded, required: " + buffer, + KeySpace.ONE_RTT, frame.frameType(), + QuicTransportErrors.CRYPTO_BUFFER_EXCEEDED); + } + CryptoFrame nextFrame = peerCryptoFlow.receive(frame); + while (nextFrame != null) { + quicTLSEngine.consumeHandshakeBytes(KeySpace.ONE_RTT, nextFrame.payload()); + nextFrame = peerCryptoFlow.poll(); + } + } + + protected void incoming1RTTFrame(final ResetStreamFrame frame) throws QuicTransportException { + final long streamId = frame.streamId(); + final QuicReceiverStream stream = getReceivingStream(streamId, frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final StreamDataBlockedFrame frame) + throws QuicTransportException { + final QuicReceiverStream stream = getReceivingStream(frame.streamId(), frame.getTypeField()); + if (stream != null) { + assert frame.streamId() == stream.streamId(); + streams.processIncomingFrame(stream, frame); + } + } + + protected void incoming1RTTFrame(final DataBlockedFrame frame) throws QuicTransportException { + // TODO implement similar logic as STREAM_DATA_BLOCKED frame receipt + // and increment gradually if consumption is more than 1/4th the window size of the + // connection + } + + protected void incoming1RTTFrame(final StreamsBlockedFrame frame) + throws QuicTransportException { + if (frame.maxStreams() > MAX_STREAMS_VALUE_LIMIT) { + throw new QuicTransportException("Invalid maxStreams value %s" + .formatted(frame.maxStreams()), + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + streams.peerStreamsBlocked(frame); + } + + protected void incoming1RTTFrame(final PaddingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incoming1RTTFrame(final MaxDataFrame frame) throws QuicTransportException { + oneRttSndQueue.setMaxData(frame.maxData(), false); + } + + protected void incoming1RTTFrame(final MaxStreamDataFrame frame) + throws QuicTransportException { + final long streamId = frame.streamID(); + final QuicSenderStream stream = getSendingStream(streamId, frame.getTypeField()); + if (stream != null) { + streams.setMaxStreamData(stream, frame.maxStreamData()); + } + } + + protected void incoming1RTTFrame(final MaxStreamsFrame frame) throws QuicTransportException { + if (frame.maxStreams() >> 60 != 0) { + throw new QuicTransportException("Invalid maxStreams value %s" + .formatted(frame.maxStreams()), + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + final boolean increased = streams.tryIncreaseStreamLimit(frame); + if (debug.on()) { + debug.log((increased ? "increased" : "did not increase") + + " " + (frame.isBidi() ? "bidi" : "uni") + + " stream limit to " + frame.maxStreams()); + } + } + + protected void incoming1RTTFrame(final StopSendingFrame frame) throws QuicTransportException { + final long streamId = frame.streamID(); + final QuicSenderStream stream = getSendingStream(streamId, frame.getTypeField()); + if (stream != null) { + streams.stopSendingReceived(stream, + frame.errorCode()); + } + } + + protected void incoming1RTTFrame(final PingFrame frame) throws QuicTransportException { + // nothing to do + } + + protected void incoming1RTTFrame(final ConnectionCloseFrame frame) + throws QuicTransportException { + terminator.incomingConnectionCloseFrame(frame); + } + + protected void incoming1RTTFrame(final HandshakeDoneFrame frame) + throws QuicTransportException { + if (quicTLSEngine.tryReceiveHandshakeDone()) { + // now that HANDSHAKE_DONE is received (and thus handshake confirmed), + // close the HANDSHAKE packet space (and thus discard the keys) + if (debug.on()) { + debug.log("received HANDSHAKE_DONE from server, initiating close of" + + " HANDSHAKE packet space"); + } + packetSpaces.handshake.close(); + } + packetSpaces.app.confirmHandshake(); + } + + protected void incoming1RTTFrame(final NewConnectionIDFrame frame) + throws QuicTransportException { + if (peerConnectionId().length() == 0) { + throw new QuicTransportException( + "NEW_CONNECTION_ID not allowed here", + null, frame.getTypeField(), PROTOCOL_VIOLATION); + } + this.peerConnIdManager.handleNewConnectionIdFrame(frame); + } + + protected void incoming1RTTFrame(final OneRttPacket oneRttPacket, + final RetireConnectionIDFrame frame) + throws QuicTransportException { + this.localConnIdManager.handleRetireConnectionIdFrame(oneRttPacket.destinationId(), + PacketType.ONERTT, frame); + } + + protected void incoming1RTTFrame(final NewTokenFrame frame) throws QuicTransportException { + // as per RFC 9000, section 19.7, token cannot be empty and if it is, then + // a connection error of type FRAME_ENCODING_ERROR needs to be raised + final byte[] newToken = frame.token(); + if (newToken.length == 0) { + throw new QuicTransportException("Empty token in NEW_TOKEN frame", + KeySpace.ONE_RTT, + frame.getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + assert this.quicInstance instanceof QuicClient : "Not a QuicClient"; + final QuicClient quicClient = (QuicClient) this.quicInstance; + // set this as the initial token to be used in INITIAL packets when attempting + // any new subsequent connections against this same target server + quicClient.registerInitialToken(this.peerAddress, newToken); + if (debug.on()) { + debug.log("Registered a new (initial) token for peer " + this.peerAddress); + } + } + + protected void incoming1RTTFrame(final PathResponseFrame frame) + throws QuicTransportException { + throw new QuicTransportException("Unmatched PATH_RESPONSE frame", + KeySpace.ONE_RTT, + frame.getTypeField(), PROTOCOL_VIOLATION); + } + + protected void incoming1RTTFrame(final PathChallengeFrame frame) + throws QuicTransportException { + pathChallengeFrameQueue.offer(frame); + if (pathChallengeFrameQueue.size() > 3) { + // we don't expect to hold more than 1 PathChallenge per path. + // If there's more than 3 outstanding challenges, drop the oldest one. + pathChallengeFrameQueue.poll(); + } + } + + /** + * Signal the connection that some stream data is available for sending on one or more streams. + * @param streamIds the stream ids + */ + public void streamDataAvailableForSending(final Set streamIds) { + for (final long id : streamIds) { + streams.enqueueForSending(id); + } + packetSpaces.app.runTransmitter(); + } + + /** + * Called when the receiving part or the sending part of a stream + * reaches a terminal state. + * @param streamId the id of the stream + * @param state the terminal state + */ + public void notifyTerminalState(long streamId, StreamState state) { + assert state.isTerminal() : state; + streams.notifyTerminalState(streamId, state); + } + + /** + * Called to request sending of a RESET_STREAM frame. + * + * @apiNote + * Should only be called for sending streams. For stopping a + * receiving stream then {@link #scheduleStopSendingFrame(long, long)} should be called. + * This method should only be called from {@code QuicSenderStreamImpl}, after + * switching the state of the stream to RESET_SENT. + * + * @param streamId the id of the stream that should be reset + * @param errorCode the application error code + */ + public void requestResetStream(long streamId, long errorCode) { + assert streams.isSendingStream(streamId); + streams.requestResetStream(streamId, errorCode); + packetSpaces.app.runTransmitter(); + } + + /** + * Called to request sending of a STOP_SENDING frame. + * @apiNote + * Should only be called for receiving streams. For stopping a + * sending stream then {@link #requestResetStream(long, long)} + * should be called. + * This method should only be called from {@code QuicReceiverStreamImpl} + * @param streamId the stream id to be cancelled + * @param errorCode the application error code + */ + public void scheduleStopSendingFrame(long streamId, long errorCode) { + assert streams.isReceivingStream(streamId); + streams.scheduleStopSendingFrame(new StopSendingFrame(streamId, errorCode)); + packetSpaces.app.runTransmitter(); + } + + /** + * Called to request sending of a MAX_STREAM_DATA frame. + * @apiNote + * Should only be called for receiving streams. + * This method should only be called from {@code QuicReceiverStreamImpl} + * @param streamId the stream id to be cancelled + * @param maxStreamData the new max data we are prepared to receive on + * this stream + */ + public void requestSendMaxStreamData(long streamId, long maxStreamData) { + assert streams.isReceivingStream(streamId); + streams.requestSendMaxStreamData(new MaxStreamDataFrame(streamId, maxStreamData)); + packetSpaces.app.runTransmitter(); + } + + /** + * Called when frame data can be safely added to the amount of + * data received by the connection for MAX_DATA flow control + * purpose. + * @throws QuicTransportException if flow control was exceeded + * @param frameType type of frame received + */ + public void increaseReceivedData(long diff, long frameType) throws QuicTransportException { + oneRttRcvQueue.checkAndIncreaseReceivedData(diff, frameType); + } + + /** + * Called when frame data is removed from the connection + * and the amount of data can be added to MAX_DATA window. + * @param diff amount of data processed + */ + public void increaseProcessedData(long diff) { + oneRttRcvQueue.increaseProcessedData(diff); + } + + public QuicTLSEngine getTLSEngine() { + return quicTLSEngine; + } + + /** + * {@return the computed PTO for the current packet number space, + * adjusted by our max ack delay} + */ + public long peerPtoMs() { + return rttEstimator.getBasePtoDuration().toMillis() + + (quicTLSEngine.getCurrentSendKeySpace() == KeySpace.ONE_RTT ? + PacketSpaceManager.ADVERTISED_MAX_ACK_DELAY : 0); + } + + public void runAppPacketSpaceTransmitter() { + this.packetSpaces.app.runTransmitter(); + } + + public void shutdown() { + packetSpaces.close(); + } + + public final String logTag() { + return logTag; + } + + /* ======================================================== + * Direct Byte Buffer Pool + * ======================================================== */ + + // Maximum size of the connection's Direct ByteBuffer Pool. + // For a connection configured to attempt sending datagrams in thread + // (QuicEndpoint.SEND_DGRAM_ASYNC == false), 2 should be enough, as we + // shouldn't have more than 2 packet number spaces active at the same time. + private static final int MAX_DBB_POOL_SIZE = 3; + // The ByteBuffer pool, which contains available byte buffers + private final ConcurrentLinkedQueue bbPool = new ConcurrentLinkedQueue<>(); + // The number of Direct Byte Buffers allocated for sending and managed by the pool. + // This is the number of Direct Byte Buffers currently in flight, plus the number + // of available byte buffers present in the pool. It will never exceed + // MAX_DBB_POOL_SIZE. + private final AtomicInteger bbAllocated = new AtomicInteger(); + + // Some counters used for printing debug statistics when Log quic:dbb is enabled + // Byte Buffers in flight: the number of byte buffers that were returned by + // getOutgoingByteBuffer() minus the number of byte buffers that were released + // through datagramReleased() + private final AtomicInteger bbInFlight = new AtomicInteger(); + // Peak number of byte buffers in flight. Never decreases. + private final AtomicInteger bbPeak = new AtomicInteger(); + // Number of unreleased byte buffers. This should eventually reach 0. + final AtomicInteger bbUnreleased = new AtomicInteger(); + + /** + * {@return a new {@code ByteBuffer} to encode and encrypt packets in a datagram} + * This method may either allocate a new heap BteBuffer or return a (possibly + * new) Direct ByteBuffer from the connection's Direct Byte Buffer Pool. + * @param size the maximum size of the datagram + */ + protected ByteBuffer getOutgoingByteBuffer(int size) { + bbUnreleased.incrementAndGet(); + if (USE_DIRECT_BUFFER_POOL) { + if (size <= getMaxDatagramSize()) { + ByteBuffer buffer = bbPool.poll(); + if (buffer != null) { + if (buffer.limit() >= getMaxDatagramSize()) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: got direct buffer from pool" + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return buffer; + } + bbAllocated.decrementAndGet(); + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: releasing direct buffer"); + } + buffer = null; + } + + assert buffer == null; + int allocated; + while ((allocated = bbAllocated.get()) < MAX_DBB_POOL_SIZE) { + if (bbAllocated.compareAndSet(allocated, allocated + 1)) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: allocating direct buffer #" + (allocated + 1) + + ", inFlight: " + bbInFlight.get() + ", peak: " + + bbPeak.get() + ", unreleased:" + bbUnreleased.get()); + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return ByteBuffer.allocateDirect(getMaxDatagramSize()); + } + } + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: too many buffers allocated: " + allocated + + ", inFlight: " + bbInFlight.get() + ", peak: " + + bbPeak.get() + ", unreleased:" + bbUnreleased.get()); + } + + } else { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: wrong size " + size); + } + } + } + int inFlight = bbInFlight.incrementAndGet(); + if (inFlight > bbPeak.get()) { + synchronized (this) { + if (inFlight > bbPeak.get()) bbPeak.set(inFlight); + } + } + return ByteBuffer.allocate(size); + } + @Override + public void datagramSent(QuicDatagram datagram) { + datagramReleased(datagram); + } + + @Override + public void datagramDiscarded(QuicDatagram datagram) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram discarded " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + datagramReleased(datagram); + } + + public void datagramDropped(QuicDatagram datagram) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram dropped " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + datagramReleased(datagram); + } + + /** + * Returns a {@link jdk.internal.net.http.quic.QuicEndpoint.Datagram} which contains + * an encrypted QUIC packet containing + * a {@linkplain ConnectionCloseFrame CONNECTION_CLOSE frame}. The CONNECTION_CLOSE + * frame will have a frame type of {@code 0x1c} and error code of {@code NO_ERROR}. + *

        + * This method should only be invoked when the {@link QuicEndpoint} is being closed + * and the endpoint wants to send out a {@code CONNECTION_CLOSE} frame on a best-effort + * basis (in a fire and forget manner). + * + * @return the datagram containing the QUIC packet with a CONNECTION_CLOSE frame or + * an {@linkplain Optional#empty() empty Optional} if the datagram couldn't + * be constructed. + */ + final Optional connectionCloseDatagram() { + try { + final ByteBuffer quicPktPayload = this.terminator.makeConnectionCloseDatagram(); + return Optional.of(new QuicDatagram(this, peerAddress, quicPktPayload)); + } catch (Exception e) { + // ignore any exception because providing the connection close datagram + // when the endpoint is being closed, is on best-effort basis + return Optional.empty(); + } + } + + /** + * Called when a datagram is being released, either from + * {@link #datagramSent(QuicDatagram)}, {@link #datagramDiscarded(QuicDatagram)}, + * or {@link #datagramDropped(QuicDatagram)}. + * This method may either release the datagram and let it get garbage collected, + * or return it to the pool. + * @param datagram the released datagram + */ + protected void datagramReleased(QuicDatagram datagram) { + bbUnreleased.decrementAndGet(); + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: datagram released " + datagram.payload().isDirect() + + ", inFlight: " + bbInFlight.get() + ", peak: " + bbPeak.get() + + ", unreleased:" + bbUnreleased.get()); + } + bbInFlight.decrementAndGet(); + if (USE_DIRECT_BUFFER_POOL) { + ByteBuffer buffer = datagram.payload(); + buffer.clear(); + if (buffer.isDirect()) { + if (buffer.limit() >= getMaxDatagramSize()) { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: offering buffer to pool"); + } + bbPool.offer(buffer); + } else { + if (Log.quicDBB()) { + Log.logQuic("[" + Thread.currentThread().getName() + "] " + + logTag() + ": DIRECTBB: releasing direct buffer (too small)"); + } + bbAllocated.decrementAndGet(); + } + } + } + } + + public String loggableState() { + // for HTTP3 debugging + // If the connection was active (open bidi streams), log connection state + if (streams.quicStreams().noneMatch(QuicStream::isBidirectional)) { + // no active requests + return "No active requests"; + } + Deadline now = TimeSource.now(); + StringBuilder result = new StringBuilder("sending: {canSend:" + oneRttSndQueue.canSend() + + ", credit: " + oneRttSndQueue.credit() + + ", sendersReady: " + streams.hasAvailableData() + + ", hasControlFrames: " + streams.hasControlFrames() + + "}, cc: { backoff: " + rttEstimator.getPtoBackoff() + + ", duration: " + ((PacketSpaceManager) packetSpaces.app).getPtoDuration() + + ", current deadline: " + Utils.debugDeadline(now, + ((PacketSpaceManager) packetSpaces.app).deadline()) + + ", prospective deadline: " + Utils.debugDeadline(now, + ((PacketSpaceManager) packetSpaces.app).prospectiveDeadline()) + + "}, streams: ["); + streams.quicStreams().filter(QuicStream::isBidirectional).forEach( + s -> { + QuicBidiStreamImpl qb = (QuicBidiStreamImpl) s; + result.append("{id:" + s.streamId() + + ", available: " + qb.senderPart().available() + + ", blocked: " + qb.senderPart().isBlocked() + "}," + ); + } + ); + result.append("]"); + return result.toString(); + } + + /** + * {@return true if the packet contains a CONNECTION_CLOSE frame, false otherwise} + * @param packet the QUIC packet + */ + private static boolean containsConnectionClose(final QuicPacket packet) { + for (final QuicFrame frame : packet.frames()) { + if (frame instanceof ConnectionCloseFrame) { + return true; + } + } + return false; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java new file mode 100644 index 00000000000..3df9b599f82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicEndpoint.java @@ -0,0 +1,2062 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketOption; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicSelector.QuicNioSelector; +import jdk.internal.net.http.quic.QuicSelector.QuicVirtualThreadPoller; +import jdk.internal.net.http.quic.packets.QuicPacket.HeadersType; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.util.OperatingSystem; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; + +import static jdk.internal.net.http.quic.QuicEndpoint.ChannelType.BLOCKING_WITH_VIRTUAL_THREADS; +import static jdk.internal.net.http.quic.QuicEndpoint.ChannelType.NON_BLOCKING_WITH_SELECTOR; +import static jdk.internal.net.http.quic.TerminationCause.forSilentTermination; + + +/** + * A QUIC Endpoint. A QUIC endpoint encapsulate a DatagramChannel + * and is registered with a Selector. It subscribes for read and + * write events from the selector, and implements a readLoop and + * a writeLoop. + *

        + * The read event or write event are triggered by the selector + * thread. When the read event is triggered, all available datagrams + * are read from the channel and pushed into a read queue. + * Then the readLoop is triggered. + * When the write event is triggered, the key interestOps are + * modified to pause write events, and the writeLoop is triggered. + *

        + * The readLoop and writeLoop should never execute on the selector + * thread, but rather, in the client's executor. + *

        + * When the writeLoop is triggered, it polls the writeQueue and + * writes as many datagram as it can to the channel. At the end, + * if there still remains some datagrams in the writeQueue, the + * write event is resumed. Otherwise, the writeLoop is next + * triggered when new datagrams are added to the writeQueue. + *

        + * When the readLoop is triggered, it polls the read queue + * and attempts to match each received packet with a + * QuicConnection. If no connection matches, it attempts + * to match the packet with stateless reset tokens. + * If no stateless reset token match, the packet is + * discarded. + *

        + */ +public abstract sealed class QuicEndpoint implements AutoCloseable + permits QuicEndpoint.QuicSelectableEndpoint, QuicEndpoint.QuicVirtualThreadedEndpoint { + + private static final int INCOMING_MAX_DATAGRAM; + static final boolean DGRAM_SEND_ASYNC; + static final int MAX_BUFFERED_HIGH; + static final int MAX_BUFFERED_LOW; + static { + // This default value is the maximum payload size of + // an IPv6 datagram, which is 65527 (which is bigger + // than that of an IPv4). + // We have only one direct buffer of this size per endpoint. + final int defSize = 65527; + // This is the value that will be transmitted to the server in the + // max_udp_payload_size parameter + int size = Utils.getIntegerProperty("jdk.httpclient.quic.maxUdpPayloadSize", defSize); + // don't allow the value to be below 1200 and above 65527, to conform with RFC-9000, + // section 18.2. + if (size < 1200 || size > 65527) { + // fallback to default size + size = defSize; + } + INCOMING_MAX_DATAGRAM = size; + // TODO: evaluate pros and cons WRT performance and decide for one or the other + // before GA. + DGRAM_SEND_ASYNC = Utils.getBooleanProperty("jdk.internal.httpclient.quic.sendAsync", false); + int maxBufferHigh = Math.clamp(Utils.getIntegerProperty("jdk.httpclient.quic.maxBufferedHigh", + 512 << 10), 128 << 10, 6 << 20); + int maxBufferLow = Math.clamp(Utils.getIntegerProperty("jdk.httpclient.quic.maxBufferedLow", + 384 << 10), 64 << 10, 6 << 20); + if (maxBufferLow >= maxBufferHigh) maxBufferLow = maxBufferHigh >> 1; + MAX_BUFFERED_HIGH = maxBufferHigh; + MAX_BUFFERED_LOW = maxBufferLow; + } + + /** + * This interface represent a UDP Datagram. This could be + * either an incoming datagram or an outgoing datagram. + */ + public sealed interface Datagram + permits QuicDatagram, StatelessReset, SendStatelessReset, UnmatchedDatagram { + /** + * {@return the peer address} + * For incoming datagrams, this is the sender address. + * For outgoing datagrams, this is the destination address. + */ + SocketAddress address(); + + /** + * {@return the datagram payload} + */ + ByteBuffer payload(); + } + + /** + * An incoming UDP Datagram for which no connection was found. + * On the server side it may represent a new connection attempt. + * @param address the {@linkplain Datagram#address() sender address} + * @param payload {@inheritDoc} + */ + public record UnmatchedDatagram(SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * A stateless reset that should be sent in response + * to an incoming datagram targeted at a deleted connection. + * @param address the {@linkplain Datagram#address() destination address} + * @param payload the outgoing stateless reset + */ + public record SendStatelessReset(SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * An incoming datagram containing a stateless reset + * @param connection the connection to reset + * @param address the {@linkplain Datagram#address() sender address} + * @param payload the datagram payload + */ + public record StatelessReset(QuicPacketReceiver connection, SocketAddress address, ByteBuffer payload) implements Datagram {} + + /** + * An outgoing datagram, or an incoming datagram for which + * a connection was identified. + * @param connection the sending or receiving connection + * @param address {@inheritDoc} + * @param payload {@inheritDoc} + */ + public record QuicDatagram(QuicPacketReceiver connection, SocketAddress address, ByteBuffer payload) + implements Datagram {} + + /** + * An enum identifying the type of channels used and supported by QuicEndpoint and + * QuicSelector + */ + public enum ChannelType { + NON_BLOCKING_WITH_SELECTOR, + BLOCKING_WITH_VIRTUAL_THREADS; + public boolean isBlocking() { + return this == BLOCKING_WITH_VIRTUAL_THREADS; + } + } + + // A temporary internal property to switch between two QuicSelector implementation: + // - if true, uses QuicNioSelector, an implementation based non-blocking and channels + // and an NIO Selector + // - if false, uses QuicVirtualThreadPoller, an implementation that use Virtual Threads + // to poll blocking channels + // On windows, we default to using non-blocking IO with a Selector in order + // to avoid a potential deadlock in WEPoll (see JDK-8334574). + private static final boolean USE_NIO_SELECTOR = + Utils.getBooleanProperty("jdk.internal.httpclient.quic.useNioSelector", + OperatingSystem.isWindows()); + /** + * The configured channel type + */ + public static final ChannelType CONFIGURED_CHANNEL_TYPE = USE_NIO_SELECTOR + ? NON_BLOCKING_WITH_SELECTOR + : BLOCKING_WITH_VIRTUAL_THREADS; + + final Logger debug = Utils.getDebugLogger(this::name); + private final QuicInstance quicInstance; + private final String name; + private final DatagramChannel channel; + private final ByteBuffer receiveBuffer; + final Executor executor; + final ConcurrentLinkedQueue readQueue = new ConcurrentLinkedQueue<>(); + final ConcurrentLinkedQueue writeQueue = new ConcurrentLinkedQueue<>(); + final QuicTimerQueue timerQueue; + private volatile boolean closed; + + // A synchronous scheduler to consume the readQueue list; + final SequentialScheduler readLoopScheduler = + SequentialScheduler.lockingScheduler(this::readLoop); + + // A synchronous scheduler to consume the writeQueue list; + final SequentialScheduler writeLoopScheduler = + SequentialScheduler.lockingScheduler(this::writeLoop); + + // A ConcurrentMap to store registered connections. + // The connection IDs might come from external sources. They implement Comparable + // to mitigate collision attacks. + // This map must not share the idFactory with other maps, + // see RFC 9000 section 21.11. Stateless Reset Oracle + private final ConcurrentMap connections = + new ConcurrentHashMap<>(); + + // a factory of local connection IDs. + private final QuicConnectionIdFactory idFactory; + + // Key used to encrypt tokens before storing in {@link #peerIssuedResetTokens} + private final Key tokenEncryptionKey; + + // keeps a link of the peer issued stateless reset token to the corresponding connection that + // will be closed if the specific stateless reset token is received + private final ConcurrentMap peerIssuedResetTokens = + new ConcurrentHashMap<>(); + + private static ByteBuffer allocateReceiveBuffer() { + return ByteBuffer.allocateDirect(INCOMING_MAX_DATAGRAM); + } + + private final AtomicInteger buffered = new AtomicInteger(); + volatile boolean readingStalled; + + public QuicConnectionIdFactory idFactory() { + return idFactory; + } + + public int buffer(int bytes) { + return buffered.addAndGet(bytes); + } + + public int unbuffer(int bytes) { + var newval = buffered.addAndGet(-bytes); + assert newval >= 0; + if (newval <= MAX_BUFFERED_LOW) { + resumeReading(); + } + return newval; + } + + boolean bufferTooBig() { + return buffered.get() >= MAX_BUFFERED_HIGH; + } + + public int buffered() { + return buffered.get(); + } + + boolean readingPaused() { + return readingStalled; + } + + abstract void resumeReading(); + + abstract void pauseReading(); + + private QuicEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + this.quicInstance = quicInstance; + this.name = name; + this.channel = channel; + this.receiveBuffer = allocateReceiveBuffer(); + this.executor = quicInstance.executor(); + this.timerQueue = timerQueue; + if (debug.on()) debug.log("created for %s", channel); + try { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + tokenEncryptionKey = kg.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("AES key generator not available", e); + } + idFactory = isServer() + ? QuicConnectionIdFactory.getServer() + : QuicConnectionIdFactory.getClient(); + } + + public String name() { + return name; + } + + public DatagramChannel channel() { + return channel; + } + + Executor writeLoopExecutor() { return executor; } + + public SocketAddress getLocalAddress() throws IOException { + return channel.getLocalAddress(); + } + + public String getLocalAddressString() { + try { + return String.valueOf(channel.getLocalAddress()); + } catch (IOException io) { + return "No address available"; + } + } + + int getMaxUdpPayloadSize() { + return INCOMING_MAX_DATAGRAM; + } + + protected abstract ChannelType channelType(); + + /** + * A {@link QuicEndpoint} implementation based on non blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * NIO {@link Selector}. + * This implementation is tied to a {@link QuicNioSelector}. + */ + static final class QuicSelectableEndpoint extends QuicEndpoint { + volatile SelectionKey key; + + private QuicSelectableEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + super(quicInstance, channel, name, timerQueue); + assert !channel.isBlocking() : "SelectableQuicEndpoint channel is blocking"; + } + + @Override + public ChannelType channelType() { + return NON_BLOCKING_WITH_SELECTOR; + } + + /** + * Attaches this endpoint to a selector. + * + * @param selector the selector to attach to + * @throws ClosedChannelException if the channel is already closed + */ + public void attach(Selector selector) throws ClosedChannelException { + var key = this.key; + assert key == null; + // this block is needed to coordinate with detach() and + // selected(). See comment in selected(). + synchronized (this) { + this.key = super.channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, this); + } + } + + @Override + void resumeReading() { + boolean resumed = false; + SelectionKey key; + synchronized (this) { + key = this.key; + if (key != null && key.isValid()) { + if (isClosed() || isChannelClosed()) return; + int ops = key.interestOps(); + int newops = ops | SelectionKey.OP_READ; + if (ops != newops) { + key.interestOpsOr(SelectionKey.OP_READ); + readingStalled = false; + resumed = true; + } + } + } + if (resumed) { + // System.out.println(this + " endpoint resumed reading"); + if (debug.on()) debug.log("endpoint resumed reading"); + key.selector().wakeup(); + } + } + + @Override + void pauseReading() { + boolean paused = false; + synchronized (this) { + if (readingStalled) return; + if (key != null && key.isValid() && bufferTooBig()) { + if (isClosed() || isChannelClosed()) return; + int ops = key.interestOps(); + int newops = ops & ~SelectionKey.OP_READ; + if (ops != newops) { + key.interestOpsAnd(~SelectionKey.OP_READ); + readingStalled = true; + paused = true; + } + } + } + if (paused) { + // System.out.println(this + " endpoint paused reading"); + if (debug.on()) debug.log("endpoint paused reading"); + } + } + + /** + * Invoked by the {@link QuicSelector} when this endpoint's channel + * is selected. + * + * @param readyOps The operations that are ready for this endpoint. + */ + public void selected(int readyOps) { + var key = this.key; + try { + if (key == null) { + // null keys have been observed here. + // key can only be null if it's been cancelled, by detach() + // or if the call to channel::register hasn't returned yet + // the synchronized block below will block until + // channel::register returns if needed. + // This can only happen once, when attaching the channel, + // so there should be no performance issue in synchronizing + // here. + synchronized (this) { + key = this.key; + } + } + + if (key == null) { + if (debug.on()) { + debug.log("key is null"); + if (QuicEndpoint.class.desiredAssertionStatus()) { + Thread.dumpStack(); + } + } + return; + } + + if (debug.on()) { + debug.log("selected(interest=%s, ready=%s)", + Utils.interestOps(key), + Utils.readyOps(key)); + } + + int interestOps = key.interestOps(); + + // Some operations may be ready even when we are not interested. + // Typically, a channel may be ready for writing even if we have + // nothing to write. The events we need to invoke are therefore + // at the intersection of the ready set with the interest set. + int event = readyOps & interestOps; + if ((event & SelectionKey.OP_READ) == SelectionKey.OP_READ) { + onReadEvent(); + if (isClosed()) { + key.interestOpsAnd(~SelectionKey.OP_READ); + } + } + if ((event & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { + onWriteEvent(); + } + if (debug.on()) { + debug.log("interestOps: %s", Utils.interestOps(key)); + } + } finally { + if (!channel().isOpen()) { + if (key != null) key.cancel(); + close(); + } + } + } + + private void onReadEvent() { + var key = this.key; + try { + if (debug.on()) debug.log("onReadEvent"); + channelReadLoop(); + } finally { + if (debug.on()) { + debug.log("Leaving readEvent: ops=%s", Utils.interestOps(key)); + } + } + } + + private void onWriteEvent() { + // trigger code that will process the received + // datagrams asynchronously + // => Use a sequential scheduler, making sure it never + // runs on this thread. + // Do we need a pub/sub mechanism here? + // The write event will be paused/resumed by the + // writeLoop if needed + if (debug.on()) debug.log("onWriteEvent"); + var key = this.key; + if (key != null && key.isValid()) { + int previous; + synchronized (this) { + previous = key.interestOpsAnd(~SelectionKey.OP_WRITE); + } + if (debug.on()) debug.log("key changed from %s to: %s", + Utils.describeOps(previous), Utils.interestOps(key)); + } + writeLoopScheduler.runOrSchedule(writeLoopExecutor()); + if (debug.on() && key != null) { + debug.log("Leaving writeEvent: ops=%s", Utils.interestOps(key)); + } + } + + @Override + void writeLoop() { + super.writeLoop(); + // update selection key if needed + var key = this.key; + try { + if (key != null && key.isValid()) { + int ops, newops; + synchronized (this) { + ops = newops = key.interestOps(); + if (writeQueue.isEmpty()) { + // we have nothing else to write for now + newops &= ~SelectionKey.OP_WRITE; + } else { + // there's more to write + newops |= SelectionKey.OP_WRITE; + } + if (newops != ops && key.selector().isOpen()) { + key.interestOps(newops); + key.selector().wakeup(); + } + } + if (debug.on()) { + debug.log("leaving writeLoop: ops=%s", Utils.describeOps(newops)); + } + } + } catch (CancelledKeyException x) { + if (debug.on()) debug.log("key cancelled"); + if (writeQueue.isEmpty()) return; + else { + closeWriteQueue(x); + } + } + } + + @Override + void readLoop() { + try { + super.readLoop(); + } finally { + if (debug.on()) { + debug.log("leaving readLoop: ops=%s", Utils.interestOps(key)); + } + } + } + + @Override + public void detach() { + var key = this.key; + if (key == null) return; + if (debug.on()) { + debug.log("cancelling key: " + key); + } + // this block is needed to coordinate with attach() and + // selected(). See comment in selected(). + synchronized (this) { + key.cancel(); + this.key = null; + } + } + } + + /** + * A {@link QuicEndpoint} implementation based on blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * Virtual Threads to poll the channel. + * This implementation is tied to a {@link QuicVirtualThreadPoller}. + */ + static final class QuicVirtualThreadedEndpoint extends QuicEndpoint { + Future key; + volatile QuicVirtualThreadPoller poller; + boolean readingDone; + + private QuicVirtualThreadedEndpoint(QuicInstance quicInstance, + DatagramChannel channel, + String name, + QuicTimerQueue timerQueue) { + super(quicInstance, channel, name, timerQueue); + } + + @Override + boolean readingPaused() { + synchronized (this) { + return readingDone = super.readingPaused(); + } + } + + @Override + void resumeReading() { + boolean resumed; + boolean resumedInOtherThread = false; + QuicVirtualThreadPoller poller; + synchronized (this) { + resumed = readingStalled; + readingStalled = false; + poller = this.poller; + // readingDone is false here, it means reading already resumed + // no need to start a new reading thread + if (poller != null && (resumedInOtherThread = readingDone)) { + readingDone = false; + attach(poller); + } + } + if (resumedInOtherThread) { + // last time readingPaused() was called it returned true, so we know + // the previous poller thread has stopped reading and will exit. + // We attached a new poller above, so reading will resume in that + // other thread + // System.out.println(this + " endpoint resumed reading in new virtual thread"); + if (debug.on()) debug.log("endpoint resumed reading in new virtual thread"); + } else if (resumed) { + // readingStalled was true, and readingDone was false - which means some + // poller thread is already active, and will find readingStalled == true + // and will continue reading. So reading will resume in the currently + // active poller thread + // System.out.println(this + " endpoint resumed reading in same virtual thread"); + if (debug.on()) debug.log("endpoint resumed reading in same virtual thread"); + } // if readingStalled was false and readingDone was false there is nothing to do. + } + + @Override + void pauseReading() { + boolean paused = false; + synchronized (this) { + if (bufferTooBig()) paused = readingStalled = true; + } + if (paused) { + // System.out.println(this + " endpoint paused reading"); + if (debug.on()) debug.log("endpoint paused reading"); + } + } + + @Override + public ChannelType channelType() { + return BLOCKING_WITH_VIRTUAL_THREADS; + } + + void attach(QuicVirtualThreadPoller poller) { + this.poller = poller; + var future = poller.startReading(this); + synchronized (this) { + this.key = future; + } + } + + Executor writeLoopExecutor() { + QuicVirtualThreadPoller poller = this.poller; + if (poller == null) return executor; + return poller.readLoopExecutor(); + } + + private final SequentialScheduler channelScheduler = SequentialScheduler.lockingScheduler(this::channelReadLoop0); + + @Override + void channelReadLoop() { + channelScheduler.runOrSchedule(); + } + + private void channelReadLoop0() { + super.channelReadLoop(); + } + + @Override + public void detach() { + var key = this.key; + try { + if (key != null) { + // do not interrupt the reading task if running: + // closing the channel later on will ensure that the + // task eventually terminates. + key.cancel(false); + } + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to cancel future: " + e); + } + } + } + } + + private ByteBuffer copyOnHeap(ByteBuffer buffer) { + ByteBuffer onHeap = ByteBuffer.allocate(buffer.remaining()); + return onHeap.put(buffer).flip(); + } + + void channelReadLoop() { + // we can't prevent incoming datagram from being received + // at this level of the stack. If there is a datagram available, + // we must read it immediately and put it in the read queue. + // + // We maintain a counter of the number of bytes currently + // in the read queue. If that number exceeds a high watermark + // threshold, we will pause reading, and thus stop adding + // to the queue. + // + // As the read queue gets emptied, reading will be resumed + // when a low watermark threshold is crossed in the other + // direction. + // + // At the moment we have a single channel per endpoint, + // and we're using a single endpoint by default. + // + // We have a single selector thread, and we copy off + // the data from off-heap to on-heap before adding it + // to the queue. + // + // We can therefore do the reading directly in the + // selector thread and offload the parsing (the readLoop) + // to the executor. + // + // The readLoop will in turn resume the reading, if needed, + // when it crosses the low watermark threshold. + // + boolean nonBlocking = channelType() == NON_BLOCKING_WITH_SELECTOR; + int count; + final var buffer = this.receiveBuffer; + buffer.clear(); + final int initialStart = 1; // start readloop at first buffer + // if blocking we want to nudge the scheduler after each read since we don't + // know how much the next receive will take. If non-blocking, we nudge it + // after three consecutive read. + final int maxBeforeStart = nonBlocking ? 3 : 1; // nudge again after 3 buffers + int readLoopStarted = initialStart; + int totalpkt = 0; + try { + int sincepkt = 0; + while (!isClosed() && !readingPaused()) { + var pos = buffer.position(); + var limit = buffer.limit(); + if (debug.on()) + debug.log("receiving with buffer(pos=%s, limit=%s)", pos, limit); + assert pos == 0; + assert limit > pos; + + final SocketAddress source = channel.receive(buffer); + assert source != null || !channel.isBlocking(); + if (source == null) { + if (debug.on()) debug.log("nothing to read..."); + if (nonBlocking) break; + } + + totalpkt++; + sincepkt++; + buffer.flip(); + count = buffer.remaining(); + if (debug.on()) { + debug.log("received %s bytes from %s", count, source); + } + if (count > 0) { + // Optimization: add some basic check here to drop the packet here if: + // - it is too small, it is not a quic packet we would handle + Datagram datagram; + if ((datagram = matchDatagram(source, buffer)) == null) { + if (debug.on()) { + debug.log("dropping invalid packet for this instance (%s bytes)", count); + } + buffer.clear(); + continue; + } + // at this point buffer has been copied. We only buffer what's + // needed. + int rcv = datagram.payload().remaining(); + int buffered = buffer(rcv); + if (debug.on()) { + debug.log("adding %s in read queue from %s, queue size %s, buffered %s, type %s", + rcv, source, readQueue.size(), buffered, datagram.getClass().getSimpleName()); + } + readQueue.add(datagram); + buffer.clear(); + if (--readLoopStarted == 0 || buffered >= MAX_BUFFERED_HIGH) { + readLoopStarted = maxBeforeStart; + if (debug.on()) debug.log("triggering readLoop"); + readLoopScheduler.runOrSchedule(executor); + Deadline now; + Deadline pending; + if (nonBlocking && totalpkt > 1 && (pending = timerQueue.pendingScheduledDeadline()) + .isBefore(now = timeSource().instant())) { + // we have read 3 packets, some events are pending, return + // to the selector to process the event queue + assert this instanceof QuicEndpoint.QuicSelectableEndpoint + : "unexpected endpoint type: " + this.getClass() + "@[" + name + "]"; + assert Thread.currentThread() instanceof QuicSelector.QuicSelectorThread; + if (Log.quicRetransmit() || Log.quicTimer()) { + Log.logQuic(name() + ": reschedule needed: " + Utils.debugDeadline(now, pending) + + ", totalpkt: " + totalpkt + + ", sincepkt: " + sincepkt); + } else if (debug.on()) { + debug.log("reschedule needed: " + Utils.debugDeadline(now, pending) + + ", totalpkt: " + totalpkt + + ", sincepkt: " + sincepkt); + } + timerQueue.processEventsAndReturnNextDeadline(now, executor); + sincepkt = 0; + } + } + // check buffered.get() directly as it may have + // been decremented by the read loop already + if (this.buffered.get() >= MAX_BUFFERED_HIGH) { + // we passed the high watermark, let's pause reading. + // the read loop should already have been kicked + // of above, or will be below when we exit the while + // loop + pauseReading(); + } + } else { + if (debug.on()) debug.log("Dropped empty datagram"); + } + } + // trigger code that will process the received + // datagrams asynchronously + // => Use a sequential scheduler, making sure it never + // runs on this thread. + if (!readQueue.isEmpty() && readLoopStarted != maxBeforeStart) { + if (debug.on()) debug.log("triggering readLoop: queue size " + readQueue.size()); + readLoopScheduler.runOrSchedule(executor); + } + } catch (Throwable t) { + // TODO: special handling for interrupts? + onReadError(t); + } finally { + if (nonBlocking) { + if ((Log.quicRetransmit() && Log.channel()) || Log.quicTimer()) { + Log.logQuic(name() + ": channelReadLoop totalpkt:" + totalpkt); + } else if (debug.on()) { + debug.log("channelReadLoop totalpkt:" + totalpkt); + } + } + } + } + + /** + * This method tries to figure out whether the received packet + * matches a connection, or a stateless reset. + * @param source the source address + * @param buffer the incoming datagram payload + * @return a {@link Datagram} to be processed by the read loop + * if a match is found, or null if the datagram can be dropped + * immediately + */ + private Datagram matchDatagram(SocketAddress source, ByteBuffer buffer) { + HeadersType headersType = QuicPacketDecoder.peekHeaderType(buffer, buffer.position()); + // short header packets whose length is < 21 are never valid + if (headersType == HeadersType.SHORT && buffer.remaining() < 21) { + return null; + } + final ByteBuffer cidbytes = switch (headersType) { + case LONG, SHORT -> peekConnectionBytes(headersType, buffer); + default -> null; + }; + if (cidbytes == null) { + return null; + } + int length = cidbytes.remaining(); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + return null; + } + if (debug.on()) { + debug.log("headers(%s), connectionId(%d), datagram(%d)", + headersType, cidbytes.remaining(), buffer.remaining()); + } + QuicPacketReceiver connection = findQuicConnectionFor(source, cidbytes, headersType == HeadersType.LONG); + // check stateless reset + if (connection == null) { + if (headersType == HeadersType.SHORT) { + // a short packet may be a stateless reset, or may + // trigger a stateless reset + connection = checkStatelessReset(source, buffer); + if (connection != null) { + // We received a stateless reset, process it later in the readLoop + return new StatelessReset(connection, source, copyOnHeap(buffer)); + } else if (buffer.remaining() > 21) { + // check if we should send a stateless reset + final ByteBuffer reset = idFactory.statelessReset(cidbytes, buffer.remaining() - 1); + if (reset != null) { + // will send stateless reset later from the read loop + return new SendStatelessReset(source, reset); + } + } + return null; // drop unmatched short packets + } + // client can drop all unmatched long quic packets here + if (isClient()) return null; + } + + if (connection != null) { + if (!connection.accepts(source)) return null; + return new QuicDatagram(connection, source, copyOnHeap(buffer)); + } else { + return new UnmatchedDatagram(source, copyOnHeap(buffer)); + } + } + + + private int send(ByteBuffer datagram, SocketAddress destination) throws IOException { + return channel.send(datagram, destination); + } + + void writeLoop() { + try { + writeLoop0(); + } catch (Throwable error) { + if (!expectExceptions && !closed) { + if (Log.errors()) { + Log.logError(name + ": failed to write to channel: " + error); + Log.logError(error); + } + abort(error); + } + } + } + + boolean sendDatagram(QuicDatagram datagram) throws IOException { + int sent; + var payload = datagram.payload(); + var tosend = payload.remaining(); + final var dest = datagram.address(); + if (isClosed() || isChannelClosed()) { + if (debug.on()) { + debug.log("endpoint or channel closed; skipping sending of datagram(%d) to %s", + tosend, dest); + } + return false; + } + if (debug.on()) { + debug.log("sending datagram(%d) to %s", + tosend, dest); + } + sent = send(payload, dest); + if (debug.on()) debug.log("sent %d bytes to %s", sent, dest); + if (sent == 0 && sent != tosend) return false; + assert sent == tosend; + if (datagram.connection != null) { + datagram.connection.datagramSent(datagram); + } + return true; + } + + void onSendError(QuicDatagram datagram, int tosend, IOException x) { + // close the connection this came from? + // close all the connections whose destination is that address? + var connection = datagram.connection(); + var dest = datagram.address(); + String msg = x.getMessage(); + if (msg != null && msg.contains("too big")) { + int max = -1; + if (connection instanceof QuicConnectionImpl cimpl) { + max = cimpl.getMaxDatagramSize(); + } + msg = "on endpoint %s: Failed to send datagram (%s bytes, max: %s) to %s: %s" + .formatted(this.name, tosend, max, dest, x); + if (connection == null && debug.on()) debug.log(msg); + x = new SocketException(msg, x); + } + if (connection != null) { + connection.datagramDiscarded(datagram); + connection.onWriteError(x); + if (!channel.isOpen()) { + abort(x); + } + } + } + + private void writeLoop0() { + // write as much as we can + while (!writeQueue.isEmpty()) { + var datagram = writeQueue.peek(); + var payload = datagram.payload(); + var tosend = payload.remaining(); + try { + if (sendDatagram(datagram)) { + var rem = writeQueue.poll(); + assert rem == datagram; + } else break; + } catch (IOException x) { + // close the connection this came from? + // close all the connections whose destination is that address? + onSendError(datagram, tosend, x); + var rem = writeQueue.poll(); + assert rem == datagram; + } + } + + } + + void closeWriteQueue(Throwable t) { + QuicDatagram qd; + while ((qd = writeQueue.poll()) != null) { + if (qd.connection != null) { + qd.connection.onWriteError(t); + } + } + } + + private ByteBuffer peekConnectionBytes(HeadersType headersType, ByteBuffer payload) { + var cidlen = idFactory.connectionIdLength(); + return switch (headersType) { + case LONG -> QuicPacketDecoder.peekLongConnectionId(payload); + case SHORT -> QuicPacketDecoder.peekShortConnectionId(payload, cidlen); + default -> null; + }; + } + + // The readloop is triggered whenever new datagrams are + // added to the read queue. + void readLoop() { + try { + if (debug.on()) debug.log("readLoop"); + while (!readQueue.isEmpty()) { + var datagram = readQueue.poll(); + var payload = datagram.payload(); + var source = datagram.address(); + int remaining = payload.remaining(); + var pos = payload.position(); + unbuffer(remaining); + if (debug.on()) { + debug.log("readLoop: type(%x) %d from %s", + payload.hasRemaining() ? payload.get(0) : 0, + remaining, + source); + } + try { + if (closed) { + if (debug.on()) { + debug.log("closed: ignoring incoming datagram"); + } + return; + } + switch (datagram) { + case QuicDatagram(var connection, var _, var _) -> { + var headersType = QuicPacketDecoder.peekHeaderType(payload, pos); + var destConnId = peekConnectionBytes(headersType, payload); + connection.processIncoming(source, destConnId, headersType, payload); + } + case UnmatchedDatagram(var _, var _) -> { + var headersType = QuicPacketDecoder.peekHeaderType(payload, pos); + unmatchedQuicPacket(datagram, headersType, payload); + } + case StatelessReset(var connection, var _, var _) -> { + if (debug.on()) { + debug.log("Processing stateless reset from %s", source); + } + connection.processStatelessReset(); + } + case SendStatelessReset(var _, var _) -> { + if (debug.on()) { + debug.log("Sending stateless reset to %s", source); + } + send(payload, source); + } + } + + } catch (Throwable t) { + if (debug.on()) debug.log("Failed to handle datagram: " + t, t); + Log.logError(t); + } + } + } catch (Throwable t) { + onReadError(t); + } + } + + private void onReadError(Throwable t) { + if (!expectExceptions) { + if (debug.on()) { + debug.log("Error handling event: ", t); + } + Log.logError(t); + if (t instanceof RejectedExecutionException + || t instanceof ClosedChannelException + || t instanceof AssertionError) { + expectExceptions = true; + abort(t); + } + } + } + + /** + * checks if the received datagram contains a stateless reset token; + * returns the associated connection if true, null otherwise + * @param source the sender's address + * @param buffer datagram contents + * @return connection associated with the stateless token, or {@code null} + */ + protected QuicPacketReceiver checkStatelessReset(SocketAddress source, final ByteBuffer buffer) { + // We couldn't identify the connection: maybe that's a stateless reset? + if (closed) return null; + if (debug.on()) { + debug.log("Check if received datagram could be stateless reset (datagram[%d, %s])", + buffer.remaining(), source); + } + if (buffer.remaining() < 21) { + // too short to be a stateless reset: + // RFC 9000: + // Endpoints MUST discard packets that are too small to be valid QUIC packets. + // To give an example, with the set of AEAD functions defined in [QUIC-TLS], + // short header packets that are smaller than 21 bytes are never valid. + if (debug.on()) { + debug.log("Packet too short for a stateless reset (%s bytes < 21)", + buffer.remaining()); + } + return null; + } + final byte[] tokenBytes = new byte[16]; + buffer.get(buffer.limit() - 16, tokenBytes); + final var token = makeToken(tokenBytes); + QuicPacketReceiver connection = peerIssuedResetTokens.get(token); + if (closed) return null; + if (connection != null) { + if (debug.on()) { + debug.log("Received reset token: %s for connection: %s", + HexFormat.of().formatHex(tokenBytes), connection); + } + } else { + if (debug.on()) { + debug.log("Not a stateless reset"); + } + } + return connection; + } + + private StatelessResetToken makeToken(byte[] tokenBytes) { + // encrypt token to block timing attacks, see RFC 9000 section 10.3.1 + try { + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, tokenEncryptionKey); + byte[] encryptedBytes = cipher.doFinal(tokenBytes); + return new StatelessResetToken(encryptedBytes); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | + IllegalBlockSizeException | BadPaddingException | + InvalidKeyException e) { + throw new RuntimeException("AES encryption failed", e); + } + } + + /** + * Called when parsing a quic packet that couldn't be matched to any registered + * connection. + * + * @param datagram The datagram containing the packet + * @param headersType The quic packet type + * @param buffer A buffer positioned at the start of the unmatched quic packet. + * The buffer may contain more coalesced quic packets. + */ + protected void unmatchedQuicPacket(Datagram datagram, + HeadersType headersType, + ByteBuffer buffer) throws IOException { + QuicInstance instance = quicInstance; + if (closed) { + if (debug.on()) { + debug.log("closed: ignoring unmatched datagram"); + } + return; + } + + var address = datagram.address(); + if (isServer() && headersType == HeadersType.LONG ) { + // long packets need to be rematched here for servers. + // we read packets in one thread and process them here in + // a different thread: + // the connection may have been added later on when processing + // a previous long packet in this thread, so we need to + // check the connection map again here. + var idbytes = peekConnectionBytes(headersType, buffer); + var connection = findQuicConnectionFor(address, idbytes, true); + if (connection != null) { + // a matching connection was found, this packet is no longer + // unmatched + if (connection.accepts(address)) { + connection.processIncoming(address, idbytes, headersType, buffer); + } + return; + } + } + + if (debug.on()) { + debug.log("Unmatched packet in datagram [%s, %d, %s] for %s", headersType, + buffer.remaining(), address, instance); + debug.log("Unmatched packet: delegating to instance"); + } + instance.unmatchedQuicPacket(address, headersType, buffer); + } + + private boolean isServer() { + return !isClient(); + } + + private boolean isClient() { + return quicInstance instanceof QuicClient; + } + + // Parses the list of active connection + // Attempts to find one that matches + // If none match return null + // Revisit: + // if we had an efficient sorted tree where we could locate a connection id + // from the idbytes we wouldn't need to use an "unsafe connection id" + // quick and dirty solution for now: we use a ConcurrentHashMap and construct + // a throw away QuicConnectionId that wrap our mutable idbytes. + // This is OK since the classes that may see these bytes are all internal + // and won't mutate them. + QuicPacketReceiver findQuicConnectionFor(SocketAddress peerAddress, ByteBuffer idbytes, boolean longHeaders) { + if (idbytes == null) return null; + var cid = idFactory.unsafeConnectionIdFor(idbytes); + if (cid == null) { + if (!longHeaders || isClient()) { + if (debug.on()) { + debug.log("No connection match for: %s", Utils.asHexString(idbytes)); + } + return null; + } + // this is a long headers packet and we're the server; + // the client might still be using the original connection ID + cid = new PeerConnectionId(idbytes, null); + } + if (debug.on()) { + debug.log("Looking up QuicConnection for: %s", cid); + } + var quicConnection = connections.get(cid); + assert quicConnection == null || allConnectionIds(quicConnection).anyMatch(cid::equals); + return quicConnection; + } + + private static Stream allConnectionIds(QuicPacketReceiver quicConnection) { + return Stream.concat(quicConnection.connectionIds().stream(), quicConnection.initialConnectionId().stream()); + } + + /** + * Detach the channel from the selector implementation + */ + public abstract void detach(); + + private void silentTerminateConnection(QuicPacketReceiver c) { + try { + if (c instanceof QuicConnectionImpl connection) { + final TerminationCause st = forSilentTermination("QUIC endpoint closed - no error"); + connection.terminator.terminate(st); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close connection %s: %s", c, t); + } + } finally { + if (c != null) c.shutdown(); + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + void abortConnection(QuicPacketReceiver c, Throwable error) { + try { + if (c instanceof QuicConnectionImpl connection) { + connection.terminator.terminate(TerminationCause.forException(error)); + } + } catch (Throwable t) { + if (debug.on()) { + debug.log("Failed to close connection %s: %s", c, t); + } + } finally { + if (c != null) c.shutdown(); + } + } + + boolean isClosed() { + return closed; + } + + private void detachAndCloseChannel() throws IOException { + try { + detach(); + } finally { + channel.close(); + } + } + + volatile boolean expectExceptions; + + @Override + public void close() { + if (closed) return; + synchronized (this) { + if (closed) return; + closed = true; + } + try { + while (!connections.isEmpty()) { + if (debug.on()) { + debug.log("closing %d connections", connections.size()); + } + final Set connCloseSent = new HashSet<>(); + for (var cid : connections.keySet()) { + // endpoint is closing, so (on a best-effort basis) we send out a datagram + // containing a QUIC packet with a CONNECTION_CLOSE frame to the peer. + // Immediately after that, we silently terminate the connection since + // there's no point maintaining the connection's infrastructure for + // sending (or receiving) additional packets when the endpoint itself + // won't be around for dealing with the packets. + final QuicPacketReceiver rcvr = connections.remove(cid); + if (rcvr instanceof QuicConnectionImpl quicConn) { + final boolean shouldSendConnClose = connCloseSent.add(quicConn); + // send the datagram containing the CONNECTION_CLOSE frame only once + // per connection + if (shouldSendConnClose) { + sendConnectionCloseQuietly(quicConn); + } + } + silentTerminateConnection(rcvr); + } + } + } finally { + try { + // TODO: do we need to wait for something (ACK?) + // before actually stopping all loop and closing the channel? + if (debug.on()) { + debug.log("Closing channel " + channel + " of endpoint " + this); + } + writeLoopScheduler.stop(); + readLoopScheduler.stop(); + QuicDatagram datagram; + while ((datagram = writeQueue.poll()) != null) { + if (datagram.connection != null) { + datagram.connection.datagramDropped(datagram); + } + } + expectExceptions = true; + detachAndCloseChannel(); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to detach and close channel: " + io); + } + } + } + + // sends a datagram with a CONNECTION_CLOSE frame for the connection and ignores + // any exceptions that may occur while trying to do so. + private void sendConnectionCloseQuietly(final QuicConnectionImpl quicConn) { + try { + final Optional datagram = quicConn.connectionCloseDatagram(); + if (datagram.isEmpty()) { + return; + } + if (debug.on()) { + debug.log("sending CONNECTION_CLOSE datagram for connection %s", quicConn); + } + send(datagram.get().payload(), datagram.get().address()); + } catch (Exception e) { + // ignore + if (debug.on()) { + debug.log("failed to send CONNECTION_CLOSE datagram for" + + " connection %s due to %s", quicConn, e); + } + } + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable error) { + + if (closed) return; + synchronized (this) { + if (closed) return; + closed = true; + } + assert closed; + if (debug.on()) { + debug.log("aborting: " + error); + } + writeLoopScheduler.stop(); + readLoopScheduler.stop(); + QuicDatagram datagram; + while ((datagram = writeQueue.poll()) != null) { + if (datagram.connection != null) { + datagram.connection.datagramDropped(datagram); + } + } + try { + while (!connections.isEmpty()) { + if (debug.on()) + debug.log("closing %d connections", connections.size()); + for (var cid : connections.keySet()) { + abortConnection(connections.remove(cid), error); + } + } + } finally { + try { + if (debug.on()) { + debug.log("Closing channel " + channel + " of endpoint " + this); + } + detachAndCloseChannel(); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to detach and close channel: " + io); + } + } + } + + + @Override + public String toString() { + return name; + } + + boolean forceSendAsync() { + return DGRAM_SEND_ASYNC || !writeQueue.isEmpty(); + // TODO remove + // perform all writes in a virtual thread. This should trigger + // JDK-8334574 more frequently. + // || (IS_WINDOWS + // && channelType().isBlocking() + // && !Thread.currentThread().isVirtual()); + } + + /** + * Schedule a datagram for writing to the underlying channel. + * If any datagram is pending the given datagram is appended + * to the list of pending datagrams for writing. + * @param source the source connection + * @param destination the destination address + * @param payload the encrypted datagram + */ + public void pushDatagram(QuicPacketReceiver source, SocketAddress destination, ByteBuffer payload) { + int tosend = payload.remaining(); + if (debug.on()) { + debug.log("attempting to send datagram [%s bytes]", tosend); + } + var datagram = new QuicDatagram(source, destination, payload); + try { + // if DGRAM_SEND_ASYNC is true we don't attempt to send from the current + // thread but push the datagram on the queue and invoke the write loop. + if (forceSendAsync() || !sendDatagram(datagram)) { + if (tosend == payload.remaining()) { + writeQueue.add(datagram); + if (debug.on()) { + debug.log("datagram [%s bytes] added to write queue, queue size %s", + tosend, writeQueue.size()); + } + writeLoopScheduler.runOrSchedule(writeLoopExecutor()); + } else { + source.datagramDropped(datagram); + if (debug.on()) { + debug.log("datagram [%s bytes] dropped: payload partially consumed, remaining %s", + tosend, payload.remaining()); + } + } + } + } catch (IOException io) { + onSendError(datagram, tosend, io); + } + } + + /** + * Called to schedule sending of a datagram that contains a {@code ConnectionCloseFrame}. + * This will replace the {@link QuicConnectionImpl} with a {@link ClosedConnection} that + * will replay the datagram containing the {@code ConnectionCloseFrame} whenever a packet + * for that connection is received. + * @param connection the connection being closed + * @param destination the peer address + * @param datagram the datagram + */ + public void pushClosingDatagram(QuicConnectionImpl connection, InetSocketAddress destination, ByteBuffer datagram) { + if (debug.on()) debug.log("Pushing closing datagram for " + connection.logTag()); + closing(connection, datagram.slice()); + pushDatagram(connection, destination, datagram); + } + + /** + * Called to schedule sending of a datagram that contains a single {@code ConnectionCloseFrame} + * sent in response to a {@code ConnectionClose} frame. + * This will completely remove the connection from the connection map. + * @param connection the connection being closed + * @param destination the peer address + * @param datagram the datagram + */ + public void pushClosedDatagram(QuicConnectionImpl connection, + InetSocketAddress destination, + ByteBuffer datagram) { + if (debug.on()) debug.log("Pushing closed datagram for " + connection.logTag()); + removeConnection(connection); + pushDatagram(connection, destination, datagram); + } + + /** + * This will completely remove the connection from the endpoint. Any subsequent packets + * directed to this connection from a peer, may end up receiving a stateless reset + * from this endpoint. + * + * @param connection the connection to be removed + */ + void removeConnection(final QuicPacketReceiver connection) { + if (debug.on()) debug.log("removing connection " + connection); + // remove the connection completely + connection.connectionIds().forEach(connections::remove); + assert !connections.containsValue(connection) : connection; + // remove references to this connection from the map which holds the peer issued + // reset tokens + dropPeerIssuedResetTokensFor(connection); + } + + /** + * Add the cid to connection mapping to the endpoint. + * + * @param cid the connection ID to be added + * @param connection the connection that should be mapped to the cid + * @return true if connection ID was added, false otherwise + */ + public boolean addConnectionId(QuicConnectionId cid, QuicPacketReceiver connection) { + var old = connections.putIfAbsent(cid, connection); + return old == null; + } + + /** + * Remove the cid to connection mapping from the endpoint. + * + * @param cid the connection ID to be removed + * @param connection the connection that is mapped to the cid + * @return true if connection ID was removed, false otherwise + */ + public boolean removeConnectionId(QuicConnectionId cid, QuicPacketReceiver connection) { + if (debug.on()) debug.log("removing connection ID " + cid); + return connections.remove(cid, connection); + } + + public final int connectionCount() { + return connections.size(); + } + + // drop peer issued stateless tokes for the given connection + private void dropPeerIssuedResetTokensFor(QuicPacketReceiver connection) { + // remove references to this connection from the map which holds the peer issued + // reset tokens + peerIssuedResetTokens.values().removeIf(conn -> connection == conn); + } + + // remap peer issued stateless token from connection `from` to connection `to` + private void remapPeerIssuedResetToken(QuicPacketReceiver from, QuicPacketReceiver to) { + assert from != null; + assert to != null; + peerIssuedResetTokens.replaceAll((tok, c) -> c == from ? to : c); + } + + public void draining(final QuicPacketReceiver connection) { + // remap the connection to a DrainingConnection + if (closed) return; + connection.connectionIds().forEach((id) -> + connections.compute(id, this::remapDraining)); + assert !connections.containsValue(connection) : connection; + } + + private DrainingConnection remapDraining(QuicConnectionId id, QuicPacketReceiver conn) { + if (closed) return null; + var debugOn = debug.on() && !Thread.currentThread().isVirtual(); + if (conn instanceof ClosingConnection closing) { + if (debugOn) debug.log("remapping %s to DrainingConnection", id); + final var draining = closing.toDraining(); + remapPeerIssuedResetToken(closing, draining); + draining.startTimer(); + return draining; + } else if (conn instanceof DrainingConnection draining) { + return draining; + } else if (conn instanceof QuicConnectionImpl impl) { + final long idleTimeout = impl.peerPtoMs() * 3; // 3 PTO + impl.localConnectionIdManager().close(); + if (debugOn) debug.log("remapping %s to DrainingConnection", id); + var draining = new DrainingConnection(conn.connectionIds(), idleTimeout); + // we can ignore stateless reset in the draining state. + remapPeerIssuedResetToken(impl, draining); + draining.startTimer(); + return draining; + } else if (conn == null) { + // connection absent (was probably removed), don't remap to draining + if (debugOn) { + debug.log("no existing connection present for %s, won't remap to draining", id); + } + return null; + } else { + assert false : "unexpected connection type: " + conn; // just remove + return null; + } + } + + protected void closing(QuicConnectionImpl connection, ByteBuffer datagram) { + if (closed) return; + ByteBuffer closing = ByteBuffer.allocate(datagram.limit()); + closing.put(datagram.slice()); + closing.flip(); + connection.connectionIds().forEach((id) -> + connections.compute(id, (i, r) -> remapClosing(i, r, closing))); + assert !connections.containsValue(connection) : connection; + } + + private ClosedConnection remapClosing(QuicConnectionId id, QuicPacketReceiver conn, ByteBuffer datagram) { + if (closed) return null; + var debugOn = debug.on() && !Thread.currentThread().isVirtual(); + if (conn instanceof ClosingConnection closing) { + // we already have a closing datagram, drop the new one + return closing; + } else if (conn instanceof DrainingConnection draining) { + return draining; + } else if (conn instanceof QuicConnectionImpl impl) { + final long idleTimeout = impl.peerPtoMs() * 3; // 3 PTO + impl.localConnectionIdManager().close(); + if (debugOn) debug.log("remapping %s to ClosingConnection", id); + var closing = new ClosingConnection(conn.connectionIds(), idleTimeout, datagram); + remapPeerIssuedResetToken(impl, closing); + closing.startTimer(); + return closing; + } else if (conn == null) { + // connection absent (was probably removed), don't remap to closing + if (debugOn) { + debug.log("no existing connection present for %s, won't remap to closing", id); + } + return null; + } else { + assert false : "unexpected connection type: " + conn; // just remove + return null; + } + } + + public void registerNewConnection(QuicConnectionImpl quicConnection) throws IOException { + if (closed) throw new ClosedChannelException(); + quicConnection.connectionIds().forEach((id) -> putConnection(id, quicConnection)); + } + + /** + * A peer issues a stateless reset token which it can then send to close the connection. This + * method links the peer issued token against the connection that needs to be closed if/when + * that stateless reset token arrives in the packet. + * + * @param statelessResetToken the peer issued (16 byte) stateless reset token + * @param connection the connection to link the token against + */ + void associateStatelessResetToken(final byte[] statelessResetToken, final QuicPacketReceiver connection) { + Objects.requireNonNull(connection); + Objects.requireNonNull(statelessResetToken); + final int tokenLength = statelessResetToken.length; + if (statelessResetToken.length != 16) { + throw new IllegalArgumentException("Invalid stateless reset token length " + tokenLength); + } + if (debug.on()) { + debug.log("associating stateless reset token with connection %s", connection); + } + this.peerIssuedResetTokens.put(makeToken(statelessResetToken), connection); + } + + /** + * Discard the stateless reset token that this endpoint might have previously + * {@link #associateStatelessResetToken(byte[], QuicPacketReceiver) associated any connection} + * with + * @param statelessResetToken The stateless reset token + */ + void forgetStatelessResetToken(final byte[] statelessResetToken) { + // just a tiny optimization - we know stateless reset token must be of 16 bytes, if the passed + // value isn't, then no point doing any more work + if (statelessResetToken.length != 16) { + return; + } + this.peerIssuedResetTokens.remove(makeToken(statelessResetToken)); + } + + /** + * {@return the timer queue associated with this endpoint} + */ + public QuicTimerQueue timer() { + return timerQueue; + } + + public boolean isChannelClosed() { + return !channel().isOpen(); + } + + /** + * {@return the time source associated with this endpoint} + * @apiNote + * There is a unique global {@linkplain TimeSource#source()} for the whole + * JVM, but this method can be overridden in tests to define an alternative + * timeline for the test. + */ + protected TimeLine timeSource() { + return TimeSource.source(); + } + + private void putConnection(QuicConnectionId id, QuicConnectionImpl quicConnection) { + // ideally we'd want to use an immutable byte buffer as a key here. + // but we don't have that. So we use the connection id instead. + var old = connections.put(id, quicConnection); + assert old == null : "%s already registered with %s (%s)" + .formatted(old, id, old == quicConnection ? "old == new" : "old != new"); + } + + + /** + * Represent a closing or draining quic connection: if we receive any packet + * for this connection we ignore them (if in draining state) or replay the + * closed packets in decreasing frequency: we reply to the + * first packet, then to the third, then to the seventh, etc... + * We stop replying after 16*16/2. + */ + sealed abstract class ClosedConnection implements QuicPacketReceiver, QuicTimedEvent + permits QuicEndpoint.ClosingConnection, QuicEndpoint.DrainingConnection { + + // default time we keep the ClosedConnection alive while closing/draining - if + // PTO information is not available (if 0 is passed as idleTimeoutMs when creating + // an instance of this class) + final static long NO_IDLE_TIMEOUT = 2000; + final List localConnectionIds; + final long maxIdleTimeMs; + final long id; + int more = 1; + int waitformore; + volatile Deadline deadline; + volatile Deadline updatedDeadline; + + ClosedConnection(List localConnectionIds, long maxIdleTimeMs) { + this.id = QuicTimerQueue.newEventId(); + this.maxIdleTimeMs = maxIdleTimeMs == 0 ? NO_IDLE_TIMEOUT : maxIdleTimeMs; + this.deadline = Deadline.MAX; + this.updatedDeadline = Deadline.MAX; + this.localConnectionIds = List.copyOf(localConnectionIds); + } + + @Override + public List connectionIds() { + return localConnectionIds; + } + + @Override + public final void processIncoming(SocketAddress source, ByteBuffer destConnId, HeadersType headersType, ByteBuffer buffer) { + Deadline updated = updatedDeadline; + var waitformore = this.waitformore; + // Deadline.MIN will be set in case of write errors + if (updated != Deadline.MIN && waitformore == 0) { + var more = this.more; + this.waitformore = more; + this.more = more = more << 1; + if (more > 16) { + // the server doesn't seem to take into account our + // connection close frame. Just stop responding + updatedDeadline = Deadline.MIN; + } else { + updatedDeadline = updated.plusMillis(maxIdleTimeMs); + } + handleIncoming(source, destConnId, headersType, buffer); + } else { + this.waitformore = waitformore - 1; + dropIncoming(source, destConnId, headersType, buffer); + } + + timer().reschedule(this, updatedDeadline); + } + + protected void handleIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer) { + dropIncoming(source, idbytes, headersType, buffer); + } + + protected abstract void dropIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer); + + @Override + public final void onWriteError(Throwable t) { + if (debug.on()) + debug.log("failed to write close packet", t); + removeConnection(this); + // handle() will be called, which will cause + // the timer queue to remove this object + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + + public final void startTimer() { + deadline = updatedDeadline = timeSource().instant().plusMillis(maxIdleTimeMs); + timer().offer(this); + } + + @Override + public final Deadline deadline() { + return deadline; + } + + @Override + public final Deadline handle() { + removeConnection(this); + // Deadline.MAX means do not reschedule + return updatedDeadline = Deadline.MAX; + } + + @Override + public final Deadline refreshDeadline() { + // Returning Deadline.MIN here will cause handle() to + // be called and will remove this task from the timer queue. + return deadline = updatedDeadline; + } + + @Override + public final long eventId() { + return id; + } + + @Override + public final void processStatelessReset() { + // the peer has sent us a stateless reset: no need to + // replay CloseConnectionFrame. Just remove this connection. + removeConnection(this); + // handle() will be called, which will cause + // the timer queue to remove this object + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + + public void shutdown() { + updatedDeadline = Deadline.MIN; + timer().reschedule(this); + } + } + + + /** + * Represent a closing quic connection: if we receive any packet for this + * connection we simply replay the packet(s) that contained the + * ConnectionCloseFrame frame. + * Packets are replayed in decreasing frequency. We reply to the + * first packet, then to the third, then to the seventh, etc... + * We stop replying after 16*16/2. + */ + final class ClosingConnection extends ClosedConnection { + + final ByteBuffer closePacket; + + ClosingConnection(List localConnIdManager, long maxIdleTimeMs, + ByteBuffer closePacket) { + super(localConnIdManager, maxIdleTimeMs); + this.closePacket = Objects.requireNonNull(closePacket); + } + + @Override + public void handleIncoming(SocketAddress source, ByteBuffer idbytes, + HeadersType headersType, ByteBuffer buffer) { + if (isClosed() || isChannelClosed()) { + // don't respond with any more datagrams and instead just drop + // the incoming ones since the channel is closed + dropIncoming(source, idbytes, headersType, buffer); + return; + } + if (debug.on()) { + debug.log("ClosingConnection(%s): sending closed packets", localConnectionIds); + } + pushDatagram(this, source, closePacket.asReadOnlyBuffer()); + } + + @Override + protected void dropIncoming(SocketAddress source, ByteBuffer idbytes, HeadersType headersType, ByteBuffer buffer) { + if (debug.on()) { + debug.log("ClosingConnection(%s): dropping %s packet", localConnectionIds, headersType); + } + } + + private DrainingConnection toDraining() { + return new DrainingConnection(localConnectionIds, maxIdleTimeMs); + } + } + + /** + * Represent a draining quic connection: if we receive any packet for this + * connection we simply ignore them. + */ + final class DrainingConnection extends ClosedConnection { + + DrainingConnection(List localConnIdManager, long maxIdleTimeMs) { + super(localConnIdManager, maxIdleTimeMs); + } + + @Override + public void dropIncoming(SocketAddress source, ByteBuffer idbytes, HeadersType headersType, ByteBuffer buffer) { + if (debug.on()) { + debug.log("DrainingConnection(%s): dropping %s packet", + localConnectionIds, headersType); + } + } + + } + + private record StatelessResetToken (byte[] token) { + StatelessResetToken(final byte[] token) { + this.token = token.clone(); + } + @Override + public int hashCode() { + return Arrays.hashCode(token); + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof StatelessResetToken other) { + return Arrays.equals(token, other.token); + } + return false; + } + } + + /** + * {@return a new {@link QuicEndpoint} of the given {@code endpointType}} + * @param endpointType the concrete endpoint type, one of {@link QuicSelectableEndpoint + * QuicSelectableEndpoint.class} or {@link QuicVirtualThreadedEndpoint + * QuicVirtualThreadedEndpoint.class}. + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @param the concrete endpoint type, one of {@link QuicSelectableEndpoint} + * or {@link QuicVirtualThreadedEndpoint} + * @throws IOException if an IOException occurs + * @throws IllegalArgumentException if the given endpoint type is not one of + * {@link QuicSelectableEndpoint QuicSelectableEndpoint.class} or + * {@link QuicVirtualThreadedEndpoint QuicVirtualThreadedEndpoint.class} + */ + private static T create(Class endpointType, + QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) throws IOException { + DatagramChannel channel = DatagramChannel.open(); + // avoid dependency on extnet + Optional> df = channel.supportedOptions().stream(). + filter(o -> "IP_DONTFRAGMENT".equals(o.name())).findFirst(); + if (df.isPresent()) { + // TODO on some platforms this doesn't work on dual stack sockets + // see Net#shouldSetBothIPv4AndIPv6Options + @SuppressWarnings("unchecked") + var option = (SocketOption) df.get(); + channel.setOption(option, true); + } + if (QuicSelectableEndpoint.class.isAssignableFrom(endpointType)) { + channel.configureBlocking(false); + } + Consumer logSink = Log.quic() ? Log::logQuic : null; + Utils.configureChannelBuffers(logSink, channel, + quicInstance.getReceiveBufferSize(), quicInstance.getSendBufferSize()); + channel.bind(bindAddress); // could do that on attach instead? + + if (endpointType.isAssignableFrom(QuicSelectableEndpoint.class)) { + return endpointType.cast(new QuicSelectableEndpoint(quicInstance, channel, name, timerQueue)); + } else if (endpointType.isAssignableFrom(QuicVirtualThreadedEndpoint.class)) { + return endpointType.cast(new QuicVirtualThreadedEndpoint(quicInstance, channel, name, timerQueue)); + } else { + throw new IllegalArgumentException(endpointType.getName()); + } + } + + public static final class QuicEndpointFactory { + + public QuicEndpointFactory() { + } + + /** + * {@return a new {@code QuicSelectableEndpoint}} + * + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @throws IOException if an IOException occurs + */ + public QuicSelectableEndpoint createSelectableEndpoint(QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) + throws IOException { + return create(QuicSelectableEndpoint.class, quicInstance, name, bindAddress, timerQueue); + } + + /** + * {@return a new {@code QuicVirtualThreadedEndpoint}} + * + * @param quicInstance the quic instance + * @param name the endpoint name + * @param bindAddress the address to bind to + * @param timerQueue the timer queue + * @throws IOException if an IOException occurs + */ + public QuicVirtualThreadedEndpoint createVirtualThreadedEndpoint(QuicInstance quicInstance, + String name, + SocketAddress bindAddress, + QuicTimerQueue timerQueue) + throws IOException { + return create(QuicVirtualThreadedEndpoint.class, quicInstance, name, bindAddress, timerQueue); + } + } + + /** + * Registers the given endpoint with the given selector. + *

        + * An endpoint of class {@link QuicSelectableEndpoint} is only + * compatible with a selector of type {@link QuicNioSelector}. + * An endpoint of tyoe {@link QuicVirtualThreadedEndpoint} is only + * compatible with a selector of type {@link QuicVirtualThreadPoller}. + *
        + * If the given endpoint implementation is not compatible with + * the given selector implementation an {@link IllegalStateException} + * is thrown. + * + * @param endpoint the endpoint + * @param selector the selector + * @param debug a logger for debugging + * + * @throws IOException if an IOException occurs + * @throws IllegalStateException if the endpoint and selector implementations + * are not compatible + */ + public static void registerWithSelector(QuicEndpoint endpoint, QuicSelector selector, Logger debug) + throws IOException { + if (selector instanceof QuicVirtualThreadPoller poller) { + var loopingEndpoint = (QuicVirtualThreadedEndpoint) endpoint; + poller.register(loopingEndpoint); + } else if (selector instanceof QuicNioSelector selectable) { + var selectableEndpoint = (QuicEndpoint.QuicSelectableEndpoint) endpoint; + selectable.register(selectableEndpoint); + } else { + throw new IllegalStateException("Incompatible selector and endpoint implementations: %s <-> %s" + .formatted(selector.getClass(), endpoint.getClass())); + } + if (debug.on()) debug.log("endpoint registered with selector"); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java new file mode 100644 index 00000000000..a963625d7f5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicInstance.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.HexFormat; +import java.util.List; +import java.util.concurrent.Executor; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +/** + * A {@code QuicInstance} represents a common abstraction which is + * either a {@code QuicClient} or a {@code QuicServer}, or possibly + * both. It defines the subset of public methods that a + * {@code QuicEndpoint} and a {@code QuicSelector} need to operate + * with a quic client, or a quic server; + */ +public interface QuicInstance { + + /** + * The executor used by this quic instance when a task needs to + * be offloaded to a separate thread. + * @implNote This is the HttpClientImpl internal executor. + * @return the executor used by this QuicClient. + */ + Executor executor(); + + /** + * {@return an endpoint to associate with a connection} + * @throws IOException + */ + QuicEndpoint getEndpoint() throws IOException; + + /** + * This method is called when a quic packet that couldn't be attributed + * to a registered connection is received. + * @param source the source address of the datagram + * @param type the packet type + * @param buffer A buffer positioned at the start of the quic packet + */ + void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer); + + /** + * {@return true if the passed version is available for use on this instance, false otherwise} + */ + boolean isVersionAvailable(QuicVersion quicVersion); + + /** + * {@return the versions that are available for use on this instance} + */ + List getAvailableVersions(); + + /** + * Instance ID used for debugging traces. + * @return A string uniquely identifying this instance. + */ + String instanceId(); + + /** + * Get the QuicTLSContext used by this quic instance. + * @return the QuicTLSContext used by this quic instance. + */ + QuicTLSContext getQuicTLSContext(); + + QuicTransportParameters getTransportParameters(); + + /** + * The {@link SSLParameters} for this Quic instance. + * May be {@code null} if no parameters have been specified. + * + * @implSpec + * The default implementation of this method returns {@code null}. + * + * @return The {@code SSLParameters} for this quic instance or {@code null}. + */ + default SSLParameters getSSLParameters() { return new SSLParameters(); } + + /** + * {@return the configured {@linkplain java.net.StandardSocketOptions#SO_RCVBUF + * UDP receive buffer} size this instance should use} + */ + default int getReceiveBufferSize() { + return Utils.getIntegerNetProperty( + "jdk.httpclient.quic.receiveBufferSize", + 0 // only set the size if > 0 + ); + } + + /** + * {@return the configured {@linkplain java.net.StandardSocketOptions#SO_SNDBUF + * UDP send buffer} size this instance should use} + */ + default int getSendBufferSize() { + return Utils.getIntegerNetProperty( + "jdk.httpclient.quic.sendBufferSize", + 0 // only set the size if > 0 + ); + } + + /** + * {@return a string describing the given application error code} + * @param errorCode an application error code + * @implSpec By default, this method returns a generic + * string containing the hexadecimal value of the given errorCode. + * Subclasses built for supporting a given application protocol, + * such as HTTP/3, may override this method to return more + * specific names, such as for instance, {@code "H3_REQUEST_CANCELLED"} + * for {@code 0x010c}. + * @apiNote This method is typically used for logging and/or debugging + * purposes, to generate a more user-friendly log message. + */ + default String appErrorToString(long errorCode) { + return "ApplicationError(code=0x" + HexFormat.of().toHexDigits(errorCode) + ")"; + } + + default String name() { + return String.format("%s(%s)", this.getClass().getSimpleName(), instanceId()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java new file mode 100644 index 00000000000..6652b44de3a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacketReceiver.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.quic.QuicEndpoint.QuicDatagram; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Optional; + +/** + * The {@code QuicPacketReceiver} is an abstraction that defines the + * interface between a {@link QuicEndpoint} and a {@link QuicConnection}. + * This defines the minimum set of methods that the endpoint will need + * in order to be able to dispatch a received {@link jdk.internal.net.http.quic.packets.QuicPacket} + * to its destination. This abstraction is typically useful when dealing with + * {@linkplain QuicEndpoint.ClosedConnection + * closed connections, which need to remain alive for a certain time + * after being closed in order to satisfy the requirement of the quic + * protocol (typically for retransmitting the CLOSE_CONNECTION frame + * if needed). + */ +public interface QuicPacketReceiver { + + /** + * {@return a list of local connectionIds for this connection) + */ + List connectionIds(); + + /** + * {@return the initial connection id assigned by the peer} + * On the client side, this is always {@link Optional#empty()}. + * On the server side, it contains the initial connection id + * that was assigned by the client in the first INITIAL packet. + * + * @implSpec + * The default implementation of this method returns {@link Optional#empty()} + */ + default Optional initialConnectionId() { + return Optional.empty(); + } + + /** + * Called when an incoming datagram is received. + *

        + * The buffer is positioned at the start of the datagram to process. + * The buffer may contain more than one QUIC packet. + * + * @param source The peer address, as received from the UDP stack + * @param destConnId Destination connection id bytes included in the packet + * @param headersType The quic packet type + * @param buffer A buffer positioned at the start of the quic packet, + * not yet decrypted, and possibly containing coalesced + * packets. + */ + void processIncoming(SocketAddress source, ByteBuffer destConnId, + QuicPacket.HeadersType headersType, ByteBuffer buffer); + + /** + * Called when a datagram scheduled for writing by this connection + * could not be written to the network. + * @param t the error that occurred + */ + void onWriteError(Throwable t); + + /** + * Called when a stateless reset token is received. + */ + void processStatelessReset(); + + /** + * Called to shut a closed connection down. + * This is the last step when closing a connection, and typically + * only release resources held by all packet spaces. + */ + void shutdown(); + + /** + * Called after a datagram has been written to the socket. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was sent + */ + default void datagramSent(QuicDatagram datagram) { } + + /** + * Called after a datagram has been discarded as a result of + * some error being raised, for instance, when an attempt + * to write it to the socket has failed, or if the encryption + * of a packet in the datagram has failed. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was discarded + */ + default void datagramDiscarded(QuicDatagram datagram) { } + + /** + * Called after a datagram has been dropped. Typically, this + * could happen if the datagram was only partly written, or if + * the connection was closed before the datagram could be sent. + * At this point the datagram's ByteBuffer can typically be released, + * or returned to a buffer pool. + * @implSpec + * The default implementation of this method does nothing. + * @param datagram the datagram that was dropped + */ + default void datagramDropped(QuicDatagram datagram) { } + + /** + * {@return whether this receiver accepts packets from the given source} + * @param source the sender address + */ + default boolean accepts(SocketAddress source) { + return true; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java new file mode 100644 index 00000000000..fde253740d1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; + +import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Implementation of QUIC congestion controller based on RFC 9002. + * This is a QUIC variant of New Reno algorithm. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +class QuicRenoCongestionController implements QuicCongestionController { + // higher of 14720 and 2*maxDatagramSize; we use fixed maxDatagramSize + private static final int INITIAL_WINDOW = Math.max(14720, 2 * QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE); + private static final int MAX_BYTES_IN_FLIGHT = Math.clamp( + Utils.getLongProperty("jdk.httpclient.quic.maxBytesInFlight", 1 << 24), + 1 << 14, 1 << 24); + private final TimeLine timeSource; + private final String dbgTag; + private final Lock lock = new ReentrantLock(); + private long congestionWindow = INITIAL_WINDOW; + private int maxDatagramSize = QuicConnectionImpl.DEFAULT_DATAGRAM_SIZE; + private int minimumWindow = 2 * maxDatagramSize; + private long bytesInFlight; + // maximum bytes in flight seen since the last congestion event + private long maxBytesInFlight; + private Deadline congestionRecoveryStartTime; + private long ssThresh = Long.MAX_VALUE; + + public QuicRenoCongestionController(String dbgTag) { + this.dbgTag = dbgTag; + this.timeSource = TimeSource.source(); + } + + private boolean inCongestionRecovery(Deadline sentTime) { + return (congestionRecoveryStartTime != null && + !sentTime.isAfter(congestionRecoveryStartTime)); + } + + private void onCongestionEvent(Deadline sentTime) { + if (inCongestionRecovery(sentTime)) { + return; + } + congestionRecoveryStartTime = timeSource.instant(); + ssThresh = congestionWindow / 2; + congestionWindow = Math.max(minimumWindow, ssThresh); + maxBytesInFlight = 0; + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow); + } + } + + private static boolean inFlight(QuicPacket packet) { + // packet is in flight if it contains anything other than a single ACK frame + // specifically, a packet containing padding is considered to be in flight. + return packet.frames().size() != 1 || + !(packet.frames().get(0) instanceof AckFrame); + } + + @Override + public boolean canSendPacket() { + lock.lock(); + try { + if (bytesInFlight >= MAX_BYTES_IN_FLIGHT) { + return false; + } + var canSend = congestionWindow - bytesInFlight >= maxDatagramSize; + return canSend; + } finally { + lock.unlock(); + } + } + + @Override + public void updateMaxDatagramSize(int newSize) { + lock.lock(); + try { + if (minimumWindow != newSize * 2) { + minimumWindow = newSize * 2; + maxDatagramSize = newSize; + congestionWindow = Math.max(congestionWindow, minimumWindow); + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetSent(int packetBytes) { + lock.lock(); + try { + bytesInFlight += packetBytes; + if (bytesInFlight > maxBytesInFlight) { + maxBytesInFlight = bytesInFlight; + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetAcked(int packetBytes, Deadline sentTime) { + lock.lock(); + try { + bytesInFlight -= packetBytes; + // RFC 9002 says we should not increase cwnd when application limited. + // The concept itself is poorly defined. + // Here we limit cwnd growth based on the maximum bytes in flight + // observed since the last congestion event + if (inCongestionRecovery(sentTime)) { + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Acked, in recovery: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } + return; + } + boolean isAppLimited; + if (congestionWindow < ssThresh) { + isAppLimited = congestionWindow >= 2 * maxBytesInFlight; + if (!isAppLimited) { + congestionWindow += packetBytes; + } + } else { + isAppLimited = congestionWindow > maxBytesInFlight + 2L * maxDatagramSize; + if (!isAppLimited) { + congestionWindow += Math.max((long) maxDatagramSize * packetBytes / congestionWindow, 1L); + } + } + if (Log.quicCC()) { + if (isAppLimited) { + Log.logQuic(dbgTag+ " Acked, not blocked: bytes: " + packetBytes + + ", in flight: " + bytesInFlight); + } else { + Log.logQuic(dbgTag + " Acked, increased: bytes: " + packetBytes + + ", in flight: " + bytesInFlight + + ", new cwnd:" + congestionWindow); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent) { + lock.lock(); + try { + for (QuicPacket packet : lostPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + onCongestionEvent(sentTime); + if (persistent) { + congestionWindow = minimumWindow; + congestionRecoveryStartTime = null; + if (Log.quicCC()) { + Log.logQuic(dbgTag+ " Persistent congestion: ssThresh: " + ssThresh + + ", in flight: " + bytesInFlight + + ", cwnd:" + congestionWindow); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void packetDiscarded(Collection discardedPackets) { + lock.lock(); + try { + for (QuicPacket packet : discardedPackets) { + if (inFlight(packet)) { + bytesInFlight -= packet.size(); + } + } + } finally { + lock.unlock(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java new file mode 100644 index 00000000000..0e2f9401bc0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRttEstimator.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Utils; + +/** + * Estimator for quic connection round trip time. + * Defined in + * RFC 9002 section 5. + * Takes RTT samples as input (max 1 sample per ACK frame) + * Produces: + * - minimum RTT over a period of time (minRtt) for internal use + * - exponentially weighted moving average (smoothedRtt) + * - mean deviation / variation in the observed samples (rttVar) + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + */ +public class QuicRttEstimator { + + // The property indicates the maximum number of retries. The constant holds the value + // 2^N where N is the value of the property (clamped between 2 and 20, default 8): + // 1=>2 2=>4 3=>8 4=>16 5=>32 6=>64 7=>128 8=>256 ... etc + public static final long MAX_PTO_BACKOFF = 1L << Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.maxPtoBackoff", 8), + 2, 20); + // The timeout calculated for PTO will stay clamped at MAX_PTO_BACKOFF_TIMEOUT if + // the calculated value exceeds MAX_PTO_BACKOFF_TIMEOUT + public static final Duration MAX_PTO_BACKOFF_TIMEOUT = Duration.ofSeconds(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.maxPtoBackoffTime", 240), + 1, 1200)); + // backoff will continue to be increased past MAX_PTO_BACKOFF if the timeout calculated + // for PTO is less than MIN_PTO_BACKOFF_TIMEOUT + public static final Duration MIN_PTO_BACKOFF_TIMEOUT = Duration.ofSeconds(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.minPtoBackoffTime", 15), + 0, 1200)); + + private static final long INITIAL_RTT = TimeUnit.MILLISECONDS.toMicros(Math.clamp( + Utils.getIntegerNetProperty("jdk.httpclient.quic.initialRTT", 333), + 50, 1000)); + + // kGranularity, 1ms is recommended by RFC 9002 section 6.1.2 + private static final long GRANULARITY_MICROS = TimeUnit.MILLISECONDS.toMicros(1); + private Deadline firstSample; + private long latestRttMicros; + private long minRttMicros; + private long smoothedRttMicros = INITIAL_RTT; + private long rttVarMicros = INITIAL_RTT / 2; + private long ptoBackoffFactor = 1; + private long rttSampleCount = 0; + + public record QuicRttEstimatorState(long latestRttMicros, + long minRttMicros, + long smoothedRttMicros, + long rttVarMicros, + long rttSampleCount) {} + + public synchronized QuicRttEstimatorState state() { + return new QuicRttEstimatorState(latestRttMicros, minRttMicros, smoothedRttMicros, rttVarMicros, rttSampleCount); + } + + /** + * Update the estimator with latest RTT sample. + * Use only samples where: + * - the largest acknowledged PN is newly acknowledged + * - at least one of the newly acked packets is ack-eliciting + * @param latestRttMicros time between when packet was sent + * and ack was received, in microseconds + * @param ackDelayMicros ack delay received in ack frame, decoded to microseconds + * @param now time at which latestRttMicros was calculated + */ + public synchronized void consumeRttSample(long latestRttMicros, long ackDelayMicros, Deadline now) { + this.rttSampleCount += 1; + this.latestRttMicros = latestRttMicros; + if (firstSample == null) { + firstSample = now; + minRttMicros = latestRttMicros; + smoothedRttMicros = latestRttMicros; + rttVarMicros = latestRttMicros / 2; + } else { + minRttMicros = Math.min(minRttMicros, latestRttMicros); + long adjustedRtt; + if (latestRttMicros >= minRttMicros + ackDelayMicros) { + adjustedRtt = latestRttMicros - ackDelayMicros; + } else { + adjustedRtt = latestRttMicros; + } + rttVarMicros = (3 * rttVarMicros + Math.abs(smoothedRttMicros - adjustedRtt)) / 4; + smoothedRttMicros = (7 * smoothedRttMicros + adjustedRtt) / 8; + } + } + + /** + * {@return time threshold for time-based loss detection} + * See + * RFC 9002 section 6.1.2 + * + */ + public synchronized Duration getLossThreshold() { + // max(kTimeThreshold * max(smoothed_rtt, latest_rtt), kGranularity) + long maxRttMicros = Math.max(smoothedRttMicros, latestRttMicros); + long lossThresholdMicros = Math.max(9*maxRttMicros / 8, GRANULARITY_MICROS); + return Duration.of(lossThresholdMicros, ChronoUnit.MICROS); + } + + /** + * {@return the amount of time to wait for acknowledgement of a sent packet, + * excluding max ack delay} + * See + * RFC 9002 section 6.1.2 + */ + public synchronized Duration getBasePtoDuration() { + // PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay + // max_ack_delay is applied by the caller + long basePtoMicros = smoothedRttMicros + + Math.max(4 * rttVarMicros, GRANULARITY_MICROS); + return Duration.of(basePtoMicros, ChronoUnit.MICROS); + } + + public synchronized boolean isMinBackoffTimeoutExceeded() { + return MIN_PTO_BACKOFF_TIMEOUT.compareTo(getBasePtoDuration().multipliedBy(ptoBackoffFactor)) < 0; + } + + public synchronized long getPtoBackoff() { + return ptoBackoffFactor; + } + + public synchronized long increasePtoBackoff() { + // limit to make sure we don't accidentally overflow + if (ptoBackoffFactor <= MAX_PTO_BACKOFF || !isMinBackoffTimeoutExceeded()) { + ptoBackoffFactor *= 2; // can go up to 2 * MAX_PTO_BACKOFF + } + return ptoBackoffFactor; + } + + public synchronized void resetPtoBackoff() { + ptoBackoffFactor = 1; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java new file mode 100644 index 00000000000..2bce3415b17 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicSelector.java @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ClosedSelectorException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.TimeLine; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint.QuicVirtualThreadedEndpoint; +import jdk.internal.net.http.quic.QuicEndpoint.QuicSelectableEndpoint; + + +/** + * A QUIC selector to select over one or several quic transport + * endpoints. + */ +public abstract sealed class QuicSelector implements Runnable, AutoCloseable + permits QuicSelector.QuicNioSelector, QuicSelector.QuicVirtualThreadPoller { + + /** + * The maximum timeout passed to Selector::select. + */ + public static final long IDLE_PERIOD_MS = 1500; + + private static final TimeLine source = TimeSource.source(); + final Logger debug = Utils.getDebugLogger(this::name); + + private final String name; + private volatile boolean done; + private final QuicInstance instance; + private final QuicSelectorThread thread; + private final QuicTimerQueue timerQueue; + + private QuicSelector(QuicInstance instance, String name) { + this.instance = instance; + this.name = name; + this.timerQueue = new QuicTimerQueue(this::wakeup, debug); + this.thread = new QuicSelectorThread(this); + } + + public String name() { + return name; + } + + // must be overridden by subclasses + public void register(T endpoint) throws ClosedChannelException { + if (debug.on()) debug.log("attaching %s", endpoint); + } + + // must be overridden by subclasses + public void wakeup() { + if (debug.on()) debug.log("waking up selector"); + } + + public QuicTimerQueue timer() { + return timerQueue; + } + + /** + * A {@link QuicSelector} implementation based on blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * Virtual Threads to poll the channels. + * This implementation is tied to {@link QuicVirtualThreadedEndpoint} instances. + */ + static final class QuicVirtualThreadPoller extends QuicSelector { + + static final boolean usePlatformThreads = + Utils.getBooleanProperty("jdk.internal.httpclient.quic.poller.usePlatformThreads", false); + + static final class EndpointTask implements Runnable { + + final QuicVirtualThreadedEndpoint endpoint; + final ConcurrentLinkedQueue endpoints; + EndpointTask(QuicVirtualThreadedEndpoint endpoint, + ConcurrentLinkedQueue endpoints) { + this.endpoint = endpoint; + this.endpoints = endpoints; + } + + public void run() { + try { + endpoint.channelReadLoop(); + } finally { + endpoints.remove(this); + } + } + } + + private final Object waiter = new Object(); + private final ConcurrentLinkedQueue endpoints = new ConcurrentLinkedQueue<>(); + private final ReentrantLock stateLock = new ReentrantLock(); + private final ExecutorService virtualThreadService; + + private volatile long wakeups; + + + private QuicVirtualThreadPoller(QuicInstance instance, String name) { + super(instance, name); + virtualThreadService = usePlatformThreads + ? Executors.newThreadPerTaskExecutor(Thread.ofPlatform() + .name(name + "-pt-worker", 1).factory()) + : Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name(name + "-vt-worker-", 1).factory()); + if (debug.on()) debug.log("created"); + } + + ExecutorService readLoopExecutor() { + return virtualThreadService; + } + + @Override + public void register(QuicVirtualThreadedEndpoint endpoint) throws ClosedChannelException { + super.register(endpoint); + endpoint.attach(this); + } + + public Future startReading(QuicVirtualThreadedEndpoint endpoint) { + EndpointTask task; + stateLock.lock(); + try { + if (done()) throw new ClosedSelectorException(); + task = new EndpointTask(endpoint, endpoints); + endpoints.add(task); + return virtualThreadService.submit(task); + } finally { + stateLock.unlock(); + } + } + + void markDone() { + // use stateLock to prevent startReading + // to be called *after* shutdown. + stateLock.lock(); + try { + super.shutdown(); + } finally { + stateLock.unlock(); + } + } + + @Override + public void shutdown() { + markDone(); + try { + virtualThreadService.shutdown(); + } finally { + wakeup(); + } + } + + @Override + public void wakeup() { + super.wakeup(); + synchronized (waiter) { + wakeups++; + // there's only one thread that can be waiting + // on waiter - the thread that executes the run() + // method. + waiter.notify(); + } + } + + @Override + public void run() { + try { + if (debug.on()) debug.log("started"); + long waited = 0; + while (!done()) { + var wakeups = this.wakeups; + long timeout = Math.min(computeNextDeadLine(), IDLE_PERIOD_MS); + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: wait(%s) wakeups:%s (+%s), waited:%s", + name(), timeout, this.wakeups, this.wakeups - wakeups, waited)); + } else if (debug.on()) { + debug.log("wait(%s) wakeups:%s (+%s), waited: %s", + timeout, this.wakeups, this.wakeups - wakeups, waited); + } + long wwaited = -1; + synchronized (waiter) { + if (done()) return; + if (wakeups == this.wakeups) { + var start = System.nanoTime(); + waiter.wait(timeout); + var stop = System.nanoTime(); + wwaited = waited = (stop - start) / 1000_000; + } else waited = 0; + } + if (wwaited != -1 && wwaited < timeout) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: waked up early: waited %s, timeout %s", + name(), waited, timeout)); + } + } + } + } catch (Throwable t) { + if (done()) return; + if (debug.on()) debug.log("Selector failed", t); + if (Log.errors()) { + Log.logError("QuicVirtualThreadPoller: selector exiting due to " + t); + Log.logError(t); + } + abort(t); + } finally { + if (debug.on()) debug.log("exiting"); + if (!done()) markDone(); + timer().stop(); + endpoints.removeIf(this::close); + virtualThreadService.close(); + } + } + + boolean close(EndpointTask task) { + try { + task.endpoint.close(); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint %s: %s", task.endpoint.name(), e); + } + } + return true; + } + + boolean abort(EndpointTask task, Throwable error) { + try { + task.endpoint.abort(error); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint %s: %s", task.endpoint.name(), e); + } + } + return true; + } + + @Override + public void abort(Throwable t) { + super.shutdown(); + endpoints.removeIf(task -> abort(task, t)); + super.abort(t); + } + } + + /** + * A {@link QuicSelector} implementation based on non-blocking + * {@linkplain DatagramChannel Datagram Channels} and using a + * NIO {@link Selector}. + * This implementation is tied to {@link QuicSelectableEndpoint} instances. + */ + static final class QuicNioSelector extends QuicSelector { + final Selector selector; + + private QuicNioSelector(QuicInstance instance, Selector selector, String name) { + super(instance, name); + this.selector = selector; + if (debug.on()) debug.log("created"); + } + + + public void register(QuicSelectableEndpoint endpoint) throws ClosedChannelException { + super.register(endpoint); + endpoint.attach(selector); + selector.wakeup(); + } + + public void wakeup() { + super.wakeup(); + selector.wakeup(); + } + + /** + * Shuts down the {@code QuicSelector} by marking the + * {@linkplain QuicSelector#shutdown() selector done}, + * and {@linkplain Selector#wakeup() waking up the + * selector thread}. + * Upon waking up, the selector thread will invoke + * {@link Selector#close()}. + * This method doesn't wait for the selector thread to terminate. + * @see #awaitTermination(long, TimeUnit) + */ + public void shutdown() { + super.shutdown(); + selector.wakeup(); + } + + @Override + public void run() { + try { + if (debug.on()) debug.log("started"); + while (!done()) { + long timeout = Math.min(computeNextDeadLine(), IDLE_PERIOD_MS); + // selected = 0 indicates that no key had its ready ops changed: + // it doesn't mean that no key is ready. Therefore - if a key + // was ready to read, and is again ready to read, it doesn't + // increment the selected count. + if (debug.on()) debug.log("select(%s)", timeout); + int selected = selector.select(timeout); + var selectedKeys = selector.selectedKeys(); + if (debug.on()) { + debug.log("Selected: changes=%s, keys=%s", selected, selectedKeys.size()); + } + + // We do not synchronize on selectedKeys: selectedKeys is only + // modified in this thread, whether directly, by calling selectedKeys.clear() below, + // or indirectly, by calling selector.close() below. + for (var key : selectedKeys) { + QuicSelectableEndpoint endpoint = (QuicSelectableEndpoint) key.attachment(); + if (debug.on()) { + debug.log("selected(%s): %s", Utils.readyOps(key), endpoint); + } + try { + endpoint.selected(key.readyOps()); + } catch (CancelledKeyException x) { + if (debug.on()) { + debug.log("Key for %s cancelled: %s", endpoint.name(), x); + } + } + } + // need to clear the selected keys. select won't do that. + selectedKeys.clear(); + } + } catch (Throwable t) { + if (done()) return; + if (debug.on()) debug.log("Selector failed", t); + if (Log.errors()) { + Log.logError("QuicNioSelector: selector exiting due to " + t); + Log.logError(t); + } + abort(t); + } finally { + if (debug.on()) debug.log("exiting"); + timer().stop(); + + try { + selector.close(); + } catch (IOException io) { + if (debug.on()) debug.log("failed to close selector: " + io); + } + } + } + + boolean abort(SelectionKey key, Throwable error) { + try { + QuicSelectableEndpoint endpoint = (QuicSelectableEndpoint) key.attachment(); + endpoint.abort(error); + } catch (Throwable e) { + if (debug.on()) { + debug.log("Failed to close endpoint associated with key %s: %s", key, error); + } + } + return true; + } + + @Override + public void abort(Throwable error) { + super.shutdown(); + try { + if (selector.isOpen()) { + for (var k : selector.keys()) { + abort(k, error); + } + } + } catch (ClosedSelectorException cse) { + // ignore + } finally { + super.abort(error); + } + } + } + + public long computeNextDeadLine() { + Deadline now = source.instant(); + Deadline deadline = timerQueue.processEventsAndReturnNextDeadline(now, instance.executor()); + if (deadline.equals(Deadline.MAX)) return IDLE_PERIOD_MS; + if (deadline.equals(Deadline.MIN)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: %s millis until %s", name, 1, "now")); + } + return 1; + } + now = source.instant(); + long millis = now.until(deadline, ChronoUnit.MILLIS); + // millis could be 0 if the next deadline is within 1ms of now. + // in that case, round up millis to 1ms since returning 0 + // means the selector would block indefinitely + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: %s millis until %s", + name, (millis <= 0L ? 1L : millis), deadline)); + } + return millis <= 0L ? 1L : millis; + } + + public void start() { + thread.start(); + } + + /** + * Shuts down the {@code QuicSelector} by invoking {@link Selector#close()}. + * This method doesn't wait for the selector thread to terminate. + * @see #awaitTermination(long, TimeUnit) + */ + public void shutdown() { + if (debug.on()) debug.log("closing"); + done = true; + } + + boolean done() { + return done; + } + + /** + * Awaits termination of the selector thread, up until + * the given timeout has elapsed. + * If the current thread is the selector thread, returns + * immediately without waiting. + * + * @param timeout the maximum time to wait for termination + * @param unit the timeout unit + */ + public void awaitTermination(long timeout, TimeUnit unit) { + if (Thread.currentThread() == thread) { + return; + } + try { + thread.join(unit.toMillis(timeout)); + } catch (InterruptedException ie) { + if (debug.on()) debug.log("awaitTermination interrupted: " + ie); + Thread.currentThread().interrupt(); + } + } + + /** + * Closes this {@code QuicSelector}. + * This method calls {@link #shutdown()} and then {@linkplain + * #awaitTermination(long, TimeUnit) waits for the selector thread + * to terminate}, up to two {@link #IDLE_PERIOD_MS}. + */ + @Override + public void close() { + shutdown(); + awaitTermination(IDLE_PERIOD_MS * 2, TimeUnit.MILLISECONDS); + } + + @Override + public String toString() { + return name; + } + + // Called in case of RejectedExecutionException, or shutdownNow; + public void abort(Throwable t) { + shutdown(); + } + + static class QuicSelectorThread extends Thread { + QuicSelectorThread(QuicSelector selector) { + super(null, selector, + "Thread(%s)".formatted(selector.name()), + 0, false); + this.setDaemon(true); + } + } + + /** + * {@return a new instance of {@code QuicNioSelector}} + *

        + * A {@code QuicNioSelector} is an implementation of {@link QuicSelector} + * based on non blocking {@linkplain DatagramChannel Datagram Channels} and + * using an underlying {@linkplain Selector NIO Selector}. + *

        + * The returned implementation can only be used with + * {@link QuicSelectableEndpoint} endpoints. + * + * @param quicInstance the quic instance + * @param name the selector name + * @throws IOException if an IOException occurs when creating the underlying {@link Selector} + */ + public static QuicSelector createQuicNioSelector(QuicInstance quicInstance, String name) + throws IOException { + Selector selector = Selector.open(); + return new QuicNioSelector(quicInstance, selector, name); + } + + /** + * {@return a new instance of {@code QuicVirtualThreadPoller}} + * A {@code QuicVirtualThreadPoller} is an implementation of + * {@link QuicSelector} based on blocking {@linkplain DatagramChannel + * Datagram Channels} and using {@linkplain Thread#ofVirtual() + * Virtual Threads} to poll the datagram channels. + *

        + * The returned implementation can only be used with + * {@link QuicVirtualThreadedEndpoint} endpoints. + * + * @param quicInstance the quic instance + * @param name the selector name + */ + public static QuicSelector createQuicVirtualThreadPoller(QuicInstance quicInstance, String name) { + return new QuicVirtualThreadPoller(quicInstance, name); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java new file mode 100644 index 00000000000..e5802fef20c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicStreamLimitException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +/** + * Used internally to indicate Quic stream limit has been reached + */ +public final class QuicStreamLimitException extends Exception { + + @java.io.Serial + private static final long serialVersionUID = 4181770819022847041L; + + public QuicStreamLimitException(String message) { + super(message); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java new file mode 100644 index 00000000000..9269b12bf64 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimedEvent.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Comparator; + +import jdk.internal.net.http.common.Deadline; + +/** + * Models an event which is triggered upon reaching a + * deadline. {@code QuicTimedEvent} instances are designed to be + * registered with a single {@link QuicTimerQueue}. + * + * @implSpec + * Implementations should make sure that each instance of + * {@code QuicTimedEvent} is only present once in a single + * {@link QuicTimerQueue} at any given time. It is however + * allowed to register the event again with the same {@link QuicTimerQueue} + * after it has been handled, or if it is no longer registered in any + * queue. + */ +sealed interface QuicTimedEvent + permits PacketSpaceManager.PacketTransmissionTask, + QuicTimerQueue.Marker, + QuicEndpoint.ClosedConnection, + IdleTimeoutManager.IdleTimeoutEvent, + IdleTimeoutManager.StreamDataBlockedEvent, + QuicConnectionImpl.MaxInitialTimer { + + /** + * {@return the deadline at which the event should be triggered, + * or {@link Deadline#MAX} if the event does not need + * to be scheduled} + * @implSpec + * Care should be taken to not change the deadline while the + * event is registered with a {@link QuicTimerQueue timer queue}. + * The only safe time when the deadline can be changed is: + *

          + *
        • when {@link #refreshDeadline()} method, since the event + * is not in any queue at that point,
        • + *
        • when the deadline is {@link Deadline#MAX}, since the + * event should not be in any queue if it has no + * deadline
        • + *
        + * + */ + Deadline deadline(); + + /** + * Handles the triggered event. + * Returns a new deadline, if the event needs to be + * rescheduled, or {@code Deadline.MAX} otherwise. + * + * @implSpec + * The {@link #deadline() deadline} should not be + * changed before {@link #refreshDeadline()} is called. + * + * @return a new deadline if the event should be + * rescheduled right away, {@code Deadline.MAX} + * otherwise. + */ + Deadline handle(); + + /** + * An event id, obtained at construction time from + * {@link QuicTimerQueue#newEventId()}. This is used + * to implement a total order among subclasses. + * @return this event's id. + */ + long eventId(); + + /** + * {@return true if this event's deadline is before the + * other's event deadline} + * + * @implSpec + * The default implementation of this method is to return {@code + * deadline().isBefore(other.deadline())}. + * + * @param other the other event + */ + default boolean isBefore(QuicTimedEvent other) { + return deadline().isBefore(other.deadline()); + } + + /** + * Compares this event's deadline with the other event's deadline. + * + * @implSpec + * The default implementation of this method compares deadlines in the same manner as + * {@link Deadline#compareTo(Deadline) this.deadline().compareTo(other.deadline())} would. + * + * @param other the other event + * + * @return {@code -1}, {@code 0}, or {@code 1} depending on whether this + * event's deadline is before, equals to, or after, the other event's + * deadline. + */ + default int compareDeadlines(QuicTimedEvent other) { return deadline().compareTo(other.deadline());} + + /** + * Called to cause an event to refresh its deadline. + * This method is called by the {@link QuicTimerQueue} + * when rescheduling an event. + * @apiNote + * The value returned by {@link #deadline()} can only be safely + * updated when this method is called. + */ + Deadline refreshDeadline(); + + /** + * Compares two instance of {@link QuicTimedEvent}. + * First compared their {@link #deadline()}, then their {@link #eventId()}. + * It is expected that two elements with same deadline and same event id + * must the same {@link QuicTimedEvent} instance. + * + * @param one a first QuicTimedEvent instance + * @param two a second QuicTimedEvent instance + * @return whether the first element is less, same, or greater than the + * second. + */ + static int compare(QuicTimedEvent one, QuicTimedEvent two) { + if (one == two) return 0; + int cmp = one.compareDeadlines(two); + cmp = cmp == 0 ? Long.compare(one.eventId(), two.eventId()) : cmp; + // ensure total ordering; + assert cmp != 0 || one.equals(two) && two.equals(one); + return cmp; + } + + /** + * A comparator that compares {@code QuicTimedEvent} instances by their deadline, in the same + * manner as {@link #compare(QuicTimedEvent, QuicTimedEvent) QuicTimedEvent::compare}. + */ + // public static final (are redundant) + Comparator COMPARATOR = QuicTimedEvent::compare; + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java new file mode 100644 index 00000000000..830415593cb --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTimerQueue.java @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.common.Deadline; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.TimeSource; +import jdk.internal.net.http.common.Utils; + +/** + * A timer queue that can process events which are due, and possibly + * reschedule them if needed. An instance of a {@link QuicTimerQueue} + * is usually associated with an instance of {@link QuicSelector} which + * provides the timer/wakeup facility. + */ +public final class QuicTimerQueue { + + // A queue that contains scheduled events + private final ConcurrentSkipListSet scheduled = + new ConcurrentSkipListSet<>(QuicTimedEvent.COMPARATOR); + + // A queue that contains events which are due. The queue is + // filled by processAndReturnNextDeadline() + private final ConcurrentLinkedQueue due = + new ConcurrentLinkedQueue<>(); + + // A queue that contains events that need to be rescheduled. + // The event may already be in the scheduled queue - in which + // case it will be removed before being added back. + private final Set rescheduled = + ConcurrentHashMap.newKeySet(); + + // A callback to tell the timer thread to wake up + private final Runnable notifier; + + // A loop to process events which are due, or which need to + // be rescheduled. + private final SequentialScheduler processor = + SequentialScheduler.lockingScheduler(this::processDue); + + private final Logger debug; + private volatile boolean closed; + private volatile Deadline scheduledDeadline = Deadline.MAX; + private volatile Deadline returnedDeadline = Deadline.MAX; + + /** + * Creates a new timer queue with the given notifier. + * A notifier is used to notify the timer thread that + * new events have been added to the queue of scheduled + * event. The notifier should wake up the thread and + * trigger a call to either {@link + * #processEventsAndReturnNextDeadline(Deadline, Executor)} + * or {@link #nextDeadline()}. + * + * @param notifier A notifier to wake up the timer thread when + * new event have been added and the next + * deadline has changed. + */ + public QuicTimerQueue(Runnable notifier, Logger debug) { + this.notifier = notifier; + this.debug = debug; + } + + // For debug purposes only + private String d(Deadline deadline) { + return Utils.debugDeadline(debugNow(), deadline); + } + + // For debug purposes only + private String d(Deadline now, Deadline deadline) { + return Utils.debugDeadline(now, deadline); + } + + // For debug purposes only + private Deadline debugNow() { + return TimeSource.now(); + } + + /** + * Schedule the given event by adding it to the timer queue. + * + * @param event an event to be scheduled + */ + public void offer(QuicTimedEvent event) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + Deadline deadline = event.deadline(); + scheduled.add(event); + scheduled(deadline); + if (debug.on()) debug.log("QuicTimerQueue: event %s offered", event); + if (notify(deadline)) { + if (debug.on()) debug.log("QuicTimerQueue: event %s will be rescheduled", event); + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be scheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + notifier.run(); + } else { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will not be scheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + } + } + + /** + * The next deadline for this timer queue. This is only weakly + * consistent. If the queue is empty, {@link Deadline#MAX} is + * returned. + * + * @return The next deadline, or {@code Deadline.MAX}. + */ + public Deadline nextDeadline() { + var event = scheduled.ceiling(FLOOR); + return event == null ? Deadline.MAX : event.deadline(); + } + + public Deadline pendingScheduledDeadline() { + return scheduledDeadline; + } + + /** + * Process all events that were due before {@code now}, and + * returns the next deadline. The events are processed within + * an executor's thread, so this method may return before all + * events have been processed. The events are processed in + * order, with respect to their deadline. Processing an event + * involves invoking its {@link QuicTimedEvent#handle() handle} + * method. If that method returns a new deadline different from + * {@link Deadline#MAX} the processed event is rescheduled + * immediately. Otherwise, it will not be rescheduled. + * + * @param now The point in time before which events are + * considered to be due. Usually, that's now. + * @param executor An executor to process events which are due. + * + * @return the next unexpired deadline, or {@link Deadline#MAX} + * if the queue is empty. + */ + public Deadline processEventsAndReturnNextDeadline(Deadline now, Executor executor) { + QuicTimedEvent event; + int drained = 0; + int dues; + synchronized (this) { + scheduledDeadline = Deadline.MAX; + } + // moved scheduled / rescheduled tasks to due, until + // nothing else is due. Then process dues. + do { + dues = processRescheduled(now); + dues = dues + processScheduled(now); + drained += dues; + } while (dues > 0); + Deadline newDeadline = (event = scheduled.ceiling(FLOOR)) == null ? Deadline.MAX : event.deadline(); + assert event == null || newDeadline.isBefore(Deadline.MAX) : "Invalid deadline for " + event; + if (debug.on()) { + debug.log("QuicTimerQueue: newDeadline: " + d(now, newDeadline) + + (event == null ? "no event scheduled" : (" for " + event))); + } + Deadline next; + synchronized (this) { + var scheduled = scheduledDeadline; + scheduledDeadline = Deadline.MAX; + // if some task is being rescheduled with a deadline + // that is before any scheduled deadline, use that deadline. + next = returnedDeadline = min(newDeadline, scheduled); + } + if (next.equals(Deadline.MAX)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: no deadline" + + " (scheduled: %s, rescheduled: %s, dues %s)", + Thread.currentThread().getName(), this.scheduled.size(), + this.rescheduled.size(), this.due.size())); + } + } + if (drained > 0) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: %s events to handle (%s in dues)", + Thread.currentThread().getName(), drained, this.due.size())); + } + processor.runOrSchedule(executor); + } + return next; + } + + // return the deadline which is before the other + private Deadline min(Deadline one, Deadline two) { + return one.isBefore(two) ? one : two; + } + + // walk through the rescheduled tasks and moves any + // that are due to `due`. Otherwise, move them to + // `scheduled` + private int processRescheduled(Deadline now) { + int drained = 0; + for (var it = rescheduled.iterator(); it.hasNext(); ) { + QuicTimedEvent event = it.next(); + it.remove(); // remove before processing to avoid race + scheduled.remove(event); + Deadline deadline = event.refreshDeadline(); + if (deadline.equals(Deadline.MAX)) { + continue; + } + if (deadline.isAfter(now)) { + scheduled.add(event); + } else { + due.add(event); + drained++; + } + } + if (drained > 0) { + if (debug.on()) { + debug.log("QuicTimerQueue: %s rescheduled tasks are due", drained); + } + } + return drained; + } + + // walk through the scheduled tasks and moves any + // that are due to `due`. + private int processScheduled(Deadline now) { + QuicTimedEvent event; + int drained = 0; + while ((event = scheduled.ceiling(FLOOR)) != null) { + Deadline deadline = event.deadline(); + if (!isDue(deadline, now)) { + break; + } + event = scheduled.pollFirst(); + if (event == null) { + break; + } + drained++; + due.add(event); + } + if (drained > 0 && debug.on()) { + debug.log("QuicTimerQueue: %s scheduled tasks are due", drained); + } + return drained; + } + + private static boolean isDue(final Deadline deadline, final Deadline now) { + return deadline.compareTo(now) <= 0; + } + + // process all due events in order + private void processDue() { + try { + QuicTimedEvent event; + if (closed) return; + if (debug.on()) debug.log("QuicTimerQueue: processDue"); + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: process %s events", + Thread.currentThread().getName(), due.size())); + } + Deadline minDeadLine = Deadline.MAX; + while ((event = due.poll()) != null) { + if (closed) return; + Deadline nextDeadline = event.handle(); + if (Deadline.MAX.equals(nextDeadline)) continue; + rescheduled.add(event); + if (nextDeadline.isBefore(minDeadLine)) minDeadLine = nextDeadline; + } + + // record the minimal deadline that was rescheduled + scheduled(minDeadLine); + + // wake up the selector thread if necessary + if (notify(minDeadLine)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: notify: minDeadline: %s", + Thread.currentThread().getName(), d(minDeadLine))); + } + notifier.run(); + } else if (!minDeadLine.equals(Deadline.MAX)) { + if (Log.quicTimer()) { + Log.logQuic(String.format("%s: TimerQueue: no need to notify: minDeadline: %s", + Thread.currentThread().getName(), d(minDeadLine))); + } + } + + } catch (Throwable t) { + if (!closed) { + if (Log.errors()) { + Log.logError(Thread.currentThread().getName() + + ": Unexpected exception while processing due events: " + t); + Log.logError(t); + } else if (debug.on()) { + debug.log("Unexpected exception while processing due events", t); + } + throw t; + } else { + if (Log.errors()) { + Log.logError(Thread.currentThread().getName() + + ": Ignoring exception while closing: " + t); + Log.logError(t); + } else if (debug.on()) { + debug.log("Ignoring exception while closing: " + t); + } + } + } + } + + // We do not need to notify the selector thread if the next scheduled + // deadline is before the given deadline, or if it is after + // the last returned deadline. + private boolean notify(Deadline deadline) { + synchronized (this) { + if (deadline.isBefore(nextDeadline()) + || deadline.isBefore(returnedDeadline)) { + return true; + } + } + return false; + } + + // Record a prospective attempt to reschedule an event at + // the given deadline + private Deadline scheduled(Deadline deadline) { + synchronized (this) { + var scheduled = scheduledDeadline; + if (deadline.isBefore(scheduled)) { + scheduledDeadline = deadline; + return deadline; + } + return scheduled; + } + } + + /** + * Reschedule the given {@code QuicTimedEvent}. + * + * @apiNote + * This method is used if the prospective future deadline at which the event + * should be scheduled is not known by the caller. + * This may cause an idle wakeup in the selector thread owning this + * {@code QuicTimerQueue}. Use {@link #reschedule(QuicTimedEvent, Deadline)} + * to minimize idle wakeup. + * + * @param event an event to reschedule + */ + public void reschedule(QuicTimedEvent event) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + rescheduled.add(event); + if (debug.on()) debug.log("QuicTimerQueue: event %s will be rescheduled", event); + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be rescheduled" + + " (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, returnedDeadline), + d(now, nextDeadline()))); + } + notifier.run(); + } + + /** + * Reschedule the given {@code QuicTimedEvent}. + * + * @apiNote + * This method should be used in preference of {@link #reschedule(QuicTimedEvent)} + * if the prospective future deadline at which the event should be scheduled is + * already known by the caller. Using this method will minimize idle wakeup + * of the selector thread, in comparison of {@link #reschedule(QuicTimedEvent)}. + * + * @param event an event to reschedule + * @param deadline the prospective future deadline at which the event should + * be rescheduled + */ + public void reschedule(QuicTimedEvent event, Deadline deadline) { + if (event instanceof Marker marker) + throw new IllegalArgumentException(marker.name()); + assert QuicTimedEvent.COMPARATOR.compare(event, FLOOR) > 0; + assert QuicTimedEvent.COMPARATOR.compare(event, CEILING) < 0; + rescheduled.add(event); + scheduled(deadline); + // no need to wake up the selector thread if the next deadline + // is already before the new deadline + + if (notify(deadline)) { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will be rescheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } else if (debug.on()) { + debug.log("QuicTimerQueue: event %s will be rescheduled", event); + } + notifier.run(); + } else { + if (Log.quicTimer()) { + var now = debugNow(); + Log.logQuic(String.format("%s: QuicTimerQueue: event %s will not be rescheduled" + + " at %s (returned deadline: %s, nextDeadline: %s)", + Thread.currentThread().getName(), event, d(now, deadline), + d(now, returnedDeadline), d(now, nextDeadline()))); + } + } + } + + private static final AtomicLong EVENTIDS = new AtomicLong(); + + /** + * {@return a unique id for a new {@link QuicTimedEvent}} + * Each new instance of {@link QuicTimedEvent} is created with a long + * ID returned by this method to ensure a total ordering of + * {@code QuicTimedEvent} instances, even when their deadlines + * are equal. + */ + public static long newEventId() { + return EVENTIDS.getAndIncrement(); + } + + // aliases + private static final Marker FLOOR = Marker.FLOOR; + private static final Marker CEILING = Marker.CEILING; + + /** + * Called to clean up the timer queue when it is no longer needed. + * Makes sure that all pending tasks are cleared from the various lists. + */ + public void stop() { + closed = true; + do { + processor.stop(); + due.clear(); + rescheduled.clear(); + scheduled.clear(); + } while (!due.isEmpty() || !rescheduled.isEmpty() || !scheduled.isEmpty()); + } + + // This class is used to work around the lack of a peek() method + // in ConcurrentSkipListSet. ConcurrentSkipListSet has a method + // called first(), but it throws NoSuchElementException if the + // set isEmpty() - whereas peek() would return {@code null}. + // The next best thing is to use ConcurrentSkipListSet::ceiling, + // but for that we need to define a minimum event which is lower + // than any other event: we do this by defining Marker.FLOOR + // which has deadline=Deadline.MIN and eventId=Long.MIN_VALUE; + // Note: it would be easier to use a record, but an enum ensures that we + // can only have the two instances FLOOR and CEILING. + enum Marker implements QuicTimedEvent { + /** + * A {@code Marker} event to pass to {@link ConcurrentSkipListSet#ceiling(Object) + * ConcurrentSkipListSet::ceiling} in order to get the first event in the list, + * or {@code null}. + * + * @apiNote + * The intended usage is:
        {@code
        +         *       var head = scheduled.ceiling(FLOOR);
        +         * }
        + * + */ + FLOOR(Deadline.MIN, Long.MIN_VALUE), + /** + * A {@code Marker} event to pass to {@link ConcurrentSkipListSet#floor(Object) + * ConcurrentSkipListSet::floor} in order to get the last event in the list, + * or {@code null}. + * + * @apiNote + * The intended usage is:
        {@code
        +         *       var head = scheduled.floor(CEILING);
        +         * }
        + * + */ + CEILING(Deadline.MAX, Long.MAX_VALUE); + private final Deadline deadline; + private final long eventId; + private Marker(Deadline deadline, long eventId) { + this.deadline = deadline; + this.eventId = eventId; + } + + @Override public Deadline deadline() { return deadline; } + @Override public Deadline refreshDeadline() {return Deadline.MAX;} + @Override public Deadline handle() { return Deadline.MAX; } + @Override public long eventId() { return eventId; } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java new file mode 100644 index 00000000000..36832575add --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicTransportParameters.java @@ -0,0 +1,1319 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.EnumMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.quic.QuicVersion; + +/** + * This class models a collection of Quic transport parameters. This class is mutable + * and not thread safe. + * + * A parameter is considered absent if {@link #getParameter(TransportParameterId)} + * yields {@code null}. The parameter is present otherwise. + * Parameters can be removed by calling {@link + * #setParameter(TransportParameterId, byte[]) setParameter(id, null)}. + * The methods {@link #getBooleanParameter(TransportParameterId)} and + * {@link #getIntParameter(TransportParameterId)} allow easy access to + * parameters whose type is boolean or int, respectively. + * When such a parameter is absent, its default value is returned by + * those methods. + + * From + * RFC 9000, section 18.2: + * + *
        + *
        {@code
        + * Many transport parameters listed here have integer values.
        + * Those transport parameters that are identified as integers use a
        + * variable-length integer encoding; see Section 16. Transport parameters
        + * have a default value of 0 if the transport parameter is absent, unless
        + * otherwise stated.
        + * }
        + * + *

        [...] + * + *

        {@code
        + * If present, transport parameters that set initial per-stream flow control limits
        + * (initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, and
        + * initial_max_stream_data_uni) are equivalent to sending a MAX_STREAM_DATA frame
        + * (Section 19.10) on every stream of the corresponding type immediately after opening.
        + * If the transport parameter is absent, streams of that type start with a flow control
        + * limit of 0.
        + *
        + * A client MUST NOT include any server-only transport parameter:
        + *        original_destination_connection_id,
        + *        preferred_address,
        + *        retry_source_connection_id, or
        + *        stateless_reset_token.
        + *
        + * A server MUST treat receipt of any of these transport parameters as a connection error
        + * of type TRANSPORT_PARAMETER_ERROR.
        + * }
        + *
        + * + * @see ParameterId + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class QuicTransportParameters { + + /** + * An interface to model a transport parameter ID. + * A transport parameter ID has a {@linkplain #name() name} (which is + * not transmitted) and an {@linkplain #idx() identifier}. + * Standard parameters are modeled by enum values in + * {@link ParameterId}. + */ + public sealed interface TransportParameterId { + /** + * {@return the transport parameter name} + * This a human-readable string. + */ + String name(); + + /** + * {@return the transport parameter identifier} + */ + int idx(); + + /** + * {@return the parameter id corresponding to the given identifier, if + * defined, an empty optional otherwise} + * @param idx a parameter identifier + */ + static Optional valueOf(long idx) { + return ParameterId.valueOf(idx); + } + } + + /** + * Standard Quic transport parameter names and ids. + * These are the transport parameters defined in IANA + * "QUIC Transport Parameters" registry. + * @see + * RFC 9000, Section 22.3 + */ + public enum ParameterId implements TransportParameterId { + /** + * original_destination_connection_id (0x00). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     This parameter is the value of the Destination Connection ID field
        +         *     from the first Initial packet sent by the client; see Section 7.3.
        +         *     This transport parameter is only sent by a server.
        +         * }
        + * @see RFC 9000, Section 7.3 + */ + original_destination_connection_id(0x00), + + /** + * max_idle_timeout (0x01). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The maximum idle timeout is a value in milliseconds that is encoded
        +         *     as an integer; see (Section 10.1).
        +         *     Idle timeout is disabled when both endpoints omit this transport
        +         *     parameter or specify a value of 0.
        +         * }
        + * @see RFC 9000, Section 10.1 + */ + max_idle_timeout(0x01), + + /** + * stateless_reset_token (0x02). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     A stateless reset token is used in verifying a stateless reset;
        +         *     see Section 10.3.
        +         *     This parameter is a sequence of 16 bytes. This transport parameter MUST NOT
        +         *     be sent by a client but MAY be sent by a server. A server that does not send
        +         *     this transport parameter cannot use stateless reset (Section 10.3) for
        +         *     the connection ID negotiated during the handshake.
        +         * }
        + * @see RFC 9000, Section 10.3 + */ + stateless_reset_token(0x02), + + /** + * max_udp_payload_size (0x03). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The maximum UDP payload size parameter is an integer value that limits the
        +         *     size of UDP payloads that the endpoint is willing to receive. UDP datagrams
        +         *     with payloads larger than this limit are not likely to be processed by
        +         *     the receiver.
        +         *
        +         *     The default for this parameter is the maximum permitted UDP payload of 65527.
        +         *     Values below 1200 are invalid.
        +         *
        +         *     This limit does act as an additional constraint on datagram size
        +         *     in the same way as the path MTU, but it is a property of the endpoint
        +         *     and not the path; see Section 14.
        +         *     It is expected that this is the space an endpoint dedicates to
        +         *     holding incoming packets.
        +         * }
        + * @see RFC 9000, Section 14 + */ + max_udp_payload_size(0x03), + + /** + * initial_max_data (0x04). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The initial maximum data parameter is an integer value that contains
        +         *     the initial value for the maximum amount of data that can be sent on
        +         *     the connection. This is equivalent to sending a MAX_DATA (Section 19.9)
        +         *     for the connection immediately after completing the handshake.
        +         * }
        + * @see RFC 9000, Section 19.9 + */ + initial_max_data(0x04), + + /** + * initial_max_stream_data_bidi_local (0x05). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     This parameter is an integer value specifying the initial flow control
        +         *     limit for locally initiated bidirectional streams. This limit applies to
        +         *     newly created bidirectional streams opened by the endpoint that
        +         *     sends the transport parameter.
        +         *     In client transport parameters, this applies to streams with an identifier
        +         *     with the least significant two bits set to 0x00;
        +         *     in server transport parameters, this applies to streams with the least
        +         *     significant two bits set to 0x01.
        +         * }
        + */ + initial_max_stream_data_bidi_local(0x05), + + /** + * initial_max_stream_data_bidi_remote (0x06). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     This parameter is an integer value specifying the initial flow control
        +         *     limit for peer-initiated bidirectional streams. This limit applies to
        +         *     newly created bidirectional streams opened by the endpoint that receives
        +         *     the transport parameter. In client transport parameters, this applies to
        +         *     streams with an identifier with the least significant two bits set to 0x01;
        +         *     in server transport parameters, this applies to streams with the least
        +         *     significant two bits set to 0x00.
        +         * }
        + */ + initial_max_stream_data_bidi_remote(0x06), + + /** + * initial_max_stream_data_uni (0x07). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     This parameter is an integer value specifying the initial flow control
        +         *     limit for unidirectional streams. This limit applies to newly created
        +         *     unidirectional streams opened by the endpoint that receives the transport
        +         *     parameter. In client transport parameters, this applies to streams with
        +         *     an identifier with the least significant two bits set to 0x03; in server
        +         *     transport parameters, this applies to streams with the least significant
        +         *     two bits set to 0x02.
        +         * }
        + */ + initial_max_stream_data_uni(0x07), + + /** + * initial_max_streams_bidi (0x08). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The initial maximum bidirectional streams parameter is an integer value
        +         *     that contains the initial maximum number of bidirectional streams the
        +         *     endpoint that receives this transport parameter is permitted to initiate.
        +         *     If this parameter is absent or zero, the peer cannot open bidirectional
        +         *     streams until a MAX_STREAMS frame is sent. Setting this parameter is equivalent
        +         *     to sending a MAX_STREAMS (Section 19.11) of the corresponding type with the
        +         *     same value.
        +         * }
        + * @see RFC 9000, Section 19.11 + */ + initial_max_streams_bidi(0x08), + + /** + * initial_max_streams_uni (0x09). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The initial maximum unidirectional streams parameter is an integer value that
        +         *     contains the initial maximum number of unidirectional streams the endpoint
        +         *     that receives this transport parameter is permitted to initiate. If this parameter
        +         *     is absent or zero, the peer cannot open unidirectional streams until a MAX_STREAMS
        +         *     frame is sent. Setting this parameter is equivalent to sending a MAX_STREAMS
        +         *     (Section 19.11) of the corresponding type with the same value.
        +         * }
        + * @see RFC 9000, Section 19.11 + */ + initial_max_streams_uni(0x09), + + /** + * ack_delay_exponent (0x0a). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The acknowledgment delay exponent is an integer value indicating an exponent
        +         *     used to decode the ACK Delay field in the ACK frame (Section 19.3). If this
        +         *     value is absent, a default value of 3 is assumed (indicating a multiplier of 8).
        +         *     Values above 20 are invalid.
        +         * }
        + * @see RFC 9000, Section 19.3 + */ + ack_delay_exponent(0x0a), + + /** + * max_ack_delay (0x0b). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The maximum acknowledgment delay is an integer value indicating the maximum
        +         *     amount of time in milliseconds by which the endpoint will delay sending acknowledgments.
        +         *     This value SHOULD include the receiver's expected delays in alarms firing. For example,
        +         *     if a receiver sets a timer for 5ms and alarms commonly fire up to 1ms late, then it
        +         *     should send a max_ack_delay of 6ms. If this value is absent, a default of 25
        +         *     milliseconds is assumed. Values of 2^14 or greater are invalid.
        +         * }
        + */ + max_ack_delay(0x0b), + + /** + * disable_active_migration (0x0c). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The disable active migration transport parameter is included if the endpoint does not
        +         *     support active connection migration (Section 9) on the address being used during the
        +         *     handshake. An endpoint that receives this transport parameter MUST NOT use a new local
        +         *     address when sending to the address that the peer used during the handshake. This transport
        +         *     parameter does not prohibit connection migration after a client has acted on a
        +         *     preferred_address transport parameter. This parameter is a zero-length value.
        +         * }
        + * @see RFC 9000, Section 9 + */ + disable_active_migration(0x0c), + + /** + * preferred_address (0x0d). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     The server's preferred address is used to effect a change in server address at the
        +         *     end of the handshake, as described in Section 9.6. This transport parameter is only
        +         *     sent by a server.
        +         *     Servers MAY choose to only send a preferred address of one address family
        +         *     by sending an all-zero address and port (0.0.0.0:0 or [::]:0) for the
        +         *     other family. IP addresses are encoded in network byte order.
        +         *
        +         *     The preferred_address transport parameter contains an address and port for both
        +         *     IPv4 and IPv6. The four-byte IPv4 Address field is followed by the associated
        +         *     two-byte IPv4 Port field. This is followed by a 16-byte IPv6 Address field and
        +         *     two-byte IPv6 Port field. After address and port pairs, a Connection ID Length
        +         *     field describes the length of the following Connection ID field.
        +         *     Finally, a 16-byte Stateless Reset Token field includes the stateless reset
        +         *     token associated with the connection ID. The format of this transport parameter
        +         *     is shown in Figure 22 below.
        +         *
        +         *     The Connection ID field and the Stateless Reset Token field contain an alternative
        +         *     connection ID that has a sequence number of 1; see Section 5.1.1. Having these values
        +         *     sent alongside the preferred address ensures that there will be at least one
        +         *     unused active connection ID when the client initiates migration to the preferred
        +         *     address.
        +         *
        +         *     The Connection ID and Stateless Reset Token fields of a preferred address are
        +         *     identical in syntax and semantics to the corresponding fields of a NEW_CONNECTION_ID
        +         *     frame (Section 19.15). A server that chooses a zero-length connection ID MUST NOT
        +         *     provide a preferred address. Similarly, a server MUST NOT include a zero-length
        +         *     connection ID in this transport parameter. A client MUST treat a violation of
        +         *     these requirements as a connection error of type TRANSPORT_PARAMETER_ERROR.
        +         *
        +         * Preferred Address {
        +         *   IPv4 Address (32),
        +         *   IPv4 Port (16),
        +         *   IPv6 Address (128),
        +         *   IPv6 Port (16),
        +         *   Connection ID Length (8),
        +         *   Connection ID (..),
        +         *   Stateless Reset Token (128),
        +         * }
        +         *
        +         * Figure 22: Preferred Address Format
        +         * }
        + * @see RFC 9000, Section 5.1.1 + * @see RFC 9000, Section 9.6 + * @see RFC 9000, Section 19.15 + */ + preferred_address(0x0d), + + /** + * active_connection_id_limit (0x0e). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     This is an integer value specifying the maximum number of connection IDs from
        +         *     the peer that an endpoint is willing to store. This value includes the connection
        +         *     ID received during the handshake, that received in the preferred_address transport
        +         *     parameter, and those received in NEW_CONNECTION_ID frames. The value of the
        +         *     active_connection_id_limit parameter MUST be at least 2. An endpoint that receives
        +         *     a value less than 2 MUST close the connection with an error of type
        +         *     TRANSPORT_PARAMETER_ERROR. If this transport parameter is absent, a default of 2 is
        +         *     assumed. If an endpoint issues a zero-length connection ID, it will never send a
        +         *     NEW_CONNECTION_ID frame and therefore ignores the active_connection_id_limit value
        +         *     received from its peer.
        +         * }
        + */ + active_connection_id_limit(0x0e), + + /** + * initial_source_connection_id (0x0f). + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +         *     This is the value that the endpoint included in the Source Connection ID field of
        +         *     the first Initial packet it sends for the connection; see Section 7.3.
        +         * }
        + * @see RFC 9000, Section 7.3 + */ + initial_source_connection_id(0x0f), + + /** + * retry_source_connection_id (0x10). + *

        + * From + * RFC 9000, Section 18.2 + *

        {@code
        +         *     This is the value that the server included in the Source Connection ID field of a
        +         *     Retry packet; see Section 7.3. This transport parameter is only sent by a server.
        +         * }
        + * @see RFC 9000, Section 7.3 + */ + retry_source_connection_id(0x10), + + /** + * version_information (0x11). + *

        + * From + * RFC 9368, Section 3 + *

        {@code
        +         *     During the handshake, endpoints will exchange Version Information,
        +         *     which consists of a Chosen Version and a list of Available Versions.
        +         *     Any version of QUIC that supports this mechanism MUST provide a mechanism
        +         *     to exchange Version Information in both directions during the handshake,
        +         *     such that this data is authenticated.
        +         * }
        + */ + version_information(0x11); + + /* + * Reserved Transport Parameters (31 * N + 27 for int values of N) + *

        + * From + * RFC 9000, Section 18.1 + *

        {@code
        +         *     Transport parameters with an identifier of the form 31 * N + 27
        +         *     for integer values of N are reserved to exercise the requirement
        +         *     that unknown transport parameters be ignored. These transport
        +         *     parameters have no semantics and can carry arbitrary values.
        +         * }
        + */ + // No values are defined here, but these will be + // ignored if received (see + // sun.security.ssl.QuicTransportParametersExtension). + + /** + * The number of known transport parameters. + * This is also the number of enum values defined by the + * {@link ParameterId} enumeration. + */ + private static final int PARAMETERS_COUNT = ParameterId.values().length; + + ParameterId(int idx) { + // idx() and valueOf() assume that idx = ordinal; + // if that's no longer the case, update the implementation + // and remove this assert. + assert idx == ordinal(); + } + + @Override + public int idx() { + return ordinal(); + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + + private static Optional valueOf(long idx) { + if (idx < 0 || idx >= PARAMETERS_COUNT) return Optional.empty(); + return Optional.of(values()[(int)idx]); + } + } + + public record VersionInformation(int chosenVersion, int[] availableVersions) { } + + /** + * A map to store transport parameter values. + * Contains a byte array corresponding to the encoded value + * of the parameter. + */ + private final Map values; + + /** + * Constructs a new empty array of Quic transport parameters. + */ + public QuicTransportParameters() { + values = new EnumMap<>(ParameterId.class); + } + + /** + * Constructs a new collection of Quic transport parameters initialized + * from the specified collection. + * @param params the parameter collection used to initialize this object * + */ + public QuicTransportParameters(QuicTransportParameters params) { + values = new EnumMap<>(params.values); + } + + /** + * {@return true if the given parameter is present, false otherwise} + * @apiNote + * This is equivalent to {@link #getParameter(TransportParameterId) + * getParameter(id) != null}, but avoids cloning the parameter value. + * @param id the parameter id + */ + public boolean isPresent(TransportParameterId id) { + byte[] value = values.get((ParameterId) id); + return value != null; + } + + /** + * {@return the value of the given parameter, as a byte array, or + * {@code null} if the parameter is absent. + * @param id the parameter id + */ + public byte[] getParameter(TransportParameterId id) { + byte[] value = values.get((ParameterId) id); + return value == null ? null : value.clone(); + } + + /** + * {@return true if the value of the given parameter matches the given connection ID} + * @param id the transport parameter id + * @param connectionId the connection id to match against + */ + public boolean matches(TransportParameterId id, QuicConnectionId connectionId) { + byte[] value = values.get((ParameterId) id); + return connectionId.matches(ByteBuffer.wrap(value).asReadOnlyBuffer()); + } + + /** + * Sets the value of the given parameter. + * If the given value is {@code null}, the parameter is removed. + * @param id the parameter id + * @param value the new parameter value, or {@code null}. + * @throws IllegalArgumentException if the given value is invalid for + * the given parameter id + */ + public void setParameter(TransportParameterId id, byte[] value) { + ParameterId pid = checkParameterValue(id, value); + if (value != null) { + values.put(pid, value.clone()); + } else { + values.remove(pid); + } + } + + /** + * {@return the value of the given parameter, as an unsigned int + * in the range {@code [0, 2^62 - 1]}} + * If the parameter is not present its default value (as specified in the RFC) is returned. + * @param id the parameter id + * @throws IllegalArgumentException if the value of the given parameter + * cannot be decoded as a variable length unsigned int + */ + public long getIntParameter(TransportParameterId id) { + return getIntParameter((ParameterId)id); + } + + private long getIntParameter(final ParameterId pid) { + return switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] value = values.get(pid); + final long res; + if (value == null) { + res = switch (pid) { + case active_connection_id_limit -> 2; + case max_udp_payload_size -> 65527; + case ack_delay_exponent -> 3; + case max_ack_delay -> 25; + default -> 0; + }; + } else { + res = decodeVLIntFully(pid, ByteBuffer.wrap(value)); + } + yield res; + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + + }; + } + + /** + * {@return the value of the given parameter, as an unsigned int + * in the range {@code [0, 2^62 - 1]}} + * If the parameter is not present then {@code defaultValue} is returned. + * @param id the parameter id + * @throws IllegalArgumentException if the value of the given parameter + * cannot be decoded as a variable length unsigned int or if the {@code defaultValue} + * exceeds the maximum allowed value for variable length integer + */ + public long getIntParameter(TransportParameterId id, long defaultValue) { + if (defaultValue > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new IllegalArgumentException("default value " + defaultValue + + " exceeds maximum allowed variable length" + + " integer value " + VariableLengthEncoder.MAX_ENCODED_INTEGER); + } + ParameterId pid = (ParameterId)id; + return switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] value = values.get(pid); + final long res; + if (value == null) { + res = defaultValue; + } else { + res = decodeVLIntFully(pid, ByteBuffer.wrap(value)); + } + yield res; + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + }; + } + + /** + * Sets the value of the given parameter, as an unsigned int. + * If a negative value is provided, the parameter is removed. + * + * @param id the parameter id + * @param value the new value of the parameter, or a negative value + * + * @throws IllegalArgumentException if the value of the given parameter is + * not an int, or if the provided value is out of range + */ + public void setIntParameter(TransportParameterId id, long value) { + ParameterId pid = (ParameterId)id; + switch (pid) { + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> { + byte[] v = null; + if (value >= 0) { + int length = VariableLengthEncoder.getEncodedSize(value); + if (length <= 0) throw new IllegalArgumentException("failed to encode " + value); + int size = VariableLengthEncoder.encode(ByteBuffer.wrap(v = new byte[length]), value); + assert size == length; + checkParameterValue(pid, v); + } + setOrRemove(pid, v); + } + default -> throw new IllegalArgumentException(String.valueOf(pid)); + } + } + + /** + * {@return the value of the given parameter, as a boolean} + * If the parameter is not present its default value (false) + * is returned. + * + * @param id the parameter id + * + * @throws IllegalArgumentException if the value of the given parameter + * is not a boolean + */ + public boolean getBooleanParameter(TransportParameterId id) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.disable_active_migration) { + throw new IllegalArgumentException(String.valueOf(id)); + } + return values.get(pid) != null; + } + + /** + * Sets the value of the given parameter, as a boolean. + * @apiNote + * It is not possible to distinguish between a boolean parameter + * whose value is absent and a parameter whose value is false. + * Both are represented by a {@code null} value in the parameter + * array. + * @param id the parameter id + * @param value the new value of the parameter + * @throws IllegalArgumentException if the value of the given parameter is + * not a boolean + */ + public void setBooleanParameter(TransportParameterId id, boolean value) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.disable_active_migration) { + throw new IllegalArgumentException(String.valueOf(id)); + } + setOrRemove(pid, value ? NOBYTES : null); + } + + private void setOrRemove(ParameterId pid, byte[] value) { + if (value != null) { + values.put(pid, value); + } else { + values.remove(pid); + } + } + + /** + * {@return the value of the given parameter, as {@link VersionInformation}} + * If the parameter is not present {@code null} is returned + * + * @param id the parameter id + * + * @throws IllegalArgumentException if the value of the given parameter + * is not a version information + * @throws QuicTransportException if the parameter value has incorrect length, + * or if any version is equal to zero + */ + public VersionInformation getVersionInformationParameter(TransportParameterId id) + throws QuicTransportException { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.version_information) { + throw new IllegalArgumentException(String.valueOf(id)); + } + byte[] val = values.get(pid); + if (val == null) { + return null; + } + if (val.length < 4 || (val.length & 3) != 0) { + throw new QuicTransportException( + "Invalid version information length " + val.length, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + ByteBuffer bbval = ByteBuffer.wrap(val); + assert bbval.order() == ByteOrder.BIG_ENDIAN; + int chosen = bbval.getInt(); + if (chosen == 0) { + throw new QuicTransportException( + "[version_information] Chosen Version = 0", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + int[] available = new int[bbval.remaining() / 4]; + for (int i = 0; i < available.length; i++) { + int version = bbval.getInt(); + if (version == 0) { + throw new QuicTransportException( + "[version_information] Available Version = 0", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + available[i] = version; + } + return new VersionInformation(chosen, available); + } + + /** + * Sets the value of the given parameter, as {@link VersionInformation}. + * @param id the parameter id + * @param value the new value of the parameter + * @throws IllegalArgumentException if the value of the given parameter is + * not a version information + */ + public void setVersionInformationParameter(TransportParameterId id, VersionInformation value) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.version_information) { + throw new IllegalArgumentException(String.valueOf(id)); + } + byte[] val = new byte[value.availableVersions.length * 4 + 4]; + ByteBuffer bbval = ByteBuffer.wrap(val); + assert bbval.order() == ByteOrder.BIG_ENDIAN; + bbval.putInt(value.chosenVersion); + for (int available : value.availableVersions) { + bbval.putInt(available); + } + assert !bbval.hasRemaining(); + values.put(pid, val); + } + + /** + * {@return a {@link VersionInformation} object corresponding to the specified versions} + * @param chosenVersion chosen version + * @param availableVersions available versions + */ + public static VersionInformation buildVersionInformation( + QuicVersion chosenVersion, List availableVersions) { + int[] available = new int[availableVersions.size()]; + for (int i = 0; i < available.length; i++) { + available[i] = availableVersions.get(i).versionNumber(); + } + return new VersionInformation(chosenVersion.versionNumber(), available); + } + + /** + * Sets the value of a parameter whose format corresponds to the + * {@link ParameterId#preferred_address} parameter. + * @param id the parameter id + * @param ipv4 the preferred IPv4 address (or the IPv4 wildcard address) + * @param port4 the preferred IPv4 port (or 0) + * @param ipv6 the preferred IPv6 address (or the IPv6 wildcard address) + * @param port6 the preferred IPv6 port (or 0) + * @param connectionId the connection id bytes + * @param statelessToken the stateless token + * @throws IllegalArgumentException if any of the given parameters has an + * illegal value, or if the given parameter value is not of the + * {@link ParameterId#preferred_address} format + * @see ParameterId#preferred_address + */ + public void setPreferredAddressParameter(TransportParameterId id, + Inet4Address ipv4, int port4, + Inet6Address ipv6, int port6, + ByteBuffer connectionId, + ByteBuffer statelessToken) { + ParameterId pid = (ParameterId)id; + if (pid != ParameterId.preferred_address) { + throw new IllegalArgumentException(String.valueOf(id)); + } + int cidlen = connectionId.remaining(); + if (cidlen == 0 || cidlen > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + throw new IllegalArgumentException( + "connection id len out of range [1..20]: " + cidlen); + } + int tklen = statelessToken.remaining(); + if (tklen != TOKEN_SIZE) { + throw new IllegalArgumentException("bad stateless token length: expected 16, found " + tklen); + } + if (port4 < 0 || port4 > MAX_PORT) + throw new IllegalArgumentException("IPv4 port out of range: " + port4); + if (port6 < 0 || port6 > MAX_PORT) + throw new IllegalArgumentException("IPv6 port out of range: " + port6); + int size = MIN_PREF_ADDR_SIZE + cidlen; + byte[] value = new byte[size]; + ByteBuffer buffer = ByteBuffer.wrap(value); + if (!ipv4.isAnyLocalAddress()) { + buffer.put(IPV4_ADDR_OFFSET, ipv4.getAddress()); + } + buffer.putShort(IPV4_PORT_OFFSET, (short) port4); + if (!ipv6.isAnyLocalAddress()) { + buffer.put(IPV6_ADDR_OFFSET, ipv6.getAddress()); + } + buffer.putShort(IPV6_PORT_OFFSET, (short)port6); + buffer.put(CID_LEN_OFFSET, (byte) cidlen); + buffer.put(CID_OFFSET, connectionId, connectionId.position(), cidlen); + assert size - CID_OFFSET - cidlen == TOKEN_SIZE : (size - CID_OFFSET - cidlen); + assert tklen == TOKEN_SIZE; + buffer.put(CID_OFFSET + cidlen, statelessToken, statelessToken.position(), tklen); + values.put(pid, value); + } + + /** + * {@return the size in bytes required to encode the parameter + * array} + */ + public int size() { + int size = 0; + for (var kv : values.entrySet()) { + var i = kv.getKey().idx(); + var value = kv.getValue(); + if (value == null) continue; + assert value.length > 0 || i == ParameterId.disable_active_migration.idx(); + size += VariableLengthEncoder.getEncodedSize(i); + size += VariableLengthEncoder.getEncodedSize(value.length); + size += value.length; + } + return size; + } + + /** + * Encodes the transport parameters into the given byte buffer. + *

        + * From + * RFC 9000, Section 18.2: + *

        {@code
        +     * The extension_data field of the quic_transport_parameters
        +     * extension defined in [QUIC-TLS] contains the QUIC transport
        +     * parameters. They are encoded as a sequence of transport
        +     * parameters, as shown in Figure 20:
        +     *
        +     * Transport Parameters {
        +     *   Transport Parameter (..) ...,
        +     * }
        +     *
        +     * Figure 20: Sequence of Transport Parameters
        +     *
        +     * Each transport parameter is encoded as an (identifier, length,
        +     * value) tuple, as shown in Figure 21:
        +     *
        +     * Transport Parameter {
        +     *   Transport Parameter ID (i),
        +     *   Transport Parameter Length (i),
        +     *   Transport Parameter Value (..),
        +     * }
        +     * }
        + * + * @param buffer a byte buffer in which to encode the transport parameters + * @return the number of bytes written + * @throws BufferOverflowException if there is not enough space in the + * provided buffer + * @see jdk.internal.net.quic.QuicTLSEngine#setLocalQuicTransportParameters(ByteBuffer) + * @see + * RFC 9000, Section 18 + * @see + * RFC 9001 [QUIC-TLS] + */ + public int encode(ByteBuffer buffer) { + int start = buffer.position(); + for (var kv : values.entrySet()) { + var i = kv.getKey().idx(); + var value = kv.getValue(); + if (value == null) continue; + + VariableLengthEncoder.encode(buffer, i); + VariableLengthEncoder.encode(buffer, value.length); + buffer.put(value); + } + var written = buffer.position() - start; + if (QuicTransportParameters.class.desiredAssertionStatus()) { + int size = size(); + assert written == size + : "unexpected number of bytes encoded: %d, expected %d" + .formatted(written, size); + } + return written; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Quic Transport Params["); + for (var kv : values.entrySet()) { + var param = kv.getKey(); + var value = kv.getValue(); + if (value != null) { + // param is set + // we just return the string representation of the param ids and don't include + // the encoded values + sb.append(param); + sb.append(", "); + } + } + return sb.append("]").toString(); + } + + // values for (variable length) integer params are decoded, for other params + // that are set, the value is printed as a hex string. + public String toStringWithValues() { + final StringBuilder sb = new StringBuilder("Quic Transport Params["); + for (var kv : values.entrySet()) { + var param = kv.getKey(); + var value = kv.getValue(); + if (value != null) { + // param is set, so include it in the string representation + sb.append(param); + final String valAsString = valueToString(param); + sb.append("=").append(valAsString); + sb.append(", "); + } + } + return sb.append("]").toString(); + } + + private String valueToString(final ParameterId parameterId) { + assert this.values.get(parameterId) != null : "param " + parameterId + " not set"; + try { + return switch (parameterId) { + // int params + case max_idle_timeout, max_udp_payload_size, initial_max_data, + initial_max_stream_data_bidi_local, + initial_max_stream_data_bidi_remote, + initial_max_stream_data_uni, initial_max_streams_bidi, + initial_max_streams_uni, ack_delay_exponent, max_ack_delay, + active_connection_id_limit -> + String.valueOf(getIntParameter(parameterId)); + default -> + '"' + HexFormat.of().formatHex(values.get(parameterId)) + '"'; + }; + } catch (RuntimeException e) { + // if the value was a malformed integer, return the hex representation + return '"' + HexFormat.of().formatHex(values.get(parameterId)) + '"'; + } + } + + /** + * Decodes the quic transport parameters from the given buffer. + * Parameters which are not supported are silently discarded. + * + * @param buffer a byte buffer containing the transport parameters + * + * @return the decoded transport parameters + * @throws QuicTransportException if the parameters couldn't be decoded + * + * @see jdk.internal.net.quic.QuicTLSEngine#setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer) (ByteBuffer) + * @see jdk.internal.net.quic.QuicTransportParametersConsumer#accept(ByteBuffer) + * @see #encode(ByteBuffer) + * @see + * RFC 9000, Section 18 + */ + public static QuicTransportParameters decode(ByteBuffer buffer) + throws QuicTransportException { + QuicTransportParameters parameters = new QuicTransportParameters(); + while (buffer.hasRemaining()) { + final long id = VariableLengthEncoder.decode(buffer); + final ParameterId pid = TransportParameterId.valueOf(id) + .orElse(null); + final String name = pid == null ? String.valueOf(id) : pid.toString(); + long length = VariableLengthEncoder.decode(buffer); + if (length < 0) { + throw new QuicTransportException( + "Can't decode length for transport parameter " + name, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (length > buffer.remaining()) { + throw new QuicTransportException("Transport parameter truncated", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + byte[] value = new byte[(int) length]; + buffer.get(value); + if (pid == null) { + // RFC-9000, section 7.4.2: An endpoint MUST ignore transport parameters + // that it does not support. + if (Log.quicControl()) { + Log.logQuic("ignoring unsupported transport parameter: " + name); + } + continue; + } + try { + checkParameterValue(pid, value); + } catch (RuntimeException e) { + throw new QuicTransportException(e.getMessage(), + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + var oldValue = parameters.values.putIfAbsent(pid, value); + if (oldValue != null) { + throw new QuicTransportException( + "Duplicate transport parameter " + name, + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + } + return parameters; + } + + /** + * Reads the preferred address encoded in the value + * of a parameter whose format corresponds to the {@link + * ParameterId#preferred_address} parameter. + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * Otherwise, the returned list contains + * at most one IPv4 address and/or one IPv6 address. + * + * @apiNote + * To obtain the list of addresses encoded in the + * {@link ParameterId#preferred_address} parameter, use + * {@link #getPreferredAddress(TransportParameterId, byte[]) + * getPreferredAddress(ParameterId.preferred_address,} + * {@link #getParameter(TransportParameterId) + * parameters.getParameter(ParameterId.preferred_address)}. + * + * @param id the parameter id + * @param value the value of the parameter + * @return a list of {@link InetSocketAddress}, or {@code null} if the + * given value is {@code null}. + * @see ParameterId#preferred_address + */ + public static List getPreferredAddress( + TransportParameterId id, byte[] value) { + if (value == null) return null; + if (value.length < MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": not enough bytes in value; found " + value.length); + } + ByteBuffer buffer = ByteBuffer.wrap(value); + int ipv4port = buffer.getShort(IPV4_PORT_OFFSET) & 0xFFFF; + int ipv6port = buffer.getShort(IPV6_PORT_OFFSET) & 0xFFFF; + + byte[] ipv4 = new byte[IPV4_SIZE]; + buffer.get(IPV4_ADDR_OFFSET, ipv4); + byte[] ipv6 = new byte[IPV6_SIZE]; + buffer.get(IPV6_ADDR_OFFSET, ipv6); + InetSocketAddress ipv4addr = new InetSocketAddress(getByAddress(id, ipv4), ipv4port); + InetSocketAddress ipv6addr = new InetSocketAddress(getByAddress(id, ipv6), ipv6port); + return Stream.of(ipv4addr, ipv6addr) + .filter((isa) -> !isa.getAddress().isAnyLocalAddress()) + .toList(); + } + + /** + * Reads the connection id bytes from the value of a parameter + * whose format corresponds to the {@link ParameterId#preferred_address} + * parameter. + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * + * @param preferredAddressValue the value of {@link ParameterId#preferred_address} param + * @return the connection id bytes + * @see ParameterId#preferred_address + */ + public static ByteBuffer getPreferredConnectionId(final byte[] preferredAddressValue) { + if (preferredAddressValue == null) { + return null; + } + final int length = getPreferredConnectionIdLength(ParameterId.preferred_address, + preferredAddressValue); + return ByteBuffer.wrap(preferredAddressValue, CID_OFFSET, length); + } + + /** + * Reads the stateless token bytes from the value of a parameter + * whose format corresponds to the {@link ParameterId#preferred_address} + * parameter. + * + * If the given {@code value} is {@code null}, this + * method returns {@code null}. + * + * @param preferredAddressValue the value of {@link ParameterId#preferred_address} param + * @return the stateless reset token bytes + * @see ParameterId#preferred_address + */ + public static byte[] getPreferredStatelessResetToken(final byte[] preferredAddressValue) { + if (preferredAddressValue == null) { + return null; + } + final int length = getPreferredConnectionIdLength(ParameterId.preferred_address, + preferredAddressValue); + final int offset = CID_OFFSET + length; + final byte[] statelessResetToken = new byte[TOKEN_SIZE]; + System.arraycopy(preferredAddressValue, offset, statelessResetToken, 0, TOKEN_SIZE); + return statelessResetToken; + } + + static final byte[] NOBYTES = new byte[0]; + static final int IPV6_SIZE = 16; + static final int IPV4_SIZE = 4; + static final int PORT_SIZE = 2; + static final int TOKEN_SIZE = 16; + static final int CIDLEN_SIZE = 1; + static final int IPV4_ADDR_OFFSET = 0; + static final int IPV4_PORT_OFFSET = IPV4_ADDR_OFFSET + IPV4_SIZE; + static final int IPV6_ADDR_OFFSET = IPV4_PORT_OFFSET + PORT_SIZE; + static final int IPV6_PORT_OFFSET = IPV6_ADDR_OFFSET + IPV6_SIZE; + static final int CID_LEN_OFFSET = IPV6_PORT_OFFSET + PORT_SIZE; + static final int CID_OFFSET = CID_LEN_OFFSET + CIDLEN_SIZE; + static final int MIN_PREF_ADDR_SIZE = CID_OFFSET + TOKEN_SIZE; + static final int MAX_PORT = 0xFFFF; + + private static int getPreferredConnectionIdLength(TransportParameterId id, byte[] value) { + if (value.length < MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": not enough bytes in value; found " + value.length); + } + int length = value[CID_LEN_OFFSET] & 0xFF; + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH || length == 0) { + throw new IllegalArgumentException(id + + ": invalid preferred connection ID length: " + length); + } + if (length != value.length - MIN_PREF_ADDR_SIZE) { + throw new IllegalArgumentException(id + + ": invalid preferred address length: " + value.length + + ", expected: " + (MIN_PREF_ADDR_SIZE + length)); + } + return length; + } + + private static InetAddress getByAddress(TransportParameterId id, byte[] address) { + try { + return InetAddress.getByAddress(address); + } catch (UnknownHostException x) { + // should not happen + throw new IllegalArgumentException(id + + "Invalid address: " + HexFormat.of().formatHex(address)); + } + } + + /** + * verifies that the {@code value} is acceptable (as specified in the RFC) for the + * {@code tpid} + * + * @param tpid the transport parameter id + * @param value the value + * @return the corresponding parameter id if the value is acceptable, else throws a + * {@link IllegalArgumentException} + */ + private static ParameterId checkParameterValue(TransportParameterId tpid, byte[] value) { + ParameterId id = (ParameterId)tpid; + if (value != null) { + switch (id) { + case disable_active_migration -> { + if (value.length > 0) + throw new IllegalArgumentException(id + + ": value must be null or 0-length; found " + + value.length + " bytes"); + } + case stateless_reset_token -> { + if (value.length != 16) + throw new IllegalArgumentException(id + + ": value must be null or 16 bytes long; found " + + value.length + " bytes"); + } + case initial_source_connection_id, original_destination_connection_id, + retry_source_connection_id -> { + if (value.length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) { + throw new IllegalArgumentException(id + + ": value must not exceed " + + QuicConnectionId.MAX_CONNECTION_ID_LENGTH + + "bytes; found " + value.length + " bytes"); + } + } + case preferred_address -> getPreferredConnectionIdLength(id, value); + case version_information -> { + if (value.length < 4 || value.length % 4 != 0) { + throw new IllegalArgumentException(id + + ": value length must be a positive multiple of 4 " + + "bytes; found " + value.length + " bytes"); + } + } + default -> { + long intvalue; + try { + intvalue = decodeVLIntFully(id, ByteBuffer.wrap(value)); + } catch (IllegalArgumentException x) { + throw x; + } catch (Exception x) { + throw new IllegalArgumentException(id + + ": value is not a valid variable length integer", x); + } + if (intvalue < 0) + throw new IllegalArgumentException(id + + ": value is not a valid variable length integer"); + switch (id) { + case max_udp_payload_size -> { + if (intvalue < 1200 || intvalue > 65527) { + throw new IllegalArgumentException(id + + ": value out of range [1200, 65527]; found " + + intvalue); + } + } + case ack_delay_exponent -> { + if (intvalue > 20) { + throw new IllegalArgumentException(id + + ": value out of range [0, 20]; found " + + intvalue); + } + } + case max_ack_delay -> { + if (intvalue >= (1 << 14)) { + throw new IllegalArgumentException(id + + ": value out of range [0, 2^14); found " + + intvalue); + } + } + case active_connection_id_limit -> { + if (intvalue < 2) { + throw new IllegalArgumentException(id + + ": value out of range [2...]; found " + + intvalue); + } + } + case initial_max_streams_bidi, initial_max_streams_uni -> { + if (intvalue >= 1L << 60) { + throw new IllegalArgumentException(id + + ": value out of range [0,2^60); found " + + intvalue); + } + } + } + } + } + } + return id; + } + + private static long decodeVLIntFully(ParameterId id, ByteBuffer buffer) { + long value = VariableLengthEncoder.decode(buffer); + if (value < 0 || value > (1L << 62) - 1) { + throw new IllegalArgumentException(id + + ": failed to decode variable length integer"); + } + if (buffer.hasRemaining()) + throw new IllegalArgumentException(id + + ": extra bytes in provided value at index " + + buffer.position()); + return value; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java new file mode 100644 index 00000000000..9e441cf7873 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/TerminationCause.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.io.IOException; +import java.util.Objects; + +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +// TODO: document this +public abstract sealed class TerminationCause { + private String logMsg; + private String peerVisibleReason; + private final long closeCode; + private final Throwable originalCause; + private final IOException reportedCause; + + private TerminationCause(final long closeCode, final Throwable closeCause) { + this.closeCode = closeCode; + this.originalCause = closeCause; + if (closeCause != null) { + this.logMsg = closeCause.toString(); + } + this.reportedCause = toReportedCause(this.originalCause, this.logMsg); + } + + private TerminationCause(final long closeCode, final String loggedAs) { + this.closeCode = closeCode; + this.originalCause = null; + this.logMsg = loggedAs; + this.reportedCause = toReportedCause(this.originalCause, this.logMsg); + } + + public final long getCloseCode() { + return this.closeCode; + } + + public final IOException getCloseCause() { + return this.reportedCause; + } + + public final String getLogMsg() { + return logMsg; + } + + public final TerminationCause loggedAs(final String logMsg) { + this.logMsg = logMsg; + return this; + } + + public final String getPeerVisibleReason() { + return this.peerVisibleReason; + } + + public final TerminationCause peerVisibleReason(final String reasonPhrase) { + this.peerVisibleReason = reasonPhrase; + return this; + } + + public abstract boolean isAppLayer(); + + public static TerminationCause forTransportError(final QuicTransportErrors err) { + return new TransportError(err); + } + + public static TerminationCause forTransportError(long errorCode, String loggedAs, long frameType) { + return new TransportError(errorCode, loggedAs, frameType); + } + + static SilentTermination forSilentTermination(final String loggedAs) { + return new SilentTermination(loggedAs); + } + + public static TerminationCause forException(final Throwable cause) { + Objects.requireNonNull(cause); + if (cause instanceof QuicTransportException qte) { + return new TransportError(qte); + } + return new InternalError(cause); + } + + // allows for higher (application) layer to inform the connection terminator + // that the higher layer had completed a graceful shutdown of the connection + // and the QUIC layer can now do an immediate close of the connection using + // the {@code closeCode} + public static TerminationCause appLayerClose(final long closeCode) { + return new AppLayerClose(closeCode, (Throwable)null); + } + + public static TerminationCause appLayerClose(final long closeCode, String loggedAs) { + return new AppLayerClose(closeCode, loggedAs); + } + + public static TerminationCause appLayerException(final long closeCode, + final Throwable cause) { + return new AppLayerClose(closeCode, cause); + } + + private static IOException toReportedCause(final Throwable original, + final String fallbackExceptionMsg) { + if (original == null) { + return fallbackExceptionMsg == null + ? new IOException("connection terminated") + : new IOException(fallbackExceptionMsg); + } else if (original instanceof QuicTransportException qte) { + return new IOException(qte.getMessage()); + } else if (original instanceof IOException ioe) { + return ioe; + } else { + return new IOException(original); + } + } + + + static final class TransportError extends TerminationCause { + final long frameType; + final QuicTLSEngine.KeySpace keySpace; + + private TransportError(final QuicTransportErrors err) { + super(err.code(), err.name()); + this.frameType = 0; // unknown frame type + this.keySpace = null; + } + + private TransportError(final QuicTransportException exception) { + super(exception.getErrorCode(), exception); + this.frameType = exception.getFrameType(); + this.keySpace = exception.getKeySpace(); + peerVisibleReason(exception.getReason()); + } + + public TransportError(long errorCode, String loggedAs, long frameType) { + super(errorCode, loggedAs); + this.frameType = frameType; + keySpace = null; + } + + @Override + public boolean isAppLayer() { + return false; + } + } + + static final class InternalError extends TerminationCause { + + private InternalError(final Throwable cause) { + super(QuicTransportErrors.INTERNAL_ERROR.code(), cause); + } + + @Override + public boolean isAppLayer() { + return false; + } + } + + static final class AppLayerClose extends TerminationCause { + private AppLayerClose(final long closeCode, String loggedAs) { + super(closeCode, loggedAs); + } + + // TODO: allow optionally to specify "name" of the close code for app layer + // like "H3_GENERAL_PROTOCOL_ERROR" (helpful in logging) + private AppLayerClose(final long closeCode, final Throwable cause) { + super(closeCode, cause); + } + + @Override + public boolean isAppLayer() { + return true; + } + } + + static final class SilentTermination extends TerminationCause { + + private SilentTermination(final String loggedAs) { + // the error code won't play any role, since silent termination + // doesn't cause any packets to be generated or sent to the peer + super(NO_ERROR.code(), loggedAs); + } + + @Override + public boolean isAppLayer() { + return false; // doesn't play a role in context of silent termination + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java new file mode 100644 index 00000000000..91380fcfca4 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/VariableLengthEncoder.java @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +import java.nio.ByteBuffer; + +/** + * QUIC packets and frames commonly use a variable-length encoding for + * non-negative values. This encoding ensures that smaller values will use less + * in the packet or frame. + * + *

        The QUIC variable-length encoding reserves the two most significant bits + * of the first byte to encode the size of the length value as a base 2 logarithm + * value. The length itself is then encoded on the remaining bits, in network + * byte order. This means that the length values will be encoded on 1, 2, 4, or + * 8 bytes and can encode 6-, 14-, 30-, or 62-bit values + * respectively, or a value within the range of 0 to 4611686018427387903 + * inclusive. + * + * @spec https://www.rfc-editor.org/rfc/rfc9000.html#integer-encoding + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public class VariableLengthEncoder { + + /** + * The maximum number of bytes on which a variable length + * integer can be encoded. + */ + public static final int MAX_INTEGER_LENGTH = 8; + + /** + * The maximum value a variable length integer can + * take. + */ + public static final long MAX_ENCODED_INTEGER = (1L << 62) - 1; + + static { + assert MAX_ENCODED_INTEGER == 4611686018427387903L; + } + + private VariableLengthEncoder() { + throw new InternalError("should not come here"); + } + + /** + * Decode a variable length value from {@code ByteBuffer}. This method assumes that the + * position of {@code buffer} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last byte read. + * + * @param buffer the {@code ByteBuffer} that the length will be decoded from + * + * @return the value. If an error occurs, {@code -1} is returned and + * the buffer position is left unchanged. + */ + public static long decode(ByteBuffer buffer) { + return decode(BuffersReader.single(buffer)); + } + + /** + * Decode a variable length value from {@code BuffersReader}. This method assumes that the + * position of {@code buffers} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last byte. + * + * @param buffers the {@code BuffersReader} that the length will be decoded from + * + * @return the value. If an error occurs, {@code -1} is returned and + * the buffer position is left unchanged. + */ + public static long decode(BuffersReader buffers) { + if (!buffers.hasRemaining()) + return -1; + + long pos = buffers.position(); + int lenByte = buffers.get(pos) & 0xFF; + pos++; + // read size of length from leading two bits + int prefix = lenByte >> 6; + int len = 1 << prefix; + // retrieve remaining bits that constitute the length + long result = lenByte & 0x3F; + long idx = 0, lim = buffers.limit(); + if (lim - pos < len - 1) return -1; + while (idx++ < len - 1) { + assert pos < lim; + result = ((result << Byte.SIZE) + (buffers.get(pos) & 0xFF)); + pos++; + } + // Set position of ByteBuffer to next byte following length + assert pos == buffers.position() + len; + assert pos <= buffers.limit(); + buffers.position(pos); + + assert (result >= 0) && (result < (1L << 62)); + return result; + } + + /** + * Encode (a variable length) value into {@code ByteBuffer}. This method assumes that the + * position of {@code buffer} has been set to the first byte where the length + * begins. If the methods completes successfully, the position will be set + * to the byte after the last length byte. + * + * @param buffer the {@code ByteBuffer} that the length will be encoded into + * @param value the variable length value + * + * @throws IllegalArgumentException + * if value supplied falls outside of acceptable bounds [0, 2^62-1], + * or if the given buffer doesn't contain enough space to encode the + * value + * + * @return the {@code position} of the buffer + */ + public static int encode(ByteBuffer buffer, long value) throws IllegalArgumentException { + // check for valid parameters + if (value < 0 || value > MAX_ENCODED_INTEGER) + throw new IllegalArgumentException( + "value supplied falls outside of acceptable bounds"); + if (!buffer.hasRemaining()) + throw new IllegalArgumentException( + "buffer does not contain enough bytes to store length"); + + // set length prefix to indicate size of length + int lengthPrefix = getVariableLengthPrefix(value); + assert lengthPrefix >= 0 && lengthPrefix <= 3; + lengthPrefix <<= (Byte.SIZE - 2); + + int lengthSize = getEncodedSize(value); + assert lengthSize > 0; + assert lengthSize <= 8; + + var limit = buffer.limit(); + var pos = buffer.position(); + + // check that it's possible to add length to buffer + if (lengthSize > limit - pos) + throw new IllegalArgumentException("buffer does not contain enough bytes to store length"); + + // create mask to use in isolating byte to transfer to buffer + long mask = 255L << (Byte.SIZE * (lengthSize - 1)); + // convert length to bytes and add to buffer + boolean isFirstByte = true; + for (int i = lengthSize; i > 0; i--) { + assert buffer.hasRemaining() : "no space left at " + (lengthSize - i); + assert mask != 0; + assert mask == (255L << ((i - 1) * 8)) + : "mask: %x, expected %x".formatted(mask, (255L << ((i - 1) * 8))); + + long b = value & mask; + for (int j = i - 1; j > 0; j--) { + b >>= Byte.SIZE; + } + + assert b == (value & mask) >> (8 * (i - 1)); + + if (isFirstByte) { + assert (b & 0xC0) == 0; + buffer.put((byte) (b | lengthPrefix)); + isFirstByte = false; + } else { + buffer.put((byte) b); + } + // move mask over to next byte - avoid carrying sign bit + mask = (mask >>> Byte.SIZE); + } + var bytes = buffer.position() - pos; + assert bytes == lengthSize; + return lengthSize; + } + + /** + * Returns the variable length prefix. + * The variable length prefix is the base 2 logarithm of + * the number of bytes required to encode + * a positive value as a variable length integer: + * [0, 1, 2, 3] for [1, 2, 4, 8] bytes. + * + * @param value the value to encode + * + * @throws IllegalArgumentException + * if the supplied value falls outside the acceptable bounds [0, 2^62-1] + * + * @return the base 2 logarithm of the number of bytes required to encode + * the value as a variable length integer. + */ + public static int getVariableLengthPrefix(long value) throws IllegalArgumentException { + if ((value > MAX_ENCODED_INTEGER) || (value < 0)) + throw new IllegalArgumentException("invalid length"); + + int lengthPrefix; + if (value > (1L << 30) - 1) + lengthPrefix = 3; // 8 bytes + else if (value > (1L << 14) - 1) + lengthPrefix = 2; // 4 bytes + else if (value > (1L << 6) - 1) + lengthPrefix = 1; // 2 bytes + else + lengthPrefix = 0; // 1 byte + + return lengthPrefix; + } + + /** + * Returns the number of bytes needed to encode + * the given value as a variable length integer. + * This a number between 1 and 8. + * + * @param value the value to encode + * + * @return the number of bytes needed to encode + * the given value as a variable length integer. + * + * @throws IllegalArgumentException + * if the value supplied falls outside of acceptable bounds [0, 2^62-1] + */ + public static int getEncodedSize(long value) throws IllegalArgumentException { + if (value < 0 || value > MAX_ENCODED_INTEGER) + throw new IllegalArgumentException("invalid variable length integer: " + value); + return 1 << getVariableLengthPrefix(value); + } + + /** + * Peeks at a variable length value encoded at the given offset. + * If the byte buffer doesn't contain enough bytes to read the + * variable length value, -1 is returned. + * + *

        This method doesn't advance the buffer position. + * + * @param buffer the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the variable length value encoded at the given offset, or -1 + */ + public static long peekEncodedValue(ByteBuffer buffer, int offset) { + return peekEncodedValue(BuffersReader.single(buffer), offset); + } + + /** + * Peeks at a variable length value encoded at the given offset. + * If the byte buffer doesn't contain enough bytes to read the + * variable length value, -1 is returned. + * + * This method doesn't advance the buffer position. + * + * @param buffers the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the variable length value encoded at the given offset, or -1 + */ + public static long peekEncodedValue(BuffersReader buffers, long offset) { + + // figure out on how many bytes the length is encoded. + int size = peekEncodedValueSize(buffers, offset); + if (size <= 0) return -1L; + assert size > 0 && size <= 8; + + // check that we have enough bytes in the buffer + long limit = buffers.limit(); + long pos = offset; + if (limit - size < pos) return -1L; + + // peek at the variable length: + // - read first byte + int first = buffers.get(pos++); + long res = first & 0x3F; + if (size == 1) return res; + + // - read the rest of the bytes + size -= 1; + assert size > 0; + for (int i=0 ; i < size; i++) { + if (limit <= pos) return -1L; + res = (res << 8) | (long) (buffers.get(pos++) & 0xFF); + } + return res; + } + + /** + * Peeks at a variable length value encoded at the given offset, + * and return the number of bytes on which this value is encoded. + * If the byte buffer is empty or the offset is past + * the limit -1 is returned. + * This method doesn't advance the buffer position. + * + * @param buffer the buffer to read from + * @param offset the offset in the buffer to start reading from + * + * @return the number of bytes on which the variable length + * value is encoded at the given offset, or -1 + */ + public static int peekEncodedValueSize(ByteBuffer buffer, int offset) { + return peekEncodedValueSize(BuffersReader.single(buffer), offset); + } + + /** + * Peeks at a variable length value encoded at the given offset, + * and return the number of bytes on which this value is encoded. + * If the byte buffer is empty or the offset is past + * the limit -1 is returned. + * This method doesn't advance the buffer position. + * + * @param buffers the buffers to read from + * @param offset the offset in the buffer to start reading from + * + * @return the number of bytes on which the variable length + * value is encoded at the given offset, or -1 + */ + public static int peekEncodedValueSize(BuffersReader buffers, long offset) { + long limit = buffers.limit(); + long pos = offset; + if (limit <= pos) return -1; + int first = buffers.get(pos); + int prefix = (first & 0xC0) >>> 6; + int size = 1 << prefix; + assert size > 0 && size <= 8; + return size; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java new file mode 100644 index 00000000000..7983d1be4f0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/AckFrame.java @@ -0,0 +1,931 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.LongConsumer; +import java.util.stream.LongStream; +import java.util.stream.StreamSupport; + +/** + * An ACK Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class AckFrame extends QuicFrame { + + private final long largestAcknowledged; + private final long ackDelay; + private final int ackRangeCount; + private final List ackRanges; + + private final boolean countsPresent; + private final long ect0Count; + private final long ect1Count; + private final long ecnCECount; + private final int size; + + private static final int COUNTS_PRESENT = 0x1; + + /** + * Reads an {@code AckFrame} from the given buffer. When entering + * this method the buffer position is supposed to be just past + * after the frame type. That, is the frame type has already + * been read. This method moves the position of the buffer to the + * first byte after the read ACK frame. + * @param buffer a buffer containing the ACK frame + * @param type the frame type read from the buffer + * @throws QuicTransportException if the ACK frame was malformed + */ + AckFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(ACK); + int pos = buffer.position(); + largestAcknowledged = decodeVLField(buffer, "largestAcknowledged"); + ackDelay = decodeVLField(buffer, "ackDelay"); + ackRangeCount = decodeVLFieldAsInt(buffer, "ackRangeCount"); + long firstAckRange = decodeVLField(buffer, "firstAckRange"); + long smallestAcknowledged = largestAcknowledged - firstAckRange; + if (smallestAcknowledged < 0) { + throw new QuicTransportException("Negative PN acknowledged", + null, type, + QuicTransportErrors.FRAME_ENCODING_ERROR); + } + var ackRanges = new ArrayList(ackRangeCount + 1); + AckRange first = AckRange.of(0, firstAckRange); + ackRanges.add(0, first); + for (int i=1; i <= ackRangeCount; i++) { + long gap = decodeVLField(buffer, "gap"); + long len = decodeVLField(buffer, "range length"); + ackRanges.add(i, AckRange.of(gap, len)); + smallestAcknowledged -= gap + len + 2; + if (smallestAcknowledged < 0) { + // verify after each range to avoid wrap around + throw new QuicTransportException("Negative PN acknowledged", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + this.ackRanges = List.copyOf(ackRanges); + if (type % 2 == 1) { + // packet contains ECN counts + countsPresent = true; + ect0Count = decodeVLField(buffer, "ect0Count"); + ect1Count = decodeVLField(buffer, "ect1Count"); + ecnCECount = decodeVLField(buffer, "ecnCECount"); + } else { + countsPresent = false; + ect0Count = -1; + ect1Count = -1; + ecnCECount = -1; + } + size = computeSize(); + int wireSize = buffer.position() - pos + getVLFieldLengthFor(getTypeField()); + assert size <= wireSize : "parsed: %s, computed size: %s" + .formatted(wireSize, size); + } + + /** + * Creates the short formed ACK frame with no count totals + */ + public AckFrame(long largestAcknowledged, long ackDelay, List ackRanges) + { + this(largestAcknowledged, ackDelay, ackRanges, -1, -1, -1); + } + + /** + * Creates the long formed ACK frame with count totals + */ + public AckFrame( + long largestAcknowledged, + long ackDelay, + List ackRanges, + long ect0Count, + long ect1Count, + long ecnCECount) + { + super(ACK); + this.largestAcknowledged = requireVLRange(largestAcknowledged, "largestAcknowledged"); + this.ackDelay = requireVLRange(ackDelay, "ackDelay"); + if (ackRanges.size() < 1) { + throw new IllegalArgumentException("insufficient ackRanges"); + } + if (ackRanges.get(0).gap() != 0) { + throw new IllegalArgumentException("first range must have zero gap"); + } + this.ackRanges = List.copyOf(ackRanges); + this.ackRangeCount = ackRanges.size() - 1; + this.countsPresent = ect0Count != -1 || ect1Count != -1 || ecnCECount != -1; + if (countsPresent) { + this.ect0Count = requireVLRange(ect0Count,"ect0Count"); + this.ect1Count = requireVLRange(ect1Count, "ect1Count"); + this.ecnCECount = requireVLRange(ecnCECount, "ecnCECount"); + } else { + this.ect0Count = ect0Count; + this.ect1Count = ect1Count; + this.ecnCECount = ecnCECount; + } + this.size = computeSize(); + } + + @Override + public long getTypeField() { + return ACK | (countsPresent ? COUNTS_PRESENT : 0); + } + + @Override + public boolean isAckEliciting() { return false; } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, largestAcknowledged, "largestAcknowledged"); + encodeVLField(buffer, ackDelay, "ackDelay"); + encodeVLField(buffer, ackRangeCount, "ackRangeCount"); + encodeVLField(buffer, ackRanges.get(0).range(), "firstAckRange"); + for (int i=1; i <= ackRangeCount; i++) { + AckRange ar = ackRanges.get(i); + encodeVLField(buffer, ar.gap(), "gap"); + encodeVLField(buffer, ar.range(), "range"); + } + if (countsPresent) { + // encode the counts + encodeVLField(buffer, ect0Count, "ect0Count"); + encodeVLField(buffer, ect1Count, "ect1Count"); + encodeVLField(buffer, ecnCECount, "ecnCECount"); + } + assert buffer.position() - pos == size(); + } + + private int computeSize() { + int size = getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(largestAcknowledged) + + getVLFieldLengthFor(ackDelay) + + getVLFieldLengthFor(ackRangeCount) + + getVLFieldLengthFor(ackRanges.get(0).range()) + + ackRanges.stream().skip(1).mapToInt(AckRange::size).sum(); + if (countsPresent) { + size = size + getVLFieldLengthFor(ect0Count) + + getVLFieldLengthFor(ect1Count) + + getVLFieldLengthFor(ecnCECount); + } + return size; + } + + @Override + public int size() { return size; } + + /** + * {@return largest packet number acknowledged by this frame} + */ + public long largestAcknowledged() { + return largestAcknowledged; + } + + /** + * The ACK delay + */ + public long ackDelay() { + return ackDelay; + } + + /** + * {@return the number of ack ranges} + * This corresponds to {@link #ackRanges() ackRange.size() -1}. + */ + public long ackRangeCount() { + return ackRangeCount; + } + + /** + * {@return a new {@code AckFrame} identical to this one, but + * with the given {@code ackDelay}}; + * @param ackDelay + */ + public AckFrame withAckDelay(long ackDelay) { + if (ackDelay == this.ackDelay) return this; + return new AckFrame(largestAcknowledged, ackDelay, ackRanges, + ect0Count, ect1Count, ecnCECount); + } + + /** + * An ACK range, composed of a gap and a range. + */ + public record AckRange(long gap, long range) { + public static final AckRange INITIAL = new AckRange(0, 0); + public AckRange { + requireVLRange(gap, "gap"); + requireVLRange(range, "range"); + } + public int size() { + return getVLFieldLengthFor(gap) + getVLFieldLengthFor(range); + } + public static AckRange of(long gap, long range) { + if (gap == 0 && range == 0) return INITIAL; + return new AckRange(gap, range); + } + } + + /** + * The ack ranges. First element is an actual range relative + * to highest acknowledged packet number. Second (if present) + * is a gap and a range following that gap, and so on until the last. + * @return the list of {@code AckRange} where the first ack range + * has a gap of {@code 0} and a range corresponding to + * the {@code First ACK Range}. + */ + public List ackRanges() { + return ackRanges; + } + + /** + * {@return the ECT0 count from this frame or -1 if not present} + */ + public long ect0Count() { + return ect0Count; + } + + /** + * {@return the ECT1 count from this frame or -1 if not present} + */ + public long ect1Count() { + return ect1Count; + } + + /** + * {@return the ECN-CE count from this frame or -1 if not present} + */ + public long ecnCECount() { + return ecnCECount; + } + + /** + * {@return true if this frame contains an acknowledgment for the + * given packet number} + * @param packetNumber a packet number + */ + public boolean isAcknowledging(long packetNumber) { + return isAcknowledging(largestAcknowledged, ackRanges, packetNumber); + } + + /** + * {@return true if the given range is acknowledged by this frame} + * @param first the first packet in the range, inclusive + * @param last the last packet in the range, inclusive + */ + public boolean isRangeAcknowledged(long first, long last) { + return isRangeAcknowledged(largestAcknowledged, ackRanges, first, last); + } + + + /** + * {@return the smallest packet number acknowledged by this {@code AckFrame}} + */ + public long smallestAcknowledged() { + return smallestAcknowledged(largestAcknowledged, ackRanges); + } + + /** + * @return a stream of packet numbers acknowledged by this frame + */ + public LongStream acknowledged() { + return StreamSupport.longStream(new AckFrameSpliterator(this), false); + } + + + private static class AckFrameSpliterator implements Spliterator.OfLong { + + final AckFrame ackFrame; + + AckFrameSpliterator(AckFrame ackFrame) { + this.ackFrame = ackFrame; + this.largest = ackFrame.largestAcknowledged(); + this.smallest = largest + 2; + this.ackRangeIterator = ackFrame.ackRanges.iterator(); + } + + @Override + public long estimateSize() { + // It is costly to compute an estimate, so we just + // return Long.MAX_VALUE instead + return Long.MAX_VALUE; + } + + @Override + public int characteristics() { + // NONNULL - nulls are not expected to be returned by this long spliterator + // IMMUTABLE - ackFrame.ackRanges() returns unmodifiable list, which cannot be + // structurally modified + return NONNULL | IMMUTABLE; + } + + @Override + public OfLong trySplit() { + // null - this spliterator cannot be split + return null; + } + private final Iterator ackRangeIterator; + private volatile long largest; + private volatile long smallest; + private volatile long pn; // the current packet number + + // The stream returns packet number in decreasing order + // (largest packet number is returned first) + private boolean ackAndDecId(LongConsumer action) { + assert ackFrame.isAcknowledging(pn) + : "%s is not acknowledging %s".formatted(ackFrame, pn); + action.accept(pn--); + return true; + } + + @Override + public boolean tryAdvance(LongConsumer action) { + // First call will see pn == 0 and smallest >= 2, + // which guarantees we will not enter the if below + // before pn has been initialized from the + // first ackRange value + if (pn >= smallest) { + return ackAndDecId(action); + } + if (ackRangeIterator.hasNext()) { + var ackRange = ackRangeIterator.next(); + largest = smallest - ackRange.gap() - 2; + smallest = largest - ackRange.range; + pn = largest; + return ackAndDecId(action); + } + return false; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + return o instanceof AckFrame ackFrame + && largestAcknowledged == ackFrame.largestAcknowledged + && ackDelay == ackFrame.ackDelay + && ackRangeCount == ackFrame.ackRangeCount + && countsPresent == ackFrame.countsPresent + && ect0Count == ackFrame.ect0Count + && ect1Count == ackFrame.ect1Count + && ecnCECount == ackFrame.ecnCECount + && ackRanges.equals(ackFrame.ackRanges); + } + + @Override + public int hashCode() { + return Objects.hash(largestAcknowledged, ackDelay, + ackRanges, ect0Count, ect1Count, ecnCECount); + } + + @Override + public String toString() { + String res = "AckFrame(" + + "largestAcknowledged=" + largestAcknowledged + + ", ackDelay=" + ackDelay + + ", ackRanges=[" + prettyRanges() + "]"; + if (countsPresent) res = res + + ", ect0Count=" + ect0Count + + ", ect1Count=" + ect1Count + + ", ecnCECount=" + ecnCECount; + res += ")"; + return res; + } + + private String prettyRanges() { + String result = null; + long largest; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + result = smallest + ".." + largest + (result != null ? ", "+result : ""); + } + return result; + } + + /** + * {@return the largest packet acknowledged by an + * {@link QuicFrame#ACK ACK} frame contained in the + * given packet, or {@code -1L} if the packet + * contains no {@code ACK} frame} + * @param packet a packet that may contain an {@code ACK} frame + */ + public static long largestAcknowledgedInPacket(QuicPacket packet) { + return packet.frames().stream() + .filter(AckFrame.class::isInstance) + .map(AckFrame.class::cast) + .mapToLong(AckFrame::largestAcknowledged) + .max().orElse(-1L); + } + + /** + * A builder that allows to incrementally build the AckFrame + * that will need to be sent, as new packets are received. + * This class is not MT-thread safe. + */ + public static final class AckFrameBuilder { + long largestAckAcked = -1; + long largestAcknowledged = -1; + long ackDelay = 0; + List ackRanges = new ArrayList<>(); + long ect0Count = -1; + long ect1Count = -1; + long ecnCECount = -1; + + /** + * An empty builder. + */ + public AckFrameBuilder() {} + + /** + * A builder initialize from the content of an AckFrame. + * @param frame the {@code AckFrame} to initialize this builder with. + * Must not be {@code null}. + */ + public AckFrameBuilder(AckFrame frame) { + largestAckAcked = -1; + largestAcknowledged = frame.largestAcknowledged; + ackDelay = frame.ackDelay; + ackRanges.addAll(frame.ackRanges); + ect0Count = frame.ect0Count; + ect1Count = frame.ect1Count; + ecnCECount = frame.ecnCECount; + } + + public long getLargestAckAcked() { + return largestAckAcked; + } + + /** + * Drops all acks for packet whose number is smaller + * than the given {@code largestAckAcked}. + * @param largestAckAcked the smallest packet number that + * should be acknowledged by this + * {@link AckFrame}. + * @return this builder + */ + public AckFrameBuilder dropAcksBefore(long largestAckAcked) { + if (largestAckAcked > this.largestAckAcked) { + this.largestAckAcked = largestAckAcked; + return dropIfSmallerThan(largestAckAcked); + } else { + this.largestAckAcked = largestAckAcked; + } + return this; + } + + /** + * Drops all instances of {@link AckRange} after the given + * index in the {@linkplain #ackRanges() Ack Range List}, and compute + * the new smallest packet number now acknowledged by this + * {@link AckFrame}: this computed packet number will then be + * returned by {@link #getLargestAckAcked()}. + * This is a no-op if index is greater or equal to + * {@code ackRanges().size() -1}. + * @param index the index after which ranges should be dropped. + * @return this builder + */ + public AckFrameBuilder dropAckRangesAfter(int index) { + if (index < 0) { + throw new IllegalArgumentException("invalid index %s for size %s" + .formatted(index, ackRanges.size())); + } + if (index >= ackRanges.size() - 1) return this; + long newLargestAckAcked = dropRangesIfAfter(index); + assert newLargestAckAcked > largestAckAcked; + largestAckAcked = newLargestAckAcked; + return this; + } + + /** + * Sets the ack delay. + * @param ackDelay the ack delay. + * @return this builder. + */ + public AckFrameBuilder ackDelay(long ackDelay) { + this.ackDelay = ackDelay; + return this; + } + + /** + * Sets the ect0Count. Passing -1 unsets the ectOcount. + * @param ect0Count the ect0Count + * @return this builder. + */ + public AckFrameBuilder ect0Count(long ect0Count) { + this.ect0Count = ect0Count; + return this; + } + + /** + * Sets the ect1Count. Passing -1 unsets the ect1count. + * @param ect1Count the ect1Count + * @return this builder. + */ + public AckFrameBuilder ect1Count(long ect1Count) { + this.ect1Count = ect1Count; + return this; + } + + /** + * Sets the ecnCECount. Passing -1 unsets the ecnCEOcount. + * @param ecnCECount the ecnCECount + * @return this builder. + */ + public AckFrameBuilder ecnCECount(long ecnCECount) { + this.ecnCECount = ecnCECount; + return this; + } + + /** + * Adds the given packet number to the list of ack ranges. + * If the packet is already being acknowledged by this frame, + * do nothing. + * @param packetNumber the packet number + * @return this builder + */ + public AckFrameBuilder addAck(long packetNumber) { + // check if we need to acknowledge this packet + if (packetNumber <= largestAckAcked) return this; + // System.out.println("adding " + packetNumber); + if (ackRanges.isEmpty()) { + // easy case: we only have one packet to acknowledge! + return acknowledgeFirstPacket(packetNumber); + } else if (packetNumber > largestAcknowledged) { + return acknowledgeLargerPacket(packetNumber); + } else if (packetNumber < largestAcknowledged) { + // now is the complex case: we need to find out: + // - whether this packet is already acknowledged, in which case, + // there is nothing to do (great) + // - or whether we can extend an existing range + // - or whether we need to create a new range (if the packet falls + // within a gap whose value is > 0). + // - or whether we should merge two ranges if the packet falls + // on a gap whose value is 0 + ListIterator iterator = ackRanges.listIterator(); + long largest = largestAcknowledged; + long smallest = largest + 2; + int index = -1; + while (iterator.hasNext()) { + var ackRange = iterator.next(); + // index of the current ackRange element + index++; + // largest packet number acknowledged by this ackRange + largest = smallest - ackRange.gap - 2; + // smallest packet number acknowledged by this ackRange + smallest = largest - ackRange.range; + + // if the packet number we want to acknowledge is greater + // than the largest packet acknowledged by this ackRange + // there are two cases: + if (packetNumber > largest) { + // the packet number is just above the largest packet + if (packetNumber -1 == largest) { + // the current ackRange must have a gap, and we can simply + // reduce that gap by 1, and extend the range by 1. + // the case where the current ackrange doesn't have a gap + // and the packet number is the largest + 1 should have + // been handled when processing the previous ackRange. + assert ackRange.gap > 0; + var gap = ackRange.gap - 1; + var range = ackRange.range + 1; + var replaced = AckRange.of(gap, range); + ackRanges.set(index, replaced); + return this; + } else { + // the packet falls within the gap of this ack range. + // we need to split the ackRange in two... + // + // in the case where we have + // [31,31] [27,27] -> 31, AckRange[g=0, r=0], AckRange[g=2, r=0] + // and we want to acknowledge 29. + // we should end up with: + // [31,31] [29,29] [27,27] -> + // 31, AckRange[g=0, r=0], AckRange[g=0, r=0], AckRange[g=0, r=0] + assert ackRange.gap > 0 : "%s at index (prev:%s, next:%s)" + .formatted(ackRanges, iterator.previousIndex(), iterator.nextIndex()); + assert packetNumber - ackRange.gap -2 <= largest; + + // compute the smallest packet that was acknowledged by the + // previous ackRange. This should be: + var previousSmallest = largest + ackRange.gap + 2; + + // System.out.printf("ack: %s, largest:%s, previousSmallest:%d%n", + // ackRange, largest, previousSmallest); + + // compute the point at which we should split the current ackRange + // the current ackRange will be split in two: first, and second + // - first will replace the current ackRange + // - second will be inserted after first + var firstgap = previousSmallest - packetNumber -2; + AckRange first = AckRange.of(firstgap, 0); + AckRange second = AckRange.of(ackRange.gap - firstgap -2, ackRange.range); + ackRanges.set(index, first); + iterator.add(second); + return this; + } + } else if (packetNumber < smallest) { + // otherwise, if the packet number is smaller than + // the smallest packet acknowledged by the current ackRange, + // there are two cases: + + // If the current ackRange is the last: it's simple! + // But there are again two cases: + if (!iterator.hasNext()) { + // If the packet number we want to acknowledge is just below + // the smallest packet number acknowledge by the current + // ackRange, there is no gap between the packet number and + // the current range, so we can simply extend the current range + // Otherwise, we need to append a new ackRange. + if (packetNumber == smallest - 1) { + // no gap: we can extend the current range + AckRange replaced = AckRange + .of(ackRange.gap, ackRange.range + 1); + ackRanges.set(index, replaced); + } else { + // gap: we need to add a new AckRange + AckRange last = AckRange.of(smallest - packetNumber - 2, 0); + iterator.add(last); + } + return this; + } else if (packetNumber == smallest - 1) { + // Otherwise, if the packet number to be acknowledged is + // just below the smallest packet ackowledged by the current + // range, there are again two cases, depending on + // whether the next ackRange has a gap that can be reduced, + // or not + assert iterator.hasNext(); + AckRange next = ackRanges.get(index + 1); + // if the gap of the next packet can be reduced, that's great! + // just do it! We need to reduce that gap by one, and extend + // the range of the current ackRange + if (next.gap > 0) { + // reduce the gap in the next ackrange, and increase + // the range in the current ackrange. + // System.out.printf("ack: %s, next: %s%n", ackRange, next); + AckRange first = AckRange.of(ackRange.gap, ackRange.range + 1); + AckRange second = AckRange.of(next.gap - 1, next.range); + // System.out.printf("first: %s, second: %s%n", first, second); + ackRanges.set(index, first); + ackRanges.set(index + 1, second); + return this; + } else { + // Otherwise, that's the complex case again. + // we have a gap of 1 packet between 2 ackranges. + // our packet number falls exactly in that gap. + // We need to merge the two ranges! + // merge with next ackRange: remove the current ackRange, + // the ackRange at the current index is now the next ackRange, + // replace it with a merged ACK range. + var mergedRanges = ackRange.range + next.range + 2; + iterator.remove(); + ackRanges.set(index, AckRange.of(ackRange.gap, mergedRanges)); + return this; + } + } + } else { + // Otherwise, the packet is already acknowledged! + // nothing to do. + assert packetNumber <= largest && packetNumber >= smallest; + return this; + } + } + } else { + // already acknowledged! + assert packetNumber == largestAcknowledged; + return this; + } + return this; + } + + /** + * {@return true if this builder contains no ACK yet} + */ + public boolean isEmpty() { + return ackRanges.isEmpty(); + } + + /** + * {@return the number of ACK ranges in this builder, including the fake + * first ACK range} + */ + public int length() { + return ackRanges.size(); + } + + /** + * {@return true if the given packet number is already acknowledged + * by this builder} + * @param packetNumber a packet number + */ + public boolean isAcknowledging(long packetNumber) { + if (isEmpty()) return false; + return AckFrame.isAcknowledging(largestAcknowledged, ackRanges, packetNumber); + } + + /** + * {@return the smallest packet number acknowledged by this {@code AckFrame}} + */ + public long smallestAcknowledged() { + if (largestAcknowledged == -1L) return -1L; + return AckFrame.smallestAcknowledged(largestAcknowledged, ackRanges); + } + + // drop acknowledgement of all packet numbers acknowledged + // by AckRange instances coming after the given index, and + // return the smallest packet number now acked by this + // AckFrame. + private long dropRangesIfAfter(int ackIndex) { + assert ackIndex > 0 && ackIndex < ackRanges.size(); + long largest = largestAcknowledged; + long smallest = largest + 2; + ListIterator iterator = ackRanges.listIterator(); + int index = -1; + boolean removeRemainings = false; + long newLargestAckAcked = -1; + while (iterator.hasNext()) { + if (index == ackIndex) { + newLargestAckAcked = smallest; + removeRemainings = true; + } + AckRange ackRange = iterator.next(); + if (removeRemainings) { + iterator.remove(); + continue; + } + index++; + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + } + return newLargestAckAcked; + } + + + // drop acknowledgement of all packet numbers less or equal + // to `largestAckAcked; + private AckFrameBuilder dropIfSmallerThan(long largestAckAcked) { + if (largestAckAcked >= largestAcknowledged) { + largestAcknowledged = -1; + ackRanges.clear(); + return this; + } + long largest = largestAcknowledged; + long smallest = largest + 2; + ListIterator iterator = ackRanges.listIterator(); + int index = -1; + boolean removeRemainings = false; + while (iterator.hasNext()) { + AckRange ackRange = iterator.next(); + if (removeRemainings) { + iterator.remove(); + continue; + } + index++; + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + if (largest <= largestAckAcked) { + iterator.remove(); + removeRemainings = true; + } else if (smallest <= largestAckAcked) { + long removed = largestAckAcked - smallest + 1; + long gap = ackRange.gap; + long range = ackRange.range - removed; + assert gap >= 0; + assert range >= 0; + ackRanges.set(index, new AckRange(gap, range)); + removeRemainings = true; + } + } + return this; + } + + /** + * Builds an {@code AckFrame} from this builder's content. + * @return a new {@code AckFrame}. + */ + public AckFrame build() { + return new AckFrame(largestAcknowledged, ackDelay, ackRanges, + ect0Count, ect1Count, ecnCECount); + } + + private AckFrameBuilder acknowledgeFirstPacket(long packetNumber) { + assert ackRanges.isEmpty(); + largestAcknowledged = packetNumber; + ackRanges.add(AckRange.INITIAL); + return this; + } + + private AckFrameBuilder acknowledgeLargerPacket(long largerThanLargest) { + var packetNumber = largerThanLargest; + // the new packet is larger than the largest acknowledged + var firstAckRange = ackRanges.get(0); + if (largestAcknowledged == packetNumber -1) { + // if packetNumber is largestAcknowledged + 1, we can simply + // extend the first ack range by 1 + firstAckRange = AckRange.of(0, firstAckRange.range + 1); + ackRanges.set(0, firstAckRange); + } else { + // otherwise - we have a gap - we need to acknowledge the new packetNumber, + // and then add the gap that separate it from the previous largestAcknowledged... + ackRanges.add(0, AckRange.INITIAL); // acknowledge packetNumber only + long gap = packetNumber - largestAcknowledged -2; + var secondAckRange = AckRange.of(gap, firstAckRange.range); + ackRanges.set(1, secondAckRange); // add the gap + } + largestAcknowledged = packetNumber; + return this; + } + + public static AckFrameBuilder ofNullable(AckFrame frame) { + return frame == null ? new AckFrameBuilder() : new AckFrameBuilder(frame); + } + + } + + // This is described in RFC 9000, Section 19.3.1 ACK Ranges + // https://www.rfc-editor.org/rfc/rfc9000#name-ack-ranges + private static boolean isAcknowledging(long largestAcknowledged, + List ackRanges, + long packetNumber) { + if (packetNumber > largestAcknowledged) return false; + var largest = largestAcknowledged; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + if (packetNumber > largest) return false; + smallest = largest - ackRange.range; + if (packetNumber >= smallest) return true; + } + return false; + } + + private static boolean isRangeAcknowledged(long largestAcknowledged, + List ackRanges, + long first, + long last) { + assert last >= first; + if (last > largestAcknowledged) return false; + var largest = largestAcknowledged; + long smallest = largestAcknowledged + 2; + for (var ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + if (last > largest) return false; + smallest = largest - ackRange.range; + if (first >= smallest) return true; + } + return false; + } + + // This is described in RFC 9000, Section 19.3.1 ACK Ranges + // https://www.rfc-editor.org/rfc/rfc9000#name-ack-ranges + private static long smallestAcknowledged(long largestAcknowledged, + List ackRanges) { + long largest = largestAcknowledged; + long smallest = largest + 2; + assert !ackRanges.isEmpty(); + for (AckRange ackRange : ackRanges) { + largest = smallest - ackRange.gap - 2; + smallest = largest - ackRange.range; + } + return smallest; + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java new file mode 100644 index 00000000000..5e0f2abd8df --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ConnectionCloseFrame.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * A CONNECTION_CLOSE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class ConnectionCloseFrame extends QuicFrame { + + /** + * This variant indicates an error originating from the higher + * level protocol, for instance, HTTP/3. + */ + public static final int CONNECTION_CLOSE_VARIANT = 0x1d; + private final long errorCode; + private final long errorFrameType; + private final boolean variant; + private final byte[] reason; + private String cachedToString; + private String cachedReason; + + /** + * An immutable ConnectionCloseFrame of type 0x1c with no reason phrase + * and an error of type APPLICATION_ERROR. + * @apiNote + * From + * RFC 9000 - section 10.2.3: + *

        + * A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE + * of type 0x1c when sending the frame in Initial or Handshake packets. + * Otherwise, information about the application state might be revealed. + * Endpoints MUST clear the value of the Reason Phrase field and SHOULD + * use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE + * of type 0x1c. + *
        + */ + public static final ConnectionCloseFrame APPLICATION_ERROR = + new ConnectionCloseFrame(QuicTransportErrors.APPLICATION_ERROR.code(), 0,""); + + /** + * Incoming CONNECTION_CLOSE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + ConnectionCloseFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(CONNECTION_CLOSE); + errorCode = decodeVLField(buffer, "errorCode"); + if (type == CONNECTION_CLOSE) { + variant = false; + errorFrameType = decodeVLField(buffer, "errorFrameType"); + } else { + assert type == CONNECTION_CLOSE_VARIANT; + errorFrameType = -1; + variant = true; + } + int reasonLength = decodeVLFieldAsInt(buffer, "reasonLength"); + validateRemainingLength(buffer, reasonLength, type); + reason = new byte[reasonLength]; + buffer.get(reason, 0, reasonLength); + } + + /** + * Outgoing CONNECTION_CLOSE frame (variant with errorFrameType - 0x1c). + * This indicates a {@linkplain jdk.internal.net.quic.QuicTransportErrors + * quic transport error}. + */ + public ConnectionCloseFrame(long errorCode, long errorFrameType, String reason) { + super(CONNECTION_CLOSE); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.errorFrameType = requireVLRange(errorFrameType, "errorFrameType"); + this.variant = false; + this.cachedReason = reason; + this.reason = getReasonBytes(reason); + } + + /** + * Outgoing CONNECTION_CLOSE frame (variant without errorFrameType). + * This indicates an error originating from the higher level protocol, + * for instance {@linkplain jdk.internal.net.http.http3.Http3Error HTTP/3}. + */ + public ConnectionCloseFrame(long errorCode, String reason) { + super(CONNECTION_CLOSE); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.errorFrameType = -1; + this.variant = true; + this.cachedReason = reason; + this.reason = getReasonBytes(reason); + } + + private static byte[] getReasonBytes(String reason) { + return reason != null ? + reason.getBytes(StandardCharsets.UTF_8) : + new byte[0]; + } + + /** + * {@return a ConnectionCloseFrame suitable for inclusion in + * an Initial or Handshake packet} + */ + public ConnectionCloseFrame clearApplicationState() { + return this.variant ? APPLICATION_ERROR : this; + } + + @Override + public long getTypeField() { + return variant ? CONNECTION_CLOSE_VARIANT : CONNECTION_CLOSE; + } + + @Override + public boolean isAckEliciting() { + return false; + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, errorCode, "errorCode"); + if (!variant) { + encodeVLField(buffer, errorFrameType, "errorFrameType"); + } + encodeVLField(buffer, reason.length, "reasonLength"); + if (reason.length > 0) { + buffer.put(reason); + } + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(errorCode) + + (variant ? 0 : getVLFieldLengthFor(errorFrameType)) + + getVLFieldLengthFor(reason.length) + + reason.length; + } + + public long errorCode() { + return errorCode; + } + + public long errorFrameType() { + return errorFrameType; + } + + public boolean variant() { + return variant; + } + + public boolean isQuicTransportCode() { + return !variant; + } + + public boolean isApplicationCode() { + return variant; + } + + public byte[] reason() { + return reason; + } + + public String reasonString() { + if (cachedReason != null) return cachedReason; + if (reason == null) return null; + if (reason.length == 0) return ""; + return cachedReason = new String(reason, StandardCharsets.UTF_8); + } + + @Override + public String toString() { + if (cachedToString == null) { + final StringBuilder sb = new StringBuilder("ConnectionCloseFrame[type=0x"); + final long type = getTypeField(); + sb.append(Long.toHexString(type)) + .append(", errorCode=0x").append(Long.toHexString(errorCode)); + // CRYPTO_ERROR codes ranging 0x0100-0x01ff + if (type == 0x1c) { + if (errorCode >= 0x0100 && errorCode <= 0x01ff) { + // this represents a CRYPTO_ERROR which as per RFC-9001, section 4.8: + // A TLS alert is converted into a QUIC connection error. The AlertDescription + // value is added to 0x0100 to produce a QUIC error code from the range reserved for + // CRYPTO_ERROR; ... The resulting value is sent in a QUIC CONNECTION_CLOSE + // frame of type 0x1c + + // find the tls alert code from the error code, by substracting 0x0100 from + // the error code + sb.append(", tlsAlertDescription=").append(errorCode - 0x0100); + } + sb.append(", errorFrameType=0x").append(Long.toHexString(errorFrameType)); + } + if (cachedReason == null) { + cachedReason = new String(reason, StandardCharsets.UTF_8); + } + sb.append(", reason=").append(cachedReason).append("]"); + + cachedToString = sb.toString(); + } + return cachedToString; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java new file mode 100644 index 00000000000..7b606527193 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/CryptoFrame.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A CRYPTO Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class CryptoFrame extends QuicFrame { + + private final long offset; + private final int length; + private final ByteBuffer cryptoData; + + CryptoFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(CRYPTO); + offset = decodeVLField(buffer, "offset"); + length = decodeVLFieldAsInt(buffer, "length"); + if (offset + length > VariableLengthEncoder.MAX_ENCODED_INTEGER) { + throw new QuicTransportException("Maximum crypto offset exceeded", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length, type); + int pos = buffer.position(); + // The buffer is the datagram: we will make a copy if the datagram + // is larger than the crypto frame by 64 bytes. + cryptoData = Utils.sliceOrCopy(buffer, pos, length, 64); + buffer.position(pos + length); + } + + /** + * Creates CryptoFrame + */ + public CryptoFrame(long offset, int length, ByteBuffer cryptoData) { + this(offset, length, cryptoData, true); + } + + private CryptoFrame(long offset, int length, ByteBuffer cryptoData, boolean slice) + { + super(CRYPTO); + this.offset = requireVLRange(offset, "offset"); + if (length != cryptoData.remaining()) + throw new IllegalArgumentException("bad length: " + length); + this.length = length; + this.cryptoData = slice + ? cryptoData.slice(cryptoData.position(), length) + : cryptoData; + } + + /** + * Creates a new CryptoFrame which is a slice of this crypto frame. + * @param offset the new offset + * @param length the new length + * @return a slice of the current crypto frame + * @throws IndexOutOfBoundsException if the offset or length + * exceed the bounds of this crypto frame + */ + public CryptoFrame slice(long offset, int length) { + long offsetdiff = offset - offset(); + long oldlen = length(); + Objects.checkFromIndexSize(offsetdiff, length, oldlen); + int pos = cryptoData.position(); + // safe cast to int since offsetdiff < length + int newpos = Math.addExact(pos, (int)offsetdiff); + ByteBuffer slice = Utils.sliceOrCopy(cryptoData, newpos, length); + return new CryptoFrame(offset, length, slice, false); + } + + @Override + public void encode(ByteBuffer dest) { + if (size() > dest.remaining()) { + throw new BufferOverflowException(); + } + int pos = dest.position(); + encodeVLField(dest, CRYPTO, "type"); + encodeVLField(dest, offset, "offset"); + encodeVLField(dest, length, "length"); + assert cryptoData.remaining() == length; + putByteBuffer(dest, cryptoData); + assert dest.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(CRYPTO) + + getVLFieldLengthFor(offset) + + getVLFieldLengthFor(length) + + length; + } + + /** + * {@return the frame offset} + */ + public long offset() { + return offset; + } + + public int length() { + return length; + } + + /** + * {@return the frame payload} + */ + public ByteBuffer payload() { + return cryptoData.slice(); + } + + @Override + public String toString() { + return "CryptoFrame(" + + "offset=" + offset + + ", length=" + length + + ')'; + } + + public static int compareOffsets(CryptoFrame cf1, CryptoFrame cf2) { + return Long.compare(cf1.offset, cf2.offset); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java new file mode 100644 index 00000000000..b008e643cda --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/DataBlockedFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A DATA_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class DataBlockedFrame extends QuicFrame { + + private final long maxData; + + /** + * Incoming DATA_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + DataBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(DATA_BLOCKED); + maxData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing DATA_BLOCKED frame + */ + public DataBlockedFrame(long maxData) { + super(DATA_BLOCKED); + this.maxData = requireVLRange(maxData, "maxData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, DATA_BLOCKED, "type"); + encodeVLField(buffer, maxData, "maxData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxData() { + return maxData; + } + + @Override + public int size() { + return getVLFieldLengthFor(DATA_BLOCKED) + + getVLFieldLengthFor(maxData); + } + + @Override + public String toString() { + return "DataBlockedFrame(" + + "maxData=" + maxData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java new file mode 100644 index 00000000000..ffe6aff2f0d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/HandshakeDoneFrame.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A HANDSHAKE_DONE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class HandshakeDoneFrame extends QuicFrame { + + /** + * Incoming HANDSHAKE_DONE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + */ + HandshakeDoneFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(HANDSHAKE_DONE); + } + + /** + * Outgoing HANDSHAKE_DONE frame + */ + public HandshakeDoneFrame() { + super(HANDSHAKE_DONE); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, HANDSHAKE_DONE, "type"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(HANDSHAKE_DONE); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java new file mode 100644 index 00000000000..26720c34494 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxDataFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RESET_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxDataFrame extends QuicFrame { + + private final long maxData; + + /** + * Incoming MAX_DATA frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxDataFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_DATA); + maxData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing MAX_DATA frame + */ + public MaxDataFrame(long maxData) { + super(MAX_DATA); + this.maxData = requireVLRange(maxData, "maxData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, MAX_DATA, "type"); + encodeVLField(buffer, maxData, "maxData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxData() { + return maxData; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_DATA) + + getVLFieldLengthFor(maxData); + } + + @Override + public String toString() { + return "MaxDataFrame(" + + "maxData=" + maxData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java new file mode 100644 index 00000000000..3fff70a377c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamDataFrame.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A MAX_STREAM_DATA Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxStreamDataFrame extends QuicFrame { + + private final long streamID; + private final long maxStreamData; + + /** + * Incoming MAX_STREAM_DATA frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxStreamDataFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_STREAM_DATA); + streamID = decodeVLField(buffer, "streamID"); + maxStreamData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing MAX_STREAM_DATA frame + */ + public MaxStreamDataFrame(long streamID, long maxStreamData) { + super(MAX_STREAM_DATA); + this.streamID = requireVLRange(streamID, "streamID"); + this.maxStreamData = requireVLRange(maxStreamData, "maxStreamData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, MAX_STREAM_DATA, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, maxStreamData, "maxStreamData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreamData() { + return maxStreamData; + } + + public long streamID() { + return streamID; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_STREAM_DATA) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(maxStreamData); + } + + @Override + public String toString() { + return "MaxStreamDataFrame(" + + "streamId=" + streamID + + ", maxStreamData=" + maxStreamData + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java new file mode 100644 index 00000000000..e35b16195a6 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/MaxStreamsFrame.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A MAX_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class MaxStreamsFrame extends QuicFrame { + static final long MAX_VALUE = 1L << 60; + + private final long maxStreams; + private final boolean bidi; + + /** + * Incoming MAX_STREAMS frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + MaxStreamsFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(MAX_STREAMS); + bidi = (type == MAX_STREAMS); + maxStreams = decodeVLField(buffer, "maxStreams"); + if (maxStreams > MAX_VALUE) { + throw new QuicTransportException("Invalid maximum streams", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * Outgoing MAX_STREAMS frame + */ + public MaxStreamsFrame(boolean bidi, long maxStreams) { + super(MAX_STREAMS); + this.bidi = bidi; + this.maxStreams = requireVLRange(maxStreams, "maxStreams"); + } + + @Override + public long getTypeField() { + return MAX_STREAMS + (bidi?0:1); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, maxStreams, "maxStreams"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreams() { + return maxStreams; + } + + public boolean isBidi() { + return bidi; + } + + @Override + public int size() { + return getVLFieldLengthFor(MAX_STREAMS) + + getVLFieldLengthFor(maxStreams); + } + + @Override + public String toString() { + return "MaxStreamsFrame(bidi=" + bidi + + ", maxStreams=" + maxStreams + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java new file mode 100644 index 00000000000..87e91b21e75 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewConnectionIDFrame.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A NEW_CONNECTION_ID Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class NewConnectionIDFrame extends QuicFrame { + + private final long sequenceNumber; + private final long retirePriorTo; + private final ByteBuffer connectionId; + private final ByteBuffer statelessResetToken; + + /** + * Incoming NEW_CONNECTION_ID frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + NewConnectionIDFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(NEW_CONNECTION_ID); + sequenceNumber = decodeVLField(buffer, "sequenceNumber"); + retirePriorTo = decodeVLField(buffer, "retirePriorTo"); + if (retirePriorTo > sequenceNumber) { + throw new QuicTransportException("Invalid retirePriorTo", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, 17, type); + int length = Byte.toUnsignedInt(buffer.get()); + if (length < 1 || length > 20) { + throw new QuicTransportException("Invalid connection ID", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length + 16, type); + int position = buffer.position(); + connectionId = buffer.slice(position, length); + position += length; + statelessResetToken = buffer.slice(position, 16); + position += 16; + buffer.position(position); + } + + /** + * Outgoing NEW_CONNECTION_ID frame + */ + public NewConnectionIDFrame(long sequenceNumber, long retirePriorTo, ByteBuffer connectionId, ByteBuffer statelessResetToken) { + super(NEW_CONNECTION_ID); + this.sequenceNumber = requireVLRange(sequenceNumber, "sequenceNumber"); + this.retirePriorTo = requireVLRange(retirePriorTo, "retirePriorTo"); + int length = connectionId.remaining(); + if (length < 1 || length > 20) + throw new IllegalArgumentException("invalid length"); + this.connectionId = connectionId.slice(); + if (statelessResetToken.remaining() != 16) + throw new IllegalArgumentException("stateless reset token must be 16 bytes"); + this.statelessResetToken = statelessResetToken.slice(); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, NEW_CONNECTION_ID, "type"); + encodeVLField(buffer, sequenceNumber, "sequenceNumber"); + encodeVLField(buffer, retirePriorTo, "retirePriorTo"); + int length = connectionId.remaining(); + buffer.put((byte)length); + putByteBuffer(buffer, connectionId); + putByteBuffer(buffer, statelessResetToken); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(NEW_CONNECTION_ID) + + getVLFieldLengthFor(sequenceNumber) + + getVLFieldLengthFor(retirePriorTo) + + 1 // connection length + + connectionId.remaining() + + statelessResetToken.remaining(); + } + + public long sequenceNumber() { + return sequenceNumber; + } + + public long retirePriorTo() { + return retirePriorTo; + } + + public ByteBuffer connectionId() { + return connectionId; + } + + public ByteBuffer statelessResetToken() { + return statelessResetToken; + } + + @Override + public String toString() { + return "NewConnectionIDFrame(seqNumber=" + sequenceNumber + + ", retirePriorTo=" + retirePriorTo + + ", connIdLength=" + connectionId.remaining() + + ")"; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java new file mode 100644 index 00000000000..08bc72ff1c2 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/NewTokenFrame.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A NEW_TOKEN frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class NewTokenFrame extends QuicFrame { + private final byte[] token; + + /** + * Incoming NEW_TOKEN frame + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + NewTokenFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(NEW_TOKEN); + int length = decodeVLFieldAsInt(buffer, "token length"); + if (length == 0) { + throw new QuicTransportException("Empty token", + null, type, + QuicTransportErrors.FRAME_ENCODING_ERROR); + } + validateRemainingLength(buffer, length, type); + final byte[] t = new byte[length]; + buffer.get(t); + this.token = t; + } + + /** + * Outgoing NEW_TOKEN frame whose token is the given ByteBuffer + * (position to limit) + */ + public NewTokenFrame(final ByteBuffer tokenBuf) { + super(NEW_TOKEN); + Objects.requireNonNull(tokenBuf); + final int length = tokenBuf.remaining(); + if (length <= 0) { + throw new IllegalArgumentException("Invalid token length"); + } + final byte[] t = new byte[length]; + tokenBuf.get(t); + this.token = t; + } + + @Override + public void encode(final ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, NEW_TOKEN, "type"); + encodeVLField(buffer, token.length, "token length"); + buffer.put(token); + assert buffer.position() - pos == size(); + } + + public byte[] token() { + return this.token; + } + + @Override + public int size() { + final int tokenLength = token.length; + return getVLFieldLengthFor(NEW_TOKEN) + + getVLFieldLengthFor(tokenLength) + + tokenLength; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java new file mode 100644 index 00000000000..d0258aee84e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PaddingFrame.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * PaddingFrames. Since padding frames comprise a single zero byte + * this class actually represents sequences of PaddingFrames. + * When decoding, the class consumes all the zero bytes that are + * available and when encoding, the number of required padding bytes + * is specified. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PaddingFrame extends QuicFrame { + + private final int size; + + /** + * Incoming + */ + PaddingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PADDING); + int count = 1; + while (buffer.hasRemaining()) { + if (buffer.get() == 0) { + count++; + } else { + int pos = buffer.position(); + buffer.position(pos - 1); + break; + } + } + size = count; + } + + /** + * Outgoing + * @param size the number of padding frames that should be written + * to the buffer. Each frame is one byte long. + */ + public PaddingFrame(int size) { + super(PADDING); + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than zero"); + } + this.size = size; + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + for (int i=0; i buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PATH_CHALLENGE, "type"); + putByteBuffer(buffer, data); + assert buffer.position() - pos == size(); + } + + /** + */ + public ByteBuffer data() { + return data; + } + + @Override + public int size() { + return getVLFieldLengthFor(PATH_CHALLENGE) + LENGTH; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java new file mode 100644 index 00000000000..2b1edcfe731 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PathResponseFrame.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A PATH_RESPONSE Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PathResponseFrame extends QuicFrame { + + public static final int LENGTH = 8; + private final ByteBuffer data; + + /** + * Incoming PATH_RESPONSE frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + PathResponseFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PATH_RESPONSE); + validateRemainingLength(buffer, LENGTH, type); + int position = buffer.position(); + data = buffer.slice(position, LENGTH); + buffer.position(position + LENGTH); + } + + /** + * Outgoing PATH_RESPONSE frame + */ + public PathResponseFrame(ByteBuffer data) { + super(PATH_RESPONSE); + if (data.remaining() != LENGTH) + throw new IllegalArgumentException("response data must be 8 bytes"); + this.data = data.slice(); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PATH_RESPONSE, "type"); + putByteBuffer(buffer, data); + assert buffer.position() - pos == size(); + } + + /** + */ + public ByteBuffer data() { + return data; + } + + @Override + public int size() { + return getVLFieldLengthFor(PATH_RESPONSE) + LENGTH; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java new file mode 100644 index 00000000000..9717a3c31d1 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/PingFrame.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A PING frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class PingFrame extends QuicFrame { + + PingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(PING); + } + + /** + */ + public PingFrame() { + super(PING); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, PING, "type"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(PING); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java new file mode 100644 index 00000000000..f4a230452d4 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/QuicFrame.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.ByteBuffer; + +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A QUIC Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public abstract sealed class QuicFrame permits + AckFrame, + DataBlockedFrame, + ConnectionCloseFrame, CryptoFrame, + HandshakeDoneFrame, + MaxDataFrame, MaxStreamDataFrame, MaxStreamsFrame, + NewConnectionIDFrame, NewTokenFrame, + PaddingFrame, PathChallengeFrame, PathResponseFrame, PingFrame, + ResetStreamFrame, RetireConnectionIDFrame, + StreamsBlockedFrame, StreamDataBlockedFrame, StreamFrame, StopSendingFrame { + + public static final long MAX_VL_INTEGER = (1L << 62) - 1; + /** + * Frame types + */ + public static final int PADDING=0x00; + public static final int PING=0x01; + public static final int ACK=0x02; + public static final int RESET_STREAM=0x04; + public static final int STOP_SENDING=0x05; + public static final int CRYPTO=0x06; + public static final int NEW_TOKEN=0x07; + public static final int STREAM=0x08; + public static final int MAX_DATA=0x10; + public static final int MAX_STREAM_DATA=0x11; + public static final int MAX_STREAMS=0x12; + public static final int DATA_BLOCKED=0x14; + public static final int STREAM_DATA_BLOCKED=0x15; + public static final int STREAMS_BLOCKED=0x16; + public static final int NEW_CONNECTION_ID=0x18; + public static final int RETIRE_CONNECTION_ID=0x19; + public static final int PATH_CHALLENGE=0x1a; + public static final int PATH_RESPONSE=0x1b; + public static final int CONNECTION_CLOSE=0x1c; + public static final int HANDSHAKE_DONE=0x1e; + private static final int MAX_KNOWN_FRAME_TYPE = HANDSHAKE_DONE; + private final int frameType; + + /** + * Concrete Frame types normally have two constructors which call this + * + * XXXFrame(ByteBuffer, int firstByte) which is called for incoming frames + * after reading the first byte to determine the type. The firstByte is also + * supplied to the constructor because it can contain additional state information + * + * XXXFrame(...) which is called to instantiate outgoing frames + * @param type the first byte of the frame, which encodes the frame type. + */ + QuicFrame(int type) { + frameType = type; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + + /** + * decode given ByteBuffer and return a QUICFrame + */ + public static QuicFrame decode(ByteBuffer buffer) throws QuicTransportException { + long frameTypeLong = VariableLengthEncoder.decode(buffer); + if (frameTypeLong < 0) { + throw new QuicTransportException("Error decoding frame type", + null, 0, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + if (frameTypeLong > Integer.MAX_VALUE) { + throw new QuicTransportException("Unrecognized frame", + null, frameTypeLong, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + int frameType = (int)frameTypeLong; + var frame = switch (maskType(frameType)) { + case ACK -> new AckFrame(buffer, frameType); + case STREAM -> new StreamFrame(buffer, frameType); + case RESET_STREAM -> new ResetStreamFrame(buffer, frameType); + case PADDING -> new PaddingFrame(buffer, frameType); + case PING -> new PingFrame(buffer, frameType); + case STOP_SENDING -> new StopSendingFrame(buffer, frameType); + case CRYPTO -> new CryptoFrame(buffer, frameType); + case NEW_TOKEN -> new NewTokenFrame(buffer, frameType); + case DATA_BLOCKED -> new DataBlockedFrame(buffer, frameType); + case MAX_DATA -> new MaxDataFrame(buffer, frameType); + case MAX_STREAMS -> new MaxStreamsFrame(buffer, frameType); + case MAX_STREAM_DATA -> new MaxStreamDataFrame(buffer, frameType); + case STREAM_DATA_BLOCKED -> new StreamDataBlockedFrame(buffer, frameType); + case STREAMS_BLOCKED -> new StreamsBlockedFrame(buffer, frameType); + case NEW_CONNECTION_ID -> new NewConnectionIDFrame(buffer, frameType); + case RETIRE_CONNECTION_ID -> new RetireConnectionIDFrame(buffer, frameType); + case PATH_CHALLENGE -> new PathChallengeFrame(buffer, frameType); + case PATH_RESPONSE -> new PathResponseFrame(buffer, frameType); + case CONNECTION_CLOSE -> new ConnectionCloseFrame(buffer, frameType); + case HANDSHAKE_DONE -> new HandshakeDoneFrame(buffer, frameType); + default -> throw new QuicTransportException("Unrecognized frame", + null, frameType, QuicTransportErrors.FRAME_ENCODING_ERROR); + }; + assert frameClassOf(maskType(frameType)) == frame.getClass(); + assert frameTypeOf(frame.getClass()) == maskType(frameType); + assert frame.getTypeField() == frameType : "frame type mismatch: " + + frameType + "!=" + frame.getTypeField() + + " for frame: " + frame; + return frame; + } + + public static Class frameClassOf(int frameType) { + return switch (maskType(frameType)) { + case ACK -> AckFrame.class; + case STREAM -> StreamFrame.class; + case RESET_STREAM -> ResetStreamFrame.class; + case PADDING -> PaddingFrame.class; + case PING -> PingFrame.class; + case STOP_SENDING -> StopSendingFrame.class; + case CRYPTO -> CryptoFrame.class; + case NEW_TOKEN -> NewTokenFrame.class; + case DATA_BLOCKED -> DataBlockedFrame.class; + case MAX_DATA -> MaxDataFrame.class; + case MAX_STREAMS -> MaxStreamsFrame.class; + case MAX_STREAM_DATA -> MaxStreamDataFrame.class; + case STREAM_DATA_BLOCKED -> StreamDataBlockedFrame.class; + case STREAMS_BLOCKED -> StreamsBlockedFrame.class; + case NEW_CONNECTION_ID -> NewConnectionIDFrame.class; + case RETIRE_CONNECTION_ID -> RetireConnectionIDFrame.class; + case PATH_CHALLENGE -> PathChallengeFrame.class; + case PATH_RESPONSE -> PathResponseFrame.class; + case CONNECTION_CLOSE -> ConnectionCloseFrame.class; + case HANDSHAKE_DONE -> HandshakeDoneFrame.class; + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + } + + public static int frameTypeOf(Class frameClass) { + // we don't have class pattern matching yet - so switch + // on the class name instead + return switch (frameClass.getSimpleName()) { + case "AckFrame" -> ACK; + case "StreamFrame" -> STREAM; + case "ResetStreamFrame" -> RESET_STREAM; + case "PaddingFrame" -> PADDING; + case "PingFrame" -> PING; + case "StopSendingFrame" -> STOP_SENDING; + case "CryptoFrame" -> CRYPTO; + case "NewTokenFrame" -> NEW_TOKEN; + case "DataBlockedFrame" -> DATA_BLOCKED; + case "MaxDataFrame" -> MAX_DATA; + case "MaxStreamsFrame" -> MAX_STREAMS; + case "MaxStreamDataFrame" -> MAX_STREAM_DATA; + case "StreamDataBlockedFrame" -> STREAM_DATA_BLOCKED; + case "StreamsBlockedFrame" -> STREAMS_BLOCKED; + case "NewConnectionIDFrame" -> NEW_CONNECTION_ID; + case "RetireConnectionIDFrame" -> RETIRE_CONNECTION_ID; + case "PathChallengeFrame" -> PATH_CHALLENGE; + case "PathResponseFrame" -> PATH_RESPONSE; + case "ConnectionCloseFrame" -> CONNECTION_CLOSE; + case "HandshakeDoneFrame" -> HANDSHAKE_DONE; + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + } + + /** + * Writes src to dest, preserving position in src + */ + protected static void putByteBuffer(ByteBuffer dest, ByteBuffer src) { + dest.put(src.asReadOnlyBuffer()); + } + + /** + * Throws a QuicTransportException if the given buffer does not have enough bytes + * to finish decoding the frame + * + * @param buffer source buffer + * @param expected minimum number of bytes required + * @param type frame type to include in exception + * @throws QuicTransportException if the buffer is shorter than {@code expected} + */ + protected static void validateRemainingLength(ByteBuffer buffer, int expected, long type) + throws QuicTransportException + { + if (buffer.remaining() < expected) { + throw new QuicTransportException("Error decoding frame", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * depending on the frame type, additional bits can be encoded + * in frameType(). This masks them out to return a unique value + * for each frame type. + */ + private static int maskType(int type) { + if (type >= ACK && type < RESET_STREAM) + return ACK; + if (type >= STREAM && type < MAX_DATA) + return STREAM; + if (type >= MAX_STREAMS && type < DATA_BLOCKED) + return MAX_STREAMS; + if (type >= STREAMS_BLOCKED && type < NEW_CONNECTION_ID) + return STREAMS_BLOCKED; + if (type >= CONNECTION_CLOSE && type < HANDSHAKE_DONE) + return CONNECTION_CLOSE; + // all others are unique + return type; + } + + /** + * {@return true if this frame is ACK-eliciting} + * A frame is ACK-eliciting if it is anything + * other than {@link QuicFrame#ACK}, + * {@link QuicFrame#PADDING} or + * {@link QuicFrame#CONNECTION_CLOSE} + * (or its variant). + */ + public boolean isAckEliciting() { return true; } + + /** + * {@return the minimum number of bytes needed to encode this frame} + */ + public abstract int size(); + + protected final long decodeVLField(ByteBuffer buffer, String name) throws QuicTransportException { + long v = VariableLengthEncoder.decode(buffer); + if (v < 0) { + throw new QuicTransportException("Error decoding field: " + name, + null, getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + return v; + } + + protected final int decodeVLFieldAsInt(ByteBuffer buffer, String name) throws QuicTransportException { + long l = decodeVLField(buffer, name); + int intval = (int)l; + if (((long)intval) != l) { + throw new QuicTransportException(name + ":field too long", + null, getTypeField(), QuicTransportErrors.FRAME_ENCODING_ERROR); + } + return intval; + } + + protected static int requireVLRange(int val, String message) { + if (val < 0) { + throw new IllegalArgumentException(message + " " + val + " not in range"); + } + return val; + } + + protected static long requireVLRange(long val, String fieldName) { + if (val < 0 || val > MAX_VL_INTEGER) { + throw new IllegalArgumentException( + String.format("%s not in VL range: %s", fieldName, val)); + } + return val; + } + + protected static void encodeVLField(ByteBuffer buffer, long val, String name) { + try { + VariableLengthEncoder.encode(buffer, val); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error encoding " + name, e); + } + } + + protected static int getVLFieldLengthFor(long val) { + return VariableLengthEncoder.getEncodedSize(val); + } + + /** + * The type of this frame, ie. one of values above, which means it + * excludes the additional information that is encoded into the first field + * of some QUIC frames. That additional info has to be maintained by the sub + * class and used by its encode() method to generate the first field for outgoing frames. + * + * see maskType() below + */ + public int frameType() { + return frameType; + } + + /** + * Encode this QUIC Frame into given ByteBuffer + */ + public abstract void encode(ByteBuffer buffer); + + /** + * {@return the type field that was / should be encoded} + * This is the {@linkplain #frameType() frame type} with + * possibly some additional bits set, depending on the + * frame. + * @implSpec + * The default implementation of this method is to return + * {@link #frameType()}. + */ + public long getTypeField() { return frameType(); } + + /** + * Tells whether this particular frame is valid in the given + * packet type. + * + *

        From + * RFC 9000, section 12.5. Frames and Number Spaces: + *

        + * Some frames are prohibited in different packet number space + * The rules here generalize those of TLS, in that frames associated + * with establishing the connection can usually appear in packets + * in any packet number space, whereas those associated with transferring + * data can only appear in the application data packet number space: + * + *
          + *
        • PADDING, PING, and CRYPTO frames MAY appear in any packet number + * space.
        • + *
        • CONNECTION_CLOSE frames signaling errors at the QUIC layer (type 0x1c) + * MAY appear in any packet number space.
        • + *
        • CONNECTION_CLOSE frames signaling application errors (type 0x1d) + * MUST only appear in the application data packet number space. + *
        • ACK frames MAY appear in any packet number space but can only + * acknowledge packets that appeared in that packet number space. + * However, as noted below, 0-RTT packets cannot contain ACK frames.
        • + *
        • All other frame types MUST only be sent in the application data + * packet number space.
        • + *
        + * + * Note that it is not possible to send the following frames in 0-RTT + * packets for various reasons: ACK, CRYPTO, HANDSHAKE_DONE, NEW_TOKEN, + * PATH_RESPONSE, and RETIRE_CONNECTION_ID. A server MAY treat receipt + * of these frames in 0-RTT packets as a connection error of + * type PROTOCOL_VIOLATION. + *
        + * + * @param packetType the packet type + * @return true if the frame can be embedded in a packet of that type + */ + public boolean isValidIn(QuicPacket.PacketType packetType) { + return switch (frameType) { + case PADDING, PING -> true; + case ACK, CRYPTO -> switch (packetType) { + case VERSIONS, ZERORTT -> false; + default -> true; + }; + case CONNECTION_CLOSE -> { + if ((getTypeField() & 0x1D) == 0x1C) yield true; + yield QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + } + case HANDSHAKE_DONE, NEW_TOKEN, PATH_RESPONSE, + RETIRE_CONNECTION_ID -> switch (packetType) { + case ZERORTT -> false; + default -> QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + }; + default -> QuicPacket.PacketNumberSpace.of(packetType) == QuicPacket.PacketNumberSpace.APPLICATION; + }; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java new file mode 100644 index 00000000000..7a3579fc831 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/ResetStreamFrame.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RESET_STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class ResetStreamFrame extends QuicFrame { + + private final long streamID; + private final long errorCode; + private final long finalSize; + + ResetStreamFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(RESET_STREAM); + streamID = decodeVLField(buffer, "streamID"); + errorCode = decodeVLField(buffer, "errorCode"); + finalSize = decodeVLField(buffer, "finalSize"); + } + + /** + */ + public ResetStreamFrame( + long streamID, + long errorCode, + long finalSize) + { + super(RESET_STREAM); + this.streamID = requireVLRange(streamID, "streamID"); + this.errorCode = requireVLRange(errorCode, "errorCode"); + this.finalSize = requireVLRange(finalSize, "finalSize"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, RESET_STREAM, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, errorCode, "errorCode"); + encodeVLField(buffer, finalSize, "finalSize"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long streamId() { + return streamID; + } + + /** + */ + public long errorCode() { + return errorCode; + } + + /** + */ + public long finalSize() { + return finalSize; + } + + @Override + public int size() { + return getVLFieldLengthFor(RESET_STREAM) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(errorCode) + + getVLFieldLengthFor(finalSize); + } + + @Override + public String toString() { + return "ResetStreamFrame(stream=" + streamID + + ", errorCode=" + errorCode + + ", finalSize=" + finalSize + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java new file mode 100644 index 00000000000..bf448f6a301 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/RetireConnectionIDFrame.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A RETIRE_CONNECTION_ID Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class RetireConnectionIDFrame extends QuicFrame { + + private final long sequenceNumber; + + /** + * Incoming RETIRE_CONNECTION_ID frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + RetireConnectionIDFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(RETIRE_CONNECTION_ID); + sequenceNumber = decodeVLField(buffer, "sequenceNumber"); + } + + /** + * Outgoing RETIRE_CONNECTION_ID frame + */ + public RetireConnectionIDFrame(long sequenceNumber) { + super(RETIRE_CONNECTION_ID); + this.sequenceNumber = requireVLRange(sequenceNumber, "sequenceNumber"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, RETIRE_CONNECTION_ID, "type"); + encodeVLField(buffer, sequenceNumber, "sequenceNumber"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long sequenceNumber() { + return sequenceNumber; + } + + @Override + public int size() { + return getVLFieldLengthFor(RETIRE_CONNECTION_ID) + + getVLFieldLengthFor(sequenceNumber); + } + + @Override + public String toString() { + return "RetireConnectionIDFrame(" + + "sequenceNumber=" + sequenceNumber + + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java new file mode 100644 index 00000000000..4a7d6525685 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StopSendingFrame.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STOP_SENDING Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StopSendingFrame extends QuicFrame { + + private final long streamID; + private final long errorCode; + + StopSendingFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STOP_SENDING); + streamID = decodeVLField(buffer, "streamID"); + errorCode = decodeVLField(buffer, "errorCode"); + } + + /** + */ + public StopSendingFrame(long streamID, long errorCode) { + super(STOP_SENDING); + this.streamID = requireVLRange(streamID, "streamID"); + this.errorCode = requireVLRange(errorCode, "errorCode"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, STOP_SENDING, "type"); + encodeVLField(buffer, streamID, "streamID"); + encodeVLField(buffer, errorCode, "errorCode"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long streamID() { + return streamID; + } + + /** + */ + public long errorCode() { + return errorCode; + } + + @Override + public int size() { + return getVLFieldLengthFor(STOP_SENDING) + + getVLFieldLengthFor(streamID) + + getVLFieldLengthFor(errorCode); + } + + @Override + public String toString() { + return "StopSendingFrame(stream=" + streamID + + ", errorCode=" + errorCode + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java new file mode 100644 index 00000000000..7dd95e2278b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamDataBlockedFrame.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STREAM_DATA_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamDataBlockedFrame extends QuicFrame { + + private final long streamId; + private final long maxStreamData; + + /** + * Incoming STREAM_DATA_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + StreamDataBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAM_DATA_BLOCKED); + assert type == STREAM_DATA_BLOCKED : "STREAM_DATA_BLOCKED, unexpected frame type 0x" + + Integer.toHexString(type); + streamId = decodeVLField(buffer, "streamID"); + maxStreamData = decodeVLField(buffer, "maxData"); + } + + /** + * Outgoing STREAM_DATA_BLOCKED frame + */ + public StreamDataBlockedFrame(long streamId, long maxStreamData) { + super(STREAM_DATA_BLOCKED); + this.streamId = requireVLRange(streamId, "streamID"); + this.maxStreamData = requireVLRange(maxStreamData, "maxStreamData"); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, STREAM_DATA_BLOCKED, "type"); + encodeVLField(buffer, streamId, "streamID"); + encodeVLField(buffer, maxStreamData, "maxStreamData"); + assert buffer.position() - pos == size(); + } + + /** + */ + public long maxStreamData() { + return maxStreamData; + } + + public long streamId() { + return streamId; + } + + @Override + public int size() { + return getVLFieldLengthFor(STREAM_DATA_BLOCKED) + + getVLFieldLengthFor(streamId) + + getVLFieldLengthFor(maxStreamData); + } + + @Override + public String toString() { + return "StreamDataBlockedFrame(" + + "streamId=" + streamId + + ", maxStreamData=" + maxStreamData + + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof StreamDataBlockedFrame that)) return false; + if (streamId != that.streamId) return false; + return maxStreamData == that.maxStreamData; + } + + @Override + public int hashCode() { + int result = (int) (streamId ^ (streamId >>> 32)); + result = 31 * result + (int) (maxStreamData ^ (maxStreamData >>> 32)); + return result; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java new file mode 100644 index 00000000000..b597e53c06c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamFrame.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.quic.QuicTransportException; + +/** + * A STREAM Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamFrame extends QuicFrame { + + // Flags in frameType() + private static final int OFF = 0x4; + private static final int LEN = 0x2; + private static final int FIN = 0x1; + + private final long streamID; + // true if the OFF bit in the type field has been set + private final boolean typeFieldHasOFF; + private final long offset; + private final int length; // -1 means consume all data in packet + private final int dataLength; + private final ByteBuffer streamData; + private final boolean fin; + + StreamFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAM); + streamID = decodeVLField(buffer, "streamID"); + if ((type & OFF) > 0) { + typeFieldHasOFF = true; + offset = decodeVLField(buffer, "offset"); + } else { + typeFieldHasOFF = false; + offset = 0; + } + if ((type & LEN) > 0) { + length = decodeVLFieldAsInt(buffer, "length"); + } else { + length = -1; + } + if (length == -1) { + int remaining = buffer.remaining(); + streamData = Utils.sliceOrCopy(buffer, buffer.position(), remaining); + buffer.position(buffer.limit()); + dataLength = remaining; + } else { + validateRemainingLength(buffer, length, type); + int pos = buffer.position(); + streamData = Utils.sliceOrCopy(buffer, pos, length); + buffer.position(pos + length); + dataLength = length; + } + fin = (type & FIN) == 1; + } + + /** + * Creates StreamFrame (length == -1 means no length specified in frame + * and is assumed to occupy the remainder of the Quic/UDP packet. + * If a length is specified then it must correspond with the remaining bytes + * in streamData + */ + // It would be interesting to have a version of this constructor that can take + // a list of ByteBuffer. + public StreamFrame(long streamID, long offset, int length, boolean fin, ByteBuffer streamData) { + this(streamID, offset, length, fin, streamData, true); + } + + private StreamFrame(long streamID, long offset, int length, boolean fin, ByteBuffer streamData, boolean slice) + { + super(STREAM); + this.streamID = requireVLRange(streamID, "streamID"); + this.offset = requireVLRange(offset, "offset"); + // if offset is non-zero then we mark that the type field has OFF bit set + // to allow for that bit to be set when encoding this frame + this.typeFieldHasOFF = this.offset != 0; + if (length != -1 && length != streamData.remaining()) { + throw new IllegalArgumentException("bad length"); + } + this.length = length; + this.dataLength = streamData.remaining(); + this.fin = fin; + this.streamData = slice + ? streamData.slice(streamData.position(), dataLength) + : streamData; + } + + /** + * Creates a new StreamFrame which is a slice of this stream frame. + * @param offset the new offset + * @param length the new length + * @return a slice of the current stream frame + * @throws IndexOutOfBoundsException if the offset or length + * exceed the bounds of this stream frame + */ + public StreamFrame slice(long offset, int length) { + long oldoffset = offset(); + long offsetdiff = offset - oldoffset; + long oldlen = dataLength(); + Objects.checkFromIndexSize(offsetdiff, length, oldlen); + int pos = streamData.position(); + // safe cast to int since offsetdiff < length + int newpos = Math.addExact(pos, (int)offsetdiff); + // preserves the FIN bit if set + boolean fin = this.fin && offset + length == oldoffset + oldlen; + ByteBuffer slice = Utils.sliceOrCopy(streamData, newpos, length); + return new StreamFrame(streamID, offset, length, fin, slice, false); + } + + /** + * {@return the stream id} + */ + public long streamId() { + return streamID; + } + + /** + * {@return whether this frame has a length} + * A frame that doesn't have a length must be the last + * frame in the packet. + */ + public boolean hasLength() { + return length != -1; + } + + /** + * {@return true if this is the last frame in the stream} + * The last frame has the FIN bit set. + */ + public boolean isLast() { return fin; } + + @Override + public long getTypeField() { + return STREAM | (hasLength() ? LEN : 0) + | (typeFieldHasOFF ? OFF : 0) + | (fin ? FIN : 0); + } + + @Override + public void encode(ByteBuffer dest) { + if (size() > dest.remaining()) { + throw new BufferOverflowException(); + } + int pos = dest.position(); + encodeVLField(dest, getTypeField(), "type"); + encodeVLField(dest, streamID, "streamID"); + if (typeFieldHasOFF) { + encodeVLField(dest, offset, "offset"); + } + if (hasLength()) { + encodeVLField(dest, length, "length"); + assert streamData.remaining() == length; + } + putByteBuffer(dest, streamData); + assert dest.position() - pos == size(); + } + + @Override + public int size() { + int size = getVLFieldLengthFor(getTypeField()) + + getVLFieldLengthFor(streamID); + if (typeFieldHasOFF) { + size += getVLFieldLengthFor(offset); + } + if (hasLength()) { + return size + getVLFieldLengthFor(length) + length; + } else { + return size + streamData.remaining(); + } + } + + /** + * {@return the frame payload} + */ + public ByteBuffer payload() { + return streamData.slice(); + } + + /** + * {@return the frame offset} + */ + public long offset() { return offset; } + + /** + * {@return the number of data bytes in the frame} + * @apiNote + * This is equivalent to calling {@code payload().remaining()}. + */ + public int dataLength() { + return dataLength; + } + + public static int compareOffsets(StreamFrame sf1, StreamFrame sf2) { + return Long.compare(sf1.offset, sf2.offset); + } + + /** + * Computes the header size that would be required to encode a frame with + * the given streamId, offset, and length. + * @apiNote + * This method is useful to figure out how many bytes can be allocated for + * the frame data, given a size constraint imposed by the space available + * for the whole datagram payload. + * @param encoder the {@code QuicPacketEncoder} - which can be used in case + * some part of the computation is Quic-version dependent. + * @param streamId the stream id + * @param offset the stream offset + * @param length the estimated length of the frame, typically this will be + * the min between the data available in the stream with respect + * to flow control, and the maximum remaining size for the datagram + * payload + * @return the estimated size of the header for a {@code StreamFrame} that would + * be created with the given parameters. + */ + public static int headerSize(QuicPacketEncoder encoder, long streamId, long offset, long length) { + // the header length is the size needed to encode the frame type, + // plus the size needed to encode the streamId, plus the size needed + // to encode the offset (if not 0) and the size needed to encode the + // length (if present) + int headerLength = getVLFieldLengthFor(STREAM | OFF | LEN | FIN) + + getVLFieldLengthFor(streamId); + if (offset != 0) headerLength += getVLFieldLengthFor(offset); + if (length >= 0) headerLength += getVLFieldLengthFor(length); + return headerLength; + } + + @Override + public String toString() { + return "StreamFrame(stream=" + streamID + + ", offset=" + offset + + ", length=" + length + + ", fin=" + fin + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java new file mode 100644 index 00000000000..69292ffbbc0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/frames/StreamsBlockedFrame.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.frames; + +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * A STREAMS_BLOCKED Frame + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public final class StreamsBlockedFrame extends QuicFrame { + + private final long maxStreams; + private final boolean bidi; + + /** + * Incoming STREAMS_BLOCKED frame returned by QuicFrame.decode() + * + * @param buffer + * @param type + * @throws QuicTransportException if the frame was malformed + */ + StreamsBlockedFrame(ByteBuffer buffer, int type) throws QuicTransportException { + super(STREAMS_BLOCKED); + bidi = (type == STREAMS_BLOCKED); + maxStreams = decodeVLField(buffer, "maxStreams"); + if (maxStreams > MaxStreamsFrame.MAX_VALUE) { + throw new QuicTransportException("Invalid maximum streams", + null, type, QuicTransportErrors.FRAME_ENCODING_ERROR); + } + } + + /** + * Outgoing STREAMS_BLOCKED frame + */ + public StreamsBlockedFrame(boolean bidi, long maxStreams) { + super(STREAMS_BLOCKED); + this.bidi = bidi; + this.maxStreams = requireVLRange(maxStreams, "maxStreams"); + } + + @Override + public long getTypeField() { + return STREAMS_BLOCKED + (bidi?0:1); + } + + @Override + public void encode(ByteBuffer buffer) { + if (size() > buffer.remaining()) { + throw new BufferOverflowException(); + } + int pos = buffer.position(); + encodeVLField(buffer, getTypeField(), "type"); + encodeVLField(buffer, maxStreams, "maxStreams"); + assert buffer.position() - pos == size(); + } + + @Override + public int size() { + return getVLFieldLengthFor(STREAMS_BLOCKED) + + getVLFieldLengthFor(maxStreams); + } + + /** + */ + public long maxStreams() { + return maxStreams; + } + + public boolean isBidi() { + return bidi; + } + + @Override + public String toString() { + return "StreamsBlockedFrame(bidi=" + bidi + + ", maxStreams=" + maxStreams + ')'; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java new file mode 100644 index 00000000000..dcdd040c0ed --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic; + +/** + *

        Internal classes for the Quic protocol implementation

        + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9002 + * RFC 9002: QUIC Loss Detection and Congestion Control + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java new file mode 100644 index 00000000000..a037f2e9387 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/HandshakePacket.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic Handshake Packets, as defined by + * RFC 9000, Section 17.2.4: + * + *
        {@code
        + *    A Handshake packet uses long headers with a type value of 0x02, followed
        + *    by the Length and Packet Number fields; see Section 17.2. The first byte
        + *    contains the Reserved and Packet Number Length bits; see Section 17.2.
        + *    It is used to carry cryptographic handshake messages and acknowledgments
        + *    from the server and client.
        + *
        + *    Handshake Packet {
        + *      Header Form (1) = 1,
        + *      Fixed Bit (1) = 1,
        + *      Long Packet Type (2) = 2,
        + *      Reserved Bits (2),
        + *      Packet Number Length (2),
        + *      Version (32),
        + *      Destination Connection ID Length (8),
        + *      Destination Connection ID (0..160),
        + *      Source Connection ID Length (8),
        + *      Source Connection ID (0..160),
        + *      Length (i),
        + *      Packet Number (8..32),
        + *      Packet Payload (..),
        + *    }
        + * }
        + * + *

        Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Handshake Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see + * RFC 9000, Section 17.2 + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface HandshakePacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.HANDSHAKE; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.HANDSHAKE; + } + + @Override + default boolean hasLength() { return true; } + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java new file mode 100644 index 00000000000..7be864c8b9c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/InitialPacket.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic Initial Packets, as defined by + * RFC 9000, Section 17.2.2: + * + *

        {@code
        + *    An Initial packet uses long headers with a type value of 0x00.
        + *    It carries the first CRYPTO frames sent by the client and server to perform
        + *    key exchange, and it carries ACK frames in either direction.
        + *
        + *    Initial Packet {
        + *      Header Form (1) = 1,
        + *      Fixed Bit (1) = 1,
        + *      Long Packet Type (2) = 0,
        + *      Reserved Bits (2),         # Protected
        + *      Packet Number Length (2),  # Protected
        + *      Version (32),
        + *      DCID Len (8),
        + *      Destination Connection ID (0..160),
        + *      SCID Len (8),
        + *      Source Connection ID (0..160),
        + *      Token Length (i),
        + *      Token (..),
        + *      Length (i),
        + *      Packet Number (8..32),     # Protected
        + *      # Protected Packet Payload (..)
        + *      Protected Payload (0..24), # Skipped Part
        + *      Protected Payload (128),   # Sampled Part
        + *      Protected Payload (..)     # Remainder
        + *    }
        + * }
        + * + *

        Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Initial Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface InitialPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.INITIAL; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.INITIAL; + } + + @Override + default boolean hasLength() { return true; } + + /** + * {@return the length of the token field, if present, 0 if not} + */ + int tokenLength(); + + /** + * {@return the token bytes, if present, {@code null} if not} + * + * From + * RFC 9000, Section 17.2.2: + * + *

        {@code
        +     *    The value of the token that was previously provided
        +     *    in a Retry packet or NEW_TOKEN frame; see Section 8.1.
        +     * }
        + * + * @see + * RFC 9000, Section 8.1 + */ + byte[] token(); + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); + + @Override + default String prettyPrint() { + return String.format("%s(pn:%s, size=%s, token[%s], frames:%s)", packetType(), packetNumber(), + size(), tokenLength(), frames()); + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java new file mode 100644 index 00000000000..9c9a6e6ef31 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeader.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.quic.QuicConnectionId; + +/** + * This class models Quic Long Header Packet header, as defined by + * RFC 8999, Section 5.1: + * + *
        {@code
        + *    Long Header Packet {
        + *       Header Form (1) = 1,
        + *       Version-Specific Bits (7),
        + *       Version (32),
        + *       Destination Connection ID Length (8),
        + *       Destination Connection ID (0..2040),
        + *       Source Connection ID Length (8),
        + *       Source Connection ID (0..2040),
        + *       Version-Specific Data (..),
        + *    }
        + * }
        + * + * @param version version + * @param destinationId Destination Connection ID + * @param sourceId Source Connection ID + * @param headerLength length in bytes of the packet header + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + */ +public record LongHeader(int version, + QuicConnectionId destinationId, + QuicConnectionId sourceId, + int headerLength) { +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java new file mode 100644 index 00000000000..960aef6530b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/LongHeaderPacket.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.quic.QuicConnectionId; + +/** + * This class models Quic Long Header Packets, as defined by + * RFC 8999, Section 5.1: + * + *
        {@code
        + *    Long Header Packet {
        + *       Header Form (1) = 1,
        + *       Version-Specific Bits (7),
        + *       Version (32),
        + *       Destination Connection ID Length (8),
        + *       Destination Connection ID (0..2040),
        + *       Source Connection ID Length (8),
        + *       Source Connection ID (0..2040),
        + *       Version-Specific Data (..),
        + *    }
        + * }
        + * + *

        Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface LongHeaderPacket extends QuicPacket { + @Override + default HeadersType headersType() { return HeadersType.LONG; } + + /** + * {@return the packet's source connection ID} + */ + QuicConnectionId sourceId(); + + /** + * {@return the Quic version of the packet} + */ + int version(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java new file mode 100644 index 00000000000..7df9f904a82 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/OneRttPacket.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic 1-RTT packets, as defined by + * RFC 9000, Section 17.3.1: + * + *

        {@code
        + *    A 1-RTT packet uses a short packet header. It is used after the
        + *    version and 1-RTT keys are negotiated.
        + *
        + *    1-RTT Packet {
        + *      Header Form (1) = 0,
        + *      Fixed Bit (1) = 1,
        + *      Spin Bit (1),
        + *      Reserved Bits (2),         # Protected
        + *      Key Phase (1),             # Protected
        + *      Packet Number Length (2),  # Protected
        + *      Destination Connection ID (0..160),
        + *      Packet Number (8..32),     # Protected
        + *      # Protected Packet Payload:
        + *      Protected Payload (0..24), # Skipped Part
        + *      Protected Payload (128),   # Sampled Part
        + *      Protected Payload (..),    # Remainder
        + *    }
        + * }
        + * + *

        Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Quic Version 2 uses the same 1-RTT packet structure than + * Quic Version 1. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface OneRttPacket extends ShortHeaderPacket { + + @Override + List frames(); + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.APPLICATION; + } + + @Override + default PacketType packetType() { + return PacketType.ONERTT; + } + + /** + * Returns the packet's Key Phase Bit: 0 or 1, if known. + * Returns -1 for outgoing packets. + * RFC 9000, Section 17.3.1: + * + *

        {@code
        +     *     Bit (0x04) of byte 0 indicates the key phase, which allows a recipient
        +     *     of a packet to identify the packet protection keys that are used to
        +     *     protect the packet. See [QUIC-TLS] for details.
        +     *     This bit is protected using header protection; see Section 5.4 of [QUIC-TLS].
        +     * }
        + * + * @return the packet's Key Phase Bit + * + * @see RFC 9001, [QUIC-TLS] + * @see RFC 9001, Section 5.4, [QUIC-TLS] + */ + default int keyPhase() { + return -1; + } + + /** + * Returns the packet's Latency Spin Bit: 0 or 1, if known. + * Returns -1 for outgoing packets. + * RFC 9000, Section 17.3.1: + * + *
        {@code
        +     *     The third most significant bit (0x20) of byte 0 is the latency spin
        +     *     bit, set as described in Section 17.4.
        +     * }
        + * + * @return the packet's Latency Spin Bit + * + * @see RFC 9000, Section 17.4 + */ + default int spin() { + return -1; + } + + @Override + default String prettyPrint() { + return String.format("%s(pn:%s, size=%s, phase:%s, spin:%s, frames:%s)", packetType(), packetNumber(), + size(), keyPhase(), spin(), frames()); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java new file mode 100644 index 00000000000..cea0854e0e5 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/PacketSpace.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.http.quic.PacketSpaceManager; +import jdk.internal.net.quic.QuicTransportException; + +/** + * An interface implemented by classes which keep track of packet + * numbers for a given packet number space. + */ +public sealed interface PacketSpace permits PacketSpaceManager { + + /** + * called on application packet space to record peer's transport parameters + * @param peerDelay max_ack_delay + * @param ackDelayExponent ack_delay_exponent + */ + void updatePeerTransportParameters(long peerDelay, long ackDelayExponent); + + /** + * {@return the packet number space managed by this class} + */ + PacketNumberSpace packetNumberSpace(); + + /** + * The largest processed PN is used to compute + * the packet number of an incoming Quic packet. + * + * @return the largest incoming packet number that + * was successfully processed in this space. + */ + long getLargestProcessedPN(); + + /** + * The largest received acked PN is used to compute the + * packet number that we include in an outgoing Quic packet. + * + * @return the largest packet number that was acknowledged by + * the peer in this space. + */ + long getLargestPeerAckedPN(); + + /** + * {@return the largest packet number that we have acknowledged in this + * space} + * + * @apiNote This is necessarily greater or equal to the packet number + * returned by {@linkplain #getMinPNThreshold()}. + */ + long getLargestSentAckedPN(); + + /** + * {@return the packet number threshold below which packets should be + * discarded without being processed in this space} + * + * @apiNote + * This corresponds to the largest acknowledged packet number + * carried in an outgoing ACK frame whose packet number has + * been acknowledged by the peer. In other words, the largest + * packet number sent by the peer for which we know that the + * peer has received an acknowledgement. + *

        + * Note that we need to track the ACK of outgoing packets that + * contain ACK frames in order to figure out whether a peer + * knows that a particular packet number has been received and + * avoid retransmission. However - we don't want ACK frames to grow + * too big and therefore we can drop some of the information, + * based on the largestSentAckedPN - see RFC 9000 Section 13.2 + * + */ + long getMinPNThreshold(); + + /** + * {@return a new packet number atomically allocated in this space} + */ + long allocateNextPN(); + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful negotiation of a new version. + * In that case we should stop retransmitting packet that have the + * "wrong" version: they will never be acknowledged. + */ + void versionChanged(); + + /** + * This method is called by {@link QuicConnectionImpl} upon reception of + * and successful processing of retry packet. + * In that case we should treat all previously sent packets as lost. + */ + void retry(); + + /** + * {@return a lock used by the transmission task}. + * Used to ensure that the transmission task does not observe partial changes + * during processing of incoming Versions and Retry packets. + */ + ReentrantLock getTransmitLock(); + /** + * Called when a packet is received. Causes the next ack frame to be + * updated. If a packet contains an {@link AckFrame}, the caller is + * expected to also later call {@link #processAckFrame(AckFrame)} + * when processing the packet payload. + * + * @param packet the received packet + * @param packetNumber the received packet number + * @param isAckEliciting whether this packet is ack eliciting + */ + void packetReceived(PacketType packet, long packetNumber, boolean isAckEliciting); + + /** + * Signals that a packet has been sent. + * This method is called by {@link QuicConnectionImpl} when a packet has been + * pushed to the endpoint for sending. + *

        The retransmitted packet is taken out the pendingRetransmission list and + * the new packet is inserted in the pendingAcknowledgement list. + * + * @param packet the new packet being retransmitted + * @param previousPacketNumber the packet number of the previous packet that was not acknowledged, + * or -1 if this is not a retransmission + * @param packetNumber the new packet number under which this packet is being retransmitted + * @throws IllegalArgumentException If {@code newPacketNumber} is lesser than 0 + */ + void packetSent(QuicPacket packet, long previousPacketNumber, long packetNumber); + + /** + * Processes a received ACK frame. + * This method is called by {@link QuicConnectionImpl}. + * + * @param frame the ACK frame received. + */ + void processAckFrame(AckFrame frame) throws QuicTransportException; + + /** + * Signals that the peer confirmed the handshake. Application space only. + */ + void confirmHandshake(); + + /** + * Get the next ack frame to send. + * This method returns the prepared ack frame if: + * - it was not sent yet + * - there are new ack-eliciting packets to acknowledge + * - optionally, if the ack frame is overdue + * + * @param onlyOverdue if true, the frame will only be returned if it's overdue + * @return The next AckFrame to send to the peer, or {@code null} + * if there is nothing to acknowledge. + */ + AckFrame getNextAckFrame(boolean onlyOverdue); + + /** + * Get the next ack frame to send. + * This method returns the prepared ack frame if: + * - it was not sent yet + * - there are new ack-eliciting packets to acknowledge + * - the ack frame size doesn't exceed {@code maxSize} + * - optionally, if the ack frame is overdue + * + * @param onlyOverdue if true, the frame will only be returned if it's overdue + * @param maxSize + * @return The next AckFrame to send to the peer, or {@code null} + * if there is nothing to acknowledge. + */ + AckFrame getNextAckFrame(boolean onlyOverdue, int maxSize); + + /** + * Used to request sending of a ping frame, for instance, to verify that + * the connection is alive. + * @return a completable future that will be completed with the time it + * took, in milliseconds, for the peer to acknowledge the packet that + * contained the PingFrame (or any packet that was sent after) + * + * @apiNote The returned completable future is actually completed + * if any packet whose packet number is greater than the packet number + * that contained the ping frame is acknowledged. + */ + CompletableFuture requestSendPing(); + + /** + * Stops retransmission for this packet space. + */ + void close(); + + /** + * {@return true if this packet space is closed} + */ + boolean isClosed(); + + /** + * Triggers immediate run of transmit loop. + * + * This method is called by {@link QuicConnectionImpl} when new data may be + * available for sending, for example: + * - new stream data is available + * - new receive credit is available + * - stream is forcibly closed + */ + void runTransmitter(); + + /** + * {@return true if a packet with that packet number + * is already being acknowledged (will be, or has been + * acknowledged)} + * @param packetNumber the packet number + */ + boolean isAcknowledged(long packetNumber); + + /** + * Immediately retransmit one unacknowledged initial packet + * @spec https://www.rfc-editor.org/rfc/rfc9002#name-speeding-up-handshake-compl + * RFC 9002 6.2.3. Speeding up Handshake Completion + */ + void fastRetransmit(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java new file mode 100644 index 00000000000..fa04cb3c947 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacket.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; +import java.util.Optional; + +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; + +/** + * A super-interface for all specific Quic packet implementation + * classes. + */ +public interface QuicPacket { + + /** + * {@return the packet's Destination Connection ID} + * + * @see + * RFC 9000, Section 7.2 + */ + QuicConnectionId destinationId(); + + /** + * The packet number space. + * NONE is for packets that don't have a packet number, + * such as Stateless Reset. + */ + enum PacketNumberSpace { + INITIAL, HANDSHAKE, APPLICATION, NONE; + + /** + * Maps a {@code PacketType} to the corresponding + * packet number space. + *

        + * For {@link PacketType#RETRY}, {@link PacketType#VERSIONS}, and + * {@link PacketType#NONE}, {@link PacketNumberSpace#NONE} is returned. + * + * @param packetType a packet type + * + * @return the packet number space that corresponds to the + * given packet type + */ + public static PacketNumberSpace of(PacketType packetType) { + return switch (packetType) { + case ONERTT, ZERORTT -> APPLICATION; + case INITIAL -> INITIAL; + case HANDSHAKE -> HANDSHAKE; + case RETRY, VERSIONS, NONE -> NONE; + }; + } + + /** + * Maps a {@code KeySpace} to the corresponding + * packet number space. + *

        + * For {@link KeySpace#RETRY}, {@link PacketNumberSpace#NONE} + * is returned. + * + * @param keySpace a key space + * + * @return the packet number space that corresponds to the given + * key space. + */ + public static PacketNumberSpace of(KeySpace keySpace) { + return switch (keySpace) { + case ONE_RTT, ZERO_RTT -> APPLICATION; + case HANDSHAKE -> HANDSHAKE; + case INITIAL -> INITIAL; + case RETRY -> NONE; + }; + } + } + + /** + * The packet type for Quic packets. + */ + enum PacketType { + NONE, INITIAL, VERSIONS, ZERORTT, HANDSHAKE, RETRY, ONERTT; + public boolean isLongHeaderType() { + return switch (this) { + case ONERTT, NONE, VERSIONS -> false; + default -> true; + }; + } + + /** + * {@return true if packets of this type are short-header packets} + */ + public boolean isShortHeaderType() { + return this == ONERTT; + } + + /** + * {@return the QUIC-TLS key space corresponding to this packet type} + * Some packet types, such as {@link #VERSIONS}, do not have an associated + * key space. + */ + public Optional keySpace() { + return switch (this) { + case INITIAL -> Optional.of(KeySpace.INITIAL); + case HANDSHAKE -> Optional.of(KeySpace.HANDSHAKE); + case RETRY -> Optional.of(KeySpace.RETRY); + case ZERORTT -> Optional.of(KeySpace.ZERO_RTT); + case ONERTT -> Optional.of(KeySpace.ONE_RTT); + case VERSIONS -> Optional.empty(); + case NONE -> Optional.empty(); + }; + } + } + + /** + * The Headers Type of the packet. + * This is either SHORT or LONG, or NONE when it can't be + * determined, or when we know that the packet is a stateless + * reset packet. A stateless reset packet is indistinguishable + * from a short header packet, so we only know that a packet + * is a stateless reset if we built it. In that case, the packet + * may advertise its header's type as NONE. + */ + enum HeadersType { NONE, SHORT, LONG} + + /** + * {@return this packet's number space} + */ + PacketNumberSpace numberSpace(); + + /** + * This packet size. + * @return the number of bytes needed to encode the packet. + * @see #payloadSize() + * @see #length() + */ + int size(); + + /** + * {@return true if this packet is ACK-eliciting} + * A packet is ACK-eliciting if it contains any + * {@linkplain QuicFrame#isAckEliciting() + * ACK-eliciting frame}. + */ + default boolean isAckEliciting() { + List frames = frames(); + if (frames == null || frames.isEmpty()) return false; + return frames.stream().anyMatch(QuicFrame::isAckEliciting); + } + + /** + * Whether this packet has a length field whose value can be read + * from the packet bytes. + * @return whether this packet has a length. + */ + default boolean hasLength() { + return switch (packetType()) { + case INITIAL, ZERORTT, HANDSHAKE -> true; + default -> false; + }; + } + + /** + * Returns the length of the payload and packet number. Includes encryption tag. + * + * This is the value stored in the {@code Length} field in Initial, + * Handshake and 0-RTT packets. + * @return the length of the payload and packet number. + * @throws UnsupportedOperationException if this packet type does not have + * the {@code Length} field. + * @see #hasLength() + * @see #size() + * @see #payloadSize() + */ + default int length() { + throw new UnsupportedOperationException(); + } + + /** + * This packet header's type. Either SHORT or LONG. + * @return this packet's header's type. + */ + HeadersType headersType(); + + /** + * {@return this packet's type} + */ + PacketType packetType(); + + /** + * {@return this packet's packet number, if applicable, {@code -1L} otherwise} + */ + default long packetNumber() { + return -1L; + } + + /** + * {@return this packet's frames} + */ + default List frames() { + return List.of(); + } + + /** + * {@return the packet's payload size} + * This is the number of bytes needed to encode the packet's + * {@linkplain #frames() frames}. + * @see #size() + * @see #length() + */ + default int payloadSize() { + List frames = frames(); + if (frames == null || frames.isEmpty()) return 0; + return frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + } + + default String prettyPrint() { + long pn = packetNumber(); + if (pn >= 0) { + return String.format("%s(pn:%s, size=%s, frames:%s)", packetType(), pn, size(), frames()); + } else { + return String.format("%s(size=%s)", packetType(), size()); + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java new file mode 100644 index 00000000000..233a1a35778 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketDecoder.java @@ -0,0 +1,1748 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http.quic.packets; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import javax.crypto.AEADBadTagException; +import javax.crypto.ShortBufferException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.Objects; +import java.util.List; +import java.nio.BufferUnderflowException; + +/** + * A {@code QuicPacketDecoder} encapsulates the logic to decode a + * quic packet. A {@code QuicPacketDecoder} is typically tied to + * a particular version of the QUIC protocol. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public class QuicPacketDecoder { + + private static final Logger debug = Utils.getDebugLogger(() -> "QuicPacketDecoder"); + + private final QuicVersion quicVersion; + private QuicPacketDecoder(final QuicVersion quicVersion) { + this.quicVersion = quicVersion; + } + + /** + * Reads the headers type from the given byte. + * @param first the first byte of a quic packet + * @return the headers type encoded in the given byte. + */ + private static QuicPacket.HeadersType headersType(byte first) { + int type = first & 0x80; + return type == 0 ? QuicPacket.HeadersType.SHORT : QuicPacket.HeadersType.LONG; + } + + /** + * Peeks at the headers type in the given byte buffer. + * Does not advance the cursor. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit.The provided offset must be less than the buffer + * limit in order for this method to read the header + * bytes. + * + * @param buffer the byte buffer containing a packet. + * @param offset the offset at which the packet starts. + * + * @return the header's type of the packet contained in this + * byte buffer. NONE if the header's type cannot be determined. + */ + public static QuicPacket.HeadersType peekHeaderType(ByteBuffer buffer, int offset) { + if (offset < 0 || offset >= buffer.limit()) return QuicPacket.HeadersType.NONE; + return headersType(buffer.get(offset)); + } + + /** + * Reads a connection ID length from the connection ID length + * byte. + * @param length the connection ID length byte. + * @return the connection ID length + */ + private static int connectionIdLength(byte length) { + // length is represented by an unsigned byte. + return length & 0xFF; + } + + /** + * Peeks at the connection id in the long header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the long header packet. + * + * @param buffer the buffer containing a long headers packet. + * @return A ByteBuffer slice containing the connection id bytes, + * or null if the packet is malformed and the connection id + * could not be read. + */ + public static ByteBuffer peekLongConnectionId(ByteBuffer buffer) { + // the connection id length starts at index 5 (1 byte for headers, + // 4 bytes for version) + var pos = buffer.position(); + var remaining = buffer.remaining(); + if (remaining < 6) return null; + int length = connectionIdLength(buffer.get(pos + 5)); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (length > remaining - 6) return null; + return buffer.slice(pos + 6, length); + } + + /** + * Peeks at the header in the long header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the long header packet. + * + * @param buffer the buffer containing a long header packet. + * @return A LongHeader containing the packet header data, + * or null if the packet is malformed + */ + public static LongHeader peekLongHeader(ByteBuffer buffer) { + return peekLongHeader(buffer, buffer.position()); + } + + /** + * Peeks at the header in the long header packet bytes. + * This method doesn't advance the cursor. + * + * @param buffer the buffer containing a long header packet. + * @param offset the position of the start of the packet + * @return A LongHeader containing the packet header data, + * or null if the packet is malformed + */ + public static LongHeader peekLongHeader(ByteBuffer buffer, int offset) { + // the destination connection id length starts at index 5 + // (1 byte for headers, 4 bytes for version) + // Therefore the packet needs at least 6 bytes to contain + // a DCID length (coded on 1 byte) + var remaining = buffer.remaining(); + var limit = buffer.limit(); + if (remaining < 7) return null; + if ((buffer.get(offset) & 0x80) == 0) { + // short header + return null; + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int version = buffer.getInt(offset+1); + + + // read the DCID length (coded on 1 byte) + int length = connectionIdLength(buffer.get(offset + 5)); + if (length < 0 || length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + QuicConnectionId destinationId = new PeerConnectionId(buffer.slice(offset + 6, length), null); + + // We need at least 6 + length + 1 byte to have + // a chance to read the SCID length (coded on 1 byte) + if (length > remaining - 7) return null; + int srcPos = offset + 6 + length; + + // read the SCID length + int srclength = connectionIdLength(buffer.get(srcPos)); + if (srclength < 0 || srclength > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + // we need at least pos + srclength + 1 byte in the + // packet to peek at the SCID + if (srclength > limit - srcPos - 1) return null; + QuicConnectionId sourceId = new PeerConnectionId(buffer.slice(srcPos + 1, srclength), null); + int headerLength = 7 + length + srclength; + + // Return the SCID as a buffer slice. + // The SCID begins at pos + 1 and has srclength bytes. + return new LongHeader(version, destinationId, sourceId, headerLength); + } + + /** + * Returns a bytebuffer containing the token from initial packet. + * This method doesn't advance the cursor. + * The buffer position must be at the start of an initial packet. + * @apiNote + * If the initial packet doesn't contain any token, an empty + * {@code ByteBuffer} is returned. + * @param buffer the buffer containing an initial packet. + * @return token or null if packet is malformed + */ + public static ByteBuffer peekInitialPacketToken(ByteBuffer buffer) { + + // the destination connection id length starts at index 5 + // (1 byte for headers, 4 bytes for version) + // Therefore the packet needs at least 6 bytes to contain + // a DCID length (coded on 1 byte) + var pos = buffer.position(); + var remaining = buffer.remaining(); + var limit = buffer.limit(); + if (remaining < 6) return null; + + // read the DCID length (coded on 1 byte) + int length = connectionIdLength(buffer.get(pos + 5)); + if (length > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (length < 0) return null; + + // skip the DCID, and read the SCID length + // We need at least 6 + length + 1 byte to have + // a chance to read the SCID length (coded on 1 byte) + pos = pos + 6 + length; + if (pos > limit - 1) return null; + + // read the SCID length + int srclength = connectionIdLength(buffer.get(pos)); + if (srclength > QuicConnectionId.MAX_CONNECTION_ID_LENGTH) return null; + if (srclength < 0) return null; + // we need at least pos + srclength + 1 byte in the + // packet to peek at the token + if (srclength > limit - pos - 1) return null; + + //skip the SCID, and read the token length + pos = pos + srclength + 1; + + // read the token length + int tokenLengthLength = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + assert tokenLengthLength <= 8; + if (pos > limit - tokenLengthLength -1) return null; + long tokenLength = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (tokenLength < 0 || tokenLength > Integer.MAX_VALUE) return null; + if (tokenLength > limit - pos - tokenLengthLength) return null; + + // return the token + return buffer.slice(pos + tokenLengthLength, (int)tokenLength); + } + + /** + * Peeks at the connection id in the short header packet bytes. + * This method doesn't advance the cursor. + * The buffer position must be at the start of the short header packet. + * + * @param buffer the buffer containing a short headers packet. + * @param length the connection id length. + * + * @return A ByteBuffer slice containing the connection id bytes, + * or null if the packet is malformed and the connection id + * could not be read. + */ + public static ByteBuffer peekShortConnectionId(ByteBuffer buffer, int length) { + int pos = buffer.position(); + int limit = buffer.limit(); + assert pos >= 0; + assert length <= QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + if (limit - pos < length + 1) return null; + return buffer.slice(pos+1, length); + } + + /** + * Returns the version of the first packet in the buffer. + * This method doesn't advance the cursor. + * Returns 0 if the version is 0 (version negotiation packet), + * or if the version cannot be determined. + * The packet is expected to start at the buffer's current position. + * + * @implNote + * This is equivalent to calling: + * {@code peekVersion(buffer, buffer.position())}. + * + * @param buffer the buffer containing the packet. + * + * @return the version of the packet in the buffer, or 0. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + public static int peekVersion(ByteBuffer buffer) { + return peekVersion(buffer, buffer.position()); + } + + /** + * Returns the version of the first packet in the buffer. + * This method doesn't advance the cursor. + * Returns 0 if the version is 0 (version negotiation packet), + * or if the version cannot be determined. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit. The buffer limit must allow for reading + * the header byte and version number starting at the offset. + * + * @param buffer the buffer containing the packet. + * @param offset the offset at which the packet starts. + * + * @return the version of the packet in the buffer, or 0. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + public static int peekVersion(ByteBuffer buffer, int offset) { + int limit = buffer.limit(); + assert offset >= 0; + if (limit - offset < 5) return 0; + QuicPacket.HeadersType headersType = peekHeaderType(buffer, offset); + if (headersType == QuicPacket.HeadersType.LONG) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.getInt(offset+1); + } + return 0; + } + + /** + * Returns true if the first packet in the buffer is a version + * negotiation packet. + * This method doesn't advance the cursor. + * + * @apiNote This method starts reading at the offset but respects + * the buffer limit. If the packet is a long header packet, + * the buffer limit must allow for reading + * the header byte and version number starting at the offset. + * + * @param buffer the buffer containing the packet. + * @param offset the offset at which the packet starts. + * + * @return true if the first packet in the buffer is a version + * negotiation packet. + * @see + * RFC 8999: Version-Independent Properties of QUIC + */ + private static boolean isVersionNegotiation(ByteBuffer buffer, int offset) { + int limit = buffer.limit(); + if (limit - offset < 5) return false; + QuicPacket.HeadersType headersType = peekHeaderType(buffer, offset); + if (headersType == QuicPacket.HeadersType.LONG) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.getInt(offset+1) == 0; + } + return false; + } + + public abstract static class IncomingQuicPacket implements QuicPacket { + private final QuicConnectionId destinationId; + + protected IncomingQuicPacket(QuicConnectionId destinationId) { + this.destinationId = destinationId; + } + + @Override + public final QuicConnectionId destinationId() { return destinationId; } + } + + private abstract static class IncomingLongHeaderPacket + extends IncomingQuicPacket implements LongHeaderPacket { + + private final QuicConnectionId sourceId; + private final int version; + IncomingLongHeaderPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version) { + super(destinationId); + this.sourceId = sourceId; + this.version = version; + } + + @Override + public final QuicConnectionId sourceId() { return sourceId; } + + @Override + public final int version() { return version; } + } + + private abstract static class IncomingShortHeaderPacket + extends IncomingQuicPacket implements ShortHeaderPacket { + + IncomingShortHeaderPacket(QuicConnectionId destinationId) { + super(destinationId); + } + } + + private static final class IncomingRetryPacket + extends IncomingLongHeaderPacket implements RetryPacket { + final int size; + final byte[] retryToken; + + private IncomingRetryPacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int size, byte[] retryToken) { + super(sourceId, destinationId, version); + this.size = size; + this.retryToken = retryToken; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] retryToken() { + return retryToken; + } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingRetryPacket}. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingRetryPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + */ + static IncomingRetryPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicTransportException { + try { + reader.verifyRetry(); + } catch (AEADBadTagException e) { + throw new IOException("Bad integrity tag", e); + } + + int size = reader.remaining(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Retry Token + byte[] retryToken = reader.readRetryToken(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(retryToken(%d), %s)", + retryToken.length, reader); + } + + // Retry Integrity Tag + assert reader.remaining() == 16; + byte[] retryIntegrityTag = reader.readRetryIntegrityTag(); + if (debug.on()) { + debug.log("IncomingRetryPacket.decode(retryIntegrityTag(%d), %s)", + retryIntegrityTag.length, reader); + } + assert size == reader.bytesRead(); + + return new IncomingRetryPacket(sourceID, destinationID, version, + size, retryToken); + } + } + + private static final class IncomingHandshakePacket + extends IncomingLongHeaderPacket implements HandshakePacket { + + final int size; + final int length; + final long packetNumber; + final List frames; + + IncomingHandshakePacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int length, long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingHandshakePacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingHandshakePacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingHandshakePacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingHandshakePacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get length of packet number and payload + var packetLength = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(length(%d), %s)", + packetLength, reader); + } + + // Remove protection before reading packet number + reader.unprotectLong(packetLength); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (packetLength - packetNumberLength); + if (debug.on()) { + debug.log("IncomingHandshakePacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt HANDSHAKE packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.HANDSHAKE, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + assert size == reader.position() - reader.offset(); + + assert packetLength == (int)packetLength; + return new IncomingHandshakePacket(sourceID, destinationID, + version, (int)packetLength, packetNumber, frames, size); + } + } + + private static final class IncomingZeroRttPacket + extends IncomingLongHeaderPacket implements ZeroRttPacket { + + final int size; + final int length; + final long packetNumber; + final List frames; + + IncomingZeroRttPacket(QuicConnectionId sourceId, QuicConnectionId destinationId, + int version, int length, long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingZeroRttPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingZeroRttPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingZeroRttPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingZeroRttPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get length of packet number and payload + var length = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(length(%d), %s)", + length, reader); + } + + // Remove protection before reading packet number + reader.unprotectLong(length); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (length - packetNumberLength); + if (debug.on()) { + debug.log("IncomingZeroRttPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt ZERORTT packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.ZERO_RTT, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + + assert length == (int)length; + return new IncomingZeroRttPacket(sourceID, destinationID, + version, (int)length, packetNumber, frames, size); + } + } + + private static final class IncomingOneRttPacket + extends IncomingShortHeaderPacket implements OneRttPacket { + + final int size; + final long packetNumber; + final List frames; + final int keyPhase; + final int spin; + + IncomingOneRttPacket(QuicConnectionId destinationId, + long packetNumber, List frames, + int spin, int keyPhase, int size) { + super(destinationId); + this.keyPhase = keyPhase; + this.spin = spin; + this.size = size; + this.packetNumber = packetNumber; + this.frames = frames; + } + + public long packetNumber() { + return packetNumber; + } + + @Override + public int size() { + return size; + } + + @Override + public int keyPhase() { + return keyPhase; + } + + @Override + public int spin() { + return spin; + } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingOneRttPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingOneRttPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingOneRttPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingOneRttPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(headers(%x), %s)", + headers, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readShortConnectionId(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + + // Remove protection before reading packet number + reader.unprotectShort(); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + // Packet Number + var packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = reader.remaining(); + if (debug.on()) { + debug.log("IncomingOneRttPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + final int keyPhase = (headers & 0x04) >> 2; + // keyphase is a 1 bit structure, so only 0 or 1 are valid values + assert keyPhase == 0 || keyPhase == 1 : "unexpected key phase: " + keyPhase; + final int spin = (headers & 0x20) >> 5; + assert spin == 0 || spin == 1 : "unexpected spin bit: " + spin; + + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, keyPhase); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt ONERTT packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.3.1 + if ((headers & 0x18) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.ONE_RTT, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + + return new IncomingOneRttPacket(destinationID, packetNumber, frames, spin, keyPhase, size); + } + } + + private static final class IncomingInitialPacket + extends IncomingLongHeaderPacket implements InitialPacket { + + final int size; + final int length; + final int tokenLength; + final long packetNumber; + final byte[] token; + final List frames; + + IncomingInitialPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, int version, + int tokenLength, byte[] token, int length, + long packetNumber, List frames, int size) { + super(sourceId, destinationId, version); + this.size = size; + this.length = length; + this.tokenLength = tokenLength; + this.token = token; + this.packetNumber = packetNumber; + this.frames = List.copyOf(frames); + } + + @Override + public int tokenLength() { return tokenLength; } + + @Override + public byte[] token() { return token; } + + @Override + public int length() { return length; } + + @Override + public long packetNumber() { return packetNumber; } + + @Override + public int size() { return size; } + + @Override + public List frames() { return frames; } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingInitialPacket}. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer, then creates an {@code IncomingInitialPacket} + * with the decoded data. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingInitialPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + */ + static IncomingInitialPacket decode(PacketReader reader, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingInitialPacket.decode([protected]headers(%x), version(%d), %s)", + headers, version, reader); + } + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Get number of bytes needed to store the length of the token + var tokenLength = (int) reader.readTokenLength(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(token-length(%d), %s)", + tokenLength, reader); + } + var token = reader.readToken(tokenLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(token(%d), %s)", + token == null ? 0 : token.length, reader); + } + + // Get length of packet number and payload + var packetLength = reader.readPacketLength(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(packetLength(%d), %s)", + packetLength, reader); + } + assert packetLength == (int)packetLength; + if (packetLength > reader.remaining()) { + if (debug.on()) { + debug.log("IncomingInitialPacket rejected, invalid length(%d/%d), %s)", + packetLength, reader.remaining(), reader); + } + throw new BufferUnderflowException(); + } + + + // get the size (in bytes) of new packet + int size = reader.bytesRead() + (int)packetLength; + + if (!context.verifyToken(destinationID, token)) { + if (debug.on()) { + debug.log("IncomingInitialPacket rejected, invalid token(%s), %s)", + token == null ? "null" : HexFormat.of().formatHex(token), + reader); + } + return null; + } + + // Remove protection before reading packet number + reader.unprotectLong(packetLength); + + // re-read headers, now that protection is removed + headers = reader.headers(); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode([unprotected]headers(%x), %s)", + headers, reader); + } + + // Packet Number + int packetNumberLength = reader.packetNumberLength(); + var packetNumber = reader.readPacketNumber(packetNumberLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(" + + "packetNumberLength(%d), packetNumber(%d), %s)", + packetNumberLength, packetNumber, reader); + } + + // Calculate payload length and retrieve payload + int payloadLen = (int) (packetLength - packetNumberLength); + if (debug.on()) { + debug.log("IncomingInitialPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + ByteBuffer payload = null; + try { + payload = reader.decryptPayload(packetNumber, payloadLen, -1 /* key phase */); + } catch (AEADBadTagException e) { + Log.logError("[Quic] Failed to decrypt INITIAL packet (Bad AEAD tag; discarding packet): " + e); + Log.logError(e); + throw new IOException("Bad AEAD tag", e); + } + // check reserved bits after checking integrity, see RFC 9000, section 17.2 + if ((headers & 0xc) != 0) { + throw new QuicTransportException("Nonzero reserved bits in packet header", + QuicTLSEngine.KeySpace.INITIAL, 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + List frames = reader.parsePayloadSlice(payload); + assert !payload.hasRemaining() : "remaining bytes in payload: " + payload.remaining(); + + assert size == reader.bytesRead() : size - reader.bytesRead(); + + return new IncomingInitialPacket(sourceID, destinationID, + version, tokenLength, token, (int)packetLength, packetNumber, frames, size); + } + + } + + private static final class IncomingVersionNegotiationPacket + extends IncomingLongHeaderPacket + implements VersionNegotiationPacket { + + final int size; + final int[] versions; + + IncomingVersionNegotiationPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, int[] versions, + int size) { + super(sourceId, destinationId, version); + this.size = size; + this.versions = Objects.requireNonNull(versions); + } + + @Override + public int size() { return size; } + + @Override + public List frames() { return List.of(); } + + @Override + public int payloadSize() { return versions.length << 2; } + + @Override + public int[] supportedVersions() { + return versions; + } + + /** + * Decode a valid {@code ByteBuffer} into an {@link IncomingVersionNegotiationPacket}. + * + * @param reader A {@code PacketReader} to decode the {@code ByteBuffer} that contains + * the bytes of this packet + * @param context the decoding context + * + * @return an {@code IncomingVersionNegotiationPacket} with its contents set + * according to the packets fields + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + */ + static IncomingVersionNegotiationPacket decode(PacketReader reader, CodingContext context) + throws IOException { + + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(%s)", reader); + } + + byte headers = reader.readHeaders(); // read headers + int version = reader.readVersion(); // read version + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(headers(%x), version(%d), %s)", + headers, version, reader); + } + // The long header bit should be set. We should ignore the other 7 bits + assert QuicPacketDecoder.headersType(headers) == HeadersType.LONG || (headers & 0x80) == 0x80; + + // Retrieve the destination and source connections IDs + var destinationID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(dcid(%d), %s)", + destinationID.length(), reader); + } + var sourceID = reader.readLongConnectionId(); + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(scid(%d), %s)", + sourceID.length(), reader); + } + + // Calculate payload length and retrieve payload + final int payloadLen = reader.remaining(); + final int versionsCount = payloadLen >> 2; + if (debug.on()) { + debug.log("IncomingVersionNegotiationPacket.decode(payloadLen(%d), %s)", + payloadLen, reader); + } + int[] versions = reader.readSupportedVersions(); + + // Finally, get the size (in bytes) of new packet + var size = reader.bytesRead(); + assert !reader.hasRemaining() : "%s superfluous bytes in buffer" + .formatted(reader.remaining()); + + // sanity checks: + var msg = "Bad version negotiation packet"; + if (payloadLen != versionsCount << 2) { + throw new IOException("%s: %s bytes after %s versions" + .formatted(msg, payloadLen % 4, versionsCount)); + } + if (versionsCount == 0) { + throw new IOException("%s: no supported versions in packet" + .formatted(msg)); + } + + return new IncomingVersionNegotiationPacket(sourceID, destinationID, + version, versions, size); + } + } + + /** + * Decode the contents of the given {@code ByteBuffer} and, depending on the + * {@link PacketType}, return a {@link QuicPacket} with the corresponding type. + * This method removes packet protection and decrypt the packet encoded into + * the provided byte buffer as appropriate. + * + *

        If successful, an {@code IncomingQuicPacket} instance is returned. + * The position of the buffer is moved to the first byte following the last + * decoded byte. The buffer limit is unchanged. + * + *

        Otherwise, an exception is thrown. The position of the buffer is unspecified, + * but is usually set at the place where the error occurred. + * + * @apiNote If successful, and the limit was not reached, this method should be + * called again to decode the next packet contained in the buffer. Otherwise, if + * an exception occurs, the remaining bytes in the buffer should be dropped, since + * the position of the next packet in the buffer cannot be determined with + * certainty. + * + * @param buffer the buffer with the bytes to be decoded + * @param context the decoding context + * + * @throws IOException if decoding fails for any reason + * @throws BufferUnderflowException if buffer does not have enough bytes + * @throws QuicTransportException if packet is correctly signed but malformed + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ + public IncomingQuicPacket decode(ByteBuffer buffer, CodingContext context) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + Objects.requireNonNull(buffer); + + assert buffer.order() == ByteOrder.BIG_ENDIAN; + PacketType type = peekPacketType(buffer); + PacketReader packetReader = new PacketReader(buffer, context, type); + + QuicTLSEngine.KeySpace keySpace = type.keySpace().orElse(null); + if (keySpace != null && !context.getTLSEngine().keysAvailable(keySpace)) { + if (debug.on()) { + debug.log("QuicPacketDecoder.decode(%s): no keys, skipping", packetReader); + } + return null; + } + + return switch (type) { + case RETRY -> IncomingRetryPacket.decode(packetReader, context); + case ONERTT -> IncomingOneRttPacket.decode(packetReader, context); + case ZERORTT -> IncomingZeroRttPacket.decode(packetReader, context); + case HANDSHAKE -> IncomingHandshakePacket.decode(packetReader, context); + case INITIAL -> IncomingInitialPacket.decode(packetReader, context); + case VERSIONS -> IncomingVersionNegotiationPacket.decode(packetReader, context); + case NONE -> throw new IOException("Unknown type: " + type); // if junk received + default -> throw new IOException("Not implemented: " + type); // if has type but not recognised + }; + } + + private static QuicConnectionId decodeConnectionID(ByteBuffer buffer) { + if (!buffer.hasRemaining()) + throw new BufferUnderflowException(); + + int len = buffer.get() & 0xFF; + if (len > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] destinationConnectionID = new byte[len]; + + // Save buffer position ahead of time to check after read + int pos = buffer.position(); + buffer.get(destinationConnectionID); + // Ensure all bytes have been read correctly + assert pos + len == buffer.position(); + + return new PeerConnectionId(destinationConnectionID); + } + + /** + * Peek at the size of the first packet present in the buffer. + * The position of the buffer must be at the first byte of the + * first packet. This method doesn't advance the buffer position. + * @param buffer A byte buffer containing quic packets + * @return the size of the first packet present in the buffer. + */ + public int peekPacketSize(ByteBuffer buffer) { + int pos = buffer.position(); + int limit = buffer.limit(); + int available = limit - pos; + assert available >= 0 : available; + if (available <= 0) return available; + PacketType type = peekPacketType(buffer); + return switch (type) { + case HANDSHAKE, INITIAL, ZERORTT -> { + assert peekVersion(buffer, pos) == quicVersion.versionNumber(); + int end = peekPacketEnd(type, buffer); + assert end <= limit; + yield end - pos; + } + // ONERTT, RETRY, VERSIONS, NONE: + default -> available; + }; + } + + /** + * Reads the Quic V1 packet type from the given byte. + * + * @param headerByte the first byte of a quic packet + * @return the packet type encoded in the given byte. + */ + private PacketType packetType(byte headerByte) { + int htype = headerByte & 0xC0; + int ptype = headerByte & 0xF0; + return switch (htype) { + case 0xC0 -> switch (quicVersion) { + case QUIC_V1 -> switch (ptype) { + case 0xC0 -> PacketType.INITIAL; + case 0xD0 -> PacketType.ZERORTT; + case 0xE0 -> PacketType.HANDSHAKE; + case 0xF0 -> PacketType.RETRY; + default -> PacketType.NONE; + }; + case QUIC_V2 -> switch (ptype) { + case 0xD0 -> PacketType.INITIAL; + case 0xE0 -> PacketType.ZERORTT; + case 0xF0 -> PacketType.HANDSHAKE; + case 0xC0 -> PacketType.RETRY; + default -> PacketType.NONE; + }; + }; + case 0x40 -> PacketType.ONERTT; // may be a stateless reset too + default -> PacketType.NONE; + }; + } + + public PacketType peekPacketType(ByteBuffer buffer) { + int offset = buffer.position(); + return peekPacketType(buffer, offset); + } + + public PacketType peekPacketType(ByteBuffer buffer, int offset) { + if (offset < 0 || offset >= buffer.limit()) return PacketType.NONE; + var headers = buffer.get(offset); + var headersType = headersType(headers); + if (headersType == QuicPacket.HeadersType.LONG) { + if (isVersionNegotiation(buffer, offset)) { + return PacketType.VERSIONS; + } + var version = peekVersion(buffer, offset); + if (version != quicVersion.versionNumber()) { + return PacketType.NONE; + } + } + return packetType(headers); + } + + /** + * Returns the position just after the first packet present in the buffer. + * @param type the first packet type. Must be INITIAL, HANDSHAKE, or ZERORTT. + * @param buffer the byte buffer containing the packet + * @return the position just after the first packet present in the buffer. + */ + private int peekPacketEnd(PacketType type, ByteBuffer buffer) { + // Store initial position to calculate size of packet decoded + int initialPosition = buffer.position(); + int limit = buffer.limit(); + assert buffer.order() == ByteOrder.BIG_ENDIAN; + assert type == PacketType.HANDSHAKE + || type == PacketType.INITIAL + || type == PacketType.ZERORTT : type; + // This case should have been handled by the caller + assert buffer.getInt(initialPosition + 1) != 0 : "version is 0"; + + int pos = initialPosition; // header bits + pos = pos + 4; // version + pos = pos + 1; // dcid length + if (pos <= 0 || pos >= limit) return limit; + int dcidlen = buffer.get(pos) & 0xFF; + pos = pos + dcidlen + 1; // scid length + if (pos <= 0 || pos >= limit) return limit; + int scidlen = buffer.get(pos) & 0xFF; + pos = pos + scidlen + 1; // token length or packet length + if (pos <= 0 || pos >= limit) return limit; + + if (type == PacketType.INITIAL) { + int tksize = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + if (tksize <= 0 || tksize > 8) return limit; + if (limit - tksize < pos) return limit; + long tklen = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (tklen < 0 || tklen > limit - pos) return limit; + pos = pos + tksize + (int)tklen; // packet length + if (pos <= 0 || pos >= limit) return limit; + } + + int lensize = VariableLengthEncoder.peekEncodedValueSize(buffer, pos); + if (lensize <= 0 || lensize > 8) return limit; + long len = VariableLengthEncoder.peekEncodedValue(buffer, pos); + if (len < 0 || len > limit - pos) return limit; + pos = pos + lensize + (int)len; // end of packet + if (pos <= 0 || pos >= limit) return limit; + return pos; + } + + /** + * Find the length of the next packet in the buffer, and return + * the next packet bytes as a slice of the original packet. + * Advances the original buffer position to after the returned + * packet. + * @param buffer a buffer containing coalesced packets + * @param offset the offset at which the next packet starts + * @return the next packet. + */ + public ByteBuffer nextPacketSlice(ByteBuffer buffer, int offset) { + assert offset >= 0; + assert offset <= buffer.limit(); + int pos = buffer.position(); + int limit = buffer.limit(); + buffer.position(offset); + ByteBuffer next = null; + try { + int size = peekPacketSize(buffer); + if (debug.on()) { + debug.log("next packet bytes from %d (%d/%d)", + offset, size, buffer.remaining()); + } + next = buffer.slice(offset, size); + buffer.position(offset + size); + } catch (Throwable tt) { + if (debug.on()) { + debug.log("failed to peek packet size: " + tt, tt); + debug.log("dropping all remaining bytes (%d)", limit - pos); + } + buffer.position(limit); + next = buffer; + } + return next; + } + + /** + * Advance the bytebuffer position to the end of the packet + * @param buffer A byte buffer containing quic packets + * @param offset The offset at which the packet starts + */ + public void skipPacket(ByteBuffer buffer, int offset) { + assert offset >= 0; + assert offset <= buffer.limit(); + int pos = buffer.position(); + int limit = buffer.limit(); + buffer.position(offset); + try { + int size = peekPacketSize(buffer); + if (debug.on()) { + debug.log("dropping packet bytes from %d (%d/%d)", + offset, size, buffer.remaining()); + } + buffer.position(offset + size); + } catch (Throwable tt) { + if (debug.on()) { + debug.log("failed to peek packet size: " + tt, tt); + debug.log("dropping all remaining bytes (%d)", limit - pos); + } + buffer.position(limit); + } + } + + /** + * Returns a decoder for the given Quic version. + * Returns {@code null} if no decoder for that version exists. + * + * @param quicVersion the Quic protocol version number + * @return a decoder for the given Quic version or {@code null} + */ + public static QuicPacketDecoder of(QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> Decoders.QUIC_V1_DECODER; + case QUIC_V2 -> Decoders.QUIC_V2_DECODER; + default -> throw new IllegalArgumentException("No packet decoder for Quic version " + quicVersion); + }; + } + + /** + * Returns a {@code QuicPacketDecoder} to decode the packet + * starting at the specified offset in the buffer. + * This method will attempt to read the quic version in the + * packet in order to return the proper decoder. + * If the version is 0, then the decoder for Quic Version 1 + * is returned. + * + * @param buffer A buffer containing a Quic packet + * @param offset The offset at which the packet starts + * @return A {@code QuicPacketDecoder} instance to decode the + * packet starting at the given offset. + */ + public static QuicPacketDecoder of(ByteBuffer buffer, int offset) { + var version = peekVersion(buffer, offset); + final QuicVersion quicVersion = version == 0 ? QuicVersion.QUIC_V1 + : QuicVersion.of(version).orElse(null); + if (quicVersion == null) { + return null; + } + return of(quicVersion); + } + + /** + * A {@code PacketReader} to read a Quic packet. + * A {@code PacketReader} may have version specific code, and therefore + * has an implicit pointer to a {@code QuicPacketDecoder} instance. + *

        + * A {@code PacketReader} offers high level helper methods to read + * data (such as Connection IDs or Packet Numbers) from a Quic packet. + * It has however no or little knowledge of the actual packet structure. + * It is driven by the {@code decode} method of the appropriate + * {@code IncomingQuicPacket} type. + *

        + * A {@code PacketReader} is stateful: it encapsulates a {@code ByteBuffer} + * (or possibly a list of byte buffers - as a future enhancement) and + * advances the position on the buffer it is reading. + * + */ + class PacketReader { + private static final int PACKET_NUMBER_MASK = 0x03; + final ByteBuffer buffer; + final int offset; + final int initialLimit; + final CodingContext context; + final PacketType packetType; + + PacketReader(ByteBuffer buffer, CodingContext context) { + this(buffer, context, peekPacketType(buffer)); + } + + PacketReader(ByteBuffer buffer, CodingContext context, PacketType packetType) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int pos = buffer.position(); + int limit = buffer.limit(); + this.buffer = buffer; + this.offset = pos; + this.initialLimit = limit; + this.context = context; + this.packetType = packetType; + } + + public int offset() { + return offset; + } + + public int position() { + return buffer.position(); + } + + public int remaining() { + return buffer.remaining(); + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + public int bytesRead() { + return position() - offset; + } + + public void reset() { + buffer.position(offset); + buffer.limit(initialLimit); + } + + public byte headers() { + return buffer.get(offset); + } + + public void headers(byte headers) { + buffer.put(offset, headers); + } + + public PacketType packetType() { + return packetType; + } + + public int packetNumberLength() { + return (headers() & PACKET_NUMBER_MASK) + 1; + } + + public byte readHeaders() { + return buffer.get(); + } + + public int readVersion() { + return buffer.getInt(); + } + + public int[] readSupportedVersions() { + // Calculate payload length and retrieve payload + final int payloadLen = buffer.remaining(); + final int versionsCount = payloadLen >> 2; + + int[] versions = new int[versionsCount]; + for (int i=0 ; i= 0 && packetLength <= VariableLengthEncoder.MAX_ENCODED_INTEGER + : packetLength; + if (packetLength > remaining()) { + throw new BufferUnderflowException(); + } + return packetLength; + } + + public long readTokenLength() { + return readVariableLength(); + } + + public byte[] readToken(int tokenLength) { + // Check to ensure that tokenLength is within valid range + if (tokenLength < 0 || tokenLength > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] token = tokenLength > 0 ? new byte[tokenLength] : null; + if (tokenLength > 0) { + buffer.get(token); + } + return token; + } + + public long readVariableLength() { + return VariableLengthEncoder.decode(buffer); + } + + public void maskPacketNumber(int packetNumberLength, ByteBuffer mask) { + int pos = buffer.position(); + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(pos + i, (byte)(buffer.get(pos + i) ^ mask.get())); + } + } + + public long readPacketNumber(int packetNumberLength) { + var packetNumberSpace = PacketNumberSpace.of(packetType); + var largestProcessedPN = context.largestProcessedPN(packetNumberSpace); + return QuicPacketNumbers.decodePacketNumber(largestProcessedPN, buffer, packetNumberLength); + } + + public long readPacketNumber() { + return readPacketNumber(packetNumberLength()); + } + + private ByteBuffer peekPayloadSlice(int relativeOffset, int length) { + int payloadStart = buffer.position() + relativeOffset; + return buffer.slice(payloadStart, length); + } + + private ByteBuffer decryptPayload(long packetNumber, int payloadLen, int keyPhase) + throws AEADBadTagException, QuicKeyUnavailableException, QuicTransportException { + // Calculate payload length and retrieve payload + ByteBuffer output = buffer.slice(); + // output's position is on the first byte of encrypted data + output.mark(); + int payloadStart = buffer.position(); + buffer.position(offset); + buffer.limit(payloadStart + payloadLen); + // buffer's position and limit are set to the boundaries of the encrypted packet + try { + context.getTLSEngine().decryptPacket(packetType.keySpace().get(), packetNumber, keyPhase, + buffer, payloadStart - offset, output); + } catch (ShortBufferException e) { + throw new QuicTransportException(e.toString(), null, 0, + QuicTransportErrors.INTERNAL_ERROR); + } + // buffer's position and limit are both at end of the packet + output.limit(output.position()); + output.reset(); + // output's position and limit are set to the boundaries of decrypted frame data + buffer.limit(initialLimit); + return output; + } + + public List parsePayloadSlice(ByteBuffer payload) + throws QuicTransportException { + if (!payload.hasRemaining()) { + throw new QuicTransportException("Packet with no frames", + packetType().keySpace().get(), 0, QuicTransportErrors.PROTOCOL_VIOLATION); + } + try { + List frames = new ArrayList<>(); + while (payload.hasRemaining()) { + int start = payload.position(); + frames.add(QuicFrame.decode(payload)); + int end = payload.position(); + assert start < end : "bytes remaining at offset %s: %s" + .formatted(start, payload.remaining()); + } + return frames; + } catch (RuntimeException e) { + throw new QuicTransportException(e.getMessage(), + packetType().keySpace().get(), 0, QuicTransportErrors.INTERNAL_ERROR); + } + } + + byte[] readRetryToken() { + var tokenLength = buffer.limit() - buffer.position() - 16; + assert tokenLength > 0; + byte[] retryToken = new byte[tokenLength]; + buffer.get(retryToken); + return retryToken; + } + + byte[] readRetryIntegrityTag() { + // The 16 last bytes in the datagram payload + assert remaining() == 16; + byte[] retryIntegrityTag = new byte[16]; + buffer.get(retryIntegrityTag); + return retryIntegrityTag; + } + + public void verifyRetry() throws AEADBadTagException, QuicTransportException { + // assume the buffer position and limit are set to packet boundaries + QuicTLSEngine tlsEngine = context.getTLSEngine(); + tlsEngine.verifyRetryPacket(quicVersion, + context.originalServerConnId().asReadOnlyBuffer(), buffer.asReadOnlyBuffer()); + } + + public QuicConnectionId readLongConnectionId() { + return decodeConnectionID(buffer); + } + + public QuicConnectionId readShortConnectionId() { + if (!buffer.hasRemaining()) + throw new BufferUnderflowException(); + + // Retrieve connection ID length from endpoint via context + int len = context.connectionIdLength(); + if (len > buffer.remaining()) { + throw new BufferUnderflowException(); + } + byte[] destinationConnectionID = new byte[len]; + + // Save buffer position ahead of time to check after read + int pos = buffer.position(); + buffer.get(destinationConnectionID); + // Ensure all bytes have been read correctly + assert pos + len == buffer.position(); + + return new PeerConnectionId(destinationConnectionID); + } + + @Override + public String toString() { + return "PacketReader(offset=%s, pos=%s, remaining=%s)" + .formatted(offset, position(), remaining()); + } + + public void unprotectLong(long packetLength) + throws QuicKeyUnavailableException, QuicTransportException { + unprotect(packetLength, (byte) 0x0f); + } + + public void unprotectShort() + throws QuicKeyUnavailableException, QuicTransportException { + unprotect(buffer.remaining(), (byte) 0x1f); + } + + private void unprotect(long packetLength, byte headerMask) + throws QuicKeyUnavailableException, QuicTransportException { + QuicTLSEngine tlsEngine = context.getTLSEngine(); + int sampleSize = tlsEngine.getHeaderProtectionSampleSize(packetType.keySpace().get()); + if (packetLength > buffer.remaining() || packetLength < sampleSize + 4) { + throw new BufferUnderflowException(); + } + ByteBuffer sample = peekPayloadSlice(4, sampleSize); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(packetType.keySpace().get(), true, sample); + byte headers = headers(); + headers ^= (byte) (encryptedSample.get() & headerMask); + headers(headers); + maskPacketNumber(packetNumberLength(), encryptedSample); + } + } + + + private static final class Decoders { + static final QuicPacketDecoder QUIC_V1_DECODER = new QuicPacketDecoder(QuicVersion.QUIC_V1); + static final QuicPacketDecoder QUIC_V2_DECODER = new QuicPacketDecoder(QuicVersion.QUIC_V2); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java new file mode 100644 index 00000000000..890c1a63a35 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketEncoder.java @@ -0,0 +1,1746 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.function.IntFunction; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import javax.crypto.ShortBufferException; + +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.encodePacketNumber; +import static jdk.internal.net.http.quic.QuicConnectionId.MAX_CONNECTION_ID_LENGTH; + +/** + * A {@code QuicPacketEncoder} encapsulates the logic to encode a + * quic packet. A {@code QuicPacketEncoder} is typically tied to + * a particular version of the QUIC protocol. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9001 + * RFC 9001: Using TLS to Secure QUIC + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public class QuicPacketEncoder { + + private static final Logger debug = Utils.getDebugLogger(() -> "QuicPacketEncoder"); + + private final QuicVersion quicVersion; + private QuicPacketEncoder(final QuicVersion quicVersion) { + this.quicVersion = quicVersion; + } + + /** + * Computes the packet's header byte, which also encodes + * the packetNumber length. + * + * @param packetTypeTag quic-dependent packet type encoding + * @param pnsize the number of bytes needed to encode the packet number + * @return the packet's header byte + */ + private static byte headers(byte packetTypeTag, int pnsize) { + int pnprefix = pnsize - 1; + assert pnprefix >= 0; + assert pnprefix <= 3; + return (byte)(packetTypeTag | pnprefix); + } + + /** + * Returns the headers tag for the given packet type. + * Returns 0 if the packet type is NONE or unknown. + *

        + * For version negotiations packet, this method returns 0x80. + * The other 7 bits must be ignored by a client. + * When emitting a version negotiation packet the server should + * also set the fix bit (0x40) to 1. + * What distinguishes a version negotiation packet from other + * long header packet types is not the packet type found in the + * header's byte, but the fact that a. it is a long header and + * b. the version number in the packet (the 4 bytes following + * the header) is 0. + * @param packetType the packet type + * @return the headers tag for the given packet type. + * + * @see + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @see + * RFC 9369: QUIC Version 2 + */ + private byte packetHeadersTag(PacketType packetType) { + return (byte) switch (quicVersion) { + case QUIC_V1 -> switch (packetType) { + case ONERTT -> 0x40; + case INITIAL -> 0xC0; + case ZERORTT -> 0xD0; + case HANDSHAKE -> 0xE0; + case RETRY -> 0xF0; + case VERSIONS -> 0x80; // remaining bits are ignored + case NONE -> 0x00; + }; + case QUIC_V2 -> switch (packetType) { + case ONERTT -> 0x40; + case INITIAL -> 0xD0; + case ZERORTT -> 0xE0; + case HANDSHAKE -> 0xF0; + case RETRY -> 0xC0; + case VERSIONS -> 0x80; // remaining bits are ignored + case NONE -> 0x00; + }; + }; + } + + /** + * Encode the OneRttPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingOneRttPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + QuicConnectionId destination = packet.destinationId(); + + if (debug.on()) { + debug.log("OneRttPacket::encodePacket(ByteBuffer(%d,%d)," + + " dst=%s, packet=%d, encodedPacket=%s," + + " payload=QuicFrames(frames: %s, bytes: %d)," + + " size=%d", + buffer.position(), buffer.limit(), destination, + packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : "incorrect packet number prefix in headers: " + headers; + + final PacketWriter writer = new PacketWriter(buffer, context, PacketType.ONERTT); + writer.writeHeaders(headers); + writer.writeShortConnectionId(destination); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderShort(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the ZeroRttPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingZeroRttPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("ZeroRttPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, payload=QuicFrame(frames: %s, bytes: %d), size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.ZERORTT); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the VersionNegotiationPacket into the provided + * buffer. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @throws BufferOverflowException if the buffer is not large enough + */ + private static void encodePacket(OutgoingVersionNegotiationPacket packet, + ByteBuffer buffer) { + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, versions=%s, size=%d", + buffer.position(), buffer.limit(), source, destination, + Arrays.toString(packet.versions), packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int offset = buffer.position(); + int limit = buffer.limit(); + assert buffer.capacity() >= packet.size; + assert limit - offset >= packet.size; + + int typeTag = 0x80; + int rand = Encoders.RANDOM.nextInt() & 0x7F; + int headers = typeTag | rand; + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket:" + + " type: 0x%02x, unused: 0x%02x, headers: 0x%02x", + typeTag, rand & ~0x80, headers); + } + assert (headers & typeTag) == typeTag : headers; + assert (headers ^ typeTag) == rand : headers; + + // headers(1 byte), version(4 bytes) + buffer.put((byte)headers); // 1 + putInt32(buffer, 0); // 4 + + // DCID: 1 byte for length, + destination id bytes + var dcidlen = destination.length(); + assert dcidlen <= MAX_CONNECTION_ID_LENGTH && dcidlen >= 0 : dcidlen; + buffer.put((byte)dcidlen); // 1 + buffer.put(destination.asReadOnlyBuffer()); + assert buffer.position() == offset + 6 + dcidlen : buffer.position(); + + // SCID: 1 byte for length, + source id bytes + var scidlen = source.length(); + assert scidlen <= MAX_CONNECTION_ID_LENGTH && scidlen >= 0 : scidlen; + buffer.put((byte) scidlen); + buffer.put(source.asReadOnlyBuffer()); + assert buffer.position() == offset + 7 + dcidlen + scidlen : buffer.position(); + + // Put payload (= supported versions) + int versionsStart = buffer.position(); + for (int i = 0; i < packet.versions.length; i++) { + putInt32(buffer, packet.versions[i]); + } + int versionsEnd = buffer.position(); + if (debug.on()) { + debug.log("VersionNegotiationPacket::encodePacket:" + + " encoded %d bytes", offset - versionsEnd); + } + + assert versionsEnd - offset == packet.size; + assert versionsEnd - versionsStart == packet.versions.length << 2; + } + + /** + * Encode the HandshakePacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingHandshakePacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("HandshakePacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, payload=QuicFrame(frames: %s, bytes: %d)," + + " size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.HANDSHAKE); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the InitialPacket into the provided buffer. + * This method encrypts the packet into the provided byte buffer as appropriate, + * adding packet protection as appropriate. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context coding context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingInitialPacket packet, + ByteBuffer buffer, + CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + if (packet.size > buffer.remaining()) { + throw new BufferOverflowException(); + } + + if (debug.on()) { + debug.log("InitialPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, packet=%d, " + + "encodedPacket=%s, token=%s, " + + "payload=QuicFrame(frames: %s, bytes: %d), size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.packetNumber, Arrays.toString(packet.encodedPacketNumber), + packet.token == null ? null : "byte[%s]".formatted(packet.token.length), + packet.frames, packet.payloadSize, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + + int encodedLength = packet.encodedPacketNumber.length; + assert encodedLength >= 1 && encodedLength <= 4 : encodedLength; + int pnprefix = encodedLength - 1; + + byte headers = headers(packetHeadersTag(packet.packetType()), + packet.encodedPacketNumber.length); + assert (headers & 0x03) == pnprefix : headers; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.INITIAL); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writeToken(packet.token); + writer.writePacketLength(packet.length); + int packetNumberStart = writer.position(); + writer.writeEncodedPacketNumber(packet.encodedPacketNumber); + int payloadStart = writer.position(); + writer.writePayload(packet.frames); + writer.encryptPayload(packet.packetNumber, payloadStart); + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + writer.protectHeaderLong(packetNumberStart, packet.encodedPacketNumber.length); + } + + /** + * Encode the RetryPacket into the provided buffer. + * + * @param packet + * @param buffer A buffer to encode the packet into. + * @param context + * @throws BufferOverflowException if the buffer is not large enough + */ + private void encodePacket(OutgoingRetryPacket packet, + ByteBuffer buffer, + CodingContext context) throws QuicTransportException { + int version = packet.version(); + if (quicVersion.versionNumber() != version) { + throw new IllegalArgumentException("Encoder version %s does not match packet version %s" + .formatted(quicVersion, version)); + } + QuicConnectionId destination = packet.destinationId(); + QuicConnectionId source = packet.sourceId(); + + if (debug.on()) { + debug.log("RetryPacket::encodePacket(ByteBuffer(%d,%d)," + + " src=%s, dst=%s, version=%d, retryToken=%d," + + " size=%d", + buffer.position(), buffer.limit(), source, destination, + version, packet.retryToken.length, packet.size); + } + assert buffer.order() == ByteOrder.BIG_ENDIAN; + assert packet.retryToken.length > 0; + assert buffer.remaining() >= packet.size; + + PacketWriter writer = new PacketWriter(buffer, context, PacketType.RETRY); + + byte headers = packetHeadersTag(packet.packetType()); + headers |= (byte)Encoders.RANDOM.nextInt(0x10); + writer.writeHeaders(headers); + writer.writeVersion(version); + writer.writeLongConnectionId(destination); + writer.writeLongConnectionId(source); + writer.writeRetryToken(packet.retryToken); + assert writer.remaining() >= 16; // 128 bits + writer.signRetry(version); + + assert writer.bytesWritten() == packet.size : writer.bytesWritten() - packet.size; + } + + public abstract static class OutgoingQuicPacket implements QuicPacket { + private final QuicConnectionId destinationId; + + protected OutgoingQuicPacket(QuicConnectionId destinationId) { + this.destinationId = destinationId; + } + + @Override + public final QuicConnectionId destinationId() { return destinationId; } + + @Override + public String toString() { + + return this.getClass().getSimpleName() + "[pn=" + this.packetNumber() + + ", frames=" + frames() + "]"; + } + } + + private abstract static class OutgoingShortHeaderPacket + extends OutgoingQuicPacket implements ShortHeaderPacket { + + OutgoingShortHeaderPacket(QuicConnectionId destinationId) { + super(destinationId); + } + } + + private abstract static class OutgoingLongHeaderPacket + extends OutgoingQuicPacket implements LongHeaderPacket { + + private final QuicConnectionId sourceId; + private final int version; + OutgoingLongHeaderPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version) { + super(destinationId); + this.sourceId = sourceId; + this.version = version; + } + + @Override + public final QuicConnectionId sourceId() { return sourceId; } + + @Override + public final int version() { return version; } + + } + + private static final class OutgoingRetryPacket + extends OutgoingLongHeaderPacket implements RetryPacket { + + final int size; + final byte[] retryToken; + + OutgoingRetryPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + byte[] retryToken) { + super(sourceId, destinationId, version); + this.retryToken = retryToken; + this.size = computeSize(retryToken.length); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the end of the retry integrity tag. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @return the total packet size. + */ + private int computeSize(int tokenLength) { + assert tokenLength > 0; + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), + // retryTokenIntegrity(128 bits) => 7 + 16 = 23 bytes + int size = Math.addExact(23, tokenLength); + size = Math.addExact(size, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + return size; + } + + @Override + public int size() { + return size; + } + + @Override + public byte[] retryToken() { + return retryToken; + } + } + + private static final class OutgoingHandshakePacket + extends OutgoingLongHeaderPacket implements HandshakePacket { + + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + final int payloadSize; + private int tagSize; + + OutgoingHandshakePacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.payloadSize = frames.stream().mapToInt(QuicFrame::size).reduce(0, Math::addExact); + this.tagSize = tagSize; + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computeSize(length); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param length The value of the length header + * + * @return the total packet size. + */ + private int computeSize(int length) { + assert length >= 0; + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + size = Math.addExact(size, lnsize); + size = Math.addExact(size, length); + return size; + } + + @Override + public List frames() { return frames; } + + } + + private static final class OutgoingZeroRttPacket + extends OutgoingLongHeaderPacket implements ZeroRttPacket { + + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + OutgoingZeroRttPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computeSize(length); + } + + @Override + public int length() { + return length; + } + + @Override + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param length The value of the length header + * + * @return the total packet size. + */ + private int computeSize(int length) { + assert length >= 0; + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + + size = Math.addExact(size, lnsize); + size = Math.addExact(size, length); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingOneRttPacket + extends OutgoingShortHeaderPacket implements OneRttPacket { + + final long packetNumber; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + OutgoingOneRttPacket(QuicConnectionId destinationId, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(destinationId); + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.size = computeSize(payloadSize, encodedPacketNumber.length, tagSize); + } + + public long packetNumber() { + return packetNumber; + } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { + return size; + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param payloadSize The size of the packet's payload + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag + * @return the total packet size. + */ + private int computeSize(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + // Fixed size bits: + // headers(1 byte) + int size = Math.addExact(1, destinationId().length()); + + size = Math.addExact(size, payloadSize); + size = Math.addExact(size, pnsize); + size = Math.addExact(size, tagSize); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingInitialPacket + extends OutgoingLongHeaderPacket implements InitialPacket { + + final byte[] token; + final long packetNumber; + final int length; + final int size; + final byte[] encodedPacketNumber; + final List frames; + private int tagSize; + final int payloadSize; + + private record InitialPacketVariableComponents(int length, byte[] token, QuicConnectionId sourceId, + QuicConnectionId destinationId) { + + } + + public OutgoingInitialPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int version, + byte[] token, + long packetNumber, + byte[] encodedPacketNumber, + List frames, int tagSize) { + super(sourceId, destinationId, version); + this.token = token; + this.packetNumber = packetNumber; + this.encodedPacketNumber = encodedPacketNumber; + this.frames = List.copyOf(frames); + this.tagSize = tagSize; + this.payloadSize = this.frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + this.length = computeLength(payloadSize, encodedPacketNumber.length, tagSize); + this.size = computePacketSize(new InitialPacketVariableComponents(length, token, sourceId, + destinationId)); + } + + @Override + public int tokenLength() { return token == null ? 0 : token.length; } + + @Override + public byte[] token() { return token; } + + @Override + public int length() { return length; } + + @Override + public long packetNumber() { return packetNumber; } + + public byte[] encodedPacketNumber() { + return encodedPacketNumber.clone(); + } + + @Override + public int size() { return size; } + + /** + * Computes the value for the packet length field. + * This is the number of bytes needed to encode the packetNumber + * and the payload. + * + * @param payloadSize The payload size + * @param pnsize The number of bytes needed to encode the packet number + * @param tagSize The size of the authentication tag added during encryption + * @return the value for the packet length field. + */ + private static int computeLength(int payloadSize, int pnsize, int tagSize) { + assert payloadSize >= 0; + assert pnsize > 0 && pnsize <= 4 : pnsize; + + return Math.addExact(Math.addExact(pnsize, payloadSize), tagSize); + } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param variableComponents The variable components of the packet + * + * @return the total packet size. + */ + private static int computePacketSize(InitialPacketVariableComponents variableComponents) { + assert variableComponents.length >= 0; + + // how many bytes are needed to encode the length of the token + final byte[] token = variableComponents.token; + int tkLenSpecifierSize = token == null || token.length == 0 + ? 1 : VariableLengthEncoder.getEncodedSize(token.length); + + // how many bytes are needed to encode the packet length + // the packet length is the number of bytes needed to encode + // the remainder of the packet: packet number + payload bytes + int lnsize = VariableLengthEncoder.getEncodedSize(variableComponents.length); + + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID length specifier(1 byte), + // SCID length specifier(1 byte), => 7 bytes + int size = Math.addExact(7, variableComponents.sourceId.length()); + size = Math.addExact(size, variableComponents.destinationId.length()); + size = Math.addExact(size, tkLenSpecifierSize); + if (token != null) { + size = Math.addExact(size, token.length); + } + size = Math.addExact(size, lnsize); + size = Math.addExact(size, variableComponents.length); + return size; + } + + @Override + public List frames() { + return frames; + } + + @Override + public int payloadSize() { + return payloadSize; + } + + } + + private static final class OutgoingVersionNegotiationPacket + extends OutgoingLongHeaderPacket + implements VersionNegotiationPacket { + + final int[] versions; + final int size; + final int payloadSize; + + public OutgoingVersionNegotiationPacket(QuicConnectionId sourceId, + QuicConnectionId destinationId, + int[] versions) { + super(sourceId, destinationId, 0); + this.versions = versions.clone(); + this.payloadSize = versions.length << 2; + this.size = computeSize(payloadSize); + } + + @Override + public int[] supportedVersions() { + return versions.clone(); + } + + @Override + public int size() { return size; } + + @Override + public int payloadSize() { return payloadSize; } + + /** + * Compute the total packet size, starting at the headers byte and + * ending at the last payload byte. This is used to allocate a + * ByteBuffer in which to encode the packet. + * + * @param payloadSize The size of the packet's payload + * @return the total packet size. + */ + private int computeSize(int payloadSize) { + assert payloadSize > 0; + // Fixed size bits: + // headers(1 byte), version(4 bytes), DCID(1 byte), SCID(1 byte), => 7 bytes + int size = Math.addExact(7, payloadSize); + size = Math.addExact(size, sourceId().length()); + size = Math.addExact(size, destinationId().length()); + return size; + } + + } + + /** + * Create a new unencrypted InitialPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param token The token field (may be null if no token) + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The initial packet payload + * + * @param codingContext + * @return the new initial packet + */ + public OutgoingQuicPacket newInitialPacket(QuicConnectionId source, + QuicConnectionId destination, + byte[] token, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newInitialPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + // https://www.rfc-editor.org/rfc/rfc9000#section-14.1 + // A client MUST expand the payload of all UDP datagrams carrying Initial packets + // to at least the smallest allowed maximum datagram size of 1200 bytes + // by adding PADDING frames to the Initial packet or by coalescing the Initial packet + + // first compute the packet size + final int originalPayloadSize = frames.stream() + .mapToInt(QuicFrame::size) + .reduce(0, Math::addExact); + final int originalLength = OutgoingInitialPacket.computeLength(originalPayloadSize, + encodedPacketNumber.length, tagSize); + final int originalPacketSize = OutgoingInitialPacket.computePacketSize( + new OutgoingInitialPacket.InitialPacketVariableComponents(originalLength, token, + source, destination)); + if (originalPacketSize >= 1200) { + return new OutgoingInitialPacket(source, destination, this.quicVersion.versionNumber(), + token, packetNumber, encodedPacketNumber, frames, tagSize); + } else { + // add padding + int numPaddingBytesNeeded = 1200 - originalPacketSize; + if (originalLength < 64 && originalLength + numPaddingBytesNeeded > 64) { + // if originalLength + numPaddingBytesNeeded == 64, will send + // 1201 bytes + numPaddingBytesNeeded--; + } + final List newFrames = new ArrayList<>(); + for (QuicFrame frame : frames) { + if (frame instanceof PaddingFrame) { + // a padding frame already exists, instead of including this and the new padding + // frame in the new frames, we just include 1 single padding frame whose + // combined size will be the sum of all existing padding frames and the + // additional padding bytes needed + numPaddingBytesNeeded += frame.size(); + continue; + } + // non-padding frame, include it in the new frames + newFrames.add(frame); + } + // add the padding frame as the first frame + newFrames.add(0, new PaddingFrame(numPaddingBytesNeeded)); + return new OutgoingInitialPacket( + source, destination, this.quicVersion.versionNumber(), + token, packetNumber, encodedPacketNumber, newFrames, tagSize); + } + } + + /** + * Create a new unencrypted VersionNegotiationPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param versions The supported quic versions + * @return the new initial packet + */ + public static OutgoingQuicPacket newVersionNegotiationPacket(QuicConnectionId source, + QuicConnectionId destination, + int[] versions) { + return new OutgoingVersionNegotiationPacket(source, destination, versions); + } + + /** + * Create a new unencrypted RetryPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param retryToken The retry token + * @return the new retry packet + */ + public OutgoingQuicPacket newRetryPacket(QuicConnectionId source, + QuicConnectionId destination, + byte[] retryToken) { + return new OutgoingRetryPacket( + source, destination, this.quicVersion.versionNumber(), retryToken); + } + + /** + * Create a new unencrypted ZeroRttPacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The zero RTT packet payload + * @param codingContext + * @return the new zero RTT packet + */ + public OutgoingQuicPacket newZeroRttPacket(QuicConnectionId source, + QuicConnectionId destination, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newZeroRttPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.ZERO_RTT); + int minLength = 4 + protectionSampleSize - encodedPacketNumber.length - tagSize; + + return new OutgoingZeroRttPacket( + source, destination, this.quicVersion.versionNumber(), packetNumber, + encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Create a new unencrypted HandshakePacket to be transmitted over the wire + * after encryption. + * + * @param source The source connection ID + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param frames The handshake packet payload + * @param codingContext + * @return the new handshake packet + */ + public OutgoingQuicPacket newHandshakePacket(QuicConnectionId source, + QuicConnectionId destination, + long packetNumber, + long largestAckedPN, + List frames, CodingContext codingContext) { + if (debug.on()) { + debug.log("newHandshakePacket: fullPN=%d ackedPN=%d", + packetNumber, largestAckedPN); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, largestAckedPN); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.HANDSHAKE); + int minLength = 4 + protectionSampleSize - encodedPacketNumber.length - tagSize; + + return new OutgoingHandshakePacket( + source, destination, this.quicVersion.versionNumber(), + packetNumber, encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Create a new unencrypted OneRttPacket to be transmitted over the wire + * after encryption. + * + * @param destination The destination connection ID + * @param packetNumber The packet number + * @param ackedPacketNumber The largest acknowledged packet number + * @param frames The one RTT packet payload + * @param codingContext + * @return the new one RTT packet + */ + public OneRttPacket newOneRttPacket(QuicConnectionId destination, + long packetNumber, + long ackedPacketNumber, + List frames, + CodingContext codingContext) { + if (debug.on()) { + debug.log("newOneRttPacket: fullPN=%d ackedPN=%d", + packetNumber, ackedPacketNumber); + } + byte[] encodedPacketNumber = encodePacketNumber(packetNumber, ackedPacketNumber); + QuicTLSEngine tlsEngine = codingContext.getTLSEngine(); + int tagSize = tlsEngine.getAuthTagSize(); + int protectionSampleSize = tlsEngine.getHeaderProtectionSampleSize(KeySpace.ONE_RTT); + // packets should be at least 22 bytes longer than the local connection id length. + // we ensure that by padding the frames to the necessary size + int minPayloadSize = codingContext.minShortPacketPayloadSize(destination.length()); + assert protectionSampleSize == tagSize; + int minLength = Math.max(5, minPayloadSize) - encodedPacketNumber.length; + return new OutgoingOneRttPacket( + destination, packetNumber, + encodedPacketNumber, padFrames(frames, minLength), tagSize); + } + + /** + * Creates a packet in the given keyspace for the purpose of sending + * a CONNECTION_CLOSE, or a generic list of frames. + * The {@code initialToken} parameter is ignored if the key + * space is not INITIAL. + * + * @param keySpace the sending key space + * @param packetSpace the packet space + * @param sourceId the source connection id + * @param destinationId the destination connection id + * @param initialToken the initial token for INITIAL packets + * @param frames the list of frames + * @param codingContext the coding context + * @return a packet in the given key space + * @throws IllegalArgumentException if the packet number space is + * not one of INITIAL, HANDSHAKE, or APPLICATION + */ + public OutgoingQuicPacket newOutgoingPacket( + KeySpace keySpace, + PacketSpace packetSpace, + QuicConnectionId sourceId, + QuicConnectionId destinationId, + byte[] initialToken, + List frames, CodingContext codingContext) { + long largestAckedPN = packetSpace.getLargestPeerAckedPN(); + return switch (packetSpace.packetNumberSpace()) { + case APPLICATION -> { + long newPacketNumber = packetSpace.allocateNextPN(); + if (keySpace == KeySpace.ZERO_RTT) { + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.ZERORTT)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + yield newZeroRttPacket(sourceId, + destinationId, + newPacketNumber, + largestAckedPN, + frames, + codingContext); + } else { + assert keySpace == KeySpace.ONE_RTT; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.ONERTT)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + final OneRttPacket oneRttPacket = newOneRttPacket(destinationId, + newPacketNumber, + largestAckedPN, + frames, + codingContext); + assert oneRttPacket instanceof OutgoingOneRttPacket : + "unexpected 1-RTT packet type: " + oneRttPacket.getClass(); + yield (OutgoingQuicPacket) oneRttPacket; + } + } + case HANDSHAKE -> { + assert keySpace == KeySpace.HANDSHAKE; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.HANDSHAKE)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + long newPacketNumber = packetSpace.allocateNextPN(); + yield newHandshakePacket(sourceId, destinationId, + newPacketNumber, largestAckedPN, + frames, codingContext); + } + case INITIAL -> { + assert keySpace == KeySpace.INITIAL; + assert !frames.stream().anyMatch(f -> !f.isValidIn(PacketType.INITIAL)) + : "%s contains frames not valid in %s" + .formatted(frames, keySpace); + long newPacketNumber = packetSpace.allocateNextPN(); + yield newInitialPacket(sourceId, destinationId, + initialToken, newPacketNumber, + largestAckedPN, + frames, codingContext); + } + case NONE -> { + throw new IllegalArgumentException("packetSpace: %s, keySpace: %s" + .formatted(packetSpace.packetNumberSpace(), keySpace)); + } + }; + } + + /** + * Encodes the given QuicPacket. + * + * @param packet the packet to encode + * @param buffer the byte buffer to write the packet into + * @param context context for encoding + * @throws IllegalArgumentException if the packet is not an OutgoingQuicPacket, + * or if the packet version does not match the encoder version + * @throws BufferOverflowException if the buffer is not large enough + * @throws QuicKeyUnavailableException if the packet could not be encrypted + * because the required encryption key is not available + * @throws QuicTransportException if encrypting the packet resulted + * in an error that requires closing the connection + */ + public void encode(QuicPacket packet, ByteBuffer buffer, CodingContext context) + throws QuicKeyUnavailableException, QuicTransportException { + switch (packet) { + case OutgoingOneRttPacket p -> encodePacket(p, buffer, context); + case OutgoingZeroRttPacket p -> encodePacket(p, buffer, context); + case OutgoingVersionNegotiationPacket p -> encodePacket(p, buffer); + case OutgoingHandshakePacket p -> encodePacket(p, buffer, context); + case OutgoingInitialPacket p -> encodePacket(p, buffer, context); + case OutgoingRetryPacket p -> encodePacket(p, buffer, context); + default -> throw new IllegalArgumentException("packet is not an outgoing packet: " + + packet.getClass()); + } + } + + /** + * Compute the max size of the usable payload of an initial + * packet, given the max size of the datagram. + *

        +     * Initial Packet {
        +     *     Header (1 byte),
        +     *     Version (4 bytes),
        +     *     Destination Connection ID Length (1 byte),
        +     *     Destination Connection ID (0..20 bytes),
        +     *     Source Connection ID Length (1 byte),
        +     *     Source Connection ID (0..20 bytes),
        +     *     Token Length (variable int),
        +     *     Token (..),
        +     *     Length (variable int),
        +     *     Packet Number (1..4 bytes),
        +     *     Packet Payload (1 to ... bytes),
        +     * }
        +     * 
        + * + * @param codingContext the coding context, used to compute the + * encoded packet number + * @param pnsize packet number length + * @param tokenLength the length of the token (or {@code 0}) + * @param scidLength the length of the source connection id + * @param dstidLength the length of the destination connection id + * @param maxDatagramSize the desired total maximum size + * of the packet after encryption + * @return the maximum size of the payload that can be fit into this + * initial packet + */ + public static int computeMaxInitialPayloadSize(CodingContext codingContext, + int pnsize, + int tokenLength, + int scidLength, + int dstidLength, + int maxDatagramSize) { + // header=1, version=4, len(scidlen)+len(dstidlen)=2 + int overhead = 1 + 4 + 2 + scidLength + dstidLength + tokenLength + + VariableLengthEncoder.getEncodedSize(tokenLength); + // encryption tag, included in the payload, but not usable for frames + int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + int length = maxDatagramSize - overhead - 1; // at least 1 byte for length encoding + if (length <= 0) return 0; + int lenbefore = VariableLengthEncoder.getEncodedSize(length); + length = length - lenbefore + 1; // discount length encoding + // int lenafter = VariableLengthEncoder.getEncodedSize(length); // check + // assert lenafter == lenbefore : "%s -> %s (before:%s, after:%s)" + // .formatted(maxDatagramSize - overhead -1, length, lenbefore, lenafter); + if (length <= 0) return 0; + int available = length - pnsize - tagSize; + if (available < 0) return 0; + return available; + } + + /** + * Compute the max size of the usable payload of a handshake + * packet, given the max size of the datagram. + *
        +     * Initial Packet {
        +     *     Header (1 byte),
        +     *     Version (4 bytes),
        +     *     Destination Connection ID Length (1 byte),
        +     *     Destination Connection ID (0..20 bytes),
        +     *     Source Connection ID Length (1 byte),
        +     *     Source Connection ID (0..20 bytes),
        +     *     Length (variable int),
        +     *     Packet Number (1..4 bytes),
        +     *     Packet Payload (1 to ... bytes),
        +     * }
        +     * 
        + * @param codingContext the coding context, used to compute the + * encoded packet number + * @param packetNumber the full packet number + * @param scidLength the length of the source connection id + * @param dstidLength the length of the destination connection id + * @param maxDatagramSize the desired total maximum size + * of the packet after encryption + * @return the maximum size of the payload that can be fit into this + * initial packet + */ + public static int computeMaxHandshakePayloadSize(CodingContext codingContext, + long packetNumber, + int scidLength, + int dstidLength, + int maxDatagramSize) { + // header=1, version=4, len(scidlen)+len(dstidlen)=2 + int overhead = 1 + 4 + 2 + scidLength + dstidLength; + int pnsize = computePacketNumberLength(packetNumber, + codingContext.largestAckedPN(PacketNumberSpace.HANDSHAKE)); + // encryption tag, included in the payload, but not usable for frames + int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + int length = maxDatagramSize - overhead -1; // at least 1 byte for length encoding + if (length < 0) return 0; + int lenbefore = VariableLengthEncoder.getEncodedSize(length); + length = length - lenbefore + 1; // discount length encoding + int available = length - pnsize - tagSize; + return available; + } + + /** + * Computes the maximum usable payload that can be carried on in a + * {@link OneRttPacket} given the max datagram size before + * encryption. + * @param codingContext the coding context + * @param packetNumber the packet number + * @param dstidLength the peer connection id length + * @param maxDatagramSizeBeforeEncryption the maximum size of the datagram + * @return the maximum payload that can be carried on in a + * {@link OneRttPacket} given the max datagram size before + * encryption + */ + public static int computeMaxOneRTTPayloadSize(final CodingContext codingContext, + final long packetNumber, + final int dstidLength, + final int maxDatagramSizeBeforeEncryption, + final long largestPeerAckedPN) { + // header=1 + final int overhead = 1 + dstidLength; + // always reserve four bytes for packet number to avoid issues with packet + // sizes when retransmitting. This is a hack, but it avoids having to + // repack StreamFrames. + final int pnsize = 4; //computePacketNumberLength(packetNumber, largestPeerAckedPN); + // encryption tag, included in the payload, but not usable for frames + final int tagSize = codingContext.getTLSEngine().getAuthTagSize(); + final int available = maxDatagramSizeBeforeEncryption - overhead - pnsize - tagSize; + if (available < 0) return 0; + return available; + } + + private static ByteBuffer putInt32(ByteBuffer buffer, int value) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + return buffer.putInt(value); + } + + + /** + * A {@code PacketWriter} to write a Quic packet. + *

        + * A {@code PacketWriter} offers high level helper methods to write + * data (such as Connection IDs or Packet Numbers) from a Quic packet. + * It has however no or little knowledge of the actual packet structure. + * It is driven by the {@code encode} method of the appropriate + * {@code OutgoingQuicPacket} type. + *

        + * A {@code PacketWriter} is stateful: it encapsulates a {@code ByteBuffer} + * (or possibly a list of byte buffers - as a future enhancement) and + * advances the position on the buffer it is writing. + * + */ + static class PacketWriter { + final ByteBuffer buffer; + final int offset; + final int initialLimit; + final CodingContext context; + final PacketType packetType; + + PacketWriter(ByteBuffer buffer, CodingContext context, PacketType packetType) { + assert buffer.order() == ByteOrder.BIG_ENDIAN; + int pos = buffer.position(); + int limit = buffer.limit(); + this.buffer = buffer; + this.offset = pos; + this.initialLimit = limit; + this.context = context; + this.packetType = packetType; + } + + public int offset() { + return offset; + } + + public int position() { + return buffer.position(); + } + + public int remaining() { + return buffer.remaining(); + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + public int bytesWritten() { + return position() - offset; + } + + public void reset() { + buffer.position(offset); + buffer.limit(initialLimit); + } + + public byte headers() { + return buffer.get(offset); + } + + public void headers(byte headers) { + buffer.put(offset, headers); + } + + public PacketType packetType() { + return packetType; + } + + public void writeHeaders(byte headers) { + buffer.put(headers); + } + + public void writeVersion(int version) { + buffer.putInt(version); + } + + public void writeSupportedVersions(int[] versions) { + for (int i=0 ; i= 0 && packetLength <= VariableLengthEncoder.MAX_ENCODED_INTEGER + : packetLength; + writeVariableLength(packetLength); + } + + private void writeTokenLength(long tokenLength) { + writeVariableLength(tokenLength); + } + + public void writeToken(byte[] token) { + if (token == null) { + buffer.put((byte)0); + } else { + writeTokenLength(token.length); + buffer.put(token); + } + } + + public void writeVariableLength(long value) { + VariableLengthEncoder.encode(buffer, value); + } + + private void maskPacketNumber(int packetNumberStart, int packetNumberLength, ByteBuffer mask) { + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(packetNumberStart + i, (byte)(buffer.get(packetNumberStart + i) ^ mask.get())); + } + } + + public void writeEncodedPacketNumber(byte[] packetNumber) { + buffer.put(packetNumber); + } + + public void encryptPayload(final long packetNumber, final int payloadstart) + throws QuicTransportException, QuicKeyUnavailableException { + final int payloadend = buffer.position(); + buffer.position(payloadstart); // position the output buffer + final int payloadLength = payloadend - payloadstart; + final int headersLength = payloadstart - offset; + final ByteBuffer packetHeader = buffer.slice(offset, headersLength); + final ByteBuffer packetPayload = buffer.slice(payloadstart, payloadLength) + .asReadOnlyBuffer(); + try { + context.getTLSEngine().encryptPacket(packetType.keySpace().get(), packetNumber, + new HeaderGenerator(this.packetType, packetHeader), packetPayload, buffer); + } catch (ShortBufferException e) { + throw new QuicTransportException(e.toString(), null, 0, + QuicTransportErrors.INTERNAL_ERROR); + } + } + + public void writePayload(List frames) { + for (var frame : frames) frame.encode(buffer); + } + + public void writeLongConnectionId(QuicConnectionId connId) { + ByteBuffer src = connId.asReadOnlyBuffer(); + assert src.remaining() <= MAX_CONNECTION_ID_LENGTH; + buffer.put((byte)src.remaining()); + buffer.put(src); + } + + public void writeShortConnectionId(QuicConnectionId connId) { + ByteBuffer src = connId.asReadOnlyBuffer(); + assert src.remaining() <= MAX_CONNECTION_ID_LENGTH; + buffer.put(src); + } + + public void writeRetryToken(byte[] retryToken) { + buffer.put(retryToken); + } + + @Override + public String toString() { + return "PacketWriter(offset=%s, pos=%s, remaining=%s)" + .formatted(offset, position(), remaining()); + } + + public void protectHeaderLong(int packetNumberStart, int packetNumberLength) + throws QuicKeyUnavailableException, QuicTransportException { + protectHeader(packetNumberStart, packetNumberLength, (byte) 0x0f); + } + + public void protectHeaderShort(int packetNumberStart, int packetNumberLength) + throws QuicKeyUnavailableException, QuicTransportException { + protectHeader(packetNumberStart, packetNumberLength, (byte) 0x1f); + } + + private void protectHeader(int packetNumberStart, int packetNumberLength, byte headerMask) + throws QuicKeyUnavailableException, QuicTransportException { + // expect position at the end of packet + QuicTLSEngine tlsEngine = context.getTLSEngine(); + int sampleSize = tlsEngine.getHeaderProtectionSampleSize(packetType.keySpace().get()); + assert buffer.position() - packetNumberStart >= sampleSize + 4 : buffer.position() - packetNumberStart - sampleSize - 4; + + ByteBuffer sample = buffer.slice(packetNumberStart + 4, sampleSize); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(packetType.keySpace().get(), false, sample); + byte headers = headers(); + headers ^= (byte) (encryptedSample.get() & headerMask); + headers(headers); + maskPacketNumber(packetNumberStart, packetNumberLength, encryptedSample); + } + + private void signRetry(final int version) throws QuicTransportException { + final QuicVersion retryVersion = QuicVersion.of(version) + .orElseThrow(() -> new IllegalArgumentException("Unknown Quic version 0x" + + Integer.toHexString(version))); + int payloadend = buffer.position(); + ByteBuffer temp = buffer.asReadOnlyBuffer(); + temp.position(offset); + temp.limit(payloadend); + try { + context.getTLSEngine().signRetryPacket(retryVersion, + context.originalServerConnId().asReadOnlyBuffer(), temp, buffer); + } catch (ShortBufferException e) { + throw new QuicTransportException("Failed to sign packet", + null, 0, QuicTransportErrors.INTERNAL_ERROR); + } + } + + // generates packet header and is capable of inserting a key phase into the header + // when appropriate + private static final class HeaderGenerator implements IntFunction { + private final PacketType packetType; + private final ByteBuffer header; + + private HeaderGenerator(final PacketType packetType, final ByteBuffer header) { + this.packetType = packetType; + this.header = header; + } + + @Override + public ByteBuffer apply(final int keyPhase) { + // we use key phase only in 1-RTT packet header + if (packetType != PacketType.ONERTT) { + assert keyPhase == 0 : "unexpected key phase " + keyPhase + + " for packet type " + packetType; + // return the packet header without setting any key phase bit + return header; + } + // update the key phase bit in the packet header + setKeyPhase(keyPhase); + return header.position(0).asReadOnlyBuffer(); + } + + private void setKeyPhase(final int kp) { + if (kp != 0 && kp != 1) { + throw new IllegalArgumentException("Invalid key phase: " + kp); + } + final byte headerFirstByte = this.header.get(); + final byte updated = (byte) (headerFirstByte | (kp << 2)); + this.header.put(0, updated); + } + } + } + + + /** + * Adds required padding frames if necessary. + * Needed to make sure there's enough bytes to apply header protection + * @param frames requested list of frames + * @param minLength requested minimum length + * @return list of frames that meets the minimum length requirement + */ + private static List padFrames(List frames, int minLength) { + if (frames.size() >= minLength) { + return frames; + } + int size = frames.stream().mapToInt(QuicFrame::size).reduce(0, Math::addExact); + if (size >= minLength) { + return frames; + } + List result = new ArrayList<>(frames.size() + 1); + // add padding frame in front - some frames extend to end of packet + result.add(new PaddingFrame(minLength - size)); + result.addAll(frames); + return result; + } + + /** + * Returns an encoder for the given Quic version. + * Returns {@code null} if no encoder for that version exists. + * + * @param quicVersion the Quic protocol version number + * @return an encoder for the given Quic version or {@code null} + */ + public static QuicPacketEncoder of(final QuicVersion quicVersion) { + return switch (quicVersion) { + case QUIC_V1 -> Encoders.QUIC_V1_ENCODER; + case QUIC_V2 -> Encoders.QUIC_V2_ENCODER; + default -> throw new IllegalArgumentException("No packet encoder for Quic version " + quicVersion); + }; + } + + private static final class Encoders { + static final Random RANDOM = new Random(); + static final QuicPacketEncoder QUIC_V1_ENCODER = new QuicPacketEncoder(QuicVersion.QUIC_V1); + static final QuicPacketEncoder QUIC_V2_ENCODER = new QuicPacketEncoder(QuicVersion.QUIC_V2); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java new file mode 100644 index 00000000000..197d46fc0b0 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/QuicPacketNumbers.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.nio.ByteBuffer; + +/** + * QUIC packet number encoding/decoding routines. + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public class QuicPacketNumbers { + + /** + * Returns the number of bytes needed to encode a packet number + * given the full packet number and the largest ACK'd packet. + * + * @param fullPN the full packet number + * @param largestAcked the largest ACK'd packet, or -1 if none so far + * + * @throws IllegalArgumentException if number can't represented in 4 bytes + * @return the number of bytes required to encode the packet + */ + public static int computePacketNumberLength(long fullPN, long largestAcked) { + + long numUnAcked; + + if (largestAcked == -1) { + numUnAcked = fullPN + 1; + } else { + numUnAcked = fullPN - largestAcked; + } + + /* + * log(n, 2) + 1; ceil(minBits / 8); + * + * value will never be non-positive, so don't need to worry about the + * special cases. + */ + assert numUnAcked > 0 : "numUnAcked %s < 0 (fullPN: %s, largestAcked: %s)" + .formatted(numUnAcked, fullPN, largestAcked); + int minBits = 64 - Long.numberOfLeadingZeros(numUnAcked) + 1; + int numBytes = (minBits + 7) / 8; + + if (numBytes > 4) { + throw new IllegalArgumentException( + "Encoded packet number needs %s bytes for pn=%s, ack=%s" + .formatted(numBytes, fullPN, largestAcked)); + } + + return numBytes; + } + + /** + * Encode the full packet number against the largest ACK'd packet. + * + * Follows the algorithm outlined in + * + * RFC 9000. Appendix A.2 + * + * @param fullPN the full packet number + * @param largestAcked the largest ACK'd packet, or -1 if none so far + * + * @throws IllegalArgumentException if number can't be represented in 4 bytes + * @return byte array containing fullPN + */ + public static byte[] encodePacketNumber( + long fullPN, long largestAcked) { + + // throws IAE if more than 4 bytes are needed + int numBytes = computePacketNumberLength(fullPN, largestAcked); + assert numBytes <= 4 : numBytes; + return truncatePacketNumber(fullPN, numBytes); + } + + /** + * Truncate the full packet number to fill into {@code numBytes}. + * + * Follows the algorithm outlined in + * + * RFC 9000, Appendix A.2 + * + * @apiNote + * {@code numBytes} should have been computed using + * {@link #computePacketNumberLength(long, long)} + * + * @param fullPN the full packet number + * @param numBytes the number of bytes in which to encode + * the packet number + * + * @throws IllegalArgumentException if numBytes is out of range + * @return byte array containing fullPN + */ + public static byte[] truncatePacketNumber( + long fullPN, int numBytes) { + + if (numBytes <= 0 || numBytes > 4) { + throw new IllegalArgumentException( + "Invalid packet number length: " + numBytes); + } + + // Fill in the array. + byte[] retval = new byte[numBytes]; + for (int i = numBytes - 1; i >= 0; i--) { + retval[i] = (byte) (fullPN & 0xff); + fullPN = fullPN >>> 8; + } + + return retval; + } + + /** + * Decode the packet numbers against the largest ACK'd packet after header + * protection has been removed. + * + * Follows the algorithm outlined in + * + * RFC 9000, Appendix A.3 + * + * @param largestPN the largest packet number that has been successfully + * processed in the current packet number space + * @param buf a {@code ByteBuffer} containing the value of the + * Packet Number field + * @param pnNBytes the number of bytes indicated by the Packet + * Number Length field + * + * @throws java.nio.BufferUnderflowException if there is not enough data in the + * buffer + * @return the decoded packet number + */ + public static long decodePacketNumber( + long largestPN, ByteBuffer buf, int pnNBytes) { + + assert pnNBytes >= 1 && pnNBytes <= 4 + : "decodePacketNumber: " + pnNBytes; + + long truncatedPN = 0; + for (int i = 0; i < pnNBytes; i++) { + truncatedPN = (truncatedPN << 8) | (buf.get() & 0xffL); + } + + int pnNBits = pnNBytes * 8; + + long expectedPN = largestPN + 1L; + assert expectedPN >= 0 : "expectedPN: " + expectedPN; + long pnWin = 1L << pnNBits; + long pnHWin = pnWin / 2L; + long pnMask = pnWin - 1L; + + // The incoming packet number should be greater than + // expectedPN - pn_HWin and less than or equal to + // expectedPN + pn_HWin + // + // This means we cannot just strip the trailing bits from + // expectedPN and add the truncatedPN because that might + // yield a value outside the window. + // + // The following code calculates a candidate value and + // makes sure it's within the packet number window. + // Note the extra checks to prevent overflow and underflow. + long candidatePN = (expectedPN & ~pnMask) | truncatedPN; + + if ((candidatePN <= (expectedPN - pnHWin)) + && (candidatePN < ((1L << 62) - pnWin))) { + return candidatePN + pnWin; + } + + if ((candidatePN - pnHWin > expectedPN) + && (candidatePN >= pnWin)) { + return candidatePN - pnWin; + } + return candidatePN; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java new file mode 100644 index 00000000000..51638caf98c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/RetryPacket.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This class models Quic Retry Packets, as defined by + * RFC 9000, Section 17.2.5: + * + *

        {@code
        + *    A Retry packet uses a long packet header with a type value of 0x03.
        + *    It carries an address validation token created by the server.
        + *    It is used by a server that wishes to perform a retry; see Section 8.1.
        + *
        + *    Retry Packet {
        + *      Header Form (1) = 1,
        + *      Fixed Bit (1) = 1,
        + *      Long Packet Type (2) = 3,
        + *      Unused (4),
        + *      Version (32),
        + *      Destination Connection ID Length (8),
        + *      Destination Connection ID (0..160),
        + *      Source Connection ID Length (8),
        + *      Source Connection ID (0..160),
        + *      Retry Token (..),
        + *      Retry Integrity Tag (128),
        + *    }
        + * }
        + * + *

        Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same Retry Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see RFC 9000, Section 8.1/a> + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface RetryPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.RETRY; + } + + /** + * This packet type is not numbered: returns + * {@link PacketNumberSpace#NONE} always. + * @return {@link PacketNumberSpace#NONE} + */ + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.NONE; + } + + /** + * This packet type is not numbered: always returns -1L. + * @return -1L + */ + @Override + default long packetNumber() { return -1L; } + + /** + * {@return the packet's retry token} + * + * As per RFC 9000, Section 17.2.5: + *

        {@code
        +     *    An opaque token that the server can use to validate the client's address.
        +     * }
        + * + * @see + * RFC 9000, Section 8.1 + */ + byte[] retryToken(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java new file mode 100644 index 00000000000..a56689e6a0b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ShortHeaderPacket.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This interface models Quic Short Header packets, as defined by + * RFC 8999, Section 5.2: + * + *
        {@code
        + *    Short Header Packet {
        + *      Header Form (1) = 0,
        + *      Version-Specific Bits (7),
        + *      Destination Connection ID (..),
        + *      Version-Specific Data (..),
        + *    }
        + * }
        + * + *

        Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * + * @spec https://www.rfc-editor.org/info/rfc8999 + * RFC 8999: Version-Independent Properties of QUIC + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface ShortHeaderPacket extends QuicPacket { + @Override + default HeadersType headersType() { return HeadersType.SHORT; } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java new file mode 100644 index 00000000000..0ba5ba08c7e --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/VersionNegotiationPacket.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +/** + * This class models Quic Version Negotiation Packets, as defined by + * RFC 9000, Section 17.2.1: + * + *

        {@code
        + *    A Version Negotiation packet is inherently not version-specific.
        + *    Upon receipt by a client, it will be identified as a Version
        + *    Negotiation packet based on the Version field having a value of 0.
        + *
        + *    The Version Negotiation packet is a response to a client packet that
        + *    contains a version that is not supported by the server, and is only
        + *    sent by servers.
        + *
        + *    The layout of a Version Negotiation packet is:
        + *
        + *    Version Negotiation Packet {
        + *      Header Form (1) = 1,
        + *      Unused (7),
        + *      Version (32) = 0,
        + *      Destination Connection ID Length (8),
        + *      Destination Connection ID (0..2040),
        + *      Source Connection ID Length (8),
        + *      Source Connection ID (0..2040),
        + *      Supported Version (32) ...,
        + *    }
        + * }
        + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + */ +public interface VersionNegotiationPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { return PacketType.VERSIONS; } + @Override + default int version() { return 0;} + /** + * This packet type is not numbered: returns + * {@link PacketNumberSpace#NONE} always. + * @return {@link PacketNumberSpace#NONE} + */ + @Override + default PacketNumberSpace numberSpace() { return PacketNumberSpace.NONE; } + /** + * This packet type is not numbered: returns -1L always. + * @return -1L + */ + @Override + default long packetNumber() { return -1L; } + int[] supportedVersions(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java new file mode 100644 index 00000000000..c680aae1486 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/packets/ZeroRttPacket.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.packets; + +import java.util.List; + +import jdk.internal.net.http.quic.frames.QuicFrame; + +/** + * This class models Quic 0-RTT Packets, as defined by + * RFC 9000, Section 17.2.3: + * + *
        {@code
        + *    A 0-RTT packet uses long headers with a type value of 0x01, followed
        + *    by the Length and Packet Number fields; see Section 17.2. The first
        + *    byte contains the Reserved and Packet Number Length bits; see Section 17.2.
        + *    A 0-RTT packet is used to carry "early" data from the client to the server
        + *    as part of the first flight, prior to handshake completion. As part of the
        + *    TLS handshake, the server can accept or reject this early data.
        + *
        + *    See Section 2.3 of [TLS13] for a discussion of 0-RTT data and its limitations.
        +
        + *    0-RTT Packet {
        + *      Header Form (1) = 1,
        + *      Fixed Bit (1) = 1,
        + *      Long Packet Type (2) = 1,
        + *      Reserved Bits (2),
        + *      Packet Number Length (2),
        + *      Version (32),
        + *      Destination Connection ID Length (8),
        + *      Destination Connection ID (0..160),
        + *      Source Connection ID Length (8),
        + *      Source Connection ID (0..160),
        + *      Length (i),
        + *      Packet Number (8..32),
        + *      Packet Payload (..),
        + *    }
        + * } 
        + * + *

        Subclasses of this class may be used to model packets exchanged with either + * Quic Version 2. + * Note that Quic Version 2 uses the same 0-RTT Packet structure than + * Quic Version 1, but uses a different long packet type than that shown above. See + * RFC 9369, Section 3.2. + * + * @see + * RFC 9000, Section 17.2 + * + * @see + * [TLS13] RFC 8446, Section 2.3 + * + * @spec https://www.rfc-editor.org/info/rfc9000 + * RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport + * @spec https://www.rfc-editor.org/info/rfc9369 + * RFC 9369: QUIC Version 2 + */ +public interface ZeroRttPacket extends LongHeaderPacket { + @Override + default PacketType packetType() { + return PacketType.ZERORTT; + } + + @Override + default PacketNumberSpace numberSpace() { + return PacketNumberSpace.APPLICATION; + } + + @Override + default boolean hasLength() { return true; } + + /** + * This packet number. + * @return this packet number. + */ + @Override + long packetNumber(); + + @Override + List frames(); +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java new file mode 100644 index 00000000000..e1a7fce1059 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/AbstractQuicStream.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * An abstract class to model a QuicStream. + * A quic stream can be either unidirectional + * or bidirectional. A unidirectional stream can + * be opened for reading or for writing. + * Concrete subclasses of {@code AbstractQuicStream} should + * implement {@link QuicSenderStream} (unidirectional {@link + * StreamMode#WRITE_ONLY} stream), or {@link QuicReceiverStream} + * (unidirectional {@link StreamMode#READ_ONLY} stream), or + * both (bidirectional {@link StreamMode#READ_WRITE} stream). + */ +abstract sealed class AbstractQuicStream implements QuicStream + permits QuicBidiStreamImpl, QuicSenderStreamImpl, QuicReceiverStreamImpl { + + private final QuicConnectionImpl connection; + private final long streamId; + private final StreamMode mode; + + AbstractQuicStream(QuicConnectionImpl connection, long streamId) { + this.mode = mode(connection, streamId); + this.streamId = streamId; + this.connection = connection; + } + + private static StreamMode mode(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return StreamMode.READ_WRITE; + if (connection.isClientConnection()) { + return QuicStreams.isClientInitiated(streamId) + ? StreamMode.WRITE_ONLY : StreamMode.READ_ONLY; + } else { + return QuicStreams.isClientInitiated(streamId) + ? StreamMode.READ_ONLY : StreamMode.WRITE_ONLY; + } + } + + /** + * {@return the {@code QuicConnectionImpl} instance this stream + * belongs to} + */ + final QuicConnectionImpl connection() { + return connection; + } + + @Override + public final long streamId() { + return streamId; + } + + @Override + public final StreamMode mode() { + return mode; + } + + @Override + public final boolean isClientInitiated() { + return QuicStreams.isClientInitiated(type()); + } + + @Override + public final boolean isServerInitiated() { + return QuicStreams.isServerInitiated(type()); + } + + @Override + public final boolean isBidirectional() { + return QuicStreams.isBidirectional(type()); + } + + @Override + public final boolean isLocalInitiated() { + return connection().isClientConnection() == isClientInitiated(); + } + + @Override + public final boolean isRemoteInitiated() { + return connection().isClientConnection() != isClientInitiated(); + } + + @Override + public final int type() { + return QuicStreams.streamType(streamId); + } + + /** + * {@return true if this stream isn't expecting anything + * from the peer and can be removed from the streams map} + */ + public abstract boolean isDone(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java new file mode 100644 index 00000000000..cbbbf6d083d --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/CryptoWriterQueue.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Queue; + +/** + * Class that buffers crypto data received from QuicTLSEngine. + * Generates CryptoFrames of requested size. + * + * Normally the frames are produced sequentially. However, when the client + * receives a Retry packet or a Version Negotiation packet, the client hello + * needs to be replayed. In that case we need to keep the processed data + * in the queues. + */ +public class CryptoWriterQueue { + private final Queue queue = new ArrayDeque<>(); + private long position = 0; + // amount of bytes remaining across all the enqueued buffers + private int totalRemaining = 0; + private boolean keepReplayData; + + /** + * Notify the writer to start keeping processed data. Can only be called on a fresh writer. + * @throws IllegalStateException if some data was processed already + */ + public synchronized void keepReplayData() { + if (position > 0) { + throw new IllegalStateException("Some data was processed already"); + } + keepReplayData = true; + } + + /** + * Notify the writer to stop keeping processed data. + */ + public synchronized void discardReplayData() { + if (!keepReplayData) { + return; + } + keepReplayData = false; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + ByteBuffer next = iterator.next(); + if (next.remaining() == 0) { + iterator.remove(); + } else { + return; + } + } + } + + /** + * Rewinds the enqueued buffer positions to allow for replaying the data + * @throws IllegalStateException if replay data is not available + */ + public synchronized void replayData() { + if (!keepReplayData) { + throw new IllegalStateException("Replay data not available"); + } + if (position == 0) { + return; + } + int rewound = 0; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + ByteBuffer next = iterator.next(); + if (next.position() != 0) { + rewound += next.position(); + next.position(0); + } else { + break; + } + } + assert rewound == position : rewound - position; + position = 0; + totalRemaining += rewound; + } + + /** + * Clears the queue and resets position back to zero + */ + public synchronized void reset() { + position = 0; + totalRemaining = 0; + queue.clear(); + } + + /** + * Enqueues the provided crypto data + * @param buffer data to enqueue + */ + public synchronized void enqueue(ByteBuffer buffer) { + queue.add(buffer.slice()); + totalRemaining += buffer.remaining(); + } + + /** + * Stores the next portion of queued crypto data in a frame. + * May return null if there's no data to enqueue or if + * maxSize is too small to fit at least one byte of data. + * The produced frame may be shorter than maxSize even if there are + * remaining bytes. + * @param maxSize maximum size of the returned frame, in bytes + * @return frame with next portion of crypto data, or null + * @throws IllegalArgumentException if maxSize < 0 + */ + public synchronized CryptoFrame produceFrame(int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException("negative maxSize"); + } + if (totalRemaining == 0) { + return null; + } + int posLength = VariableLengthEncoder.getEncodedSize(position); + // 1 (type) + posLength (position) + 1 (length) + 1 (payload) + if (maxSize < 3 + posLength) { + return null; + } + int maxPayloadPlusLen = maxSize - 1 - posLength; + int maxPayload; + if (maxPayloadPlusLen <= 64) { //63 bytes + 1 byte for length + maxPayload = maxPayloadPlusLen - 1; + } else if (maxPayloadPlusLen <= 16385) { // 16383 bytes + 2 bytes for length + maxPayload = maxPayloadPlusLen - 2; + } else { // 4 bytes for length + maxPayload = maxPayloadPlusLen - 4; + } + // the frame length that we decide upon + final int computedFrameLength = Math.min(maxPayload, totalRemaining); + assert computedFrameLength > 0 : computedFrameLength; + ByteBuffer frameData = null; + for (Iterator iterator = queue.iterator(); iterator.hasNext(); ) { + final ByteBuffer buffer = iterator.next(); + // amount of remaining bytes in the current bytebuffer being processed + final int numRemainingInBuffer = buffer.remaining(); + if (numRemainingInBuffer == 0) { + if (!keepReplayData) { + iterator.remove(); + } + continue; + } + if (frameData == null) { + frameData = ByteBuffer.allocate(computedFrameLength); + } + if (frameData.remaining() >= numRemainingInBuffer) { + // frame data can accommodate the entire buffered data, so copy it over + frameData.put(buffer); + if (!keepReplayData) { + iterator.remove(); + } + } else { + // target frameData buffer cannot accommodate the entire buffered data, + // so we copy over only that much that the target buffer can accommodate + + // amount of data available in the target buffer + final int spaceAvail = frameData.remaining(); + // copy over the buffered data into the target frameData buffer + frameData.put(frameData.position(), buffer, buffer.position(), spaceAvail); + // manually move the position of the target buffer to account for the copied data + frameData.position(frameData.position() + spaceAvail); + // manually move the position of the (input) buffered data to account for + // data that we just copied + buffer.position(buffer.position() + spaceAvail); + // target frameData buffer is fully populated, no more processing of available + // input buffer necessary in this round + break; + } + } + assert frameData != null; + assert !frameData.hasRemaining() : frameData.remaining(); + frameData.flip(); + long oldPosition = position; + position += computedFrameLength; + totalRemaining -= computedFrameLength; + assert totalRemaining >= 0 : totalRemaining; + assert totalRemaining > 0 || keepReplayData || queue.isEmpty(); + return new CryptoFrame(oldPosition, computedFrameLength, frameData); + } + + /** + * {@return the current number of buffered bytes} + */ + public synchronized int remaining() { + return totalRemaining; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java new file mode 100644 index 00000000000..2185507d0db --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStream.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +/** + * An interface that represents a bidirectional stream. + * A bidirectional stream implements both {@link QuicSenderStream} + * and {@link QuicReceiverStream}. + */ +public non-sealed interface QuicBidiStream extends QuicStream, QuicReceiverStream, QuicSenderStream { + + /** + * The state of a bidirectional stream can be obtained by combining + * the state of its sending part and receiving part. + * + * A bidirectional stream is composed of sending and receiving + * parts. Implementations can represent states of the bidirectional + * stream as composites of sending and receiving stream states. + * The simplest model presents the stream as "open" when either + * sending or receiving parts are in a non-terminal state and + * "closed" when both sending and receiving streams are in + * terminal states. + * + * See RFC 9000, [Section 3.4] + * (https://www.rfc-editor.org/rfc/rfc9000#name-bidirectional-stream-states) + */ + enum BidiStreamState implements QuicStream.StreamState { + /** + * A bidirectional stream is considered "idle" if no + * data has been sent or received on that stream. + */ + IDLE, + /** + * A bidirectional stream is considered "open" until all data + * has been received, or all data has been sent, and no reset + * has been sent or received. + */ + OPENED, + /** + * A bidirectional stream is considered locally half closed + * if the sending part is locally closed: + * all data has been sent and acknowledged, or a reset has + * been sent, but the receiving part is still receiving. + */ + HALF_CLOSED_LOCAL, + /** + * A bidirectional stream is considered remotely half closed + * if the receiving part is closed: + * all data has been read or received on the receiving part, + * or reset has been read or received on the receiving part, but + * the sending part is still sending. + */ + HALF_CLOSED_REMOTE, + /** + * A bidirectional stream is considered closed when both parts + * have been reset or all data has been sent and acknowledged + * and all data has been received. + */ + CLOSED; + + /** + * @inheritDoc + * @apiNote + * A bidirectional stream may be considered closed (which is a terminal state), + * even if the sending or receiving part of a stream haven't reached a terminal + * state. Typically, if the sending part has sent a RESET frame, the stream + * may be considered closed even if the acknowledgement hasn't been received + * yet. + */ + @Override + public boolean isTerminal() { + return this == CLOSED; + } + } + + /** + * {@return a composed simplified state computed from the state of + * the receiving part and sending part of the stream} + *

        + * See RFC 9000, [Section 3.4] + * (https://www.rfc-editor.org/rfc/rfc9000#name-bidirectional-stream-states) + */ + default BidiStreamState getBidiStreamState() { + return switch (sendingState()) { + case READY -> switch (receivingState()) { + case RECV -> dataReceived() == 0 + ? BidiStreamState.IDLE + : BidiStreamState.OPENED; + case SIZE_KNOWN -> BidiStreamState.OPENED; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.HALF_CLOSED_REMOTE; + }; + case SEND, DATA_SENT -> switch (receivingState()) { + case RECV, SIZE_KNOWN -> BidiStreamState.OPENED; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.HALF_CLOSED_REMOTE; + }; + case DATA_RECVD, RESET_RECVD, RESET_SENT -> switch (receivingState()) { + case RECV, SIZE_KNOWN -> BidiStreamState.HALF_CLOSED_LOCAL; + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ + -> BidiStreamState.CLOSED; + }; + }; + } + + @Override + default StreamState state() { return getBidiStreamState(); } + + @Override + default boolean hasError() { + return rcvErrorCode() >= 0 || sndErrorCode() >= 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java new file mode 100644 index 00000000000..a964d96ef9c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicBidiStreamImpl.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * An implementation of a bidirectional stream. + * A bidirectional stream implements both {@link QuicSenderStream} + * and {@link QuicReceiverStream}. + */ +public final class QuicBidiStreamImpl extends AbstractQuicStream implements QuicBidiStream { + + // The sender part of this bidirectional stream + private final QuicSenderStreamImpl senderPart; + + // The receiver part of this bidirectional stream + private final QuicReceiverStreamImpl receiverPart; + + QuicBidiStreamImpl(QuicConnectionImpl connection, long streamId) { + this(connection, streamId, new QuicSenderStreamImpl(connection, streamId), + new QuicReceiverStreamImpl(connection, streamId)); + } + + private QuicBidiStreamImpl(QuicConnectionImpl connection, long streamId, + QuicSenderStreamImpl sender, QuicReceiverStreamImpl receiver) { + super(connection, streamId); + this.senderPart = sender; + this.receiverPart = receiver; + assert isBidirectional(); + } + + @Override + public ReceivingStreamState receivingState() { + return receiverPart.receivingState(); + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + return receiverPart.connectReader(scheduler); + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + receiverPart.disconnectReader(reader); + } + + @Override + public void requestStopSending(long errorCode) { + receiverPart.requestStopSending(errorCode); + } + + @Override + public boolean isStopSendingRequested() { + return receiverPart.isStopSendingRequested(); + } + + @Override + public long dataReceived() { + return receiverPart.dataReceived(); + } + + @Override + public long maxStreamData() { + return receiverPart.maxStreamData(); + } + + @Override + public SendingStreamState sendingState() { + return senderPart.sendingState(); + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + return senderPart.connectWriter(scheduler); + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + senderPart.disconnectWriter(writer); + } + + @Override + public void reset(long errorCode) throws IOException { + senderPart.reset(errorCode); + } + + @Override + public long dataSent() { + return senderPart.dataSent(); + } + + /** + * {@return the sender part implementation of this bidirectional stream} + */ + public QuicSenderStreamImpl senderPart() { + return senderPart; + } + + /** + * {@return the receiver part implementation of this bidirectional stream} + */ + public QuicReceiverStreamImpl receiverPart() { + return receiverPart; + } + + @Override + public boolean isDone() { + return receiverPart.isDone() && senderPart.isDone(); + } + + @Override + public long rcvErrorCode() { + return receiverPart.rcvErrorCode(); + } + + @Override + public long sndErrorCode() { + return senderPart.sndErrorCode(); + } + + @Override + public boolean stopSendingReceived() { + return senderPart.stopSendingReceived(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java new file mode 100644 index 00000000000..3b6198682df --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicConnectionStreams.java @@ -0,0 +1,1590 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicStreamLimitException; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.frames.StreamsBlockedFrame; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.KeySpace; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.http.quic.frames.MaxStreamDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamsFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.StopSendingFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState; +import jdk.internal.net.http.quic.streams.QuicStream.StreamMode; +import jdk.internal.net.http.quic.streams.QuicStream.StreamState; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.quic.QuicTransportErrors; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static jdk.internal.net.http.quic.streams.QuicStreams.*; + +/** + * A helper class to help manage Quic streams in a Quic connection + */ +public final class QuicConnectionStreams { + + // the (sliding) window size of MAX_STREAMS limit + private static final long MAX_BIDI_STREAMS_WINDOW_SIZE = QuicConnectionImpl.DEFAULT_MAX_BIDI_STREAMS; + private static final long MAX_UNI_STREAMS_WINDOW_SIZE = QuicConnectionImpl.DEFAULT_MAX_UNI_STREAMS; + + // These atomic long ids record the expected next stream ID that + // should be allocated for the next stream of a given type. + // The type of a stream is a number in [0..3], and is used + // as an index in this list. + private final List nextStreamID = List.of( + new AtomicLong(), // 0: client initiated bidi + new AtomicLong(SRV_MASK), // 1: server initiated bidi + new AtomicLong(UNI_MASK), // 2: client initiated uni + new AtomicLong(UNI_MASK | SRV_MASK)); // 3: server initiated uni + + // the max uni streams that the current endpoint is allowed to initiate against the peer + private final StreamCreationPermit localUniMaxStreamLimit = new StreamCreationPermit(0); + // the max bidi streams that the current endpoint is allowed to initiate against the peer + private final StreamCreationPermit localBidiMaxStreamLimit = new StreamCreationPermit(0); + // the max uni streams that the remote peer is allowed to initiate against the current endpoint + private final AtomicLong remoteUniMaxStreamLimit = new AtomicLong(0); + // the max bidi streams that the remote peer is allowed to initiate against the current endpoint + private final AtomicLong remoteBidiMaxStreamLimit = new AtomicLong(0); + + private final StreamsContainer streams = new StreamsContainer(); + + // A collection of senders which have available data ready to send, (or which possibly + // are blocked and need to send STREAM_DATA_BLOCKED). + // A stream stays in the queue until it is blocked or until it + // has no more data available to send: when a stream has no more data available for sending it is not + // put back in the queue. It will be put in the queue again when selectForSending is called. + private final ReadyStreamCollection sendersReady; + + // A map that contains streams for which sending a RESET_STREAM frame was requested + // and their corresponding error codes. + // Once the frame has been sent (or has been scheduled to be sent) the stream removed from the map. + private final ConcurrentMap sendersReset = new ConcurrentHashMap<>(); + + // A map that contains streams for which sending a MAX_STREAM_DATA frame was requested. + // Once the frame has been sent (or has been scheduled to be sent) the stream removed from the map. + private final ConcurrentMap receiversSend = new ConcurrentHashMap<>(); + + // A queue of remote initiated streams that have not been acquired yet. + // see pollNewRemoteStreams and addRemoteStreamListener + private final ConcurrentLinkedQueue newRemoteStreams = new ConcurrentLinkedQueue<>(); + // A set of listeners listening to new streams created by the peer + private final Set> streamListeners = ConcurrentHashMap.newKeySet(); + // A lock to ensure consistency between invocation of streamListeners and + // the content of the newRemoteStreams queue. + private final Lock newRemoteStreamsLock = new ReentrantLock(); + + // The connection to which the streams managed by this + // instance of QuicConnectionStreams belong to. + private final QuicConnectionImpl connection; + + // will hold the highest limit from a STREAMS_BLOCKED frame that was sent by a peer for uni + // streams. this indicates the peer isn't able to create any more uni streams, past this limit + private final AtomicLong peerUniStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit from a STREAMS_BLOCKED frame that was sent by a peer for bidi + // streams. this indicates the peer isn't able to create any more bidi streams, past this limit + private final AtomicLong peerBidiStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit at which the local endpoint couldn't create a uni stream + // and a STREAMS_BLOCKED was required to be sent. -1 indicates the local endpoint hasn't yet + // been blocked for stream creation + private final AtomicLong uniStreamsBlocked = new AtomicLong(-1); + // will hold the highest limit at which the local endpoint couldn't create a bidi stream + // and a STREAMS_BLOCKED was required to be sent. -1 indicates the local endpoint hasn't yet + // been blocked for stream creation + private final AtomicLong bidiStreamsBlocked = new AtomicLong(-1); + // will hold the limit with which the local endpoint last sent a STREAMS_BLOCKED frame to the + // peer for uni streams. -1 indicates no STREAMS_BLOCKED frame has been sent yet. A new + // STREAMS_BLOCKED will be sent only if "uniStreamsBlocked" exceeds this + // "lastUniStreamsBlockedSent" + private final AtomicLong lastUniStreamsBlockedSent = new AtomicLong(-1); + // will hold the limit with which the local endpoint last sent a STREAMS_BLOCKED frame to the + // peer for bidi streams. -1 indicates no STREAMS_BLOCKED frame has been sent yet. A new + // STREAMS_BLOCKED will be sent only if "bidiStreamsBlocked" exceeds this + // "lastBidiStreamsBlockedSent" + private final AtomicLong lastBidiStreamsBlockedSent = new AtomicLong(-1); + // streams that have been blocked and aren't able to send data to the peer, + // due to reaching flow control limit imposed on those streams by the peer. + private final Set flowControlBlockedStreams = Collections.synchronizedSet(new HashSet<>()); + + private final Logger debug; + + // A QuicConnectionStream instance can be tied to a client connection + // or a server connection. + // If the connection is a client connection, then localFlag=0x00, + // localBidi=0x00, remoteBidi=0x01, localUni=0x02, remoteUni=0x03 + // If the connection is a server connection, then localFlag=0x01, + // localBidi=0x01, remoteBidi=0x00, localUni=0x03, remoteUni=0x02 + private final int localFlag, localBidi, remoteBidi, localUni, remoteUni; + + /** + * Creates a new instance of {@code QuicConnectionStreams} for the + * given connection. There is a 1-1 relationship between a + * {@code QuicConnectionImpl} instance and a {@code QuicConnectionStreams} + * instance. + * @param connection the connection to which the streams managed by this + * instance of {@code QuicConnectionStreams} belong. + */ + public QuicConnectionStreams(QuicConnectionImpl connection, Logger debug) { + this.connection = connection; + this.debug = Objects.requireNonNull(debug); + // implicit null check for connection + boolean isClient = connection.isClientConnection(); + localFlag = isClient ? 0 : SRV_MASK; + localBidi = isClient ? 0 : SRV_MASK; + remoteBidi = isClient ? SRV_MASK : 0; + localUni = isClient ? UNI_MASK : UNI_MASK | SRV_MASK; + remoteUni = isClient ? UNI_MASK | SRV_MASK : UNI_MASK; + sendersReady = isClient ? + new ReadyStreamQueue() : // faster stream opening + new ReadyStreamSortedQueue(); // faster stream closing + } + + /** + * {@return the next unallocated stream ID that would be expected + * for a stream of the given type} + * This method expects {@code streamType} to be a number in [0..3] but + * does not check it. An assert may be fired if an invalid type is passed. + * @param streamType The stream type, a number in [0..3] + */ + public long peekNextStreamId(int streamType) { + assert streamType >= 0 && streamType < 4; + var id = nextStreamID.get(streamType & 0x03); + return id.get(); + } + + /** + * Creates a new locally initiated unidirectional stream. + *

        + * If the stream cannot be created due to stream creation limit being reached, then this method + * will return a {@code CompletableFuture} which will complete either when the {@code timeout} + * has reached or the stream limit has been increased and the stream creation was successful. + * If the stream creation doesn't complete within the specified timeout then the returned + * {@code CompletableFuture} will complete exceptionally with a {@link QuicStreamLimitException} + * + * @param timeout the maximum duration to wait to acquire a permit for stream creation + * @return a CompletableFuture whose result on successful completion will return the newly + * created {@code QuicSenderStream} + */ + public CompletableFuture createNewLocalUniStream(final Duration timeout) { + @SuppressWarnings("unchecked") + final var streamCF = (CompletableFuture) createNewLocalStream(localUni, + StreamMode.WRITE_ONLY, timeout); + return streamCF; + } + + /** + * Creates a new locally initiated bidirectional stream. + *

        + * If the stream cannot be created due to stream creation limit being reached, then this method + * will return a {@code CompletableFuture} which will complete either when the {@code timeout} + * has reached or the stream limit has been increased and the stream creation was successful. + * If the stream creation doesn't complete within the specified timeout then the returned + * {@code CompletableFuture} will complete exceptionally with a {@link QuicStreamLimitException} + * + * @param timeout the maximum duration to wait to acquire a permit for stream creation + * @return a CompletableFuture whose result on successful completion will return the newly + * created {@code QuicBidiStream} + */ + public CompletableFuture createNewLocalBidiStream(final Duration timeout) { + @SuppressWarnings("unchecked") + final var streamCF = (CompletableFuture) createNewLocalStream(localBidi, + StreamMode.READ_WRITE, timeout); + return streamCF; + } + + private void register(long streamId, AbstractQuicStream stream) { + var previous = streams.put(streamId, stream); + assert previous == null : "stream " + streamId + " is already registered!"; + QuicTransportParameters peerParameters = connection.peerTransportParameters(); + if (peerParameters != null) { + if (debug.on()) { + debug.log("setting initial peer parameters on stream " + streamId); + } + newInitialPeerParameters(stream, peerParameters); + } + QuicTransportParameters localParameters = connection.localTransportParameters(); + if (localParameters != null) { + if (debug.on()) { + debug.log("setting initial local parameters on stream " + streamId); + } + newInitialLocalParameters(stream, localParameters); + } + if (stream instanceof QuicReceiverStream receiver && stream.isRemoteInitiated()) { + if (debug.on()) { + debug.log("accepting remote stream " + streamId); + } + newRemoteStreams.add(receiver); + acceptRemoteStreams(); + } + if (debug.on()) { + debug.log("new stream %s %s registered", streamId, stream.mode()); + } + } + + private void acceptRemoteStreams() { + newRemoteStreamsLock.lock(); + try { + for (var listener : streamListeners) { + var iterator = newRemoteStreams.iterator(); + while (iterator.hasNext()) { + var stream = iterator.next(); + if (debug.on()) { + debug.log("invoking remote stream listener for stream %s", + stream.streamId()); + } + if (listener.test(stream)) iterator.remove(); + } + } + } finally { + newRemoteStreamsLock.unlock(); + } + } + + private CompletableFuture createNewLocalStream( + final int localType, final StreamMode mode, final Duration timeout) { + assert localType >= 0 && localType < 4 : "bad local stream type " + localType; + assert (localType & SRV_MASK) == localFlag : "bad local stream type " + localType; + assert (localType & UNI_MASK) == 0 || mode == StreamMode.WRITE_ONLY + : "bad combination of local stream type (%s) and mode %s" + .formatted(localType, mode); + assert (localType & UNI_MASK) == UNI_MASK || mode == StreamMode.READ_WRITE + : "bad combination of local stream type (%s) and mode %s" + .formatted(localType, mode); + final boolean bidi = isBidirectional(localType); + final StreamCreationPermit permit = bidi ? this.localBidiMaxStreamLimit + : this.localUniMaxStreamLimit; + final CompletableFuture permitAcquisitionCF; + final long currentLimit = permit.currentLimit(); + final boolean acquired = permit.tryAcquire(); + if (acquired) { + permitAcquisitionCF = MinimalFuture.completedFuture(true); + } else { + // stream limit reached, request sending a STREAMS_BLOCKED frame + announceStreamsBlocked(bidi, currentLimit); + if (timeout.isPositive()) { + final Executor executor = this.connection.quicInstance().executor(); + if (debug.on()) { + debug.log("stream creation limit = " + permit.currentLimit() + + " reached; waiting for it to increase, timeout=" + timeout); + } + permitAcquisitionCF = permit.tryAcquire(timeout.toNanos(), NANOSECONDS, executor); + } else { + permitAcquisitionCF = MinimalFuture.completedFuture(false); + } + } + final CompletableFuture streamCF = + permitAcquisitionCF.thenCompose((acq) -> { + if (!acq) { + final String msg = "Stream limit = " + permit.currentLimit() + + " reached for locally initiated " + + (bidi ? "bidi" : "uni") + " streams"; + return MinimalFuture.failedFuture(new QuicStreamLimitException(msg)); + } + // stream limit hasn't been reached, we are allowed to create new one + final long streamId = nextStreamID.get(localType).getAndAdd(4); + final AbstractQuicStream stream = QuicStreams.createStream(connection, streamId); + assert stream.mode() == mode; + assert stream.type() == localType; + if (debug.on()) { + var strtype = (localType & UNI_MASK) == UNI_MASK ? "uni" : "bidi"; + debug.log("created new local %s stream type:%s, mode:%s, id:%s", + strtype, localType, mode, streamId); + } + register(streamId, stream); + return MinimalFuture.completedFuture(stream); + }); + return streamCF; + } + + /** + * Runs the APPLICATION space packet transmitter, if necessary, + * to potentially trigger sending a STREAMS_BLOCKED frame to the peer + * @param bidi true if the local endpoint is blocked for bidi streams, false for uni streams + * @param blockedOnLimit the stream creation limit due to which the local endpoint is + * currently blocked + */ + private void announceStreamsBlocked(final boolean bidi, final long blockedOnLimit) { + boolean runTransmitter = false; + if (bidi) { + long prevBlockedLimit = this.bidiStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.bidiStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.bidiStreamsBlocked.get(); + } + } else { + long prevBlockedLimit = this.uniStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.uniStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.uniStreamsBlocked.get(); + } + } + if (runTransmitter) { + if (debug.on()) { + debug.log("requesting packet transmission to send " + (bidi ? "bidi" : "uni") + + " STREAMS_BLOCKED with limit " + blockedOnLimit); + } + this.connection.runAppPacketSpaceTransmitter(); + } + } + + /** + * Runs the APPLICATION space packet transmitter, if necessary, to potentially trigger + * sending a MAX_STREAMS frame to the peer, upon receiving the STREAMS_BLOCKED {@code frame} + * from that peer + * @param frame the STREAMS_BLOCKED frame that was received from the peer + */ + public void peerStreamsBlocked(final StreamsBlockedFrame frame) { + final boolean bidi = frame.isBidi(); + final long blockedOnLimit = frame.maxStreams(); + boolean runTransmitter = false; + if (bidi) { + long prevBlockedLimit = this.peerBidiStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.peerBidiStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.peerBidiStreamsBlocked.get(); + } + } else { + long prevBlockedLimit = this.peerUniStreamsBlocked.get(); + while (blockedOnLimit > prevBlockedLimit) { + if (this.peerUniStreamsBlocked.compareAndSet(prevBlockedLimit, blockedOnLimit)) { + runTransmitter = true; + break; + } + prevBlockedLimit = this.peerUniStreamsBlocked.get(); + } + } + if (runTransmitter) { + if (debug.on()) { + debug.log("requesting packet transmission in response to receiving " + + (bidi ? "bidi" : "uni") + " STREAMS_BLOCKED from peer," + + " blocked with limit " + blockedOnLimit); + } + this.connection.runAppPacketSpaceTransmitter(); + } + } + + /** + * Gets or opens a remotely initiated stream with the given stream ID. + * Creates all streams with lower IDs if needed. + * @param streamId the stream ID + * @param frameType type of the frame received, used in exceptions + * @return a remotely initiated stream with the given stream ID. + * May return null if the stream was already closed. + * @throws IllegalArgumentException if the streamID is of the wrong type for + * a remote stream. + * @throws QuicTransportException if the streamID is higher than allowed + */ + public QuicStream getOrCreateRemoteStream(long streamId, long frameType) + throws QuicTransportException { + final int streamType = streamType(streamId); + if ((streamId & SRV_MASK) == localFlag) { + throw new IllegalArgumentException("bad remote stream type %s for stream %s" + .formatted(streamType, streamId)); + } + final boolean bidi = isBidirectional(streamId); + final long maxStreamLimit = bidi ? this.remoteBidiMaxStreamLimit.get() + : this.remoteUniMaxStreamLimit.get(); + if (maxStreamLimit <= (streamId >> 2)) { + throw new QuicTransportException("stream ID %s exceeds the number of allowed streams(%s)" + .formatted(streamId, maxStreamLimit), QuicTLSEngine.KeySpace.ONE_RTT, frameType, + QuicTransportErrors.STREAM_LIMIT_ERROR); + } + + newRemoteStreamsLock.lock(); + try { + var id = nextStreamID.get(streamType); + long nextId = id.get(); + if (nextId > streamId) { + // already created + return streams.get(streamId); + } + // id must not be modified outside newRemoteStreamsLock + long altId = id.getAndSet(streamId + 4); + assert altId == nextId : "next ID concurrently modified"; + + AbstractQuicStream stream = null; + for (long i = nextId; i <= streamId; i += 4) { + stream = QuicStreams.createStream(connection, i); + assert stream.isRemoteInitiated(); + register(i, stream); + } + assert stream != null; + assert stream.streamId() == streamId : stream.streamId(); + return stream; + } finally { + newRemoteStreamsLock.unlock(); + } + } + + + /** + * Finds a stream with the given stream ID. Returns {@code null} if no + * stream with that ID is found. + * @param streamId a stream ID + * @return the stream with the given stream ID if found, {@code null} + * otherwise. + */ + public QuicStream findStream(long streamId) { + return streams.get(streamId); + } + + /** + * Adds a listener that will be invoked when a remote stream is + * created. + * + * @apiNote The listener will be invoked with any remote streams + * already opened, and not yet acquired by another listener. + * Any stream passed to the listener is either a {@link QuicBidiStream} + * or a {@link QuicReceiverStream} depending on the + * {@linkplain QuicStreams#streamType(long) + * stream type} of the given streamId. + * The listener should return true if it wishes to acquire + * the stream. + * + * @param streamConsumer the listener + * + */ + public void addRemoteStreamListener(Predicate streamConsumer) { + newRemoteStreamsLock.lock(); + try { + streamListeners.add(streamConsumer); + acceptRemoteStreams(); + } finally { + newRemoteStreamsLock.unlock(); + } + } + + /** + * Removes a listener previously added with {@link #addRemoteStreamListener(Predicate)} + * @return {@code true} if the listener was found and removed, {@code false} otherwise + */ + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + newRemoteStreamsLock.lock(); + try { + return streamListeners.remove(streamConsumer); + } finally { + newRemoteStreamsLock.unlock(); + } + } + + /** + * {@return a stream of all currently active {@link QuicStream} in the connection} + */ + public Stream quicStreams() { + return streams.all(); + } + + /** + * {@return {@code true} if there is some data to send} + * @apiNote + * This method may return true in the case where a + * STREAM_DATA_BLOCKED frame needs to be sent, even if no + * other data is available. + */ + public boolean hasAvailableData() { + return !sendersReady.isEmpty(); + } + + /** + * {@return true if there are control frames to send} + * Typically, these are STREAMS_BLOCKED, MAX_STREAMS, RESET_STREAM, STOP_SENDING, and + * MAX_STREAM_DATA. + */ + public boolean hasControlFrames() { + return !sendersReset.isEmpty() || !receiversSend.isEmpty() + // either of these imply we may send a MAX_STREAMS frame + || peerUniStreamsBlocked.get() != -1 || peerBidiStreamsBlocked.get() != -1 + // either of these imply we should send a STREAMS_BLOCKED frame + || uniStreamsBlocked.get() > lastUniStreamsBlockedSent.get() + || bidiStreamsBlocked.get() > lastBidiStreamsBlockedSent.get(); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a stream + * that has a receiving part} + * In other words, returns {@code true} if the given stream is either + * bidirectional or peer-initiated. + * @param streamId a stream ID + */ + public boolean isReceivingStream(long streamId) { + return !isLocalUni(streamId); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a stream + * that has a sending part} + * In other words, returns {@code true} if the given stream is either + * bidirectional or local-initiated. + * @param streamId a stream ID + */ + public boolean isSendingStream(long streamId) { + return !isRemoteUni(streamId); + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a local + * unidirectional stream} + * @param streamId a stream ID + */ + public boolean isLocalUni(long streamId) { + return streamType(streamId) == localUni; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a local + * bidirectional stream} + * @param streamId a stream ID + */ + public boolean isLocalBidi(long streamId) { + return streamType(streamId) == localBidi; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a + * peer initiated unidirectional stream} + * @param streamId a stream ID + */ + public boolean isRemoteUni(long streamId) { + return streamType(streamId) == remoteUni; + } + + /** + * {@return {@code true} if the given {@code streamId} indicates a + * peer initiated bidirectional stream} + * @param streamId a stream ID + */ + public boolean isRemoteBidi(long streamId) { + return streamType(streamId) == remoteBidi; + } + + /** + * Mark the stream whose ID is encoded in the given + * {@code ResetStreamFrame} as needing a RESET_STREAM frame to be sent. + * It will put the stream and the frame in the {@code sendersReset} map. + * @param streamId the id of the stream that should be reset + * @param errorCode the application error code + */ + public void requestResetStream(long streamId, long errorCode) { + assert isSendingStream(streamId); + var stream = senderImpl(streams.get(streamId)); + if (stream == null) { + if (debug.on()) { + debug.log("Can't reset stream %d: no such stream", streamId); + } + return; + } + sendersReset.putIfAbsent(stream, errorCode); + if (debug.on()) { + debug.log("Reset stream scheduled"); + } + } + + /** + * Mark the stream whose ID is encoded in the given + * {@code MaxStreamDataFrame} as needing a MAX_STREAM_DATA frame to be sent. + * It will put the stream and the frame in the {@code receiversSend} map. + * @param maxStreamDataFrame the MAX_STREAM_DATA frame to send + */ + public void requestSendMaxStreamData(MaxStreamDataFrame maxStreamDataFrame) { + Objects.requireNonNull(maxStreamDataFrame, "maxStreamDataFrame"); + long streamId = maxStreamDataFrame.streamID(); + assert isReceivingStream(streamId); + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) { + debug.log("Can't send MaxStreamDataFrame %d: no such stream", streamId); + } + return; + } + if (stream instanceof QuicReceiverStream receiver) { + // don't replace a stop sending frame, and don't replace + // a max stream data frame if it has a bigger max stream data + receiversSend.compute(receiver, (s, frame) -> { + if (frame instanceof StopSendingFrame stopSendingFrame) { + assert s.streamId() == stopSendingFrame.streamID(); + // no need to send max data frame if we are requesting + // stop sending + return frame; + } + if (frame instanceof MaxStreamDataFrame maxFrame) { + assert s.streamId() == maxFrame.streamID(); + if (maxFrame.maxStreamData() > maxStreamDataFrame.maxStreamData()) { + // send the frame that has the greater max data + return maxFrame; + } else return maxStreamDataFrame; + } + assert frame == null; + return maxStreamDataFrame; + }); + } else { + if (debug.on()) { + debug.log("Can't send %s stream %d: not a receiver stream", + maxStreamDataFrame.getClass(), streamId); + } + } + } + + + /** + * Mark the stream whose ID is encoded in the given + * {@code StopSendingFrame} as needing a STOP_SENDING frame to be sent. + * It will put the stream and the frame in the {@code receiversSend} map. + * @param stopSendingFrame the STOP_SENDING frame to send + */ + public void scheduleStopSendingFrame(StopSendingFrame stopSendingFrame) { + Objects.requireNonNull(stopSendingFrame, "stopSendingFrame"); + long streamId = stopSendingFrame.streamID(); + assert isReceivingStream(streamId); + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) { + debug.log("Can't send STOP_SENDING to stream %d: no such stream", streamId); + } + return; + } + if (stream instanceof QuicReceiverStream receiver) { + // don't need to check if we already have a frame registered: + // stop sending takes precedence. + receiversSend.put(receiver, stopSendingFrame); + } else { + if (debug.on()) { + debug.log("Can't send %s stream %d: not a receiver stream", + stopSendingFrame.getClass(), streamId); + } + } + } + + /** + * Called when the RESET_STREAM frame is acknowledged by the peer. + * @param reset the RESET_STREAM frame + */ + public void streamResetAcknowledged(ResetStreamFrame reset) { + Objects.requireNonNull(reset, "reset"); + long streamId = reset.streamId(); + assert isSendingStream(streamId) : + "stream %s is not a sending stream".formatted(streamId); + final var stream = streams.get(streamId); + if (stream == null) { + return; + } + var sender = senderImpl(stream); + if (sender != null) { + sender.resetAcknowledged(reset.finalSize()); + assert !stream.isDone() || !streams.streams.containsKey(streamId) + : "resetAcknowledged() should have removed the stream"; + if (debug.on()) { + debug.log("acknowledged reset for stream %d", streamId); + } + } + } + + /** + * Called when the final STREAM frame is acknowledged by the peer. + * @param streamFrame the final STREAM frame + */ + public void streamDataSentAcknowledged(StreamFrame streamFrame) { + long streamId = streamFrame.streamId(); + assert isSendingStream(streamId) : + "stream %s is not a sending stream".formatted(streamId); + assert streamFrame.isLast(); + final var stream = streams.get(streamId); + if (stream == null) { + return; + } + var sender = senderImpl(stream); + if (sender != null) { + sender.dataAcknowledged(streamFrame.offset() + streamFrame.dataLength()); + assert !stream.isDone() || !streams.streams.containsKey(streamId) + : "dataAcknowledged() should have removed the stream"; + if (debug.on()) { + debug.log("acknowledged data for stream %d", streamId); + } + } + } + + /** + * Tracks a stream, belonging to this connection, as being blocked from sending data + * due to flow control limit. + * + * @param streamId the stream id + */ + final void trackBlockedStream(final long streamId) { + this.flowControlBlockedStreams.add(streamId); + } + + /** + * Stops tracking a stream, belonging to this connection, that may have been previously + * tracked as being blocked due to flow control limit. + * + * @param streamId the stream id + */ + final void untrackBlockedStream(final long streamId) { + this.flowControlBlockedStreams.remove(streamId); + } + + + /** + * Removes a stream from the stream map after its state has been + * switched to DATA_RECVD or RESET_RECVD + * @param streamId the stream id + * @param stream the stream instance + */ + private void removeStream(long streamId, QuicStream stream) { + // if we were tracking this stream as blocked due to flow control, then + // stop tracking the stream. + untrackBlockedStream(streamId); + if (stream instanceof AbstractQuicStream astream) { + if (astream.isDone()) { + if (debug.on()) { + debug.log("Removing stream %d (%s)", + stream.streamId(), stream.getClass().getSimpleName()); + } + streams.remove(streamId, astream); + if (stream.isRemoteInitiated()) { + // the queue is not expected to contain many elements. + newRemoteStreams.remove(stream); + if (shouldSendMaxStreams(stream.isBidirectional())) { + this.connection.runAppPacketSpaceTransmitter(); + } + } + } else { + if (debug.on()) { + debug.log("Can't remove stream yet: %d (%s) is %s", + stream.streamId(), stream.getClass().getSimpleName(), + stream.state()); + } + } + } + assert stream instanceof AbstractQuicStream + : "stream %s: unexpected stream class: %s" + .formatted(streamId, stream.getClass()); + } + + /** + * Called when new local transport parameters are available + * @param params the new local transport parameters + */ + public void newLocalTransportParameters(final QuicTransportParameters params) { + // the limit imposed on the remote peer by the local endpoint + final long newRemoteUniMax = params.getIntParameter(ParameterId.initial_max_streams_uni); + tryIncreaseLimitTo(this.remoteUniMaxStreamLimit, newRemoteUniMax); + final long newRemoteBidiMax = params.getIntParameter(ParameterId.initial_max_streams_bidi); + tryIncreaseLimitTo(this.remoteBidiMaxStreamLimit, newRemoteBidiMax); + streams.all().forEach(s -> newInitialLocalParameters(s, params)); + } + + /** + * Called when new peer transport parameters are available + * @param params the new local transport parameters + */ + public void newPeerTransportParameters(final QuicTransportParameters params) { + // the limit imposed on the local endpoint by the remote peer + final long localUniMaxStreams = params.getIntParameter(ParameterId.initial_max_streams_uni); + if (debug.on()) { + debug.log("increasing localUniMaxStreamLimit to initial_max_streams_uni: " + + localUniMaxStreams); + } + this.localUniMaxStreamLimit.tryIncreaseLimitTo(localUniMaxStreams); + final long localBidiMaxStreams = params.getIntParameter(ParameterId.initial_max_streams_bidi); + if (debug.on()) { + debug.log("increasing localBidiMaxStreamLimit to initial_max_streams_bidi: " + + localBidiMaxStreams); + } + this.localBidiMaxStreamLimit.tryIncreaseLimitTo(localBidiMaxStreams); + // set initial parameters on streams + streams.all().forEach(s -> newInitialPeerParameters(s, params)); + if (debug.on()) { + debug.log("all streams updated (%s)", streams.streams.size()); + } + } + + /** + * Called to set initial peer parameters on a stream + * @param stream the stream on which parameters might be set + * @param params the peer transport parameters + */ + private void newInitialPeerParameters(QuicStream stream, QuicTransportParameters params) { + long streamId = stream.streamId(); + if (isLocalUni(stream.streamId())) { + if (params.isPresent(ParameterId.initial_max_stream_data_uni)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_uni); + senderImpl(stream).setMaxStreamData(maxData); + } + } else if (isLocalBidi(streamId)) { + // remote for the peer is local for us + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_remote)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_remote); + senderImpl(stream).setMaxStreamData(maxData); + } + } else if (isRemoteBidi(streamId)) { + // local for the peer is remote for us + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_local)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_local); + senderImpl(stream).setMaxStreamData(maxData); + } + } + } + + private static boolean tryIncreaseLimitTo(final AtomicLong limit, final long newLimit) { + long currentLimit = limit.get(); + while (currentLimit < newLimit) { + if (limit.compareAndSet(currentLimit, newLimit)) { + return true; + } + currentLimit = limit.get(); + } + return false; + } + + /** + * Called to set initial peer parameters on a stream + * @param stream the stream on which parameters might be set + * @param params the peer transport parameters + */ + private void newInitialLocalParameters(QuicStream stream, QuicTransportParameters params) { + long streamId = stream.streamId(); + if (isRemoteUni(stream.streamId())) { + if (params.isPresent(ParameterId.initial_max_stream_data_uni)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_uni); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } else if (isLocalBidi(streamId)) { + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_local)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_local); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } else if (isRemoteBidi(streamId)) { + if (params.isPresent(ParameterId.initial_max_stream_data_bidi_remote)) { + long maxData = params.getIntParameter(ParameterId.initial_max_stream_data_bidi_remote); + receiverImpl(stream).updateMaxStreamData(maxData); + } + } + } + + /** + * Set max stream data for a stream. + * Called when a {@link jdk.internal.net.http.quic.frames.MaxStreamDataFrame + * MaxStreamDataFrame} is received. + * @param stream the stream + * @param maxStreamData the max data that the peer is willing to accept on this stream + */ + public void setMaxStreamData(QuicSenderStream stream, long maxStreamData) { + var sender = senderImpl(stream); + if (sender != null) { + final long newFinalizedLimit = sender.setMaxStreamData(maxStreamData); + // if the connection was tracking this stream as blocked due to flow control + // and if this new MAX_STREAM_DATA limit unblocked this stream, then + // stop tracking the stream. + if (newFinalizedLimit == maxStreamData) { // the proposed limit was accepted + if (!sender.isBlocked()) { + untrackBlockedStream(stream.streamId()); + } + } + } + } + + /** + * This method is called when a {@link + * jdk.internal.net.http.quic.frames.StopSendingFrame} is received + * from the peer. + * @param stream the stream for which stop sending was requested + * by the peer + * @param errorCode the error code + */ + public void stopSendingReceived(QuicSenderStream stream, long errorCode) { + var sender = senderImpl(stream); + if (sender != null) { + // if the stream was being tracked as blocked from sending data, + // due to flow control limits imposed by the peer, then we now + // stop tracking it since the peer no longer wants us to send data + // on this stream. + untrackBlockedStream(stream.streamId()); + sender.stopSendingReceived(errorCode); + } + } + + /** + * Called when the receiving part or the sending part of a stream + * reaches a terminal state. + * @param streamId the id of the stream + * @param state the terminal state + */ + public void notifyTerminalState(long streamId, StreamState state) { + assert state.isTerminal() : state; + var stream = streams.get(streamId); + if (stream != null) { + removeStream(streamId, stream); + } + } + + /** + * Called when the connection is closed by the higher level + * protocol + * @param terminationCause the termination cause + */ + public void terminate(final TerminationCause terminationCause) { + assert terminationCause != null : "termination cause is null"; + // make sure all active streams are woken up when we close a connection + streams.all().forEach((stream) -> { + if (stream instanceof QuicSenderStream) { + var sender = senderImpl(stream); + try { + sender.terminate(terminationCause); + } catch (Throwable t) { + if (debug.on()) { + debug.log("failed to close sender stream %s: %s", sender.streamId(), t); + } + } + } + if (stream instanceof QuicReceiverStream) { + var receiver = receiverImpl(stream); + try { + receiver.terminate(terminationCause); + } catch (Throwable t) { + // log and ignore + if (debug.on()) { + debug.log("failed to close receiver stream %s: %s", receiver.streamId(), t); + } + } + } + }); + } + + /** + * This method is called by when a stream has data available for sending. + * + * @param streamId the stream id of the stream which is ready + * @see QuicConnectionImpl#streamDataAvailableForSending + */ + public void enqueueForSending(long streamId) { + var stream = streams.get(streamId); + if (stream == null) { + if (debug.on()) + debug.log("WARNING: stream %d not found", streamId); + return; + } + if (stream instanceof QuicSenderStream sender) { + // No need to check/assert the presence of this sender in the queue. + // In fact there is no guarantee that the sender isn't already in the + // queue, since the scheduler loop can also put it back into the queue, + // if for example, not everything that the sender wanted to send could + // fit in the quic packet. + sendersReady.add(sender); + } else { + String msg = String.format("Stream %s not a sending or bidi stream: %s", + streamId, stream.getClass().getName()); + if (debug.on()) { + debug.log("WARNING: " + msg); + } + throw new AssertionError(msg); + } + } + + /** + * If there are any streams in this connection that have been blocked from sending + * data due to flow control limit on that stream, then this method enqueues a + * {@code STREAM_DATA_BLOCKED} frame to be sent for each such stream. + */ + public final void enqueueStreamDataBlocked() { + connection.streamDataAvailableForSending(this.flowControlBlockedStreams); + } + + /** + * {@return the sender part implementation of the given stream, or {@code null}} + * This method returns null if the given stream doesn't have a sending part + * (that is, if it is a unidirectional peer initiated stream). + * @param stream a sending or bidirectional stream + */ + QuicSenderStreamImpl senderImpl(QuicStream stream) { + if (stream instanceof QuicSenderStreamImpl sender) { + return sender; + } else if (stream instanceof QuicBidiStreamImpl bidi) { + return bidi.senderPart(); + } + return null; + } + + /** + * {@return the receiver part implementation of the given stream, or {@code null}} + * This method returns null if the given stream doesn't have a receiver part + * (that is, if it is a unidirectional local initiated stream). + * @param stream a receiving or bidirectional stream + */ + QuicReceiverStreamImpl receiverImpl(QuicStream stream) { + if (stream instanceof QuicReceiverStreamImpl receiver) { + return receiver; + } else if (stream instanceof QuicBidiStreamImpl bidi) { + return bidi.receiverPart(); + } + return null; + } + + /** + * Called when a StreamFrame is received. + * @param stream the stream for which the StreamFrame was received + * @param frame the stream frame + * @throws QuicTransportException if an error occurred processing the frame + */ + public void processIncomingFrame(QuicStream stream, StreamFrame frame) throws QuicTransportException { + var receiver = receiverImpl(stream); + assert receiver != null; + receiver.processIncomingFrame(frame); + } + + /** + * Called when a ResetStreamFrame is received. + * @param stream the stream for which the ResetStreamFrame was received + * @param frame the reset stream frame + * @throws QuicTransportException if an error occurred processing the frame + */ + public void processIncomingFrame(QuicStream stream, ResetStreamFrame frame) throws QuicTransportException { + var receiver = receiverImpl(stream); + assert receiver != null; + receiver.processIncomingResetFrame(frame); + } + + public void processIncomingFrame(final QuicStream stream, final StreamDataBlockedFrame frame) { + assert stream.streamId() == frame.streamId() : "unexpected stream id " + frame.streamId() + + " in frame, expected " + stream.streamId(); + final QuicReceiverStreamImpl rcvrStream = receiverImpl(stream); + assert rcvrStream != null : "missing receiver stream for stream " + stream.streamId(); + rcvrStream.processIncomingFrame(frame); + } + + public boolean tryIncreaseStreamLimit(final MaxStreamsFrame maxStreamsFrame) { + final StreamCreationPermit permit = maxStreamsFrame.isBidi() + ? localBidiMaxStreamLimit : localUniMaxStreamLimit; + final long newLimit = maxStreamsFrame.maxStreams(); + if (debug.on()) { + if (maxStreamsFrame.isBidi()) { + debug.log("increasing localBidiMaxStreamLimit limit to: " + newLimit); + } else { + debug.log("increasing localUniMaxStreamLimit limit to: " + newLimit); + } + } + return permit.tryIncreaseLimitTo(newLimit); + } + + /** + * Checks whether any stream needs to have a STOP_SENDING, RESET_STREAM or any connection + * control frames like STREAMS_BLOCKED, MAX_STREAMS sent and adds the frame to the list + * if there's room. + * @param frames list of frames + * @param remaining maximum number of bytes that can be added by this method + * @return number of bytes actually added + */ + private long checkResetAndOtherControls(List frames, long remaining) { + if (debug.on()) + debug.log("checking reset and other control frames..."); + long added = 0; + // check STREAMS_BLOCKED, only send it if the local endpoint is blocked on a limit + // for which we haven't yet sent a STREAMS_BLOCKED + final long uniStreamsBlockedLimit = this.uniStreamsBlocked.get(); + final long lastUniStreamsBlockedSent = this.lastUniStreamsBlockedSent.get(); + if (uniStreamsBlockedLimit != -1 && uniStreamsBlockedLimit > lastUniStreamsBlockedSent) { + final StreamsBlockedFrame frame = new StreamsBlockedFrame(false, uniStreamsBlockedLimit); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Not enough space to add a STREAMS_BLOCKED frame for uni streams"); + } + } else { + frames.add(frame); + added += size; + // now that we are sending a STREAMS_BLOCKED frame, keep track of the limit + // that we sent it with + this.lastUniStreamsBlockedSent.set(frame.maxStreams()); + } + } + final long bidiStreamsBlockedLimit = this.bidiStreamsBlocked.get(); + final long lastBidiStreamsBlockedSent = this.lastBidiStreamsBlockedSent.get(); + if (bidiStreamsBlockedLimit != -1 && bidiStreamsBlockedLimit > lastBidiStreamsBlockedSent) { + final StreamsBlockedFrame frame = new StreamsBlockedFrame(true, bidiStreamsBlockedLimit); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Not enough space to add a STREAMS_BLOCKED frame for bidi streams"); + } + } else { + frames.add(frame); + added += size; + // now that we are sending a STREAMS_BLOCKED frame, keep track of the limit + // that we sent it with + this.lastBidiStreamsBlockedSent.set(frame.maxStreams()); + } + } + // check STOP_SENDING and MAX_STREAM_DATA + var rcvIterator = receiversSend.entrySet().iterator(); + while (rcvIterator.hasNext()) { + var entry = rcvIterator.next(); + var frame = entry.getValue(); + if (frame.size() > remaining - added) { + if (debug.on()) { + debug.log("Stream %s: not enough space for %s", + entry.getKey().streamId(), frame); + } + break; + } + var receiver = receiverImpl(entry.getKey()); + var size = checkSendControlFrame(receiver, frame, frames); + if (size > 0) { + added += size; + } + rcvIterator.remove(); + } + + // check RESET_STREAM + var sndIterator = sendersReset.entrySet().iterator(); + while (sndIterator.hasNext()) { + Map.Entry entry = sndIterator.next(); + var sender = senderImpl(entry.getKey()); + assert sender != null; + long finalSize = sender.dataSent(); + ResetStreamFrame frame = new ResetStreamFrame(sender.streamId(), entry.getValue(), finalSize); + final int size = frame.size(); + if (size > remaining - added) { + if (debug.on()) { + debug.log("Stream %s: not enough space for ResetFrame", + sender.streamId()); + } + break; + } + if (debug.on()) + debug.log("Stream %s: Adding ResetFrame", sender.streamId()); + frames.add(frame); + added += size; + sender.resetSent(); + sndIterator.remove(); + } + + if (remaining - added > 18) { + // add MAX_STREAMS if necessary + added += addMaxStreamsFrame(frames, false); + added += addMaxStreamsFrame(frames, true); + } + return added; + } + + private boolean shouldSendMaxStreams(final boolean bidi) { + final boolean rcvdStreamsBlocked = bidi + ? this.peerBidiStreamsBlocked.get() != -1 + : this.peerUniStreamsBlocked.get() != -1; + // if we either received a STREAMS_BLOCKED from the peer for that stream type + // or if our internal algorithm decides that the peer is about to reach the stream + // creation limit + return rcvdStreamsBlocked || nextMaxStreamsLimit(bidi) > 0; + } + + private long addMaxStreamsFrame(final List frames, final boolean bidi) { + final long newMaxStreamsLimit = connection.nextMaxStreamsLimit(bidi); + if (newMaxStreamsLimit == 0) { + return 0; + } + final boolean limitIncreased; + if (bidi) { + limitIncreased = tryIncreaseLimitTo(remoteBidiMaxStreamLimit, newMaxStreamsLimit); + } else { + limitIncreased = tryIncreaseLimitTo(remoteUniMaxStreamLimit, newMaxStreamsLimit); + } + if (!limitIncreased) { + return 0; + } + final MaxStreamsFrame frame = new MaxStreamsFrame(bidi, newMaxStreamsLimit); + frames.add(frame); + // now that we are sending MAX_STREAMS frame to the peer, reset the relevant + // STREAMS_BLOCKED flag that we might have set when/if we had received a STREAMS_BLOCKED + // from the peer + if (bidi) { + this.peerBidiStreamsBlocked.set(-1); + } else { + this.peerUniStreamsBlocked.set(-1); + } + if (debug.on()) { + debug.log("Increasing max remote %s streams to %s", + bidi ? "bidi" : "uni", newMaxStreamsLimit); + } + return frame.size(); + } + + public long nextMaxStreamsLimit(final boolean bidi) { + return bidi ? streams.remoteBidiNextMaxStreams : streams.remoteUniNextMaxStreams; + } + + /** + * {@return true if there are any streams on this connection which are blocked from + * sending data due to flow control limit, false otherwise} + */ + public final boolean hasBlockedStreams() { + return !this.flowControlBlockedStreams.isEmpty(); + } + + /** + * Checks whether the given stream is recorded as needing a control + * frame to be sent, and if so, add that frame to the list + * + * @param receiver the receiver part of the stream + * @param frame the frame to send + * @param frames list of frames + * @return size of the added frame, or zero if no frame was added + * @apiNote Typically, the control frame that is sent is either a MAX_STREAM_DATA + * or a STOP_SENDING frame + */ + private long checkSendControlFrame(QuicReceiverStreamImpl receiver, + QuicFrame frame, + List frames) { + if (frame == null) { + if (debug.on()) + debug.log("Stream %s: no receiver frame to send", receiver.streamId()); + return 0; + } + if (frame instanceof MaxStreamDataFrame maxStreamDataFrame) { + if (receiver.receivingState() == ReceivingStreamState.RECV) { + // if we know the final size, no point in increasing max data + if (debug.on()) + debug.log("Stream %s: Adding MaxStreamDataFrame", receiver.streamId()); + frames.add(frame); + receiver.updateMaxStreamData(maxStreamDataFrame.maxStreamData()); + return frame.size(); + } + return 0; + } else if (frame instanceof StopSendingFrame) { + if (debug.on()) + debug.log("Stream %s: Adding StopSendingFrame", receiver.streamId()); + frames.add(frame); + return frame.size(); + } else { + throw new InternalError("Should not reach here - not a control frame: " + frame); + } + } + + /** + * Package available data in {@link StreamFrame} instances and add them + * to the provided frames list. Additional frames, like connection control frames + * {@code STREAMS_BLOCKED}, {@code MAX_STREAMS} or stream flow control frames like + * {@code STREAM_DATA_BLOCKED} may also be added if space allows. The {@link StreamDataBlockedFrame} + * is added only once for a given stream, until the stream becomes ready again. + * @implSpec + * The total cumulated size of the returned frames must not exceed {@code maxSize}. + * The total cumulated lengths of the returned frames must not exceed {@code maxConnectionData}. + * + * @param encoder the {@link QuicPacketEncoder}, used if anything is quic version + * dependent. + * @param maxSize the cumulated maximum size of all the frames + * @param maxConnectionData the maximum number of stream data bytes that can + * be packaged to respect connection flow control + * constraints + * @param frames a list of frames in which to add the packaged data + * @return the total number of stream data bytes packaged in the created + * frames. This will not exceed the given {@code maxConnectionData}. + */ + public long produceFramesToSend(QuicPacketEncoder encoder, long maxSize, + long maxConnectionData, List frames) + throws QuicTransportException { + long remaining = maxSize; + long produced = 0; + try { + remaining -= checkResetAndOtherControls(frames, remaining); + // scan the streams and compose a list of frames - possibly including + // stream data blocked frames, + QuicSenderStreamImpl sender; + NEXT_STREAM: while ((sender = senderImpl(sendersReady.poll())) != null) { + long streamId = sender.streamId(); + boolean stillReady = true; + try { + do { + if (remaining == 0 || maxConnectionData == 0) break; + var state = sender.sendingState(); + switch (state) { + case SEND -> { + long offset = sender.dataSent(); + int headerSize = StreamFrame.headerSize(encoder, streamId, offset, remaining); + if (headerSize >= remaining) { + break NEXT_STREAM; + } + long maxControlled = Math.min(maxConnectionData, remaining - headerSize); + int maxData = (int) Math.min(Integer.MAX_VALUE, maxControlled); + if (maxData <= 0) { + break NEXT_STREAM; + } + ByteBuffer buffer = sender.poll(maxData); + if (buffer != null) { + int length = buffer.remaining(); + assert length <= remaining; + assert length <= maxData; + long streamSize = sender.streamSize(); + boolean fin = streamSize >= 0 && streamSize == offset + length; + if (fin) { + stillReady = false; + } + if (length > 0 || fin) { + StreamFrame frame = new StreamFrame(streamId, offset, length, fin, buffer); + int size = frame.size(); + assert size <= remaining : "stream:%s: size %s > remaining %s" + .formatted(streamId, size, remaining); + if (debug.on()) { + debug.log("stream:%s Adding StreamFrame: %s", + streamId, frame); + } + frames.add(frame); + remaining -= size; + produced += length; + maxConnectionData -= length; + } + } + var blocked = sender.isBlocked(); + if (blocked) { + // track this stream as blocked due to flow control + trackBlockedStream(streamId); + final var dataBlocked = new StreamDataBlockedFrame(streamId, sender.dataSent()); + // This might produce multiple StreamDataBlocked frames + // if the stream was added to sendersReady multiple times, so + // we check before actually sending a STREAM_DATA_BLOCKED frame + if (!frames.contains(dataBlocked)) { + var fdbSize = dataBlocked.size(); + if (dataBlocked.size() > remaining) { + // keep the stream in the ready list if we haven't been + // able to generate the StreamDataBlockedFrame + break NEXT_STREAM; + } + if (debug.on()) { + debug.log("stream:" + streamId + " sender is blocked: " + dataBlocked); + } + frames.add(dataBlocked); + remaining -= fdbSize; + } + stillReady = false; + continue NEXT_STREAM; + } + if (buffer == null) { + stillReady = sender.available() != 0; + continue NEXT_STREAM; + } + } + case DATA_SENT, DATA_RECVD, RESET_SENT, RESET_RECVD -> { + stillReady = false; + continue NEXT_STREAM; + } + case READY -> { + String msg = "stream:%s: illegal state %s".formatted(streamId, state); + throw new IllegalStateException(msg); + } + } + if (debug.on()) { + debug.log("packageStreamData: stream:%s, remaining:%s, " + + "maxConnectionData: %s, produced:%s", + streamId, remaining, maxConnectionData, produced); + } + } while (remaining > 0 && maxConnectionData > 0); + } catch (RuntimeException | AssertionError x) { + stillReady = false; + throw new QuicTransportException("Failed to compose frames for stream " + streamId, + KeySpace.ONE_RTT, 0, QuicTransportErrors.INTERNAL_ERROR.code(), x); + } finally { + if (stillReady) { + if (debug.on()) + debug.log("stream:%s is still ready", streamId); + enqueueForSending(streamId); + } else { + if (debug.on()) + debug.log("stream:%s is no longer ready", streamId); + } + } + assert maxConnectionData >= 0 : "produced " + produced + " max is " + maxConnectionData; + if (remaining == 0 || maxConnectionData == 0) break; + } + } catch (RuntimeException | AssertionError x) { + if (debug.on()) debug.log("Failed to compose frames", x); + if (Log.errors()) { + Log.logError(connection.logTag() + + ": Failed to compose frames", x); + } + throw new QuicTransportException("Failed to compose frames", + KeySpace.ONE_RTT, 0, QuicTransportErrors.INTERNAL_ERROR.code(), x); + } + return produced; + } + + private interface ReadyStreamCollection { + boolean isEmpty(); + + void add(QuicSenderStream sender); + + QuicStream poll(); + } + //This queue is used to ensure fair sending of stream data: the packageStreamData method + // will pop and push streams from/to this queue in a round-robin fashion so that one stream + // doesn't starve all the others. + private static class ReadyStreamQueue implements ReadyStreamCollection { + private ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public void add(QuicSenderStream sender) { + queue.add(sender); + } + + public QuicStream poll() { + return queue.poll(); + } + } + // This queue is used to ensure fast closing of streams: it always returns + // the ready stream with the lowest ID. + private static class ReadyStreamSortedQueue implements ReadyStreamCollection { + private ConcurrentSkipListMap queue = new ConcurrentSkipListMap<>(); + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public void add(QuicSenderStream sender) { + queue.put(sender.streamId(), sender); + } + + public QuicStream poll() { + Map.Entry entry = queue.pollFirstEntry(); + if (entry == null) return null; + return entry.getValue(); + } + } + + // provides a limited view/operations over a ConcurrentHashMap(). we compute additional + // state in the remove() and put() APIs. providing only a limited set of APIs allows us + // to keep the places where we do that additional state computation, to minimal. + private final class StreamsContainer { + // A map of + private final ConcurrentMap streams = new ConcurrentHashMap<>(); + // active remote bidi stream count + private final AtomicLong remoteBidiActiveStreams = new AtomicLong(); + // active remote uni stream count + private final AtomicLong remoteUniActiveStreams = new AtomicLong(); + + private volatile long remoteBidiNextMaxStreams; + private volatile long remoteUniNextMaxStreams; + + AbstractQuicStream get(final long streamId) { + return streams.get(streamId); + } + + boolean remove(final long streamId, final AbstractQuicStream stream) { + if (!streams.remove(streamId, stream)) { + return false; + } + final int streamType = (int) (stream.streamId() & TYPE_MASK); + if (streamType == remoteBidi) { + final long currentActive = remoteBidiActiveStreams.decrementAndGet(); + remoteBidiNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteBidiMaxStreamLimit.get()); + } else if (streamType == remoteUni) { + final long currentActive = remoteUniActiveStreams.decrementAndGet(); + remoteUniNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteUniMaxStreamLimit.get()); + } + return true; + } + + AbstractQuicStream put(final long streamId, final AbstractQuicStream stream) { + final AbstractQuicStream previous = streams.put(streamId, stream); + final int streamType = (int) (stream.streamId() & TYPE_MASK); + if (streamType == remoteBidi) { + final long currentActive = remoteBidiActiveStreams.incrementAndGet(); + remoteBidiNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteBidiMaxStreamLimit.get()); + } else if (streamType == remoteUni) { + final long currentActive = remoteUniActiveStreams.incrementAndGet(); + remoteUniNextMaxStreams = computeNextMaxStreamsLimit(streamType, currentActive, + remoteUniMaxStreamLimit.get()); + } + return previous; + } + + Stream all() { + return streams.values().stream(); + } + + /** + * Returns the next (higher) max streams limit that can be advertised to the remote peer. + * Returns {@code 0} if the limit should not be increased. + */ + private long computeNextMaxStreamsLimit( + final int streamType, final long currentActiveCount, + final long currentMaxStreamsLimit) { + // we only deal with remote bidi or remote uni + assert (streamType == remoteBidi || streamType == remoteUni) + : "stream type is neither remote bidi nor remote uni: " + streamType; + final long usedRemoteStreams = peekNextStreamId(streamType) >> 2; + final boolean bidi = streamType == remoteBidi; + final var desiredStreamCount = bidi ? MAX_BIDI_STREAMS_WINDOW_SIZE + : MAX_UNI_STREAMS_WINDOW_SIZE; + final long desiredMaxStreams = usedRemoteStreams - currentActiveCount + desiredStreamCount; + // we compute a new limit after we consumed 25% (arbitrary decision) of the desired window + if (desiredMaxStreams - currentMaxStreamsLimit > desiredStreamCount >> 2) { + return desiredMaxStreams; + } + return 0; + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java new file mode 100644 index 00000000000..1a20cbe5211 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStream.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * An interface that represents the receiving part of a stream. + *

        From RFC 9000: + * + * On the receiving part of a stream, an application protocol can: + *

          + *
        • read data; and
        • + *
        • abort reading of the stream and request closure, possibly + * resulting in a STOP_SENDING frame (Section 19.5).
        • + *
        + * + */ +public non-sealed interface QuicReceiverStream extends QuicStream { + + /** + * An enum that models the state of the receiving part of a stream. + */ + enum ReceivingStreamState implements QuicStream.StreamState { + /** + * The initial state for the receiving part of a + * stream is "Recv". + *

        + * In the "Recv" state, the endpoint receives STREAM + * and STREAM_DATA_BLOCKED frames. Incoming data is buffered + * and can be reassembled into the correct order for delivery + * to the application. As data is consumed by the application + * and buffer space becomes available, the endpoint sends + * MAX_STREAM_DATA frames to allow the peer to send more data. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RECV, + /** + * When a STREAM frame with a FIN bit is received, the final size of + * the stream is known; see Section 4.5. The receiving part of the + * stream then enters the "Size Known" state. In this state, the + * endpoint no longer needs to send MAX_STREAM_DATA frames; it only + * receives any retransmissions of stream data. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + SIZE_KNOWN, + /** + * Once all data for the stream has been received, the receiving part + * enters the "Data Recvd" state. This might happen as a result of + * receiving the same STREAM frame that causes the transition to + * "Size Known". After all data has been received, any STREAM or + * STREAM_DATA_BLOCKED frames for the stream can be discarded. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_RECVD, + /** + * The "Data Recvd" state persists until stream data has been delivered + * to the application. Once stream data has been delivered, the stream + * enters the "Data Read" state, which is a terminal state. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_READ, + /** + * Receiving a RESET_STREAM frame in the "Recv" or "Size Known" state + * causes the stream to enter the "Reset Recvd" state. This might + * cause the delivery of stream data to the application to be + * interrupted. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_RECVD, + /** + * Once the application receives the signal indicating that the + * stream was reset, the receiving part of the stream transitions to + * the "Reset Read" state, which is a terminal state. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_READ; + + @Override + public boolean isTerminal() { + return this == DATA_READ || this == RESET_READ; + } + + /** + * {@return true if this state indicates that the stream has been reset by the sender} + */ + public boolean isReset() { return this == RESET_RECVD || this == RESET_READ; } + } + + /** + * {@return the receiving state of the stream} + */ + ReceivingStreamState receivingState(); + + /** + * Connects an {@linkplain QuicStreamReader#started() unstarted} reader + * to the receiver end of this stream. + * @param scheduler A sequential scheduler that will be invoked + * when the reader is started and new data becomes available for reading + * @return a {@code QuicStreamReader} to read data from this + * stream. + * @throws IllegalStateException if a reader is already connected. + */ + QuicStreamReader connectReader(SequentialScheduler scheduler); + + /** + * Disconnect the reader, so that a new reader can be connected. + * + * @apiNote + * This can be useful for handing the stream over after having read + * or peeked at some bytes. + * + * @param reader the reader to be disconnected + * @throws IllegalStateException if the given reader is not currently + * connected to the stream + */ + void disconnectReader(QuicStreamReader reader); + + /** + * Cancels the reading side of this stream by sending + * a STOP_SENDING frame. + * + * @param errorCode the application error code + * + */ + void requestStopSending(long errorCode); + + /** + * {@return the amount of data that has been received so far} + * @apiNote This may include data that has not been read by the + * application yet, but does not count any data that may have + * been received twice. + */ + long dataReceived(); + + /** + * {@return the maximum amount of data that can be received on + * this stream} + * + * @apiNote This corresponds to the maximum amount of data that + * the peer has been allowed to send. + */ + long maxStreamData(); + + /** + * {@return the error code for this stream, or {@code -1}} + */ + long rcvErrorCode(); + + default boolean isStopSendingRequested() { return false; } + + @Override + default boolean hasError() { + return rcvErrorCode() >= 0; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java new file mode 100644 index 00000000000..120a9bffd5b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicReceiverStreamImpl.java @@ -0,0 +1,942 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.OrderedFlow.StreamDataFlow; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static jdk.internal.net.http.quic.QuicConnectionImpl.DEFAULT_INITIAL_STREAM_MAX_DATA; +import static jdk.internal.net.http.quic.frames.QuicFrame.MAX_VL_INTEGER; +import static jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState.*; + +/** + * A class that implements the receiver part of a quic stream. + */ +final class QuicReceiverStreamImpl extends AbstractQuicStream implements QuicReceiverStream { + + private static final int MAX_SMALL_FRAGMENTS = + Utils.getIntegerProperty("jdk.httpclient.quic.maxSmallFragments", 100); + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final String dbgTag; + + // The dataFlow reorders incoming stream frames and removes duplicates. + // It contains frames that cannot be delivered yet because they are not + // at the expected offset. + private final StreamDataFlow dataFlow = new StreamDataFlow(); + // The orderedQueue contains frames that can be delivered to the application now. + // They are inserted in the queue in order. + // The QuicStreamReader's scheduler loop consumes this queue. + private final ConcurrentLinkedQueue orderedQueue = new ConcurrentLinkedQueue<>(); + // Desired buffer size; used when updating maxStreamData + private final long desiredBufferSize; + // Maximum stream data + private volatile long maxStreamData; + // how much data has been processed on this stream. + // This is data that was poll'ed from orderedQueue or dropped after stream reset. + private volatile long processed; + // how much data has been delivered to orderedQueue. This doesn't take into account + // frames that may be stored in the dataFlow. + private volatile long received; + // maximum of offset+length across all received frames + private volatile long maxReceivedData; + // the size of the stream, when known. Defaults to 0 when unknown. + private volatile long knownSize; + // the connected reader + private volatile QuicStreamReaderImpl reader; + // eof when the last payload has been polled by the application + private volatile boolean eof; + // the state of the receiving stream + private volatile ReceivingStreamState receivingState; + private volatile boolean requestedStopSending; + private volatile long errorCode; + + private final static long MIN_BUFFER_SIZE = 16L << 10; + QuicReceiverStreamImpl(QuicConnectionImpl connection, long streamId) { + super(connection, validateStreamId(connection, streamId)); + errorCode = -1; + receivingState = ReceivingStreamState.RECV; + dbgTag = connection.streamDbgTag(streamId, "R"); + long bufsize = DEFAULT_INITIAL_STREAM_MAX_DATA; + desiredBufferSize = Math.clamp(bufsize, MIN_BUFFER_SIZE, MAX_VL_INTEGER); + } + + private static long validateStreamId(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return streamId; + if (connection.isClientConnection() == QuicStreams.isClientInitiated(streamId)) { + throw new IllegalArgumentException("A locally initiated stream can't be read-only"); + } + return streamId; + } + + /** + * Sends a {@link ConnectionCloseFrame} due to MAX_STREAM_DATA exceeded + * for the stream. + * @param streamFrame the stream frame that caused the excess + * @param maxData the value of MAX_STREAM_DATA which was exceeded + */ + private static QuicTransportException streamControlOverflow(StreamFrame streamFrame, long maxData) throws QuicTransportException { + String reason = "Stream max data exceeded: offset=%s, length=%s, max stream data=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), maxData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FLOW_CONTROL_ERROR); + } + + // debug tag for debug logger + String dbgTag() { + return dbgTag; + } + + @Override + public StreamState state() { + return receivingState(); + } + + @Override + public ReceivingStreamState receivingState() { + return receivingState; + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + var reader = this.reader; + if (reader == null) { + reader = new QuicStreamReaderImpl(scheduler); + if (Handles.READER.compareAndSet(this, null, reader)) { + if (debug.on()) debug.log("reader connected"); + return reader; + } + } + throw new IllegalStateException("reader already connected"); + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + var previous = this.reader; + if (reader == previous) { + if (Handles.READER.compareAndSet(this, reader, null)) { + if (debug.on()) debug.log("reader disconnected"); + return; + } + } + throw new IllegalStateException("reader not connected"); + } + + @Override + public boolean isStopSendingRequested() { + return requestedStopSending; + } + + @Override + public void requestStopSending(final long errorCode) { + if (Handles.STOP_SENDING.compareAndSet(this, false, true)) { + assert requestedStopSending : "requestedStopSending should be true!"; + if (debug.on()) debug.log("requestedStopSending: true"); + var state = receivingState; + try { + setErrorCode(errorCode); + switch(state) { + case RECV, SIZE_KNOWN -> { + connection().scheduleStopSendingFrame(streamId(), errorCode); + } + // otherwise do nothing + } + } finally { + // RFC-9000, section 3.5: "If an application is no longer interested in the data it is + // receiving on a stream, it can abort reading the stream and specify an application + // error code." + // So it implies that the application isn't anymore interested in receiving the data + // that has been buffered in the stream, so we drop all buffered data on this stream + if (state != RECV && state != DATA_READ) { + // we know the final size; we can remove the stream + increaseProcessedData(knownSize); + if (switchReceivingState(RESET_READ)) { + eof = false; + } + } + dataFlow.clear(); + orderedQueue.clear(); + if (debug.on()) { + debug.log("Dropped all buffered frames on stream %d after STOP_SENDING was requested" + + " with error code 0x%s", streamId(), Long.toHexString(errorCode)); + } + } + } + } + + @Override + public long dataReceived() { + return received; + } + + @Override + public long maxStreamData() { + return maxStreamData; + } + + @Override + public boolean isDone() { + return switch (receivingState()) { + case DATA_READ, DATA_RECVD, RESET_READ, RESET_RECVD -> + // everything received from peer + true; + default -> + // the stream is only half closed + false; + }; + } + + /** + * Receives a QuicFrame from the remote peer. + * + * @param resetStreamFrame the frame received + */ + void processIncomingResetFrame(final ResetStreamFrame resetStreamFrame) + throws QuicTransportException { + try { + checkUpdateState(resetStreamFrame); + if (requestedStopSending) { + increaseProcessedData(knownSize); + switchReceivingState(RESET_READ); + } + } finally { + // make sure the state is switched to reset received. + // even if we're closing the connection + switchReceivingState(RESET_RECVD); + // wakeup reader, then throw exception. + QuicStreamReaderImpl reader = this.reader; + if (reader != null) reader.wakeup(); + } + } + + void processIncomingFrame(final StreamDataBlockedFrame streamDataBlocked) { + assert streamDataBlocked.streamId() == streamId() : "unexpected stream id"; + final long peerBlockedOn = streamDataBlocked.maxStreamData(); + final long currentLimit = this.maxStreamData; + if (peerBlockedOn > currentLimit) { + // shouldn't have happened. ignore and don't increase the limit. + return; + } + // the peer has stated that the stream is blocked due to flow control limit that we have + // imposed and has requested for increasing the limit. we approve that request + // and increase the limit only if the amount of received data that we have received and + // processed on this stream is more than 1/4 of the credit window. + if (!requestedStopSending + && currentLimit - processed < (desiredBufferSize - desiredBufferSize / 4)) { + demand(desiredBufferSize); + } else { + if (debug.on()) { + debug.log("ignoring STREAM_DATA_BLOCKED frame %s," + + " since current limit %d is large enough", streamDataBlocked, currentLimit); + } + } + } + + private void demand(final long additional) { + assert additional > 0 && additional < MAX_VL_INTEGER : "invalid demand: " + additional; + var received = dataReceived(); + var maxStreamData = maxStreamData(); + + final long newMax = Math.clamp(received + additional, maxStreamData, MAX_VL_INTEGER); + if (newMax > maxStreamData) { + connection().requestSendMaxStreamData(streamId(), newMax); + updateMaxStreamData(newMax); + } + } + + /** + * Called when the connection is closed + * @param terminationCause the termination cause + */ + void terminate(final TerminationCause terminationCause) { + setErrorCode(terminationCause.getCloseCode()); + final QuicStreamReaderImpl reader = this.reader; + if (reader != null) { + reader.wakeup(); + } + } + + @Override + public long rcvErrorCode() { + return errorCode; + } + + /** + * Receives a QuicFrame from the remote peer. + * + * @param streamFrame the frame received + */ + public void processIncomingFrame(final StreamFrame streamFrame) + throws QuicTransportException { + // RFC-9000, section 3.5: "STREAM frames received after sending a STOP_SENDING frame + // are still counted toward connection and stream flow control, even though these + // frames can be discarded upon receipt." + // so we do the necessary data size checks before checking if we sent a "STOP_SENDING" + // frame + checkUpdateState(streamFrame); + final ReceivingStreamState state = receivingState; + if (debug.on()) debug.log("receivingState: " + state); + long knownSize = this.knownSize; + // RESET was read or received: drop the frame. + if (state == RESET_READ || state == RESET_RECVD) { + if (debug.on()) { + debug.log("Dropping frame on stream %d since state is %s", + streamId(), state); + } + return; + } + if (requestedStopSending) { + // drop the frame + if (debug.on()) { + debug.log("Dropping frame that was received after a STOP_SENDING" + + " frame was sent on stream %d", streamId()); + } + increaseProcessedData(maxReceivedData); + if (state != RECV) { + // we know the final size; we can remove the stream + switchReceivingState(RESET_READ); + } + return; + } + + var readyFrame = dataFlow.receive(streamFrame); + var received = this.received; + boolean needWakeup = false; + while (readyFrame != null) { + // check again - this avoids a race condition where a frame + // would be considered ready if requestStopSending had been + // called concurrently, and `receive` was called after the + // state had been switched + if (requestedStopSending) { + return; + } + assert received == readyFrame.offset() + : "data received (%s) doesn't match offset (%s)" + .formatted(received, readyFrame.offset()); + this.received = received = received + readyFrame.dataLength(); + offer(readyFrame); + needWakeup = true; + readyFrame = dataFlow.poll(); + } + if (state == SIZE_KNOWN && received == knownSize) { + if (switchReceivingState(DATA_RECVD)) { + offerEof(); + needWakeup = true; + } + } + if (needWakeup) { + var reader = this.reader; + if (reader != null) reader.wakeup(); + } else { + int numFrames = dataFlow.size(); + long numBytes = dataFlow.buffered(); + if (numFrames > MAX_SMALL_FRAGMENTS && numBytes / numFrames < 400) { + // The peer sent a large number of small fragments + // that follow a gap and can't be immediately released to the reader; + // we need to buffer them, and the memory overhead is unreasonably high. + throw new QuicTransportException("Excessive stream fragmentation", + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.frameType(), + QuicTransportErrors.INTERNAL_ERROR); + } + } + } + + /** + * Checks for error conditions: + * - max stream data errors + * - max data errors + * - final size errors + * If everything checks OK, updates counters and returns, otherwise throws. + * + * @implNote + * This method may update counters before throwing. This is OK + * because we do not expect to use them again in this case. + * @param streamFrame received stream frame + * @throws QuicTransportException if frame is invalid + */ + private void checkUpdateState(StreamFrame streamFrame) throws QuicTransportException { + long offset = streamFrame.offset(); + long length = streamFrame.dataLength(); + assert offset >= 0; + assert length >= 0; + + // check maxStreamData + long maxData = maxStreamData; + assert maxData >= 0; + long size; + try { + size = Math.addExact(offset, length); + } catch (ArithmeticException x) { + // should not happen + if (debug.on()) { + debug.log("offset + length exceeds max value", x); + } + throw streamControlOverflow(streamFrame, Long.MAX_VALUE); + } + if (size > maxData) { + throw streamControlOverflow(streamFrame, maxData); + } + ReceivingStreamState state = receivingState; + // check finalSize if known + long knownSize = this.knownSize; + assert knownSize >= 0; + if (state != RECV && size > knownSize) { + String reason = "Stream final size exceeded: offset=%s, length=%s, final size=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + // check maxData + updateMaxReceivedData(size, streamFrame.getTypeField()); + if (streamFrame.isLast()) { + // check max received data, throw if we have data beyond the (new) EOF + if (size < maxReceivedData) { + String reason = "Stream truncated: offset=%s, length=%s, max received=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), maxReceivedData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + if (state == RECV && switchReceivingState(SIZE_KNOWN)) { + this.knownSize = size; + } else { + if (size != knownSize) { + String reason = "Stream final size changed: offset=%s, length=%s, final size=%s" + .formatted(streamFrame.offset(), streamFrame.dataLength(), knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, streamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + } + } + } + + /** + * Checks for error conditions: + * - max stream data errors + * - max data errors + * - final size errors + * If everything checks OK, updates counters and returns, otherwise throws. + * + * @implNote + * This method may update counters before throwing. This is OK + * because we do not expect to use them again in this case. + * @param resetStreamFrame received reset stream frame + * @throws QuicTransportException if frame is invalid + */ + private void checkUpdateState(ResetStreamFrame resetStreamFrame) throws QuicTransportException { + // check maxStreamData + long maxData = maxStreamData; + assert maxData >= 0; + long size = resetStreamFrame.finalSize(); + long errorCode = resetStreamFrame.errorCode(); + setErrorCode(errorCode); + if (size > maxData) { + String reason = "Stream max data exceeded: finalSize=%s, max stream data=%s" + .formatted(size, maxData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FLOW_CONTROL_ERROR); + } + ReceivingStreamState state = receivingState; + updateMaxReceivedData(size, resetStreamFrame.getTypeField()); + // check max received data, throw if we have data beyond the (new) EOF + if (size < maxReceivedData) { + String reason = "Stream truncated: finalSize=%s, max received=%s" + .formatted(size, maxReceivedData); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + if (state == RECV && switchReceivingState(RESET_RECVD)) { + this.knownSize = size; + } else { + if (state == SIZE_KNOWN) { + switchReceivingState(RESET_RECVD); + } + if (size != knownSize) { + String reason = "Stream final size changed: new finalSize=%s, old final size=%s" + .formatted(size, knownSize); + throw new QuicTransportException(reason, + QuicTLSEngine.KeySpace.ONE_RTT, resetStreamFrame.getTypeField(), QuicTransportErrors.FINAL_SIZE_ERROR); + } + } + } + + void checkOpened() throws IOException { + final TerminationCause terminationCause = connection().terminationCause(); + if (terminationCause == null) { + return; + } + throw terminationCause.getCloseCause(); + } + + private void offer(StreamFrame frame) { + var payload = frame.payload(); + if (payload.hasRemaining()) { + orderedQueue.add(payload.slice()); + } + } + + private void offerEof() { + orderedQueue.add(QuicStreamReader.EOF); + } + + /** + * Update the value of MAX_STREAM_DATA for this stream + * @param newMaxStreamData + */ + public void updateMaxStreamData(long newMaxStreamData) { + long maxStreamData = this.maxStreamData; + boolean updated = false; + while (maxStreamData < newMaxStreamData) { + if (updated = Handles.MAX_STREAM_DATA.compareAndSet(this, maxStreamData, newMaxStreamData)) break; + maxStreamData = this.maxStreamData; + } + if (updated) { + if (debug.on()) { + debug.log("updateMaxStreamData: max stream data updated from %s to %s", + maxStreamData, newMaxStreamData); + } + } + } + + /** + * Update the {@code maxReceivedData} value, and return the amount + * by which {@code maxReceivedData} was increased. This method is a + * no-op and returns 0 if {@code maxReceivedData >= newMax}. + * + * @param newMax the new max offset - typically obtained + * by adding the length of a frame to its + * offset + * @param frameType type of frame received + * @throws QuicTransportException if flow control was violated + */ + private void updateMaxReceivedData(long newMax, long frameType) throws QuicTransportException { + assert newMax >= 0; + var max = this.maxReceivedData; + while (max < newMax) { + if (Handles.MAX_RECEIVED_DATA.compareAndSet(this, max, newMax)) { + // report accepted data to connection flow control, + // and update the amount of data received in the + // connection. This will also check whether connection + // flow control is exceeded, and throw in + // this case + connection().increaseReceivedData(newMax - max, frameType); + return; + } + max = this.maxReceivedData; + } + } + + /** + * Notifies the connection about received data that is no longer buffered. + */ + private void increaseProcessedDataBy(int diff) { + assert diff >= 0; + if (diff <= 0) return; + synchronized (this) { + if (requestedStopSending) { + // once we request stop sending, updates are handled by increaseProcessedData + return; + } + assert processed + diff <= received : processed+"+"+diff+">"+received+"("+maxReceivedData+")"; + processed += diff; + } + connection().increaseProcessedData(diff); + } + + /** + * Notifies the connection about received data that is no longer buffered. + */ + private void increaseProcessedData(long newProcessed) { + long diff; + synchronized (this) { + if (newProcessed > processed) { + diff = newProcessed - processed; + processed = newProcessed; + } else { + diff = 0; + } + } + if (diff > 0) { + connection().increaseProcessedData(diff); + } + } + + // private implementation of a QuicStreamReader for this stream + private final class QuicStreamReaderImpl extends QuicStreamReader { + + static final int STARTED = 1; + static final int PENDING = 2; + // should not need volatile here, as long as we + // switch to using synchronize whenever state & STARTED == 0 + // Once state & STARTED != 0 the state should no longer change + private int state; + + QuicStreamReaderImpl(SequentialScheduler scheduler) { + super(scheduler); + } + + @Override + public ReceivingStreamState receivingState() { + checkConnected(); + return QuicReceiverStreamImpl.this.receivingState(); + } + + @Override + public ByteBuffer poll() throws IOException { + checkConnected(); + var buffer = orderedQueue.poll(); + if (buffer == null) { + if (eof) return EOF; + var state = receivingState; + if (state == RESET_RECVD) { + increaseProcessedData(knownSize); + } + checkReset(); + // unfulfilled = maxStreamData - received; + // if we have received more than 1/4 of the buffer, update maxStreamData + if (!requestedStopSending && unfulfilled() < desiredBufferSize - desiredBufferSize / 4) { + demand(desiredBufferSize); + } + return null; + } + + if (requestedStopSending) { + // check reset again + checkReset(); + return null; + } + increaseProcessedDataBy(buffer.capacity()); + if (buffer == EOF) { + eof = true; + assert processed == received : processed + "!=" + received; + switchReceivingState(DATA_READ); + return EOF; + } + // if the amount of received data that has been processed on this stream is + // more than 1/4 of the credit window then send a MaxStreamData frame. + if (!requestedStopSending && maxStreamData - processed < desiredBufferSize - desiredBufferSize / 4) { + demand(desiredBufferSize); + } + return buffer; + } + + /** + * Checks whether the stream was reset and throws an exception if + * yes. + * + * @throws IOException if the stream is reset + */ + private void checkReset() throws IOException { + var state = receivingState; + if (state == RESET_READ || state == RESET_RECVD) { + if (state == RESET_RECVD) { + switchReceivingState(RESET_READ); + } + if (requestedStopSending) { + throw new IOException("Stream %s closed".formatted(streamId())); + } else { + throw new IOException("Stream %s reset by peer".formatted(streamId())); + } + } + checkOpened(); + } + + @Override + public ByteBuffer peek() throws IOException { + checkConnected(); + var buffer = orderedQueue.peek(); + if (buffer == null) { + checkReset(); + return eof ? EOF : null; + } + return buffer; + } + + private long unfulfilled() { + // TODO: should we synchronize to ensure consistency? + var max = maxStreamData; + var rcved = received; + return max - rcved; + } + + @Override + public QuicReceiverStream stream() { + var stream = QuicReceiverStreamImpl.this; + var reader = stream.reader; + return reader == this ? stream : null; + } + + @Override + public boolean connected() { + var reader = QuicReceiverStreamImpl.this.reader; + return reader == this; + } + + @Override + public boolean started() { + int state = this.state; + if ((state & STARTED) == STARTED) return true; + synchronized (this) { + state = this.state; + return (state & STARTED) == STARTED; + } + } + + private boolean wakeupOnStart(int state) { + assert Thread.holdsLock(this); + return (state & PENDING) != 0 + || !orderedQueue.isEmpty() + || receivingState != RECV; + } + + @Override + public void start() { + // Run the scheduler if woken up before starting + int state = this.state; + if ((state & STARTED) == 0) { + boolean wakeup = false; + synchronized (this) { + state = this.state; + if ((state & STARTED) == 0) { + wakeup = wakeupOnStart(state); + state = this.state = STARTED; + } + } + assert started(); + if (debug.on()) { + debug.log("reader started (wakeup: %s)", wakeup); + } + if (wakeup || !orderedQueue.isEmpty() || receivingState != RECV) wakeup(); + } + assert started(); + } + + private void checkConnected() { + if (!connected()) throw new IllegalStateException("reader not connected"); + } + + void wakeup() { + // Only run the scheduler after the reader is started. + int state = this.state; + boolean notstarted, pending = false; + if (notstarted = ((state & STARTED) == 0)) { + synchronized (this) { + state = this.state; + if (notstarted = ((state & STARTED) == 0)) { + state = this.state = (state | PENDING); + pending = (state & PENDING) == PENDING; + assert !started(); + } + } + } + if (notstarted) { + if (debug.on()) { + debug.log("reader not started (pending: %s)", pending); + } + return; + } + assert started(); + scheduler.runOrSchedule(connection().quicInstance().executor()); + } + } + + /** + * Called when a state change is needed + * @param newState the new state. + */ + private boolean switchReceivingState(ReceivingStreamState newState) { + ReceivingStreamState oldState = receivingState; + if (debug.on()) { + debug.log("switchReceivingState %s -> %s", + oldState, newState); + } + boolean switched = switch(newState) { + case SIZE_KNOWN -> markSizeKnown(); + case DATA_RECVD -> markDataRecvd(); + case RESET_RECVD -> markResetRecvd(); + case RESET_READ -> markResetRead(); + case DATA_READ -> markDataRead(); + default -> throw new UnsupportedOperationException("switch state to " + newState); + }; + if (debug.on()) { + if (switched) { + debug.log("switched receiving state from %s to %s", oldState, newState); + } else { + debug.log("receiving state not switched; state is %s", receivingState); + } + } + + if (switched && newState.isTerminal()) { + notifyTerminalState(newState); + } + + return switched; + } + + private void notifyTerminalState(ReceivingStreamState state) { + assert state == DATA_READ || state == RESET_READ : state; + connection().notifyTerminalState(streamId(), state); + } + + // DATA_RECV is reached when the last frame is received, + // and there's no gap + private boolean markDataRecvd() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV, SIZE_KNOWN -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, DATA_RECVD); + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ -> true; + }; + } while (!done); + return switched; + } + + // SIZE_KNOWN is reached when a stream frame with the FIN bit is received + private boolean markSizeKnown() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, SIZE_KNOWN); + case DATA_RECVD, DATA_READ, SIZE_KNOWN, RESET_RECVD, RESET_READ -> true; + }; + } while(!done); + return switched; + } + + // RESET_RECV is reached when a RESET_STREAM frame is received + private boolean markResetRecvd() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case RECV, SIZE_KNOWN -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, RESET_RECVD); + case DATA_RECVD, DATA_READ, RESET_RECVD, RESET_READ -> true; + }; + } while(!done); + return switched; + } + + // Called when the consumer has polled the last data + // DATA_READ is a terminal state + private boolean markDataRead() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case SIZE_KNOWN, DATA_RECVD, RESET_RECVD -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, DATA_READ); + case RESET_READ, DATA_READ -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, DATA_READ)); + }; + } while(!done); + return switched; + } + + // Called when the consumer has read the reset + // RESET_READ is a terminal state + private boolean markResetRead() { + boolean done, switched = false; + ReceivingStreamState oldState; + do { + oldState = receivingState; + done = switch (oldState) { + // CAS: Compare And Set + case SIZE_KNOWN, DATA_RECVD, RESET_RECVD -> switched = + Handles.RECEIVING_STATE.compareAndSet(this, + oldState, RESET_READ); + case RESET_READ, DATA_READ -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, RESET_READ)); + }; + } while(!done); + return switched; + } + + private void setErrorCode(long code) { + Handles.ERROR_CODE.compareAndSet(this, -1, code); + } + + private static final class Handles { + static final VarHandle READER; + static final VarHandle RECEIVING_STATE; + static final VarHandle MAX_STREAM_DATA; + static final VarHandle MAX_RECEIVED_DATA; + static final VarHandle STOP_SENDING; + static final VarHandle ERROR_CODE; + static { + try { + var lookup = MethodHandles.lookup(); + RECEIVING_STATE = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "receivingState", ReceivingStreamState.class); + READER = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "reader", QuicStreamReaderImpl.class); + MAX_STREAM_DATA = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "maxStreamData", long.class); + MAX_RECEIVED_DATA = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "maxReceivedData", long.class); + STOP_SENDING = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "requestedStopSending", boolean.class); + ERROR_CODE = lookup.findVarHandle(QuicReceiverStreamImpl.class, + "errorCode", long.class); + } catch (Exception x) { + throw new ExceptionInInitializerError(x); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java new file mode 100644 index 00000000000..bdd5b55ee0b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStream.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; + +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * An interface that represents the sending part of a stream. + *

        From RFC 9000: + * + * On the sending part of a stream, an application protocol can: + *

          + *
        • write data, understanding when stream flow control credit + * (Section 4.1) has successfully been reserved to send the + * written data;
        • + *
        • end the stream (clean termination), resulting in a STREAM frame + * (Section 19.8) with the FIN bit set; and
        • + *
        • reset the stream (abrupt termination), resulting in a RESET_STREAM + * frame (Section 19.4) if the stream was not already in a terminal + * state.
        • + *
        + * + */ +public non-sealed interface QuicSenderStream extends QuicStream { + + /** + * An enum that models the state of the sending part of a stream. + */ + enum SendingStreamState implements QuicStream.StreamState { + /** + * The "Ready" state represents a newly created stream that is able + * to accept data from the application. Stream data might be + * buffered in this state in preparation for sending. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + READY, + /** + * In the "Send" state, an endpoint transmits -- and retransmits as + * necessary -- stream data in STREAM frames. The endpoint respects + * the flow control limits set by its peer and continues to accept + * and process MAX_STREAM_DATA frames. An endpoint in the "Send" state + * generates STREAM_DATA_BLOCKED frames if it is blocked from sending + * by stream flow control limits (Section 4.1). + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + SEND, + /** + * After the application indicates that all stream data has been sent + * and a STREAM frame containing the FIN bit is sent, the sending part + * of the stream enters the "Data Sent" state. From this state, the + * endpoint only retransmits stream data as necessary. The endpoint + * does not need to check flow control limits or send STREAM_DATA_BLOCKED + * frames for a stream in this state. MAX_STREAM_DATA frames might be received + * until the peer receives the final stream offset. The endpoint can safely + * ignore any MAX_STREAM_DATA frames it receives from its peer for a + * stream in this state. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_SENT, + /** + * From any state that is one of "Ready", "Send", or "Data Sent", an + * application can signal that it wishes to abandon transmission of + * stream data. Alternatively, an endpoint might receive a STOP_SENDING + * frame from its peer. In either case, the endpoint sends a RESET_STREAM + * frame, which causes the stream to enter the "Reset Sent" state. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_SENT, + /** + * Once all stream data has been successfully acknowledged, the sending + * part of the stream enters the "Data Recvd" state, which is a + * terminal state. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + DATA_RECVD, + /** + * Once a packet containing a RESET_STREAM has been acknowledged, the + * sending part of the stream enters the "Reset Recvd" state, which + * is a terminal state. + *

        + * [RFC 9000, Section 3.1] + * (https://www.rfc-editor.org/rfc/rfc9000#name-sending-stream-states) + */ + RESET_RECVD; + + @Override + public boolean isTerminal() { + return this == DATA_RECVD || this == RESET_RECVD; + } + + /** + * {@return true if a stream in this state can be used for sending, that is, + * if this state is either {@link #READY} or {@link #SEND}}. + */ + public boolean isSending() { return this == READY || this == SEND; } + + /** + * {@return true if this state indicates that the stream has been reset by the sender} + */ + public boolean isReset() { return this == RESET_SENT || this == RESET_RECVD; } + } + + /** + * {@return the sending state of the stream} + */ + SendingStreamState sendingState(); + + /** + * Connects a writer to the sending end of this stream. + * @param scheduler A sequential scheduler that will + * push data on the returned {@linkplain + * QuicStreamWriter#QuicStreamWriter(SequentialScheduler) + * writer}. + * @return a {@code QuicStreamWriter} to write data to this + * stream. + * @throws IllegalStateException if a writer is already connected. + */ + QuicStreamWriter connectWriter(SequentialScheduler scheduler); + + /** + * Disconnect the writer, so that a new writer can be connected. + * + * @apiNote + * This can be useful for handing the stream over after having written + * some bytes. + * + * @param writer the writer to be disconnected + * @throws IllegalStateException if the given writer is not currently + * connected to the stream + */ + public void disconnectWriter(QuicStreamWriter writer); + + /** + * Abruptly closes the writing side of a stream by sending + * a RESET_STREAM frame. + * @param errorCode the application error code + */ + void reset(long errorCode) throws IOException; + + /** + * {@return the amount of data that has been sent} + * @apiNote + * This may include data that has not been acknowledged. + */ + long dataSent(); + + /** + * {@return the error code for this stream, or {@code -1}} + */ + long sndErrorCode(); + + /** + * {@return true if STOP_SENDING was received} + */ + boolean stopSendingReceived(); + + @Override + default boolean hasError() { + return sndErrorCode() >= 0; + } + + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java new file mode 100644 index 00000000000..292face444c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicSenderStreamImpl.java @@ -0,0 +1,662 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.util.Set; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; + +/** + * A class that implements the sending part of a quic stream. + */ +public final class QuicSenderStreamImpl extends AbstractQuicStream implements QuicSenderStream { + private volatile SendingStreamState sendingState; + private volatile QuicStreamWriterImpl writer; + private final Logger debug = Utils.getDebugLogger(this::dbgTag); + private final String dbgTag; + private final StreamWriterQueueImpl queue = new StreamWriterQueueImpl(); + private volatile long errorCode; + private volatile boolean stopSendingReceived; + + QuicSenderStreamImpl(QuicConnectionImpl connection, long streamId) { + super(connection, validateStreamId(connection, streamId)); + errorCode = -1; + sendingState = SendingStreamState.READY; + dbgTag = connection.streamDbgTag(streamId, "W"); + } + + private static long validateStreamId(QuicConnectionImpl connection, long streamId) { + if (QuicStreams.isBidirectional(streamId)) return streamId; + if (connection.isClientConnection() != QuicStreams.isClientInitiated(streamId)) { + throw new IllegalArgumentException("A remotely initiated stream can't be write-only"); + } + return streamId; + } + + String dbgTag() { + return dbgTag; + } + + @Override + public SendingStreamState sendingState() { + return sendingState; + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + var writer = this.writer; + if (writer == null) { + writer = new QuicStreamWriterImpl(scheduler); + if (Handles.WRITER.compareAndSet(this, null, writer)) { + if (debug.on()) debug.log("writer connected"); + return writer; + } + } + throw new IllegalStateException("writer already connected"); + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + var previous = this.writer; + if (writer == previous) { + if (Handles.WRITER.compareAndSet(this, writer, null)) { + if (debug.on()) debug.log("writer disconnected"); + return; + } + } + throw new IllegalStateException("reader not connected"); + } + + + @Override + public void reset(long errorCode) throws IOException { + if (debug.on()) { + debug.log("Resetting stream %s due to %s", streamId(), + connection().quicInstance().appErrorToString(errorCode)); + } + setErrorCode(errorCode); + if (switchSendingState(SendingStreamState.RESET_SENT)) { + long streamId = streamId(); + if (debug.on()) { + debug.log("Requesting to send RESET_STREAM(%d, %d)", + streamId, errorCode); + } + queue.markReset(); + if (connection().isOpen()) { + connection().requestResetStream(streamId, errorCode); + } + } + } + + @Override + public long sndErrorCode() { + return errorCode; + } + + @Override + public boolean stopSendingReceived() { + return stopSendingReceived; + } + + @Override + public long dataSent() { + // returns the amount of data that has been submitted for + // sending downstream. This will be the amount of data that + // has been consumed by the downstream consumer. + return queue.bytesConsumed(); + } + + /** + * Called to set the max stream data for this stream. + * @apiNote as per RFC 9000, any value less than the current + * max stream data is ignored + * @param newLimit the proposed new max stream data + * @return the new limit that has been finalized for max stream data. + * This new limit may or may not have been increased to the proposed {@code newLimit}. + */ + public long setMaxStreamData(final long newLimit) { + return queue.setMaxStreamData(newLimit); + } + + /** + * Called by {@link QuicConnectionStreams} after a RESET_STREAM frame + * has been sent + */ + public void resetSent() { + queue.markReset(); + queue.close(); + } + + /** + * Called when the packet containing the RESET_STREAM frame for this + * stream has been acknowledged. + * @param finalSize the final size acknowledged + * @return true if the state was switched to RESET_RECVD as a result + * of this method invocation + */ + public boolean resetAcknowledged(long finalSize) { + long queueSize = queue.bytesConsumed(); + if (switchSendingState(SendingStreamState.RESET_RECVD)) { + if (debug.on()) { + debug.log("Reset received: final: %d, processed: %d", + finalSize, queueSize); + } + if (finalSize != queueSize) { + if (Log.errors()) { + Log.logError("Stream %d: Acknowledged reset has wrong size: acked: %d, expected: %d", + streamId(), finalSize, queueSize); + } + } + return true; + } + return false; + } + + /** + * Called when the packet containing the final STREAM frame for this + * stream has been acknowledged. + * @param finalSize the final size acknowledged + * @return true if the state was switched to DATA_RECVD as a result + * of this method invocation + */ + public boolean dataAcknowledged(long finalSize) { + long queueSize = queue.bytesConsumed(); + if (switchSendingState(SendingStreamState.DATA_RECVD)) { + if (debug.on()) { + debug.log("Last data received: final: %d, processed: %d", + finalSize, queueSize); + } + if (finalSize != queueSize) { + if (Log.errors()) { + Log.logError("Stream %d: Acknowledged data has wrong size: acked: %d, expected: %d", + streamId(), finalSize, queueSize); + } + } + } + return false; + } + + /** + * Called when a STOP_SENDING frame is received from the peer + * @param errorCode the error code + */ + public void stopSendingReceived(long errorCode) { + if (queue.stopSending(errorCode)) { + stopSendingReceived = true; + setErrorCode(errorCode); + try { + if (connection().isOpen()) { + reset(errorCode); + } + } catch (IOException io) { + if (debug.on()) debug.log("Reset failed: " + io); + } finally { + QuicStreamWriterImpl writer = this.writer; + if (writer != null) writer.wakeupWriter(); + } + } + } + + /** + * Called when the connection is closed locally + * @param terminationCause the termination cause + */ + void terminate(final TerminationCause terminationCause) { + setErrorCode(terminationCause.getCloseCode()); + queue.close(); + final QuicStreamWriterImpl writer = this.writer; + if (writer != null) { + writer.wakeupWriter(); + } + } + + /** + * A concrete implementation of the {@link StreamWriterQueue} for this + * stream. + */ + private final class StreamWriterQueueImpl extends StreamWriterQueue { + @Override + protected void wakeupProducer() { + // The scheduler is provided by the producer + // to wakeup and run the producer's write loop. + var writer = QuicSenderStreamImpl.this.writer; + if (writer != null) { + writer.wakeupWriter(); + } + } + + @Override + protected Logger debug() { + return debug; + } + + @Override + protected void wakeupConsumer() { + // Notify the connection impl that either the data is available + // for writing or the stream is blocked and the peer needs to be + // made aware. The connection should + // eventually call QuicSenderStreamImpl::poll to + // get the data available for writing and package it + // in a StreamFrame or notice that the stream is blocked and send a + // STREAM_DATA_BLOCKED frame. + connection().streamDataAvailableForSending(Set.of(streamId())); + } + + @Override + protected void switchState(SendingStreamState state) { + // called to indicate a change in the stream state. + // at the moment the only expected value is DATA_SENT + assert state == SendingStreamState.DATA_SENT; + switchSendingState(state); + } + + @Override + protected long streamId() { + return QuicSenderStreamImpl.this.streamId(); + } + } + + + /** + * The stream internal implementation of a QuicStreamWriter. + * Most of the logic is implemented in the StreamWriterQueue, + * which is subclassed here to provide an implementation of its + * few abstract methods. + */ + private class QuicStreamWriterImpl extends QuicStreamWriter { + QuicStreamWriterImpl(SequentialScheduler scheduler) { + super(scheduler); + } + + void wakeupWriter() { + scheduler.runOrSchedule(connection().quicInstance().executor()); + } + + @Override + public SendingStreamState sendingState() { + checkConnected(); + return QuicSenderStreamImpl.this.sendingState(); + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + checkConnected(); + SendingStreamState state = sending(last); + switch (state) { + // this isn't atomic but it doesn't really matter since reset + // will be handled by the same thread that polls. + case READY, SEND -> { + // allow a last empty buffer to be submitted even + // if the connection is closed. That can help + // unblock the consumer side. + if (buffer != QuicStreamReader.EOF || !last) { + checkOpened(); + } + queue.submit(buffer, last); + } + case RESET_SENT, RESET_RECVD -> throw streamResetException(); + case DATA_SENT, DATA_RECVD -> throw streamClosedException(); + } + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + checkConnected(); + SendingStreamState state = sending(false); + switch (state) { + // this isn't atomic but it doesn't really matter since reset + // will be handled by the same thread that polls. + case READY, SEND -> { + checkOpened(); + queue.queue(buffer); + } + case RESET_SENT, RESET_RECVD -> throw streamResetException(); + case DATA_SENT, DATA_RECVD -> throw streamClosedException(); + } + } + + /** + * Compose an exception to throw if data is submitted after the + * stream was reset + * @return a new IOException + */ + IOException streamResetException() { + long resetByPeer = queue.resetByPeer(); + if (resetByPeer < 0) { + return new IOException("stream %s reset by peer: errorCode %s" + .formatted(streamId(), - resetByPeer - 1)); + } else { + return new IOException("stream %s has been reset".formatted(streamId())); + } + } + + /** + * Compose an exception to throw if data is submitted after the + * the final data has been sent + * @return a new IOException + */ + IOException streamClosedException() { + return new IOException("stream %s is closed - all data has been sent" + .formatted(streamId())); + } + + @Override + public long credit() { + checkConnected(); + // how much data the producer can send before + // reaching the flow control limit. Could be + // negative if the limit has been reached already. + return queue.producerCredit(); + } + + @Override + public void reset(long errorCode) throws IOException { + setErrorCode(errorCode); + checkConnected(); + QuicSenderStreamImpl.this.reset(errorCode); + } + + @Override + public QuicSenderStream stream() { + var stream = QuicSenderStreamImpl.this; + var writer = stream.writer; + return writer == this ? stream : null; + } + + @Override + public boolean connected() { + var writer = QuicSenderStreamImpl.this.writer; + return writer == this; + } + + private void checkConnected() { + if (!connected()) { + throw new IllegalStateException("writer not connected"); + } + } + } + + void checkOpened() throws IOException { + final TerminationCause terminationCause = connection().terminationCause(); + if (terminationCause == null) { + return; + } + throw terminationCause.getCloseCause(); + } + + /** + * {@return the number of bytes that are available for sending, subject + * to flow control} + * @implSpec + * This method does not return more than what flow control for this + * stream would allow at the time the method is called. + * @implNote + * If the sender part is not finished initializing the default + * implementation of this method will return 0. + */ + public long available() { + return queue.readyToSend(); + } + + /** + * Whether the sending is blocked due to flow control. + * @return {@code true} if sending is blocked due to flow control + */ + public boolean isBlocked() { + return queue.consumerBlocked(); + } + + /** + * {@return the size of this stream, if known} + * @implSpec + * This method returns {@code -1} if the size of the stream is not + * known. + */ + public long streamSize() { + return queue.streamSize(); + } + + /** + * Polls at most {@code maxBytes} from the {@link StreamWriterQueue} of + * this stream. The semantics are equivalent to that of {@link + * StreamWriterQueue#poll(int)} + * @param maxBytes the maximum number of bytes to poll for sending + * @return a ByteBuffer containing at most {@code maxBytes} remaining + * bytes. + */ + public ByteBuffer poll(int maxBytes) { + return queue.poll(maxBytes); + } + + @Override + public boolean isDone() { + return switch (sendingState()) { + case DATA_RECVD, RESET_RECVD -> + // everything acknowledged + true; + default -> + // the stream is only half closed + false; + }; + } + + @Override + public StreamState state() { + return sendingState(); + } + + /** + * Called when some data is submitted (or offered) by the + * producer. If the stream is in the READY state, this will + * switch the sending state to SEND. + * @implNote + * The parameter {@code last} is ignored at this stage. + * {@link #switchSendingState(SendingStreamState) + * switchSendingState(SendingStreamState.DATA_SENT)} will be called + * later on when the last piece of data has been pushed downstream. + * + * @param last whether there will be no further data submitted + * by the producer. + * + * @return the state before switching to SEND. + */ + private SendingStreamState sending(boolean last) { + SendingStreamState state = sendingState; + if (state == SendingStreamState.READY) { + switchSendingState(SendingStreamState.SEND); + } + return state; + } + + /** + * Called when the StreamWriterQueue implementation notifies of + * a state change. + * @param newState the new state, according to the StreamWriterQueue. + */ + private boolean switchSendingState(SendingStreamState newState) { + SendingStreamState oldState = sendingState; + if (debug.on()) { + debug.log("switchSendingState %s -> %s", + oldState, newState); + } + boolean switched = switch(newState) { + case SEND -> markSending(); + case DATA_SENT -> markDataSent(); + case DATA_RECVD -> markDataRecvd(); + case RESET_SENT -> markResetSent(); + case RESET_RECVD -> markResetRecvd(); + default -> throw new UnsupportedOperationException("switch state to " + newState); + }; + if (debug.on()) { + if (switched) { + debug.log("switched sending state from %s to %s", oldState, newState); + } else { + debug.log("sending state not switched; state is %s", sendingState); + } + } + + if (switched && newState.isTerminal()) { + notifyTerminalState(newState); + } + + return switched; + } + + private void notifyTerminalState(SendingStreamState state) { + assert state.isTerminal() : state; + connection().notifyTerminalState(streamId(), state); + } + + // SEND can only be set from the READY state + private boolean markSending() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case READY -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.SEND); + case SEND, RESET_RECVD, RESET_SENT -> true; + // there should be no further submission of data after DATA_SENT + case DATA_SENT, DATA_RECVD -> + throw new IllegalStateException(String.valueOf(oldState)); + }; + } while(!done); + return switched; + } + + // DATA_SENT can only be set from the SEND state + private boolean markDataSent() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case SEND -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.DATA_SENT); + case DATA_SENT, RESET_RECVD, RESET_SENT, DATA_RECVD -> true; + case READY -> throw new IllegalStateException(String.valueOf(oldState)); + }; + } while (!done); + return switched; + } + + // Reset can only be set in the READY, SEND, or DATA_SENT state + private boolean markResetSent() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case READY, SEND, DATA_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.RESET_SENT); + case RESET_RECVD, RESET_SENT, DATA_RECVD -> true; + }; + } while(!done); + return switched; + } + + // Called when the packet containing the last frame is acknowledged + // DATA_RECVD is a terminal state + private boolean markDataRecvd() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case DATA_SENT, RESET_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.DATA_RECVD); + case RESET_RECVD, DATA_RECVD -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, SendingStreamState.RESET_RECVD)); + }; + } while(!done); + return switched; + } + + // Called when the packet containing the reset frame is acknowledged + // RESET_RECVD is a terminal state + private boolean markResetRecvd() { + boolean done, switched = false; + SendingStreamState oldState; + do { + oldState = sendingState; + done = switch (oldState) { + // CAS: Compare And Set + case DATA_SENT, RESET_SENT -> switched = + Handles.SENDING_STATE.compareAndSet(this, + oldState, SendingStreamState.RESET_RECVD); + case RESET_RECVD, DATA_RECVD -> true; + default -> throw new IllegalStateException("%s: %s -> %s" + .formatted(streamId(), oldState, SendingStreamState.RESET_RECVD)); + }; + } while(!done); + return switched; + } + + private void setErrorCode(long code) { + Handles.ERROR_CODE.compareAndSet(this, -1, code); + } + + // Some VarHandles to implement CAS semantics on top of plain + // volatile fields in this class. + private static class Handles { + static final VarHandle SENDING_STATE; + static final VarHandle WRITER; + static final VarHandle ERROR_CODE; + static { + Lookup lookup = MethodHandles.lookup(); + try { + SENDING_STATE = lookup.findVarHandle(QuicSenderStreamImpl.class, + "sendingState", SendingStreamState.class); + WRITER = lookup.findVarHandle(QuicSenderStreamImpl.class, + "writer", QuicStreamWriterImpl.class); + ERROR_CODE = lookup.findVarHandle(QuicSenderStreamImpl.class, + "errorCode", long.class); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java new file mode 100644 index 00000000000..4d99784299f --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStream.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +/** + * An interface to model a QuicStream. + * A quic stream can be either unidirectional + * or bidirectional. A unidirectional stream can + * be opened for reading or for writing. + * Concrete subclasses of {@code QuicStream} should + * implement {@link QuicSenderStream} (unidirectional {@link + * StreamMode#WRITE_ONLY} stream), or {@link QuicReceiverStream} + * (unidirectional {@link StreamMode#READ_ONLY} stream), or + * {@link QuicBidiStream} (bidirectional {@link StreamMode#READ_WRITE} stream). + */ +public sealed interface QuicStream + permits QuicSenderStream, QuicReceiverStream, QuicBidiStream, AbstractQuicStream { + + /** + * An interface that unifies the three different stream states. + * @apiNote + * This is mostly used for logging purposes, to log the + * combined state of a stream. + */ + sealed interface StreamState permits + QuicReceiverStream.ReceivingStreamState, + QuicSenderStream.SendingStreamState, + QuicBidiStream.BidiStreamState { + String name(); + + /** + * {@return true if this is a terminal state} + */ + boolean isTerminal(); + } + + /** + * The stream operation mode. + * One of {@link #READ_ONLY}, {@link #WRITE_ONLY}, or {@link #READ_WRITE}. + */ + enum StreamMode { + READ_ONLY, WRITE_ONLY, READ_WRITE; + + /** + * {@return true if this operation mode allows reading data from the stream} + */ + public boolean isReadable() { + return this != WRITE_ONLY; + } + + /** + * {@return true if this operation mode allows writing data to the stream} + */ + public boolean isWritable() { + return this != READ_ONLY; + } + } + + /** + * {@return the stream ID of this stream} + */ + long streamId(); + + /** + * {@return this stream operation mode} + * One of {@link StreamMode#READ_ONLY}, {@link StreamMode#WRITE_ONLY}, + * or {@link StreamMode#READ_WRITE}. + */ + StreamMode mode(); + + /** + * {@return whether this stream is client initiated} + */ + boolean isClientInitiated(); + + /** + * {@return whether this stream is server initiated} + */ + boolean isServerInitiated(); + + /** + * {@return whether this stream is bidirectional} + */ + boolean isBidirectional(); + + /** + * {@return true if this stream is local initiated} + */ + boolean isLocalInitiated(); + + /** + * {@return true if this stream is remote initiated} + */ + boolean isRemoteInitiated(); + + /** + * The type of this stream, as an int. This is a number between + * 0 and 3 inclusive, and corresponds to the last two lowest bits + * of the stream ID. + *

          + *
        • 0x00: bidirectional, client initiated
        • + *
        • 0x01: bidirectional, server initiated
        • + *
        • 0x02: unidirectional, client initiated
        • + *
        • 0x03: unidirectional, server initiated
        • + *
        + * @return the type of this stream, as an int + */ + int type(); + + /** + * {@return the combined stream state} + * + * @apiNote + * This is mostly used for logging purposes, to log the + * combined state of a stream. + */ + StreamState state(); + + /** + * {@return true if the stream has errors} + * For a {@linkplain QuicBidiStream bidirectional} stream, + * this method returns true if either its sending part or + * its receiving part was closed with a non-zero error code. + */ + boolean hasError(); + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java new file mode 100644 index 00000000000..f38a12d3d7c --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamReader.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.streams.QuicReceiverStream.ReceivingStreamState; + +/** + * An abstract class to model a reader plugged into + * a QuicStream from which data can be read + */ +public abstract class QuicStreamReader { + + /** + * A sentinel inserted into the queue after the FIN it has been received. + */ + public static final ByteBuffer EOF = ByteBuffer.wrap(new byte[0]).asReadOnlyBuffer(); + + // The scheduler to invoke when data becomes + // available. + final SequentialScheduler scheduler; + + /** + * Creates a new instance of a QuicStreamReader. + * The given scheduler will not be invoked until the reader + * is {@linkplain #start() started}. + * + * @param scheduler A sequential scheduler that will + * poll data out of this reader. + */ + public QuicStreamReader(SequentialScheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * {@return the receiving state of the stream} + * + * @apiNote + * This method returns the state of the {@link QuicReceiverStream} + * to which this writer is {@linkplain + * QuicReceiverStream#connectReader(SequentialScheduler) connected}. + * + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + * + */ + public abstract ReceivingStreamState receivingState(); + + /** + * {@return the ByteBuffer at the head of the queue, + * or null if no data is available}. If the end of the stream is + * reached then {@link #EOF} is returned. + * + * @implSpec + * This method behave just like {@link Queue#poll()}. + * + * @throws IOException if the stream was closed locally or + * reset by the peer + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + */ + public abstract ByteBuffer poll() throws IOException; + + /** + * {@return the ByteBuffer at the head of the queue, + * or null if no data is available} + * + * @implSpec + * This method behave just like {@link Queue#peek()}. + * + * @throws IOException if the stream was reset by the peer + * @throws IllegalStateException if this reader is {@linkplain + * QuicReceiverStream#disconnectReader(QuicStreamReader) no longer connected} + * to its stream + */ + public abstract ByteBuffer peek() throws IOException; + + /** + * {@return the stream this reader is connected to, or {@code null} + * if this reader is not currently {@linkplain #connected() connected}} + */ + public abstract QuicReceiverStream stream(); + + + /** + * {@return true if this reader is connected to its stream} + * @see QuicReceiverStream#connectReader(SequentialScheduler) + * @see QuicReceiverStream#disconnectReader(QuicStreamReader) + */ + public abstract boolean connected(); + + /** + * {@return true if this reader has been {@linkplain #start() started}} + */ + public abstract boolean started(); + + /** + * Starts the reader. The {@linkplain + * QuicReceiverStream#connectReader(SequentialScheduler) scheduler} + * will not be invoked until the reader is {@linkplain #start() started}. + */ + public abstract void start(); + + /** + * {@return whether reset was received or read by this reader} + */ + public boolean isReset() { + return stream().receivingState().isReset(); + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java new file mode 100644 index 00000000000..ef1de558e10 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreamWriter.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; + +/** + * An abstract class to model a writer plugged into + * a QuicStream to which data can be written. The data + * is wrapped in {@link StreamFrame} + * before being written. + */ +public abstract class QuicStreamWriter { + + // The scheduler to invoke when flow credit + // become available. + final SequentialScheduler scheduler; + + /** + * Creates a new instance of a QuicStreamWriter. + * @param scheduler A sequential scheduler that will + * push data into this writer. + */ + public QuicStreamWriter(SequentialScheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * {@return the sending state of the stream} + * + * @apiNote + * This method returns the state of the {@link QuicSenderStream} + * to which this writer is {@linkplain + * QuicSenderStream#connectWriter(SequentialScheduler) connected}. + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract SendingStreamState sendingState(); + + /** + * Pushes a ByteBuffer to be scheduled for writing on the stream. + * The ByteBuffer will be wrapped in a StreamFrame before being + * sent. Data that cannot be sent due to a lack of flow + * credit will be buffered. + * + * @param buffer A byte buffer to schedule for writing + * @param last Whether that's the last data that will be sent + * through this stream. + * + * @throws IOException if the state of the stream isn't + * {@link SendingStreamState#READY} or {@link SendingStreamState#SEND} + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void scheduleForWriting(ByteBuffer buffer, boolean last) + throws IOException; + + /** + * Queues a {@code ByteBuffer} on the writing queue for this stream. + * The consumer will not be woken up. More data should be submitted + * using {@link #scheduleForWriting(ByteBuffer, boolean)} in order + * to wake the consumer. + * + * @apiNote + * Use this method as a hint that more data will be + * upcoming shortly that might be aggregated with + * the data being queued in order to reduce the number + * of packets that will be sent to the peer. + * This is useful when a small number of bytes + * need to be written to the stream before actual stream + * data. Typically, this can be used for writing the + * HTTP/3 stream type for a unidirectional HTTP/3 stream + * before starting to send stream data. + * + * @param buffer A byte buffer to schedule for writing + * + * @throws IOException if the state of the stream isn't + * {@link SendingStreamState#READY} or {@link SendingStreamState#SEND} + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void queueForWriting(ByteBuffer buffer) + throws IOException; + + /** + * Indicates how many bytes the writer is + * prepared to received for sending. + * When that value grows from 0, and if the queue has + * no pending data, the {@code scheduler} + * is triggered to elicit more calls to + * {@link #scheduleForWriting(ByteBuffer,boolean)}. + * + * @apiNote This information is used to avoid + * buffering too much data while waiting for flow + * credit on the underlying stream. When flow credit + * is available, the {@code scheduler} loop is + * invoked to resume writing. The scheduler can then + * call this method to figure out how much data to + * request from upstream. + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract long credit(); + + /** + * Abruptly resets the stream. + * + * @param errorCode the application error code + * + * @throws IllegalStateException if this writer is {@linkplain + * QuicSenderStream#disconnectWriter(QuicStreamWriter) no longer connected} + * to its stream + */ + public abstract void reset(long errorCode) throws IOException; + + /** + * {@return the stream this writer is connected to, or {@code null} + * if this writer isn't currently {@linkplain #connected() connected}} + */ + public abstract QuicSenderStream stream(); + + /** + * {@return true if this writer is connected to its stream} + * @see QuicSenderStream#connectWriter(SequentialScheduler) + * @see QuicSenderStream#disconnectWriter(QuicStreamWriter) + */ + public abstract boolean connected(); + + /** + * {@return true if STOP_SENDING was received} + */ + public boolean stopSendingReceived() { + return connected() ? stream().stopSendingReceived() : false; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java new file mode 100644 index 00000000000..8706cdcf887 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/QuicStreams.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import jdk.internal.net.http.quic.QuicConnectionImpl; + +/** + * A collection of utilities methods to analyze and work with + * quic streams. + */ +public final class QuicStreams { + private QuicStreams() { throw new InternalError("should not come here"); } + + public static final int TYPE_MASK = 0x03; + public static final int UNI_MASK = 0x02; + public static final int SRV_MASK = 0x01; + + public static int streamType(long streamId) { + return (int) streamId & TYPE_MASK; + } + + public static boolean isBidirectional(long streamId) { + return ((int) streamId & UNI_MASK) == 0; + } + + public static boolean isUnidirectional(long streamId) { + return ((int) streamId & UNI_MASK) == UNI_MASK; + } + + public static boolean isBidirectional(int streamType) { + return (streamType & UNI_MASK) == 0; + } + + public static boolean isUnidirectional(int streamType) { + return (streamType & UNI_MASK) == UNI_MASK; + } + + public static boolean isClientInitiated(long streamId) { + return ((int) streamId & SRV_MASK) == 0; + } + + public static boolean isServerInitiated(long streamId) { + return ((int) streamId & SRV_MASK) == SRV_MASK; + } + + public static boolean isClientInitiated(int streamType) { + return (streamType & SRV_MASK) == 0; + } + + public static boolean isServerInitiated(int streamType) { + return (streamType & SRV_MASK) == SRV_MASK; + } + + public static AbstractQuicStream createStream(QuicConnectionImpl connection, long streamId) { + int type = streamType(streamId); + boolean isClient = connection.isClientConnection(); + return switch (type) { + case 0x00, 0x01 -> new QuicBidiStreamImpl(connection, streamId); + case 0x02 -> isClient ? new QuicSenderStreamImpl(connection, streamId) + : new QuicReceiverStreamImpl(connection, streamId); + case 0x03 -> isClient ? new QuicReceiverStreamImpl(connection, streamId) + : new QuicSenderStreamImpl(connection, streamId); + default -> throw new IllegalArgumentException("bad stream type %s for stream %s" + .formatted(type, streamId)); + }; + } + +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java new file mode 100644 index 00000000000..5495681b12a --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamCreationPermit.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.AbstractQueuedLongSynchronizer; +import java.util.function.Function; + +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; + +/** + * Quic specifies limits on the number of uni and bidi streams that an endpoint can create. + * This {@code StreamCreationPermit} is used to keep track of that limit and is expected to be + * used before attempting to open a Quic stream. Either of {@link #tryAcquire()} or + * {@link #tryAcquire(long, TimeUnit, Executor)} must be used before attempting to open a new stream. Stream + * must only be opened if that method returns {@code true} which implies the stream creation limit + * hasn't yet reached. + *

        + * It is expected that for each of the stream types (remote uni, remote bidi, local uni + * and local bidi) a separate instance of {@code StreamCreationPermit} will be used. + *

        + * An instance of {@code StreamCreationPermit} starts with an initial limit and that limit can be + * increased to newer higher values whenever necessary. The limit however cannot be reduced to a + * lower value. + *

        + * None of the methods, including {@link #tryAcquire(long, TimeUnit, Executor)} and {@link #tryAcquire()} + * block the caller thread. + */ +final class StreamCreationPermit { + + private final InternalSemaphore semaphore; + private final SequentialScheduler permitAcquisitionScheduler = + SequentialScheduler.lockingScheduler(new TryAcquireTask()); + + private final ConcurrentLinkedQueue acquirers = new ConcurrentLinkedQueue<>(); + + /** + * @param initialMaxStreams the initial max streams limit + * @throws IllegalArgumentException if {@code initialMaxStreams} is less than 0 + * @throws NullPointerException if executor is null + */ + StreamCreationPermit(final long initialMaxStreams) { + if (initialMaxStreams < 0) { + throw new IllegalArgumentException("Invalid max streams limit: " + initialMaxStreams); + } + this.semaphore = new InternalSemaphore(initialMaxStreams); + } + + /** + * Attempts to increase the limit to {@code newLimit}. The limit will be atomically increased + * to the {@code newLimit}. If the {@linkplain #currentLimit() current limit} is higher than + * the {@code newLimit}, then the limit isn't changed and this method returns {@code false}. + * + * @param newLimit the new limit + * @return true if the limit was successfully increased to {@code newLimit}, false otherwise. + */ + boolean tryIncreaseLimitTo(final long newLimit) { + final boolean increased = this.semaphore.tryIncreaseLimitTo(newLimit); + if (increased) { + // let any waiting acquirers attempt acquiring a permit + permitAcquisitionScheduler.runOrSchedule(); + } + return increased; + } + + /** + * Attempts to acquire a permit to open a new stream. This method does not block and returns + * immediately. A stream should only be opened if the permit was successfully acquired. + * + * @return true if the permit was acquired and a new stream is allowed to be opened. + * false otherwise. + */ + boolean tryAcquire() { + return this.semaphore.tryAcquireShared(1) >= 0; + } + + /** + * Attempts to acquire a permit to open a new stream. If the permit is available then this method + * returns immediately with a {@link CompletableFuture} whose result is {@code true}. If the + * permit isn't currently available then this method returns a {@code CompletableFuture} which + * completes with a result of {@code false} if no permits were available for the duration + * represented by the {@code timeout}. If during this {@code timeout} period, a permit is + * acquired, because of an increase in the stream limit, then the returned + * {@code CompletableFuture} completes with a result of {@code true}. + * + * @param timeout the maximum amount of time to attempt acquiring a permit, after which the + * {@code CompletableFuture} will complete with a result of {@code false} + * @param unit the timeout unit + * @param executor the executor that will be used to asynchronously complete the + * returned {@code CompletableFuture} if a permit is acquired after this + * method has returned + * @return a {@code CompletableFuture} whose result will be {@code true} if the permit was + * acquired and {@code false} otherwise + * @throws IllegalArgumentException if {@code timeout} is negative + * @throws NullPointerException if the {@code executor} is null + */ + CompletableFuture tryAcquire(final long timeout, final TimeUnit unit, + final Executor executor) { + Objects.requireNonNull(executor); + if (timeout < 0) { + throw new IllegalArgumentException("invalid timeout: " + timeout); + } + if (tryAcquire()) { + return MinimalFuture.completedFuture(true); + } + final CompletableFuture future = new MinimalFuture() + .orTimeout(timeout, unit) + .handle((acquired, t) -> { + if (t instanceof TimeoutException te) { + // timed out + return MinimalFuture.completedFuture(false); + } + if (t == null) { + // completed normally + return MinimalFuture.completedFuture(acquired); + } + return MinimalFuture.failedFuture(t); + }).thenComposeAsync(Function.identity(), executor); + var waiter = new Waiter(future, executor); + this.acquirers.add(waiter); + // if the future completes in timeout the Waiter should be removed from the list. + // because this is a queue it might not be too efficient... + future.whenComplete((r,t) -> { if (r != null && !r) acquirers.remove(waiter);}); + // if stream limit might have increased in the meantime, + // trigger the task to have this newly registered waiter notified + // TODO: should we call runOrSchedule(executor) here instead? + permitAcquisitionScheduler.runOrSchedule(); + return future; + } + + /** + * {@return the current limit for stream creation} + */ + long currentLimit() { + return this.semaphore.currentLimit(); + } + + private final record Waiter(CompletableFuture acquirer, + Executor executor) { + Waiter { + assert acquirer != null : "Acquirer cannot be null"; + assert executor != null : "Executor cannot be null"; + } + } + + /** + * A task which iterates over the waiting acquirers and attempt + * to acquire a permit. If successful, the waiting acquirer(s) (i.e. the CompletableFuture(s)) + * are completed successfully. If not, the waiting acquirers continue to stay in the wait list + */ + private final class TryAcquireTask implements Runnable { + + @Override + public void run() { + Waiter waiter = null; + while ((waiter = acquirers.peek()) != null) { + final CompletableFuture acquirer = waiter.acquirer; + if (acquirer.isCancelled() || acquirer.isDone()) { + // no longer interested, or already completed, remove it + acquirers.remove(waiter); + continue; + } + if (!tryAcquire()) { + // limit reached, no permits available yet + break; + } + // compose a step which rolls back the acquired permit if the + // CompletableFuture completed in some other thread, after the permit was acquired. + acquirer.whenComplete((acquired, t) -> { + final boolean shouldRollback = acquirer.isCancelled() + || t != null + || !acquired; + if (shouldRollback) { + final boolean released = StreamCreationPermit.this.semaphore.releaseShared(1); + assert released : "acquired permit wasn't released"; + // an additional permit is now available due to the release, let any waiters + // acquire it if needed + permitAcquisitionScheduler.runOrSchedule(); + } + }); + // got a permit, complete the waiting acquirer + acquirers.remove(waiter); + acquirer.completeAsync(() -> true, waiter.executor); + } + } + } + + /** + * A {@link AbstractQueuedLongSynchronizer} whose {@linkplain #getState() state} represents + * the number of permits that have currently been acquired. This {@code Semaphore} only + * supports "shared" mode; i.e. exclusive mode isn't supported. + *

        + * The {@code Semaphore} maintains a {@linkplain #limit limit} which represents + * the maximum number of permits that can be acquired through an instance of this class. + * The {@code limit} can be {@linkplain #tryIncreaseLimitTo(long) increased} but cannot be + * reduced from the previous set limit. + */ + private static final class InternalSemaphore extends AbstractQueuedLongSynchronizer { + private static final long serialVersionUID = 4280985311770761500L; + + private final AtomicLong limit; + + /** + * @param initialLimit the initial limit, must be >=0 + */ + private InternalSemaphore(final long initialLimit) { + assert initialLimit >= 0 : "not a positive initial limit: " + initialLimit; + this.limit = new AtomicLong(initialLimit); + setState(0 /* num acquired */); + } + + /** + * Attempts to acquire additional permits. If no permits can be acquired, + * then this method returns -1. Upon successfully acquiring the + * {@code additionalAcquisitions} this method returns a value {@code >=0} which represents + * the additional number of permits that are available for acquisition. + * + * @param additionalAcquisitions the additional permits that are requested + * @return -1 If no permits can be acquired. Value >=0, representing the permits that are + * still available for acquisition. + */ + @Override + protected long tryAcquireShared(final long additionalAcquisitions) { + while (true) { + final long alreadyAcquired = getState(); + final long totalOnAcquisition = alreadyAcquired + additionalAcquisitions; + final long currentLimit = limit.get(); + if (totalOnAcquisition > currentLimit) { + return -1; // exceeds limit, so cannot acquire + } + final long numAvailableUponAcquisition = currentLimit - totalOnAcquisition; + if (compareAndSetState(alreadyAcquired, totalOnAcquisition)) { + return numAvailableUponAcquisition; + } + } + } + + /** + * Attempts to release permits + * + * @param releases the number of permits to release + * @return true if the permits were released, false otherwise + * @throws IllegalArgumentException if the number of {@code releases} exceeds the total + * number of permits that have been acquired + */ + @Override + protected boolean tryReleaseShared(final long releases) { + while (true) { + final long currentAcquisitions = getState(); + final long totalAfterRelease = currentAcquisitions - releases; + if (totalAfterRelease < 0) { + // we attempted to release more permits than what was acquired + throw new IllegalArgumentException("cannot release " + releases + + " permits from " + currentAcquisitions + " acquisitions"); + } + if (compareAndSetState(currentAcquisitions, totalAfterRelease)) { + return true; + } + } + } + + /** + * Tries to increase the limit to the {@code newLimit}. If the {@code newLimit} is lesser + * than the current limit, then this method returns false. Otherwise, this method will attempt + * to atomically increase the limit to {@code newLimit}. + * + * @param newLimit The new limit to set + * @return true if the limit was increased to {@code newLimit}. false otherwise + */ + private boolean tryIncreaseLimitTo(final long newLimit) { + long currentLimit = this.limit.get(); + while (currentLimit < newLimit) { + if (this.limit.compareAndSet(currentLimit, newLimit)) { + return true; + } + currentLimit = this.limit.get(); + } + return false; + } + + /** + * {@return the current limit} + */ + private long currentLimit() { + return this.limit.get(); + } + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java new file mode 100644 index 00000000000..ebf205479f6 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/streams/StreamWriterQueue.java @@ -0,0 +1,550 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.quic.streams; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; + +/** + * A class to handle the writing queue of a {@link QuicSenderStream}. + * This class maintains a queue of byte buffer containing stream data + * that has not yet been packaged for sending. It also keeps track of + * the max stream data value. + * It acts as a mailbox between a producer (typically a {@link QuicStreamWriter}), + * and a consumer (typically a {@link jdk.internal.net.http.quic.QuicConnectionImpl}). + * This class is abstract: a concrete implementation of this class must only + * implement {@link #wakeupProducer()} and {@link #wakeupConsumer()} which should + * wake up the producer and consumer respectively, when data can be polled or + * submitted from the queue. + */ +abstract class StreamWriterQueue { + /** + * The amount of data that a StreamWriterQueue is willing to buffer. + * The queue will buffer excess data, but will not wake up the producer + * until the excess is consumed. + */ + private static final int BUFFER_SIZE = + Utils.getIntegerProperty("jdk.httpclient.quic.streamBufferSize", 1 << 16); + + // The current buffer containing data to send. + private ByteBuffer current; + // The offset of the data that has been consumed + private volatile long bytesConsumed; + // The offset of the data that has been supplied by the + // producer. + // bytesProduced >= bytesConsumed at all times. + private volatile long bytesProduced; + // The stream size, when known, -1 otherwise. + // The stream size may be known at the creation of the stream, + // or at the latest when the last ByteBuffer is provided by + // the producer. + private volatile long streamSize = -1; + // true if reset was requested, false otherwise + private volatile boolean resetRequested; + // The maximum offset that will be accepted by the peer at this + // time. bytesConsumed <= maxStreamData at all times. + private volatile long maxStreamData; + // negative if stop sending was received; contains -(errorCode + 1) + private volatile long stopSending; + // The queue to buffer data before it's polled by the consumer + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + private final ReentrantLock lock = new ReentrantLock(); + + protected final void lock() { + lock.lock(); + } + + protected final void unlock() { + lock.unlock(); + } + + protected abstract Logger debug(); + + /** + * This method is called by the consumer to poll data from the stream + * queue. This method will return a {@code ByteBuffer} with at most + * {@code maxbytes} remaining bytes. The {@code ByteBuffer} may contain + * less bytes if not enough bytes are available, or if there is not + * enough {@linkplain #consumerCredit() credit} to send {@code maxbytes} + * to the peer. Only stream credit is taken into account. Taking into + * account connection credit is the responsibility of the caller. + * If there is no credit, or if there is no data available, {@code null} + * is returned. When credit and data are available again, {@link #wakeupConsumer()} + * is called to wake up the consumer. + * + * @apiNote + * This method increases the consumer offset. It must not be called concurrently + * by two different threads. + * + * @implNote + * If the producer was blocked due to full buffer before this method was called + * and the method removes enough of buffered data, + * {@link #wakeupProducer()} is called. + * + * @param maxbytes the maximum number of bytes the consumer is prepared + * to consume. + * @return a {@code ByteBuffer} containing at most {@code maxbytes}, or {@code null} + * if no data is available or data is blocked by flow control. + */ + public final ByteBuffer poll(int maxbytes) { + boolean producerWasBlocked, producerUnblocked; + long produced, consumed; + ByteBuffer buffer; + long credit = consumerCredit(); + assert credit >= 0 : credit; + if (credit < maxbytes) { + maxbytes = (int)credit; + } + if (maxbytes <= 0) return null; + lock(); + try { + producerWasBlocked = producerBlocked(); + buffer = current; + if (buffer == null) { + buffer = current = queue.poll(); + } + if (buffer == null) { + return null; + } + int remaining = buffer.remaining(); + int position = buffer.position(); + consumed = bytesConsumed; + if (remaining <= maxbytes) { + current = queue.poll(); + bytesConsumed = consumed = Math.addExact(consumed, remaining); + } else { + buffer = buffer.slice(position, maxbytes); + current.position(position + maxbytes); + bytesConsumed = consumed = Math.addExact(consumed, maxbytes); + } + long size = streamSize; + produced = bytesProduced; + producerUnblocked = producerWasBlocked && !producerBlocked(); + if (StreamWriterQueue.class.desiredAssertionStatus()) { + assert consumed <= produced + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + assert size == -1 || consumed <= size + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + assert size == -1 || produced <= size + : "consumed: " + consumed + ", produced: " + produced + ", size: " + size; + } + if (size >= 0 && consumed == size) { + switchState(SendingStreamState.DATA_SENT); + } + } finally { + unlock(); + } + if (producerUnblocked) { + debug().log("producer unblocked produced:%s, consumed:%s", + produced, consumed); + wakeupProducer(); + } + return buffer; + } + + /** + * Updates the flow control credit for this queue. + * The maximum offset that will be accepted by the consumer + * can only increase. Value that are less or equal to the + * current value of the max stream data are ignored. + * + * @implSpec + * If the consumer was blocked due to flow control before + * this method was called, and the new value of the max + * stream data allows to unblock the consumer, and data + * is available, {@link #wakeupConsumer()} is called. + * + * @param data the maximum offset that will be accepted by + * the consumer + * @return the maximum offset that will be accepted by the + * consumer. + */ + public final long setMaxStreamData(long data) { + assert data >= 0 : "maxStreamData: " + data; + long max, produced, consumed; + boolean consumerWasBlocked, consumerUnblocked; + lock(); + try { + max = maxStreamData; + consumed = bytesConsumed; + produced = bytesProduced; + consumerWasBlocked = consumerBlocked(); + if (data <= max) return max; + maxStreamData = max = data; + consumerUnblocked = consumerWasBlocked && !consumerBlocked(); + if (StreamWriterQueue.class.desiredAssertionStatus()) { + long size = streamSize; + assert consumed <= produced; + assert size == -1 || consumed <= size; + assert size == -1 || produced <= size; + } + } finally { + unlock(); + } + debug().log("set max stream data: %s", max); + if (consumerUnblocked && produced > 0) { + debug().log("consumer unblocked produced:%s, consumed:%s, max stream data:%s", + produced, consumed, max); + wakeupConsumer(); + } + return max; + } + + /** + * Whether the producer is blocked due to flow control. + * + * @return whether the producer is blocked due to full buffers + */ + public final boolean producerBlocked() { + return producerCredit() <= 0; + } + + /** + * Whether the consumer is blocked due to flow control. + * + * @return whether the producer is blocked due to flow control + */ + public final boolean consumerBlocked() { + return consumerCredit() <= 0; + } + + /** + * {@return the offset of the data consumed by the consumer} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #poll(int)} is called concurrently + * by another thread. + */ + public final long bytesConsumed() { + return bytesConsumed; + } + + /** + * {@return the offset of the data provided by the producer} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #queue(ByteBuffer)} are called concurrently + * by another thread. + */ + public final long bytesProduced() { + return bytesProduced; + } + + /** + * {@return the amount of produced data which has not been consumed yet} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #queue(ByteBuffer)} or + * {@link #poll(int)} are called concurrently + * by another thread. + */ + public final long available() { + return bytesProduced - bytesConsumed; + } + + /** + * {@return the stream size if known, {@code -1} otherwise} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * is called concurrently by another thread. + */ + public final long streamSize() { + return streamSize; + } + + /** + * {@return the maximum offset that the peer is prepared to accept} + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #setMaxStreamData(long)} is called + * concurrently by another thread. + */ + public final long maxStreamData() { + return maxStreamData; + } + + /** + * {@return {@code true} if the consumer has reached the end of + * this stream (equivalent to EOF)} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * or {@link #poll(int)} are called concurrently + * by another thread. + */ + public final boolean isConsumerDone() { + long size = streamSize; + long consumed = bytesConsumed; + assert size == -1 || size >= consumed; + return size >= 0 && size <= consumed; + } + + /** + * {@return {@code true} if the producer has reached the end of + * this stream (equivalent to EOF)} + * This is independent of flow control. + * + * @apiNote + * The returned value is only weakly consistent: it is subject + * to race conditions if {@link #submit(ByteBuffer, boolean)} + * is called concurrently by another thread. + */ + public final boolean isProducerDone() { + return streamSize >= 0; + } + + /** + * This method is called by the producer to submit data to this + * stream. The producer should not modify the provided buffer + * after this point. The provided buffer will be queued even if + * the produced data exceeds the maximum offset that the peer + * is prepared to accept. + * + * @apiNote + * If sufficient credit is available, this method will wake + * up the consumer. + * + * @param buffer a buffer containing data for the stream + * @param last whether this is the last buffer that will ever be + * provided by the provided + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + public final void submit(ByteBuffer buffer, boolean last) throws IOException { + offer(buffer, last, false); + } + + /** + * This method is called by the producer to queue data to this + * stream. The producer should not modify the provided buffer + * after this point. The provided buffer will be queued even if + * the produced data exceeds the maximum offset that the peer + * is prepared to accept. + * + * @apiNote + * The consumer will not be woken, even if enough credit is + * available. More data should be submitted using + * {@link #submit(ByteBuffer, boolean)} in order to wake up the consumer. + * + * @param buffer a buffer containing data for the stream + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + public final void queue(ByteBuffer buffer) throws IOException { + offer(buffer, false, true); + } + + /** + * Queues a buffer in the writing queue. + * + * @param buffer the buffer to queue + * @param last whether this is the last data for the stream + * @param waitForMore whether we should wait for the next submission before + * waking up the consumer + * @throws IOException if the stream was reset by peer + * @throws IllegalStateException if the last data was submitted already + */ + private void offer(ByteBuffer buffer, boolean last, boolean waitForMore) + throws IOException { + long length = buffer.remaining(); + long consumed, produced, max; + boolean wakeupConsumer; + lock(); + try { + long stopSending = this.stopSending; + if (stopSending < 0) { + throw new IOException("Stream %s reset by peer: errorCode %s" + .formatted(streamId(), 1 - stopSending)); + } + if (resetRequested) return; + if (streamSize >= 0) { + throw new IllegalStateException("Too many bytes provided"); + } + consumed = bytesConsumed; + max = maxStreamData; + produced = Math.addExact(bytesProduced, length); + bytesProduced = produced; + if (length > 0 || last) { + // allow to queue a zero-length buffer if it's the last. + queue.offer(buffer); + } + if (last) { + streamSize = produced; + } + assert consumed <= produced; + wakeupConsumer = consumed < max && consumed < produced + || consumed == produced && last; + } finally { + unlock(); + } + if (wakeupConsumer && !waitForMore) { + debug().log("consumer unblocked produced:%s, consumed:%s, max stream data:%s", + produced, consumed, max); + wakeupConsumer(); + } + } + + /** + * {@return the credit of the producer} + * @implSpec + * this is the desired buffer size minus the amount of data already buffered. + */ + public final long producerCredit() { + lock(); + try { + return BUFFER_SIZE - available(); + } finally { + unlock(); + } + } + + /** + * {@return the credit of the consumer} + * @implSpec + * This is equivalent to {@link #maxStreamData()} - {@link #bytesConsumed()}. + */ + public final long consumerCredit() { + lock(); + try { + return maxStreamData - bytesConsumed; + } finally { + unlock(); + } + } + + /** + * {@return the amount of available data that can be sent + * with respect to flow control in this stream}. + * This does not take into account the global connection + * flow control. + */ + public final long readyToSend() { + long consumed, produced, max; + lock(); + try { + consumed = bytesConsumed; + max = maxStreamData; + produced = bytesProduced; + } finally { + unlock(); + } + assert max >= consumed; + assert produced >= consumed; + return Math.min(max - consumed, produced - consumed); + } + + public final void markReset() { + lock(); + try { + resetRequested = true; + } finally { + unlock(); + } + } + + final void close() { + lock(); + try { + bytesProduced = bytesConsumed; + queue.clear(); + current = null; + } finally { + unlock(); + } + } + + /** + * Called when a stop sending frame is received for this stream + * @param errorCode the error code + */ + protected final boolean stopSending(long errorCode) { + long stopSending; + lock(); + try { + if (resetRequested) return false; + if (streamSize >= 0 && bytesConsumed == streamSize) return false; + if ((stopSending = this.stopSending) < 0) return false; + this.stopSending = stopSending = - (errorCode + 1); + } finally { + unlock(); + } + assert stopSending < 0 && stopSending == - (errorCode + 1); + return true; + } + + /** + * {@return -1 minus the error code that was supplied by the peer + * when requesting for stop sending} + * @apiNote a strictly negative value indicates that the stream was + * reset by the peer. The error code supplied by the peer + * can be obtained with the formula:

        {@code
        +     *    long errorCode = - (resetByPeer() + 1);
        +     *    }
        + */ + final long resetByPeer() { + return stopSending; + } + + /** + * This method is called to wake up the consumer when there is + * credit and data available for the consumer. + */ + protected abstract void wakeupConsumer(); + + /** + * This method is called to wake up the producer when there is + * credit available for the producer. + */ + protected abstract void wakeupProducer(); + + /** + * Called to switch the sending state when data has been sent. + * @param dataSent the new state - typically {@link SendingStreamState#DATA_SENT} + */ + protected abstract void switchState(SendingStreamState dataSent); + + /** + * {@return the stream id this queue was created for} + */ + protected abstract long streamId(); + +} diff --git a/src/java.net.http/share/classes/module-info.java b/src/java.net.http/share/classes/module-info.java index 14cbb85291d..392385136b0 100644 --- a/src/java.net.http/share/classes/module-info.java +++ b/src/java.net.http/share/classes/module-info.java @@ -75,6 +75,18 @@ *
      • {@systemProperty jdk.httpclient.hpack.maxheadertablesize} (default: 16384 or * 16 kB)
        The HTTP/2 client maximum HPACK header table size in bytes. *

      • + *
      • {@systemProperty jdk.httpclient.qpack.decoderMaxTableCapacity} (default: 0) + *
        The HTTP/3 client maximum QPACK decoder dynamic header table size in bytes. + *
        Setting this value to a positive number will allow HTTP/3 servers to add entries + * to the QPack decoder's dynamic table. When set to 0, servers are not permitted to add + * entries to the client's QPack encoder's dynamic table. + *

      • + *
      • {@systemProperty jdk.httpclient.qpack.encoderTableCapacityLimit} (default: 4096, + * or 4 kB) + *
        The HTTP/3 client maximum QPACK encoder dynamic header table size in bytes. + *
        Setting this value to a positive number allows the HTTP/3 client's QPack encoder to + * add entries to the server's QPack decoder's dynamic table, if the server permits it. + *

      • *
      • {@systemProperty jdk.httpclient.HttpClient.log} (default: none)
        * Enables high-level logging of various events through the {@linkplain java.lang.System.Logger * Platform Logging API}. The value contains a comma-separated list of any of the @@ -88,6 +100,8 @@ *

      • ssl
      • *
      • trace
      • *
      • channel
      • + *
      • http3
      • + *
      • quic
      • *
        * You can append the frames item with a colon-separated list of any of the following items: *
          @@ -96,31 +110,57 @@ *
        • window
        • *
        • all
        • *

        + * You can append the quic item with a colon-separated list of any of the following items; + * packets are logged in an abridged form that only shows frames offset and length, + * but not content: + *
          + *
        • ack: packets containing ack frames will be logged
        • + *
        • cc: information on congestion control will be logged
        • + *
        • control: packets containing quic controls (such as frames affecting + * flow control, or frames opening or closing streams) + * will be logged
        • + *
        • crypto: packets containing crypto frames will be logged
        • + *
        • data: packets containing stream frames will be logged
        • + *
        • dbb: information on direct byte buffer usage will be logged
        • + *
        • ping: packets containing ping frames will be logged
        • + *
        • processed: information on flow control (processed bytes) will be logged
        • + *
        • retransmit: information on packet loss and recovery will be logged
        • + *
        • timer: information on send task scheduling will be logged
        • + *
        • all
        • + *

        * Specifying an item adds it to the HTTP client's log. For example, if you specify the * following value, then the Platform Logging API logs all possible HTTP Client events:
        * "errors,requests,headers,frames:control:data:window,ssl,trace,channel"
        * Note that you can replace control:data:window with all. The name of the logger is * "jdk.httpclient.HttpClient", and all logging is at level INFO. + * To debug issues with the quic protocol a good starting point is to specify + * {@code quic:control:retransmit}. * *
      • {@systemProperty jdk.httpclient.keepalive.timeout} (default: 30)
        - * The number of seconds to keep idle HTTP connections alive in the keep alive cache. This - * property applies to both HTTP/1.1 and HTTP/2. The value for HTTP/2 can be overridden - * with the {@code jdk.httpclient.keepalive.timeout.h2 property}. + * The number of seconds to keep idle HTTP connections alive in the keep alive cache. + * By default this property applies to HTTP/1.1, HTTP/2 and HTTP/3. + * The value for HTTP/2 and HTTP/3 can be overridden with the + * {@code jdk.httpclient.keepalive.timeout.h2} and {@code jdk.httpclient.keepalive.timeout.h3} + * properties respectively. The value specified for HTTP/2 acts as default value for HTTP/3. *

      • *
      • {@systemProperty jdk.httpclient.keepalive.timeout.h2} (default: see * below)
        The number of seconds to keep idle HTTP/2 connections alive. If not set, then the * {@code jdk.httpclient.keepalive.timeout} setting is used. *

      • + *
      • {@systemProperty jdk.httpclient.keepalive.timeout.h3} (default: see + * below)
        The number of seconds to keep idle HTTP/3 connections alive. If not set, then the + * {@code jdk.httpclient.keepalive.timeout.h2} setting is used. + *

      • *
      • {@systemProperty jdk.httpclient.maxframesize} (default: 16384 or 16kB)
        * The HTTP/2 client maximum frame size in bytes. The server is not permitted to send a frame * larger than this. *

      • *
      • {@systemProperty jdk.httpclient.maxLiteralWithIndexing} (default: 512)
        * The maximum number of header field lines (header name and value pairs) that a - * client is willing to add to the HPack Decoder dynamic table during the decoding + * client is willing to add to the HPack or QPACK Decoder dynamic table during the decoding * of an entire header field section. * This is purely an implementation limit. - * If a peer sends a field section with encoding that + * If a peer sends a field section or a set of QPACK instructions with encoding that * exceeds this limit a {@link java.net.ProtocolException ProtocolException} will be raised. * A value of zero or a negative value means no limit. *

      • @@ -135,7 +175,7 @@ * A value of zero or a negative value means no limit. * *
      • {@systemProperty jdk.httpclient.maxstreams} (default: 100)
        - * The maximum number of HTTP/2 push streams that the client will permit servers to open + * The maximum number of HTTP/2 or HTTP/3 push streams that the client will permit servers to open * simultaneously. *

      • *
      • {@systemProperty jdk.httpclient.receiveBufferSize} (default: operating system @@ -187,6 +227,61 @@ * value means no limit. *

      • * + *

        + * The following system properties can be used to configure some aspects of the + * QUIC Protocol + * implementation used for HTTP/3: + *

          + *
        • {@systemProperty jdk.httpclient.quic.receiveBufferSize} (default: operating system + * default)
          The QUIC {@linkplain java.nio.channels.DatagramChannel UDP client socket} + * {@linkplain java.net.StandardSocketOptions#SO_RCVBUF receive buffer size} in bytes. + * Values less than or equal to zero are ignored. + *

        • + *
        • {@systemProperty jdk.httpclient.quic.sendBufferSize} (default: operating system + * default)
          The QUIC {@linkplain java.nio.channels.DatagramChannel UDP client socket} + * {@linkplain java.net.StandardSocketOptions#SO_SNDBUF send buffer size} in bytes. + * Values less than or equal to zero are ignored. + *

        • + *
        • {@systemProperty jdk.httpclient.quic.defaultMTU} (default: 1200 bytes)
          + * The default Maximum Transmission Unit (MTU) size that will be used on quic connections. + * The default implementation of the HTTP/3 client does not implement Path MTU Detection, + * but will attempt to send 1-RTT packets up to the size defined by this property. + * Specifying a higher value may give better upload performance when the client and + * servers are located on the same machine, but is likely to result in irrecoverable + * packet loss if used over the network. Allowed values are in the range [1200, 65527]. + * If an out-of-range value is specified, the minimum default value will be used. + *

        • + *
        • {@systemProperty jdk.httpclient.quic.maxBytesInFlight} (default: + * 16777216 bytes or 16MB)
          + * This is the maximum number of unacknowledged bytes that the quic congestion + * controller allows to be in flight. When this amount is reached, no new + * data is sent until some of the packets in flight are acknowledged. + *
          + * Allowed values are in the range [2^14, 2^24] (or [16kB, 16MB]). + * If an out-of-range value is specified, it will be clamped to the closest + * value in range. + *

        • + *
        • {@systemProperty jdk.httpclient.quic.maxInitialData} (default: 15728640 + * bytes, or 15MB)
          + * The initial flow control limit for quic connections in bytes. Valid values are in + * the range [0, 2^60]. The initial limit is also used to initialize the receive window + * size. If less than 16kB, the window size will be set to 16kB. + *

        • + *
        • {@systemProperty jdk.httpclient.quic.maxStreamInitialData} (default: 6291456 + * bytes, or 6MB)
          + * The initial flow control limit for quic streams in bytes. Valid values are in + * the range [0, 2^60]. The initial limit is also used to initialize the receive window + * size. If less than 16kB, the window size will be set to 16kB. + *

        • + *
        • {@systemProperty jdk.httpclient.quic.maxInitialTimeout} (default: 30 + * seconds)
          + * This is the maximum time, in seconds, during which the client will wait for a + * response from the server, and continue retransmitting the first Quic INITIAL packet, + * before raising a {@link java.net.ConnectException}. The first INITIAL packet received + * from the target server will disarm this timeout. + *

        • + * + *
        * @moduleGraph * @since 11 */ diff --git a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java index a2c7a072769..9f42d610c4d 100644 --- a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java +++ b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11SecretKeyFactory.java @@ -285,6 +285,10 @@ final class P11SecretKeyFactory extends SecretKeyFactorySpi { putKeyInfo(new TLSKeyInfo("TlsServerAppTrafficSecret")); putKeyInfo(new TLSKeyInfo("TlsServerHandshakeTrafficSecret")); putKeyInfo(new TLSKeyInfo("TlsUpdateNplus1")); + // QUIC-specific + putKeyInfo(new TLSKeyInfo("TlsInitialSecret")); + putKeyInfo(new TLSKeyInfo("TlsClientInitialTrafficSecret")); + putKeyInfo(new TLSKeyInfo("TlsServerInitialTrafficSecret")); putKeyInfo(new KeyInfo("Generic", CKK_GENERIC_SECRET, CKM_GENERIC_SECRET_KEY_GEN)); diff --git a/test/jdk/com/sun/net/httpserver/SANTest.java b/test/jdk/com/sun/net/httpserver/SANTest.java index dc7f5e7bdd5..d35a590931d 100644 --- a/test/jdk/com/sun/net/httpserver/SANTest.java +++ b/test/jdk/com/sun/net/httpserver/SANTest.java @@ -36,6 +36,18 @@ * java.base/sun.net.www.http * java.base/sun.net.www * java.base/sun.net + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.base/jdk.internal.util * * @run main/othervm SANTest * @summary Update SimpleSSLContext keystore to use SANs for localhost IP addresses diff --git a/test/jdk/java/net/httpclient/AbstractNoBody.java b/test/jdk/java/net/httpclient/AbstractNoBody.java index 603fe16d216..9cc26704bb5 100644 --- a/test/jdk/java/net/httpclient/AbstractNoBody.java +++ b/test/jdk/java/net/httpclient/AbstractNoBody.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,9 +29,6 @@ import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.net.http.HttpClient; @@ -39,6 +36,7 @@ import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -49,6 +47,8 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; public abstract class AbstractNoBody implements HttpServerAdapters { @@ -58,6 +58,7 @@ public abstract class AbstractNoBody implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -66,12 +67,16 @@ public abstract class AbstractNoBody implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final String SIMPLE_STRING = "Hello world. Goodbye world"; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test static final ExecutorService executor = Executors.newFixedThreadPool(ITERATION_COUNT * 2); static final ExecutorService serverExecutor = Executors.newFixedThreadPool(ITERATION_COUNT * 4); + static final AtomicLong serverCount = new AtomicLong(); static final AtomicLong clientCount = new AtomicLong(); static final long start = System.nanoTime(); public static String now() { @@ -85,6 +90,11 @@ public abstract class AbstractNoBody implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][]{ + { http3URI_fixed, false,}, + { http3URI_chunk, false }, + { http3URI_fixed, true,}, + { http3URI_chunk, true }, + { httpURI_fixed, false }, { httpURI_chunk, false }, { httpsURI_fixed, false }, @@ -112,17 +122,39 @@ public abstract class AbstractNoBody implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } HttpRequest.Builder newRequestBuilder(String uri) { var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } return builder; } + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() .executor(executor) .proxy(NO_PROXY) .sslContext(sslContext) @@ -190,10 +222,22 @@ public abstract class AbstractNoBody implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/noBodyFixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/noBodyChunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new FixedLengthNoBodyHandler(); + HttpTestHandler h3_chunkedHandler = new ChunkedNoBodyHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/noBodyFixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/noBodyChunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/noBodyHead"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/noBodyFixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/noBodyChunk"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/noBodyHead"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); var shared = newHttpClient(true); @@ -201,9 +245,13 @@ public abstract class AbstractNoBody implements HttpServerAdapters { out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); - + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); out.println("Shared client is: " + shared); + headRequest(shared); + printStamp(END,"setup"); } @@ -215,6 +263,7 @@ public abstract class AbstractNoBody implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); executor.close(); serverExecutor.close(); printStamp(END, "teardown"); @@ -270,26 +319,4 @@ public abstract class AbstractNoBody implements HttpServerAdapters { } } } - - /* - * Converts a ByteBuffer containing bytes encoded using - * the given charset into a string. - * This method does not throw but will replace - * unrecognized sequences with the replacement character. - */ - public static String asString(ByteBuffer buffer, Charset charset) { - var decoded = charset.decode(buffer); - char[] chars = new char[decoded.length()]; - decoded.get(chars); - return new String(chars); - } - - /* - * Converts a ByteBuffer containing UTF-8 bytes into a - * string. This method does not throw but will replace - * unrecognized sequences with the replacement character. - */ - public static String asString(ByteBuffer buffer) { - return asString(buffer, StandardCharsets.UTF_8); - } } diff --git a/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java b/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java index 859169dcaae..9a9c2b44cd7 100644 --- a/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java +++ b/test/jdk/java/net/httpclient/AbstractThrowingPublishers.java @@ -21,6 +21,7 @@ * questions. */ +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -30,7 +31,6 @@ import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; import javax.net.ssl.SSLContext; import java.io.IOException; @@ -39,6 +39,7 @@ import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; @@ -70,9 +71,12 @@ import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.String.format; +import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -84,6 +88,7 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -92,6 +97,9 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final int ITERATION_COUNT = 1; // a shared executor helps reduce the amount of threads created by the test @@ -151,6 +159,41 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { + (params == null ? "()" : Arrays.toString(result.getParameters())); } + static Version version(String uri) { + if (uri.contains("/http1/") || uri.contains("/https1/")) + return HTTP_1_1; + if (uri.contains("/http2/") || uri.contains("/https2/")) + return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + System.out.println("\n" + now() + "--- Sending HEAD request ----\n"); + System.err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + System.out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + System.err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + @BeforeMethod void beforeMethod(ITestContext context) { if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { @@ -189,6 +232,8 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, httpURI_fixed, httpURI_chunk, httpsURI_fixed, @@ -326,7 +371,7 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -354,8 +399,12 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { HttpClient client = null; out.printf("%n%s testSanity(%s, %b)%n", now(), uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } SubmissionPublisher publisher = new SubmissionPublisher<>(executor,10); @@ -374,7 +423,7 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { }, executor); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .POST(bodyPublisher) .build(); BodyHandler handler = BodyHandlers.ofString(); @@ -445,18 +494,21 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { for (Where where : whereValues) { //if (where == Where.ON_SUBSCRIBE) continue; //if (where == Where.ON_ERROR) continue; - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } ThrowingBodyPublisher bodyPublisher = new ThrowingBodyPublisher(where.select(thrower), publishers.get()); - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .header("X-expect-exception", "true") .POST(bodyPublisher) .build(); BodyHandler handler = BodyHandlers.ofString(); - System.out.println("try throwing in " + where); + System.out.println(now() + " try throwing in " + where); HttpResponse response = null; if (async) { try { @@ -564,8 +616,12 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { static final class UncheckedCustomExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedCustomException(where.name()); + var thread = Thread.currentThread().getName(); + var thrown = new UncheckedCustomException("[" + thread + "] " + where.name()); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + thrown.printStackTrace(); + throw thrown; } @Override @@ -591,8 +647,13 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { static final class UncheckedIOExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedIOException(new CustomIOException(where.name())); + var thread = Thread.currentThread().getName(); + var cause = new CustomIOException("[" + thread + "] " + where.name()); + var thrown = new UncheckedIOException(cause); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + cause.printStackTrace(); + throw thrown; } @Override @@ -719,6 +780,9 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { @BeforeTest public void setup() throws Exception { + System.out.println(now() + "setup"); + System.err.println(now() + "setup"); + sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); @@ -732,12 +796,16 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x"; httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x"; + System.out.println(now() + "HTTP/1.1 server created"); + httpsTestServer = HttpTestServer.create(HTTP_1_1, sslContext); httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed"); httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk"); httpsURI_fixed = "https://" + httpsTestServer.serverAuthority() + "/https1/fixed/x"; httpsURI_chunk = "https://" + httpsTestServer.serverAuthority() + "/https1/chunk/x"; + System.out.println(now() + "TLS HTTP/1.1 server created"); + // HTTP/2 HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler(); HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler(); @@ -748,31 +816,67 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x"; + System.out.println(now() + "HTTP/2 server created"); + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(4); + System.out.println(now() + "TLS HTTP/2 server created"); + + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + + System.out.println(now() + "HTTP/3 server created"); + System.err.println(now() + "Starting servers"); + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + + out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); + out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); + out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); + out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); + + System.out.println(now() + "setup done"); + System.err.println(now() + "setup done"); } @AfterTest public void teardown() throws Exception { + System.out.println(now() + "teardown"); + System.err.println(now() + "teardown"); + String sharedClientName = sharedClient == null ? null : sharedClient.toString(); sharedClient = null; Thread.sleep(100); - AssertionError fail = TRACKER.check(500); + AssertionError fail = TRACKER.check(10000); try { httpTestServer.stop(); httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { @@ -781,6 +885,8 @@ public abstract class AbstractThrowingPublishers implements HttpServerAdapters { throw fail; } } + System.out.println(now() + "teardown done"); + System.err.println(now() + "teardown done"); } static class HTTP_FixedLengthHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java b/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java index b5230d48d10..a7aea89c379 100644 --- a/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java +++ b/test/jdk/java/net/httpclient/AbstractThrowingPushPromises.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -50,6 +50,7 @@ import org.testng.annotations.DataProvider; import javax.net.ssl.SSLContext; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -58,8 +59,10 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -88,12 +91,14 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.lang.System.err; import static java.lang.String.format; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -103,10 +108,13 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters SSLContext sslContext; HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http2URI_fixed; String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 1; // a shared executor helps reduce the amount of threads created by the test @@ -205,6 +213,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, http2URI_fixed, http2URI_chunk, https2URI_fixed, @@ -282,7 +292,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() + .version(HTTP_3) .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -302,6 +313,22 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters } } + Http3DiscoveryMode config(String uri) { + return uri.contains("/http3/") ? HTTP_3_URI_ONLY : null; + } + + Version version(String uri) { + return uri.contains("/http3/") ? HTTP_3 : HTTP_2; + } + + HttpRequest request(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)) + .version(version(uri)); + var config = config(uri); + if (config != null) builder.setOption(H3_DISCOVERY, config); + return builder.build(); + } + // @Test(dataProvider = "sanity") protected void testSanityImpl(String uri, boolean sameClient) throws Exception { @@ -311,8 +338,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters if (!sameClient || client == null) client = newHttpClient(sameClient); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) - .build(); + HttpRequest req = request(uri); + BodyHandler> handler = new ThrowingBodyHandler((w) -> {}, BodyHandlers.ofLines()); @@ -339,7 +366,7 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters .collect(Collectors.joining("|")); assertEquals(promisedBody, promised.uri().toASCIIString()); } - assertEquals(3, pushPromises.size()); + assertEquals(pushPromises.size(), 3); if (!sameClient) { // Wait for the client to be garbage collected. // we use the ReferenceTracker API rather than HttpClient::close here, @@ -423,9 +450,8 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters if (!sameClient || client == null) client = newHttpClient(sameClient); - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) - .build(); + HttpRequest req = request(uri); + ConcurrentMap>> promiseMap = new ConcurrentHashMap<>(); Supplier> throwing = () -> @@ -739,24 +765,31 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters throw new AssertionError("Unexpected null sslContext"); // HTTP/2 - HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler(); - HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler(); + HttpTestHandler fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler chunkedHandler = new HTTP_ChunkedHandler(); http2TestServer = HttpTestServer.create(HTTP_2); - http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed"); - http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk"); + http2TestServer.addHandler(fixedLengthHandler, "/http2/fixed"); + http2TestServer.addHandler(chunkedHandler, "/http2/chunk"); http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x"; https2TestServer = HttpTestServer.create(HTTP_2, sslContext); - https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); - https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); + https2TestServer.addHandler(fixedLengthHandler, "/https2/fixed"); + https2TestServer.addHandler(chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(2); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + + serverCount.addAndGet(3); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -769,6 +802,7 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters try { http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { @@ -794,15 +828,69 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); out.printf("TestServer: %s Pushing promise: %s%n", now(), promise); err.printf("TestServer: %s Pushing promise: %s%n", now(), promise); - HttpHeaders headers; + HttpHeaders reqHaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; if (fixed) { String length = String.valueOf(promiseBytes.length); - headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), ACCEPT_ALL); } else { - headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty } - t.serverPush(promise, headers, promiseBytes); + t.serverPush(promise, reqHaders, rspHeaders, promiseBytes); + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static long sendHttp3PushPromiseFrame(HttpTestExchange t, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending PushPromiseFrame: %s%n", now(), promise); + err.printf("TestServer: %s Pushing PushPromiseFrame: %s%n", now(), promise); + // headers are added to the request headers sent in the push promise + HttpHeaders headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + long pushId = t.sendHttp3PushPromiseFrame(-1, promise, headers); + out.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + err.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + return pushId; + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static void sendHttp3PushResponse(HttpTestExchange t, + long pushId, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending push response pushId=%s: %s%n", now(), pushId, promise); + err.printf("TestServer: %s Pushing push response pushId=%s: %s%n", now(), pushId, promise); + HttpHeaders reqHaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; + if (fixed) { + String length = String.valueOf(promiseBytes.length); + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + ACCEPT_ALL); + } else { + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + } + t.sendHttp3PushResponse(pushId, promise, reqHaders, rspHeaders, new ByteArrayInputStream(promiseBytes)); } catch (URISyntaxException x) { throw new IOException(x.getMessage(), x); } @@ -822,13 +910,31 @@ public abstract class AbstractThrowingPushPromises implements HttpServerAdapters } byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8); t.sendResponseHeaders(200, resp.length); //fixed content length + + // With HTTP/3 fixed length we send a single DataFrame, + // therefore we can't interleave a PushPromiseFrame in + // the middle of the DataFrame, so we're going to send + // the PushPromiseFrame before the DataFrame, and then + // fulfill the promise later while sending the response + // body. + long[] pushIds = new long[2]; + if (t.getExchangeVersion() == HTTP_3) { + for (int i = 0; i < 2; i++) { + String path = requestURI.getPath() + "/after/promise-" + (i + 2); + pushIds[i] = sendHttp3PushPromiseFrame(t, requestURI, path, true); + } + } try (OutputStream os = t.getResponseBody()) { int bytes = resp.length/3; for (int i = 0; i<2; i++) { - String path = requestURI.getPath() + "/after/promise-" + (i + 2); os.write(resp, i * bytes, bytes); os.flush(); - pushPromiseFor(t, requestURI, path, true); + String path = requestURI.getPath() + "/after/promise-" + (i + 2); + if (t.getExchangeVersion() == HTTP_2) { + pushPromiseFor(t, requestURI, path, true); + } else if (t.getExchangeVersion() == HTTP_3) { + sendHttp3PushResponse(t, pushIds[i], requestURI, path, true); + } } os.write(resp, 2*bytes, resp.length - 2*bytes); } diff --git a/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java b/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java index 7362ada9772..0dc808b8bb2 100644 --- a/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java +++ b/test/jdk/java/net/httpclient/AbstractThrowingSubscribers.java @@ -21,6 +21,7 @@ * questions. */ +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -40,6 +41,7 @@ import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; @@ -66,10 +68,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import static java.lang.System.err; import static java.lang.System.out; import static java.lang.String.format; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -81,6 +86,7 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -89,6 +95,9 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final int ITERATION_COUNT = 1; static final int REPEAT_RESPONSE = 3; @@ -149,6 +158,41 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters + (params == null ? "()" : Arrays.toString(result.getParameters())); } + static Version version(String uri) { + if (uri.contains("/http1/") || uri.contains("/https1/")) + return HTTP_1_1; + if (uri.contains("/http2/") || uri.contains("/https2/")) + return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + System.out.println("\n" + now() + "--- Sending HEAD request ----\n"); + System.err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + System.out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + System.err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + @BeforeMethod void beforeMethod(ITestContext context) { if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { @@ -188,6 +232,8 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, httpURI_fixed, httpURI_chunk, httpsURI_fixed, @@ -238,7 +284,7 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters private HttpClient makeNewClient() { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -296,10 +342,14 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters String uri2 = uri + "-" + URICOUNT.incrementAndGet() + "/sanity"; out.printf("%ntestSanity(%s, %b)%n", uri2, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } - HttpRequest req = HttpRequest.newBuilder(URI.create(uri2)) + HttpRequest req = newRequestBuilder(uri2) .build(); BodyHandler handler = new ThrowingBodyHandler((w) -> {}, @@ -439,12 +489,15 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters HttpClient client = null; var throwing = thrower; for (Where where : EnumSet.complementOf(excludes)) { - - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } + String uri2 = uri + "-" + where; - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri2)) + HttpRequest req = newRequestBuilder(uri2) .build(); thrower = thrower(where, throwing); @@ -593,8 +646,12 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters static final class UncheckedCustomExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedCustomException(where.name()); + var thread = Thread.currentThread().getName(); + var thrown = new UncheckedCustomException("[" + thread + "] " + where.name()); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + thrown.printStackTrace(); + throw thrown; } @Override @@ -611,8 +668,13 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters static final class UncheckedIOExceptionThrower implements Thrower { @Override public void accept(Where where) { - out.println(now() + "Throwing in " + where); - throw new UncheckedIOException(new CustomIOException(where.name())); + var thread = Thread.currentThread().getName(); + var cause = new CustomIOException("[" + thread + "] " + where.name()); + var thrown = new UncheckedIOException(cause); + out.println(now() + "Throwing in " + where + ": " + thrown); + err.println(now() + "Throwing in " + where + ": " + thrown); + cause.printStackTrace(); + throw thrown; } @Override @@ -759,10 +821,15 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters @BeforeTest public void setup() throws Exception { + System.out.println(now() + "setup"); + System.err.println(now() + "setup"); + sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); + System.out.println(now() + "HTTP/1.1 server created"); + // HTTP/1.1 HttpTestHandler h1_fixedLengthHandler = new HTTP_FixedLengthHandler(); HttpTestHandler h1_chunkHandler = new HTTP_ChunkedHandler(); @@ -772,6 +839,8 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed/x"; httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk/x"; + System.out.println(now() + "TLS HTTP/1.1 server created"); + httpsTestServer = HttpTestServer.create(HTTP_1_1, sslContext); httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed"); httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk"); @@ -788,21 +857,56 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/x"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/x"; + System.out.println(now() + "HTTP/2 server created"); + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(4); + System.out.println(now() + "TLS HTTP/2 server created"); + + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + + System.out.println(now() + "HTTP/3 server created"); + System.err.println(now() + "Starting servers"); + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + + out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); + out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); + out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); + out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); + + System.out.println(now() + "setup done"); + System.err.println(now() + "setup done"); } @AfterTest public void teardown() throws Exception { + System.out.println(now() + "teardown"); + System.err.println(now() + "teardown"); + String sharedClientName = sharedClient == null ? null : sharedClient.toString(); sharedClient = null; @@ -813,6 +917,7 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { @@ -821,6 +926,8 @@ public abstract class AbstractThrowingSubscribers implements HttpServerAdapters throw fail; } } + System.out.println(now() + "teardown done"); + System.err.println(now() + "teardown done"); } static class HTTP_FixedLengthHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java b/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java index a879525b4b4..8ec3b256e62 100644 --- a/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java +++ b/test/jdk/java/net/httpclient/AggregateRequestBodyTest.java @@ -28,7 +28,7 @@ * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters * ReferenceTracker AggregateRequestBodyTest * @run testng/othervm -Djdk.internal.httpclient.debug=true - * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,headers,frames * AggregateRequestBodyTest * @summary Tests HttpRequest.BodyPublishers::concat */ @@ -69,6 +69,7 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import javax.net.ssl.SSLContext; import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; import org.testng.Assert; import org.testng.ITestContext; import org.testng.ITestResult; @@ -83,6 +84,8 @@ import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -95,14 +98,15 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { HttpTestServer https1TestServer; // HTTPS/1.1 ( https ) HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) - String http1URI; - String https1URI; - String http2URI; - String https2URI; + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) + URI http1URI; + URI https1URI; + URI http2URI; + URI https2URI; + URI http3URI; static final int RESPONSE_CODE = 200; static final int ITERATION_COUNT = 4; - static final Class IAE = IllegalArgumentException.class; static final Class CE = CompletionException.class; // a shared executor helps reduce the amount of threads created by the test static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); @@ -197,12 +201,13 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { } } - private String[] uris() { - return new String[] { + private URI[] uris() { + return new URI[] { http1URI, https1URI, http2URI, https2URI, + http3URI, }; } @@ -213,12 +218,18 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { return new Object[0][]; } - String[] uris = uris(); + URI[] uris = uris(); Object[][] result = new Object[uris.length * 2][]; int i = 0; for (boolean sameClient : List.of(false, true)) { - for (String uri : uris()) { - result[i++] = new Object[]{uri, sameClient}; + for (URI uri : uris()) { + HttpClient.Version version = null; + if (uri.equals(http1URI) || uri.equals(https1URI)) version = HttpClient.Version.HTTP_1_1; + else if (uri.equals(http2URI) || uri.equals(https2URI)) version = HttpClient.Version.HTTP_2; + else if (uri.equals(http3URI)) version = HTTP_3; + else throw new AssertionError("Unexpected URI: " + uri); + + result[i++] = new Object[]{uri, version, sameClient}; } } assert i == uris.length * 2; @@ -227,7 +238,7 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -802,7 +813,7 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { } @Test(dataProvider = "variants") - public void test(String uri, boolean sameClient) throws Exception { + public void test(URI uri, HttpClient.Version version, boolean sameClient) throws Exception { checkSkip(); System.out.printf("Request to %s (sameClient: %s)%n", uri, sameClient); System.err.printf("Request to %s (sameClient: %s)%n", uri, sameClient); @@ -814,9 +825,12 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { .map(BodyPublishers::ofString) .toArray(HttpRequest.BodyPublisher[]::new) ); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + + HttpRequest request = HttpRequest.newBuilder(uri) + .version(version) .POST(publisher) .build(); + for (int i = 0; i < ITERATION_COUNT; i++) { System.out.println(uri + ": Iteration: " + i); System.err.println(uri + ": Iteration: " + i); @@ -826,9 +840,19 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode())); assertEquals(response.body(), BODIES.stream().collect(Collectors.joining())); } + if (!sameClient) client.close(); System.out.println("test: DONE"); } + private URI buildURI(String scheme, String path, int port) { + return URIBuilder.newBuilder() + .scheme(scheme) + .loopback() + .port(port) + .path(path) + .buildUnchecked(); + } + @BeforeTest public void setup() throws Exception { sslContext = new SimpleSSLContext().get(); @@ -838,32 +862,37 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { HttpTestHandler handler = new HttpTestEchoHandler(); http1TestServer = HttpTestServer.create(HTTP_1_1); http1TestServer.addHandler(handler, "/http1/echo/"); - http1URI = "http://" + http1TestServer.serverAuthority() + "/http1/echo/x"; + http1URI = buildURI("http", "/http1/echo/x", http1TestServer.getAddress().getPort()); https1TestServer = HttpTestServer.create(HTTP_1_1, sslContext); https1TestServer.addHandler(handler, "/https1/echo/"); - https1URI = "https://" + https1TestServer.serverAuthority() + "/https1/echo/x"; + https1URI = buildURI("https", "/https1/echo/x", https1TestServer.getAddress().getPort()); - // HTTP/2 http2TestServer = HttpTestServer.create(HTTP_2); http2TestServer.addHandler(handler, "/http2/echo/"); - http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo/x"; + http2URI = buildURI("http", "/http2/echo/x", http2TestServer.getAddress().getPort()); https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(handler, "/https2/echo/"); - https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo/x"; + https2URI = buildURI("https", "/https2/echo/x", https2TestServer.getAddress().getPort()); - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(handler, "/http3/echo/"); + http3URI = buildURI("https", "/http3/echo/x", http3TestServer.getAddress().getPort()); + + serverCount.addAndGet(5); http1TestServer.start(); https1TestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest public void teardown() throws Exception { String sharedClientName = sharedClient == null ? null : sharedClient.toString(); + sharedClient.close(); sharedClient = null; Thread.sleep(100); AssertionError fail = TRACKER.check(500); @@ -872,6 +901,7 @@ public class AggregateRequestBodyTest implements HttpServerAdapters { https1TestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/AltServiceUsageTest.java b/test/jdk/java/net/httpclient/AltServiceUsageTest.java new file mode 100644 index 00000000000..2322b36918b --- /dev/null +++ b/test/jdk/java/net/httpclient/AltServiceUsageTest.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.OptionalLong; + +import javax.net.ssl.SSLContext; + +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + +/* + * @test + * @summary Verifies that the HTTP client correctly handles various alt-svc usages + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters + * + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * AltServiceUsageTest + */ +public class AltServiceUsageTest implements HttpServerAdapters { + + private SSLContext sslContext; + private HttpTestServer originServer; + private HttpTestServer altServer; + + private DatagramChannel udpNotResponding; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + + // attempt to create an HTTP/3 server, an HTTP/2 server, and a + // DatagramChannel bound to the same port as the HTTP/2 server + int count = 0; + InetSocketAddress altServerAddress = null, originServerAddress = null; + while (count++ < 10) { + + createServers(); + altServerAddress = altServer.getAddress(); + originServerAddress = originServer.getAddress(); + + if (originServerAddress.equals(altServerAddress)) break; + udpNotResponding = DatagramChannel.open(); + try { + udpNotResponding.bind(originServerAddress); + break; + } catch (IOException x) { + System.out.printf("Failed to bind udpNotResponding to %s: %s%n", + originServerAddress, x); + safeStop(altServer); + safeStop(originServer); + udpNotResponding.close(); + } + } + if (count > 10) { + throw new AssertionError("Couldn't reserve UDP port at " + originServerAddress); + } + + System.out.println("HTTP/3 service started at " + altServerAddress); + System.out.println("HTTP/2 service started at " + originServerAddress); + System.err.println("**** All servers started. Test will start shortly ****"); + } + + private void createServers() throws IOException { + altServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + altServer.addHandler(new All200OKHandler(), "/foo/"); + altServer.addHandler(new RequireAltUsedHeader(), "/bar/"); + altServer.addHandler(new All200OKHandler(), "/maxAgeAltSvc/"); + altServer.start(); + + originServer = HttpTestServer.create(HTTP_2, sslContext); + originServer.addHandler(new H3AltServicePublisher(altServer.getAddress()), "/foo/"); + originServer.addHandler(new H3AltSvcPublisherWith421(altServer.getAddress()), "/foo421/"); + originServer.addHandler(new H3AltServicePublisher(altServer.getAddress()), "/bar/"); + originServer.addHandler(new H3AltSvcPublisherWithMaxAge(altServer.getAddress()), "/maxAgeAltSvc/"); + originServer.start(); + } + + @AfterClass + public void afterClass() throws Exception { + safeStop(originServer); + safeStop(altServer); + udpNotResponding.close(); + } + + private static void safeStop(final HttpTestServer server) { + if (server == null) { + return; + } + final InetSocketAddress serverAddr = server.getAddress(); + try { + System.out.println("Stopping server " + serverAddr); + server.stop(); + } catch (Exception e) { + System.err.println("Ignoring exception: " + e.getMessage() + " that occurred " + + "during stop of server: " + serverAddr); + } + } + + private static class H3AltServicePublisher implements HttpTestHandler { + private static final String RESPONSE_CONTENT = "apple"; + + private final String altSvcHeader; + + /** + * @param altServerAddr Address of the alt service which will be advertised by this handler + */ + private H3AltServicePublisher(final InetSocketAddress altServerAddr) { + this.altSvcHeader = "h3=\"" + altServerAddr.getHostName() + ":" + altServerAddr.getPort() + + "\"; persist=1; intentional-unknown-param-which-is-expected-to-be-ignored=foo;"; + } + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + String maxAgeParam = ""; + if (getMaxAge().isPresent()) { + maxAgeParam = "; ma=" + getMaxAge().getAsLong(); + } + exchange.getResponseHeaders().addHeader("alt-svc", altSvcHeader + maxAgeParam); + final int statusCode = getResponseCode(); + System.out.println("Sending response with status code " + statusCode + " and " + + "alt-svc header " + altSvcHeader); + final byte[] content = RESPONSE_CONTENT.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(statusCode, content.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(content); + } + } + + protected OptionalLong getMaxAge() { + return OptionalLong.empty(); + } + + protected int getResponseCode() { + return 200; + } + } + + private static final class H3AltSvcPublisherWith421 extends H3AltServicePublisher { + + private H3AltSvcPublisherWith421(InetSocketAddress h3ServerAddr) { + super(h3ServerAddr); + } + + @Override + protected int getResponseCode() { + return 421; + } + } + + private static final class H3AltSvcPublisherWithMaxAge extends H3AltServicePublisher { + + private H3AltSvcPublisherWithMaxAge(InetSocketAddress h3ServerAddr) { + super(h3ServerAddr); + } + + @Override + protected OptionalLong getMaxAge() { + return OptionalLong.of(2); + } + } + + private static final class All200OKHandler implements HttpTestHandler { + private static final String RESPONSE_CONTENT = "orange"; + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + final byte[] content = RESPONSE_CONTENT.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, content.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(content); + } + } + } + + private static final class RequireAltUsedHeader implements HttpTestHandler { + private static final String RESPONSE_CONTENT = "tomato"; + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + final Optional altUsed = exchange.getRequestHeaders().firstValue("alt-used"); + if (altUsed.isEmpty()) { + System.out.println("Error - Missing alt-used header in request"); + exchange.sendResponseHeaders(400, 0); + return; + } + if (altUsed.get().isBlank()) { + System.out.println("Error - Blank value for alt-used header in request"); + exchange.sendResponseHeaders(400, 0); + return; + } + System.out.println("Found alt-used request header: " + altUsed.get()); + final byte[] content = RESPONSE_CONTENT.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, content.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(content); + } + } + } + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server. The test then issues the exact same request again and this time it + * expects the alt-service host/server to handle that request. + */ + @Test + public void testAltSvcHeaderUsage() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/foo/"); + final HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(secondResponse.body(), All200OKHandler.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server and a response code of 421. The spec states that when this response code + * is sent, any alt-svc headers should be ignored by the HTTP client. This test then issues the same + * request again and expects that the alt-service wasn't used. + */ + @Test + public void testDontUseAltServiceFor421() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/foo421/"); + final HttpRequest request = HttpRequest.newBuilder().GET().uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 421, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 421, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server. The test then issues the exact same request again and this time it + * expects the alt-service host/server to handle that request. The alt-service host/server which + * handles this request will assert that the request came in with an "alt-used" header (which + * is expected to be set by the HTTP client). If such a header is missing then that server + * responds back with an erroneous response code of 4xx. + */ + @Test + public void testAltUsedHeader() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/bar/"); + final HttpRequest request = HttpRequest.newBuilder().GET().uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + // (which on the server side will assert the presence of the alt-used header, set by the + // HTTP client) + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(secondResponse.body(), RequireAltUsedHeader.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + + /** + * This test sends a HTTP3 request to a server which responds back with an alt-svc header pointing + * to a different server. The advertised alt-svc is expected to have a max age of some seconds. + * The test then immediately issues the exact same request again and this time it + * expects the alt-service host/server to handle that request. Once this is done, the test waits + * for a while (duration is greater than the max age of the advertised alt-service) and then + * issues the exact same request again. This time the request is expected to be handled by the + * origin server and not the alt-service (since the alt-service is expected to have expired by + * now) + */ + @Test + public void testAltSvcMaxAgeExpiry() throws Exception { + HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI reqURI = URI.create("https://" + toHostPort(originServer) + "/maxAgeAltSvc/"); + final HttpRequest request = HttpRequest.newBuilder().GET().uri(reqURI).build(); + System.out.println("Issuing request " + reqURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(response.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + Assert.assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + System.out.println("Received alt-svc header value: " + altSvcHeader.get()); + final String expectedHeader = "h3=\"" + toHostPort(altServer) + "\""; + Assert.assertTrue(altSvcHeader.get().contains(expectedHeader), + "Unexpected alt-svc header value: " + altSvcHeader.get() + + ", was expected to contain: " + expectedHeader); + + // now issue the same request again and this time expect it to be handled by the alt-service + System.out.println("Again issuing request " + reqURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(secondResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(secondResponse.body(), All200OKHandler.RESPONSE_CONTENT, + "Unexpected response body"); + + // wait for alt-service to expire + final long sleepDuration = 4000; + System.out.println("Sleeping for " + sleepDuration + " milli seconds for alt-service to expire"); + Thread.sleep(sleepDuration); + // now issue the same request again and this time expect it to be handled by the origin server + // since the alt-service is expected to be expired + System.out.println("Issuing request for a third time " + reqURI); + final HttpResponse thirdResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(thirdResponse.statusCode(), 200, "Unexpected response code"); + Assert.assertEquals(thirdResponse.body(), H3AltServicePublisher.RESPONSE_CONTENT, + "Unexpected response body"); + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + System.gc(); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + + private static String toHostPort(final HttpTestServer server) { + final InetSocketAddress addr = server.getAddress(); + return addr.getHostName() + ":" + addr.getPort(); + } +} diff --git a/test/jdk/java/net/httpclient/AsFileDownloadTest.java b/test/jdk/java/net/httpclient/AsFileDownloadTest.java index 31d919230a4..42567602dff 100644 --- a/test/jdk/java/net/httpclient/AsFileDownloadTest.java +++ b/test/jdk/java/net/httpclient/AsFileDownloadTest.java @@ -35,6 +35,8 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; @@ -48,11 +50,16 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; + import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.common.TestServerConfigurator; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2Handler; @@ -61,6 +68,8 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static java.lang.System.out; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.net.http.HttpResponse.BodyHandlers.ofFileDownload; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.StandardOpenOption.*; @@ -75,6 +84,7 @@ import static org.testng.Assert.fail; * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * jdk.test.lib.Platform jdk.test.lib.util.FileUtils + * jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.httpclient.test.lib.common.TestServerConfigurator * @run testng/othervm/timeout=480 AsFileDownloadTest */ @@ -85,10 +95,12 @@ public class AsFileDownloadTest { HttpsServer httpsTestServer; // HTTPS/1.1 Http2TestServer http2TestServer; // HTTP/2 ( h2c ) Http2TestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h3TestServer; // HTTP/3 String httpURI; String httpsURI; String http2URI; String https2URI; + String h3URI; final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; Path tempDir; @@ -144,38 +156,50 @@ public class AsFileDownloadTest { List list = new ArrayList<>(); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {httpURI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {httpURI + "?" + e[0], e[1], e[2], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], e[2], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {http2URI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {http2URI + "?" + e[0], e[1], e[2], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionValues).stream() - .map(e -> new Object[] {https2URI + "?" + e[0], e[1], e[2]}) + .map(e -> new Object[] {https2URI + "?" + e[0], e[1], e[2], Optional.empty()}) + .forEach(list::add); + Arrays.asList(contentDispositionValues).stream() + .map(e -> new Object[] {h3URI + "?" + e[0], e[1], e[2], Optional.of(Version.HTTP_3)}) .forEach(list::add); return list.stream().toArray(Object[][]::new); } + HttpClient newHttpClient(Optional version) { + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpClient.newBuilder() + : HttpServerAdapters.createClientBuilderForH3().version(Version.HTTP_3); + return builder.sslContext(sslContext).proxy(Builder.NO_PROXY).build(); + } + @Test(dataProvider = "positive") - void test(String uriString, String contentDispositionValue, String expectedFilename) - throws Exception - { + void test(String uriString, String contentDispositionValue, String expectedFilename, + Optional requestVersion) throws Exception { out.printf("test(%s, %s, %s): starting", uriString, contentDispositionValue, expectedFilename); - HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); - TRACKER.track(client); - ReferenceQueue queue = new ReferenceQueue<>(); - WeakReference ref = new WeakReference<>(client, queue); - try { + try (HttpClient client = newHttpClient(requestVersion)) { + TRACKER.track(client); + ReferenceQueue queue = new ReferenceQueue<>(); + WeakReference ref = new WeakReference<>(client, queue); URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri) - .POST(BodyPublishers.ofString("May the luck of the Irish be with you!")) - .build(); + HttpRequest.Builder requestBuilder = newRequestBuilder(uriString); + if (requestVersion.isPresent()) { + requestBuilder.version(requestVersion.get()); + } + HttpRequest request = requestBuilder.POST( + BodyPublishers.ofString("May the luck of the Irish be with you!")).build(); BodyHandler bh = ofFileDownload(tempDir.resolve(uri.getPath().substring(1)), CREATE, TRUNCATE_EXISTING, WRITE); + out.println("Issuing request " + request); HttpResponse response = client.send(request, bh); Path body = response.body(); out.println("Got response: " + response); @@ -189,6 +213,10 @@ public class AsFileDownloadTest { assertEquals(response.headers().firstValue("Content-Disposition").get(), contentDispositionValue); assertEquals(fileContents, "May the luck of the Irish be with you!"); + if (requestVersion.isPresent()) { + assertEquals(response.version(), requestVersion.get(), "unexpected HTTP version" + + " in response"); + } if (!body.toAbsolutePath().startsWith(tempDir.toAbsolutePath())) { System.out.println("Tempdir = " + tempDir.toAbsolutePath()); @@ -198,16 +226,9 @@ public class AsFileDownloadTest { // additional checks unrelated to file download caseInsensitivityOfHeaders(request.headers()); caseInsensitivityOfHeaders(response.headers()); - } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { - System.gc(); - if (queue.remove(100) == ref) break; - } - AssertionError failed = TRACKER.checkShutdown(1000); - if (failed != null) throw failed; } + AssertionError failed = TRACKER.checkClosed(1000); + if (failed != null) throw failed; } // --- Negative @@ -238,54 +259,49 @@ public class AsFileDownloadTest { List list = new ArrayList<>(); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {httpURI + "?" + e[0], e[1]}) + .map(e -> new Object[] {httpURI + "?" + e[0], e[1], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {httpsURI + "?" + e[0], e[1]}) + .map(e -> new Object[] {httpsURI + "?" + e[0], e[1], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {http2URI + "?" + e[0], e[1]}) + .map(e -> new Object[] {http2URI + "?" + e[0], e[1], Optional.empty()}) .forEach(list::add); Arrays.asList(contentDispositionBADValues).stream() - .map(e -> new Object[] {https2URI + "?" + e[0], e[1]}) + .map(e -> new Object[] {https2URI + "?" + e[0], e[1], Optional.empty()}) + .forEach(list::add); + Arrays.asList(contentDispositionBADValues).stream() + .map(e -> new Object[] {h3URI + "?" + e[0], e[1], Optional.of(Version.HTTP_3)}) .forEach(list::add); - return list.stream().toArray(Object[][]::new); } @Test(dataProvider = "negative") - void negativeTest(String uriString, String contentDispositionValue) - throws Exception - { + void negativeTest(String uriString, String contentDispositionValue, + Optional requestVersion) throws Exception { out.printf("negativeTest(%s, %s): starting", uriString, contentDispositionValue); - HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build(); - TRACKER.track(client); - ReferenceQueue queue = new ReferenceQueue<>(); - WeakReference ref = new WeakReference<>(client, queue); + try (HttpClient client = newHttpClient(requestVersion)) { + TRACKER.track(client); + ReferenceQueue queue = new ReferenceQueue<>(); + WeakReference ref = new WeakReference<>(client, queue); - try { - URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri) - .POST(BodyPublishers.ofString("Does not matter")) + HttpRequest.Builder reqBuilder = newRequestBuilder(uriString); + if (requestVersion.isPresent()) { + reqBuilder.version(requestVersion.get()); + } + HttpRequest request = reqBuilder.POST(BodyPublishers.ofString("Does not matter")) .build(); - BodyHandler bh = ofFileDownload(tempDir, CREATE, TRUNCATE_EXISTING, WRITE); try { + out.println("Issuing request " + request); HttpResponse response = client.send(request, bh); fail("UNEXPECTED response: " + response + ", path:" + response.body()); } catch (UncheckedIOException | IOException ioe) { System.out.println("Caught expected: " + ioe); } - } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { - System.gc(); - if (queue.remove(100) == ref) break; - } - AssertionError failed = TRACKER.checkShutdown(1000); - if (failed != null) throw failed; } + AssertionError failed = TRACKER.checkClosed(1000); + if (failed != null) throw failed; } // -- Infrastructure @@ -297,6 +313,23 @@ public class AsFileDownloadTest { return h + ":" + server.getAddress().getPort(); } + Version version(String uri) { + if (uri.contains("/http1/")) return Version.HTTP_1_1; + if (uri.contains("/https1/")) return Version.HTTP_1_1; + if (uri.contains("/http2/")) return Version.HTTP_2; + if (uri.contains("/https2/")) return Version.HTTP_2; + if (uri.contains("/h3/")) return Version.HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == Version.HTTP_3) { + builder.setOption(H3_DISCOVERY, h3TestServer.h3DiscoveryConfig()); + } + return builder; + } + @BeforeTest public void setup() throws Exception { tempDir = Paths.get("asFileDownloadTest.tmp.dir"); @@ -309,6 +342,7 @@ public class AsFileDownloadTest { Files.createDirectories(tempDir.resolve("https1/afdt/")); Files.createDirectories(tempDir.resolve("http2/afdt/")); Files.createDirectories(tempDir.resolve("https2/afdt/")); + Files.createDirectories(tempDir.resolve("h3/afdt/")); // HTTP/1.1 server logging in case of security exceptions ( uncomment if needed ) //Logger logger = Logger.getLogger("com.sun.net.httpserver"); @@ -339,10 +373,15 @@ public class AsFileDownloadTest { https2TestServer.addHandler(new Http2FileDispoHandler(), "/https2/afdt"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/afdt"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new Http2FileDispoHandler(), "/h3/afdt"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/afdt"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -351,6 +390,7 @@ public class AsFileDownloadTest { httpsTestServer.stop(0); http2TestServer.stop(); https2TestServer.stop(); + h3TestServer.stop(); if (Files.exists(tempDir)) { // clean up @@ -392,7 +432,8 @@ public class AsFileDownloadTest { } } - static class Http2FileDispoHandler implements Http2Handler { + static class Http2FileDispoHandler implements Http2Handler, HttpServerAdapters.HttpTestHandler { + @Override public void handle(Http2TestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); @@ -407,6 +448,21 @@ public class AsFileDownloadTest { os.write(bytes); } } + + @Override + public void handle(HttpTestExchange t) throws IOException { + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + byte[] bytes = is.readAllBytes(); + + String value = contentDispositionValueFromURI(t.getRequestURI()); + if (!value.equals("<>")) + t.getResponseHeaders().addHeader("Content-Disposition", value); + + t.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } } // --- diff --git a/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java b/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java index 7e7709c033c..5338d64892b 100644 --- a/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java +++ b/test/jdk/java/net/httpclient/AsyncExecutorShutdown.java @@ -40,12 +40,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -62,15 +62,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; -import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import javax.net.ssl.SSLContext; + import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -82,6 +77,9 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -94,15 +92,21 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } static final Random RANDOM = RandomFactory.getRandom(); + ExecutorService readerService; SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 6 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/2 ( h2+h3 ) + HttpTestServer h3TestServer; // HTTP/2 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h3URI; + String h2h3Head; static final String MESSAGE = "AsyncExecutorShutdown message body"; static final int ITERATIONS = 3; @@ -110,10 +114,12 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig() }, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig() }, + { httpURI, HTTP_1_1, null }, + { httpsURI, HTTP_1_1, null }, + { http2URI, HTTP_2, null }, + { https2URI, HTTP_2, null }, }; } @@ -127,8 +133,9 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { return t; } - static String readBody(InputStream in) { - try { + static String readBody(InputStream body) { + out.printf("[%s] reading body%n", Thread.currentThread()); + try (var in = body) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); @@ -159,26 +166,37 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting (%s, %s, %s) ----%n%n", uriString, version, config); ExecutorService executorService = Executors.newCachedThreadPool(); - ExecutorService readerService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) .executor(executorService) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); assert client.executor().isPresent(); + Throwable failed = null; int step = RANDOM.nextInt(ITERATIONS); + int head = Math.min(1, step); + List> bodies = new ArrayList<>(); try { - List> bodies = new ArrayList<>(); for (int i = 0; i < ITERATIONS; i++) { + if (i == head && version == HTTP_3 && config != HTTP_3_URI_ONLY) { + // let's the first request go through whatever version, + // but ensure that the second will find an AltService + // record + out.printf("%d: sending head request%n", i); + headRequest(client); + out.printf("%d: head request sent%n", i); + } URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; @@ -189,11 +207,15 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { .thenApply((response) -> { out.println(si + ": Got response: " + response); assertEquals(response.statusCode(), 200); + if (si >= head) assertEquals(response.version(), version); return response; }); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(AsyncExecutorShutdown::readBody) - .thenApply((s) -> { assertEquals(s, MESSAGE); return s;}); + .thenApply((s) -> { + assertEquals(s, MESSAGE); + return s; + }); } catch (RejectedExecutionException x) { out.println(i + ": Got expected exception: " + x); continue; @@ -205,7 +227,8 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } if (i == step) { out.printf("%d: shutting down executor now%n", i, sleep); - executorService.shutdownNow(); + var list = executorService.shutdownNow(); + out.printf("%d: executor shut down: %s%n", i, list); } var cf = bodyCF.exceptionally((t) -> { Throwable cause = getCause(t); @@ -219,39 +242,72 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { checkCause(String.valueOf(si), cause); return null; }); + out.printf("%d: adding body to bodies list%n", i); bodies.add(cf); } - CompletableFuture.allOf(bodies.toArray(new CompletableFuture[0])).get(); + } catch (Throwable t) { + failed = t; } finally { client = null; - executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); - readerService.shutdown(); - readerService.awaitTermination(2000, TimeUnit.MILLISECONDS); + System.gc(); + try { + out.println("Awaiting executorService termination"); + executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); + out.println("Done"); + } catch (Throwable t) { + if (failed != null) { + failed.addSuppressed(t); + } else failed = t; + } + var error = TRACKER.checkShutdown(2000); + if (error != null) { + out.println("Client hasn't shutdown properly: " + error); + if (failed != null) failed.addSuppressed(error); + else failed = error; + } } + if (failed instanceof Exception fe) { + throw fe; + } else if (failed instanceof Error e) { + throw e; + } + out.println("Awaiting all bodies..."); + CompletableFuture.allOf(bodies.toArray(new CompletableFuture[0])).get(); } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting (%s, %s, %s) ----%n%n", uriString, version, config); ExecutorService executorService = Executors.newCachedThreadPool(); - ExecutorService readerService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .executor(executorService) .sslContext(sslContext) .build(); TRACKER.track(client); assert client.executor().isPresent(); + Throwable failed = null; int step = RANDOM.nextInt(ITERATIONS); + int head = Math.min(1, step); out.printf("will shutdown executor in step %d%n", step); try { for (int i = 0; i < ITERATIONS; i++) { + if (i == head && version == HTTP_3 && config != HTTP_3_URI_ONLY) { + // let's the first request go through whatever version, + // but ensure that the second will find an AltService + // record + out.printf("%d: sending head request%n", i); + headRequest(client); + out.printf("%d: head request sent%n", i); + } URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) - .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) - .build(); + .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) + .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); final int si = i; CompletableFuture> responseCF; @@ -261,6 +317,7 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { .thenApply((response) -> { out.println(si + ": Got response: " + response); assertEquals(response.statusCode(), 200); + if (si > 0) assertEquals(response.version(), version); return response; }); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) @@ -278,9 +335,10 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } if (i == step) { out.printf("%d: shutting down executor now%n", i, sleep); - executorService.shutdownNow(); + var list = executorService.shutdownNow(); + out.printf("%d: executor shut down: %s%n", i, list); } - bodyCF.handle((r,t) -> { + bodyCF.handle((r, t) -> { if (t != null) { try { Throwable cause = getCause(t); @@ -301,22 +359,63 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { } }).thenCompose((c) -> c).get(); } - } finally { + } catch (Throwable t) { + t.printStackTrace(); + failed = t; + } finally { client = null; - executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); - readerService.shutdown(); - readerService.awaitTermination(2000, TimeUnit.MILLISECONDS); + System.gc(); + try { + out.println("Awaiting executorService termination"); + executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); + out.println("Done"); + } catch (Throwable t) { + t.printStackTrace(); + if (failed != null) { + failed.addSuppressed(t); + } else failed = t; + } + var error = TRACKER.checkShutdown(2000); + if (error != null) { + out.println("Client hasn't shutdown properly: " + error); + if (failed != null) failed.addSuppressed(error); + else failed = error; + } + } + if (failed instanceof Exception fe) { + throw fe; + } else if (failed instanceof Error e) { + throw e; } } // -- Infrastructure + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + + static void shutdown(ExecutorService executorService) { + try { + executorService.shutdown(); + executorService.awaitTermination(2000, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + executorService.shutdownNow(); + } + } + @BeforeTest public void setup() throws Exception { out.println("\n**** Setup ****\n"); sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); + readerService = Executors.newCachedThreadPool(); httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new ServerRequestHandler(), "/http1/exec/"); @@ -332,10 +431,21 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -343,10 +453,13 @@ public class AsyncExecutorShutdown implements HttpServerAdapters { Thread.sleep(100); AssertionError fail = TRACKER.checkShutdown(5000); try { + shutdown(readerService); httpTestServer.stop(); httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/AsyncShutdownNow.java b/test/jdk/java/net/httpclient/AsyncShutdownNow.java index 39c82735248..2617b60ee1c 100644 --- a/test/jdk/java/net/httpclient/AsyncShutdownNow.java +++ b/test/jdk/java/net/httpclient/AsyncShutdownNow.java @@ -45,7 +45,9 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -62,6 +64,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; + import jdk.httpclient.test.lib.common.HttpServerAdapters; import javax.net.ssl.SSLContext; @@ -72,11 +75,14 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -96,10 +102,15 @@ public class AsyncShutdownNow implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "AsyncShutdownNow message body"; static final int ITERATIONS = 3; @@ -107,10 +118,12 @@ public class AsyncShutdownNow implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -124,14 +137,57 @@ public class AsyncShutdownNow implements HttpServerAdapters { return t; } - static String readBody(InputStream in) { - try { + static String readBody(InputStream body) { + try (InputStream in = body) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); } } + record ExchangeResult(int step, + Version version, + Http3DiscoveryMode config, + HttpResponse response, + boolean firstVersionMayNotMatch) { + + static ExchangeResult afterHead(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, false); + } + + static ExchangeResult ofSequential(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, true); + } + + ExchangeResult withResponse(HttpResponse response) { + return new ExchangeResult(step(), version(), config(), response, firstVersionMayNotMatch()); + } + + // Ensures that the input stream gets closed in case of assertion + ExchangeResult assertResponseState() { + out.println(step + ": Got response: " + response); + out.printf("%s: expect status 200 and version %s (%s) for %s%n", step, version, config, + response.request().uri()); + assertEquals(response.statusCode(), 200); + if (step == 0 && version == HTTP_3 && firstVersionMayNotMatch) { + out.printf("%s: version not checked%n", step); + } else { + assertEquals(response.version(), version); + out.printf("%s: got expected version %s%n", step, response.version()); + } + return this; + } + } + + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + static boolean hasExpectedMessage(IOException io) { String message = io.getMessage(); if (message == null) return false; @@ -168,11 +224,12 @@ public class AsyncShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting concurrent (%s) ----%n%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting concurrent (%s, %s, %s) ----%n%n", uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -181,21 +238,25 @@ public class AsyncShutdownNow implements HttpServerAdapters { Throwable failed = null; List> bodies = new ArrayList<>(); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; CompletableFuture bodyCF; final int si = i; + ExchangeResult result = ExchangeResult.afterHead(si, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) - .thenApply((response) -> { - out.println(si + ": Got response: " + response); - assertEquals(response.statusCode(), 200); - return response; - }); + .thenApply(result::withResponse) + .thenApplyAsync(ExchangeResult::assertResponseState, readerService) + .thenApply(ExchangeResult::response); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(AsyncShutdownNow::readBody) .thenApply((s) -> { @@ -260,11 +321,13 @@ public class AsyncShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting sequential (%s) ----%n%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting sequential (%s, %s, %s) ----%n%n", + uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -277,17 +340,17 @@ public class AsyncShutdownNow implements HttpServerAdapters { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); final int si = i; + ExchangeResult result = ExchangeResult.ofSequential(si, version, config); CompletableFuture> responseCF; CompletableFuture bodyCF; responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) - .thenApply((response) -> { - out.println(si + ": Got response: " + response); - assertEquals(response.statusCode(), 200); - return response; - }); + .thenApply(result::withResponse) + .thenApplyAsync(ExchangeResult::assertResponseState, readerService) + .thenApply(ExchangeResult::response); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(AsyncShutdownNow::readBody) .thenApply((s) -> { @@ -362,10 +425,21 @@ public class AsyncShutdownNow implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -378,6 +452,8 @@ public class AsyncShutdownNow implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/AuthFilterCacheTest.java b/test/jdk/java/net/httpclient/AuthFilterCacheTest.java index e819f96d947..32026db57fb 100644 --- a/test/jdk/java/net/httpclient/AuthFilterCacheTest.java +++ b/test/jdk/java/net/httpclient/AuthFilterCacheTest.java @@ -26,14 +26,18 @@ import java.net.*; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.Collectors; + import jdk.httpclient.test.lib.common.HttpServerAdapters; -import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsServer; import jdk.httpclient.test.lib.common.TestServerConfigurator; import org.testng.annotations.AfterClass; @@ -45,6 +49,12 @@ import javax.net.ssl.SSLContext; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; /* * @test @@ -52,9 +62,9 @@ import static java.net.http.HttpClient.Version.HTTP_2; * @summary AuthenticationFilter.Cache::remove may throw ConcurrentModificationException * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext - * DigestEchoServer jdk.httpclient.test.lib.common.TestServerConfigurator + * DigestEchoServer ReferenceTracker jdk.httpclient.test.lib.common.TestServerConfigurator * @run testng/othervm -Dtest.requiresHost=true - * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.httpclient.HttpClient.log=requests,headers,errors,quic * -Djdk.internal.httpclient.debug=false * AuthFilterCacheTest */ @@ -63,7 +73,7 @@ public class AuthFilterCacheTest implements HttpServerAdapters { static final String RESPONSE_BODY = "Hello World!"; static final int REQUEST_COUNT = 5; - static final int URI_COUNT = 6; + static final int URI_COUNT = 8; static final CyclicBarrier barrier = new CyclicBarrier(REQUEST_COUNT * URI_COUNT); static final SSLContext context; @@ -80,11 +90,15 @@ public class AuthFilterCacheTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer h3onlyServer; + HttpTestServer h3altSvcServer; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI h3onlyURI; + URI h3altSvcURI; InetSocketAddress proxyAddress; ProxySelector proxySelector; MyAuthenticator auth; @@ -101,14 +115,15 @@ public class AuthFilterCacheTest implements HttpServerAdapters { https1URI.resolve("proxy/orig/"), http2URI.resolve("direct/orig/"), https2URI.resolve("direct/orig/"), - https2URI.resolve("proxy/orig/"))} + https2URI.resolve("proxy/orig/"), + h3onlyURI.resolve("direct/orig/"), + h3altSvcURI.resolve("direct/orig/"))} }; return uris; } public HttpClient newHttpClient(ProxySelector ps, Authenticator auth) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .executor(virtualExecutor) .sslContext(context) .authenticator(auth) @@ -154,12 +169,34 @@ public class AuthFilterCacheTest implements HttpServerAdapters { https2URI = new URI("https://" + https2Server.serverAuthority() + "/AuthFilterCacheTest/https2/"); + h3onlyServer = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + h3onlyServer.addHandler(new TestHandler(), "/AuthFilterCacheTest/h3-only/"); + h3onlyURI = new URI("https://" + h3onlyServer.serverAuthority() + + "/AuthFilterCacheTest/h3-only/"); + h3onlyServer.start(); + + h3altSvcServer = HttpTestServer.create(ANY, SSLContext.getDefault()); + h3altSvcServer.addHandler(new TestHandler(), "/AuthFilterCacheTest/h3-alt-svc/"); + h3altSvcServer.addHandler(new HttpHeadOrGetHandler(RESPONSE_BODY), + "/AuthFilterCacheTest/h3-alt-svc/direct/head/"); + h3altSvcURI = new URI("https://" + h3altSvcServer.serverAuthority() + + "/AuthFilterCacheTest/h3-alt-svc/"); + h3altSvcServer.start(); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); proxySelector = new HttpProxySelector(proxyAddress); client = newHttpClient(proxySelector, auth); + HttpRequest headRequest = HttpRequest.newBuilder(h3altSvcURI.resolve("direct/head/h2")) + .HEAD() + .version(HTTP_2).build(); + System.out.println("Sending head request: " + headRequest); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), HTTP_2); + System.out.println("Setup: done"); } catch (Exception x) { tearDown(); @@ -177,6 +214,8 @@ public class AuthFilterCacheTest implements HttpServerAdapters { https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + h3onlyServer = stop(h3onlyServer, HttpTestServer::stop); + h3altSvcServer = stop(h3altSvcServer, HttpTestServer::stop); client.close(); virtualExecutor.close(); @@ -229,6 +268,7 @@ public class AuthFilterCacheTest implements HttpServerAdapters { @Override public void handle(HttpTestExchange t) throws IOException { var count = respCounter.incrementAndGet(); + System.out.println("Server got request: " + t.getRequestURI()); System.out.println("Responses handled: " + count); t.getRequestBody().readAllBytes(); @@ -237,15 +277,19 @@ public class AuthFilterCacheTest implements HttpServerAdapters { t.getResponseHeaders() .addHeader("WWW-Authenticate", "Basic realm=\"Earth\""); t.sendResponseHeaders(401, 0); + System.out.println("Server sent 401 for " + t.getRequestURI()); } else { byte[] resp = RESPONSE_BODY.getBytes(StandardCharsets.UTF_8); t.sendResponseHeaders(200, resp.length); + System.out.println("Server sent 200 for " + t.getRequestURI() + "; awaiting barrier"); try { barrier.await(); } catch (Exception e) { + e.printStackTrace(); throw new IOException(e); } t.getResponseBody().write(resp); + System.out.println("Server sent body for " + t.getRequestURI()); } } t.close(); @@ -255,23 +299,54 @@ public class AuthFilterCacheTest implements HttpServerAdapters { void doClient(List uris) { assert uris.size() == URI_COUNT; barrier.reset(); - System.out.println("Client opening connection to: " + uris.toString()); + System.out.println("Client will connect " + REQUEST_COUNT + " times to: " + + uris.stream().map(URI::toString) + .collect(Collectors.joining("\n\t", "\n\t", "\n"))); List>> cfs = new ArrayList<>(); + int count = 0; for (int i = 0; i < REQUEST_COUNT; i++) { for (URI uri : uris) { - HttpRequest req = HttpRequest.newBuilder() - .uri(uri) - .build(); - cfs.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString())); + String uriStr = uri.toString() + (++count); + var builder = HttpRequest.newBuilder() + .uri(URI.create(uriStr)); + var config = uriStr.contains("h3-only") ? HTTP_3_URI_ONLY + : uriStr.contains("h3-alt-svc") ? ALT_SVC + : null; + if (config != null) { + builder = builder.setOption(H3_DISCOVERY, config).version(HTTP_3); + } else { + builder = builder.version(HTTP_2); + } + HttpRequest req = builder.build(); + System.out.printf("Sending request %s (version=%s, config=%s)%n", + req, req.version(), config); + cfs.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString()) + .handleAsync((r, t) -> logResponse(req, r, t)) + .thenCompose(Function.identity())); } } CompletableFuture.allOf(cfs.toArray(new CompletableFuture[0])).join(); } + CompletableFuture> logResponse(HttpRequest req, + HttpResponse resp, + Throwable t) { + if (t != null) { + System.out.printf("Request failed: %s (version=%s, config=%s): %s%n", + req, req.version(), req.getOption(H3_DISCOVERY).orElse(null), t); + t.printStackTrace(System.out); + return CompletableFuture.failedFuture(t); + } else { + System.out.printf("Request succeeded: %s (version=%s, config=%s): %s%n", + req, req.version(), req.getOption(H3_DISCOVERY).orElse(null), resp); + return CompletableFuture.completedFuture(resp); + } + } + static final class MyAuthenticator extends Authenticator { - private int count = 0; + private final AtomicInteger count = new AtomicInteger(); MyAuthenticator() { super(); @@ -294,7 +369,7 @@ public class AuthFilterCacheTest implements HttpServerAdapters { PasswordAuthentication passwordAuthentication; int count; synchronized (this) { - count = ++this.count; + count = this.count.incrementAndGet(); passwordAuthentication = super.requestPasswordAuthenticationInstance( host, addr, port, protocol, prompt, scheme, url, reqType); } @@ -304,13 +379,17 @@ public class AuthFilterCacheTest implements HttpServerAdapters { } public int getCount() { - return count; + return count.get(); } } @Test(dataProvider = "uris") public void test(List uris) throws Exception { - System.out.println("Server listening at " + uris.toString()); + System.out.println("Servers listening at " + + uris.stream().map(URI::toString) + .collect(Collectors.joining("\n\t", "\n\t", "\n"))); + System.out.println("h3-alt-svc server listening for h3 at: " + + h3altSvcServer.getH3AltService().map(s -> s.getAddress()).orElse(null)); doClient(uris); } } diff --git a/test/jdk/java/net/httpclient/BasicAuthTest.java b/test/jdk/java/net/httpclient/BasicAuthTest.java index e0aec3b19b7..35d5a7803d6 100644 --- a/test/jdk/java/net/httpclient/BasicAuthTest.java +++ b/test/jdk/java/net/httpclient/BasicAuthTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,6 +49,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.net.ssl.SSLContext; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; public class BasicAuthTest implements HttpServerAdapters { @@ -62,13 +63,15 @@ public class BasicAuthTest implements HttpServerAdapters { test(Version.HTTP_2, false); test(Version.HTTP_1_1, true); test(Version.HTTP_2, true); + test(Version.HTTP_3, true); } public static void test(Version version, boolean secure) throws Exception { ExecutorService e = Executors.newCachedThreadPool(); Handler h = new Handler(); - SSLContext sslContext = secure ? new SimpleSSLContext().get() : null; + SSLContext sslContext = secure || version == Version.HTTP_3 + ? new SimpleSSLContext().get() : null; HttpTestServer server = HttpTestServer.create(version, sslContext, e); HttpTestContext serverContext = server.addHandler(h,"/test/"); ServerAuth sa = new ServerAuth("foo realm"); @@ -77,7 +80,7 @@ public class BasicAuthTest implements HttpServerAdapters { System.out.println("Server auth = " + server.serverAuthority()); ClientAuth ca = new ClientAuth(); - var clientBuilder = HttpClient.newBuilder(); + var clientBuilder = HttpServerAdapters.createClientBuilderForH3(); if (sslContext != null) clientBuilder.sslContext(sslContext); HttpClient client = clientBuilder.authenticator(ca).build(); @@ -85,6 +88,11 @@ public class BasicAuthTest implements HttpServerAdapters { String scheme = sslContext == null ? "http" : "https"; URI uri = new URI(scheme + "://" + server.serverAuthority() + "/test/foo/"+version); var builder = HttpRequest.newBuilder(uri); + if (version == Version.HTTP_3) { + builder.version(version); + var config = server.h3DiscoveryConfig(); + builder.setOption(H3_DISCOVERY, server.h3DiscoveryConfig()); + } HttpRequest req = builder.copy().GET().build(); System.out.println("\n\nSending request: " + req); diff --git a/test/jdk/java/net/httpclient/BasicHTTP2Test.java b/test/jdk/java/net/httpclient/BasicHTTP2Test.java new file mode 100644 index 00000000000..65bcf7631ea --- /dev/null +++ b/test/jdk/java/net/httpclient/BasicHTTP2Test.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * ReferenceTracker + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * BasicHTTP2Test + * @summary Basic HTTP/2 test when HTTP/3 is requested + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.DatagramSocket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.lang.System.out; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class BasicHTTP2Test implements HttpServerAdapters { + + SSLContext sslContext; + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + String https2URI; + DatagramSocket udp; + + // a shared executor helps reduce the amount of threads created by the test + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private volatile HttpClient sharedClient; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static final void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.entrySet().forEach((e) -> { + out.printf("\t%s: %s%n", e.getKey(), e.getValue()); + e.getValue().printStackTrace(out); + e.getValue().printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + https2URI, + }; + } + + static AtomicLong URICOUNT = new AtomicLong(); + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = HttpClient.newBuilder() + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + + @Test + public void testH2() throws Exception { + + System.err.println("XXXXX ====== xxxxx first xxxxx ====== XXXXX"); + HttpClient client = makeNewClient(); + URI uri = URI.create(https2URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(Version.HTTP_3) + .GET(); + if (udp == null) { + out.println("Using config " + ALT_SVC); + builder.setOption(H3_DISCOVERY, ALT_SVC); + } + HttpRequest request = builder.build(); + + // send first request: that will go through regular HTTP/2. + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_2, "first response version"); + + Thread.sleep(1000); + + // send second request: we still will not find an endpoint in the + // AltServicesRegistry and will send everything to an + // HTTP/2 connection + System.err.println("XXXXX ====== xxxxx second xxxxx ====== XXXXX"); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_2, "second response version"); + + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + // HTTP/2 + HttpTestHandler handler = new Handler(); + HttpTestHandler h3Handler = new Handler(); + + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); + https2TestServer.addHandler(handler, "/https2/test204/"); + https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test204/x"; + try { + // attempt to prevent any other server/process to use this port + // for UDP + udp = new DatagramSocket(https2TestServer.getAddress()); + } catch (Exception x) { + System.out.println("Failed to allocate UDP socket at: " + https2TestServer.getAddress()); + udp = null; + } + + serverCount.addAndGet(1); + https2TestServer.start(); + } + + @AfterTest + public void teardown() throws Exception { + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + if (udp != null) udp.close(); + https2TestServer.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + + public Handler() {} + + volatile int invocation = 0; + + @Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + String type = uri.getScheme().toLowerCase(); + InputStream is = t.getRequestBody(); + while (is.read() != -1); + is.close(); + + + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + OutputStream os = t.getResponseBody(); + os.close(); + } else { + System.err.printf("Server sending %d - 0 length\n", 200); + t.sendResponseHeaders(200, 0); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/BasicHTTP3Test.java b/test/jdk/java/net/httpclient/BasicHTTP3Test.java new file mode 100644 index 00000000000..bd31b932b80 --- /dev/null +++ b/test/jdk/java/net/httpclient/BasicHTTP3Test.java @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +import static java.lang.System.out; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; + + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * ReferenceTracker + * jdk.httpclient.test.lib.quic.QuicStandaloneServer + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djavax.net.debug=all + * BasicHTTP3Test + * @summary Basic HTTP/3 test + */ +public class BasicHTTP3Test implements HttpServerAdapters { + + SSLContext sslContext; + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + String https2URI; + HttpTestServer h3TestServer; // HTTP/2 ( h2 + h3) + String h3URI; + HttpTestServer h3qv2TestServer; // HTTP/2 ( h2 + h3 on Quic v2, incompatible nego) + String h3URIQv2; + HttpTestServer h3qv2CTestServer; // HTTP/2 ( h2 + h3 on Quic v2, compatible nego) + String h3URIQv2C; + HttpTestServer h3mtlsTestServer; // HTTP/2 ( h2 + h3), h3 requires client cert + String h3mtlsURI; + HttpTestServer h3TestServerWithRetry; // h3 + String h3URIRetry; + HttpTestServer h3TestServerWithTLSHelloRetry; // h3 + String h3URITLSHelloRetry; + + static final int ITERATION_COUNT = 4; + // a shared executor helps reduce the amount of threads created by the test + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private volatile HttpClient sharedClient; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static final void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d clients%n", + now(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.entrySet().forEach((e) -> { + out.printf("\t%s: %s%n", e.getKey(), e.getValue()); + e.getValue().printStackTrace(out); + e.getValue().printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + https2URI, + h3URI + }; + } + + @DataProvider(name = "variants") + public Object[][] variants(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + String[] uris = uris(); + Object[][] result = new Object[uris.length * 2 * 2][]; + int i = 0; + for (var version : List.of(Optional.empty(), Optional.of(Version.HTTP_3))) { + for (boolean sameClient : List.of(false, true)) { + for (String uri : uris()) { + result[i++] = new Object[]{uri, sameClient, version}; + } + } + } + assert i == uris.length * 2 * 2; + return result; + } + + @DataProvider(name = "h3URIs") + public Object[][] versions(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = { + {h3URI}, {h3URIRetry}, + {h3URIQv2}, {h3URIQv2C}, + {h3mtlsURI}, {h3URITLSHelloRetry}, + }; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + @Test(dataProvider = "variants") + public void test(String uri, boolean sameClient, Optional version) throws Exception { + System.out.println("Request to " + uri); + + HttpClient client = newHttpClient(sameClient); + + Builder builder = HttpRequest.newBuilder(URI.create(uri)) + .GET(); + version.ifPresent(builder::version); + for (int i = 0; i < ITERATION_COUNT; i++) { + // don't want to attempt direct connection as there could be another + // HTTP/3 endpoint listening at the URI port. + // sameClient should be fine because version.empty() should + // have come first and populated alt-services. + builder.setOption(H3_DISCOVERY, ALT_SVC); + HttpRequest request = builder.build(); + System.out.println("Iteration: " + i); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response: " + response); + out.println("Version: " + response.version()); + int expectedResponse = 200; + if (response.statusCode() != expectedResponse) + throw new RuntimeException("wrong response code " + response.statusCode()); + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + System.out.println("test: DONE"); + } + + @Test(dataProvider = "h3URIs") + public void testH3(final String h3URI) throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_2) + .GET(); + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_2, "first response version"); + + request = builder.version(Version.HTTP_3).build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), Version.HTTP_3, "second response version"); + + if (h3URI == h3mtlsURI) { + assertNotNull(response.sslSession().get().getLocalCertificates()); + } else { + assertNull(response.sslSession().get().getLocalCertificates()); + } + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + // verify that the client handles HTTP/3 reset stream correctly + @Test + public void testH3Reset() throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_2) + .GET(); + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_2, "first response version"); + + // instruct the server side handler to throw an exception + // that then causes the test server to reset the stream + final String resetCausingURI = h3URI + "?handlerShouldThrow=true"; + builder = HttpRequest.newBuilder(URI.create(resetCausingURI)) + .GET(); + request = builder.version(Version.HTTP_3) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + try { + response = client.send(request, BodyHandlers.ofString()); + throw new RuntimeException("Unexpectedly received a response instead of an exception," + + " response: " + response); + } catch (IOException e) { + final String msg = e.getMessage(); + if (msg == null || !msg.contains("reset by peer")) { + // unexpected message in the exception, propagate the exception + throw e; + } + } + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); + https2TestServer.addHandler(new Handler(), "/https2/test/"); + https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test/x"; + + // A HTTP2 server with H3 enabled on a different host:port than the HTTP2 server + h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + final HttpTestHandler h3Handler = new Handler(); + h3TestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/h3"; + assertTrue(h3TestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 server with H3 QUICv2 enabled on a different host:port than the HTTP2 server + final Http2TestServer h2q2Server = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnEphemeralPortWithVersion(QuicVersion.QUIC_V2, false); + h3qv2TestServer = HttpTestServer.of(h2q2Server); + h3qv2TestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URIQv2 = "https://" + h3qv2TestServer.serverAuthority() + "/h3/testH3/h3qv2";; + assertTrue(h3qv2TestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 server with H3 QUICv2 compatible negotiation enabled on a different host:port than the HTTP2 server + final Http2TestServer h2q2CServer = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnEphemeralPortWithVersion(QuicVersion.QUIC_V2, true); + h3qv2CTestServer = HttpTestServer.of(h2q2CServer); + h3qv2CTestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URIQv2C = "https://" + h3qv2CTestServer.serverAuthority() + "/h3/testH3/h3qv2c";; + assertTrue(h3qv2CTestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 server with H3 enabled on a different host:port than the HTTP2 server + // H3 server requires the client to authenticate with a certificate + h3mtlsTestServer = HttpTestServer.create(HTTP_3, sslContext); + h3mtlsTestServer.addHandler(h3Handler, "/h3/testH3/"); + h3mtlsTestServer.getH3AltService().get().getQuicServer().setNeedClientAuth(true); + h3mtlsURI = "https://" + h3mtlsTestServer.serverAuthority() + "/h3/testH3/h3mtls"; + assertTrue(h3mtlsTestServer.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + // A HTTP2 test server with H3 alt service listening on different host:port + // and the underlying quic server for H3 is configured to send a RETRY packet + final Http2TestServer h2Server = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnEphemeralPort(); + // configure send retry on QUIC server + h2Server.getH3AltService().get().getQuicServer().sendRetry(true); + h3TestServerWithRetry = HttpTestServer.of(h2Server); + h3TestServerWithRetry.addHandler(h3Handler, "/h3/testH3Retry/"); + h3URIRetry = "https://" + h3TestServerWithRetry.serverAuthority() + "/h3/testH3Retry/x"; + + // A HTTP2 server with H3 enabled on a different host:port than the HTTP2 server + // TLS server rejects X25519 and secp256r1 key shares, + // which forces a hello retry at the moment of writing this test. + h3TestServerWithTLSHelloRetry = HttpTestServer.create(HTTP_3, sslContext); + h3TestServerWithTLSHelloRetry.addHandler(h3Handler, "/h3/testH3tlsretry/"); + h3TestServerWithTLSHelloRetry.getH3AltService().get().getQuicServer().setRejectKeyAgreement(Set.of("x25519", "secp256r1")); + h3URITLSHelloRetry = "https://" + h3TestServerWithTLSHelloRetry.serverAuthority() + "/h3/testH3tlsretry/x"; + assertTrue(h3TestServerWithTLSHelloRetry.canHandle(HTTP_2, Version.HTTP_3), "Server was expected" + + " to handle both HTTP2 and HTTP3, but doesn't"); + + https2TestServer.start(); + h3TestServer.start(); + h3qv2TestServer.start(); + h3qv2CTestServer.start(); + h3mtlsTestServer.start(); + h3TestServerWithRetry.start(); + h3TestServerWithTLSHelloRetry.start(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + https2TestServer.stop(); + h3TestServer.stop(); + h3qv2CTestServer.stop(); + h3qv2TestServer.stop(); + h3mtlsTestServer.stop(); + h3TestServerWithRetry.stop(); + h3TestServerWithTLSHelloRetry.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + final String query = uri.getQuery(); + if (query != null && query.contains("handlerShouldThrow=true")) { + System.err.printf("intentionally throwing an exception for request %s\n", uri); + throw new RuntimeException("intentionally thrown by handler for request " + uri); + } + try (InputStream is = t.getRequestBody()) { + is.readAllBytes(); + } + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + OutputStream os = t.getResponseBody(); + os.close(); + } else { + System.err.printf("Server sending %d - 0 length\n", 200); + t.sendResponseHeaders(200, 0); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/BasicRedirectTest.java b/test/jdk/java/net/httpclient/BasicRedirectTest.java index 8ea1653b4d0..a19b1444ac6 100644 --- a/test/jdk/java/net/httpclient/BasicRedirectTest.java +++ b/test/jdk/java/net/httpclient/BasicRedirectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,25 +28,25 @@ * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext * @run testng/othervm * -Djdk.httpclient.HttpClient.log=trace,headers,requests + * -Djdk.internal.httpclient.debug=true * BasicRedirectTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import java.net.http.HttpResponse.BodyHandlers; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -56,11 +56,16 @@ import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; + public class BasicRedirectTest implements HttpServerAdapters { SSLContext sslContext; @@ -68,14 +73,21 @@ public class BasicRedirectTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; - String httpURIToMoreSecure; // redirects HTTP to HTTPS + String httpURIToMoreSecure; // redirects HTTP to HTTPS + String httpURIToH3MoreSecure; // redirects HTTP to HTTPS/3 String httpsURI; String httpsURIToLessSecure; // redirects HTTPS to HTTP String http2URI; String http2URIToMoreSecure; // redirects HTTP to HTTPS + String http2URIToH3MoreSecure; // redirects HTTP to HTTPS/3 String https2URI; String https2URIToLessSecure; // redirects HTTPS to HTTP + String https3URI; + String https3HeadURI; + String http3URIToLessSecure; // redirects HTTP3 to HTTP + String http3URIToH2cLessSecure; // redirects HTTP3 to h2c static final String MESSAGE = "Is fearr Gaeilge briste, na Bearla cliste"; static final int ITERATIONS = 3; @@ -83,31 +95,67 @@ public class BasicRedirectTest implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, Redirect.ALWAYS }, - { httpsURI, Redirect.ALWAYS }, - { http2URI, Redirect.ALWAYS }, - { https2URI, Redirect.ALWAYS }, - { httpURIToMoreSecure, Redirect.ALWAYS }, - { http2URIToMoreSecure, Redirect.ALWAYS }, - { httpsURIToLessSecure, Redirect.ALWAYS }, - { https2URIToLessSecure, Redirect.ALWAYS }, + { httpURI, Redirect.ALWAYS, Optional.empty() }, + { httpsURI, Redirect.ALWAYS, Optional.empty() }, + { http2URI, Redirect.ALWAYS, Optional.empty() }, + { https2URI, Redirect.ALWAYS, Optional.empty() }, + { https3URI, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { httpURIToMoreSecure, Redirect.ALWAYS, Optional.empty() }, + { httpURIToH3MoreSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { http2URIToMoreSecure, Redirect.ALWAYS, Optional.empty() }, + { http2URIToH3MoreSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { httpsURIToLessSecure, Redirect.ALWAYS, Optional.empty() }, + { https2URIToLessSecure, Redirect.ALWAYS, Optional.empty() }, + { http3URIToLessSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, + { http3URIToH2cLessSecure, Redirect.ALWAYS, Optional.of(HTTP_3) }, - { httpURI, Redirect.NORMAL }, - { httpsURI, Redirect.NORMAL }, - { http2URI, Redirect.NORMAL }, - { https2URI, Redirect.NORMAL }, - { httpURIToMoreSecure, Redirect.NORMAL }, - { http2URIToMoreSecure, Redirect.NORMAL }, + { httpURI, Redirect.NORMAL, Optional.empty() }, + { httpsURI, Redirect.NORMAL, Optional.empty() }, + { http2URI, Redirect.NORMAL, Optional.empty() }, + { https2URI, Redirect.NORMAL, Optional.empty() }, + { https3URI, Redirect.NORMAL, Optional.of(HTTP_3) }, + { httpURIToMoreSecure, Redirect.NORMAL, Optional.empty() }, + { http2URIToMoreSecure, Redirect.NORMAL, Optional.empty() }, + { httpURIToH3MoreSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, + { http2URIToH3MoreSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, }; } - @Test(dataProvider = "positive") - void test(String uriString, Redirect redirectPolicy) throws Exception { - out.printf("%n---- starting positive (%s, %s) ----%n", uriString, redirectPolicy); - HttpClient client = HttpClient.newBuilder() + HttpClient createClient(Redirect redirectPolicy, Optional version) throws Exception { + var clientBuilder = newClientBuilderForH3() .followRedirects(redirectPolicy) - .sslContext(sslContext) + .sslContext(sslContext); + HttpClient client = version.map(clientBuilder::version) + .orElse(clientBuilder) .build(); + if (version.stream().anyMatch(HTTP_3::equals)) { + var builder = HttpRequest.newBuilder(URI.create(https3HeadURI)) + .setOption(H3_DISCOVERY, ALT_SVC); + var head = builder.copy().HEAD().version(HTTP_2).build(); + var get = builder.copy().GET().build(); + out.printf("%n---- sending initial head request (%s) -----%n", head.uri()); + var resp = client.send(head, BodyHandlers.ofString()); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.version(), HTTP_2); + out.println("HEADERS: " + resp.headers()); + var length = resp.headers().firstValueAsLong("Content-Length") + .orElseThrow(AssertionError::new); + if (length < 0) throw new AssertionError("negative length " + length); + out.printf("%n---- sending initial HTTP/3 GET request (%s) -----%n", get.uri()); + resp = client.send(get, BodyHandlers.ofString()); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.version(), HTTP_3); + assertEquals(resp.body().getBytes(UTF_8).length, length, + "body \"" + resp.body() + "\": "); + } + return client; + } + + @Test(dataProvider = "positive") + void test(String uriString, Redirect redirectPolicy, Optional clientVersion) throws Exception { + out.printf("%n---- starting positive (%s, %s, %s) ----%n", uriString, redirectPolicy, + clientVersion.map(Version::name).orElse("empty")); + HttpClient client = createClient(redirectPolicy, clientVersion); URI uri = URI.create(uriString); HttpRequest request = HttpRequest.newBuilder(uri).build(); @@ -125,20 +173,24 @@ public class BasicRedirectTest implements HttpServerAdapters { assertEquals(response.body(), MESSAGE); // asserts redirected URI in response.request().uri() assertTrue(response.uri().getPath().endsWith("message")); - assertPreviousRedirectResponses(request, response); + assertPreviousRedirectResponses(request, response, clientVersion); } } static void assertPreviousRedirectResponses(HttpRequest initialRequest, - HttpResponse finalResponse) { + HttpResponse finalResponse, + Optional clientVersion) { // there must be at least one previous response finalResponse.previousResponse() .orElseThrow(() -> new RuntimeException("no previous response")); HttpResponse response = finalResponse; + List versions = new ArrayList<>(); + versions.add(response.version()); do { URI uri = response.uri(); response = response.previousResponse().get(); + versions.add(response.version()); assertTrue(300 <= response.statusCode() && response.statusCode() <= 309, "Expected 300 <= code <= 309, got:" + response.statusCode()); assertEquals(response.body(), null, "Unexpected body: " + response.body()); @@ -153,6 +205,11 @@ public class BasicRedirectTest implements HttpServerAdapters { assertEquals(initialRequest, response.request(), String.format("Expected initial request [%s] to equal last prev req [%s]", initialRequest, response.request())); + if (clientVersion.stream().anyMatch(HTTP_3::equals)) { + out.println(versions.stream().map(Version::name) + .collect(Collectors.joining(" <-- ", "Redirects: ", ";"))); + assertTrue(versions.stream().anyMatch(HTTP_3::equals), "at least one version should be HTTP/3"); + } } // -- negatives @@ -160,27 +217,33 @@ public class BasicRedirectTest implements HttpServerAdapters { @DataProvider(name = "negative") public Object[][] negative() { return new Object[][] { - { httpURI, Redirect.NEVER }, - { httpsURI, Redirect.NEVER }, - { http2URI, Redirect.NEVER }, - { https2URI, Redirect.NEVER }, - { httpURIToMoreSecure, Redirect.NEVER }, - { http2URIToMoreSecure, Redirect.NEVER }, - { httpsURIToLessSecure, Redirect.NEVER }, - { https2URIToLessSecure, Redirect.NEVER }, + { httpURI, Redirect.NEVER, Optional.empty() }, + { httpsURI, Redirect.NEVER, Optional.empty() }, + { http2URI, Redirect.NEVER, Optional.empty() }, + { https2URI, Redirect.NEVER, Optional.empty() }, + { https3URI, Redirect.NEVER, Optional.of(HTTP_3) }, + { httpURIToMoreSecure, Redirect.NEVER, Optional.empty() }, + { http2URIToMoreSecure, Redirect.NEVER, Optional.empty() }, + { httpURIToH3MoreSecure, Redirect.NEVER, Optional.of(HTTP_3) }, + { http2URIToH3MoreSecure, Redirect.NEVER, Optional.of(HTTP_3) }, + { httpsURIToLessSecure, Redirect.NEVER, Optional.empty() }, + { https2URIToLessSecure, Redirect.NEVER, Optional.empty() }, + { http3URIToLessSecure, Redirect.NEVER, Optional.of(HTTP_3) }, + { http3URIToH2cLessSecure, Redirect.NEVER, Optional.of(HTTP_3) }, - { httpsURIToLessSecure, Redirect.NORMAL }, - { https2URIToLessSecure, Redirect.NORMAL }, + { httpsURIToLessSecure, Redirect.NORMAL, Optional.empty() }, + { https2URIToLessSecure, Redirect.NORMAL, Optional.empty() }, + { http3URIToLessSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, + { http3URIToH2cLessSecure, Redirect.NORMAL, Optional.of(HTTP_3) }, }; } @Test(dataProvider = "negative") - void testNegatives(String uriString,Redirect redirectPolicy) throws Exception { - out.printf("%n---- starting negative (%s, %s) ----%n", uriString, redirectPolicy); - HttpClient client = HttpClient.newBuilder() - .followRedirects(redirectPolicy) - .sslContext(sslContext) - .build(); + void testNegatives(String uriString, Redirect redirectPolicy, Optional clientVersion) + throws Exception { + out.printf("%n---- starting negative (%s, %s, %s) ----%n", uriString, redirectPolicy, + clientVersion.map(Version::name).orElse("empty")); + HttpClient client = createClient(redirectPolicy, clientVersion); URI uri = URI.create(uriString); HttpRequest request = HttpRequest.newBuilder(uri).build(); @@ -225,13 +288,25 @@ public class BasicRedirectTest implements HttpServerAdapters { https2TestServer.addHandler(new BasicHttpRedirectHandler(), "/https2/same/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/same/redirect"; + http3TestServer = HttpTestServer.create(ANY, sslContext); + http3TestServer.addHandler(new BasicHttpRedirectHandler(), "/http3/same/"); + https3URI = "https://" + http3TestServer.serverAuthority() + "/http3/same/redirect"; + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + https3HeadURI = "https://" + http3TestServer.serverAuthority() + "/http3/head"; + // HTTP to HTTPS redirect handler httpTestServer.addHandler(new ToSecureHttpRedirectHandler(httpsURI), "/http1/toSecure/"); httpURIToMoreSecure = "http://" + httpTestServer.serverAuthority()+ "/http1/toSecure/redirect"; + // HTTP to HTTP/3 redirect handler + httpTestServer.addHandler(new ToSecureHttpRedirectHandler(https3URI), "/http1/toSecureH3/"); + httpURIToH3MoreSecure = "http://" + httpTestServer.serverAuthority()+ "/http1/toSecureH3/redirect"; // HTTP2 to HTTP2S redirect handler http2TestServer.addHandler(new ToSecureHttpRedirectHandler(https2URI), "/http2/toSecure/"); http2URIToMoreSecure = "http://" + http2TestServer.serverAuthority() + "/http2/toSecure/redirect"; + // HTTP2 to HTTP2S redirect handler + http2TestServer.addHandler(new ToSecureHttpRedirectHandler(https3URI), "/http2/toSecureH3/"); + http2URIToH3MoreSecure = "http://" + http2TestServer.serverAuthority() + "/http2/toSecureH3/redirect"; // HTTPS to HTTP redirect handler httpsTestServer.addHandler(new ToLessSecureRedirectHandler(httpURI), "/https1/toLessSecure/"); @@ -239,11 +314,19 @@ public class BasicRedirectTest implements HttpServerAdapters { // HTTPS2 to HTTP2 redirect handler https2TestServer.addHandler(new ToLessSecureRedirectHandler(http2URI), "/https2/toLessSecure/"); https2URIToLessSecure = "https://" + https2TestServer.serverAuthority() + "/https2/toLessSecure/redirect"; + // HTTP3 to HTTP redirect handler + http3TestServer.addHandler(new ToLessSecureRedirectHandler(httpURI), "/http3/toLessSecure/"); + http3URIToLessSecure = "https://" + http3TestServer.serverAuthority() + "/http3/toLessSecure/redirect"; + // HTTP3 to HTTP2 redirect handler + http3TestServer.addHandler(new ToLessSecureRedirectHandler(http2URI), "/http3/toLessSecureH2/"); + http3URIToH2cLessSecure = "https://" + http3TestServer.serverAuthority() + "/http3/toLessSecureH2/redirect"; httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + createClient(Redirect.NEVER, Optional.of(HTTP_3)); } @AfterTest diff --git a/test/jdk/java/net/httpclient/CancelRequestTest.java b/test/jdk/java/net/httpclient/CancelRequestTest.java index 7851b112498..bfc1eff9cf9 100644 --- a/test/jdk/java/net/httpclient/CancelRequestTest.java +++ b/test/jdk/java/net/httpclient/CancelRequestTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,6 +49,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -57,6 +58,7 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -79,8 +81,9 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.System.out; import static java.lang.System.err; -import static java.net.http.HttpClient.Version.HTTP_1_1; -import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.*; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -95,10 +98,15 @@ public class CancelRequestTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final long SERVER_LATENCY = 75; static final int MAX_CLIENT_DELAY = 75; @@ -200,6 +208,8 @@ public class CancelRequestTest implements HttpServerAdapters { httpsURI, http2URI, https2URI, + h2h3URI, + h3URI, }; } @@ -245,7 +255,7 @@ public class CancelRequestTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -265,6 +275,17 @@ public class CancelRequestTest implements HttpServerAdapters { } } + // set HTTP/3 version on the request when targeting + // an HTTP/3 server + private HttpRequest.Builder requestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (uri.contains("h3")) { + builder.version(HTTP_3); + } + return builder; + } + + final static String BODY = "Some string | that ? can | be split ? several | ways."; // should accept SSLHandshakeException because of the connectionAborter @@ -273,8 +294,12 @@ public class CancelRequestTest implements HttpServerAdapters { // rewrap in "Request Cancelled" when the multi exchange was aborted... private static boolean isCancelled(Throwable t) { while (t instanceof ExecutionException) t = t.getCause(); - if (t instanceof CancellationException) return true; - if (t instanceof IOException) return String.valueOf(t).contains("Request cancelled"); + Throwable cause = t; + while (cause != null) { + if (cause instanceof CancellationException) return true; + if (cause instanceof IOException && String.valueOf(cause).contains("Request cancelled")) return true; + cause = cause.getCause(); + } out.println("Not a cancellation exception: " + t); t.printStackTrace(out); return false; @@ -290,6 +315,15 @@ public class CancelRequestTest implements HttpServerAdapters { } } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @Test(dataProvider = "asyncurls") public void testGetSendAsync(String uri, boolean sameClient, boolean mayInterruptIfRunning) throws Exception { @@ -302,14 +336,19 @@ public class CancelRequestTest implements HttpServerAdapters { client = newHttpClient(sameClient); Tracker tracker = TRACKER.getTracker(client); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + // Populate alt-svc registry with h3 service + if (uri.contains("h2h3")) headRequest(client); + Http3DiscoveryMode config = uri.contains("h3-only") ? h3TestServer.h3DiscoveryConfig() : null; + HttpRequest req = requestBuilder(uri) .GET() + .setOption(H3_DISCOVERY, config) .build(); BodyHandler handler = BodyHandlers.ofString(); CountDownLatch latch = new CountDownLatch(1); CompletableFuture> response = client.sendAsync(req, handler); var cf1 = response.whenComplete((r,t) -> System.out.println(t)); CompletableFuture> cf2 = cf1.whenComplete((r,t) -> latch.countDown()); + out.println("iteration: " + i + ", req: " + req.uri()); out.println("response: " + response); out.println("cf1: " + cf1); out.println("cf2: " + cf2); @@ -352,10 +391,12 @@ public class CancelRequestTest implements HttpServerAdapters { Throwable wrapped = x.getCause(); Throwable cause = wrapped; if (mayInterruptIfRunning) { - assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass()), - "Unexpected exception: " + wrapped); - cause = wrapped.getCause(); - out.println("CancellationException cause: " + x); + if (CancellationException.class.isAssignableFrom(wrapped.getClass())) { + cause = wrapped.getCause(); + out.println("CancellationException cause: " + x); + } else if (!isCancelled(cause)) { + throw new RuntimeException("Unexpected cause: " + cause); + } if (cause instanceof HttpConnectTimeoutException) { cause.printStackTrace(out); throw new RuntimeException("Unexpected timeout exception", cause); @@ -426,8 +467,12 @@ public class CancelRequestTest implements HttpServerAdapters { } }; - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + // Populate alt-svc registry with h3 service + if (uri.contains("h2h3")) headRequest(client); + Http3DiscoveryMode config = uri.contains("h3-only") ? h3TestServer.h3DiscoveryConfig() : null; + HttpRequest req = requestBuilder(uri) .POST(HttpRequest.BodyPublishers.ofByteArrays(iterable)) + .setOption(H3_DISCOVERY, config) .build(); BodyHandler handler = BodyHandlers.ofString(); CountDownLatch latch = new CountDownLatch(1); @@ -473,8 +518,13 @@ public class CancelRequestTest implements HttpServerAdapters { } catch (ExecutionException x) { assertTrue(response.isDone()); Throwable wrapped = x.getCause(); - assertTrue(CancellationException.class.isAssignableFrom(wrapped.getClass())); - Throwable cause = wrapped.getCause(); + Throwable cause = wrapped; + if (CancellationException.class.isAssignableFrom(wrapped.getClass())) { + cause = wrapped.getCause(); + out.println("CancellationException cause: " + x); + } else if (!isCancelled(cause)) { + throw new RuntimeException("Unexpected cause: " + cause); + } assertTrue(IOException.class.isAssignableFrom(cause.getClass())); if (cause instanceof HttpConnectTimeoutException) { cause.printStackTrace(out); @@ -536,8 +586,12 @@ public class CancelRequestTest implements HttpServerAdapters { return List.of(BODY.getBytes(UTF_8)).iterator(); }; - HttpRequest req = HttpRequest.newBuilder(URI.create(uriStr)) + // Populate alt-svc registry with h3 service + if (uri.contains("h2h3")) headRequest(client); + Http3DiscoveryMode config = uri.contains("h3-only") ? h3TestServer.h3DiscoveryConfig() : null; + HttpRequest req = requestBuilder(uriStr) .POST(HttpRequest.BodyPublishers.ofByteArrays(iterable)) + .setOption(H3_DISCOVERY, config) .build(); String body = null; Exception failed = null; @@ -613,11 +667,24 @@ public class CancelRequestTest implements HttpServerAdapters { https2TestServer.addHandler(h2_chunkedHandler, "/https2/x/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x/"; - serverCount.addAndGet(4); + HttpTestHandler h3_chunkedHandler = new HTTPSlowHandler(); + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(h3_chunkedHandler, "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(h3_chunkedHandler, "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + + serverCount.addAndGet(6); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -632,6 +699,8 @@ public class CancelRequestTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java b/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java index e0199b18e68..e304168ba33 100644 --- a/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java +++ b/test/jdk/java/net/httpclient/CancelStreamedBodyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,11 +32,6 @@ * @run testng/othervm -Djdk.internal.httpclient.debug=true * CancelStreamedBodyTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; -import jdk.internal.net.http.common.OperationTrackers.Tracker; -import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -53,25 +48,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.Reference; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; import java.util.Arrays; -import java.util.Iterator; import java.util.List; -import java.util.Random; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; @@ -79,12 +64,13 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import static java.lang.System.arraycopy; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; @@ -97,10 +83,12 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String https3URI; static final long SERVER_LATENCY = 75; static final int ITERATION_COUNT = 3; @@ -198,6 +186,7 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { private String[] uris() { return new String[] { + https3URI, httpURI, httpsURI, http2URI, @@ -221,9 +210,9 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { return result; } - private HttpClient makeNewClient() { + private HttpClient makeNewClient(HttpClient.Builder builder) { clientCount.incrementAndGet(); - var client = HttpClient.newBuilder() + var client = builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -236,21 +225,45 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { return TRACKER.track(client); } - HttpClient newHttpClient(boolean share) { - if (!share) return makeNewClient(); + private Version version(String uri) { + if (uri == null) return null; + if (uri.contains("/http3/")) return HTTP_3; + if (uri.contains("/http2/")) return HTTP_2; + if (uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http1/")) return HTTP_1_1; + if (uri.contains("/https1/")) return HTTP_1_1; + return null; + } + + HttpClient makeNewClient(Version version) { + var builder = (version == HTTP_3) + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return makeNewClient(builder); + } + + HttpClient newHttpClient(boolean share, String uri) { + if (!share) return makeNewClient(version(uri)); HttpClient shared = sharedClient; if (shared != null) return shared; synchronized (this) { shared = sharedClient; if (shared == null) { - shared = sharedClient = makeNewClient(); + shared = sharedClient = makeNewClient(HTTP_3); } return shared; } } - final static String BODY = "Some string |\n that ?\n can |\n be split ?\n several |\n ways."; + HttpRequest.Builder requestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + var version = version(uri); + return version == HTTP_3 + ? builder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + : builder; + } + final static String BODY = "Some string |\n that ?\n can |\n be split ?\n several |\n ways."; @Test(dataProvider = "urls") public void testAsLines(String uri, boolean sameClient) @@ -261,10 +274,10 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { out.printf("%n%s testAsLines(%s, %b)%n", now(), uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(sameClient); + client = newHttpClient(sameClient, uri); var tracker = TRACKER.getTracker(client); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = requestBuilder(uri) .GET() .build(); List lines; @@ -302,10 +315,10 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { out.printf("%n%s testInputStream(%s, %b)%n", now(), uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(sameClient); + client = newHttpClient(sameClient, uri); var tracker = TRACKER.getTracker(client); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = requestBuilder(uri) .GET() .build(); int read = -1; @@ -318,7 +331,7 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { } // Only check our still alive client for outstanding operations // and outstanding subscribers here: it should have none. - var error = TRACKER.check(tracker, 1, + var error = TRACKER.check(tracker, 500, (t) -> t.getOutstandingOperations() > 0 || t.getOutstandingSubscribers() > 0, "subscribers for testInputStream(%s)\n\t step [%s,%s]".formatted(req.uri(), i,j), false); @@ -364,11 +377,16 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { https2TestServer.addHandler(h2_chunkedHandler, "/https2/x/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x/"; - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h2_chunkedHandler, "/http3/x/"); + https3URI = "https://" + http3TestServer.serverAuthority() + "/http3/x/"; + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -386,6 +404,7 @@ public class CancelStreamedBodyTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/http2/ExpectContinueResetTest.java b/test/jdk/java/net/httpclient/CancelledPartialResponseTest.java similarity index 52% rename from test/jdk/java/net/httpclient/http2/ExpectContinueResetTest.java rename to test/jdk/java/net/httpclient/CancelledPartialResponseTest.java index 5d09c01b96f..41493bbdaff 100644 --- a/test/jdk/java/net/httpclient/http2/ExpectContinueResetTest.java +++ b/test/jdk/java/net/httpclient/CancelledPartialResponseTest.java @@ -23,16 +23,19 @@ /* * @test - * @summary Verifies that the client reacts correctly to receiving RST_STREAM at various stages of - * a Partial Response. + * @summary Verifies that the client reacts correctly to receiving RST_STREAM or StopSendingFrame at various stages of + * a Partial/Expect-continue type Response for HTTP/2 and HTTP/3. * @bug 8309118 * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters - * @run testng/othervm/timeout=40 -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=trace,errors,headers - * ExpectContinueResetTest + * @run testng/othervm/timeout=40 -Djdk.internal.httpclient.debug=false -Djdk.httpclient.HttpClient.log=trace,errors,headers + * CancelledPartialResponseTest */ import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.http2.BodyOutputStream; import jdk.httpclient.test.lib.http2.Http2Handler; import jdk.httpclient.test.lib.http2.Http2TestExchange; @@ -42,12 +45,15 @@ import jdk.httpclient.test.lib.http2.Http2TestServerConnection; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.frame.ResetFrame; +import jdk.internal.net.http.http3.Http3Error; +import jdk.test.lib.net.SimpleSSLContext; import org.testng.TestException; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import java.io.IOException; import java.io.InputStream; @@ -55,47 +61,58 @@ import java.io.OutputStream; import java.io.PrintStream; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.util.Iterator; import java.util.concurrent.ExecutionException; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.testng.Assert.*; -public class ExpectContinueResetTest { +public class CancelledPartialResponseTest { Http2TestServer http2TestServer; + + HttpTestServer http3TestServer; + // "NoError" urls complete with an exception. "NoError" or "Error" here refers to the error code in the RST_STREAM frame // and not the outcome of the test. - URI warmup, partialResponseResetNoError, partialResponseResetError, fullResponseResetNoError, fullResponseResetError; + URI warmup, h2PartialResponseResetNoError, h2PartialResponseResetError, h2FullResponseResetNoError, h2FullResponseResetError; + URI h3PartialResponseStopSending, h3FullResponseStopSending; + + SSLContext sslContext; static PrintStream err = new PrintStream(System.err); static PrintStream out = System.out; + // TODO: Investigate further if checking against HTTP/3 Full Response is necessary @DataProvider(name = "testData") public Object[][] testData() { - // Not consuming the InputStream in the server's handler results in different handling of RST_STREAM client-side return new Object[][] { - { partialResponseResetNoError }, - { partialResponseResetError }, // Checks RST_STREAM is processed if client sees no END_STREAM - { fullResponseResetNoError }, - { fullResponseResetError } + { HTTP_2, h2PartialResponseResetNoError }, + { HTTP_2, h2PartialResponseResetError }, // Checks RST_STREAM is processed if client sees no END_STREAM + { HTTP_2, h2FullResponseResetNoError }, + { HTTP_2, h2FullResponseResetError }, + { HTTP_3, h3PartialResponseStopSending }, // All StopSending frames received by client throw exception regardless of code + { HTTP_3, h3FullResponseStopSending } }; } @Test(dataProvider = "testData") - public void test(URI uri) { - out.printf("\nTesting with Version: %s, URI: %s\n", HTTP_2, uri.toASCIIString()); - err.printf("\nTesting with Version: %s, URI: %s\n", HTTP_2, uri.toASCIIString()); + public void test(Version version, URI uri) { + out.printf("\nTesting with Version: %s, URI: %s\n", version, uri.toASCIIString()); + err.printf("\nTesting with Version: %s, URI: %s\n", version, uri.toASCIIString()); Iterable iterable = EndlessDataChunks::new; HttpRequest.BodyPublisher testPub = HttpRequest.BodyPublishers.ofByteArrays(iterable); Exception expectedException = null; try { - performRequest(testPub, uri); + performRequest(version, testPub, uri); throw new AssertionError("Expected exception not raised for " + uri); } catch (Exception e) { expectedException = e; @@ -105,20 +122,37 @@ public class ExpectContinueResetTest { throw new AssertionError("Unexpected null cause for " + expectedException, expectedException); } - assertEquals(testThrowable.getClass(), IOException.class, - "Test should have closed with an IOException"); - testThrowable.printStackTrace(); + if (!(testThrowable instanceof IOException)) { + throw new AssertionError( + "Test should have closed with an IOException, got: " + testThrowable, + testThrowable); + } + if (version == HTTP_3) { + if (testThrowable.getMessage().contains(Http3Error.H3_EXCESSIVE_LOAD.name())) { + System.out.println("Got expected message: " + testThrowable.getMessage()); + } else { + throw new AssertionError("Expected " + Http3Error.H3_EXCESSIVE_LOAD.name() + + ", got " + testThrowable, testThrowable); + } + } } static public class EndlessDataChunks implements Iterator { - byte[] data = new byte[16]; + byte[] data = new byte[32]; + boolean hasNext = true; @Override public boolean hasNext() { - return true; + return hasNext; } @Override public byte[] next() { + try { + Thread.sleep(2500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + hasNext = false; return data; } @Override @@ -129,20 +163,32 @@ public class ExpectContinueResetTest { @BeforeTest public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + http2TestServer = new Http2TestServer(false, 0); + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http2TestServer.setExchangeSupplier(ExpectContinueResetTestExchangeImpl::new); http2TestServer.addHandler(new GetHandler().toHttp2Handler(), "/warmup"); - http2TestServer.addHandler(new NoEndStreamOnPartialResponse(), "/partialResponse/codeNoError"); - http2TestServer.addHandler(new NoEndStreamOnPartialResponse(), "/partialResponse/codeError"); - http2TestServer.addHandler(new NoEndStreamOnFullResponse(), "/fullResponse/codeNoError"); - http2TestServer.addHandler(new NoEndStreamOnFullResponse(), "/fullResponse/codeError"); + http2TestServer.addHandler(new PartialResponseResetStreamH2(), "/partialResponse/codeNoError"); + http2TestServer.addHandler(new PartialResponseResetStreamH2(), "/partialResponse/codeError"); + http2TestServer.addHandler(new FullResponseResetStreamH2(), "/fullResponse/codeNoError"); + http2TestServer.addHandler(new FullResponseResetStreamH2(), "/fullResponse/codeError"); + http3TestServer.addHandler(new PartialResponseStopSendingH3(), "/partialResponse/codeNoError"); + http3TestServer.addHandler(new FullResponseStopSendingH3(), "/fullResponse/codeNoError"); warmup = URI.create("http://" + http2TestServer.serverAuthority() + "/warmup"); - partialResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeNoError"); - partialResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeError"); - fullResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeNoError"); - fullResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeError"); + h2PartialResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeNoError"); + h2PartialResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/partialResponse/codeError"); + h2FullResponseResetNoError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeNoError"); + h2FullResponseResetError = URI.create("http://" + http2TestServer.serverAuthority() + "/fullResponse/codeError"); + h3PartialResponseStopSending = URI.create("https://" + http3TestServer.serverAuthority() + "/partialResponse/codeNoError"); + h3FullResponseStopSending = URI.create("https://" + http3TestServer.serverAuthority() + "/fullResponse/codeNoError"); + http2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -150,26 +196,38 @@ public class ExpectContinueResetTest { http2TestServer.stop(); } - private void performRequest(HttpRequest.BodyPublisher bodyPublisher, URI uri) + private void performRequest(Version version, HttpRequest.BodyPublisher bodyPublisher, URI uri) throws IOException, InterruptedException, ExecutionException { - try (HttpClient client = HttpClient.newBuilder().proxy(HttpClient.Builder.NO_PROXY).version(HTTP_2).build()) { + + HttpClient.Builder builder = HttpServerAdapters.createClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(version) + .sslContext(sslContext); + Http3DiscoveryMode requestConfig = null; + if (version == HTTP_3) + requestConfig = Http3DiscoveryMode.HTTP_3_URI_ONLY; + + try (HttpClient client = builder.build()) { err.printf("Performing warmup request to %s", warmup); - client.send(HttpRequest.newBuilder(warmup).GET().version(HTTP_2).build(), HttpResponse.BodyHandlers.discarding()); + if (version == HTTP_2) + client.send(HttpRequest.newBuilder(warmup).GET().version(HTTP_2).build(), + HttpResponse.BodyHandlers.discarding()); + HttpRequest postRequest = HttpRequest.newBuilder(uri) - .version(HTTP_2) + .version(version) .POST(bodyPublisher) + .setOption(H3_DISCOVERY, requestConfig) .expectContinue(true) .build(); - err.printf("Sending request (%s): %s%n", HTTP_2, postRequest); - // TODO: when test is stable and complete, see then if fromSubscriber makes our subscriber non null + err.printf("Sending request (%s): %s%n", version, postRequest); client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString()).get(); } } - static class GetHandler implements HttpServerAdapters.HttpTestHandler { + static class GetHandler implements HttpTestHandler { @Override - public void handle(HttpServerAdapters.HttpTestExchange exchange) throws IOException { + public void handle(HttpTestExchange exchange) throws IOException { try (OutputStream os = exchange.getResponseBody()) { byte[] bytes = "Response Body".getBytes(UTF_8); err.printf("Server sending 200 (length=%s)", bytes.length); @@ -180,7 +238,7 @@ public class ExpectContinueResetTest { } } - static class NoEndStreamOnPartialResponse implements Http2Handler { + static class PartialResponseResetStreamH2 implements Http2Handler { @Override public void handle(Http2TestExchange exchange) throws IOException { @@ -200,14 +258,14 @@ public class ExpectContinueResetTest { } } - static class NoEndStreamOnFullResponse implements Http2Handler { + static class FullResponseResetStreamH2 implements Http2Handler { @Override public void handle(Http2TestExchange exchange) throws IOException { err.println("Sending 100"); exchange.sendResponseHeaders(100, -1); - err.println("Sending 200"); + err.println("Sending 200"); exchange.sendResponseHeaders(200, 0); if (exchange instanceof ExpectContinueResetTestExchangeImpl testExchange) { err.println("Sending Reset"); @@ -222,6 +280,38 @@ public class ExpectContinueResetTest { } } + static class PartialResponseStopSendingH3 implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange exchange) throws IOException { + err.println("Sending 100"); + exchange.sendResponseHeaders(100, 0); + // sending StopSending(NO_ERROR) before or after sending 100 with no data + // should not fail. + System.err.println("Sending StopSendingFrame"); + exchange.requestStopSending(Http3Error.H3_REQUEST_REJECTED.code()); + // Not resetting the stream would cause the client to wait forever + exchange.resetStream(Http3Error.H3_EXCESSIVE_LOAD.code()); + } + } + + static class FullResponseStopSendingH3 implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange exchange) throws IOException { + err.println("Sending 100"); + exchange.sendResponseHeaders(100, 0); + err.println("Sending 200"); + + // sending StopSending before or after sending 200 with no data + // should not fail. + err.println("Sending StopSendingFrame"); + exchange.requestStopSending(Http3Error.H3_REQUEST_REJECTED.code()); + exchange.sendResponseHeaders(200, 10); + exchange.resetStream(Http3Error.H3_EXCESSIVE_LOAD.code()); + } + } + static class ExpectContinueResetTestExchangeImpl extends Http2TestExchangeImpl { public ExpectContinueResetTestExchangeImpl(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { diff --git a/test/jdk/java/net/httpclient/CancelledResponse.java b/test/jdk/java/net/httpclient/CancelledResponse.java index 44be38317dc..1f28ce87221 100644 --- a/test/jdk/java/net/httpclient/CancelledResponse.java +++ b/test/jdk/java/net/httpclient/CancelledResponse.java @@ -23,7 +23,6 @@ import java.net.http.HttpClient; import java.net.http.HttpClient.Version; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import jdk.test.lib.net.SimpleSSLContext; @@ -50,7 +49,10 @@ import java.net.http.HttpResponse.BodySubscriber; import static java.lang.String.format; import static java.lang.System.out; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; /** * @test @@ -164,7 +166,10 @@ public class CancelledResponse { server.start(); HttpClient client = newHttpClient(); - HttpRequest request = HttpRequest.newBuilder(uri).version(version).build(); + HttpRequest request = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, version == HTTP_3 ? ALT_SVC : null) + .version(version) + .build(); try { for (int i = 0; i < responses.length; i++) { HttpResponse r = null; diff --git a/test/jdk/java/net/httpclient/CancelledResponse2.java b/test/jdk/java/net/httpclient/CancelledResponse2.java index 263620acf81..dfcdb03f06e 100644 --- a/test/jdk/java/net/httpclient/CancelledResponse2.java +++ b/test/jdk/java/net/httpclient/CancelledResponse2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,8 +21,8 @@ * questions. */ -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.OperationTrackers.Tracker; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -38,6 +38,7 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.nio.ByteBuffer; @@ -51,8 +52,11 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static java.lang.System.out; -import static java.net.http.HttpClient.Version.*; -import static jdk.httpclient.test.lib.common.HttpServerAdapters.*; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -60,27 +64,36 @@ import static org.testng.Assert.assertTrue; * @test * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext + * @compile ReferenceTracker.java * @run testng/othervm -Djdk.internal.httpclient.debug=true CancelledResponse2 */ +// -Djdk.internal.httpclient.debug=true +public class CancelledResponse2 implements HttpServerAdapters { -public class CancelledResponse2 { - + private static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private static final Random RANDOM = RandomFactory.getRandom(); + private static final int MAX_CLIENT_DELAY = 160; HttpTestServer h2TestServer; URI h2TestServerURI; - private SSLContext sslContext; - private static final Random random = RandomFactory.getRandom(); - private static final int MAX_CLIENT_DELAY = 160; + URI h2h3TestServerURI; + URI h2h3HeadTestServerURI; + URI h3TestServerURI; + HttpTestServer h2h3TestServer; + HttpTestServer h3TestServer; + SSLContext sslContext; @DataProvider(name = "versions") public Object[][] positive() { return new Object[][]{ - { HTTP_2, h2TestServerURI }, + { HTTP_2, null, h2TestServerURI }, + { HTTP_3, null, h2h3TestServerURI }, + { HTTP_3, HTTP_3_URI_ONLY, h3TestServerURI }, }; } private static void delay() { - int delay = random.nextInt(MAX_CLIENT_DELAY); + int delay = RANDOM.nextInt(MAX_CLIENT_DELAY); try { System.out.println("client delay: " + delay); Thread.sleep(delay); @@ -88,13 +101,25 @@ public class CancelledResponse2 { out.println("Unexpected exception: " + x); } } - @Test(dataProvider = "versions") - public void test(Version version, URI uri) throws Exception { + public void test(Version version, Http3DiscoveryMode config, URI uri) throws Exception { for (int i = 0; i < 5; i++) { - HttpClient httpClient = HttpClient.newBuilder().sslContext(sslContext).version(version).build(); + HttpClient httpClient = newClientBuilderForH3().sslContext(sslContext).version(version).build(); + Http3DiscoveryMode reqConfig = null; + if (version.equals(HTTP_3)) { + if (config != null) { + reqConfig = (config.equals(HTTP_3_URI_ONLY)) ? HTTP_3_URI_ONLY : ALT_SVC; + } + // if config is null, we are talking to the H2H3 server, which may + // not support direct connection, in which case we should send a headRequest + if ((config == null && !h2h3TestServer.supportsH3DirectConnection()) + || (reqConfig != null && reqConfig.equals(ALT_SVC))) { + headRequest(httpClient); + } + } HttpRequest httpRequest = HttpRequest.newBuilder(uri) .version(version) + .setOption(H3_DISCOVERY, reqConfig) .GET() .build(); AtomicBoolean cancelled = new AtomicBoolean(); @@ -104,26 +129,56 @@ public class CancelledResponse2 { cf.get(); } catch (Exception e) { e.printStackTrace(); - assertTrue(e.getCause() instanceof IOException, "HTTP/2 should cancel with an IOException when the Subscription is cancelled."); + assertTrue(e.getCause() instanceof IOException, "HTTP/2 & HTTP/3 should cancel with an IOException when the Subscription is cancelled."); } assertTrue(cf.isCompletedExceptionally()); assertTrue(cancelled.get()); + + Tracker tracker = TRACKER.getTracker(httpClient); + httpClient = null; + var error = TRACKER.check(tracker, 5000); + if (error != null) throw error; } } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(h2h3HeadTestServerURI) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, HttpResponse.BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @BeforeTest public void setup() throws IOException { sslContext = new SimpleSSLContext().get(); + h2TestServer = HttpTestServer.create(HTTP_2, sslContext); h2TestServer.addHandler(new CancelledResponseHandler(), "/h2"); h2TestServerURI = URI.create("https://" + h2TestServer.serverAuthority() + "/h2"); + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new CancelledResponseHandler(), "/h2h3"); + h2h3TestServerURI = URI.create("https://" + h2h3TestServer.serverAuthority() + "/h2h3"); + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head"); + h2h3HeadTestServerURI = URI.create("https://" + h2h3TestServer.serverAuthority() + "/h2h3/head"); + + + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new CancelledResponseHandler(), "/h3"); + h3TestServerURI = URI.create("https://" + h3TestServer.serverAuthority() + "/h3"); + h2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest public void teardown() { h2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } BodyHandler ofString(String expected, AtomicBoolean cancelled) { @@ -234,6 +289,7 @@ public class CancelledResponse2 { } } + @Override public void onError(Throwable throwable) { result.completeExceptionally(throwable); diff --git a/test/jdk/java/net/httpclient/ConcurrentResponses.java b/test/jdk/java/net/httpclient/ConcurrentResponses.java index c68ebd0975c..1cabe461e80 100644 --- a/test/jdk/java/net/httpclient/ConcurrentResponses.java +++ b/test/jdk/java/net/httpclient/ConcurrentResponses.java @@ -30,16 +30,19 @@ * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.TestServerConfigurator * @run testng/othervm - * -Djdk.httpclient.HttpClient.log=headers,errors,channel + * -Djdk.internal.httpclient.debug=true * ConcurrentResponses */ +//* -Djdk.internal.httpclient.HttpClient.log=all import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; @@ -64,6 +67,9 @@ import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; @@ -73,6 +79,8 @@ import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; + +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpResponse.BodyHandlers.discarding; import static org.testng.Assert.assertEquals; @@ -86,8 +94,10 @@ public class ConcurrentResponses { HttpsServer httpsTestServer; // HTTPS/1.1 Http2TestServer http2TestServer; // HTTP/2 ( h2c ) Http2TestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer https3TestServer; String httpFixedURI, httpsFixedURI, httpChunkedURI, httpsChunkedURI; String http2FixedURI, https2FixedURI, http2VariableURI, https2VariableURI; + String https3FixedURI, https3VariableURI; static final int CONCURRENT_REQUESTS = 13; static final AtomicInteger IDS = new AtomicInteger(); @@ -145,7 +155,9 @@ public class ConcurrentResponses { { http2FixedURI }, { https2FixedURI }, { http2VariableURI }, - { https2VariableURI } + { https2VariableURI }, + { https3FixedURI }, + { https3VariableURI } }; } @@ -157,20 +169,25 @@ public class ConcurrentResponses { int id = IDS.getAndIncrement(); ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() .name("HttpClient-" + id + "-Worker", 0).factory()); - HttpClient client = HttpClient.newBuilder() - .sslContext(sslContext) + var http3 = uri.contains("/https3/"); + Http3DiscoveryMode config = http3 ? Http3DiscoveryMode.HTTP_3_URI_ONLY : null; + var builder = http3 ? HttpServerAdapters.createClientBuilderForH3() : HttpClient.newBuilder(); + if (http3) builder.version(Version.HTTP_3); + HttpClient client = builder .executor(virtualExecutor) - .build(); + .sslContext(sslContext).build(); try { Map requests = new HashMap<>(); for (int i = 0; i < CONCURRENT_REQUESTS; i++) { HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "?" + i)) + .setOption(H3_DISCOVERY, config) .build(); requests.put(request, BODIES[i]); } // initial connection to seed the cache so next parallel connections reuse it - client.sendAsync(HttpRequest.newBuilder(URI.create(uri)).build(), discarding()).join(); + client.sendAsync(HttpRequest.newBuilder(URI.create(uri)) + .setOption(H3_DISCOVERY, config).build(), discarding()).join(); // will reuse connection cached from the previous request ( when HTTP/2 ) CompletableFuture.allOf(requests.keySet().parallelStream() @@ -192,19 +209,25 @@ public class ConcurrentResponses { int id = IDS.getAndIncrement(); ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() .name("HttpClient-" + id + "-Worker", 0).factory()); - HttpClient client = HttpClient.newBuilder() + var http3 = uri.contains("/https3/"); + Http3DiscoveryMode config = http3 ? Http3DiscoveryMode.HTTP_3_URI_ONLY : null; + var builder = http3 ? HttpServerAdapters.createClientBuilderForH3() : HttpClient.newBuilder(); + if (http3) builder.version(Version.HTTP_3); + HttpClient client = builder .executor(virtualExecutor) .sslContext(sslContext).build(); try { Map requests = new HashMap<>(); for (int i = 0; i < CONCURRENT_REQUESTS; i++) { HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "?" + i)) + .setOption(H3_DISCOVERY, config) .build(); requests.put(request, BODIES[i]); } // initial connection to seed the cache so next parallel connections reuse it - client.sendAsync(HttpRequest.newBuilder(URI.create(uri)).build(), discarding()).join(); + client.sendAsync(HttpRequest.newBuilder(URI.create(uri)) + .setOption(H3_DISCOVERY, config).build(), discarding()).join(); // will reuse connection cached from the previous request ( when HTTP/2 ) CompletableFuture.allOf(requests.keySet().parallelStream() @@ -310,10 +333,17 @@ public class ConcurrentResponses { https2TestServer.addHandler(new Http2VariableHandler(), "/https2/variable"); https2VariableURI = "https://" + https2TestServer.serverAuthority() + "/https2/variable"; + https3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + https3TestServer.addHandler(new Http3FixedHandler(), "/https3/fixed"); + https3FixedURI = "https://" + https3TestServer.serverAuthority() + "/https3/fixed"; + https3TestServer.addHandler(new Http3VariableHandler(), "/https3/variable"); + https3VariableURI = "https://" + https3TestServer.serverAuthority() + "/https3/variable"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + https3TestServer.start(); } @AfterTest @@ -322,6 +352,7 @@ public class ConcurrentResponses { httpsTestServer.stop(0); http2TestServer.stop(); https2TestServer.stop(); + https3TestServer.stop(); } interface SendResponseHeadersFunction { @@ -407,4 +438,26 @@ public class ConcurrentResponses { (rcode, ignored) -> t.sendResponseHeaders(rcode, 0 /* no Content-Length */)); } } + + static class Http3FixedHandler implements HttpTestHandler { + + @Override + public void handle(HttpServerAdapters.HttpTestExchange t) throws IOException { + serverHandlerImpl(t.getRequestBody(), + t.getResponseBody(), + t.getRequestURI(), + (rcode, length) -> t.sendResponseHeaders(rcode, length)); + } + } + + static class Http3VariableHandler implements HttpTestHandler { + + @Override + public void handle(HttpServerAdapters.HttpTestExchange t) throws IOException { + serverHandlerImpl(t.getRequestBody(), + t.getResponseBody(), + t.getRequestURI(), + (rcode, ignored) -> t.sendResponseHeaders(rcode, -1/* no Content-Length */)); + } + } } diff --git a/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java b/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java index 0ff21c23a8f..f302de4ee48 100644 --- a/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java +++ b/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,7 +30,9 @@ * @build jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.HttpServerAdapters * @bug 8283544 - * @run testng/othervm ContentLengthHeaderTest + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * ContentLengthHeaderTest */ @@ -51,6 +53,7 @@ import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.util.Optional; import javax.net.ssl.SSLContext; import jdk.test.lib.net.URIBuilder; @@ -58,6 +61,8 @@ import jdk.test.lib.net.URIBuilder; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -69,24 +74,29 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { static HttpTestServer testContentLengthServerH1; static HttpTestServer testContentLengthServerH2; + static HttpTestServer testContentLengthServerH3; static PrintStream testLog = System.err; static SSLContext sslContext; HttpClient hc; URI testContentLengthURIH1; URI testContentLengthURIH2; + URI testContentLengthURIH3; @BeforeTest - public void setup() throws IOException, URISyntaxException { + public void setup() throws IOException, URISyntaxException, InterruptedException { sslContext = new SimpleSSLContext().get(); testContentLengthServerH1 = HttpTestServer.create(HTTP_1_1); testContentLengthServerH2 = HttpTestServer.create(HTTP_2, sslContext); + testContentLengthServerH3 = HttpTestServer.create(HTTP_3, sslContext); // Create handlers for tests that check for the presence of a Content-length header testContentLengthServerH1.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); testContentLengthServerH2.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); + testContentLengthServerH3.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); testContentLengthServerH1.addHandler(new ContentLengthHandler(), BODY_PATH); testContentLengthServerH2.addHandler(new OptionalContentLengthHandler(), BODY_PATH); + testContentLengthServerH3.addHandler(new OptionalContentLengthHandler(), BODY_PATH); testContentLengthURIH1 = URIBuilder.newBuilder() .scheme("http") .loopback() @@ -97,6 +107,11 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { .loopback() .port(testContentLengthServerH2.getAddress().getPort()) .build(); + testContentLengthURIH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(testContentLengthServerH3.getAddress().getPort()) + .build(); testContentLengthServerH1.start(); testLog.println("HTTP/1.1 Server up at address: " + testContentLengthServerH1.getAddress()); @@ -106,25 +121,45 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { testLog.println("HTTP/2 Server up at address: " + testContentLengthServerH2.getAddress()); testLog.println("Request URI for Client: " + testContentLengthURIH2); - hc = HttpClient.newBuilder() + testContentLengthServerH3.start(); + testLog.println("HTTP/3 Server up at address: " + + testContentLengthServerH3.getAddress()); + testLog.println("HTTP/3 Quic Endpoint up at address: " + + testContentLengthServerH3.getH3AltService().get().getAddress()); + testLog.println("Request URI for Client: " + testContentLengthURIH3); + + hc = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .build(); + var firstReq = HttpRequest.newBuilder(URI.create(testContentLengthURIH3 + NO_BODY_PATH)) + .setOption(H3_DISCOVERY, testContentLengthServerH3.h3DiscoveryConfig()) + .HEAD() + .version(HTTP_2) + .build(); + // populate alt-service registry + var resp = hc.send(firstReq, BodyHandlers.ofString()); + assertEquals(resp.statusCode(), 200); + testLog.println("**** setup done ****"); } @AfterTest public void teardown() { + testLog.println("**** tearing down ****"); if (testContentLengthServerH1 != null) testContentLengthServerH1.stop(); if (testContentLengthServerH2 != null) testContentLengthServerH2.stop(); + if (testContentLengthServerH3 != null) + testContentLengthServerH3.stop(); } @DataProvider(name = "bodies") Object[][] bodies() { return new Object[][]{ {HTTP_1_1, URI.create(testContentLengthURIH1 + BODY_PATH)}, - {HTTP_2, URI.create(testContentLengthURIH2 + BODY_PATH)} + {HTTP_2, URI.create(testContentLengthURIH2 + BODY_PATH)}, + {HTTP_3, URI.create(testContentLengthURIH3 + BODY_PATH)} }; } @@ -132,7 +167,8 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { Object[][] nobodies() { return new Object[][]{ {HTTP_1_1, URI.create(testContentLengthURIH1 + NO_BODY_PATH)}, - {HTTP_2, URI.create(testContentLengthURIH2 + NO_BODY_PATH)} + {HTTP_2, URI.create(testContentLengthURIH2 + NO_BODY_PATH)}, + {HTTP_3, URI.create(testContentLengthURIH3 + NO_BODY_PATH)} }; } diff --git a/test/jdk/java/net/httpclient/CookieHeaderTest.java b/test/jdk/java/net/httpclient/CookieHeaderTest.java index b39a23371ab..455b1048b06 100644 --- a/test/jdk/java/net/httpclient/CookieHeaderTest.java +++ b/test/jdk/java/net/httpclient/CookieHeaderTest.java @@ -75,6 +75,8 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -86,12 +88,14 @@ public class CookieHeaderTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) DummyServer httpDummyServer; DummyServer httpsDummyServer; String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; String httpDummy; String httpsDummy; @@ -115,6 +119,7 @@ public class CookieHeaderTest implements HttpServerAdapters { { httpsDummy, HTTP_1_1 }, { httpURI, HttpClient.Version.HTTP_2 }, { httpsURI, HttpClient.Version.HTTP_2 }, + { http3URI, HttpClient.Version.HTTP_3 }, { httpDummy, HttpClient.Version.HTTP_2 }, { httpsDummy, HttpClient.Version.HTTP_2 }, { http2URI, null }, @@ -130,7 +135,7 @@ public class CookieHeaderTest implements HttpServerAdapters { ConcurrentHashMap> cookieHeaders = new ConcurrentHashMap<>(); CookieHandler cookieManager = new TestCookieHandler(cookieHeaders); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .followRedirects(Redirect.ALWAYS) .cookieHandler(cookieManager) .sslContext(sslContext) @@ -150,6 +155,9 @@ public class CookieHeaderTest implements HttpServerAdapters { if (version != null) { requestBuilder.version(version); } + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } HttpRequest request = requestBuilder.build(); out.println("Initial request: " + request.uri()); @@ -157,7 +165,8 @@ public class CookieHeaderTest implements HttpServerAdapters { out.println("iteration: " + i); HttpResponse response = client.send(request, BodyHandlers.ofString()); - out.println(" Got response: " + response); + out.println(" Got response: " + response + ", config=" + request.getOption(H3_DISCOVERY) + + ", version=" + response.version()); out.println(" Got body Path: " + response.body()); assertEquals(response.statusCode(), 200); @@ -166,11 +175,17 @@ public class CookieHeaderTest implements HttpServerAdapters { cookies.stream() .filter(s -> !s.startsWith("LOC")) .collect(Collectors.toList())); + if (version == HTTP_3 && i > 0) { + assertEquals(response.version(), HTTP_3); + } requestBuilder = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()); if (version != null) { requestBuilder.version(version); } + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } request = requestBuilder.build(); } } @@ -196,6 +211,9 @@ public class CookieHeaderTest implements HttpServerAdapters { https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(new CookieValidationHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new CookieValidationHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/retry"; // DummyServer @@ -209,6 +227,7 @@ public class CookieHeaderTest implements HttpServerAdapters { httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); httpDummyServer.start(); httpsDummyServer.start(); } diff --git a/test/jdk/java/net/httpclient/CustomRequestPublisher.java b/test/jdk/java/net/httpclient/CustomRequestPublisher.java index 140a3a98ec4..c8d33030a06 100644 --- a/test/jdk/java/net/httpclient/CustomRequestPublisher.java +++ b/test/jdk/java/net/httpclient/CustomRequestPublisher.java @@ -25,13 +25,14 @@ * @test * @summary Checks correct handling of Publishers that call onComplete without demand * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext + * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * @run testng/othervm CustomRequestPublisher */ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; +import java.net.http.HttpClient.Builder; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Optional; @@ -57,9 +58,12 @@ import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.net.http.HttpResponse.BodyHandlers.ofString; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -70,10 +74,12 @@ public class CustomRequestPublisher implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; @DataProvider(name = "variants") public Object[][] variants() { @@ -98,19 +104,26 @@ public class CustomRequestPublisher implements HttpServerAdapters { { http2URI, unknownSupplier, true }, { https2URI, fixedSupplier, true,}, { https2URI, unknownSupplier, true }, + { http3URI, fixedSupplier, true,}, // always use same client with h3 + { http3URI, unknownSupplier, true }, // always use same client with h3 }; } static final int ITERATION_COUNT = 10; /** Asserts HTTP Version, and SSLSession presence when applicable. */ - static void assertVersionAndSession(HttpResponse response, String uri) { - if (uri.contains("http2") || uri.contains("https2")) + static void assertVersionAndSession(int step, HttpResponse response, String uri) { + if (uri.contains("http2") || uri.contains("https2")) { assertEquals(response.version(), HTTP_2); - else if (uri.contains("http1") || uri.contains("https1")) + } else if (uri.contains("http1") || uri.contains("https1")) { assertEquals(response.version(), HTTP_1_1); - else + } else if (uri.contains("http3")) { + if (step == 0) assertNotEquals(response.version(), HTTP_1_1); + else assertEquals(response.version(), HTTP_3, + "unexpected response version on step " + step); + } else { fail("Unknown HTTP version in test for: " + uri); + } Optional ssl = response.sslSession(); if (uri.contains("https")) { @@ -125,6 +138,28 @@ public class CustomRequestPublisher implements HttpServerAdapters { } } + HttpClient.Builder newHttpClientBuilder(String uri) { + HttpClient.Builder builder; + if (uri.contains("/http3/")) { + builder = newClientBuilderForH3(); + // ensure that the preferred version for the client + // is HTTP/3 + builder.version(HTTP_3); + } else builder = HttpClient.newBuilder(); + return builder.proxy(Builder.NO_PROXY); + } + + HttpRequest.Builder newHttpRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (uri.contains("/http3/") && !http3TestServer.supportsH3DirectConnection()) { + // Ensure we don't attempt to connect to a + // potentially different server if HTTP/3 endpoint and + // HTTP/2 endpoint are not on the same port + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + @Test(dataProvider = "variants") void test(String uri, Supplier bpSupplier, boolean sameClient) throws Exception @@ -132,10 +167,10 @@ public class CustomRequestPublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = HttpClient.newBuilder().sslContext(sslContext).build(); + client = newHttpClientBuilder(uri).sslContext(sslContext).build(); BodyPublisher bodyPublisher = bpSupplier.get(); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest request = newHttpRequestBuilder(uri) .POST(bodyPublisher) .build(); @@ -147,7 +182,7 @@ public class CustomRequestPublisher implements HttpServerAdapters { "Expected 200, got:" + resp.statusCode()); assertEquals(resp.body(), bodyPublisher.bodyAsString()); - assertVersionAndSession(resp, uri); + assertVersionAndSession(i, resp, uri); } } @@ -158,10 +193,10 @@ public class CustomRequestPublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = HttpClient.newBuilder().sslContext(sslContext).build(); + client = newHttpClientBuilder(uri).sslContext(sslContext).build(); BodyPublisher bodyPublisher = bpSupplier.get(); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest request = newHttpRequestBuilder(uri) .POST(bodyPublisher) .build(); @@ -174,7 +209,7 @@ public class CustomRequestPublisher implements HttpServerAdapters { "Expected 200, got:" + resp.statusCode()); assertEquals(resp.body(), bodyPublisher.bodyAsString()); - assertVersionAndSession(resp, uri); + assertVersionAndSession(0, resp, uri); } } @@ -325,10 +360,15 @@ public class CustomRequestPublisher implements HttpServerAdapters { https2TestServer.addHandler(new HttpTestEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new HttpTestEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -337,6 +377,7 @@ public class CustomRequestPublisher implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } } diff --git a/test/jdk/java/net/httpclient/CustomResponseSubscriber.java b/test/jdk/java/net/httpclient/CustomResponseSubscriber.java index be71278a450..e8b5073d9a2 100644 --- a/test/jdk/java/net/httpclient/CustomResponseSubscriber.java +++ b/test/jdk/java/net/httpclient/CustomResponseSubscriber.java @@ -46,7 +46,6 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsServer; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; @@ -58,6 +57,7 @@ import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2Handler; import javax.net.ssl.SSLContext; + import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; diff --git a/test/jdk/java/net/httpclient/DependentActionsTest.java b/test/jdk/java/net/httpclient/DependentActionsTest.java index c1e9025fe4b..7d903157de8 100644 --- a/test/jdk/java/net/httpclient/DependentActionsTest.java +++ b/test/jdk/java/net/httpclient/DependentActionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,16 +29,17 @@ * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext * DependentActionsTest - * @run testng/othervm -Djdk.internal.httpclient.debug=true DependentActionsTest - */ + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * DependentActionsTest + */ import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.StackWalker.StackFrame; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; +import org.testng.SkipException; import org.testng.annotations.AfterTest; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeTest; @@ -49,12 +50,12 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -73,21 +74,20 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Flow; import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.lang.String.format; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.util.stream.Collectors.toList; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -99,6 +99,7 @@ public class DependentActionsTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -107,6 +108,9 @@ public class DependentActionsTest implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); @@ -118,6 +122,7 @@ public class DependentActionsTest implements HttpServerAdapters { static volatile boolean tasksFailed; static final AtomicLong serverCount = new AtomicLong(); static final AtomicLong clientCount = new AtomicLong(); + static final AtomicReference errorRef = new AtomicReference<>(); static final long start = System.nanoTime(); public static String now() { long now = System.nanoTime() - start; @@ -184,6 +189,8 @@ public class DependentActionsTest implements HttpServerAdapters { http2URI_chunk, https2URI_fixed, https2URI_chunk, + http3URI_fixed, + http3URI_chunk }; } @@ -232,7 +239,8 @@ public class DependentActionsTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() + .proxy(Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) .build(); @@ -257,10 +265,14 @@ public class DependentActionsTest implements HttpServerAdapters { HttpClient client = null; out.printf("%ntestNoStalls(%s, %b)%n", uri, sameClient); for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .build(); BodyHandler handler = new StallingBodyHandler((w) -> {}, @@ -317,10 +329,16 @@ public class DependentActionsTest implements HttpServerAdapters { Staller staller) throws Exception { + if (errorRef.get() != null) { + SkipException sk = new SkipException("skipping due to previous failure: " + name); + sk.setStackTrace(new StackTraceElement[0]); + throw sk; + } out.printf("%n%s%s%n", now(), name); try { testDependent(uri, sameClient, handlers, finisher, extractor, staller); } catch (Error | Exception x) { + errorRef.compareAndSet(null, x); FAILURES.putIfAbsent(name, x); throw x; } @@ -335,20 +353,36 @@ public class DependentActionsTest implements HttpServerAdapters { { HttpClient client = null; for (Where where : EnumSet.of(Where.BODY_HANDLER)) { - if (!sameClient || client == null) + if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } + } - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .build(); BodyHandler handler = new StallingBodyHandler(where.select(staller), handlers.get()); - System.out.println("try stalling in " + where); - staller.acquire(); - assert staller.willStall(); - CompletableFuture> responseCF = client.sendAsync(req, handler); - assert !responseCF.isDone(); - finisher.finish(where, responseCF, staller, extractor); + for (int i = 0; i < 2; i++) { + System.out.println("try stalling in " + where); + staller.acquire(); + assert staller.willStall(); + CompletableFuture> responseCF = client.sendAsync(req, handler); + assert !responseCF.isDone(); + var resp = finisher.finish(where, responseCF, staller, extractor); + if (version(uri) == HTTP_3 && resp.version() != HTTP_3) { + if (i == 0) continue; + // it's possible that the first request still went through HTTP/2 + // if the config was HTTP3_ANY. Retry it - the next time we should + // have HTTP/3 + assertEquals(resp.version(), HTTP_3, + "expected second request to go through HTTP/3 (serverConfig=" + + http3TestServer.h3DiscoveryConfig() + ")"); + } + break; + } + } } @@ -388,7 +422,7 @@ public class DependentActionsTest implements HttpServerAdapters { } interface Finisher { - public void finish(Where w, + public HttpResponse finish(Where w, CompletableFuture> cf, Staller staller, Extractor extractor); @@ -424,7 +458,7 @@ public class DependentActionsTest implements HttpServerAdapters { } } - void finish(Where w, CompletableFuture> cf, + HttpResponse finish(Where w, CompletableFuture> cf, Staller staller, Extractor extractor) { Thread thread = Thread.currentThread(); @@ -446,6 +480,11 @@ public class DependentActionsTest implements HttpServerAdapters { + w + ": " + response, error); } assertEquals(result, List.of(response.request().uri().getPath())); + var uriStr = response.request().uri().toString(); + if (HTTP_3 != version(uriStr) || http3TestServer.h3DiscoveryConfig() != Http3DiscoveryMode.ANY) { + assertEquals(response.version(), version(uriStr), uriStr); + } + return response; } finally { staller.reset(); } @@ -570,6 +609,37 @@ public class DependentActionsTest implements HttpServerAdapters { } } + static Version version(String uri) { + if (uri.contains("/http1/") || uri.contains("/https1/")) + return HTTP_1_1; + if (uri.contains("/http2/") || uri.contains("/https2/")) + return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; + return null; + } + + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + System.out.println("\n--- HEAD request succeeded ----\n"); + System.err.println("\n--- HEAD request succeeded ----\n"); + return response; + } @BeforeTest public void setup() throws Exception { @@ -608,11 +678,33 @@ public class DependentActionsTest implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/x"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/x"; - serverCount.addAndGet(4); + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + + serverCount.addAndGet(5); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); + + out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); + out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); + out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); + out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); } @AfterTest @@ -622,6 +714,7 @@ public class DependentActionsTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class HTTP_FixedLengthHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java b/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java index e47f4be46f7..12903294315 100644 --- a/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java +++ b/test/jdk/java/net/httpclient/DependentPromiseActionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,7 @@ */ import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.InputStreamReader; import java.lang.StackWalker.StackFrame; import jdk.test.lib.net.SimpleSSLContext; @@ -49,8 +50,10 @@ import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -81,12 +84,14 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.err; import static java.lang.System.out; import static java.lang.String.format; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -96,10 +101,13 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { SSLContext sslContext; HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http2URI_fixed; String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); @@ -170,6 +178,8 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI_fixed, + http3URI_chunk, http2URI_fixed, http2URI_chunk, https2URI_fixed, @@ -224,9 +234,11 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() .executor(executor) .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) .build(); } @@ -243,47 +255,69 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { } } + Http3DiscoveryMode config(String uri) { + return uri.contains("/http3/") ? HTTP_3_URI_ONLY : null; + } + + Version version(String uri) { + return uri.contains("/http3/") ? HTTP_3 : HTTP_2; + } + + HttpRequest request(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)) + .version(version(uri)); + var config = config(uri); + if (config != null) builder.setOption(H3_DISCOVERY, config); + return builder.build(); + } + @Test(dataProvider = "noStalls") public void testNoStalls(String rootUri, boolean sameClient) throws Exception { if (!FAILURES.isEmpty()) return; HttpClient client = null; out.printf("%ntestNoStalls(%s, %b)%n", rootUri, sameClient); - for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) - client = newHttpClient(sameClient); + try { + for (int i = 0; i < ITERATION_COUNT; i++) { + if (!sameClient || client == null) + client = newHttpClient(sameClient); - String uri = rootUri + "/" + requestCount.incrementAndGet(); - out.printf("\tsending request %s%n", uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) - .build(); - BodyHandler> handler = - new StallingBodyHandler((w) -> {}, - BodyHandlers.ofLines()); - Map>>> pushPromises = - new ConcurrentHashMap<>(); - PushPromiseHandler> pushHandler = new PushPromiseHandler<>() { - @Override - public void applyPushPromise(HttpRequest initiatingRequest, - HttpRequest pushPromiseRequest, - Function>, - CompletableFuture>>> - acceptor) { - pushPromises.putIfAbsent(pushPromiseRequest, acceptor.apply(handler)); + String uri = rootUri + "/" + requestCount.incrementAndGet(); + out.printf("\tsending request %s%n", uri); + HttpRequest req = request(uri); + + BodyHandler> handler = + new StallingBodyHandler((w) -> {}, + BodyHandlers.ofLines()); + Map>>> pushPromises = + new ConcurrentHashMap<>(); + PushPromiseHandler> pushHandler = new PushPromiseHandler<>() { + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function>, + CompletableFuture>>> + acceptor) { + pushPromises.putIfAbsent(pushPromiseRequest, acceptor.apply(handler)); + } + }; + HttpResponse> response = + client.sendAsync(req, BodyHandlers.ofLines(), pushHandler).get(); + String body = response.body().collect(Collectors.joining("|")); + assertEquals(URI.create(body).getPath(), URI.create(uri).getPath()); + for (HttpRequest promised : pushPromises.keySet()) { + out.printf("%s Received promise: %s%n\tresponse: %s%n", + now(), promised, pushPromises.get(promised).get()); + String promisedBody = pushPromises.get(promised).get().body() + .collect(Collectors.joining("|")); + assertEquals(promisedBody, promised.uri().toASCIIString()); } - }; - HttpResponse> response = - client.sendAsync(req, BodyHandlers.ofLines(), pushHandler).get(); - String body = response.body().collect(Collectors.joining("|")); - assertEquals(URI.create(body).getPath(), URI.create(uri).getPath()); - for (HttpRequest promised : pushPromises.keySet()) { - out.printf("%s Received promise: %s%n\tresponse: %s%n", - now(), promised, pushPromises.get(promised).get()); - String promisedBody = pushPromises.get(promised).get().body() - .collect(Collectors.joining("|")); - assertEquals(promisedBody, promised.uri().toASCIIString()); + assertEquals(3, pushPromises.size()); + } + } finally { + if (!sameClient && client != null) { + client.close(); } - assertEquals(3, pushPromises.size()); } } @@ -357,29 +391,33 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { throws Exception { HttpClient client = null; - for (Where where : EnumSet.of(Where.BODY_HANDLER)) { - if (!sameClient || client == null) - client = newHttpClient(sameClient); + try { + for (Where where : EnumSet.of(Where.BODY_HANDLER)) { + if (!sameClient || client == null) + client = newHttpClient(sameClient); + String uri = rootUri + "/" + requestCount.incrementAndGet(); + out.printf("\tsending request %s%n", uri); + HttpRequest req = request(uri); - String uri = rootUri + "/" + requestCount.incrementAndGet(); - out.printf("\tsending request %s%n", uri); - HttpRequest req = HttpRequest. - newBuilder(URI.create(uri)) - .build(); - StallingPushPromiseHandler promiseHandler = - new StallingPushPromiseHandler<>(where, handlers, stallers); - BodyHandler handler = handlers.get(); - System.out.println("try stalling in " + where); - CompletableFuture> responseCF = - client.sendAsync(req, handler, promiseHandler); - // The body of the main response can be received before the body - // of the push promise handlers are received. - // The body of the main response doesn't stall, so the cf of - // the main response may be done here even for EAGER subscribers. - // We cannot make any assumption on the state of the main response - // cf here, so the only thing we can do is to call the finisher - // which will wait for them all. - finisher.finish(where, responseCF, promiseHandler, extractor); + StallingPushPromiseHandler promiseHandler = + new StallingPushPromiseHandler<>(where, handlers, stallers); + BodyHandler handler = handlers.get(); + System.out.println("try stalling in " + where); + CompletableFuture> responseCF = + client.sendAsync(req, handler, promiseHandler); + // The body of the main response can be received before the body + // of the push promise handlers are received. + // The body of the main response doesn't stall, so the cf of + // the main response may be done here even for EAGER subscribers. + // We cannot make any assumption on the state of the main response + // cf here, so the only thing we can do is to call the finisher + // which will wait for them all. + finisher.finish(where, responseCF, promiseHandler, extractor); + } + } finally { + if (client != null && !sameClient) { + client.close(); + } } } @@ -499,9 +537,7 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { httpStack.forEach(f -> System.out.printf("\t%s%n", f)); failed.set(new RuntimeException("Dependant action has unexpected frame in " + Thread.currentThread() + ": " + httpStack.get(0))); - } - return; } else { List httpStack = WALKER.walk(s -> s.filter(f -> f.getDeclaringClass() .getModule().equals(HttpClient.class.getModule())) @@ -670,31 +706,42 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { throw new AssertionError("Unexpected null sslContext"); // HTTP/2 - HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler(); - HttpTestHandler h2_chunkedHandler = new HTTP_ChunkedHandler(); + HttpTestHandler fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler chunkedHandler = new HTTP_ChunkedHandler(); http2TestServer = HttpTestServer.create(HTTP_2); - http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed"); - http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk"); + http2TestServer.addHandler(fixedLengthHandler, "/http2/fixed"); + http2TestServer.addHandler(chunkedHandler, "/http2/chunk"); http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed/y"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk/y"; https2TestServer = HttpTestServer.create(HTTP_2, sslContext); - https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); - https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); + https2TestServer.addHandler(fixedLengthHandler, "/https2/fixed"); + https2TestServer.addHandler(chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed/y"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk/y"; - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + + serverCount.addAndGet(3); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest public void teardown() throws Exception { + if (sharedClient != null) { + sharedClient.close(); + } sharedClient = null; http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static final BiPredicate ACCEPT_ALL = (x, y) -> true; @@ -712,15 +759,68 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); out.printf("TestServer: %s Pushing promise: %s%n", now(), promise); err.printf("TestServer: %s Pushing promise: %s%n", now(), promise); - HttpHeaders headers; + HttpHeaders reqHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; if (fixed) { String length = String.valueOf(promiseBytes.length); - headers = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), ACCEPT_ALL); } else { - headers = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty } - t.serverPush(promise, headers, promiseBytes); + t.serverPush(promise, reqHeaders, rspHeaders, promiseBytes); + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static long sendHttp3PushPromiseFrame(HttpTestExchange t, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending PushPromiseFrame: %s%n", now(), promise); + err.printf("TestServer: %s Pushing PushPromiseFrame: %s%n", now(), promise); + HttpHeaders reqHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); + long pushId = t.sendHttp3PushPromiseFrame(-1, promise, reqHeaders); + out.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + err.printf("TestServer: %s PushPromiseFrame pushId=%s sent%n", now(), pushId); + return pushId; + } catch (URISyntaxException x) { + throw new IOException(x.getMessage(), x); + } + } + + private static void sendHttp3PushResponse(HttpTestExchange t, + long pushId, + URI requestURI, + String pushPath, + boolean fixed) + throws IOException + { + try { + URI promise = new URI(requestURI.getScheme(), + requestURI.getAuthority(), + pushPath, null, null); + byte[] promiseBytes = promise.toASCIIString().getBytes(UTF_8); + out.printf("TestServer: %s sending push response pushId=%s: %s%n", now(), pushId, promise); + err.printf("TestServer: %s Pushing push response pushId=%s: %s%n", now(), pushId, promise); + HttpHeaders reqHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + HttpHeaders rspHeaders; + if (fixed) { + String length = String.valueOf(promiseBytes.length); + rspHeaders = HttpHeaders.of(Map.of("Content-Length", List.of(length)), + ACCEPT_ALL); + } else { + rspHeaders = HttpHeaders.of(Map.of(), ACCEPT_ALL); // empty + } + t.sendHttp3PushResponse(pushId, promise, reqHeaders, rspHeaders, new ByteArrayInputStream(promiseBytes)); } catch (URISyntaxException x) { throw new IOException(x.getMessage(), x); } @@ -739,14 +839,33 @@ public class DependentPromiseActionsTest implements HttpServerAdapters { pushPromiseFor(t, requestURI, path, true); } byte[] resp = t.getRequestURI().toString().getBytes(StandardCharsets.UTF_8); - t.sendResponseHeaders(200, resp.length); //fixed content length + t.sendResponseHeaders(200, resp.length); + + //fixed content length + // With HTTP/3 fixed length we send a single DataFrame, + // therefore we can't interleave a PushPromiseFrame in + // the middle of the DataFrame, so we're going to send + // the PushPromiseFrame before the DataFrame, and then + // fulfill the promise later while sending the response + // body. + long[] pushIds = new long[2]; + if (t.getExchangeVersion() == HTTP_3) { + for (int i = 0; i < 2; i++) { + String path = requestURI.getPath() + "/after/promise-" + (i + 2); + pushIds[i] = sendHttp3PushPromiseFrame(t, requestURI, path, true); + } + } try (OutputStream os = t.getResponseBody()) { int bytes = resp.length/3; for (int i = 0; i<2; i++) { String path = requestURI.getPath() + "/after/promise-" + (i + 2); os.write(resp, i * bytes, bytes); os.flush(); - pushPromiseFor(t, requestURI, path, true); + if (t.getExchangeVersion() == HTTP_2) { + pushPromiseFor(t, requestURI, path, true); + } else if (t.getExchangeVersion() == HTTP_3) { + sendHttp3PushResponse(t, pushIds[i], requestURI, path, true); + } } os.write(resp, 2*bytes, resp.length - 2*bytes); } diff --git a/test/jdk/java/net/httpclient/DigestEchoClient.java b/test/jdk/java/net/httpclient/DigestEchoClient.java index 0aa70a26586..10add19cb94 100644 --- a/test/jdk/java/net/httpclient/DigestEchoClient.java +++ b/test/jdk/java/net/httpclient/DigestEchoClient.java @@ -33,6 +33,7 @@ import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; @@ -44,7 +45,6 @@ import java.util.List; import java.util.Optional; import java.util.Random; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; @@ -52,7 +52,9 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; + import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.Utils; import jdk.test.lib.net.SimpleSSLContext; import sun.net.NetProperties; import sun.net.www.HeaderParser; @@ -60,8 +62,9 @@ import sun.net.www.HeaderParser; import static java.lang.System.out; import static java.lang.System.err; import static java.lang.String.format; +import static java.net.http.HttpOption.H3_DISCOVERY; -/** +/* * @test * @summary this test verifies that a client may provides authorization * headers directly when connecting with a server. @@ -189,7 +192,11 @@ public class DigestEchoClient { static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; public HttpClient newHttpClient(DigestEchoServer server) { clientCount.incrementAndGet(); - HttpClient.Builder builder = HttpClient.newBuilder(); + HttpClient.Builder builder = switch (server.version()) { + case HTTP_3 -> HttpServerAdapters.createClientBuilderForH3() + .version(Version.HTTP_3); + default -> HttpClient.newBuilder(); + }; builder = builder.proxy(ProxySelector.of(null)); if (useSSL) { builder.sslContext(context); @@ -223,11 +230,11 @@ public class DigestEchoClient { } public static List serverVersions(Version clientVersion) { - if (clientVersion == Version.HTTP_1_1) { - return List.of(clientVersion); - } else { - return List.of(Version.values()); - } + return switch (clientVersion) { + case HTTP_1_1 -> List.of(clientVersion); + case HTTP_2 -> List.of(Version.HTTP_1_1, Version.HTTP_2); + case HTTP_3 -> List.of(Version.HTTP_2, Version.HTTP_3); + }; } public static List clientVersions() { @@ -273,6 +280,13 @@ public class DigestEchoClient { authType); for (Version clientVersion : clientVersions()) { for (Version serverVersion : serverVersions(clientVersion)) { + if (serverVersion == Version.HTTP_3) { + if (!useSSL) continue; + switch (authType) { + case PROXY, PROXY305: continue; + default: break; + } + } for (boolean expectContinue : expectContinue(serverVersion)) { for (boolean async : BOOLEANS) { for (boolean preemptive : BOOLEANS) { @@ -293,6 +307,13 @@ public class DigestEchoClient { authType); for (Version clientVersion : clientVersions()) { for (Version serverVersion : serverVersions(clientVersion)) { + if (serverVersion == Version.HTTP_3) { + if (!useSSL) continue; + switch (authType) { + case PROXY, PROXY305: continue; + default: break; + } + } for (boolean expectContinue : expectContinue(serverVersion)) { for (boolean async : BOOLEANS) { dec.testDigest(clientVersion, serverVersion, @@ -314,7 +335,7 @@ public class DigestEchoClient { throw t; } finally { Thread.sleep(100); - AssertionError trackFailed = TRACKER.check(500); + AssertionError trackFailed = TRACKER.check(Utils.adjustTimeout(1000)); EchoServers.stop(); System.out.println(" ---------------------------------------------------------- "); System.out.println(String.format("DigestEchoClient %s %s", useSSL ? "SSL" : "CLEAR", types)); @@ -370,6 +391,14 @@ public class DigestEchoClient { .isPresent(); } + static Http3DiscoveryMode serverConfig(int step, DigestEchoServer server) { + var config = server.serverConfig(); + return switch (config) { + case HTTP_3_URI_ONLY -> config; + default -> Http3DiscoveryMode.ALT_SVC; + }; + } + final static AtomicLong basics = new AtomicLong(); final static AtomicLong basicCount = new AtomicLong(); // @Test @@ -413,6 +442,7 @@ public class DigestEchoClient { + ",expectContinue=" + expectContinue + ",version=" + clientVersion); reqURI = URI.create(baseReq + ",basicCount=" + basicCount.get()); HttpRequest.Builder builder = HttpRequest.newBuilder(reqURI).version(clientVersion) + .setOption(H3_DISCOVERY, serverConfig(i, server)) .POST(reqBody).expectContinue(expectContinue); boolean isTunnel = isProxy(authType) && useSSL; if (addHeaders) { @@ -450,6 +480,16 @@ public class DigestEchoClient { out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); resp = client.send(request, BodyHandlers.ofLines()); } + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } } catch (Throwable t) { long stop = System.nanoTime(); synchronized (basicCount) { @@ -471,6 +511,7 @@ public class DigestEchoClient { reqURI = URI.create(baseReq + ",withAuthorization=" + authType + ",basicCount=" + basicCount.get()); request = HttpRequest.newBuilder(reqURI).version(clientVersion) + .setOption(H3_DISCOVERY, server.serverConfig()) .POST(reqBody).header(authorizationKey(authType), auth).build(); if (async) { out.printf("%s client.sendAsync(%s)%n", DigestEchoServer.now(), request); @@ -479,6 +520,16 @@ public class DigestEchoClient { out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); resp = client.send(request, BodyHandlers.ofLines()); } + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } } final List respLines; try { @@ -520,16 +571,19 @@ public class DigestEchoClient { failed = decorated; throw decorated; } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; System.gc(); - if (queue.remove(100) == ref) break; - } - var error = TRACKER.checkShutdown(900); - if (error != null) { - if (failed != null) error.addSuppressed(failed); - throw error; + while (!ref.refersTo(null)) { + System.gc(); + if (queue.remove(100) == ref) break; + } + var error = TRACKER.checkShutdown(tracker, Utils.adjustTimeout(900), false); + if (error != null) { + if (failed != null) error.addSuppressed(failed); + throw error; + } } } System.out.println("OK"); @@ -563,13 +617,13 @@ public class DigestEchoClient { server.getServerAddress(), "/foo/"); HttpClient client = newHttpClient(server); + ReferenceQueue queue = new ReferenceQueue<>(); + WeakReference ref = new WeakReference<>(client, queue); HttpResponse r; CompletableFuture> cf1; byte[] cnonce = new byte[16]; String cnonceStr = null; DigestEchoServer.DigestResponse challenge = null; - ReferenceQueue queue = new ReferenceQueue<>(); - WeakReference ref = new WeakReference<>(client, queue); URI reqURI = null; Throwable failed = null; try { @@ -584,6 +638,7 @@ public class DigestEchoClient { reqURI = URI.create(baseReq + ",digestCount=" + digestCount.get()); HttpRequest.Builder reqBuilder = HttpRequest .newBuilder(reqURI).version(clientVersion).POST(reqBody) + .setOption(H3_DISCOVERY, serverConfig(i, server)) .expectContinue(expectContinue); boolean isTunnel = isProxy(authType) && useSSL; @@ -612,6 +667,16 @@ public class DigestEchoClient { out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); resp = client.send(request, BodyHandlers.ofLines()); } + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } System.out.println(resp); assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407 : "challenge=" + challenge + ", resp=" + resp + ", test=[" + test + "]"; @@ -642,6 +707,7 @@ public class DigestEchoClient { reqURI = URI.create(baseReq + ",withAuth=" + authType + ",digestCount=" + digestCount.get()); try { request = HttpRequest.newBuilder(reqURI).version(clientVersion) + .setOption(H3_DISCOVERY, serverConfig(i, server)) .POST(reqBody).header(authorizationKey(authType), auth).build(); } catch (IllegalArgumentException x) { throw x; @@ -654,6 +720,16 @@ public class DigestEchoClient { resp = client.send(request, BodyHandlers.ofLines()); } System.out.println(resp); + if (serverVersion == Version.HTTP_3 && clientVersion == Version.HTTP_3) { + out.println("Response version [" + i + "]: " + resp.version()); + int required = isRedirecting(authType) ? 1 : 0; + if (i > required) { + if (resp.version() != serverVersion) { + throw new AssertionError("Expected HTTP/3, but got: " + + resp.version()); + } + } + } } final List respLines; try { @@ -691,24 +767,27 @@ public class DigestEchoClient { failed = decorated; throw decorated; } finally { - client = null; - System.gc(); - while (!ref.refersTo(null)) { + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; System.gc(); - if (queue.remove(100) == ref) break; - } - var error = TRACKER.checkShutdown(900); - if (error != null) { - if (failed != null) { - error.addSuppressed(failed); + while (!ref.refersTo(null)) { + System.gc(); + if (queue.remove(100) == ref) break; + } + var error = TRACKER.checkShutdown(tracker, Utils.adjustTimeout(900), false); + if (error != null) { + if (failed != null) { + error.addSuppressed(failed); + } + throw error; } - throw error; } } System.out.println("OK"); } - // WARNING: This is not a full fledged implementation of DIGEST. + // WARNING: This is not a full-fledged implementation of DIGEST. // It does contain bugs and inaccuracy. static String digestResponse(URI uri, String method, DigestEchoServer.DigestResponse challenge, String cnonce) throws NoSuchAlgorithmException { @@ -729,32 +808,34 @@ public class DigestEchoClient { } static String authenticateKey(DigestEchoServer.HttpAuthType authType) { - switch (authType) { - case SERVER: return "www-authenticate"; - case SERVER307: return "www-authenticate"; - case PROXY: return "proxy-authenticate"; - case PROXY305: return "proxy-authenticate"; - default: throw new InternalError("authType: " + authType); - } + return switch (authType) { + case SERVER -> "www-authenticate"; + case SERVER307 -> "www-authenticate"; + case PROXY -> "proxy-authenticate"; + case PROXY305 -> "proxy-authenticate"; + }; } static String authorizationKey(DigestEchoServer.HttpAuthType authType) { - switch (authType) { - case SERVER: return "authorization"; - case SERVER307: return "Authorization"; - case PROXY: return "Proxy-Authorization"; - case PROXY305: return "proxy-Authorization"; - default: throw new InternalError("authType: " + authType); - } + return switch (authType) { + case SERVER -> "authorization"; + case SERVER307 -> "Authorization"; + case PROXY -> "Proxy-Authorization"; + case PROXY305 -> "proxy-Authorization"; + }; } static boolean isProxy(DigestEchoServer.HttpAuthType authType) { - switch (authType) { - case SERVER: return false; - case SERVER307: return false; - case PROXY: return true; - case PROXY305: return true; - default: throw new InternalError("authType: " + authType); - } + return switch (authType) { + case SERVER, SERVER307 -> false; + case PROXY, PROXY305 -> true; + }; + } + + static boolean isRedirecting(DigestEchoServer.HttpAuthType authType) { + return switch (authType) { + case SERVER307, PROXY305 -> true; + case SERVER, PROXY -> false; + }; } } diff --git a/test/jdk/java/net/httpclient/DigestEchoClientSSL.java b/test/jdk/java/net/httpclient/DigestEchoClientSSL.java index 9a7f5acb88a..ecf043f58c8 100644 --- a/test/jdk/java/net/httpclient/DigestEchoClientSSL.java +++ b/test/jdk/java/net/httpclient/DigestEchoClientSSL.java @@ -31,10 +31,18 @@ * DigestEchoClient ReferenceTracker DigestEchoClientSSL * jdk.httpclient.test.lib.common.HttpServerAdapters * @run main/othervm/timeout=300 - * DigestEchoClientSSL SSL + * -Djdk.internal.httpclient.debug=err + * -Djdk.httpclient.HttpClient.log=headers + * DigestEchoClientSSL SSL SERVER307 + * @run main/othervm/timeout=300 + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers + * DigestEchoClientSSL SSL SERVER PROXY * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes= * -Djdk.http.auth.tunneling.disabledSchemes= + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers * DigestEchoClientSSL SSL PROXY * */ diff --git a/test/jdk/java/net/httpclient/DigestEchoServer.java b/test/jdk/java/net/httpclient/DigestEchoServer.java index 187951405c2..f9a2ec1e017 100644 --- a/test/jdk/java/net/httpclient/DigestEchoServer.java +++ b/test/jdk/java/net/httpclient/DigestEchoServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -47,6 +47,8 @@ import java.net.StandardSocketOptions; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.UnsupportedProtocolVersionException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -67,8 +69,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import jdk.httpclient.test.lib.common.HttpServerAdapters.AbstractHttpAuthFilter.HttpAuthMode; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import sun.net.www.HeaderParser; import java.net.http.HttpClient.Version; import jdk.httpclient.test.lib.common.HttpServerAdapters; @@ -159,15 +163,21 @@ public abstract class DigestEchoServer implements HttpServerAdapters { final String key; DigestEchoServer(String key, - HttpTestServer server, - DigestEchoServer target, - HttpTestHandler delegate) { + HttpTestServer server, + DigestEchoServer target, + HttpTestHandler delegate) { this.key = key; this.serverImpl = server; this.redirect = target; this.delegate = delegate; } + public Version version() { + if (serverImpl.canHandle(Version.HTTP_3)) + return Version.HTTP_3; + return serverImpl.getVersion(); + } + public static void main(String[] args) throws IOException { @@ -186,6 +196,17 @@ public abstract class DigestEchoServer implements HttpServerAdapters { } } + public Http3DiscoveryMode serverConfig() { + // If the client request is HTTP_3, but the server + // doesn't support HTTP/3, we don't want the client + // to attempt a direct HTTP/3 connection - so use + // ALT_SVC to prevent that + var config = serverImpl.h3DiscoveryConfig(); + return config == null + ? Http3DiscoveryMode.ALT_SVC + : config; + } + private static String toString(HttpTestRequestHeaders headers) { return headers.entrySet().stream() .map((e) -> e.getKey() + ": " + e.getValue()) @@ -249,6 +270,13 @@ public abstract class DigestEchoServer implements HttpServerAdapters { } } + private static String authority(InetSocketAddress address) { + String addressStr = address.getAddress().getHostAddress(); + if (addressStr.indexOf(':') >= 0) { + addressStr = "[" + addressStr + "]"; + } + return addressStr + ":" + address.getPort(); + } /** * The SocketBindableFactory ensures that the local port used by an HttpServer @@ -267,7 +295,8 @@ public abstract class DigestEchoServer implements HttpServerAdapters { for (int i = 1; i <= max; i++) { B bindable = createBindable(); InetSocketAddress address = getAddress(bindable); - String key = "localhost:" + address.getPort(); + + String key = authority(address); if (addresses.addIfAbsent(key)) { System.out.println("Socket bound to: " + key + " after " + i + " attempt(s)"); @@ -461,6 +490,15 @@ public abstract class DigestEchoServer implements HttpServerAdapters { return HttpTestServer.of(createHttp1Server(protocol)); case HTTP_2: return HttpTestServer.of(createHttp2Server(protocol)); + case HTTP_3: + try { + if (!"https".equalsIgnoreCase(protocol)) { + throw new UnsupportedProtocolVersionException("HTTP/3 requires https"); + } + return HttpTestServer.create(Version.HTTP_3, SSLContext.getDefault()); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } default: throw new InternalError("Unexpected version: " + version); } @@ -481,7 +519,7 @@ public abstract class DigestEchoServer implements HttpServerAdapters { static HttpsServer configure(HttpsServer server) throws IOException { try { SSLContext ctx = SSLContext.getDefault(); - server.setHttpsConfigurator(new Configurator(ctx)); + server.setHttpsConfigurator(new Configurator(server.getAddress().getAddress(), ctx)); } catch (NoSuchAlgorithmException ex) { throw new IOException(ex); } @@ -516,6 +554,16 @@ public abstract class DigestEchoServer implements HttpServerAdapters { Objects.requireNonNull(authType); Objects.requireNonNull(auth); + if (version == Version.HTTP_3) { + if (!"https".equalsIgnoreCase(protocol)) { + throw new UnsupportedProtocolVersionException("HTTP/3 requires TLS"); + } + switch (authType) { + case PROXY, PROXY305 -> + throw new UnsupportedProtocolVersionException("proxying not supported for HTTP/3"); + case SERVER, SERVER307 -> {} + } + } HttpTestServer impl = createHttpServer(version, protocol); String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s", ProcessHandle.current().pid(), @@ -545,6 +593,10 @@ public abstract class DigestEchoServer implements HttpServerAdapters { System.out.println("WARNING: can't use HTTP/1.1 proxy with unsecure HTTP/2 server"); version = Version.HTTP_1_1; } + if (version == Version.HTTP_3) { + System.out.println("WARNING: can't use HTTP/1.1 proxy with HTTP/3 server"); + version = Version.HTTP_2; + } HttpTestServer impl = createHttpServer(version, protocol); String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s", ProcessHandle.current().pid(), @@ -932,15 +984,11 @@ public abstract class DigestEchoServer implements HttpServerAdapters { @Override protected void requestAuthentication(HttpTestExchange he) throws IOException { - String separator; Version v = he.getExchangeVersion(); - if (v == Version.HTTP_1_1) { - separator = "\r\n "; - } else if (v == Version.HTTP_2) { - separator = " "; - } else { - throw new InternalError(String.valueOf(v)); - } + String separator = switch (v) { + case HTTP_1_1 -> "\r\n "; + case HTTP_2, HTTP_3 -> " "; + }; String headerName = getAuthenticate(); String headerValue = "Digest realm=\"" + auth.getRealm() + "\"," + separator + "qop=\"auth\"," @@ -1136,13 +1184,18 @@ public abstract class DigestEchoServer implements HttpServerAdapters { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } @Override public void configure (HttpsParameters params) { - params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } @@ -1553,8 +1606,8 @@ public abstract class DigestEchoServer implements HttpServerAdapters { port = port == -1 ? 443 : port; targetAddress = new InetSocketAddress(uri.getHost(), port); if (serverImpl != null) { - assert targetAddress.getHostString() - .equalsIgnoreCase(serverImpl.getAddress().getHostString()); + assert targetAddress.getAddress().getHostAddress() + .equalsIgnoreCase(serverImpl.getAddress().getAddress().getHostAddress()); assert targetAddress.getPort() == serverImpl.getAddress().getPort(); } } catch (Throwable x) { @@ -1716,15 +1769,16 @@ public abstract class DigestEchoServer implements HttpServerAdapters { public static URL url(String protocol, InetSocketAddress address, String path) throws MalformedURLException { - return new URL(protocol(protocol), - address.getHostString(), - address.getPort(), path); + try { + return uri(protocol, address, path).toURL(); + } catch (URISyntaxException e) { + throw new MalformedURLException(e.getMessage()); + } } public static URI uri(String protocol, InetSocketAddress address, String path) throws URISyntaxException { return new URI(protocol(protocol) + "://" + - address.getHostString() + ":" + - address.getPort() + path); + authority(address) + path); } } diff --git a/test/jdk/java/net/httpclient/EmptyAuthenticate.java b/test/jdk/java/net/httpclient/EmptyAuthenticate.java index ca5c41594e8..e445a974ad0 100644 --- a/test/jdk/java/net/httpclient/EmptyAuthenticate.java +++ b/test/jdk/java/net/httpclient/EmptyAuthenticate.java @@ -89,11 +89,13 @@ class EmptyAuthenticate { } static Stream args() { - return Stream - .of(Version.HTTP_1_1, Version.HTTP_2) - .flatMap(version -> Stream - .of(true, false) - .map(secure -> Arguments.of(version, secure))); + return Stream.concat( + Stream + .of(Version.HTTP_1_1, Version.HTTP_2) + .flatMap(version -> Stream + .of(true, false) + .map(secure -> Arguments.of(version, secure))), + Stream.of(Arguments.of(Version.HTTP_3, true))); } private static HttpTestServer createServer(Version version, boolean secure, String uriPath) @@ -128,7 +130,7 @@ class EmptyAuthenticate { } private static HttpClient createClient(Version version, boolean secure) { - HttpClient.Builder clientBuilder = HttpClient.newBuilder().version(version).proxy(NO_PROXY); + HttpClient.Builder clientBuilder = HttpServerAdapters.createClientBuilderFor(version).proxy(NO_PROXY); if (secure) { clientBuilder.sslContext(SSL_CONTEXT); } diff --git a/test/jdk/java/net/httpclient/EncodedCharsInURI.java b/test/jdk/java/net/httpclient/EncodedCharsInURI.java index bcda1f32539..91bace42699 100644 --- a/test/jdk/java/net/httpclient/EncodedCharsInURI.java +++ b/test/jdk/java/net/httpclient/EncodedCharsInURI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,6 +36,7 @@ */ //* -Djdk.internal.httpclient.debug=true +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterTest; @@ -78,6 +79,8 @@ import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -92,6 +95,7 @@ public class EncodedCharsInURI implements HttpServerAdapters { HttpTestServer https2TestServer; // HTTP/2 ( h2 ) DummyServer httpDummyServer; // HTTP/1.1 [ 2 servers ] DummyServer httpsDummyServer; // HTTPS/1.1 + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -100,6 +104,9 @@ public class EncodedCharsInURI implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; + String http3URI_head; String httpDummy; String httpsDummy; @@ -169,6 +176,8 @@ public class EncodedCharsInURI implements HttpServerAdapters { return new String[] { httpDummy, httpsDummy, + http3URI_fixed, + http3URI_chunk, httpURI_fixed, httpURI_chunk, httpsURI_fixed, @@ -202,12 +211,39 @@ public class EncodedCharsInURI implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + private HttpClient makeNewClient() { clientCount.incrementAndGet(); - return HttpClient.newBuilder() + return newClientBuilderForH3() .executor(executor) .proxy(NO_PROXY) .sslContext(sslContext) @@ -246,11 +282,14 @@ public class EncodedCharsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { BodyPublisher bodyPublisher = BodyPublishers.ofString(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(uri) .POST(bodyPublisher) .build(); BodyHandler handler = BodyHandlers.ofString(); @@ -314,15 +353,27 @@ public class EncodedCharsInURI implements HttpServerAdapters { httpDummy = "http://" + httpDummyServer.serverAuthority() + "/http1/dummy/x"; httpsDummy = "https://" + httpsDummyServer.serverAuthority() + "/https1/dummy/x"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_ChunkedHandler(); + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed/x"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk/x"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + err.println(now() + "Starting servers"); - serverCount.addAndGet(6); + serverCount.addAndGet(7); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); httpDummyServer.start(); httpsDummyServer.start(); + http3TestServer.start(); out.println("HTTP/1.1 dummy server (http) listening at: " + httpDummyServer.serverAuthority()); out.println("HTTP/1.1 dummy server (TLS) listening at: " + httpsDummyServer.serverAuthority()); @@ -330,6 +381,11 @@ public class EncodedCharsInURI implements HttpServerAdapters { out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); out.println(now() + "setup done"); err.println(now() + "setup done"); @@ -342,6 +398,7 @@ public class EncodedCharsInURI implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); httpDummyServer.stopServer(); httpsDummyServer.stopServer(); } diff --git a/test/jdk/java/net/httpclient/EscapedOctetsInURI.java b/test/jdk/java/net/httpclient/EscapedOctetsInURI.java index 8a17cea78c4..d9d8ba1ddd7 100644 --- a/test/jdk/java/net/httpclient/EscapedOctetsInURI.java +++ b/test/jdk/java/net/httpclient/EscapedOctetsInURI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -50,6 +50,7 @@ import java.util.Arrays; import java.util.List; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -60,6 +61,8 @@ import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -67,14 +70,17 @@ import static org.testng.Assert.assertEquals; public class EscapedOctetsInURI implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; + String http3URI_head; private volatile HttpClient sharedClient; @@ -106,6 +112,9 @@ public class EscapedOctetsInURI implements HttpServerAdapters { Arrays.asList(pathsAndQueryStrings).stream() .map(e -> new Object[] {https2URI + e[0] + e[1], sameClient}) .forEach(list::add); + Arrays.asList(pathsAndQueryStrings).stream() + .map(e -> new Object[] {http3URI + e[0] + e[1], sameClient}) + .forEach(list::add); } return list.stream().toArray(Object[][]::new); } @@ -126,12 +135,38 @@ public class EscapedOctetsInURI implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } private HttpClient makeNewClient() { - return HttpClient.newBuilder() + return newClientBuilderForH3() .proxy(NO_PROXY) .sslContext(sslContext) .build(); @@ -171,10 +206,13 @@ public class EscapedOctetsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); HttpResponse resp = client.send(request, BodyHandlers.ofString()); out.println("Got response: " + resp); @@ -200,10 +238,13 @@ public class EscapedOctetsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); client.sendAsync(request, BodyHandlers.ofString()) .thenApply(response -> { out.println("Got response: " + response); @@ -247,16 +288,28 @@ public class EscapedOctetsInURI implements HttpServerAdapters { https2TestServer.addHandler(new HttpASCIIUriStringHandler(), "/https2/get"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/get"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new HttpASCIIUriStringHandler(), "/http3/get"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/get"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + err.println(now() + "Starting servers"); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); out.println(now() + "setup done"); err.println(now() + "setup done"); @@ -269,6 +322,7 @@ public class EscapedOctetsInURI implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } /** A handler that returns as its body the exact escaped request URI. */ diff --git a/test/jdk/java/net/httpclient/ExecutorShutdown.java b/test/jdk/java/net/httpclient/ExecutorShutdown.java index e829daa556b..7d4cbec6d92 100644 --- a/test/jdk/java/net/httpclient/ExecutorShutdown.java +++ b/test/jdk/java/net/httpclient/ExecutorShutdown.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -39,12 +39,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -61,13 +61,8 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -79,6 +74,9 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; @@ -91,14 +89,19 @@ public class ExecutorShutdown implements HttpServerAdapters { static final Random RANDOM = RandomFactory.getRandom(); SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 6 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/2 ( h2+h3 ) + HttpTestServer h3TestServer; // HTTP/2 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h3URI; + String h2h3Head; static final String MESSAGE = "ExecutorShutdown message body"; static final int ITERATIONS = 3; @@ -106,10 +109,12 @@ public class ExecutorShutdown implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig() }, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig() }, + { httpURI, HTTP_1_1, null }, + { httpsURI, HTTP_1_1, null }, + { http2URI, HTTP_2, null }, + { https2URI, HTTP_2, null }, }; } @@ -134,6 +139,13 @@ public class ExecutorShutdown implements HttpServerAdapters { } else if (t instanceof ClosedChannelException) { out.println(what + ": Accepting ClosedChannelException as a valid cause: " + t); accepted = t; + } else if (t instanceof IOException io) { + var msg = io.getMessage(); + // Stream 0 cancelled should also be accepted + if (msg != null && msg.matches("Stream (0|([1-9][0-9]*)) cancelled")) { + out.println(what + ": Accepting Stream cancelled as a valid cause: " + io); + accepted = t; + } } t = t.getCause(); } @@ -147,12 +159,13 @@ public class ExecutorShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); ExecutorService executorService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .executor(executorService) .sslContext(sslContext) .build(); @@ -160,11 +173,19 @@ public class ExecutorShutdown implements HttpServerAdapters { assert client.executor().isPresent(); int step = RANDOM.nextInt(ITERATIONS); + int head = Math.min(1, step); + List>> responses = new ArrayList<>(); try { - List>> responses = new ArrayList<>(); for (int i = 0; i < ITERATIONS; i++) { + if (i == head && version == HTTP_3 && config != HTTP_3_URI_ONLY) { + // let's the first request go through whatever version, + // but ensure that the second will find an AltService + // record + headRequest(client); + } URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, config) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); @@ -189,6 +210,7 @@ public class ExecutorShutdown implements HttpServerAdapters { out.println(si + ": Got response: " + response); out.println(si + ": Got body Path: " + response.body()); assertEquals(response.statusCode(), 200); + if (si >= head) assertEquals(response.version(), version); assertEquals(response.body(), MESSAGE); return response; }).exceptionally((t) -> { @@ -207,12 +229,13 @@ public class ExecutorShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting (%s, %s, %s) ----%n%n", uriString, version, config); ExecutorService executorService = Executors.newCachedThreadPool(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .executor(executorService) .sslContext(sslContext) .build(); @@ -225,8 +248,9 @@ public class ExecutorShutdown implements HttpServerAdapters { for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) - .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) - .build(); + .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) + .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; try { @@ -249,6 +273,7 @@ public class ExecutorShutdown implements HttpServerAdapters { out.println(si + ": Got response: " + response); out.println(si + ": Got body Path: " + response.body()); assertEquals(response.statusCode(), 200); + if (si > 0) assertEquals(response.version(), version); assertEquals(response.body(), MESSAGE); return response; }).handle((r,t) -> { @@ -274,6 +299,15 @@ public class ExecutorShutdown implements HttpServerAdapters { // -- Infrastructure + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @BeforeTest public void setup() throws Exception { out.println("\n**** Setup ****\n"); @@ -295,10 +329,21 @@ public class ExecutorShutdown implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -310,6 +355,8 @@ public class ExecutorShutdown implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/ExpectContinueTest.java b/test/jdk/java/net/httpclient/ExpectContinueTest.java index 50e43099255..a703fbfaacf 100644 --- a/test/jdk/java/net/httpclient/ExpectContinueTest.java +++ b/test/jdk/java/net/httpclient/ExpectContinueTest.java @@ -37,6 +37,7 @@ import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestServerConnection; +import jdk.httpclient.test.lib.http2.Http2TestServerConnection.ResponseHeaders; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.frame.HeaderFrame; import org.testng.TestException; @@ -252,16 +253,25 @@ public class ExpectContinueTest implements HttpServerAdapters { static class ExpectContinueTestExchangeImpl extends Http2TestExchangeImpl { - public ExpectContinueTestExchangeImpl(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { + public ExpectContinueTestExchangeImpl(int streamid, + String method, + HttpHeaders reqheaders, + HttpHeadersBuilder rspheadersBuilder, + URI uri, InputStream is, + SSLSession sslSession, + BodyOutputStream os, + Http2TestServerConnection conn, + boolean pushAllowed) { super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed); } private void sendEndStreamHeaders() throws IOException { this.responseLength = 0; - rspheadersBuilder.setHeader(":status", Integer.toString(100)); + HttpHeadersBuilder pseudoHeadersBuilder = new HttpHeadersBuilder(); + pseudoHeadersBuilder.setHeader(":status", Integer.toString(100)); + HttpHeaders pseudoHeaders = pseudoHeadersBuilder.build(); HttpHeaders headers = rspheadersBuilder.build(); - Http2TestServerConnection.ResponseHeaders response - = new Http2TestServerConnection.ResponseHeaders(headers); + ResponseHeaders response = new ResponseHeaders(pseudoHeaders, headers); response.streamid(streamid); response.setFlag(HeaderFrame.END_HEADERS); response.setFlag(HeaderFrame.END_STREAM); diff --git a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java index 5d0935d7216..ddefd2a9aa7 100644 --- a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java +++ b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java @@ -27,9 +27,11 @@ import java.io.OutputStream; import java.net.URI; import java.net.http.HttpClient.Builder; import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.Flow; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicBoolean; @@ -46,6 +48,8 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; + +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.util.stream.Collectors.joining; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpRequest.BodyPublishers.fromPublisher; @@ -61,20 +65,22 @@ import static org.testng.Assert.fail; * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.test.lib.net.SimpleSSLContext - * @run testng/othervm FlowAdapterPublisherTest + * @run testng/othervm -Djdk.internal.httpclient.debug=err FlowAdapterPublisherTest */ public class FlowAdapterPublisherTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; @DataProvider(name = "uris") public Object[][] variants() { @@ -83,6 +89,7 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { { httpsURI }, { http2URI }, { https2URI }, + { http3URI }, }; } @@ -94,16 +101,26 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { if (uri.contains("/https1/")) return Version.HTTP_1_1; if (uri.contains("/http2/")) return Version.HTTP_2; if (uri.contains("/https2/")) return Version.HTTP_2; + if (uri.contains("/http3/")) return Version.HTTP_3; return null; } private HttpClient newHttpClient(String uri) { - var builder = HttpClient.newBuilder(); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpClient.newBuilder() + : HttpServerAdapters.createClientBuilderForH3().version(Version.HTTP_3); return builder.sslContext(sslContext).proxy(Builder.NO_PROXY).build(); } private HttpRequest.Builder newRequestBuilder(String uri) { - return HttpRequest.newBuilder(URI.create(uri)); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpRequest.newBuilder(URI.create(uri)) + : HttpRequest.newBuilder(URI.create(uri)) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + return builder; } @Test @@ -378,10 +395,15 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -390,6 +412,7 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class HttpEchoHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java b/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java index 8319204f5c2..5de943278eb 100644 --- a/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java +++ b/test/jdk/java/net/httpclient/FlowAdapterSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,9 +30,11 @@ import java.lang.StackWalker.StackFrame; import java.net.URI; import java.net.http.HttpClient.Builder; import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.ByteBuffer; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -56,6 +58,8 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; + +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; @@ -74,14 +78,16 @@ import static org.testng.Assert.assertTrue; public class FlowAdapterSubscriberTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); @@ -102,6 +108,7 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { { httpsURI }, { http2URI }, { https2URI }, + { http3URI }, }; } @@ -112,16 +119,26 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { if (uri.contains("/https1/")) return Version.HTTP_1_1; if (uri.contains("/http2/")) return Version.HTTP_2; if (uri.contains("/https2/")) return Version.HTTP_2; + if (uri.contains("/http3/")) return Version.HTTP_3; return null; } private HttpClient newHttpClient(String uri) { - var builder = HttpClient.newBuilder(); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpClient.newBuilder() + : HttpServerAdapters.createClientBuilderForH3().version(Version.HTTP_3); return builder.sslContext(sslContext).proxy(Builder.NO_PROXY).build(); } private HttpRequest.Builder newRequestBuilder(String uri) { - return HttpRequest.newBuilder(URI.create(uri)); + var version = Optional.ofNullable(version(uri)); + var builder = version.isEmpty() || version.get() != Version.HTTP_3 + ? HttpRequest.newBuilder(URI.create(uri)) + : HttpRequest.newBuilder(URI.create(uri)) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + return builder; } @Test @@ -636,10 +653,15 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -648,6 +670,7 @@ public class FlowAdapterSubscriberTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class HttpEchoHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/ForbiddenHeadTest.java b/test/jdk/java/net/httpclient/ForbiddenHeadTest.java index 1498aa118b3..2a50d03b365 100644 --- a/test/jdk/java/net/httpclient/ForbiddenHeadTest.java +++ b/test/jdk/java/net/httpclient/ForbiddenHeadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,9 +35,6 @@ * ForbiddenHeadTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; import org.testng.ITestResult; @@ -53,7 +50,6 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.net.Authenticator; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.Proxy; @@ -76,12 +72,14 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -93,12 +91,14 @@ public class ForbiddenHeadTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) DigestEchoServer.TunnelingProxy proxy; DigestEchoServer.TunnelingProxy authproxy; String httpURI; String httpsURI; String http2URI; String https2URI; + String https3URI; HttpClient authClient; HttpClient noAuthClient; @@ -218,6 +218,8 @@ public class ForbiddenHeadTest implements HttpServerAdapters { for (var uri : List.of(httpURI, httpsURI, http2URI, https2URI)) { result.add(new Object[]{uri + srv + auth, pcode, async, useAuth}); } + if (code == PROXY_UNAUTHORIZED) continue; // no HTTP/3 with proxy + result.add(new Object[] {https3URI + srv + auth, pcode, async, useAuth}); } } } @@ -275,6 +277,10 @@ public class ForbiddenHeadTest implements HttpServerAdapters { .newBuilder(uri) .method("HEAD", HttpRequest.BodyPublishers.noBody()); + if (uriString.contains("/http3/")) { + requestBuilder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + HttpRequest request = requestBuilder.build(); out.println("Initial request: " + request.uri()); @@ -349,10 +355,14 @@ public class ForbiddenHeadTest implements HttpServerAdapters { https2TestServer.addHandler(new UnauthorizedHandler(), "/https2/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new UnauthorizedHandler(), "/http3/"); + https3URI = "https://" + http3TestServer.serverAuthority() + "/http3"; + proxy = DigestEchoServer.createHttpsProxyTunnel(DigestEchoServer.HttpAuthSchemeType.NONE); authproxy = DigestEchoServer.createHttpsProxyTunnel(DigestEchoServer.HttpAuthSchemeType.BASIC); - authClient = TRACKER.track(HttpClient.newBuilder() + authClient = TRACKER.track(newClientBuilderForH3() .proxy(TestProxySelector.of(proxy, authproxy, httpTestServer)) .sslContext(sslContext) .executor(executor) @@ -360,7 +370,7 @@ public class ForbiddenHeadTest implements HttpServerAdapters { .build()); clientCount.incrementAndGet(); - noAuthClient = TRACKER.track(HttpClient.newBuilder() + noAuthClient = TRACKER.track(newClientBuilderForH3() .proxy(TestProxySelector.of(proxy, authproxy, httpTestServer)) .sslContext(sslContext) .executor(executor) @@ -375,6 +385,8 @@ public class ForbiddenHeadTest implements HttpServerAdapters { serverCount.incrementAndGet(); https2TestServer.start(); serverCount.incrementAndGet(); + http3TestServer.start(); + serverCount.incrementAndGet(); } @AfterTest @@ -389,6 +401,7 @@ public class ForbiddenHeadTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/GZIPInputStreamTest.java b/test/jdk/java/net/httpclient/GZIPInputStreamTest.java index 1215fc6c0a0..a8ce8fb2de5 100644 --- a/test/jdk/java/net/httpclient/GZIPInputStreamTest.java +++ b/test/jdk/java/net/httpclient/GZIPInputStreamTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,8 +31,6 @@ */ import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -45,10 +43,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; @@ -62,11 +60,13 @@ import java.util.function.Supplier; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -77,10 +77,12 @@ public class GZIPInputStreamTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer https3TestServer; // HTTP/3 String httpURI; String httpsURI; String http2URI; String https2URI; + String https3URI; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -163,26 +165,28 @@ public class GZIPInputStreamTest implements HttpServerAdapters { { http2URI, true }, { https2URI, false }, { https2URI, true }, + { https3URI, false }, + { https3URI, true } }; } final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .executor(executor) .sslContext(sslContext) .build()); } HttpClient newSingleThreadClient() { - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .executor(singleThreadExecutor) .sslContext(sslContext) .build()); } HttpClient newInLineClient() { - return TRACKER.track(HttpClient.newBuilder() + return TRACKER.track(newClientBuilderForH3() .executor((r) -> r.run() ) .sslContext(sslContext) .build()); @@ -198,18 +202,10 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri +"/txt/LoremIpsum.txt")) - .build(); + HttpRequest req = buildRequest(URI.create(uri +"/txt/LoremIpsum.txt")); BodyHandler handler = BodyHandlers.ofString(UTF_8); HttpResponse response = client.send(req, handler); - String lorem = response.body(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(response.body()); } } @@ -222,18 +218,10 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/txt/LoremIpsum.txt")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/txt/LoremIpsum.txt")); BodyHandler handler = BodyHandlers.ofInputStream(); HttpResponse response = client.send(req, handler); - String lorem = new String(response.body().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().readAllBytes(), UTF_8)); } } @@ -247,19 +235,11 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); BodyHandler handler = BodyHandlers.ofInputStream(); HttpResponse response = client.send(req, handler); GZIPInputStream gz = new GZIPInputStream(response.body()); - String lorem = new String(gz.readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(gz.readAllBytes(), UTF_8)); } } @@ -273,20 +253,12 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous, because the finisher will block. // We support this, but the executor must have enough threads. BodyHandler handler = new GZIPBodyHandler(); HttpResponse response = client.send(req, handler); - String lorem = new String(response.body().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().readAllBytes(), UTF_8)); } } @@ -301,8 +273,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newSingleThreadClient(); // should work with 1 single thread - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous, because the finisher will block. // We support this, but the executor must have enough threads. BodyHandler> handler = new BodyHandler>() { @@ -329,14 +300,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { } }; HttpResponse> response = client.send(req, handler); - String lorem = new String(response.body().get().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().get().readAllBytes(), UTF_8)); } } @@ -350,27 +314,20 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/txt/LoremIpsum.txt")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/txt/LoremIpsum.txt")); BodyHandler handler = BodyHandlers.ofInputStream(); CompletableFuture> responseCF = client.sendAsync(req, handler); // This is dangerous. Blocking in the mapping function can wedge the // response. We do support it provided that there enough threads in // the executor. - String lorem = responseCF.thenApply((r) -> { + String responseBody = responseCF.thenApply((r) -> { try { return new String(r.body().readAllBytes(), UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); } }).join(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(responseBody); } } @@ -384,27 +341,20 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); BodyHandler handler = new GZIPBodyHandler(); CompletableFuture> responseCF = client.sendAsync(req, handler); // This is dangerous - we support this, but it can block // if there are not enough threads available. // Correct custom code should use thenApplyAsync instead. - String lorem = responseCF.thenApply((r) -> { + String responseBody = responseCF.thenApply((r) -> { try { return new String(r.body().readAllBytes(), UTF_8); } catch (IOException io) { throw new UncheckedIOException(io); } }).join(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(responseBody); } } @@ -419,8 +369,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newHttpClient(); // needs at least 2 threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous. Blocking in the mapping function can wedge the // response. We do support it provided that there enough thread in // the executor. @@ -433,14 +382,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { } }); HttpResponse response = client.send(req, handler); - String lorem = response.body(); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(response.body()); } } @@ -455,8 +397,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { if (!sameClient || client == null) client = newInLineClient(); // should even work with no threads - HttpRequest req = HttpRequest.newBuilder(URI.create(uri + "/gz/LoremIpsum.txt.gz")) - .build(); + HttpRequest req = buildRequest(URI.create(uri + "/gz/LoremIpsum.txt.gz")); // This is dangerous, because the finisher will block. // We support this, but the executor must have enough threads. BodyHandler> handler = new BodyHandler>() { @@ -483,17 +424,29 @@ public class GZIPInputStreamTest implements HttpServerAdapters { } }; HttpResponse> response = client.send(req, handler); - String lorem = new String(response.body().get().readAllBytes(), UTF_8); - if (!LOREM_IPSUM.equals(lorem)) { - out.println("Response doesn't match"); - out.println("[" + LOREM_IPSUM + "] != [" + lorem + "]"); - assertEquals(LOREM_IPSUM, lorem); - } else { - out.println("Received expected response."); - } + verifyResponse(new String(response.body().get().readAllBytes(), UTF_8)); } } + private void verifyResponse(String responseBody) { + if (!LOREM_IPSUM.equals(responseBody)) { + out.println("Response doesn't match"); + out.println("[" + LOREM_IPSUM + "] != [" + responseBody + "]"); + assertEquals(LOREM_IPSUM, responseBody); + } else { + out.println("Received expected response."); + } + } + + private HttpRequest buildRequest(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getPath().contains("/https3/")) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder.build(); + } + static final class GZIPBodyHandler implements BodyHandler { @Override public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseInfo) { @@ -565,10 +518,16 @@ public class GZIPInputStreamTest implements HttpServerAdapters { https2TestServer.addHandler(gzipHandler, "/https2/chunk/gz"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + https3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + https3TestServer.addHandler(plainHandler, "/https3/chunk/txt"); + https3TestServer.addHandler(gzipHandler, "/https3/chunk/gz"); + https3URI = "https://" + https3TestServer.serverAuthority() + "/https3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + https3TestServer.start(); } @AfterTest @@ -580,6 +539,7 @@ public class GZIPInputStreamTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + https3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/HandshakeFailureTest.java b/test/jdk/java/net/httpclient/HandshakeFailureTest.java index a6db6be817a..e07dfacbd85 100644 --- a/test/jdk/java/net/httpclient/HandshakeFailureTest.java +++ b/test/jdk/java/net/httpclient/HandshakeFailureTest.java @@ -142,6 +142,7 @@ public class HandshakeFailureTest { SSLParameters params = new SSLParameters(); params.setProtocols(new String[] { tlsProtocol }); return HttpClient.newBuilder() + .version(Version.HTTP_2) .sslParameters(params) .build(); } diff --git a/test/jdk/java/net/httpclient/HeadTest.java b/test/jdk/java/net/httpclient/HeadTest.java index 5b3b1671d43..e14339a1121 100644 --- a/test/jdk/java/net/httpclient/HeadTest.java +++ b/test/jdk/java/net/httpclient/HeadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,17 +24,12 @@ /* * @test * @bug 8203433 8276559 - * @summary (httpclient) Add tests for HEAD and 304 responses. + * @summary Tests Client handles HEAD and 304 responses correctly. * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext - * @run testng/othervm - * -Djdk.httpclient.HttpClient.log=trace,headers,requests - * HeadTest + * @build jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=trace,headers,requests HeadTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -44,20 +39,22 @@ import org.testng.annotations.Test; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; +import java.io.PrintStream; import java.net.URI; -import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import static java.lang.System.out; -import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_3; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.httpclient.test.lib.common.HttpServerAdapters.createClientBuilderForH3; import static org.testng.Assert.assertEquals; public class HeadTest implements HttpServerAdapters { @@ -67,10 +64,10 @@ public class HeadTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) - String httpURI; - String httpsURI; - String http2URI; - String https2URI; + HttpTestServer https3TestServer; // HTTP/3 + String httpURI, httpsURI; + String http2URI, https2URI; + String https3URI; static final String CONTENT_LEN = "300"; @@ -81,7 +78,7 @@ public class HeadTest implements HttpServerAdapters { */ static final int HTTP_NOT_MODIFIED = 304; static final int HTTP_OK = 200; - + static final PrintStream out = System.out; @DataProvider(name = "positive") public Object[][] positive() { @@ -96,28 +93,33 @@ public class HeadTest implements HttpServerAdapters { { httpURI + "transfer/", "HEAD", HTTP_OK, HTTP_1_1 }, { httpsURI + "transfer/", "HEAD", HTTP_OK, HTTP_1_1 }, // HTTP/2 - { http2URI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 }, - { https2URI, "GET", HTTP_NOT_MODIFIED, HttpClient.Version.HTTP_2 }, - { http2URI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 }, - { https2URI, "HEAD", HTTP_OK, HttpClient.Version.HTTP_2 }, - // HTTP2 forbids transfer-encoding + { http2URI, "GET", HTTP_NOT_MODIFIED, HTTP_2 }, + { https2URI, "GET", HTTP_NOT_MODIFIED, HTTP_2 }, + { http2URI, "HEAD", HTTP_OK, HTTP_2 }, + { https2URI, "HEAD", HTTP_OK, HTTP_2 }, + // HTTP/3 + { https3URI, "GET", HTTP_NOT_MODIFIED, HTTP_3 }, + { https3URI, "HEAD", HTTP_OK, HTTP_3 }, }; } @Test(dataProvider = "positive") void test(String uriString, String method, - int expResp, HttpClient.Version version) throws Exception { + int expResp, Version version) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); URI uri = URI.create(uriString); + Http3DiscoveryMode config = version.equals(HTTP_3) ? HTTP_3_URI_ONLY : null; HttpRequest.Builder requestBuilder = HttpRequest .newBuilder(uri) .version(version) + .setOption(H3_DISCOVERY, config) .method(method, HttpRequest.BodyPublishers.noBody()); doTest(requestBuilder.build(), expResp); // repeat the test this time by building the request using convenience // GET and HEAD methods requestBuilder = HttpRequest.newBuilder(uri) - .version(version); + .version(version) + .setOption(H3_DISCOVERY, config); switch (method) { case "GET" -> requestBuilder.GET(); case "HEAD" -> requestBuilder.HEAD(); @@ -128,32 +130,27 @@ public class HeadTest implements HttpServerAdapters { // issue a request with no body and verify the response code is the expected response code private void doTest(HttpRequest request, int expResp) throws Exception { - HttpClient client = HttpClient.newBuilder() - .followRedirects(Redirect.ALWAYS) - .sslContext(sslContext) - .build(); - out.println("Initial request: " + request.uri()); + try (var client = createClientBuilderForH3().followRedirects(Redirect.ALWAYS).sslContext(sslContext).build()) { + out.println("Initial request: " + request.uri()); + HttpResponse response = client.send(request, BodyHandlers.ofString()); - HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println(" Got response: " + response); - out.println(" Got response: " + response); - - assertEquals(response.statusCode(), expResp); - assertEquals(response.body(), ""); - assertEquals(response.headers().firstValue("Content-length").get(), CONTENT_LEN); - assertEquals(response.version(), request.version().get()); + assertEquals(response.statusCode(), expResp); + assertEquals(response.body(), ""); + assertEquals(response.headers().firstValue("Content-length").get(), CONTENT_LEN); + assertEquals(response.version(), request.version().get()); + } } // -- Infrastructure - + // TODO: See if test performs better with Vthreads, see H3SimplePost and H3SimpleGet @BeforeTest public void setup() throws Exception { sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); - InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); - httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new HeadHandler(), "/"); httpURI = "http://" + httpTestServer.serverAuthority() + "/"; @@ -168,11 +165,16 @@ public class HeadTest implements HttpServerAdapters { https2TestServer.addHandler(new HeadHandler(), "/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/"; + https3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + https3TestServer.addHandler(new HeadHandler(), "/"); + https3URI = "https://" + https3TestServer.serverAuthority() + "/"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + https3TestServer.start(); } @AfterTest @@ -181,6 +183,7 @@ public class HeadTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + https3TestServer.stop(); } static class HeadHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/HeadersLowerCaseTest.java b/test/jdk/java/net/httpclient/HeadersLowerCaseTest.java new file mode 100644 index 00000000000..26c791eaca8 --- /dev/null +++ b/test/jdk/java/net/httpclient/HeadersLowerCaseTest.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @summary Verify that the request/response headers of HTTP/2 and HTTP/3 + * are sent and received in lower case + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run junit/othervm -Djdk.internal.httpclient.debug=true HeadersLowerCaseTest + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class HeadersLowerCaseTest implements HttpServerAdapters { + + private static Set REQUEST_HEADERS; + + private HttpTestServer h2server; + private HttpTestServer h3server; + private String h2ReqURIBase; + private String h3ReqURIBase; + private SSLContext sslContext; + + @BeforeAll + public void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + h2server = HttpTestServer.create(HTTP_2, sslContext); + h2server.start(); + h2ReqURIBase = "https://" + h2server.serverAuthority(); + h2server.addHandler(new ReqHeadersVerifier(), "/h2verifyReqHeaders"); + System.out.println("HTTP/2 server listening on " + h2server.getAddress()); + + + h3server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3server.start(); + h3ReqURIBase = "https://" + h3server.serverAuthority(); + h3server.addHandler(new ReqHeadersVerifier(), "/h3verifyReqHeaders"); + System.out.println("HTTP/3 server listening on " + h3server.getAddress()); + + REQUEST_HEADERS = new HashSet<>(); + REQUEST_HEADERS.add("AbCdeFgh"); + REQUEST_HEADERS.add("PQRSTU"); + REQUEST_HEADERS.add("xyz"); + REQUEST_HEADERS.add("A1243Bde2"); + REQUEST_HEADERS.add("123243"); + REQUEST_HEADERS.add("&1bacd*^d"); + REQUEST_HEADERS.add("~!#$%^&*_+"); + } + + @AfterAll + public void afterAll() throws Exception { + if (h2server != null) { + h2server.stop(); + } + if (h3server != null) { + h3server.stop(); + } + } + + /** + * Handler which verifies that the request header names are lowercase (as mandated by the spec) + */ + private static final class ReqHeadersVerifier implements HttpTestHandler { + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + System.out.println("Verifying request headers for " + exchange.getRequestURI()); + final Set missing = new HashSet<>(REQUEST_HEADERS); + final HttpTestRequestHeaders headers = exchange.getRequestHeaders(); + for (final Map.Entry> e : headers.entrySet()) { + final String header = e.getKey(); + // check validity of (non-pseudo) header names + if (!header.startsWith(":") && !Utils.isValidLowerCaseName(header)) { + System.err.println("Header name " + header + " is not valid"); + sendResponse(exchange, 500); + return; + } + final List headerVals = e.getValue(); + if (headerVals.isEmpty()) { + System.err.println("Header " + header + " is missing value"); + sendResponse(exchange, 500); + return; + } + // the header value represents the original form of the header key held in the + // REQUEST_HEADERS set + final String originalForm = headerVals.get(0); + missing.remove(originalForm); + } + if (!missing.isEmpty()) { + System.err.println("Missing headers in request: " + missing); + sendResponse(exchange, 500); + return; + } + System.out.println("All expected headers received in lower case for " + exchange.getRequestURI()); + sendResponse(exchange, 200); + } + + private static void sendResponse(final HttpTestExchange exchange, final int statusCode) throws IOException { + final HttpTestResponseHeaders respHeaders = exchange.getResponseHeaders(); + // we just send the pre-defined (request) headers back as the response headers + for (final String k : REQUEST_HEADERS) { + respHeaders.addHeader(k, k); + } + exchange.sendResponseHeaders(statusCode, 0); + } + } + + private Stream params() throws Exception { + return Stream.of( + Arguments.of(HTTP_2, new URI(h2ReqURIBase + "/h2verifyReqHeaders")), + Arguments.of(Version.HTTP_3, new URI(h3ReqURIBase + "/h3verifyReqHeaders"))); + } + + /** + * Issues a HTTP/2 or HTTP/3 request with header names of varying case (some in lower, + * some mixed, some upper case) and expects that the client internally converts them + * to lower case before encoding and sending to the server. The server side handler verifies + * that it receives the header names in lower case and if it doesn't then it returns a + * non-200 response + */ + @ParameterizedTest + @MethodSource("params") + public void testRequestHeaders(final Version version, final URI requestURI) throws Exception { + try (final HttpClient client = newClientBuilderForH3() + .version(version) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY).build()) { + Http3DiscoveryMode config = switch (version) { + case HTTP_3 -> HTTP_3_URI_ONLY; + default -> ALT_SVC; + }; + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(requestURI) + .setOption(H3_DISCOVERY, config) + .version(version); + for (final String k : REQUEST_HEADERS) { + reqBuilder.header(k, k); + } + final HttpRequest req = reqBuilder.build(); + System.out.println("Issuing " + version + " request to " + requestURI); + final HttpResponse resp = client.send(req, BodyHandlers.discarding()); + assertEquals(resp.version(), version, "Unexpected HTTP version in response"); + assertEquals(resp.statusCode(), 200, "Unexpected response code"); + // now try with async + System.out.println("Issuing (async) request to " + requestURI); + final CompletableFuture> futureResp = client.sendAsync(req, + BodyHandlers.discarding()); + final HttpResponse asyncResp = futureResp.get(); + assertEquals(asyncResp.version(), version, "Unexpected HTTP version in response"); + assertEquals(asyncResp.statusCode(), 200, "Unexpected response code"); + } + } + + /** + * Verifies that when a HTTP/2 or HTTP/3 request is being built using + * {@link HttpRequest.Builder}, only valid header names are allowed to be added to the request + */ + @ParameterizedTest + @MethodSource("params") + public void testInvalidHeaderName(final Version version, final URI requestURI) throws Exception { + Http3DiscoveryMode config = switch (version) { + case HTTP_3 -> HTTP_3_URI_ONLY; + default -> ALT_SVC; + }; + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(requestURI) + .setOption(H3_DISCOVERY, config) + .version(version); + final String copyrightSign = new String(Character.toChars(0x00A9)); // copyright sign + final String invalidHeaderName = "abcd" + copyrightSign; + System.out.println("Adding header name " + invalidHeaderName + " to " + version + " request"); + // Field names are strings containing a subset of ASCII characters. + // This header name contains a unicode character, so it should fail + assertThrows(IllegalArgumentException.class, + () -> reqBuilder.header(invalidHeaderName, "something")); + } +} diff --git a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java index 6074a3a855b..596320d49e6 100644 --- a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java +++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java @@ -357,6 +357,13 @@ public class HttpClientBuilderTest { @Test public void testVersion() { HttpClient.Builder builder = HttpClient.newBuilder(); + try (var closer = closeable(builder)) { + assertTrue(closer.build().version() == Version.HTTP_2); + } + builder.version(Version.HTTP_3); + try (var closer = closeable(builder)) { + assertTrue(closer.build().version() == Version.HTTP_3); + } builder.version(Version.HTTP_2); try (var closer = closeable(builder)) { assertTrue(closer.build().version() == Version.HTTP_2); @@ -366,6 +373,10 @@ public class HttpClientBuilderTest { assertTrue(closer.build().version() == Version.HTTP_1_1); } assertThrows(NPE, () -> builder.version(null)); + builder.version(Version.HTTP_3); + try (var closer = closeable(builder)) { + assertTrue(closer.build().version() == Version.HTTP_3); + } builder.version(Version.HTTP_2); try (var closer = closeable(builder)) { assertTrue(closer.build().version() == Version.HTTP_2); diff --git a/test/jdk/java/net/httpclient/HttpClientClose.java b/test/jdk/java/net/httpclient/HttpClientClose.java index 0425d27a25e..f761396487c 100644 --- a/test/jdk/java/net/httpclient/HttpClientClose.java +++ b/test/jdk/java/net/httpclient/HttpClientClose.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -46,7 +46,9 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; @@ -57,13 +59,11 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Flow; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.Flow.Subscriber; import java.util.concurrent.Flow.Subscription; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; import javax.net.ssl.SSLContext; @@ -75,12 +75,14 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static java.lang.System.err; -import static java.lang.System.in; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -100,10 +102,15 @@ public class HttpClientClose implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "HttpClientClose message body"; static final int ITERATIONS = 3; @@ -111,10 +118,12 @@ public class HttpClientClose implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -160,19 +169,39 @@ public class HttpClientClose implements HttpServerAdapters { } } - private record ExchangeResult(int step, HttpResponse response) { - public static ExchangeResult ofStep(int step) { - return new ExchangeResult(step, null); + record ExchangeResult(int step, + Version version, + Http3DiscoveryMode config, + HttpResponse response, + boolean firstVersionMayNotMatch) { + + static ExchangeResult afterHead(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, false); } + + static ExchangeResult ofSequential(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, true); + } + ExchangeResult withResponse(HttpResponse response) { - return new ExchangeResult(step, response); + return new ExchangeResult(step(), version(), config(), response, firstVersionMayNotMatch()); } + + // Ensures that the input stream gets closed in case of assertion ExchangeResult assertResponseState() { + out.println(step + ": Got response: " + response); try { - out.println(step + ": Got response: " + response); + out.printf("%s: expect status 200 and version %s (%s) for %s%n", step, version, config, + response.request().uri()); assertEquals(response.statusCode(), 200); + if (step == 0 && version == HTTP_3 && firstVersionMayNotMatch) { + out.printf("%s: version not checked%n", step); + } else { + assertEquals(response.version(), version); + out.printf("%s: got expected version %s%n", step, response.version()); + } } catch (AssertionError error) { - out.printf("%s: Closing body due to assertion - %s", error); + out.printf("%s: Closing body due to assertion - %s", step, error); ensureClosed(this); throw error; } @@ -180,35 +209,62 @@ public class HttpClientClose implements HttpServerAdapters { } } + static String readBody(int i, HttpResponse resp) { + try (var in = resp.body()) { + out.println(i + ": reading body for " + resp.request().uri()); + var body = new String(in.readAllBytes(), StandardCharsets.UTF_8); + out.println(i + ": got body " + body); + return body; + } catch (IOException io) { + out.println(i + ": failed to read body"); + throw new UncheckedIOException(io); + } + } + + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting concurrent (%s) ----%n%n", uriString); + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting concurrent (%s, %s, %s) ----%n%n", uriString, version, config); Throwable failed = null; HttpClient toCheck = null; List> bodies = new ArrayList<>(); - try (HttpClient client = toCheck = HttpClient.newBuilder() + try (HttpClient client = toCheck = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build()) { TRACKER.track(client); + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; CompletableFuture bodyCF; final int si = i; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.afterHead(i, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) .thenApply(result::withResponse) .thenApplyAsync(ExchangeResult::assertResponseState, readerService) .thenApply(ExchangeResult::response); - bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) - .thenApply(HttpClientClose::readBody) + bodyCF = responseCF + .thenApplyAsync((resp) -> readBody(si, resp), readerService) .thenApply((s) -> { assertEquals(s, MESSAGE); return s; @@ -223,18 +279,25 @@ public class HttpClientClose implements HttpServerAdapters { } } assertTrue(toCheck.isTerminated()); - // assert all operations eventually terminate + + // Ensure all CF are eventually completed + out.printf("waiting for requests to complete%n"); CompletableFuture.allOf(bodies.toArray(new CompletableFuture[0])).get(); + out.printf("all requests completed%n"); + out.printf("%n---- end concurrent (%s, %s, %s): %s ----%n", + uriString, version, config, + failed == null ? "done" : failed.toString()); } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting sequential (%s) ----%n%n", uriString); + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting sequential (%s, %s, %s) ----%n%n", uriString, version, config); Throwable failed = null; HttpClient toCheck = null; - try (HttpClient client = toCheck = HttpClient.newBuilder() + try (HttpClient client = toCheck = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build()) { TRACKER.track(client); @@ -243,10 +306,11 @@ public class HttpClientClose implements HttpServerAdapters { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); final int si = i; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.ofSequential(si, version, config); CompletableFuture> responseCF; CompletableFuture bodyCF; responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) @@ -283,6 +347,7 @@ public class HttpClientClose implements HttpServerAdapters { if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); readerService = Executors.newCachedThreadPool(); + httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new ServerRequestHandler(), "/http1/exec/"); httpURI = "http://" + httpTestServer.serverAuthority() + "/http1/exec/retry"; @@ -297,10 +362,21 @@ public class HttpClientClose implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -313,6 +389,8 @@ public class HttpClientClose implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/HttpClientShutdown.java b/test/jdk/java/net/httpclient/HttpClientShutdown.java index 3c77881c8aa..192abcba39d 100644 --- a/test/jdk/java/net/httpclient/HttpClientShutdown.java +++ b/test/jdk/java/net/httpclient/HttpClientShutdown.java @@ -47,7 +47,9 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -77,11 +79,14 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -101,10 +106,15 @@ public class HttpClientShutdown implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "HttpClientShutdown message body"; static final int ITERATIONS = 3; @@ -112,10 +122,12 @@ public class HttpClientShutdown implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -180,17 +192,37 @@ public class HttpClientShutdown implements HttpServerAdapters { } } - private record ExchangeResult(int step, HttpResponse response) { - public static ExchangeResult ofStep(int step) { - return new ExchangeResult(step, null); + record ExchangeResult(int step, + Version version, + Http3DiscoveryMode config, + HttpResponse response, + boolean firstVersionMayNotMatch) { + + static ExchangeResult afterHead(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, false); } + + static ExchangeResult ofSequential(int step, Version version, Http3DiscoveryMode config) { + return new ExchangeResult(step, version, config, null, true); + } + ExchangeResult withResponse(HttpResponse response) { - return new ExchangeResult(step, response); + return new ExchangeResult(step(), version(), config(), response, firstVersionMayNotMatch()); } + + // Ensures that the input stream gets closed in case of assertion ExchangeResult assertResponseState() { + out.println(now() + step + ": Got response: " + response); try { - out.println(now() + step + ": Got response: " + response); + out.printf(now() + "%s: expect status 200 and version %s (%s) for %s%n", step, version, config, + response.request().uri()); assertEquals(response.statusCode(), 200); + if (step == 0 && version == HTTP_3 && firstVersionMayNotMatch) { + out.printf(now() + "%s: version not checked%n", step); + } else { + assertEquals(response.version(), version); + out.printf(now() + "%s: got expected version %s%n", step, response.version()); + } } catch (AssertionError error) { out.printf(now() + "%s: Closing body due to assertion - %s", step, error); ensureClosed(this); @@ -200,6 +232,15 @@ public class HttpClientShutdown implements HttpServerAdapters { } } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + static boolean hasExpectedMessage(IOException io) { String message = io.getMessage(); if (message == null) return false; @@ -232,11 +273,13 @@ public class HttpClientShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- %sstarting concurrent (%s) ----%n%n", now(), uriString); - HttpClient client = HttpClient.newBuilder() + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- %sstarting concurrent (%s, %s, %s) ----%n%n", + now(), uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -245,19 +288,24 @@ public class HttpClientShutdown implements HttpServerAdapters { Throwable failed = null; List> bodies = new ArrayList<>(); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf(now() + "Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; CompletableFuture bodyCF; final int si = i; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.afterHead(si, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) .thenApply(result::withResponse) - .thenApplyAsync(ExchangeResult::assertResponseState, readerService) + .thenApplyAsync(ExchangeResult::assertResponseState) .thenApply(ExchangeResult::response); bodyCF = responseCF.thenApplyAsync(HttpResponse::body, readerService) .thenApply(HttpClientShutdown::readBody) @@ -328,11 +376,13 @@ public class HttpClientShutdown implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- %sstarting sequential (%s) ----%n%n", now(), uriString); - HttpClient client = HttpClient.newBuilder() + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- %sstarting sequential (%s, %s, %s) ----%n%n", + now(), uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -345,12 +395,13 @@ public class HttpClientShutdown implements HttpServerAdapters { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf(now() + "Iteration %d request: %s%n", i, request.uri()); final int si = i; CompletableFuture> responseCF; CompletableFuture bodyCF; - ExchangeResult result = ExchangeResult.ofStep(si); + ExchangeResult result = ExchangeResult.ofSequential(si, version, config); responseCF = client.sendAsync(request, BodyHandlers.ofInputStream()) .thenApply(result::withResponse) .thenApplyAsync(ExchangeResult::assertResponseState, readerService) @@ -432,10 +483,21 @@ public class HttpClientShutdown implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); start = System.nanoTime(); } @@ -449,6 +511,8 @@ public class HttpClientShutdown implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java b/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java index baa64356fa7..4ddc0bdfdc2 100644 --- a/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java +++ b/test/jdk/java/net/httpclient/HttpGetInCancelledFuture.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; @@ -30,6 +31,8 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; import java.net.http.HttpResponse; import java.time.Duration; import java.util.List; @@ -47,20 +50,29 @@ import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.internal.net.http.common.OperationTrackers.Tracker; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; + +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /* * @test * @bug 8316580 - * @library /test/lib + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build HttpGetInCancelledFuture ReferenceTracker * @run junit/othervm -DuseReferenceTracker=false * HttpGetInCancelledFuture * @run junit/othervm -DuseReferenceTracker=true @@ -82,7 +94,9 @@ public class HttpGetInCancelledFuture { static ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; HttpClient makeClient(URI uri, Version version, Executor executor) { - var builder = HttpClient.newBuilder(); + var builder = version == HTTP_3 + ? HttpServerAdapters.createClientBuilderForH3() + : HttpClient.newBuilder(); if (uri.getScheme().equalsIgnoreCase("https")) { try { builder.sslContext(new SimpleSSLContext().get()); @@ -96,9 +110,19 @@ public class HttpGetInCancelledFuture { .build(); } - record TestCase(String url, int reqCount, Version version) {} + record TestCase(String url, int reqCount, Version version, Http3DiscoveryMode config) { + TestCase(String url, int reqCount, Version version) { + this(url, reqCount, version, null); + } + TestCase(String url, int reqCount, Http3DiscoveryMode config) { + this(url, reqCount, HTTP_3, null); + } + } + + // A server that doesn't accept - static volatile ServerSocket NOT_ACCEPTING; + static volatile ServerSocket NOT_ACCEPTING; + static volatile DatagramSocket NOT_RESPONDING; static List parameters() { ServerSocket ss = NOT_ACCEPTING; @@ -116,6 +140,28 @@ public class HttpGetInCancelledFuture { } } } + + DatagramSocket ds = NOT_RESPONDING; + boolean sameport = false; + if (ds == null) { + synchronized (HttpGetInCancelledFuture.class) { + if ((ds = NOT_RESPONDING) == null) { + try { + var loopback = InetAddress.getLoopbackAddress(); + try { + ds = new DatagramSocket(new InetSocketAddress(loopback, ss.getLocalPort())); + sameport = true; + } catch (IOException io) { + ds = new DatagramSocket(new InetSocketAddress(loopback,0)); + } + NOT_RESPONDING = ds; + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + } + } + URI http = URIBuilder.newBuilder() .loopback() .scheme("http") @@ -128,13 +174,25 @@ public class HttpGetInCancelledFuture { .port(ss.getLocalPort()) .path("/not-accepting/") .buildUnchecked(); + URI https3 = URIBuilder.newBuilder() + .loopback() + .scheme("https") + .port(ds.getLocalPort()) + .path("/not-responding/") + .buildUnchecked(); // use all HTTP versions, without and with TLS - return List.of( - new TestCase(http.toString(), 200, Version.HTTP_2), - new TestCase(http.toString(), 200, Version.HTTP_1_1), - new TestCase(https.toString(), 200, Version.HTTP_2), - new TestCase(https.toString(), 200, Version.HTTP_1_1) + var def = Stream.of( + new TestCase(https3.toString(), 200, HTTP_3_URI_ONLY), + new TestCase(http.toString(), 200, HTTP_2), + new TestCase(http.toString(), 200, HTTP_1_1), + new TestCase(https.toString(), 200, HTTP_2), + new TestCase(https.toString(), 200, HTTP_1_1) ); + var first = sameport + ? Stream.of(new TestCase(https3.toString(), 200, ANY)) + : Stream.empty(); + var cases= Stream.concat(first, def); + return cases.toList(); } @ParameterizedTest @@ -251,7 +309,7 @@ public class HttpGetInCancelledFuture { Throwable failed = null; try { try (final var scope = new TestTaskScope.ShutdownOnFailure()) { - launchAndProcessRequests(scope, httpClient, reqCount, dest); + launchAndProcessRequests(scope, httpClient, reqCount, version, dest); } finally { System.out.printf("StructuredTaskScope closed: STARTED=%s, SUCCESS=%s, INTERRUPT=%s, FAILED=%s%n", STARTED.get(), SUCCESS.get(), INTERRUPT.get(), FAILED.get()); @@ -311,10 +369,11 @@ public class HttpGetInCancelledFuture { TestTaskScope.ShutdownOnFailure scope, HttpClient httpClient, int reqCount, + Version version, URI dest) { for (int counter = 0; counter < reqCount; counter++) { scope.fork(() -> - getAndCheck(httpClient, dest) + getAndCheck(httpClient, dest, version) ); } try { @@ -335,19 +394,21 @@ public class HttpGetInCancelledFuture { final AtomicLong FAILED = new AtomicLong(); final AtomicLong STARTED = new AtomicLong(); final CopyOnWriteArrayList EXCEPTIONS = new CopyOnWriteArrayList<>(); - private String getAndCheck(HttpClient httpClient, URI url) { + private String getAndCheck(HttpClient httpClient, URI url, Version version) { STARTED.incrementAndGet(); - final var response = sendRequest(httpClient, url); + final var response = sendRequest(httpClient, url, version); String res = response.body(); int statusCode = response.statusCode(); assertEquals(200, statusCode); return res; } - private HttpResponse sendRequest(HttpClient httpClient, URI url) { + private HttpResponse sendRequest(HttpClient httpClient, URI url, Version version) { var id = ID.incrementAndGet(); try { - var request = HttpRequest.newBuilder(url).GET().build(); + var builder = HttpRequest.newBuilder(url).version(version).GET(); + if (version == HTTP_3) builder.setOption(HttpOption.H3_DISCOVERY, HTTP_3_URI_ONLY); + var request = builder.build(); var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // System.out.println("Got response for " + id + ": " + response); SUCCESS.incrementAndGet(); @@ -372,16 +433,18 @@ public class HttpGetInCancelledFuture { if (error != null) throw error; } finally { ServerSocket ss; + DatagramSocket ds; synchronized (HttpGetInCancelledFuture.class) { ss = NOT_ACCEPTING; NOT_ACCEPTING = null; + ds = NOT_RESPONDING; + NOT_RESPONDING = null; } - if (ss != null) { - try { - ss.close(); - } catch (IOException io) { - throw new UncheckedIOException(io); - } + try (var ss1 = ss; var ds1 = ds;) { + System.out.printf("Cleaning up: ss=%s, ds=%s%n", + ss1.getLocalSocketAddress(), ds1.getLocalSocketAddress()); + } catch (IOException io) { + throw new UncheckedIOException(io); } } } diff --git a/test/jdk/java/net/httpclient/HttpRedirectTest.java b/test/jdk/java/net/httpclient/HttpRedirectTest.java index dedcc36dda7..358b908a03c 100644 --- a/test/jdk/java/net/httpclient/HttpRedirectTest.java +++ b/test/jdk/java/net/httpclient/HttpRedirectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,6 +29,9 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.*; import javax.net.ssl.SSLContext; @@ -93,12 +96,14 @@ public class HttpRedirectTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -111,8 +116,7 @@ public class HttpRedirectTest implements HttpServerAdapters { TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // Used by the client public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .followRedirects(HttpClient.Redirect.ALWAYS) @@ -123,6 +127,7 @@ public class HttpRedirectTest implements HttpServerAdapters { @DataProvider(name="uris") Object[][] testURIs() throws URISyntaxException { List uris = List.of( + http3URI.resolve("direct/orig/"), http1URI.resolve("direct/orig/"), https1URI.resolve("direct/orig/"), https1URI.resolve("proxy/orig/"), @@ -195,6 +200,13 @@ public class HttpRedirectTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/HttpRedirectTest/https2/"); + // HTTPS/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestRedirectHandler("https", http3Server), + "/HttpRedirectTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/HttpRedirectTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -255,10 +267,18 @@ public class HttpRedirectTest implements HttpServerAdapters { } } + private HttpRequest.Builder newRequestBuilder(URI u) { + var builder = HttpRequest.newBuilder(u); + if (u.getRawPath().contains("/http3/")) { + builder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "uris") public void testPOST(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .POST(HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); // POST is not considered idempotent. testNonIdempotent(u, request, code, method); @@ -268,7 +288,7 @@ public class HttpRedirectTest implements HttpServerAdapters { public void testPUT(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); System.out.println("Testing with " + u); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .PUT(HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); // PUT is considered idempotent. testIdempotent(u, request, code, method); @@ -278,7 +298,7 @@ public class HttpRedirectTest implements HttpServerAdapters { public void testFoo(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); System.out.println("Testing with " + u); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .method("FOO", HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); // FOO is considered idempotent. @@ -289,7 +309,7 @@ public class HttpRedirectTest implements HttpServerAdapters { public void testGet(URI uri, int code, String method) throws Exception { URI u = uri.resolve("foo?n=" + requestCounter.incrementAndGet()); System.out.println("Testing with " + u); - HttpRequest request = HttpRequest.newBuilder(u) + HttpRequest request = newRequestBuilder(u) .method("GET", HttpRequest.BodyPublishers.ofString(REQUEST_BODY)).build(); CompletableFuture> respCf = @@ -320,6 +340,7 @@ public class HttpRedirectTest implements HttpServerAdapters { https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java b/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java index 9401c10cdf2..4544c85c5e8 100644 --- a/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java +++ b/test/jdk/java/net/httpclient/HttpRequestBuilderTest.java @@ -23,11 +23,14 @@ import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -36,6 +39,8 @@ import java.util.stream.Stream; import java.net.http.HttpRequest; import static java.net.http.HttpRequest.BodyPublishers.ofString; import static java.net.http.HttpRequest.BodyPublishers.noBody; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /* * @test @@ -206,6 +211,15 @@ public class HttpRequestBuilderTest { builder = test2("method", builder, builder::method, null, ofString("foo"), NullPointerException.class); + + builder = test2("setOption", builder, builder::setOption, + (HttpOption)null, (Http3DiscoveryMode) null, + NullPointerException.class); + + builder = test2("setOption", builder, builder::setOption, + (HttpOption)null, HTTP_3_URI_ONLY, + NullPointerException.class); + // see JDK-8170093 // // builder = test2("method", builder, builder::method, "foo", @@ -268,6 +282,20 @@ public class HttpRequestBuilderTest { HttpRequest defaultHeadReq = new NotOverriddenHEADImpl().HEAD().uri(TEST_URI).build(); assertEquals("HEAD", defaultHeadReq.method(), "Method"); assertEquals(false, defaultHeadReq.bodyPublisher().isEmpty(), "Body publisher absence"); + HttpRequest defaultReqWithoutOption = new NotOverriddenHEADImpl().HEAD().uri(TEST_URI).build(); + assertEquals(Optional.empty(), defaultReqWithoutOption.getOption(H3_DISCOVERY), "default without options"); + HttpRequest defaultReqWithOption = new NotOverriddenHEADImpl().HEAD().uri(TEST_URI) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + assertEquals(Optional.empty(), defaultReqWithOption.getOption(H3_DISCOVERY), "default with options"); + HttpRequest reqWithoutOption = HttpRequest.newBuilder().HEAD().uri(TEST_URI).build(); + assertEquals(Optional.empty(), reqWithoutOption.getOption(H3_DISCOVERY), "req without options"); + HttpRequest reqWithOption = HttpRequest.newBuilder().HEAD().uri(TEST_URI) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + assertEquals(Optional.of(HTTP_3_URI_ONLY), reqWithOption.getOption(H3_DISCOVERY), "req with options"); + HttpRequest resetReqWithOption = HttpRequest.newBuilder().HEAD().uri(TEST_URI) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .setOption(H3_DISCOVERY, null).build(); + assertEquals(Optional.empty(), resetReqWithOption.getOption(H3_DISCOVERY), "req with option reset"); verifyCopy(); @@ -383,6 +411,7 @@ public class HttpRequestBuilderTest { .method("GET", noBody()) .expectContinue(true) .timeout(Duration.ofSeconds(0xBEEF)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) .version(HttpClient.Version.HTTP_2); // Create the original and the _copy_ requests @@ -399,6 +428,8 @@ public class HttpRequestBuilderTest { assertEquals(request.expectContinue(), copiedRequest.expectContinue(), "Expect continue setting"); assertEquals(request.timeout(), copiedRequest.timeout(), "Timeout"); assertEquals(request.version(), copiedRequest.version(), "Version"); + assertEquals(request.getOption(H3_DISCOVERY), copiedRequest.getOption(H3_DISCOVERY), "H3_DISCOVERY option"); + assertEquals(Optional.of(HTTP_3_URI_ONLY), copiedRequest.getOption(H3_DISCOVERY), "copied H3_DISCOVERY option"); // Verify headers assertEquals(request.headers().map(), Map.of("X-Foo", List.of("1")), "Request headers"); @@ -415,66 +446,85 @@ public class HttpRequestBuilderTest { // doesn't override the default HEAD() method private static final class NotOverriddenHEADImpl implements HttpRequest.Builder { - private final HttpRequest.Builder underlying = HttpRequest.newBuilder(); + private final HttpRequest.Builder underlying; + + NotOverriddenHEADImpl() { + this(HttpRequest.newBuilder()); + } + + NotOverriddenHEADImpl(HttpRequest.Builder underlying) { + this.underlying = underlying; + } @Override public HttpRequest.Builder uri(URI uri) { - return this.underlying.uri(uri); + underlying.uri(uri); + return this; } @Override public HttpRequest.Builder expectContinue(boolean enable) { - return this.underlying.expectContinue(enable); + underlying.expectContinue(enable); return this; } @Override public HttpRequest.Builder version(HttpClient.Version version) { - return this.underlying.version(version); + this.underlying.version(version); + return this; } @Override public HttpRequest.Builder header(String name, String value) { - return this.underlying.header(name, value); + this.underlying.header(name, value); + return this; } @Override public HttpRequest.Builder headers(String... headers) { - return this.underlying.headers(headers); + underlying.headers(headers); + return this; } @Override public HttpRequest.Builder timeout(Duration duration) { - return this.underlying.timeout(duration); + this.underlying.timeout(duration); + return this; } @Override public HttpRequest.Builder setHeader(String name, String value) { - return this.underlying.setHeader(name, value); + underlying.setHeader(name, value); + return this; } @Override public HttpRequest.Builder GET() { - return this.underlying.GET(); + this.underlying.GET(); + return this; } @Override public HttpRequest.Builder POST(HttpRequest.BodyPublisher bodyPublisher) { - return this.underlying.POST(bodyPublisher); + this.underlying.POST(bodyPublisher); + return this; } @Override public HttpRequest.Builder PUT(HttpRequest.BodyPublisher bodyPublisher) { - return this.underlying.PUT(bodyPublisher); + this.underlying.PUT(bodyPublisher); + return this; } @Override public HttpRequest.Builder DELETE() { - return this.underlying.DELETE(); + this.underlying.DELETE(); + return this; } @Override public HttpRequest.Builder method(String method, HttpRequest.BodyPublisher bodyPublisher) { - return this.underlying.method(method, bodyPublisher); + this.underlying.method(method, bodyPublisher); + return this; } @Override @@ -484,7 +534,7 @@ public class HttpRequestBuilderTest { @Override public HttpRequest.Builder copy() { - return this.underlying.copy(); + return new NotOverriddenHEADImpl(underlying.copy()); } } } diff --git a/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.java b/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.java index eab9ecfc2c7..d7598ede3be 100644 --- a/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.java +++ b/test/jdk/java/net/httpclient/HttpRequestNewBuilderTest.java @@ -1,5 +1,5 @@ /* -* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. +* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -39,6 +39,9 @@ import java.util.function.BiConsumer; import java.util.function.BiPredicate; import static java.net.http.HttpClient.Version.HTTP_2; import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import org.testng.annotations.Test; @@ -66,6 +69,7 @@ public class HttpRequestNewBuilderTest { new NamedAssertion("timeout", (r1, r2) -> assertEquals(r1.timeout(), r2.timeout())), new NamedAssertion("version", (r1, r2) -> assertEquals(r1.version(), r2.version())), new NamedAssertion("headers", (r1, r2) -> assertEquals(r1.headers(), r2.headers())), + new NamedAssertion("options", (r1, r2) -> assertEquals(r1.getOption(H3_DISCOVERY), r2.getOption(H3_DISCOVERY))), new NamedAssertion("expectContinue", (r1, r2) -> assertEquals(r1.expectContinue(), r2.expectContinue())), new NamedAssertion("method", (r1, r2) -> { assertEquals(r1.method(), r2.method()); @@ -141,6 +145,9 @@ public class HttpRequestNewBuilderTest { { HttpRequest.newBuilder(URI.create("https://all-fields-1/")).GET().expectContinue(true).version(HTTP_2) .timeout(Duration.ofSeconds(1)).header("testName1", "testValue1").build() }, + { HttpRequest.newBuilder(URI.create("https://all-fields-2/")).GET().expectContinue(true).version(HTTP_2) + .timeout(Duration.ofSeconds(1)).header("testName1", "testValue1") + .setOption(H3_DISCOVERY, ANY).build() }, }; } @@ -313,6 +320,15 @@ public class HttpRequestNewBuilderTest { assertAllOtherElementsEqual(r, request, "headers"); } + @Test(dataProvider = "testRequests") + public void testSetOption(HttpRequest request) { + BiPredicate filter = (n, v) -> true; + + var r = HttpRequest.newBuilder(request, filter).setOption(H3_DISCOVERY, ALT_SVC).build(); + assertEquals(r.getOption(H3_DISCOVERY).get(), ALT_SVC); + assertAllOtherElementsEqual(r, request, "options"); + } + @Test(dataProvider = "testRequests") public void testRemoveHeader(HttpRequest request) { if(!request.headers().map().isEmpty()) { @@ -325,6 +341,18 @@ public class HttpRequestNewBuilderTest { assertEquals(r.headers().map(), HttpHeaders.of(request.headers().map(), filter).map()); } + @Test(dataProvider = "testRequests") + public void testRemoveOption(HttpRequest request) { + if(!request.getOption(H3_DISCOVERY).isEmpty()) { + assertEquals(request.getOption(H3_DISCOVERY).get(), ANY); + } + + var r = HttpRequest.newBuilder(request, (a, b) -> true) + .setOption(H3_DISCOVERY, null).build(); + assertTrue(r.getOption(H3_DISCOVERY).isEmpty()); + assertAllOtherElementsEqual(r, request, "options"); + } + @Test(dataProvider = "testRequests") public void testRemoveSingleHeaderValue(HttpRequest request) { if(!request.headers().map().isEmpty()) { diff --git a/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java b/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java index b6c13c51ee1..d1dc8b0a5f5 100644 --- a/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java +++ b/test/jdk/java/net/httpclient/HttpResponseConnectionLabelTest.java @@ -29,10 +29,13 @@ * @build jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.test.lib.net.SimpleSSLContext * - * @comment Use a higher idle timeout to increase the chances of the same connection being used for sequential HTTP requests - * @run junit/othervm -Djdk.httpclient.keepalive.timeout=120 HttpResponseConnectionLabelTest + * @comment Use a higher idle timeout to increase the chances of + * the same connection being used for sequential HTTP requests + * @run junit/othervm -Djdk.httpclient.keepalive.timeout=120 + * HttpResponseConnectionLabelTest */ +import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.internal.net.http.common.Logger; @@ -55,6 +58,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.Charset; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -64,6 +68,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -83,8 +89,12 @@ class HttpResponseConnectionLabelTest { private static final SSLContext SSL_CONTEXT = createSslContext(); - // Start with a fresh client having no connections in the pool - private final HttpClient client = HttpClient.newBuilder().sslContext(SSL_CONTEXT).proxy(NO_PROXY).build(); + // For each test instance, start with a fresh client having no connections in the pool + private final HttpClient client = HttpServerAdapters + .createClientBuilderForH3() + .sslContext(SSL_CONTEXT) + .proxy(NO_PROXY) + .build(); // Primary server-client pairs @@ -96,6 +106,8 @@ class HttpResponseConnectionLabelTest { private static final ServerRequestPair PRI_HTTPS2 = ServerRequestPair.of(Version.HTTP_2, true); + private static final ServerRequestPair PRI_HTTP3 = ServerRequestPair.of(Version.HTTP_3, true); + // Secondary server-client pairs private static final ServerRequestPair SEC_HTTP1 = ServerRequestPair.of(Version.HTTP_1_1, false); @@ -106,6 +118,8 @@ class HttpResponseConnectionLabelTest { private static final ServerRequestPair SEC_HTTPS2 = ServerRequestPair.of(Version.HTTP_2, true); + private static final ServerRequestPair SEC_HTTP3 = ServerRequestPair.of(Version.HTTP_3, true); + private static SSLContext createSslContext() { try { return new SimpleSSLContext().get(); @@ -140,8 +154,8 @@ class HttpResponseConnectionLabelTest { AtomicReference serverResponseLatchRef = new AtomicReference<>(); server.addHandler(createServerHandler(serverId, serverResponseLatchRef), handlerPath); - // Create the client and the request - HttpRequest request = HttpRequest.newBuilder(requestUri).version(version).build(); + // Create the request + HttpRequest request = createRequest(version, requestUri); // Create the pair ServerRequestPair pair = new ServerRequestPair( @@ -168,13 +182,15 @@ class HttpResponseConnectionLabelTest { // - Only the HTTP/1.1 test server gets wedged when running // tests involving parallel request handling. // - // - The HTTP/2 test server creates its own sufficiently sized - // executor, and the thread names used there makes it easy to - // find which server they belong to. + // - The HTTP/2 and HTTP/3 test servers create their own + // sufficiently sized executor, and the thread names used + // there makes it easy to find which server they belong to. executorRef[0] = Version.HTTP_1_1.equals(version) ? createExecutor(version, secure, serverId) : null; - return HttpTestServer.create(version, sslContext, executorRef[0]); + return Version.HTTP_3.equals(version) + ? HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, executorRef[0]) + : HttpTestServer.create(version, sslContext, executorRef[0]); } catch (IOException exception) { throw new UncheckedIOException(exception); } @@ -196,7 +212,7 @@ class HttpResponseConnectionLabelTest { return (exchange) -> { String responseBody = "" + SERVER_RESPONSE_COUNTER.getAndIncrement(); String connectionKey = exchange.getConnectionKey(); - LOGGER.log("Server[%d] has received request (connectionKey=%s)", serverId, connectionKey); + LOGGER.log("Server[%s] has received request (connectionKey=%s)", serverId, connectionKey); try (exchange) { // Participate in the latch count down @@ -232,6 +248,14 @@ class HttpResponseConnectionLabelTest { }; } + private static HttpRequest createRequest(Version version, URI requestUri) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(requestUri).version(version); + if (Version.HTTP_3.equals(version)) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return requestBuilder.build(); + } + @Override public String toString() { String version = server.getVersion().toString(); @@ -244,7 +268,9 @@ class HttpResponseConnectionLabelTest { static void closeServers() { Exception[] exceptionRef = {null}; Stream - .of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2) + .of( + PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, PRI_HTTP3, + SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2, SEC_HTTP3) .flatMap(pair -> Stream.of( pair.server::stop, () -> { if (pair.executor != null) { pair.executor.shutdownNow(); } })) @@ -274,7 +300,8 @@ class HttpResponseConnectionLabelTest { PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, - PRI_HTTPS2 + PRI_HTTPS2, + PRI_HTTP3 }; } @@ -283,8 +310,9 @@ class HttpResponseConnectionLabelTest { void testParallelRequestsToSameServer(ServerRequestPair pair) throws Exception { // There is no implementation-agnostic reliable way to force admission - // of multiple connections targeting the same server to an HTTP/2 pool. - if (Version.HTTP_2.equals(pair.server.getVersion())) { + // of multiple connections targeting the same server to an HTTP/2 or + // HTTP/3 client connection pool. + if (Set.of(Version.HTTP_2, Version.HTTP_3).contains(pair.server.getVersion())) { return; } @@ -359,9 +387,9 @@ class HttpResponseConnectionLabelTest { static Stream testParallelRequestsToDifferentServers() { return Stream - .of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2) + .of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, PRI_HTTP3) .flatMap(source -> Stream - .of(SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2) + .of(SEC_HTTP1, SEC_HTTPS1, SEC_HTTP2, SEC_HTTPS2, SEC_HTTP3) .map(target -> Arguments.of(source, target))); } @@ -440,7 +468,7 @@ class HttpResponseConnectionLabelTest { } static Stream testSerialRequestsToSameServer() { - return Stream.of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2); + return Stream.of(PRI_HTTP1, PRI_HTTPS1, PRI_HTTP2, PRI_HTTPS2, PRI_HTTP3); } @ParameterizedTest diff --git a/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java b/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java index b87e7ea8e49..e0bda0b0071 100644 --- a/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java +++ b/test/jdk/java/net/httpclient/HttpResponseLimitingTest.java @@ -34,6 +34,7 @@ * @run junit HttpResponseLimitingTest */ +import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; @@ -66,6 +67,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.copyOfRange; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -89,15 +95,17 @@ class HttpResponseLimitingTest { */ private static final String RESPONSE_HEADER_VALUE = "!".repeat(RESPONSE_BODY.length + 1); - private static final ServerClientPair HTTP1 = ServerClientPair.of(HttpClient.Version.HTTP_1_1, false); + private static final ServerClientPair H1 = ServerClientPair.of(HTTP_1_1, false); - private static final ServerClientPair HTTPS1 = ServerClientPair.of(HttpClient.Version.HTTP_1_1, true); + private static final ServerClientPair H1S = ServerClientPair.of(HTTP_1_1, true); - private static final ServerClientPair HTTP2 = ServerClientPair.of(HttpClient.Version.HTTP_2, false); + private static final ServerClientPair H2 = ServerClientPair.of(HTTP_2, false); - private static final ServerClientPair HTTPS2 = ServerClientPair.of(HttpClient.Version.HTTP_2, true); + private static final ServerClientPair H2S = ServerClientPair.of(HTTP_2, true); - private record ServerClientPair(HttpTestServer server, HttpClient client, HttpRequest request) { + private static final ServerClientPair H3 = ServerClientPair.of(HTTP_3, true); + + private record ServerClientPair(HttpTestServer server, HttpClient client, HttpRequest request, boolean secure) { private static final SSLContext SSL_CONTEXT = createSslContext(); @@ -128,7 +136,7 @@ class HttpResponseLimitingTest { // Create the server and the request URI SSLContext sslContext = secure ? SSL_CONTEXT : null; HttpTestServer server = createServer(version, sslContext); - String handlerPath = "/"; + String handlerPath = "/" + /* salting the path: */ HttpResponseLimitingTest.class.getSimpleName(); String requestUriScheme = secure ? "https" : "http"; URI requestUri = URI.create(requestUriScheme + "://" + server.serverAuthority() + handlerPath); @@ -146,29 +154,40 @@ class HttpResponseLimitingTest { // Create the client and the request HttpClient client = createClient(version, sslContext); - HttpRequest request = HttpRequest.newBuilder(requestUri).version(version).build(); + HttpRequest request = createRequest(version, requestUri); // Create the pair - return new ServerClientPair(server, client, request); + return new ServerClientPair(server, client, request, secure); } private static HttpTestServer createServer(HttpClient.Version version, SSLContext sslContext) { try { - return HttpTestServer.create(version, sslContext); + return HTTP_3.equals(version) + ? HttpTestServer.create(HTTP_3_URI_ONLY, sslContext) + : HttpTestServer.create(version, sslContext); } catch (IOException exception) { throw new UncheckedIOException(exception); } } private static HttpClient createClient(HttpClient.Version version, SSLContext sslContext) { - HttpClient.Builder builder = HttpClient.newBuilder().version(version).proxy(NO_PROXY); + HttpClient.Builder builder = HttpServerAdapters.createClientBuilderFor(version) + .version(version).proxy(NO_PROXY); if (sslContext != null) { builder.sslContext(sslContext); } return builder.build(); } + private static HttpRequest createRequest(HttpClient.Version version, URI requestUri) { + HttpRequest.Builder builder = HttpRequest.newBuilder(requestUri).version(version); + if (HTTP_3.equals(version)) { + builder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder.build(); + } + private HttpResponse request(BodyHandler downstreamHandler, long capacity) throws Exception { var handler = BodyHandlers.limiting(downstreamHandler, capacity); return client.send(request, handler); @@ -176,8 +195,9 @@ class HttpResponseLimitingTest { @Override public String toString() { - String version = client.version().toString(); - return client.sslContext() != null ? version.replaceFirst("_", "S_") : version; + HttpClient.Version version = client.version(); + String versionString = version.toString(); + return secure && !HTTP_3.equals(version) ? versionString.replaceFirst("_", "S_") : versionString; } } @@ -186,7 +206,7 @@ class HttpResponseLimitingTest { static void closeServerClientPairs() { Exception[] exceptionRef = {null}; Stream - .of(HTTP1, HTTPS1, HTTP2, HTTPS2) + .of(H1, H1S, H2, H2S, H3) .flatMap(pair -> Stream.of( pair.client::close, pair.server::stop)) @@ -306,7 +326,7 @@ class HttpResponseLimitingTest { private static Arguments[] capacityArgs(long... capacities) { return Stream - .of(HTTP1, HTTPS1, HTTP2, HTTPS2) + .of(H1, H1S, H2, H2S, H3) .flatMap(pair -> Arrays .stream(capacities) .mapToObj(capacity -> Arguments.of(pair, capacity))) diff --git a/test/jdk/java/net/httpclient/HttpSlowServerTest.java b/test/jdk/java/net/httpclient/HttpSlowServerTest.java index a71eaf746ad..ca841cd8ae9 100644 --- a/test/jdk/java/net/httpclient/HttpSlowServerTest.java +++ b/test/jdk/java/net/httpclient/HttpSlowServerTest.java @@ -52,6 +52,9 @@ import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -62,8 +65,9 @@ import static java.net.http.HttpClient.Version.HTTP_2; * DigestEchoServer HttpSlowServerTest * jdk.httpclient.test.lib.common.TestServerConfigurator * @run main/othervm/timeout=480 -Dtest.requiresHost=true - * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.httpclient.HttpClient.log=errors,headers,quic:hs * -Djdk.internal.httpclient.debug=false + * -Djdk.httpclient.quic.maxInitialTimeout=60 * HttpSlowServerTest * */ @@ -97,12 +101,14 @@ public class HttpSlowServerTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -115,8 +121,7 @@ public class HttpSlowServerTest implements HttpServerAdapters { TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // Used by the client public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .proxy(ps); @@ -155,6 +160,12 @@ public class HttpSlowServerTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/HttpSlowServerTest/https2/"); + // HTTP/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestSlowHandler(), "/HttpSlowServerTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/HttpSlowServerTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -185,9 +196,10 @@ public class HttpSlowServerTest implements HttpServerAdapters { } public void run(String... args) throws Exception { - List serverURIs = List.of(http1URI, http2URI, https1URI, https2URI); + List serverURIs = List.of(http3URI, http1URI, http2URI, https1URI, https2URI); for (int i=0; i<20; i++) { for (URI base : serverURIs) { + if (base.getRawPath().contains("/http3/")) continue; // proxy not supported if (base.getScheme().equalsIgnoreCase("https")) { URI proxy = i % 1 == 0 ? base.resolve(URI.create("proxy/foo?n="+requestCounter.incrementAndGet())) : base.resolve(URI.create("direct/foo?n="+requestCounter.incrementAndGet())); @@ -205,7 +217,11 @@ public class HttpSlowServerTest implements HttpServerAdapters { public void test(URI uri) throws Exception { System.out.println("Testing with " + uri); pending.add(uri); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + HttpRequest request = builder.build(); CompletableFuture> resp = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .whenComplete((r, t) -> this.requestCompleted(request, r, t)); @@ -228,6 +244,7 @@ public class HttpSlowServerTest implements HttpServerAdapters { https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/ISO_8859_1_Test.java b/test/jdk/java/net/httpclient/ISO_8859_1_Test.java index e81b0130418..a5465ad5103 100644 --- a/test/jdk/java/net/httpclient/ISO_8859_1_Test.java +++ b/test/jdk/java/net/httpclient/ISO_8859_1_Test.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,50 +41,31 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; -import java.net.URL; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublisher; -import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.Flow.Subscription; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.LongStream; -import java.util.stream.Stream; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; -import org.testng.Assert; import org.testng.ITestContext; import org.testng.ITestResult; import org.testng.SkipException; @@ -98,24 +79,28 @@ import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; public class ISO_8859_1_Test implements HttpServerAdapters { SSLContext sslContext; DummyServer http1DummyServer; - HttpServerAdapters.HttpTestServer http1TestServer; // HTTP/1.1 ( http ) - HttpServerAdapters.HttpTestServer https1TestServer; // HTTPS/1.1 ( https ) - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http1TestServer; // HTTP/1.1 ( http ) + HttpTestServer https1TestServer; // HTTPS/1.1 ( https ) + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http1Dummy; String http1URI; String https1URI; String http2URI; String https2URI; + String http3URI; static final int RESPONSE_CODE = 200; static final int ITERATION_COUNT = 4; @@ -216,6 +201,7 @@ public class ISO_8859_1_Test implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI, http1Dummy, http1URI, https1URI, @@ -243,9 +229,12 @@ public class ISO_8859_1_Test implements HttpServerAdapters { return result; } - private HttpClient makeNewClient() { + private HttpClient makeNewClient(Version version) { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + var builder = version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -253,14 +242,24 @@ public class ISO_8859_1_Test implements HttpServerAdapters { return TRACKER.track(client); } - HttpClient newHttpClient(boolean share) { - if (!share) return makeNewClient(); + Version version(String uri) { + if (uri == null) return null; + if (uri.contains("/http1/")) return HTTP_1_1; + if (uri.contains("/https1/")) return HTTP_1_1; + if (uri.contains("/http2/")) return HTTP_2; + if (uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) return HTTP_3; + return null; + } + + HttpClient newHttpClient(String uri, boolean share) { + if (!share) return makeNewClient(version(uri)); HttpClient shared = sharedClient; if (shared != null) return shared; synchronized (this) { shared = sharedClient; if (shared == null) { - shared = sharedClient = makeNewClient(); + shared = sharedClient = makeNewClient(HTTP_3); } return shared; } @@ -277,16 +276,25 @@ public class ISO_8859_1_Test implements HttpServerAdapters { return (Exception)c; } + private static HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void test(String uri, boolean sameClient) throws Exception { checkSkip(); System.out.println("Request to " + uri); - HttpClient client = newHttpClient(sameClient); + HttpClient client = newHttpClient(uri, sameClient); List>> cfs = new ArrayList<>(); for (int i = 0; i < ITERATION_COUNT; i++) { - HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "/" + i)) + HttpRequest request = newRequestBuilder(URI.create(uri + "/" + i)) .build(); cfs.add(client.sendAsync(request, BodyHandlers.ofString())); } @@ -431,12 +439,17 @@ public class ISO_8859_1_Test implements HttpServerAdapters { https2TestServer.addHandler(handler, "/https2/server/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/server/x"; - serverCount.addAndGet(5); + http3TestServer = HttpServerAdapters.HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(handler, "/http3/server/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/server/x"; + + serverCount.addAndGet(6); http1TestServer.start(); https1TestServer.start(); http2TestServer.start(); https2TestServer.start(); http1DummyServer.start(); + http3TestServer.start(); } @AfterTest @@ -452,6 +465,7 @@ public class ISO_8859_1_Test implements HttpServerAdapters { http2TestServer.stop(); https2TestServer.stop(); http1DummyServer.close(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/IdleConnectionTimeoutTest.java b/test/jdk/java/net/httpclient/IdleConnectionTimeoutTest.java new file mode 100644 index 00000000000..07bb0be455c --- /dev/null +++ b/test/jdk/java/net/httpclient/IdleConnectionTimeoutTest.java @@ -0,0 +1,360 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.BodyOutputStream; +import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; +import jdk.httpclient.test.lib.http2.Http2TestServerConnection; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.net.SocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2Handler; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.net.http.HttpClient.Version.HTTP_2; +import static org.testng.Assert.assertEquals; + +/* + * @test + * @bug 8288717 + * @summary Tests that when the idle connection timeout is configured for a HTTP connection, + * then the connection is closed if it has been idle for that long + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.httpclient.test.lib.http3.Http3TestServer + * + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout=1 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout=20 + * IdleConnectionTimeoutTest + * + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=1 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=20 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=abc + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h2=-1 + * IdleConnectionTimeoutTest + * + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=1 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=20 + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=abc + * IdleConnectionTimeoutTest + * @run testng/othervm -Djdk.httpclient.HttpClient.log=all -Djdk.httpclient.keepalive.timeout.h3=-1 + * IdleConnectionTimeoutTest + */ +public class IdleConnectionTimeoutTest { + + URI timeoutUriH2, noTimeoutUriH2, timeoutUriH3, noTimeoutUriH3, getH3; + SSLContext sslContext; + static volatile QuicServerConnection latestServerConn; + final String KEEP_ALIVE_PROPERTY = "jdk.httpclient.keepalive.timeout"; + final String IDLE_CONN_PROPERTY_H2 = "jdk.httpclient.keepalive.timeout.h2"; + final String IDLE_CONN_PROPERTY_H3 = "jdk.httpclient.keepalive.timeout.h3"; + final String TIMEOUT_PATH = "/serverTimeoutHandler"; + final String NO_TIMEOUT_PATH = "/noServerTimeoutHandler"; + static Http2TestServer http2TestServer; + static Http3TestServer http3TestServer; + static final PrintStream testLog = System.err; + + @BeforeTest + public void setup() throws Exception { + http2TestServer = new Http2TestServer(false, 0); + http2TestServer.addHandler(new ServerTimeoutHandlerH2(), TIMEOUT_PATH); + http2TestServer.addHandler(new ServerNoTimeoutHandlerH2(), NO_TIMEOUT_PATH); + http2TestServer.setExchangeSupplier(TestExchange::new); + + sslContext = new SimpleSSLContext().get(); + http3TestServer = new Http3TestServer(sslContext) { + @Override + public boolean acceptIncoming(SocketAddress source, QuicServerConnection quicConn) { + final boolean accepted = super.acceptIncoming(source, quicConn); + if (accepted) { + // Quic Connection maps to Http3Connection, can use this to verify h3 timeouts + latestServerConn = quicConn; + } + return accepted; + } + }; + http3TestServer.addHandler(TIMEOUT_PATH, new ServerTimeoutHandlerH3()); + http3TestServer.addHandler(NO_TIMEOUT_PATH, new ServerNoTimeoutHandlerH3()); + + http2TestServer.start(); + http3TestServer.start(); + int port = http2TestServer.getAddress().getPort(); + timeoutUriH2 = URIBuilder.newBuilder() + .scheme("http") + .loopback() + .port(port) + .path(TIMEOUT_PATH) + .build(); + noTimeoutUriH2 = URIBuilder.newBuilder() + .scheme("http") + .loopback() + .port(port) + .path(NO_TIMEOUT_PATH) + .build(); + + port = http3TestServer.getAddress().getPort(); + getH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(port) + .path("/get") + .build(); + timeoutUriH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(port) + .path(TIMEOUT_PATH) + .build(); + noTimeoutUriH3 = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(port) + .path(NO_TIMEOUT_PATH) + .build(); + } + + @Test + public void testRoot() { + String keepAliveVal = System.getProperty(KEEP_ALIVE_PROPERTY); + String idleConnectionH2Val = System.getProperty(IDLE_CONN_PROPERTY_H2); + String idleConnectionH3Val = System.getProperty(IDLE_CONN_PROPERTY_H3); + + if (keepAliveVal != null) { + try (HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build()) { + // test H2 inherits value + testLog.println("Testing HTTP/2 connections set idleConnectionTimeout value to keep alive value"); + test(hc, keepAliveVal, HTTP_2, timeoutUriH2, noTimeoutUriH2); + } + try (HttpClient hc = HttpServerAdapters.createClientBuilderForH3().sslContext(sslContext).build()) { + // test H3 inherits value + testLog.println("Testing HTTP/3 connections set idleConnectionTimeout value to keep alive value"); + test(hc, keepAliveVal, HTTP_3, timeoutUriH3, noTimeoutUriH3); + } + } else if (idleConnectionH2Val != null) { + try (HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build()) { + testLog.println("Testing HTTP/2 idleConnectionTimeout"); + test(hc, idleConnectionH2Val, HTTP_2, timeoutUriH2, noTimeoutUriH2); + } + } else if (idleConnectionH3Val != null) { + try (HttpClient hc = HttpServerAdapters.createClientBuilderForH3().sslContext(sslContext).build()) { + testLog.println("Testing HTTP/3 idleConnectionTimeout"); + test(hc, idleConnectionH3Val, HTTP_3, timeoutUriH3, noTimeoutUriH3); + } + } + + } + + private void test(HttpClient hc, String propVal, Version version, URI timeoutUri, URI noTimeoutUri) { + if (propVal.equals("1")) { + testTimeout(hc, timeoutUri, version); + } else if (propVal.equals("20")) { + testNoTimeout(hc, noTimeoutUri, version); + } else if (propVal.equals("abc") || propVal.equals("-1")) { + testNoTimeout(hc, noTimeoutUri, version); + } else { + throw new RuntimeException("Unexpected timeout value"); + } + } + + private void testTimeout(HttpClient hc, URI uri, Version version) { + // Timeout should occur + var config = version == HTTP_3 ? HTTP_3_URI_ONLY : null; + HttpRequest hreq = HttpRequest.newBuilder(uri).version(version).GET() + .setOption(H3_DISCOVERY, config).build(); + HttpResponse hresp = runRequest(hc, hreq, 2750); + assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); + } + + private void testNoTimeout(HttpClient hc, URI uri, Version version) { + // Timeout should not occur + var config = version == HTTP_3 ? HTTP_3_URI_ONLY : null; + HttpRequest hreq = HttpRequest.newBuilder(uri).version(version).GET() + .setOption(H3_DISCOVERY, config).build(); + HttpResponse hresp = runRequest(hc, hreq, 0); + assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); + } + + private HttpResponse runRequest(HttpClient hc, HttpRequest req, int sleepTime) { + CompletableFuture> request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + HttpResponse hresp = request.join(); + assertEquals(hresp.statusCode(), 200); + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + return request.join(); + } + + static class ServerTimeoutHandlerH2 implements Http2Handler { + + volatile Object firstConnection = null; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (exchange instanceof TestExchange exch) { + if (firstConnection == null) { + firstConnection = exch.getServerConnection(); + exch.sendResponseHeaders(200, 0); + } else { + var secondConnection = exch.getServerConnection(); + + if (firstConnection != secondConnection) { + testLog.println("ServerTimeoutHandlerH2: New Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH2: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(400, 0); + } + } + } + } + } + + static class ServerNoTimeoutHandlerH2 implements Http2Handler { + + volatile Object firstConnection; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (exchange instanceof TestExchange exch) { + if (firstConnection == null) { + firstConnection = exch.getServerConnection(); + exch.sendResponseHeaders(200, 0); + } else { + var secondConnection = exch.getServerConnection(); + + if (firstConnection == secondConnection) { + testLog.println("ServerTimeoutHandlerH2: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH2: Different Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exch.sendResponseHeaders(400, 0); + } + } + } + } + } + + static class ServerTimeoutHandlerH3 implements Http2Handler { + + volatile Object firstConnection; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (firstConnection == null) { + firstConnection = latestServerConn; + exchange.sendResponseHeaders(200, 0); + } else { + var secondConnection = latestServerConn; + if (firstConnection != secondConnection) { + testLog.println("ServerTimeoutHandlerH3: New Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH3: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(400, 0); + } + } + exchange.close(); + } + } + + static class ServerNoTimeoutHandlerH3 implements Http2Handler { + + volatile Object firstConnection = null; + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + if (firstConnection == null) { + firstConnection = latestServerConn; + exchange.sendResponseHeaders(200, 0); + } else { + var secondConnection = latestServerConn; + + if (firstConnection == secondConnection) { + testLog.println("ServerTimeoutHandlerH3: Same Connection was used, idleConnectionTimeoutEvent did not fire." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(200, 0); + } else { + testLog.println("ServerTimeoutHandlerH3: New Connection was used, idleConnectionTimeoutEvent fired." + + " First Connection: " + firstConnection + ", Second Connection Hash: " + secondConnection); + exchange.sendResponseHeaders(400, 0); + } + } + exchange.close(); + } + } + + static class TestExchange extends Http2TestExchangeImpl { + + public TestExchange(int streamid, String method, + HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, + URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, + Http2TestServerConnection conn, boolean pushAllowed) { + super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, + conn, pushAllowed); + } + + public Http2TestServerConnection getServerConnection() { + return this.conn; + } + } +} diff --git a/test/jdk/java/net/httpclient/ImmutableFlowItems.java b/test/jdk/java/net/httpclient/ImmutableFlowItems.java index b440ad646e8..186d3bbd9f8 100644 --- a/test/jdk/java/net/httpclient/ImmutableFlowItems.java +++ b/test/jdk/java/net/httpclient/ImmutableFlowItems.java @@ -46,7 +46,6 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsServer; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; diff --git a/test/jdk/java/net/httpclient/ImmutableSSLSessionTest.java b/test/jdk/java/net/httpclient/ImmutableSSLSessionTest.java new file mode 100644 index 00000000000..179900479bc --- /dev/null +++ b/test/jdk/java/net/httpclient/ImmutableSSLSessionTest.java @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.ImmutableExtendedSSLSession; +import jdk.internal.net.http.common.ImmutableSSLSession; +import jdk.internal.net.http.common.ImmutableSSLSessionAccess; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * @test + * @summary Verify that the request/response headers of HTTP/2 and HTTP/3 + * are sent and received in lower case + * @library /test/lib /test/jdk/java/net/httpclient/lib /test/jdk/java/net/httpclient/access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * java.net.http/jdk.internal.net.http.common.ImmutableSSLSessionAccess + * @run junit/othervm -Djdk.httpclient.HttpClient.log=request,response,headers,errors + * ImmutableSSLSessionTest + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class ImmutableSSLSessionTest implements HttpServerAdapters { + + private HttpTestServer h1server; + private HttpTestServer h2server; + private HttpTestServer h3server; + private String h1ReqURIBase; + private String h2ReqURIBase; + private String h3ReqURIBase; + private static SSLContext sslContext; + private final AtomicInteger counter = new AtomicInteger(); + + @BeforeAll + public void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + h1server = HttpTestServer.create(HTTP_1_1, sslContext); + h1server.start(); + h1ReqURIBase = "https://" + h1server.serverAuthority() + "/h1ImmutableSSLSessionTest/"; + h1server.addHandler(new HttpHeadOrGetHandler(), "/h1ImmutableSSLSessionTest/"); + System.out.println("HTTP/1.1 server listening on " + h1server.getAddress()); + + h2server = HttpTestServer.create(HTTP_2, sslContext); + h2server.start(); + h2ReqURIBase = "https://" + h2server.serverAuthority() + "/h2ImmutableSSLSessionTest/"; + h2server.addHandler(new HttpHeadOrGetHandler(), "/h2ImmutableSSLSessionTest/"); + System.out.println("HTTP/2 server listening on " + h2server.getAddress()); + + + h3server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3server.start(); + h3ReqURIBase = "https://" + h3server.serverAuthority() + "/h3ImmutableSSLSessionTest/"; + h3server.addHandler(new HttpHeadOrGetHandler(), "/h3ImmutableSSLSessionTest/"); + System.out.println("HTTP/3 server listening on " + h3server.getAddress()); + + } + + @AfterAll + public void afterAll() throws Exception { + if (h2server != null) { + h2server.stop(); + } + if (h3server != null) { + h3server.stop(); + } + } + + private Stream params() throws Exception { + return Stream.of( + Arguments.of(HTTP_1_1, new URI(h1ReqURIBase)), + Arguments.of(HTTP_2, new URI(h2ReqURIBase)), + Arguments.of(HTTP_3, new URI(h3ReqURIBase))); + } + + private Stream sessions() throws Exception { + return Stream.of( + Arguments.of(ImmutableSSLSessionAccess.immutableSSLSession(new DummySession())), + Arguments.of(ImmutableSSLSessionAccess.immutableExtendedSSLSession(new DummySession()))); + } + + /** + * Issues an HTTPS request and verifies that the SSLSession + * is immutable. + */ + @ParameterizedTest + @MethodSource("params") + public void testImmutableSSLSession(final Version version, final URI requestURI) throws Exception { + Http3DiscoveryMode config = switch (version) { + case HTTP_3 -> HTTP_3_URI_ONLY; + default -> null; + }; + + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() + .setOption(H3_DISCOVERY, config) + .version(version); + final HttpClient.Builder clientBuilder = (version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder()) + .version(version) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY); + + try (HttpClient client = clientBuilder.build()) { + final URI uriSync = URI.create(requestURI.toString() + "?sync=true,req=" + counter.incrementAndGet()); + final HttpRequest reqSync = reqBuilder.uri(uriSync).build(); + System.out.println("Issuing " + version + " request to " + uriSync); + final HttpResponse resp = client.send(reqSync, BodyHandlers.discarding()); + final Optional syncSession = resp.sslSession(); + assertEquals(resp.version(), version, "Unexpected HTTP version in response"); + assertEquals(resp.statusCode(), 200, "Unexpected response code"); + checkImmutableSession(resp.sslSession()); + } + + // now try with async + try (HttpClient client = clientBuilder.build()) { + final URI uriAsync = URI.create(requestURI.toString() + "?sync=false,req=" + counter.incrementAndGet()); + final HttpRequest reqAsync = reqBuilder.copy().uri(uriAsync).build(); + System.out.println("Issuing (async) request to " + uriAsync); + final CompletableFuture> futureResp = client.sendAsync(reqAsync, + BodyHandlers.discarding()); + final HttpResponse asyncResp = futureResp.get(); + assertEquals(asyncResp.version(), version, "Unexpected HTTP version in response"); + assertEquals(asyncResp.statusCode(), 200, "Unexpected response code"); + checkImmutableSession(asyncResp.sslSession()); + } + } + + @ParameterizedTest + @MethodSource("sessions") + public void testImmutableSSLSessionClass(SSLSession session) throws Exception { + System.out.println("Checking session class: " + session.getClass()); + checkDummySession(session); + } + + + private void checkImmutableSession(Optional session) { + assertNotNull(session); + assertTrue(session.isPresent()); + SSLSession sess = session.get(); + assertNotNull(sess); + checkImmutableSession(sess); + } + + private void checkImmutableSession(SSLSession session) { + if (session instanceof ExtendedSSLSession) { + assertEquals(ImmutableExtendedSSLSession.class, session.getClass()); + } else { + assertEquals(ImmutableSSLSession.class, session.getClass()); + } + assertThrows(UnsupportedOperationException.class, session::invalidate); + assertThrows(UnsupportedOperationException.class, + () -> session.putValue("foo", "bar")); + for (String name : session.getValueNames()) { + assertThrows(UnsupportedOperationException.class, + () -> session.removeValue(name)); + } + } + + private void checkDummySession(SSLSession session) throws Exception { + checkImmutableSession(session); + assertEquals("abcd", new String(session.getId(), US_ASCII)); + assertEquals(sslContext.getClientSessionContext(), session.getSessionContext()); + assertEquals(42, session.getCreationTime()); + assertEquals(4242, session.getLastAccessedTime()); + assertFalse(session.isValid()); + assertEquals("bar", session.getValue("foo")); + assertEquals(List.of("foo"), Arrays.asList(session.getValueNames())); + assertEquals(0, session.getPeerCertificates().length); + assertEquals(0, session.getLocalCertificates().length); + assertNull(session.getPeerPrincipal()); + assertNull(session.getLocalPrincipal()); + assertEquals("MyCipherSuite", session.getCipherSuite()); + assertEquals("TLSv1.3", session.getProtocol()); + assertEquals("dummy", session.getPeerHost()); + assertEquals(42, session.getPeerPort()); + assertEquals(42, session.getPacketBufferSize()); + assertEquals(42, session.getApplicationBufferSize()); + if (session instanceof ExtendedSSLSession ext) { + assertEquals(List.of("bar", "foo"), + Arrays.asList(ext.getPeerSupportedSignatureAlgorithms())); + assertEquals(List.of("foo", "bar"), + Arrays.asList(ext.getLocalSupportedSignatureAlgorithms())); + assertEquals(List.of(new SNIHostName("localhost")), ((ExtendedSSLSession) session).getRequestedServerNames()); + List status = ext.getStatusResponses(); + assertEquals(1, status.size()); + assertEquals("42", new String(status.get(0), US_ASCII)); + assertThrows(UnsupportedOperationException.class, + () -> ext.exportKeyingMaterialData("foo", new byte[] {1,2,3,4}, 4)); + assertThrows(UnsupportedOperationException.class, + () -> ext.exportKeyingMaterialKey("foo", "foo", new byte[] {1,2,3,4}, 4)); + } + } + + static class DummySession extends ExtendedSSLSession { + + @Override + public byte[] getId() { + return new byte[] {'a', 'b', 'c', 'd'}; + } + + @Override + public SSLSessionContext getSessionContext() { + return sslContext.getClientSessionContext(); + } + + @Override + public long getCreationTime() { + return 42; + } + + @Override + public long getLastAccessedTime() { + return 4242; + } + + @Override + public void invalidate() {} + + @Override + public boolean isValid() { + return false; + } + + @Override + public void putValue(String name, Object value) { + + } + + @Override + public Object getValue(String name) { + if (name.equals("foo")) return "bar"; + return null; + } + + @Override + public void removeValue(String name) { + + } + + @Override + public String[] getValueNames() { + return new String[] {"foo"}; + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + return new Certificate[0]; + } + + @Override + public Certificate[] getLocalCertificates() { + return new Certificate[0]; + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + return null; + } + + @Override + public Principal getLocalPrincipal() { + return null; + } + + @Override + public String getCipherSuite() { + return "MyCipherSuite"; + } + + @Override + public String getProtocol() { + return "TLSv1.3"; + } + + @Override + public String getPeerHost() { + return "dummy"; + } + + @Override + public int getPeerPort() { + return 42; + } + + @Override + public int getPacketBufferSize() { + return 42; + } + + @Override + public int getApplicationBufferSize() { + return 42; + } + + @Override + public String[] getLocalSupportedSignatureAlgorithms() { + return new String[] {"foo", "bar"}; + } + + @Override + public String[] getPeerSupportedSignatureAlgorithms() { + return new String[] {"bar", "foo"}; + } + + @Override + public List getRequestedServerNames() { + return List.of(new SNIHostName("localhost")); + } + + @Override + public List getStatusResponses() { + return List.of(new byte[] {'4', '2'}); + } + } + +} diff --git a/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java b/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java index 2f4f96e8b9b..d1e3cc026d3 100644 --- a/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java +++ b/test/jdk/java/net/httpclient/InvalidInputStreamSubscriptionRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,19 +22,39 @@ */ /* - * @test + * @test id=http3 * @summary Tests an asynchronous BodySubscriber that completes * immediately with an InputStream which issues bad * requests * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext ReferenceTracker * jdk.httpclient.test.lib.common.HttpServerAdapters - * @run testng/othervm InvalidInputStreamSubscriptionRequest + * @run testng/othervm -Dtest.http.version=http3 + * -Djdk.internal.httpclient.debug=true + * InvalidInputStreamSubscriptionRequest + */ +/* + * @test id=http2 + * @summary Tests an asynchronous BodySubscriber that completes + * immediately with an InputStream which issues bad + * requests + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext ReferenceTracker + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm -Dtest.http.version=http2 InvalidInputStreamSubscriptionRequest + */ +/* + * @test id=http1 + * @summary Tests an asynchronous BodySubscriber that completes + * immediately with an InputStream which issues bad + * requests + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext ReferenceTracker + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm -Dtest.http.version=http1 InvalidInputStreamSubscriptionRequest */ import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterTest; @@ -47,7 +67,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -55,7 +74,6 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.HttpResponse.BodySubscribers; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -67,15 +85,16 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -86,6 +105,7 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -94,6 +114,8 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -180,46 +202,81 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters @DataProvider(name = "variants") public Object[][] variants() { - return new Object[][]{ - { httpURI_fixed, false, OF_INPUTSTREAM }, - { httpURI_chunk, false, OF_INPUTSTREAM }, - { httpsURI_fixed, false, OF_INPUTSTREAM }, - { httpsURI_chunk, false, OF_INPUTSTREAM }, + Object[][] http3 = new Object[][]{ + {http3URI_fixed, false, OF_INPUTSTREAM}, + {http3URI_chunk, false, OF_INPUTSTREAM}, + {http3URI_fixed, true, OF_INPUTSTREAM}, + {http3URI_chunk, true, OF_INPUTSTREAM}, + }; + Object[][] http1 = new Object[][] { + {httpURI_fixed, false, OF_INPUTSTREAM}, + {httpURI_chunk, false, OF_INPUTSTREAM}, + {httpsURI_fixed, false, OF_INPUTSTREAM}, + {httpsURI_chunk, false, OF_INPUTSTREAM}, + {httpURI_fixed, true, OF_INPUTSTREAM}, + {httpURI_chunk, true, OF_INPUTSTREAM}, + {httpsURI_fixed, true, OF_INPUTSTREAM}, + {httpsURI_chunk, true, OF_INPUTSTREAM}, + }; + Object[][] http2 = new Object[][] { { http2URI_fixed, false, OF_INPUTSTREAM }, { http2URI_chunk, false, OF_INPUTSTREAM }, { https2URI_fixed, false, OF_INPUTSTREAM }, { https2URI_chunk, false, OF_INPUTSTREAM }, - - { httpURI_fixed, true, OF_INPUTSTREAM }, - { httpURI_chunk, true, OF_INPUTSTREAM }, - { httpsURI_fixed, true, OF_INPUTSTREAM }, - { httpsURI_chunk, true, OF_INPUTSTREAM }, { http2URI_fixed, true, OF_INPUTSTREAM }, { http2URI_chunk, true, OF_INPUTSTREAM }, { https2URI_fixed, true, OF_INPUTSTREAM }, { https2URI_chunk, true, OF_INPUTSTREAM }, }; + String version = System.getProperty("test.http.version"); + if ("http3".equals(version)) { + return http3; + } + if ("http2".equals(version)) { + return http2; + } + if ("http1".equals(version)) { + return http1; + } + if (version == null) throw new AssertionError("test.http.version not set"); + throw new AssertionError("test.http.version should be set to http3|http2|http1. Found " + version); } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; - HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + HttpClient newHttpClient(String uri) { + HttpClient.Builder builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return TRACKER.track(builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) .build()); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void testNoBody(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { - if (!sameClient || client == null) - client = newHttpClient(); + if (!sameClient || client == null) { + client = newHttpClient(uri); + } - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -246,7 +303,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -256,11 +329,12 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -295,7 +369,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -305,11 +395,12 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -331,7 +422,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -341,11 +448,12 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> @@ -374,7 +482,23 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 1500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -476,20 +600,32 @@ public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_VariableLengthHandler(); + + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest public void teardown() throws Exception { - AssertionError fail = TRACKER.check(500); + AssertionError fail = TRACKER.check(1500); try { httpTestServer.stop(); httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java b/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java index 3cd7cb629ea..327dc57d6df 100644 --- a/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java +++ b/test/jdk/java/net/httpclient/InvalidSubscriptionRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,8 +34,6 @@ */ import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -47,7 +45,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -69,11 +66,13 @@ import java.util.concurrent.Flow.Publisher; import java.util.function.Supplier; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; @@ -84,6 +83,7 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -92,6 +92,8 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -127,6 +129,11 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][]{ + { http3URI_fixed, false, OF_PUBLISHER_API }, + { http3URI_chunk, false, OF_PUBLISHER_API }, + { http3URI_fixed, true, OF_PUBLISHER_API }, + { http3URI_chunk, true, OF_PUBLISHER_API }, + { httpURI_fixed, false, OF_PUBLISHER_API }, { httpURI_chunk, false, OF_PUBLISHER_API }, { httpsURI_fixed, false, OF_PUBLISHER_API }, @@ -148,22 +155,35 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; - HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + HttpClient newHttpClient(String uri) { + HttpClient.Builder builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return TRACKER.track(builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) .build()); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void testNoBody(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -190,7 +210,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -198,11 +234,12 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @Test(dataProvider = "variants") public void testNoBodyAsync(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -234,7 +271,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -242,11 +295,12 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @Test(dataProvider = "variants") public void testAsString(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -269,7 +323,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -277,11 +347,12 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { @Test(dataProvider = "variants") public void testAsStringAsync(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; + Throwable failed = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -307,7 +378,23 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); - } else throw x; + } else { + failed = x; + } + } finally { + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 500); + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + } else throw error; + } + } + } + if (failed != null) { + throw new AssertionError("Unexpected exception: " + failed, failed); } } } @@ -409,10 +496,21 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_VariableLengthHandler(); + + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -423,6 +521,7 @@ public class InvalidSubscriptionRequest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/LargeHandshakeTest.java b/test/jdk/java/net/httpclient/LargeHandshakeTest.java index 1f4c85b361e..ff3981e8b3d 100644 --- a/test/jdk/java/net/httpclient/LargeHandshakeTest.java +++ b/test/jdk/java/net/httpclient/LargeHandshakeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -62,6 +62,9 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.TestServerConfigurator; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -964,12 +967,14 @@ public class LargeHandshakeTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -988,8 +993,7 @@ public class LargeHandshakeTest implements HttpServerAdapters { } public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .proxy(ps); @@ -1028,6 +1032,12 @@ public class LargeHandshakeTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/LargeHandshakeTest/https2/"); + // HTTP/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestLargeHandler(), "/LargeHandshakeTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/LargeHandshakeTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -1073,9 +1083,11 @@ public class LargeHandshakeTest implements HttpServerAdapters { } public void run(String... args) throws Exception { - List serverURIs = List.of(http1URI, http2URI, https1URI, https2URI); + List serverURIs = List.of(http3URI, http1URI, http2URI, https1URI, https2URI); for (int i = 0; i < 5; i++) { for (URI base : serverURIs) { + // skip HTTP/3 if proxy + if (base.getRawPath().contains("/http3/")) continue; if (base.getScheme().equalsIgnoreCase("https")) { URI proxy = i % 1 == 0 ? base.resolve(URI.create("proxy/foo?n=" + requestCounter.incrementAndGet())) : base.resolve(URI.create("direct/foo?n=" + requestCounter.incrementAndGet())); @@ -1090,10 +1102,19 @@ public class LargeHandshakeTest implements HttpServerAdapters { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + public void test(URI uri) throws Exception { System.out.println("Testing with " + uri); pending.add(uri); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); CompletableFuture> resp = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .whenComplete((r, t) -> this.requestCompleted(request, r, t)); @@ -1111,11 +1132,13 @@ public class LargeHandshakeTest implements HttpServerAdapters { } public void tearDown() { + client.close(); proxy = stop(proxy, DigestEchoServer.TunnelingProxy::stop); http1Server = stop(http1Server, HttpTestServer::stop); https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/LargeResponseTest.java b/test/jdk/java/net/httpclient/LargeResponseTest.java index bfc7c9ca5db..02cf5ddcc8d 100644 --- a/test/jdk/java/net/httpclient/LargeResponseTest.java +++ b/test/jdk/java/net/httpclient/LargeResponseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -51,6 +51,9 @@ import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -65,6 +68,7 @@ import static java.net.http.HttpClient.Version.HTTP_2; * @run main/othervm -Dtest.requiresHost=true * -Djdk.httpclient.HttpClient.log=headers * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.quic.maxInitialTimeout=60 * LargeResponseTest * */ @@ -94,12 +98,14 @@ public class LargeResponseTest implements HttpServerAdapters { HttpTestServer http2Server; HttpTestServer https1Server; HttpTestServer https2Server; + HttpTestServer http3Server; DigestEchoServer.TunnelingProxy proxy; URI http1URI; URI https1URI; URI http2URI; URI https2URI; + URI http3URI; InetSocketAddress proxyAddress; ProxySelector proxySelector; HttpClient client; @@ -112,8 +118,7 @@ public class LargeResponseTest implements HttpServerAdapters { TimeUnit.SECONDS, new LinkedBlockingQueue<>()); public HttpClient newHttpClient(ProxySelector ps) { - HttpClient.Builder builder = HttpClient - .newBuilder() + HttpClient.Builder builder = newClientBuilderForH3() .sslContext(context) .executor(clientexec) .proxy(ps); @@ -152,6 +157,12 @@ public class LargeResponseTest implements HttpServerAdapters { https2Server.start(); https2URI = new URI("https://" + https2Server.serverAuthority() + "/LargeResponseTest/https2/"); + // HTTP/3 + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + http3Server.addHandler(new HttpTestLargeHandler(), "/LargeResponseTest/http3/"); + http3Server.start(); + http3URI = new URI("https://" + http3Server.serverAuthority() + "/LargeResponseTest/http3/"); + proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); proxyAddress = proxy.getProxyAddress(); @@ -182,9 +193,11 @@ public class LargeResponseTest implements HttpServerAdapters { } public void run(String... args) throws Exception { - List serverURIs = List.of(http1URI, http2URI, https1URI, https2URI); + List serverURIs = List.of(http3URI, http1URI, http2URI, https1URI, https2URI); for (int i=0; i<5; i++) { for (URI base : serverURIs) { + // no proxy with HTTP/3 + if (base.getRawPath().contains("/http3/")) continue; if (base.getScheme().equalsIgnoreCase("https")) { URI proxy = i % 1 == 0 ? base.resolve(URI.create("proxy/foo?n="+requestCounter.incrementAndGet())) : base.resolve(URI.create("direct/foo?n="+requestCounter.incrementAndGet())); @@ -199,10 +212,19 @@ public class LargeResponseTest implements HttpServerAdapters { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + public void test(URI uri) throws Exception { System.out.println("Testing with " + uri); pending.add(uri); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); CompletableFuture> resp = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .whenComplete((r, t) -> this.requestCompleted(request, r, t)); @@ -220,11 +242,13 @@ public class LargeResponseTest implements HttpServerAdapters { } public void tearDown() { + client.close(); proxy = stop(proxy, DigestEchoServer.TunnelingProxy::stop); http1Server = stop(http1Server, HttpTestServer::stop); https1Server = stop(https1Server, HttpTestServer::stop); http2Server = stop(http2Server, HttpTestServer::stop); https2Server = stop(https2Server, HttpTestServer::stop); + http3Server = stop(http3Server, HttpTestServer::stop); client = null; try { executor.awaitTermination(2000, TimeUnit.MILLISECONDS); diff --git a/test/jdk/java/net/httpclient/LineBodyHandlerTest.java b/test/jdk/java/net/httpclient/LineBodyHandlerTest.java index 3271f21a130..62afcab9ee2 100644 --- a/test/jdk/java/net/httpclient/LineBodyHandlerTest.java +++ b/test/jdk/java/net/httpclient/LineBodyHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,8 +28,6 @@ import java.io.PrintStream; import java.io.StringReader; import java.io.UncheckedIOException; import java.math.BigInteger; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Builder; @@ -55,10 +53,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -67,6 +61,9 @@ import org.testng.annotations.Test; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_16; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpRequest.BodyPublishers.ofString; @@ -90,14 +87,16 @@ import static org.testng.Assert.assertTrue; public class LineBodyHandlerTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; final AtomicInteger clientCount = new AtomicInteger(); @@ -106,6 +105,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { @DataProvider(name = "uris") public Object[][] variants() { return new Object[][]{ + { http3URI }, { httpURI }, { httpsURI }, { http2URI }, @@ -195,17 +195,26 @@ public class LineBodyHandlerTest implements HttpServerAdapters { return sharedClient; } clientCount.incrementAndGet(); - return sharedClient = TRACKER.track(HttpClient.newBuilder() + return sharedClient = TRACKER.track(newClientBuilderForH3() .sslContext(sslContext) .proxy(Builder.NO_PROXY) .build()); } + HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "uris") void testStringWithFinisher(String url) { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -226,7 +235,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStream(String url) { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -249,7 +258,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { String body = "May the luck\r\n\r\n of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -270,7 +279,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithCRLF(String url) { String body = "May the luck\r\n\r\n of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -294,7 +303,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testStringWithFinisherBlocking(String url) throws Exception { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)).build(); StringSubscriber subscriber = new StringSubscriber(); @@ -311,7 +320,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testStringWithoutFinisherBlocking(String url) throws Exception { String body = "May the luck of the Irish be with you!"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)).build(); StringSubscriber subscriber = new StringSubscriber(); @@ -330,7 +339,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithMixedCRLF(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -357,7 +366,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithMixedCRLF_UTF8(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .header("Content-type", "text/text; charset=UTF-8") .POST(BodyPublishers.ofString(body, UTF_8)).build(); @@ -383,7 +392,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testAsStreamWithMixedCRLF_UTF16(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .header("Content-type", "text/text; charset=UTF-16") .POST(BodyPublishers.ofString(body, UTF_16)).build(); @@ -410,7 +419,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithFinisher(String url) { String body = "May\r\n the wind\r\n always be\rat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -435,7 +444,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithFinisher_UTF16(String url) { String body = "May\r\n the wind\r\n always be\rat your back.\r\r"; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .header("Content-type", "text/text; charset=UTF-16") .POST(BodyPublishers.ofString(body, UTF_16)).build(); ObjectSubscriber subscriber = new ObjectSubscriber(); @@ -461,7 +470,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithoutFinisher(String url) { String body = "May\r\n the wind\r\n always be\rat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -487,7 +496,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithFinisherBlocking(String url) throws Exception { String body = "May\r\n the wind\r\n always be\nat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -511,7 +520,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testObjectWithoutFinisherBlocking(String url) throws Exception { String body = "May\r\n the wind\r\n always be\nat your back."; HttpClient client = newClient(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(body)) .build(); @@ -546,7 +555,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testBigTextFromLineSubscriber(String url) { HttpClient client = newClient(); String bigtext = bigtext(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(bigtext)) .build(); @@ -567,7 +576,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { void testBigTextAsStream(String url) { HttpClient client = newClient(); String bigtext = bigtext(); - HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + HttpRequest request = newRequestBuilder(URI.create(url)) .POST(BodyPublishers.ofString(bigtext)) .build(); @@ -690,10 +699,15 @@ public class LineBodyHandlerTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpTestEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpTestEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -712,6 +726,7 @@ public class LineBodyHandlerTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/ManyRequests.java b/test/jdk/java/net/httpclient/ManyRequests.java index c196ef76466..190205a9ef5 100644 --- a/test/jdk/java/net/httpclient/ManyRequests.java +++ b/test/jdk/java/net/httpclient/ManyRequests.java @@ -25,11 +25,11 @@ * @test * @bug 8087112 8180044 8256459 * @key intermittent - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib - * @build jdk.test.lib.net.SimpleSSLContext + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/EchoHandler.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java @@ -77,7 +77,9 @@ import java.util.logging.Level; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.Platform; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; @@ -107,7 +109,7 @@ public class ManyRequests { InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); HttpsServer server = HttpsServer.create(addr, 0); ExecutorService executor = executorFor("HTTPS/1.1 Server Thread"); - server.setHttpsConfigurator(new Configurator(ctx)); + server.setHttpsConfigurator(new Configurator(addr.getAddress(), ctx)); server.setExecutor(executor); ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() .name("HttpClient-Worker", 0).factory()); @@ -366,12 +368,17 @@ public class ManyRequests { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } + @Override public void configure(HttpsParameters params) { - params.setSSLParameters(getSSLContext().getSupportedSSLParameters()); + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } diff --git a/test/jdk/java/net/httpclient/ManyRequests2.java b/test/jdk/java/net/httpclient/ManyRequests2.java index e6aa5ffa83f..a1e2307b820 100644 --- a/test/jdk/java/net/httpclient/ManyRequests2.java +++ b/test/jdk/java/net/httpclient/ManyRequests2.java @@ -24,11 +24,11 @@ /* * @test * @bug 8087112 8180044 8256459 - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib - * @build jdk.test.lib.net.SimpleSSLContext + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/EchoHandler.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java diff --git a/test/jdk/java/net/httpclient/ManyRequestsLegacy.java b/test/jdk/java/net/httpclient/ManyRequestsLegacy.java index 010c04cc51d..1d8091d3085 100644 --- a/test/jdk/java/net/httpclient/ManyRequestsLegacy.java +++ b/test/jdk/java/net/httpclient/ManyRequestsLegacy.java @@ -23,11 +23,11 @@ /* * @test - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib - * @build jdk.test.lib.net.SimpleSSLContext + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/EchoHandler.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java @@ -65,6 +65,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; @@ -81,6 +82,7 @@ import java.util.Random; import java.util.logging.Logger; import java.util.logging.Level; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.Platform; import jdk.test.lib.RandomFactory; import jdk.test.lib.net.SimpleSSLContext; @@ -112,7 +114,7 @@ public class ManyRequestsLegacy { }); InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); HttpsServer server = HttpsServer.create(addr, 0); - server.setHttpsConfigurator(new Configurator(ctx)); + server.setHttpsConfigurator(new Configurator(addr.getAddress(), ctx)); LegacyHttpClient client = new LegacyHttpClient(); @@ -431,12 +433,18 @@ public class ManyRequestsLegacy { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } + @Override public void configure(HttpsParameters params) { - params.setSSLParameters(getSSLContext().getSupportedSSLParameters()); + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } } diff --git a/test/jdk/java/net/httpclient/MappingResponseSubscriber.java b/test/jdk/java/net/httpclient/MappingResponseSubscriber.java index 6f5964970cc..5e5497cf0e5 100644 --- a/test/jdk/java/net/httpclient/MappingResponseSubscriber.java +++ b/test/jdk/java/net/httpclient/MappingResponseSubscriber.java @@ -49,11 +49,9 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsServer; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscribers; import java.net.http.HttpResponse.BodySubscriber; import java.util.function.Function; @@ -64,6 +62,7 @@ import jdk.internal.net.http.common.OperationTrackers.Tracker; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.test.lib.Utils; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -149,14 +148,14 @@ public class MappingResponseSubscriber { Tracker tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker, 1500); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1500)); if (error != null) throw error; // the client didn't shut down properly } if (sameClient) { Tracker tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker,1500); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1500)); if (error != null) throw error; // the client didn't shut down properly } } diff --git a/test/jdk/java/net/httpclient/NoBodyPartOne.java b/test/jdk/java/net/httpclient/NoBodyPartOne.java index d05d3e9c5e1..7c7a51c92e7 100644 --- a/test/jdk/java/net/httpclient/NoBodyPartOne.java +++ b/test/jdk/java/net/httpclient/NoBodyPartOne.java @@ -44,6 +44,7 @@ import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -57,6 +58,9 @@ public class NoBodyPartOne extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -78,6 +82,9 @@ public class NoBodyPartOne extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { @@ -101,6 +108,9 @@ public class NoBodyPartOne extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { diff --git a/test/jdk/java/net/httpclient/NoBodyPartThree.java b/test/jdk/java/net/httpclient/NoBodyPartThree.java index d0f71130421..d5e310d1914 100644 --- a/test/jdk/java/net/httpclient/NoBodyPartThree.java +++ b/test/jdk/java/net/httpclient/NoBodyPartThree.java @@ -28,6 +28,7 @@ * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer * @run testng/othervm + * -Djdk.httpclient.HttpClient.log=quic,errors * -Djdk.httpclient.HttpClient.log=all * NoBodyPartThree */ @@ -44,8 +45,10 @@ import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import jdk.internal.net.http.common.Utils; import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -62,6 +65,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testAsByteArrayPublisher/first/" + REQID.getAndIncrement(); @@ -72,7 +78,7 @@ public class NoBodyPartThree extends AbstractNoBody { Consumer> consumer = oba -> { consumerHasBeenCalled = true; oba.ifPresent(ba -> fail("Unexpected non-empty optional:" - + asString(ByteBuffer.wrap(ba)))); + + Utils.asString(ByteBuffer.wrap(ba)))); }; consumerHasBeenCalled = false; var response = client.send(req, BodyHandlers.ofByteArrayConsumer(consumer)); @@ -99,6 +105,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testStringPublisher/" + REQID.getAndIncrement(); @@ -121,6 +130,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testInputStreamPublisherBuffering/" + REQID.getAndIncrement(); @@ -144,6 +156,9 @@ public class NoBodyPartThree extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { var u = uri + "/testEmptyArrayPublisher/" + REQID.getAndIncrement(); diff --git a/test/jdk/java/net/httpclient/NoBodyPartTwo.java b/test/jdk/java/net/httpclient/NoBodyPartTwo.java index 563516b11bf..f7d331cb526 100644 --- a/test/jdk/java/net/httpclient/NoBodyPartTwo.java +++ b/test/jdk/java/net/httpclient/NoBodyPartTwo.java @@ -43,8 +43,10 @@ import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; +import jdk.internal.net.http.common.Utils; import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -59,6 +61,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -67,7 +72,7 @@ public class NoBodyPartTwo extends AbstractNoBody { Consumer> consumer = oba -> { consumerHasBeenCalled = true; oba.ifPresent(ba -> fail("Unexpected non-empty optional: " - + asString(ByteBuffer.wrap(ba)))); + + Utils.asString(ByteBuffer.wrap(ba)))); }; consumerHasBeenCalled = false; client.send(req, BodyHandlers.ofByteArrayConsumer(consumer)); @@ -83,6 +88,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -102,6 +110,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) @@ -122,6 +133,9 @@ public class NoBodyPartTwo extends AbstractNoBody { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uri) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { HttpRequest req = newRequestBuilder(uri) diff --git a/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java b/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java index 56a35119ae2..ba93645aef4 100644 --- a/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java +++ b/test/jdk/java/net/httpclient/NonAsciiCharsInURI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,7 +30,7 @@ * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * @compile -encoding utf-8 NonAsciiCharsInURI.java * @run testng/othervm - * -Djdk.httpclient.HttpClient.log=reqeusts,headers + * -Djdk.httpclient.HttpClient.log=requests,headers,errors,quic * NonAsciiCharsInURI */ @@ -49,6 +49,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -58,6 +59,8 @@ import static java.lang.System.err; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -65,14 +68,17 @@ import static org.testng.Assert.assertEquals; public class NonAsciiCharsInURI implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; + String http3URI_head; private volatile HttpClient sharedClient; @@ -104,6 +110,9 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { Arrays.asList(pathsAndQueryStrings).stream() .map(e -> new Object[] {https2URI + e[0], sameClient}) .forEach(list::add); + Arrays.asList(pathsAndQueryStrings).stream() + .map(e -> new Object[] {http3URI + e[0], sameClient}) + .forEach(list::add); } return list.stream().toArray(Object[][]::new); } @@ -122,11 +131,38 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { return HTTP_1_1; if (uri.contains("/http2/") || uri.contains("/https2/")) return HTTP_2; + if (uri.contains("/http3/")) + return HTTP_3; return null; } + HttpRequest.Builder newRequestBuilder(String uri) { + var builder = HttpRequest.newBuilder(URI.create(uri)); + if (version(uri) == HTTP_3) { + builder.version(HTTP_3); + builder.setOption(H3_DISCOVERY, http3TestServer.h3DiscoveryConfig()); + } + return builder; + } + + HttpResponse headRequest(HttpClient client) + throws IOException, InterruptedException + { + out.println("\n" + now() + "--- Sending HEAD request ----\n"); + err.println("\n" + now() + "--- Sending HEAD request ----\n"); + + var request = newRequestBuilder(http3URI_head) + .HEAD().version(HTTP_2).build(); + var response = client.send(request, BodyHandlers.ofString()); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_2); + out.println("\n" + now() + "--- HEAD request succeeded ----\n"); + err.println("\n" + now() + "--- HEAD request succeeded ----\n"); + return response; + } + private HttpClient makeNewClient() { - return HttpClient.newBuilder() + return newClientBuilderForH3() .proxy(NO_PROXY) .sslContext(sslContext) .build(); @@ -167,11 +203,14 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); HttpResponse resp = client.send(request, BodyHandlers.ofString()); out.println("Got response: " + resp); @@ -203,10 +242,13 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = newHttpClient(sameClient); + if (!sameClient && version(uriString) == HTTP_3) { + headRequest(client); + } } try (var cl = new CloseableClient(client, sameClient)) { - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uriString).build(); client.sendAsync(request, BodyHandlers.ofString()) .thenApply(response -> { @@ -259,16 +301,28 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { https2TestServer.addHandler(handler, "/https2/get"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/get"; + http3TestServer = HttpTestServer.create(HTTP_3, sslContext); + http3TestServer.addHandler(new HttpUriStringHandler(), "/http3/get"); + http3TestServer.addHandler(new HttpHeadOrGetHandler(), "/http3/head"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/get"; + http3URI_head = "https://" + http3TestServer.serverAuthority() + "/http3/head/x"; + err.println(now() + "Starting servers"); httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); out.println("HTTP/1.1 server (http) listening at: " + httpTestServer.serverAuthority()); out.println("HTTP/1.1 server (TLS) listening at: " + httpsTestServer.serverAuthority()); out.println("HTTP/2 server (h2c) listening at: " + http2TestServer.serverAuthority()); out.println("HTTP/2 server (h2) listening at: " + https2TestServer.serverAuthority()); + out.println("HTTP/3 server (h2) listening at: " + http3TestServer.serverAuthority()); + out.println(" + alt endpoint (h3) listening at: " + http3TestServer.getH3AltService() + .map(Http3TestServer::getAddress)); + + headRequest(newHttpClient(true)); out.println(now() + "setup done"); err.println(now() + "setup done"); @@ -281,6 +335,7 @@ public class NonAsciiCharsInURI implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } /** A handler that returns, as its body, the exact received request URI. */ diff --git a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.java b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.java index f3bf5521f4b..b5ea5da13e4 100644 --- a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.java +++ b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileDownloadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -40,9 +40,6 @@ * @run testng/othervm BodyHandlerOfFileDownloadTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import org.testng.annotations.AfterTest; @@ -54,8 +51,6 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -68,16 +63,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; -import jdk.httpclient.test.lib.http2.OutgoingPushPromise; -import jdk.httpclient.test.lib.http2.Queue; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; @@ -89,14 +81,16 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { static final String contentDispositionValue = "attachment; filename=example.html"; SSLContext sslContext; - HttpServerAdapters.HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] - HttpServerAdapters.HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] + HttpTestServer httpsTestServer; // HTTPS/1.1 + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; FileSystem zipFs; Path defaultFsPath; @@ -115,6 +109,9 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { @DataProvider(name = "defaultFsData") public Object[][] defaultFsData() { return new Object[][]{ + { http3URI, defaultFsPath, MSG, true }, + { http3URI, defaultFsPath, MSG, false }, + { httpURI, defaultFsPath, MSG, true }, { httpsURI, defaultFsPath, MSG, true }, { http2URI, defaultFsPath, MSG, true }, @@ -138,6 +135,24 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { private static final int ITERATION_COUNT = 3; + private HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return builder.proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + } + + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + private void receive(String uriString, Path path, String expectedMsg, @@ -146,12 +161,9 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { for (int i = 0; i < ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(NO_PROXY) - .sslContext(sslContext) - .build(); + client = newHttpClient(uriString); } - var req = HttpRequest.newBuilder(URI.create(uriString)) + var req = newRequestBuilder(URI.create(uriString)) .POST(BodyPublishers.noBody()) .build(); var resp = client.send(req, BodyHandlers.ofFileDownload(path, CREATE, TRUNCATE_EXISTING, WRITE)); @@ -163,6 +175,12 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { assertEquals(msg, expectedMsg); assertTrue(resp.headers().firstValue("Content-Disposition").isPresent()); assertEquals(resp.headers().firstValue("Content-Disposition").get(), contentDispositionValue); + if (!sameClient) { + client.close(); + } + } + if (sameClient && client != null) { + client.close(); } } @@ -198,26 +216,31 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { zipFs = newZipFs(); zipFsPath = zipFsDir(zipFs); - httpTestServer = HttpServerAdapters.HttpTestServer.create(HTTP_1_1); + httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler(new HttpEchoHandler(), "/http1/echo"); httpURI = "http://" + httpTestServer.serverAuthority() + "/http1/echo"; - httpsTestServer = HttpServerAdapters.HttpTestServer.create(HTTP_1_1, sslContext); + httpsTestServer = HttpTestServer.create(HTTP_1_1, sslContext); httpsTestServer.addHandler(new HttpEchoHandler(), "/https1/echo"); httpsURI = "https://" + httpsTestServer.serverAuthority() + "/https1/echo"; - http2TestServer = HttpServerAdapters.HttpTestServer.create(HTTP_2); + http2TestServer = HttpTestServer.create(HTTP_2); http2TestServer.addHandler(new HttpEchoHandler(), "/http2/echo"); http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo"; - https2TestServer = HttpServerAdapters.HttpTestServer.create(HTTP_2, sslContext); + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -231,6 +254,7 @@ public class BodyHandlerOfFileDownloadTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); zipFs.close(); } diff --git a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java index ee8a9c7dcbb..42d5c2596c7 100644 --- a/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java +++ b/test/jdk/java/net/httpclient/PathSubscriber/BodyHandlerOfFileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -39,9 +39,6 @@ * @run testng/othervm BodyHandlerOfFileTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import org.testng.annotations.AfterTest; @@ -53,8 +50,6 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -64,30 +59,29 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.Map; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; -import jdk.httpclient.test.lib.http2.OutgoingPushPromise; -import jdk.httpclient.test.lib.http2.Queue; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; public class BodyHandlerOfFileTest implements HttpServerAdapters { static final String MSG = "msg"; SSLContext sslContext; - HttpServerAdapters.HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] - HttpServerAdapters.HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] + HttpTestServer httpsTestServer; // HTTPS/1.1 + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; FileSystem zipFs; Path defaultFsPath; @@ -106,6 +100,9 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { @DataProvider(name = "defaultFsData") public Object[][] defaultFsData() { return new Object[][]{ + { http3URI, defaultFsPath, MSG, true }, + { http3URI, defaultFsPath, MSG, false }, + { httpURI, defaultFsPath, MSG, true }, { httpsURI, defaultFsPath, MSG, true }, { http2URI, defaultFsPath, MSG, true }, @@ -145,6 +142,9 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { @DataProvider(name = "zipFsData") public Object[][] zipFsData() { return new Object[][]{ + { http3URI, zipFsPath, MSG, true }, + { http3URI, zipFsPath, MSG, false }, + { httpURI, zipFsPath, MSG, true }, { httpsURI, zipFsPath, MSG, true }, { http2URI, zipFsPath, MSG, true }, @@ -168,6 +168,24 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { private static final int ITERATION_COUNT = 3; + private HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return builder.proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + } + + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + private void receive(String uriString, Path path, String expectedMsg, @@ -176,12 +194,9 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { for (int i = 0; i < ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(NO_PROXY) - .sslContext(sslContext) - .build(); + client = newHttpClient(uriString); } - var req = HttpRequest.newBuilder(URI.create(uriString)) + var req = newRequestBuilder(URI.create(uriString)) .POST(BodyPublishers.noBody()) .build(); var resp = client.send(req, HttpResponse.BodyHandlers.ofFile(path)); @@ -190,6 +205,12 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { out.printf("Msg written to %s: %s\n", resp.body(), msg); assertEquals(resp.statusCode(), 200); assertEquals(msg, expectedMsg); + if (!sameClient) { + client.close(); + } + } + if (sameClient && client != null) { + client.close(); } } @@ -219,10 +240,15 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -236,6 +262,7 @@ public class BodyHandlerOfFileTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); zipFs.close(); } diff --git a/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.java b/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.java index f2adc89ec17..2a7ba90c7b2 100644 --- a/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.java +++ b/test/jdk/java/net/httpclient/PathSubscriber/BodySubscriberOfFileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,9 +38,6 @@ * @run testng/othervm BodySubscriberOfFileTest */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.util.FileUtils; import org.testng.annotations.AfterTest; @@ -52,14 +49,11 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandler; -import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; import java.nio.Buffer; import java.nio.ByteBuffer; @@ -69,30 +63,29 @@ import java.util.Map; import java.util.concurrent.Flow; import java.util.stream.IntStream; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; -import jdk.httpclient.test.lib.http2.OutgoingPushPromise; -import jdk.httpclient.test.lib.http2.Queue; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static org.testng.Assert.assertEquals; public class BodySubscriberOfFileTest implements HttpServerAdapters { static final String MSG = "msg"; SSLContext sslContext; - HttpServerAdapters.HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] - HttpServerAdapters.HttpTestServer httpsTestServer; // HTTPS/1.1 - HttpServerAdapters.HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpServerAdapters.HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] + HttpTestServer httpsTestServer; // HTTPS/1.1 + HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; FileSystem zipFs; Path defaultFsPath; @@ -111,6 +104,9 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { @DataProvider(name = "defaultFsData") public Object[][] defaultFsData() { return new Object[][]{ + { http3URI, defaultFsPath, MSG, true }, + { http3URI, defaultFsPath, MSG, false }, + { httpURI, defaultFsPath, MSG, true }, { httpsURI, defaultFsPath, MSG, true }, { http2URI, defaultFsPath, MSG, true }, @@ -150,6 +146,9 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { @DataProvider(name = "zipFsData") public Object[][] zipFsData() { return new Object[][]{ + { http3URI, zipFsPath, MSG, true }, + { http3URI, zipFsPath, MSG, false }, + { httpURI, zipFsPath, MSG, true }, { httpsURI, zipFsPath, MSG, true }, { http2URI, zipFsPath, MSG, true }, @@ -173,6 +172,24 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { private static final int ITERATION_COUNT = 3; + private HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return builder.proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + } + + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + private void receive(String uriString, Path path, String expectedMsg, @@ -181,12 +198,9 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { for (int i = 0; i < ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(NO_PROXY) - .sslContext(sslContext) - .build(); + client = newHttpClient(uriString); } - var req = HttpRequest.newBuilder(URI.create(uriString)) + var req = newRequestBuilder(URI.create(uriString)) .POST(BodyPublishers.noBody()) .build(); @@ -197,6 +211,12 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { out.printf("Msg written to %s: %s\n", resp.body(), msg); assertEquals(resp.statusCode(), 200); assertEquals(msg, expectedMsg); + if (!sameClient) { + client.close(); + } + } + if (sameClient && client != null) { + client.close(); } } @@ -250,10 +270,15 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { https2TestServer.addHandler(new HttpEchoHandler(), "/https2/echo"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new HttpEchoHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -267,6 +292,7 @@ public class BodySubscriberOfFileTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); zipFs.close(); } diff --git a/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java b/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java index f0eb63f2a40..6d9d7e4f11b 100644 --- a/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java +++ b/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java @@ -35,16 +35,29 @@ * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes=Basic,Digest * -Djdk.http.auth.tunneling.disabledSchemes=Digest,Basic - * ProxyAuthDisabledSchemesSSL SSL + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.internal.httpclient.debug=err + * -Djdk.httpclient.HttpClient.log=headers + * ProxyAuthDisabledSchemesSSL SSL SERVER307 + * @run main/othervm/timeout=300 + * -Djdk.http.auth.proxying.disabledSchemes=Basic,Digest + * -Djdk.http.auth.tunneling.disabledSchemes=Digest,Basic + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers + * ProxyAuthDisabledSchemesSSL SSL SERVER PROXY * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes=Basic * -Djdk.http.auth.tunneling.disabledSchemes=Basic * -Dtest.requiresHost=true + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers * ProxyAuthDisabledSchemesSSL SSL PROXY * @run main/othervm/timeout=300 * -Djdk.http.auth.proxying.disabledSchemes=Digest * -Djdk.http.auth.tunneling.disabledSchemes=Digest * -Dtest.requiresHost=true + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=100 + * -Djdk.httpclient.HttpClient.log=headers * ProxyAuthDisabledSchemesSSL SSL PROXY */ diff --git a/test/jdk/java/net/httpclient/ProxyTest.java b/test/jdk/java/net/httpclient/ProxyTest.java index 8763e168a06..45c21e7ef4a 100644 --- a/test/jdk/java/net/httpclient/ProxyTest.java +++ b/test/jdk/java/net/httpclient/ProxyTest.java @@ -52,10 +52,13 @@ import java.util.concurrent.CopyOnWriteArrayList; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; + +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.net.SimpleSSLContext; import static java.net.Proxy.NO_PROXY; @@ -67,9 +70,10 @@ import static java.net.Proxy.NO_PROXY; * Verifies that downgrading from HTTP/2 to HTTP/1.1 works through * an SSL Tunnel connection when the client is HTTP/2 and the server * and proxy are HTTP/1.1 - * @modules java.net.http - * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.common + * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext ProxyTest + * jdk.httpclient.test.lib.common.TestServerConfigurator * @run main/othervm ProxyTest * @author danielfuchs */ @@ -103,9 +107,8 @@ public class ProxyTest { he.close(); } }); - - server.setHttpsConfigurator(new Configurator(SSLContext.getDefault())); InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + server.setHttpsConfigurator(new Configurator(addr.getAddress(), SSLContext.getDefault())); server.bind(addr, 0); return server; } @@ -407,13 +410,18 @@ public class ProxyTest { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } @Override - public void configure (HttpsParameters params) { - params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); + public void configure (final HttpsParameters params) { + final SSLParameters parameters = getSSLContext().getSupportedSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, parameters); + params.setSSLParameters(parameters); } } diff --git a/test/jdk/java/net/httpclient/RedirectMethodChange.java b/test/jdk/java/net/httpclient/RedirectMethodChange.java index ed1afb384e1..f713c0dc5d1 100644 --- a/test/jdk/java/net/httpclient/RedirectMethodChange.java +++ b/test/jdk/java/net/httpclient/RedirectMethodChange.java @@ -33,19 +33,13 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -53,6 +47,9 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.US_ASCII; import static org.testng.Assert.assertEquals; @@ -61,14 +58,16 @@ public class RedirectMethodChange implements HttpServerAdapters { SSLContext sslContext; HttpClient client; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String RESPONSE = "Hello world"; static final String POST_BODY = "This is the POST body 123909090909090"; @@ -90,6 +89,22 @@ public class RedirectMethodChange implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][] { + { http3URI, "GET", 301, "GET" }, + { http3URI, "GET", 302, "GET" }, + { http3URI, "GET", 303, "GET" }, + { http3URI, "GET", 307, "GET" }, + { http3URI, "GET", 308, "GET" }, + { http3URI, "POST", 301, "GET" }, + { http3URI, "POST", 302, "GET" }, + { http3URI, "POST", 303, "GET" }, + { http3URI, "POST", 307, "POST" }, + { http3URI, "POST", 308, "POST" }, + { http3URI, "PUT", 301, "PUT" }, + { http3URI, "PUT", 302, "PUT" }, + { http3URI, "PUT", 303, "GET" }, + { http3URI, "PUT", 307, "PUT" }, + { http3URI, "PUT", 308, "PUT" }, + { httpURI, "GET", 301, "GET" }, { httpURI, "GET", 302, "GET" }, { httpURI, "GET", 303, "GET" }, @@ -156,6 +171,15 @@ public class RedirectMethodChange implements HttpServerAdapters { }; } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void test(String uriString, String method, @@ -163,7 +187,7 @@ public class RedirectMethodChange implements HttpServerAdapters { String expectedMethod) throws Exception { - HttpRequest req = HttpRequest.newBuilder(URI.create(uriString)) + HttpRequest req = newRequestBuilder(URI.create(uriString)) .method(method, getRequestBodyFor(method)) .header("X-Redirect-Code", Integer.toString(redirectCode)) .header("X-Expect-Method", expectedMethod) @@ -183,7 +207,7 @@ public class RedirectMethodChange implements HttpServerAdapters { if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); - client = HttpClient.newBuilder() + client = newClientBuilderForH3() .followRedirects(HttpClient.Redirect.NORMAL) .sslContext(sslContext) .build(); @@ -212,10 +236,17 @@ public class RedirectMethodChange implements HttpServerAdapters { https2TestServer.addHandler(handler, "/https2/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test/rmt"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + targetURI = "https://" + http3TestServer.serverAuthority() + "/http3/redirect/rmt"; + handler = new RedirMethodChgeHandler(targetURI); + http3TestServer.addHandler(handler, "/http3/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/test/rmt"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -225,6 +256,7 @@ public class RedirectMethodChange implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } /** diff --git a/test/jdk/java/net/httpclient/RedirectTimeoutTest.java b/test/jdk/java/net/httpclient/RedirectTimeoutTest.java index 88b8fd964b0..be634398ba2 100644 --- a/test/jdk/java/net/httpclient/RedirectTimeoutTest.java +++ b/test/jdk/java/net/httpclient/RedirectTimeoutTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,10 +32,8 @@ * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,trace -Djdk.internal.httpclient.debug=false RedirectTimeoutTest */ -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestResponseHeaders; -import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; import org.testng.TestException; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -54,16 +52,24 @@ import java.net.http.HttpTimeoutException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.util.Optional; + +import javax.net.ssl.SSLContext; import static java.net.http.HttpClient.Redirect.ALWAYS; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static jdk.test.lib.Utils.adjustTimeout; -public class RedirectTimeoutTest { +public class RedirectTimeoutTest implements HttpServerAdapters { - static HttpTestServer h1TestServer, h2TestServer; - static URI h1Uri, h1RedirectUri, h2Uri, h2RedirectUri, h2WarmupUri, testRedirectURI; + static SSLContext sslContext; + static HttpTestServer h1TestServer, h2TestServer, h3TestServer; + static URI h1Uri, h1RedirectUri, h2Uri, h2RedirectUri, + h3Uri, h3RedirectUri, h2WarmupUri, h3WarmupUri, testRedirectURI; private static final long TIMEOUT_MILLIS = 3000L; // 3s private static final long SLEEP_TIME = 1500L; // 1.5s public static final int ITERATIONS = 4; @@ -71,33 +77,47 @@ public class RedirectTimeoutTest { @BeforeTest public void setup() throws IOException { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + h1TestServer = HttpTestServer.create(HTTP_1_1); h2TestServer = HttpTestServer.create(HTTP_2); + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); h1Uri = URI.create("http://" + h1TestServer.serverAuthority() + "/h1_test"); h1RedirectUri = URI.create("http://" + h1TestServer.serverAuthority() + "/h1_redirect"); h2Uri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_test"); h2RedirectUri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_redirect"); + h3Uri = URI.create("https://" + h3TestServer.serverAuthority() + "/h3_test"); + h3RedirectUri = URI.create("https://" + h3TestServer.serverAuthority() + "/h3_redirect"); h2WarmupUri = URI.create("http://" + h2TestServer.serverAuthority() + "/h2_warmup"); + h3WarmupUri = URI.create("https://" + h3TestServer.serverAuthority() + "/h3_warmup"); h1TestServer.addHandler(new GetHandler(), "/h1_test"); h1TestServer.addHandler(new RedirectHandler(), "/h1_redirect"); h2TestServer.addHandler(new GetHandler(), "/h2_test"); h2TestServer.addHandler(new RedirectHandler(), "/h2_redirect"); - h2TestServer.addHandler(new Http2Warmup(), "/h2_warmup"); + h3TestServer.addHandler(new GetHandler(), "/h3_test"); + h3TestServer.addHandler(new RedirectHandler(), "/h3_redirect"); + h2TestServer.addHandler(new HttpWarmup(), "/h2_warmup"); + h3TestServer.addHandler(new HttpWarmup(), "/h3_warmup"); h1TestServer.start(); h2TestServer.start(); + h3TestServer.start(); } @AfterTest public void teardown() { h1TestServer.stop(); h2TestServer.stop(); + h3TestServer.stop(); } @DataProvider(name = "testData") public Object[][] testData() { return new Object[][] { + { HTTP_3, h3Uri, h3RedirectUri }, { HTTP_1_1, h1Uri, h1RedirectUri }, - { HTTP_2, h2Uri, h2RedirectUri } + { HTTP_2, h2Uri, h2RedirectUri }, }; } @@ -105,16 +125,31 @@ public class RedirectTimeoutTest { public void test(Version version, URI uri, URI redirectURI) throws InterruptedException { out.println("Testing for " + version); testRedirectURI = redirectURI; - HttpClient.Builder clientBuilder = HttpClient.newBuilder().followRedirects(ALWAYS); - HttpRequest request = HttpRequest.newBuilder().uri(uri) + HttpClient.Builder clientBuilder = version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + clientBuilder = clientBuilder.followRedirects(ALWAYS).sslContext(sslContext); + HttpRequest.Builder reqBuilder = HttpRequest.newBuilder().uri(uri); + if (version == HTTP_3) { + reqBuilder = reqBuilder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + HttpRequest request = reqBuilder .GET() .version(version) .timeout(Duration.ofMillis(adjustTimeout(TIMEOUT_MILLIS))) .build(); try (HttpClient client = clientBuilder.build()) { - if (version.equals(HTTP_2)) - client.send(HttpRequest.newBuilder(h2WarmupUri).HEAD().build(), HttpResponse.BodyHandlers.discarding()); + Optional warmupUri = switch (version) { + case HTTP_1_1 -> Optional.empty(); + case HTTP_2 -> Optional.of(h2WarmupUri); + case HTTP_3 -> Optional.of(h3WarmupUri); + }; + if (warmupUri.isPresent()) { + HttpRequest head = reqBuilder.copy().uri(warmupUri.get()) + .version(version).HEAD().build(); + client.send(head, HttpResponse.BodyHandlers.discarding()); + } /* With TIMEOUT_MILLIS set to 1500ms and the server's RedirectHandler sleeping for 750ms before responding to each request, 4 iterations will take a guaranteed minimum time of 3000ms which will ensure that any @@ -135,7 +170,7 @@ public class RedirectTimeoutTest { } } - public static class Http2Warmup implements HttpTestHandler { + public static class HttpWarmup implements HttpTestHandler { @Override public void handle(HttpTestExchange t) throws IOException { diff --git a/test/jdk/java/net/httpclient/RedirectWithCookie.java b/test/jdk/java/net/httpclient/RedirectWithCookie.java index 13dfe766647..afc8618835b 100644 --- a/test/jdk/java/net/httpclient/RedirectWithCookie.java +++ b/test/jdk/java/net/httpclient/RedirectWithCookie.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,15 +31,10 @@ * RedirectWithCookie */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CookieManager; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; @@ -49,7 +44,6 @@ import java.net.http.HttpResponse.BodyHandlers; import java.util.List; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -58,6 +52,9 @@ import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -69,10 +66,12 @@ public class RedirectWithCookie implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String MESSAGE = "BasicRedirectTest message body"; static final int ITERATIONS = 3; @@ -80,6 +79,7 @@ public class RedirectWithCookie implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, }, { httpURI, }, { httpsURI, }, { http2URI, }, @@ -87,10 +87,22 @@ public class RedirectWithCookie implements HttpServerAdapters { }; } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "positive") void test(String uriString) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); - HttpClient client = HttpClient.newBuilder() + var builder = uriString.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .followRedirects(Redirect.ALWAYS) .cookieHandler(new CookieManager()) .sslContext(sslContext) @@ -98,7 +110,7 @@ public class RedirectWithCookie implements HttpServerAdapters { assert client.cookieHandler().isPresent(); URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); out.println("Initial request: " + request.uri()); for (int i=0; i< ITERATIONS; i++) { @@ -114,6 +126,8 @@ public class RedirectWithCookie implements HttpServerAdapters { assertTrue(response.uri().getPath().endsWith("message")); assertPreviousRedirectResponses(request, response); } + + client.close(); } static void assertPreviousRedirectResponses(HttpRequest initialRequest, @@ -164,10 +178,15 @@ public class RedirectWithCookie implements HttpServerAdapters { https2TestServer.addHandler(new CookieRedirectHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/redirect"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new CookieRedirectHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/redirect"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -176,6 +195,7 @@ public class RedirectWithCookie implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class CookieRedirectHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/ReferenceTracker.java b/test/jdk/java/net/httpclient/ReferenceTracker.java index d7e16d01201..6ea97ab6ac2 100644 --- a/test/jdk/java/net/httpclient/ReferenceTracker.java +++ b/test/jdk/java/net/httpclient/ReferenceTracker.java @@ -133,6 +133,15 @@ public class ReferenceTracker { "outstanding operations or unreleased resources", true); } + public AssertionError checkClosed(long graceDelayMs) { + Predicate hasOperations = (t) -> t.getOutstandingOperations() > 0; + Predicate hasSubscribers = (t) -> t.getOutstandingSubscribers() > 0; + return check(graceDelayMs, + hasOperations.or(hasSubscribers) + .or(Tracker::isSelectorAlive), + "outstanding operations or unreleased resources", true); + } + // This method is copied from ThreadInfo::toString, but removes the // limit on the stack trace depth (8 frames max) that ThreadInfo::toString // forcefully implement. We want to print all frames for better diagnosis. @@ -369,6 +378,7 @@ public class ReferenceTracker { warning.append("\n\tPending HTTP Requests: " + tracker.getOutstandingHttpRequests()); warning.append("\n\tPending HTTP/1.1 operations: " + tracker.getOutstandingHttpOperations()); warning.append("\n\tPending HTTP/2 streams: " + tracker.getOutstandingHttp2Streams()); + warning.append("\n\tPending HTTP/3 streams: " + tracker.getOutstandingHttp3Streams()); warning.append("\n\tPending WebSocket operations: " + tracker.getOutstandingWebSocketOperations()); warning.append("\n\tPending TCP connections: " + tracker.getOutstandingTcpConnections()); warning.append("\n\tPending Subscribers: " + tracker.getOutstandingSubscribers()); @@ -404,4 +414,23 @@ public class ReferenceTracker { "outstanding unclosed resources", true); return failed; } + + // This is a slightly more permissive check than the default checks, + // it only verifies that all CFs returned by send/sendAsync have been + // completed, and that all opened channels have been closed, and that + // the selector manager thread has exited. + // It doesn't check that all refcounts have reached 0. + // This is typically useful to only check that resources have been released. + public AssertionError checkShutdown(Tracker tracker, long graceDelayMs, boolean dumpThreads) { + Predicate isAlive = Tracker::isSelectorAlive; + Predicate hasPendingRequests = (t) -> t.getOutstandingHttpRequests() > 0; + Predicate hasPendingConnections = (t) -> t.getOutstandingTcpConnections() > 0; + Predicate hasPendingSubscribers = (t) -> t.getOutstandingSubscribers() > 0; + AssertionError failed = check(tracker, graceDelayMs, + isAlive.or(hasPendingRequests) + .or(hasPendingConnections) + .or(hasPendingSubscribers), + "outstanding unclosed resources", dumpThreads); + return failed; + } } diff --git a/test/jdk/java/net/httpclient/RequestBuilderTest.java b/test/jdk/java/net/httpclient/RequestBuilderTest.java index 991b6f9d2d9..97f4793d63c 100644 --- a/test/jdk/java/net/httpclient/RequestBuilderTest.java +++ b/test/jdk/java/net/httpclient/RequestBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,19 +30,25 @@ import java.net.URI; import java.net.URISyntaxException; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; import java.util.List; import java.util.Map; import java.util.Set; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpRequest.newBuilder; import static java.time.Duration.ofNanos; import static java.time.Duration.ofMinutes; import static java.time.Duration.ofSeconds; import static java.time.Duration.ZERO; -import static java.net.http.HttpClient.Version.HTTP_1_1; -import static java.net.http.HttpClient.Version.HTTP_2; -import static java.net.http.HttpRequest.newBuilder; import static org.testng.Assert.*; import org.testng.annotations.Test; @@ -96,6 +102,8 @@ public class RequestBuilderTest { assertThrows(NPE, () -> builder.setHeader(null, null)); assertThrows(NPE, () -> builder.setHeader("name", null)); assertThrows(NPE, () -> builder.setHeader(null, "value")); + assertThrows(NPE, () -> builder.setOption(null, null)); + assertThrows(NPE, () -> builder.setOption((HttpOption) null, ANY)); assertThrows(NPE, () -> builder.timeout(null)); assertThrows(NPE, () -> builder.POST(null)); assertThrows(NPE, () -> builder.PUT(null)); @@ -402,6 +410,7 @@ public class RequestBuilderTest { .header("A", "B") .POST(BodyPublishers.ofString("")) .timeout(ofSeconds(30)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) .version(HTTP_1_1); HttpRequest.Builder copy = builder.copy(); assertTrue(builder != copy); @@ -418,6 +427,8 @@ public class RequestBuilderTest { assertEquals(copyRequest.timeout().get(), ofSeconds(30)); assertTrue(copyRequest.version().isPresent()); assertEquals(copyRequest.version().get(), HTTP_1_1); + assertTrue(copyRequest.getOption(H3_DISCOVERY).isPresent()); + assertEquals(copyRequest.getOption(H3_DISCOVERY).get(), HTTP_3_URI_ONLY); // lazy set URI ( maybe builder as a template ) copyRequest = newBuilder().copy().uri(uri).build(); diff --git a/test/jdk/java/net/httpclient/Response1xxTest.java b/test/jdk/java/net/httpclient/Response1xxTest.java index 57aed9407c2..d0148f0d684 100644 --- a/test/jdk/java/net/httpclient/Response1xxTest.java +++ b/test/jdk/java/net/httpclient/Response1xxTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,22 +30,29 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpTimeoutException; import java.nio.charset.StandardCharsets; import java.time.Duration; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpClient.Version.valueOf; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -55,7 +62,7 @@ import static java.net.http.HttpClient.Version.HTTP_2; * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.httpclient.test.lib.http2.Http2TestServer * @run testng/othervm -Djdk.internal.httpclient.debug=true - * * -Djdk.httpclient.HttpClient.log=headers,requests,responses,errors Response1xxTest + * -Djdk.httpclient.HttpClient.log=headers,requests,responses,errors Response1xxTest */ public class Response1xxTest implements HttpServerAdapters { private static final String EXPECTED_RSP_BODY = "Hello World"; @@ -73,6 +80,9 @@ public class Response1xxTest implements HttpServerAdapters { private HttpTestServer https2Server; // h2 private String https2RequestURIBase; + private HttpTestServer http3Server; // h3 + private String http3RequestURIBase; + private final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; @BeforeClass @@ -81,7 +91,7 @@ public class Response1xxTest implements HttpServerAdapters { server = new Http11Server(serverSocket); new Thread(server).start(); http1RequestURIBase = URIBuilder.newBuilder().scheme("http").loopback() - .port(serverSocket.getLocalPort()).build().toString(); + .port(serverSocket.getLocalPort()).path("/http1").build().toString(); http2Server = HttpTestServer.create(HTTP_2); http2Server.addHandler(new Http2Handler(), "/http2/102"); @@ -109,6 +119,19 @@ public class Response1xxTest implements HttpServerAdapters { https2Server.start(); System.out.println("Started (https) HTTP2 server at " + https2Server.getAddress()); + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3Server.addHandler(new Http3Handler(), "/http3/102"); + http3Server.addHandler(new Http3Handler(), "/http3/103"); + http3Server.addHandler(new Http3Handler(), "/http3/100"); + http3Server.addHandler(new Http3Handler(), "/http3/101"); + http3Server.addHandler(new OKHandler(), "/http3/200"); + http3Server.addHandler(new OnlyInformationalHandler(), "/http3/only-informational"); + http3RequestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(http3Server.getAddress().getPort()) + .path("/http3").build().toString(); + http3Server.start(); + System.out.println("Started (https) HTTP3 server at " + http3Server.getAddress()); + } @AfterClass @@ -132,6 +155,10 @@ public class Response1xxTest implements HttpServerAdapters { https2Server.stop(); System.out.println("Stopped (https) HTTP2 server"); } + if (http3Server != null) { + http3Server.stop(); + System.out.println("Stopped (https) HTTP3 server"); + } } } @@ -142,10 +169,11 @@ public class Response1xxTest implements HttpServerAdapters { "Content-Length: " + CONTENT_LENGTH + "\r\n\r\n" + EXPECTED_RSP_BODY; - private static final String REQ_LINE_FOO = "GET /test/foo HTTP/1.1\r\n"; - private static final String REQ_LINE_BAR = "GET /test/bar HTTP/1.1\r\n"; - private static final String REQ_LINE_HELLO = "GET /test/hello HTTP/1.1\r\n"; - private static final String REQ_LINE_BYE = "GET /test/bye HTTP/1.1\r\n"; + private static final String REQ_LINE_102 = "GET /http1/102 HTTP/1.1\r\n"; + private static final String REQ_LINE_103 = "GET /http1/103 HTTP/1.1\r\n"; + private static final String REQ_LINE_100 = "GET /http1/100 HTTP/1.1\r\n"; + private static final String REQ_LINE_101 = "GET /http1/101 HTTP/1.1\r\n"; + private static final String REQ_LINE_ONLY_INFO = "GET /http1/only-informational HTTP/1.1\r\n"; private final ServerSocket serverSocket; @@ -160,6 +188,7 @@ public class Response1xxTest implements HttpServerAdapters { System.out.println("Server running at " + serverSocket); while (!stop) { Socket socket = null; + boolean onlyInfo = false; try { // accept a connection socket = serverSocket.accept(); @@ -180,18 +209,22 @@ public class Response1xxTest implements HttpServerAdapters { System.out.println("Received following request line from client " + socket + " :\n" + requestLine); final int informationalResponseCode; - if (requestLine.startsWith(REQ_LINE_FOO)) { + if (requestLine.startsWith(REQ_LINE_102)) { // we will send intermediate/informational 102 response informationalResponseCode = 102; - } else if (requestLine.startsWith(REQ_LINE_BAR)) { + } else if (requestLine.startsWith(REQ_LINE_103)) { // we will send intermediate/informational 103 response informationalResponseCode = 103; - } else if (requestLine.startsWith(REQ_LINE_HELLO)) { + } else if (requestLine.startsWith(REQ_LINE_100)) { // we will send intermediate/informational 100 response informationalResponseCode = 100; - } else if (requestLine.startsWith(REQ_LINE_BYE)) { + } else if (requestLine.startsWith(REQ_LINE_101)) { // we will send intermediate/informational 101 response informationalResponseCode = 101; + } else if (requestLine.startsWith(REQ_LINE_ONLY_INFO)) { + // we will send intermediate/informational 102 response + informationalResponseCode = 102; + onlyInfo = true; } else { // unexpected client. ignore and close the client System.err.println("Ignoring unexpected request from client " + socket); @@ -215,6 +248,10 @@ public class Response1xxTest implements HttpServerAdapters { os.flush(); System.out.println("Sent response code " + informationalResponseCode + " to client " + socket); + if (onlyInfo) { + Thread.sleep(2000); + i = 1; + } } // now send a final response System.out.println("Now sending 200 response code to client " + socket); @@ -226,8 +263,10 @@ public class Response1xxTest implements HttpServerAdapters { // close the client connection safeClose(socket); // continue accepting any other client connections until we are asked to stop - System.err.println("Ignoring exception in server:"); - t.printStackTrace(); + if (!onlyInfo) { + System.err.println("Ignoring exception in server:"); + t.printStackTrace(); + } } } } @@ -261,6 +300,8 @@ public class Response1xxTest implements HttpServerAdapters { @Override public void handle(final HttpTestExchange exchange) throws IOException { final URI requestURI = exchange.getRequestURI(); + final Version version = exchange.getServerVersion(); + final int informationResponseCode; if (requestURI.getPath().endsWith("/102")) { informationResponseCode = 102; @@ -281,25 +322,29 @@ public class Response1xxTest implements HttpServerAdapters { // be sent multiple times) for (int i = 0; i < 3; i++) { exchange.sendResponseHeaders(informationResponseCode, -1); - System.out.println("Sent " + informationResponseCode + " response code from H2 server"); + System.out.println("Sent " + informationResponseCode + " response code from " + version + " server"); } // now send 200 response try { final byte[] body = EXPECTED_RSP_BODY.getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, body.length); - System.out.println("Sent 200 response from H2 server"); + System.out.println("Sent 200 response from " + version + " server"); try (OutputStream os = exchange.getResponseBody()) { os.write(body); } - System.out.println("Sent response body from H2 server"); + System.out.println("Sent response body from " + version + " server"); } catch (Throwable e) { - System.err.println("Failed to send response from HTTP2 handler:"); + System.err.println("Failed to send response from " + version + " handler:"); e.printStackTrace(); throw e; } } } + private static final class Http3Handler extends Http2Handler { + + } + private static class OnlyInformationalHandler implements HttpTestHandler { @Override @@ -338,20 +383,7 @@ public class Response1xxTest implements HttpServerAdapters { .version(HttpClient.Version.HTTP_1_1) .proxy(HttpClient.Builder.NO_PROXY).build(); TRACKER.track(client); - final URI[] requestURIs = new URI[]{ - new URI(http1RequestURIBase + "/test/foo"), - new URI(http1RequestURIBase + "/test/bar"), - new URI(http1RequestURIBase + "/test/hello")}; - for (final URI requestURI : requestURIs) { - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); - System.out.println("Issuing request to " + requestURI); - final HttpResponse response = client.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - Assert.assertEquals(response.version(), HttpClient.Version.HTTP_1_1, - "Unexpected HTTP version in response"); - Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); - Assert.assertEquals(response.body(), EXPECTED_RSP_BODY, "Unexpected response body"); - } + test1xxFor(client, HTTP_1_1, http1RequestURIBase); } /** @@ -364,23 +396,58 @@ public class Response1xxTest implements HttpServerAdapters { final HttpClient client = HttpClient.newBuilder() .version(HTTP_2) .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxFor(client, HTTP_2, http2RequestURIBase); + } + + /** + * Tests that when a HTTP3 server sends intermediate 1xx response codes and then the final + * response, the client (internally) will ignore those intermediate informational response codes + * and only return the final response to the application + */ + @Test + public void test1xxForHTTP3() throws Exception { + final HttpClient client = newClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxFor(client, HTTP_3, http3RequestURIBase); + } + + private void test1xxFor(HttpClient client, Version version, String baseURI) throws Exception { TRACKER.track(client); final URI[] requestURIs = new URI[]{ - new URI(http2RequestURIBase + "/102"), - new URI(http2RequestURIBase + "/103"), - new URI(http2RequestURIBase + "/100")}; + new URI(baseURI + "/102"), + new URI(baseURI + "/103"), + new URI(baseURI + "/100")}; + var requestBuilder = HttpRequest.newBuilder(); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } for (final URI requestURI : requestURIs) { - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); + final HttpRequest request = requestBuilder.copy().uri(requestURI).build(); System.out.println("Issuing request to " + requestURI); final HttpResponse response = client.send(request, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - Assert.assertEquals(response.version(), HTTP_2, + BodyHandlers.ofString(StandardCharsets.UTF_8)); + Assert.assertEquals(response.version(), version, "Unexpected HTTP version in response"); Assert.assertEquals(response.statusCode(), 200, "Unexpected response code"); Assert.assertEquals(response.body(), EXPECTED_RSP_BODY, "Unexpected response body"); } } + /** + * Tests that when a request is issued with a specific request timeout and the server + * responds with intermediate 1xx response code but doesn't respond with a final response within + * the timeout duration, then the application fails with a request timeout + */ + @Test + public void test1xxRequestTimeoutH1() throws Exception { + final HttpClient client = HttpClient.newBuilder() + .version(HTTP_1_1) + .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxRequestTimeout(client, HTTP_1_1, http1RequestURIBase); + } + /** * Tests that when a request is issued with a specific request timeout and the server @@ -388,22 +455,45 @@ public class Response1xxTest implements HttpServerAdapters { * the timeout duration, then the application fails with a request timeout */ @Test - public void test1xxRequestTimeout() throws Exception { + public void test1xxRequestTimeoutH2() throws Exception { final HttpClient client = HttpClient.newBuilder() .version(HTTP_2) .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxRequestTimeout(client, HTTP_2, http2RequestURIBase); + } + + /** + * Tests that when a request is issued with a specific request timeout and the server + * responds with intermediate 1xx response code but doesn't respond with a final response within + * the timeout duration, then the application fails with a request timeout + */ + @Test + public void test1xxRequestTimeoutH3() throws Exception { + final HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY).build(); + test1xxRequestTimeout(client, HTTP_3, http3RequestURIBase); + } + + private void test1xxRequestTimeout(HttpClient client, Version version, String uriBase) throws Exception { TRACKER.track(client); - final URI requestURI = new URI(http2RequestURIBase + "/only-informational"); + final URI requestURI = new URI(uriBase + "/only-informational"); final Duration requestTimeout = Duration.ofSeconds(2); - final HttpRequest request = HttpRequest.newBuilder(requestURI).timeout(requestTimeout) + var requestBuilder = HttpRequest.newBuilder(requestURI); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + final HttpRequest request = requestBuilder.timeout(requestTimeout) .build(); System.out.println("Issuing request to " + requestURI); // we expect the request to timeout Assert.assertThrows(HttpTimeoutException.class, () -> { - client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + client.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8)); }); } + /** * Tests that when the HTTP/1.1 server sends a 101 response when the request hasn't asked * for an "Upgrade" then the request fails. @@ -413,13 +503,7 @@ public class Response1xxTest implements HttpServerAdapters { final HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .proxy(HttpClient.Builder.NO_PROXY).build(); - TRACKER.track(client); - final URI requestURI = new URI(http1RequestURIBase + "/test/bye"); - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); - System.out.println("Issuing request to " + requestURI); - // we expect the request to fail because the server sent an unexpected 101 - Assert.assertThrows(ProtocolException.class, - () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); + testUnexpected101(client, HTTP_1_1, http1RequestURIBase); } @@ -433,13 +517,20 @@ public class Response1xxTest implements HttpServerAdapters { .version(HTTP_2) .sslContext(sslContext) .proxy(HttpClient.Builder.NO_PROXY).build(); - TRACKER.track(client); - final URI requestURI = new URI(https2RequestURIBase + "/101"); - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); - System.out.println("Issuing request to " + requestURI); - // we expect the request to fail because the server sent an unexpected 101 - Assert.assertThrows(ProtocolException.class, - () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); + testUnexpected101(client, HTTP_2, https2RequestURIBase); + } + + /** + * Tests that when the HTTP2 server (over HTTPS) sends a 101 response when the request + * hasn't asked for an "Upgrade" then the request fails. + */ + @Test + public void testHTT3Unexpected101() throws Exception { + final HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .sslContext(sslContext) + .proxy(HttpClient.Builder.NO_PROXY).build(); + testUnexpected101(client, HTTP_3, http3RequestURIBase); } /** @@ -451,7 +542,6 @@ public class Response1xxTest implements HttpServerAdapters { final HttpClient client = HttpClient.newBuilder() .version(HTTP_2) .proxy(HttpClient.Builder.NO_PROXY).build(); - TRACKER.track(client); // when using HTTP2 version against a "http://" (non-secure) URI // the HTTP client (implementation) internally initiates a HTTP/1.1 connection // and then does an "Upgrade:" to "h2c". This it does when there isn't already a @@ -462,12 +552,21 @@ public class Response1xxTest implements HttpServerAdapters { // start our testing warmupH2Client(client); // start the actual testing - final URI requestURI = new URI(http2RequestURIBase + "/101"); - final HttpRequest request = HttpRequest.newBuilder(requestURI).build(); + testUnexpected101(client, HTTP_2, http2RequestURIBase); + } + + private void testUnexpected101(HttpClient client, Version version, String baseUri) throws Exception { + TRACKER.track(client); + final URI requestURI = new URI(baseUri + "/101"); + var requestBuilder = HttpRequest.newBuilder(requestURI); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + final HttpRequest request = requestBuilder.build(); System.out.println("Issuing request to " + requestURI); // we expect the request to fail because the server sent an unexpected 101 Assert.assertThrows(ProtocolException.class, - () -> client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))); + () -> client.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8))); } // sends a request and expects a 200 response back diff --git a/test/jdk/java/net/httpclient/Response204V2Test.java b/test/jdk/java/net/httpclient/Response204V2Test.java index 610c312b667..9f867d0fb17 100644 --- a/test/jdk/java/net/httpclient/Response204V2Test.java +++ b/test/jdk/java/net/httpclient/Response204V2Test.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -54,7 +54,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.ITestContext; @@ -71,14 +70,19 @@ import javax.net.ssl.SSLContext; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; public class Response204V2Test implements HttpServerAdapters { SSLContext sslContext; HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String http2URI; String https2URI; + String http3URI; static final int RESPONSE_CODE = 204; static final int ITERATION_COUNT = 4; @@ -177,6 +181,7 @@ public class Response204V2Test implements HttpServerAdapters { private String[] uris() { return new String[] { + http3URI, http2URI, https2URI, }; @@ -201,9 +206,9 @@ public class Response204V2Test implements HttpServerAdapters { return result; } - private HttpClient makeNewClient() { + private HttpClient makeNewClient(HttpClient.Builder builder) { clientCount.incrementAndGet(); - HttpClient client = HttpClient.newBuilder() + HttpClient client = builder .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) @@ -211,14 +216,17 @@ public class Response204V2Test implements HttpServerAdapters { return TRACKER.track(client); } - HttpClient newHttpClient(boolean share) { - if (!share) return makeNewClient(); + HttpClient newHttpClient(String uri, boolean share) { + if (!share) return makeNewClient(newClientBuilderForH3()); HttpClient shared = sharedClient; if (shared != null) return shared; synchronized (this) { shared = sharedClient; if (shared == null) { - shared = sharedClient = makeNewClient(); + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + shared = sharedClient = makeNewClient(builder); } return shared; } @@ -241,25 +249,37 @@ public class Response204V2Test implements HttpServerAdapters { } } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } @Test(dataProvider = "variants") public void test(String uri, boolean sameClient) throws Exception { checkSkip(); - System.out.println("Request to " + uri); + out.println("Request to " + uri); - HttpClient client = newHttpClient(sameClient); + HttpClient client = newHttpClient(uri, sameClient); - HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest request = newRequestBuilder(URI.create(uri)) .GET() .build(); for (int i = 0; i < ITERATION_COUNT; i++) { - System.out.println("Iteration: " + i); + out.println("Iteration: " + i); HttpResponse response = client.send(request, BodyHandlers.ofString()); int expectedResponse = RESPONSE_CODE; if (response.statusCode() != expectedResponse) - throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode())); + throw new RuntimeException("wrong response code " + response.statusCode()); } - System.out.println("test: DONE"); + if (!sameClient) { + out.println("test: closing test client"); + client.close(); + } + out.println("test: DONE"); } @BeforeTest @@ -279,9 +299,14 @@ public class Response204V2Test implements HttpServerAdapters { https2TestServer.addHandler(handler204, "/https2/test204/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/test204/x"; - serverCount.addAndGet(4); + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(handler204, "/http3/test204/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/test204/x"; + + serverCount.addAndGet(3); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -294,6 +319,7 @@ public class Response204V2Test implements HttpServerAdapters { try { http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { if (sharedClientName != null) { diff --git a/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java b/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java index beed8f86fa6..04ce3d531af 100644 --- a/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java +++ b/test/jdk/java/net/httpclient/ResponseBodyBeforeError.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff --git a/test/jdk/java/net/httpclient/ResponsePublisher.java b/test/jdk/java/net/httpclient/ResponsePublisher.java index 3d6cf601b2f..5d90e20c626 100644 --- a/test/jdk/java/net/httpclient/ResponsePublisher.java +++ b/test/jdk/java/net/httpclient/ResponsePublisher.java @@ -31,11 +31,7 @@ * @run testng/othervm/timeout=480 ResponsePublisher */ -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.internal.net.http.common.OperationTrackers; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; @@ -48,10 +44,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; @@ -71,11 +65,13 @@ import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -84,10 +80,11 @@ import static org.testng.Assert.assertTrue; public class ResponsePublisher implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; @@ -96,6 +93,8 @@ public class ResponsePublisher implements HttpServerAdapters { String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; + String http3URI_fixed; + String http3URI_chunk; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test @@ -143,6 +142,16 @@ public class ResponsePublisher implements HttpServerAdapters { @DataProvider(name = "variants") public Object[][] variants() { return new Object[][]{ + { http3URI_fixed, false, OF_PUBLISHER_API }, + { http3URI_chunk, false, OF_PUBLISHER_API }, + { http3URI_fixed, true, OF_PUBLISHER_API }, + { http3URI_chunk, true, OF_PUBLISHER_API }, + + { http3URI_fixed, false, OF_PUBLISHER_TEST }, + { http3URI_chunk, false, OF_PUBLISHER_TEST }, + { http3URI_fixed, true, OF_PUBLISHER_TEST }, + { http3URI_chunk, true, OF_PUBLISHER_TEST }, + { httpURI_fixed, false, OF_PUBLISHER_API }, { httpURI_chunk, false, OF_PUBLISHER_API }, { httpsURI_fixed, false, OF_PUBLISHER_API }, @@ -182,21 +191,33 @@ public class ResponsePublisher implements HttpServerAdapters { } final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; - HttpClient newHttpClient() { - return TRACKER.track(HttpClient.newBuilder() + HttpClient newHttpClient(String uri) { + var builder = uri.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + return TRACKER.track(builder .executor(executor) .sslContext(sslContext) .build()); } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "variants") public void testExceptions(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -241,9 +262,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -270,9 +291,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) + HttpRequest req = newRequestBuilder(URI.create(uri)) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -302,9 +323,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); HttpResponse>> response = client.send(req, handler); @@ -331,9 +352,9 @@ public class ResponsePublisher implements HttpServerAdapters { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) - client = newHttpClient(); + client = newHttpClient(uri); - HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) + HttpRequest req = newRequestBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler>> handler = handlers.get(); // We can reuse our BodySubscribers implementations to subscribe to the @@ -470,10 +491,21 @@ public class ResponsePublisher implements HttpServerAdapters { https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; + // HTTP/3 + HttpTestHandler h3_fixedLengthHandler = new HTTP_FixedLengthHandler(); + HttpTestHandler h3_chunkedHandler = new HTTP_VariableLengthHandler(); + + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(h3_fixedLengthHandler, "/http3/fixed"); + http3TestServer.addHandler(h3_chunkedHandler, "/http3/chunk"); + http3URI_fixed = "https://" + http3TestServer.serverAuthority() + "/http3/fixed"; + http3URI_chunk = "https://" + http3TestServer.serverAuthority() + "/http3/chunk"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -485,6 +517,7 @@ public class ResponsePublisher implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) { throw fail; diff --git a/test/jdk/java/net/httpclient/RestrictedHeadersTest.java b/test/jdk/java/net/httpclient/RestrictedHeadersTest.java index 7e5881d3cee..f2ddf85b3f5 100644 --- a/test/jdk/java/net/httpclient/RestrictedHeadersTest.java +++ b/test/jdk/java/net/httpclient/RestrictedHeadersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -47,7 +47,7 @@ public class RestrictedHeadersTest { // This list must be same as impl static Set defaultRestrictedHeaders = - Set.of("connection", "content-length", "expect", "host", "upgrade"); + Set.of("connection", "content-length", "expect", "host", "upgrade", "alt-used"); private static void runDefaultTest() { System.out.println("DEFAULT TEST: no property set"); diff --git a/test/jdk/java/net/httpclient/RetryWithCookie.java b/test/jdk/java/net/httpclient/RetryWithCookie.java index ec83fa5df89..dc046031bd3 100644 --- a/test/jdk/java/net/httpclient/RetryWithCookie.java +++ b/test/jdk/java/net/httpclient/RetryWithCookie.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,9 +34,6 @@ * RetryWithCookie */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -48,8 +45,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CookieManager; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; @@ -63,12 +58,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -76,14 +73,16 @@ import static org.testng.Assert.assertTrue; public class RetryWithCookie implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 5 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String MESSAGE = "BasicRedirectTest message body"; static final int ITERATIONS = 3; @@ -91,6 +90,7 @@ public class RetryWithCookie implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, }, { httpURI, }, { httpsURI, }, { http2URI, }, @@ -101,11 +101,23 @@ public class RetryWithCookie implements HttpServerAdapters { static final AtomicLong requestCounter = new AtomicLong(); final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "positive") void test(String uriString) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); CookieManager cookieManager = new CookieManager(); - HttpClient client = HttpClient.newBuilder() + var builder = uriString.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) .cookieHandler(cookieManager) @@ -121,7 +133,7 @@ public class RetryWithCookie implements HttpServerAdapters { cookieHeaders.put("Set-Cookie", cookies); cookieManager.put(uri, cookieHeaders); - HttpRequest request = HttpRequest.newBuilder(uri) + HttpRequest request = newRequestBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) .build(); out.println("Initial request: " + request.uri()); @@ -136,7 +148,7 @@ public class RetryWithCookie implements HttpServerAdapters { assertEquals(response.statusCode(), 200); assertEquals(response.body(), MESSAGE); assertEquals(response.headers().allValues("X-Request-Cookie"), cookies); - request = HttpRequest.newBuilder(uri) + request = newRequestBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) .build(); } @@ -164,10 +176,15 @@ public class RetryWithCookie implements HttpServerAdapters { https2TestServer.addHandler(new CookieRetryHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new CookieRetryHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -179,6 +196,7 @@ public class RetryWithCookie implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/ShutdownNow.java b/test/jdk/java/net/httpclient/ShutdownNow.java index 045876597a2..a5850b4af61 100644 --- a/test/jdk/java/net/httpclient/ShutdownNow.java +++ b/test/jdk/java/net/httpclient/ShutdownNow.java @@ -45,7 +45,9 @@ import java.io.OutputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.channels.ClosedChannelException; @@ -73,6 +75,10 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -90,10 +96,15 @@ public class ShutdownNow implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer h2h3TestServer; // HTTP/3 ( h2 + h3 ) + HttpTestServer h3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String h2h3URI; + String h2h3Head; + String h3URI; static final String MESSAGE = "ShutdownNow message body"; static final int ITERATIONS = 3; @@ -101,10 +112,12 @@ public class ShutdownNow implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { - { httpURI, }, - { httpsURI, }, - { http2URI, }, - { https2URI, }, + { h2h3URI, HTTP_3, h2h3TestServer.h3DiscoveryConfig()}, + { h3URI, HTTP_3, h3TestServer.h3DiscoveryConfig()}, + { httpURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { httpsURI, HTTP_1_1, ALT_SVC}, // do not attempt HTTP/3 + { http2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 + { https2URI, HTTP_2, ALT_SVC}, // do not attempt HTTP/3 }; } @@ -118,6 +131,15 @@ public class ShutdownNow implements HttpServerAdapters { return t; } + void headRequest(HttpClient client) throws Exception { + HttpRequest request = HttpRequest.newBuilder(URI.create(h2h3Head)) + .version(HTTP_2) + .HEAD() + .build(); + var resp = client.send(request, BodyHandlers.discarding()); + assertEquals(resp.statusCode(), 200); + } + static boolean hasExpectedMessage(IOException io) { String message = io.getMessage(); if (message == null) return false; @@ -155,22 +177,28 @@ public class ShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testConcurrent(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testConcurrent(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting concurrent (%s, %s, %s) ----%n%n", uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); int step = RANDOM.nextInt(ITERATIONS); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + List>> responses = new ArrayList<>(); for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/concurrent/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; @@ -216,11 +244,13 @@ public class ShutdownNow implements HttpServerAdapters { } @Test(dataProvider = "positive") - void testSequential(String uriString) throws Exception { - out.printf("%n---- starting (%s) ----%n", uriString); - HttpClient client = HttpClient.newBuilder() + void testSequential(String uriString, Version version, Http3DiscoveryMode config) throws Exception { + out.printf("%n---- starting sequential (%s, %s, %s) ----%n%n", + uriString, version, config); + HttpClient client = newClientBuilderForH3() .proxy(NO_PROXY) .followRedirects(Redirect.ALWAYS) + .version(version == HTTP_1_1 ? HTTP_2 : version) .sslContext(sslContext) .build(); TRACKER.track(client); @@ -228,10 +258,15 @@ public class ShutdownNow implements HttpServerAdapters { int step = RANDOM.nextInt(ITERATIONS); out.printf("will shutdown client in step %d%n", step); try { + if (version == HTTP_3 && config != HTTP_3_URI_ONLY) { + headRequest(client); + } + for (int i = 0; i < ITERATIONS; i++) { URI uri = URI.create(uriString + "/sequential/iteration-" + i); HttpRequest request = HttpRequest.newBuilder(uri) .header("X-uuid", "uuid-" + requestCounter.incrementAndGet()) + .setOption(H3_DISCOVERY, config) .build(); out.printf("Iteration %d request: %s%n", i, request.uri()); CompletableFuture> responseCF; @@ -304,10 +339,21 @@ public class ShutdownNow implements HttpServerAdapters { https2TestServer.addHandler(new ServerRequestHandler(), "/https2/exec/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/exec/retry"; + h2h3TestServer = HttpTestServer.create(HTTP_3, sslContext); + h2h3TestServer.addHandler(new ServerRequestHandler(), "/h2h3/exec/"); + h2h3URI = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/exec/retry"; + h2h3TestServer.addHandler(new HttpHeadOrGetHandler(), "/h2h3/head/"); + h2h3Head = "https://" + h2h3TestServer.serverAuthority() + "/h2h3/head/"; + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestHandler(), "/h3-only/exec/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3-only/exec/retry"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + h2h3TestServer.start(); + h3TestServer.start(); } @AfterTest @@ -319,6 +365,8 @@ public class ShutdownNow implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + h2h3TestServer.stop(); + h3TestServer.stop(); } finally { if (fail != null) throw fail; } diff --git a/test/jdk/java/net/httpclient/SmokeTest.java b/test/jdk/java/net/httpclient/SmokeTest.java index 56294e8f02f..a10afef562f 100644 --- a/test/jdk/java/net/httpclient/SmokeTest.java +++ b/test/jdk/java/net/httpclient/SmokeTest.java @@ -24,11 +24,12 @@ /* * @test * @bug 8087112 8178699 8338569 - * @modules java.net.http + * @modules java.net.http/jdk.internal.net.http.common * java.logging * jdk.httpserver - * @library /test/lib / + * @library /test/lib /test/jdk/java/net/httpclient/lib / * @build jdk.test.lib.net.SimpleSSLContext ProxyServer + * jdk.httpclient.test.lib.common.TestServerConfigurator * @compile ../../../com/sun/net/httpserver/LogFilter.java * @compile ../../../com/sun/net/httpserver/FileServerHandler.java * @run main/othervm @@ -92,6 +93,8 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Random; + +import jdk.httpclient.test.lib.common.TestServerConfigurator; import jdk.test.lib.net.SimpleSSLContext; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; @@ -808,7 +811,7 @@ public class SmokeTest { ctx = new SimpleSSLContext().get(); sslparams = ctx.getDefaultSSLParameters(); //sslparams.setProtocols(new String[]{"TLSv1.2"}); - s2.setHttpsConfigurator(new Configurator(ctx)); + s2.setHttpsConfigurator(new Configurator(addr.getAddress(), ctx)); s1.start(); s2.start(); @@ -935,14 +938,19 @@ public class SmokeTest { } static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { + private final InetAddress serverAddr; + + public Configurator(InetAddress serverAddr, SSLContext ctx) { super(ctx); + this.serverAddr = serverAddr; } - public void configure (HttpsParameters params) { - SSLParameters p = getSSLContext().getDefaultSSLParameters(); + @Override + public void configure(final HttpsParameters params) { + final SSLParameters p = getSSLContext().getDefaultSSLParameters(); + TestServerConfigurator.addSNIMatcher(this.serverAddr, p); //p.setProtocols(new String[]{"TLSv1.2"}); - params.setSSLParameters (p); + params.setSSLParameters(p); } } diff --git a/test/jdk/java/net/httpclient/SpecialHeadersTest.java b/test/jdk/java/net/httpclient/SpecialHeadersTest.java index d3e1f37b592..9ad3114da2a 100644 --- a/test/jdk/java/net/httpclient/SpecialHeadersTest.java +++ b/test/jdk/java/net/httpclient/SpecialHeadersTest.java @@ -92,6 +92,7 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; import static java.nio.charset.StandardCharsets.US_ASCII; import org.testng.Assert; import static org.testng.Assert.assertEquals; @@ -347,7 +348,7 @@ public class SpecialHeadersTest implements HttpServerAdapters { boolean isInitialRequest = i == 0; boolean isSecure = uri.getScheme().equalsIgnoreCase("https"); - boolean isHTTP2 = resp.version() == HTTP_2; + boolean isHTTP1 = resp.version() == HTTP_1_1; boolean isNotH2CUpgrade = isSecure || (sameClient == true && !isInitialRequest); boolean isDefaultHostHeader = name.equalsIgnoreCase("host") && useDefault; @@ -356,13 +357,13 @@ public class SpecialHeadersTest implements HttpServerAdapters { // header in the response, except the response to the h2c Upgrade // request which will have been sent through HTTP/1.1. - if (isDefaultHostHeader && isHTTP2 && isNotH2CUpgrade) { + if (isDefaultHostHeader && !isHTTP1 && isNotH2CUpgrade) { assertTrue(resp.headers().firstValue("X-" + key).isEmpty()); assertTrue(resp.headers().allValues("X-" + key).isEmpty()); out.println("No X-" + key + " header received, as expected"); } else { String receivedHeaderString = value == null ? null - : resp.headers().firstValue("X-" + key).orElse(null); + : resp.headers().firstValue("X-" + key).get(); out.println("Got X-" + key + ": " + resp.headers().allValues("X-" + key)); if (value != null) { assertEquals(receivedHeaderString, value); @@ -512,7 +513,7 @@ public class SpecialHeadersTest implements HttpServerAdapters { // header in the response, except the response to the h2c Upgrade // request which will have been sent through HTTP/1.1. - if (isDefaultHostHeader && resp.version() == HTTP_2 && isNotH2CUpgrade) { + if (isDefaultHostHeader && resp.version() != HTTP_1_1 && isNotH2CUpgrade) { assertTrue(resp.headers().firstValue("X-" + key).isEmpty()); assertTrue(resp.headers().allValues("X-" + key).isEmpty()); out.println("No X-" + key + " header received, as expected"); diff --git a/test/jdk/java/net/httpclient/SplitResponse.java b/test/jdk/java/net/httpclient/SplitResponse.java index ad04e8a1627..9213901df4b 100644 --- a/test/jdk/java/net/httpclient/SplitResponse.java +++ b/test/jdk/java/net/httpclient/SplitResponse.java @@ -24,11 +24,8 @@ import java.io.IOException; import java.net.SocketException; import java.net.URI; -import java.util.ArrayList; import java.util.EnumSet; -import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.List; import java.util.concurrent.CompletableFuture; import javax.net.ssl.SSLContext; import javax.net.ServerSocketFactory; @@ -43,6 +40,8 @@ import java.util.stream.Stream; import jdk.test.lib.net.SimpleSSLContext; import static java.lang.System.out; import static java.lang.String.format; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.net.http.HttpResponse.BodyHandlers.ofString; @@ -212,7 +211,10 @@ public class SplitResponse { HttpClient client = newHttpClient(); - HttpRequest request = HttpRequest.newBuilder(uri).version(version).build(); + HttpRequest request = HttpRequest.newBuilder(uri) + .version(version) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); HttpResponse r; CompletableFuture> cf1; diff --git a/test/jdk/java/net/httpclient/StreamCloseTest.java b/test/jdk/java/net/httpclient/StreamCloseTest.java index 2d00a539e80..fcb00188581 100644 --- a/test/jdk/java/net/httpclient/StreamCloseTest.java +++ b/test/jdk/java/net/httpclient/StreamCloseTest.java @@ -27,13 +27,12 @@ * @test * @bug 8257736 * @library /test/jdk/java/net/httpclient/lib + * @library /test/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters * jdk.httpclient.test.lib.http2.Http2TestServer * @run testng/othervm StreamCloseTest */ -import com.sun.net.httpserver.HttpServer; - import java.io.InputStream; import java.io.IOException; import java.net.http.HttpClient; @@ -42,11 +41,8 @@ import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; diff --git a/test/jdk/java/net/httpclient/StreamingBody.java b/test/jdk/java/net/httpclient/StreamingBody.java index 7943968d239..bff6a2b39c4 100644 --- a/test/jdk/java/net/httpclient/StreamingBody.java +++ b/test/jdk/java/net/httpclient/StreamingBody.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,14 +32,9 @@ * StreamingBody */ -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -47,7 +42,6 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -56,6 +50,9 @@ import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpClient.Builder.NO_PROXY; import static org.testng.Assert.assertEquals; @@ -67,10 +64,12 @@ public class StreamingBody implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; static final String MESSAGE = "StreamingBody message body"; static final int ITERATIONS = 100; @@ -78,6 +77,7 @@ public class StreamingBody implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, }, { httpURI, }, { httpsURI, }, { http2URI, }, @@ -85,15 +85,27 @@ public class StreamingBody implements HttpServerAdapters { }; } + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "positive") void test(String uriString) throws Exception { out.printf("%n---- starting (%s) ----%n", uriString); URI uri = URI.create(uriString); - HttpRequest request = HttpRequest.newBuilder(uri).build(); + HttpRequest request = newRequestBuilder(uri).build(); for (int i=0; i< ITERATIONS; i++) { out.println("iteration: " + i); - HttpResponse response = HttpClient.newBuilder() + var builder = uriString.contains("/http3/") + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpResponse response = builder .sslContext(sslContext) .proxy(NO_PROXY) .build() @@ -129,14 +141,20 @@ public class StreamingBody implements HttpServerAdapters { http2TestServer = HttpTestServer.create(HTTP_2); http2TestServer.addHandler(new MessageHandler(), "/http2/streamingbody/"); http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/streamingbody/y"; + https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(new MessageHandler(), "/https2/streamingbody/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/streamingbody/z"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new MessageHandler(), "/http3/streamingbody/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/streamingbody/z"; + httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -145,6 +163,7 @@ public class StreamingBody implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); } static class MessageHandler implements HttpTestHandler { diff --git a/test/jdk/java/net/httpclient/TEST.properties b/test/jdk/java/net/httpclient/TEST.properties index b4ca833b5e5..b79f7113952 100644 --- a/test/jdk/java/net/httpclient/TEST.properties +++ b/test/jdk/java/net/httpclient/TEST.properties @@ -1,9 +1,22 @@ -modules=java.base/sun.net.www.http \ +modules=java.base/jdk.internal.util \ + java.base/sun.net.www.http \ java.base/sun.net.www \ java.base/sun.net \ + java.net.http/jdk.internal.net.http \ java.net.http/jdk.internal.net.http.common \ java.net.http/jdk.internal.net.http.frame \ java.net.http/jdk.internal.net.http.hpack \ + java.base/jdk.internal.net.quic \ + java.net.http/jdk.internal.net.http.quic \ + java.net.http/jdk.internal.net.http.quic.packets \ + java.net.http/jdk.internal.net.http.quic.frames \ + java.net.http/jdk.internal.net.http.quic.streams \ + java.net.http/jdk.internal.net.http.http3.streams \ + java.net.http/jdk.internal.net.http.http3.frames \ + java.net.http/jdk.internal.net.http.http3 \ + java.net.http/jdk.internal.net.http.qpack \ + java.net.http/jdk.internal.net.http.qpack.readers \ + java.net.http/jdk.internal.net.http.qpack.writers \ + java.security.jgss \ java.logging \ jdk.httpserver -maxOutputSize = 2500000 diff --git a/test/jdk/java/net/httpclient/TimeoutBasic.java b/test/jdk/java/net/httpclient/TimeoutBasic.java index cdaedf06219..47210209408 100644 --- a/test/jdk/java/net/httpclient/TimeoutBasic.java +++ b/test/jdk/java/net/httpclient/TimeoutBasic.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,10 +32,13 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpTimeoutException; import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; import javax.net.ServerSocketFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLServerSocketFactory; +import java.nio.channels.DatagramChannel; import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -43,6 +46,13 @@ import java.util.concurrent.CompletionException; import java.util.function.Function; import static java.lang.System.out; +import static java.net.StandardSocketOptions.SO_REUSEADDR; +import static java.net.StandardSocketOptions.SO_REUSEPORT; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.H3_DISCOVERY; /** * @test @@ -67,7 +77,7 @@ public class TimeoutBasic { null); static final List VERSIONS = - Arrays.asList(HttpClient.Version.HTTP_2, HttpClient.Version.HTTP_1_1, null); + Arrays.asList(HTTP_2, HTTP_1_1, HTTP_3, null); static final List SCHEMES = List.of("https", "http"); @@ -81,7 +91,7 @@ public class TimeoutBasic { public static void main(String[] args) throws Exception { for (Function m : METHODS) { - for (HttpClient.Version version : List.of(HttpClient.Version.HTTP_1_1)) { + for (HttpClient.Version version : List.of(HTTP_1_1)) { for (HttpClient.Version reqVersion : VERSIONS) { for (String scheme : SCHEMES) { ServerSocketFactory ssf; @@ -141,11 +151,46 @@ public class TimeoutBasic { if (version != null) builder.version(version); HttpClient client = builder.build(); out.printf("%ntest(version=%s, reqVersion=%s, scheme=%s)%n", version, reqVersion, scheme); + DatagramChannel dc = null; try (ServerSocket ss = ssf.createServerSocket()) { ss.setReuseAddress(false); ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); int port = ss.getLocalPort(); - URI uri = new URI(scheme +"://localhost:" + port + "/"); + boolean useAltSvc = false; + if (reqVersion == HTTP_3 && "https".equalsIgnoreCase(scheme)) { + // Prevent the client to connecting to any random server + // opened by other tests on the machine, by opening a + // datagram channel on the same port than the server socket + dc = DatagramChannel.open(); + try { + if (dc.supportedOptions().contains(SO_REUSEADDR)) { + dc.setOption(SO_REUSEADDR, false); + } + if (dc.supportedOptions().contains(SO_REUSEPORT)) { + dc.setOption(SO_REUSEPORT, false); + } + dc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), port)); + } catch (IOException io) { + // failed to bind - presumably the port was already taken + // we will configure the request to use ALT_SVC instead, which + // means no HTTP/3 connection will be attempted + useAltSvc = true; + // cleanup channel + dc.close(); + dc = null; + out.println("HTTP/3 direct connection cannot be tested: " + io); + } + } + + // can only reach here if dc port == port + assert dc == null || ((InetSocketAddress)dc.getLocalAddress()).getPort() == port; + + URI uri = URIBuilder.newBuilder() + .scheme(scheme) + .loopback() + .port(port) + .path("/") + .build(); out.println("--- TESTING Async"); int count = 0; @@ -153,6 +198,13 @@ public class TimeoutBasic { out.println(" with duration of " + duration); HttpRequest request = newRequest(uri, duration, reqVersion, method); if (request == null) continue; + if (useAltSvc) { + // make sure request will be downgraded to HTTP/2 if we + // have not been able to create `dc`. + request = HttpRequest.newBuilder(request, (n,v) -> true) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + } count++; try { HttpResponse resp = client.sendAsync(request, BodyHandlers.discarding()).join(); @@ -163,11 +215,17 @@ public class TimeoutBasic { out.println("Body (should be null): " + resp.body()); throw new RuntimeException("Unexpected response: " + resp.statusCode()); } catch (CompletionException e) { - if (!(e.getCause() instanceof HttpTimeoutException)) { + Throwable x = e; + if (x.getCause() instanceof SSLHandshakeException s) { + if (s.getCause() instanceof HttpTimeoutException) { + x = s; + } + } + if (!(x.getCause() instanceof HttpTimeoutException)) { e.printStackTrace(out); throw new RuntimeException("Unexpected exception: " + e.getCause()); } else { - out.println("Caught expected timeout: " + e.getCause()); + out.println("Caught expected timeout: " + x.getCause()); } } } @@ -179,15 +237,25 @@ public class TimeoutBasic { out.println(" with duration of " + duration); HttpRequest request = newRequest(uri, duration, reqVersion, method); if (request == null) continue; + if (useAltSvc) { + // make sure request will be downgraded to HTTP/2 if we + // have not been able to create `dc`. + request = HttpRequest.newBuilder(request, (n,v) -> true) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + } count++; try { - client.send(request, BodyHandlers.discarding()); + HttpResponse resp = client.send(request, BodyHandlers.discarding()); + throw new RuntimeException("Unexpected response: " + resp.statusCode()); } catch (HttpTimeoutException e) { out.println("Caught expected timeout: " + e); } } assert count >= TIMEOUTS.size() -1; + } finally { + if (dc != null) dc.close(); } } } diff --git a/test/jdk/java/net/httpclient/TlsContextTest.java b/test/jdk/java/net/httpclient/TlsContextTest.java index 707a2e2d7c7..f24007f6d90 100644 --- a/test/jdk/java/net/httpclient/TlsContextTest.java +++ b/test/jdk/java/net/httpclient/TlsContextTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -42,6 +42,7 @@ import static java.lang.System.out; import static java.net.http.HttpClient.Version; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; import static java.net.http.HttpResponse.BodyHandlers.ofString; import static org.testng.Assert.assertEquals; import jdk.test.lib.security.SecurityUtils; @@ -75,7 +76,8 @@ public class TlsContextTest implements HttpServerAdapters { server = SimpleSSLContext.getContext("TLS"); final ExecutorService executor = Executors.newCachedThreadPool(); https2Server = HttpTestServer.of( - new Http2TestServer("localhost", true, 0, executor, 50, null, server, true)); + new Http2TestServer("localhost", true, 0, executor, 50, null, server, true) + .enableH3AltServiceOnSamePort()); https2Server.addHandler(new TlsVersionTestHandler("https", https2Server), "/server/"); https2Server.start(); @@ -89,6 +91,9 @@ public class TlsContextTest implements HttpServerAdapters { { SimpleSSLContext.getContext("TLSv1.2"), HTTP_2, "TLSv1.2" }, { SimpleSSLContext.getContext("TLSv1.1"), HTTP_1_1, "TLSv1.1" }, { SimpleSSLContext.getContext("TLSv1.1"), HTTP_2, "TLSv1.1" }, + { SimpleSSLContext.getContext("TLSv1.3"), HTTP_3, "TLSv1.3" }, + { SimpleSSLContext.getContext("TLSv1.2"), HTTP_3, "TLSv1.2" }, + { SimpleSSLContext.getContext("TLSv1.1"), HTTP_3, "TLSv1.1" }, }; } @@ -99,21 +104,32 @@ public class TlsContextTest implements HttpServerAdapters { public void testVersionProtocols(SSLContext context, Version version, String expectedProtocol) throws Exception { - HttpClient client = HttpClient.newBuilder() - .sslContext(context) - .version(version) + // for HTTP/3 we won't accept to set the version to HTTP/3 on the + // client if we don't have TLSv1.3; We will set the version + // on the request instead in that case. + var builder = version == HTTP_3 ? newClientBuilderForH3() + : HttpClient.newBuilder().version(version); + var reqBuilder = HttpRequest.newBuilder(new URI(https2URI)); + + HttpClient client = builder.sslContext(context) .build(); - HttpRequest request = HttpRequest.newBuilder(new URI(https2URI)) - .GET() - .build(); + if (version == HTTP_3) { + // warmup to obtain AltService + client.send(reqBuilder.version(HTTP_2).GET().build(), ofString()); + reqBuilder = reqBuilder.version(HTTP_3); + } + + HttpRequest request = reqBuilder.GET().build(); for (int i = 0; i < ITERATIONS; i++) { HttpResponse response = client.send(request, ofString()); - testAllProtocols(response, expectedProtocol); + testAllProtocols(response, expectedProtocol, version); } + client.close(); } private void testAllProtocols(HttpResponse response, - String expectedProtocol) throws Exception { + String expectedProtocol, + Version clientVersion) throws Exception { String protocol = response.sslSession().get().getProtocol(); int statusCode = response.statusCode(); Version version = response.version(); @@ -121,7 +137,12 @@ public class TlsContextTest implements HttpServerAdapters { out.println("The protocol negotiated is :" + protocol); assertEquals(statusCode, 200); assertEquals(protocol, expectedProtocol); - assertEquals(version, expectedProtocol.equals("TLSv1.1") ? HTTP_1_1 : HTTP_2); + if (clientVersion == HTTP_3) { + assertEquals(version, expectedProtocol.equals("TLSv1.1") ? HTTP_1_1 : + expectedProtocol.equals("TLSv1.2") ? HTTP_2 : HTTP_3); + } else { + assertEquals(version, expectedProtocol.equals("TLSv1.1") ? HTTP_1_1 : HTTP_2); + } } @AfterTest @@ -143,8 +164,10 @@ public class TlsContextTest implements HttpServerAdapters { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); - t.sendResponseHeaders(200, 10); - os.write(bytes); + t.sendResponseHeaders(200, bytes.length); + if (bytes.length > 0) { + os.write(bytes); + } } } } diff --git a/test/jdk/java/net/httpclient/UnauthorizedTest.java b/test/jdk/java/net/httpclient/UnauthorizedTest.java index e2124f2a7ed..0c8c2b1d2cd 100644 --- a/test/jdk/java/net/httpclient/UnauthorizedTest.java +++ b/test/jdk/java/net/httpclient/UnauthorizedTest.java @@ -30,7 +30,8 @@ * for the client. If no authenticator is configured the client * should simply let the caller deal with the unauthorized response. * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext ReferenceTracker * @run testng/othervm * -Djdk.httpclient.HttpClient.log=headers * UnauthorizedTest @@ -60,6 +61,9 @@ import jdk.httpclient.test.lib.common.HttpServerAdapters; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; @@ -71,10 +75,12 @@ public class UnauthorizedTest implements HttpServerAdapters { HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; HttpClient authClient; HttpClient noAuthClient; @@ -97,6 +103,12 @@ public class UnauthorizedTest implements HttpServerAdapters { @DataProvider(name = "all") public Object[][] positive() { return new Object[][] { + { http3URI + "/server", UNAUTHORIZED, true, ref(authClient)}, + { http3URI + "/server", UNAUTHORIZED, false, ref(authClient)}, + { http3URI + "/server", UNAUTHORIZED, true, ref(noAuthClient)}, + { http3URI + "/server", UNAUTHORIZED, false, ref(noAuthClient)}, + + { httpURI + "/server", UNAUTHORIZED, true, ref(authClient)}, { httpsURI + "/server", UNAUTHORIZED, true, ref(authClient)}, { http2URI + "/server", UNAUTHORIZED, true, ref(authClient)}, @@ -137,6 +149,15 @@ public class UnauthorizedTest implements HttpServerAdapters { static final Authenticator authenticator = new Authenticator() { }; + private HttpRequest.Builder newRequestBuilder(URI uri) { + var builder = HttpRequest.newBuilder(uri); + if (uri.getRawPath().contains("/http3/")) { + builder = builder.version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } + return builder; + } + @Test(dataProvider = "all") void test(String uriString, int code, boolean async, WeakReference clientRef) throws Throwable { HttpClient client = clientRef.get(); @@ -145,8 +166,7 @@ public class UnauthorizedTest implements HttpServerAdapters { client.authenticator().isPresent() ? "authClient" : "noAuthClient"); URI uri = URI.create(uriString); - HttpRequest.Builder requestBuilder = HttpRequest - .newBuilder(uri) + HttpRequest.Builder requestBuilder = newRequestBuilder(uri) .GET(); HttpRequest request = requestBuilder.build(); @@ -163,6 +183,7 @@ public class UnauthorizedTest implements HttpServerAdapters { try { response = client.sendAsync(request, BodyHandlers.ofString()).get(); } catch (ExecutionException ex) { + ex.printStackTrace(); throw ex.getCause(); } } @@ -204,13 +225,17 @@ public class UnauthorizedTest implements HttpServerAdapters { https2TestServer.addHandler(new UnauthorizedHandler(), "/https2/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2"; - authClient = HttpClient.newBuilder() + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new UnauthorizedHandler(), "/http3/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3"; + + authClient = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .authenticator(authenticator) .build(); - noAuthClient = HttpClient.newBuilder() + noAuthClient = newClientBuilderForH3() .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .build(); @@ -219,6 +244,7 @@ public class UnauthorizedTest implements HttpServerAdapters { httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); } @AfterTest @@ -236,6 +262,7 @@ public class UnauthorizedTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); if (error != null) throw error; } diff --git a/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java b/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java index 97be90f8587..ee28980c035 100644 --- a/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java +++ b/test/jdk/java/net/httpclient/UserAuthWithAuthenticator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,34 +21,12 @@ * questions. */ -/** - * @test - * @bug 8326949 - * @summary Authorization header is removed when a proxy Authenticator is set - * @library /test/lib /test/jdk/java/net/httpclient /test/jdk/java/net/httpclient/lib - * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters - * jdk.httpclient.test.lib.http2.Http2TestServer - * jdk.test.lib.net.IPSupport - * - * @modules java.net.http/jdk.internal.net.http.common - * java.net.http/jdk.internal.net.http.frame - * java.net.http/jdk.internal.net.http.hpack - * java.logging - * java.base/sun.net.www.http - * java.base/sun.net.www - * java.base/sun.net - * - * @run main/othervm UserAuthWithAuthenticator - */ - import java.io.*; import java.net.*; import java.net.http.HttpClient; -import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; import javax.net.ssl.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -56,132 +34,165 @@ import java.util.regex.*; import java.util.*; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; -import jdk.test.lib.net.IPSupport; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; import jdk.httpclient.test.lib.http2.Http2TestServer; import com.sun.net.httpserver.BasicAuthenticator; - -import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.Test; import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; -public class UserAuthWithAuthenticator { - private static final String AUTH_PREFIX = "Basic "; +/* + * @test + * @bug 8326949 + * @summary Authorization header is removed when a proxy Authenticator is set + * @library /test/lib /test/jdk/java/net/httpclient /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.net.IPSupport + * @run junit UserAuthWithAuthenticator + */ +class UserAuthWithAuthenticator { - static class AuthTestHandler implements HttpTestHandler { - volatile String authValue; - final String response = "Hello world"; + private static final class AuthTestHandler implements HttpTestHandler { + private volatile String authHeaderValue; @Override public void handle(HttpTestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { - byte[] bytes = is.readAllBytes(); - authValue = t.getRequestHeaders() + is.readAllBytes(); + authHeaderValue = t.getRequestHeaders() .firstValue("Authorization") - .orElse(AUTH_PREFIX) - .substring(AUTH_PREFIX.length()); + .orElse(""); + String response = "Hello world"; t.sendResponseHeaders(200, response.length()); os.write(response.getBytes(US_ASCII)); t.close(); } } - String authValue() {return authValue;} } - // if useHeader is true, we expect the Authenticator was not called - // and the user set header used. If false, Authenticator must - // be called and the user set header not used. + @Test + void h2Test() throws Exception { + h2Test(true, true); + h2Test(false, true); + h2Test(true, false); + } - // If rightPassword is true we expect the authentication to succeed and 200 OK - // If false, then an error should be returned. - - static void h2Test(final boolean useHeader, boolean rightPassword) throws Exception { - SSLContext ctx; - HttpTestServer h2s = null; - HttpClient client = null; - ExecutorService ex=null; - try { - ctx = new SimpleSSLContext().get(); - ex = Executors.newCachedThreadPool(); - InetAddress addr = InetAddress.getLoopbackAddress(); - - h2s = HttpTestServer.of(new Http2TestServer(addr, "::1", true, 0, ex, - 10, null, ctx, false)); - AuthTestHandler h = new AuthTestHandler(); - var context = h2s.addHandler(h, "/test1"); - context.setAuthenticator(new BasicAuthenticator("realm") { - public boolean checkCredentials(String username, String password) { - if (useHeader) { - return username.equals("user") && password.equals("pwd"); - } else { - return username.equals("serverUser") && password.equals("serverPwd"); - } - } - }); - h2s.start(); - - int port = h2s.getAddress().getPort(); - ServerAuth sa = new ServerAuth(); - var plainCreds = rightPassword? "user:pwd" : "user:wrongPwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); - - URI uri = URIBuilder.newBuilder() - .scheme("https") - .host(addr.getHostAddress()) - .port(port) - .path("/test1/foo.txt") - .build(); - - HttpClient.Builder builder = HttpClient.newBuilder() - .sslContext(ctx) - .executor(ex); - - builder.authenticator(sa); - client = builder.build(); - - HttpRequest req = HttpRequest.newBuilder(uri) - .version(HttpClient.Version.HTTP_2) - .header(useHeader ? "Authorization" : "X-Ignore", AUTH_PREFIX + encoded) - .GET() - .build(); - - HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); - if (!useHeader) { - assertTrue(resp.statusCode() == 200, "Expected 200 response"); - assertTrue(!h.authValue().equals(encoded), "Expected user set header to not be set"); - assertTrue(h.authValue().equals(sa.authValue()), "Expected auth value from Authenticator"); - assertTrue(sa.wasCalled(), "Expected authenticator to be called"); - System.out.println("h2Test: using authenticator OK"); - } else if (rightPassword) { - assertTrue(resp.statusCode() == 200, "Expected 200 response"); - assertTrue(h.authValue().equals(encoded), "Expected user set header to be set"); - assertTrue(!sa.wasCalled(), "Expected authenticator not to be called"); - System.out.println("h2Test: using user set header OK"); - } else { - assertTrue(resp.statusCode() == 401, "Expected 401 response"); - assertTrue(!sa.wasCalled(), "Expected authenticator not to be called"); - System.out.println("h2Test: using user set header with wrong password OK"); - } - } finally { - if (h2s != null) - h2s.stop(); - if (client != null) - client.close(); - if (ex != null) - ex.shutdown(); + private static void h2Test(final boolean useHeader, boolean rightPassword) throws Exception { + SSLContext sslContext = new SimpleSSLContext().get(); + try (ExecutorService executor = Executors.newCachedThreadPool(); + HttpTestServer server = HttpTestServer.of(new Http2TestServer( + InetAddress.getLoopbackAddress(), + "::1", + true, + 0, + executor, + 10, + null, + sslContext, + false)); + HttpClient client = HttpClient.newBuilder() + .sslContext(sslContext) + .executor(executor) + .authenticator(new ServerAuth()) + .build()) { + hXTest(useHeader, rightPassword, server, client, HttpClient.Version.HTTP_2); } } - static final String data = "0123456789"; + @Test + void h3Test() throws Exception { + h3Test(true, true); + h3Test(false, true); + h3Test(true, false); + } - static final String data1 = "ABCDEFGHIJKL"; + private static void h3Test(final boolean useHeader, boolean rightPassword) throws Exception { + SSLContext sslContext = new SimpleSSLContext().get(); + try (ExecutorService executor = Executors.newCachedThreadPool(); + HttpTestServer server = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext, executor); + HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .executor(executor) + .authenticator(new ServerAuth()) + .build()) { + hXTest(useHeader, rightPassword, server, client, HttpClient.Version.HTTP_3); + } + } - static final String[] proxyResponses = { + /** + * @param useHeader If {@code true}, we expect the authenticator was not called and the user set header used. + * If {@code false}, authenticator must be called and the user set header discarded. + * @param rightPassword If {@code true}, we expect the authentication to succeed with {@code 200 OK}. + * If {@code false}, then an error should be returned. + */ + private static void hXTest( + final boolean useHeader, + boolean rightPassword, + HttpTestServer server, + HttpClient client, + HttpClient.Version version) + throws Exception { + + AuthTestHandler handler = new AuthTestHandler(); + var context = server.addHandler(handler, "/test1"); + context.setAuthenticator(new BasicAuthenticator("realm") { + public boolean checkCredentials(String username, String password) { + if (useHeader) { + return username.equals("user") && password.equals("pwd"); + } else { + return username.equals("serverUser") && password.equals("serverPwd"); + } + } + }); + server.start(); + + URI uri = URIBuilder.newBuilder() + .scheme("https") + .host(server.getAddress().getAddress()) + .port(server.getAddress().getPort()) + .path("/test1/foo.txt") + .build(); + + var authHeaderValue = authHeaderValue("user", rightPassword ? "pwd" : "wrongPwd"); + HttpRequest req = HttpRequest.newBuilder(uri) + .version(version) + .header(useHeader ? "Authorization" : "X-Ignore", authHeaderValue) + .GET() + .build(); + + HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); + var sa = (ServerAuth) client.authenticator().orElseThrow(); + if (!useHeader) { + assertEquals(200, resp.statusCode(), "Expected 200 response"); + assertNotEquals(handler.authHeaderValue, authHeaderValue, "Expected user set header to not be set"); + assertEquals(handler.authHeaderValue, ServerAuth.AUTH_HEADER_VALUE, "Expected auth value from Authenticator"); + assertTrue(sa.called, "Expected authenticator to be called"); + } else if (rightPassword) { + assertEquals(200, resp.statusCode(), "Expected 200 response"); + assertEquals(authHeaderValue, handler.authHeaderValue, "Expected user set header to be set"); + assertFalse(sa.called, "Expected authenticator not to be called"); + } else { + assertEquals(401, resp.statusCode(), "Expected 401 response"); + assertFalse(sa.called, "Expected authenticator not to be called"); + } + + } + + private static final String data = "0123456789"; + + private static final String data1 = "ABCDEFGHIJKL"; + + private static final String[] proxyResponses = { "HTTP/1.1 407 Proxy Authentication Required\r\n"+ "Content-Length: 0\r\n" + "Proxy-Authenticate: Basic realm=\"Access to the proxy\"\r\n\r\n" @@ -192,7 +203,7 @@ public class UserAuthWithAuthenticator { "Content-Length: " + data.length() + "\r\n\r\n" + data }; - static final String[] proxyWithErrorResponses = { + private static final String[] proxyWithErrorResponses = { "HTTP/1.1 407 Proxy Authentication Required\r\n"+ "Content-Length: 0\r\n" + "Proxy-Authenticate: Basic realm=\"Access to the proxy\"\r\n\r\n" @@ -202,14 +213,14 @@ public class UserAuthWithAuthenticator { "Proxy-Authenticate: Basic realm=\"Access to the proxy\"\r\n\r\n" }; - static final String[] serverResponses = { + private static final String[] serverResponses = { "HTTP/1.1 200 OK\r\n"+ "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" + "Server: Apache/1.3.14 (Unix)\r\n" + "Content-Length: " + data1.length() + "\r\n\r\n" + data1 }; - static final String[] authenticatorResponses = { + private static final String[] authenticatorResponses = { "HTTP/1.1 401 Authentication Required\r\n"+ "Content-Length: 0\r\n" + "WWW-Authenticate: Basic realm=\"Access to the server\"\r\n\r\n" @@ -220,119 +231,97 @@ public class UserAuthWithAuthenticator { "Content-Length: " + data1.length() + "\r\n\r\n" + data1 }; - public static void main(String[] args) throws Exception { - testServerOnly(); - testServerWithProxy(); - testServerWithProxyError(); - testServerOnlyAuthenticator(); - h2Test(true, true); - h2Test(false, true); - h2Test(true, false); - } - - static void testServerWithProxy() throws IOException, InterruptedException { - Mocker proxyMock = new Mocker(proxyResponses); - proxyMock.start(); + @Test + void h1TestServerWithProxy() throws IOException, InterruptedException { ProxyAuth p = new ProxyAuth(); - try (var client = HttpClient.newBuilder() + try (var proxyMock = new Mocker(proxyResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .proxy(new ProxySel(proxyMock.getPort())) .authenticator(p) .build()) { - var plainCreds = "user:pwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); + var authHeaderValue = authHeaderValue("user", "pwd"); var request = HttpRequest.newBuilder().uri(URI.create("http://127.0.0.1/some_url")) .setHeader("User-Agent", "myUserAgent") - .setHeader("Authorization", AUTH_PREFIX + encoded) + .setHeader("Authorization", authHeaderValue) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); - assertTrue(p.wasCalled(), "Proxy authenticator was not called"); + assertTrue(p.called, "Proxy authenticator was not called"); assertEquals(data, response.body()); - var proxyStr = proxyMock.getRequest(1); + var proxyStr = proxyMock.requests.get(1); assertContains(proxyStr, "/some_url"); - assertPattern(".*^Proxy-Authorization:.*Basic " + encoded + ".*", proxyStr); + assertPattern(".*^Proxy-Authorization:.*\\Q" + authHeaderValue + "\\E.*", proxyStr); assertPattern(".*^User-Agent:.*myUserAgent.*", proxyStr); assertPattern(".*^Authorization:.*Basic.*", proxyStr); - System.out.println("testServerWithProxy: OK"); - } finally { - proxyMock.stopMocker(); } } - static void testServerWithProxyError() throws IOException, InterruptedException { - Mocker proxyMock = new Mocker(proxyWithErrorResponses); - proxyMock.start(); + @Test + void h1TestServerWithProxyError() throws IOException, InterruptedException { ProxyAuth p = new ProxyAuth(); - try (var client = HttpClient.newBuilder() + try (var proxyMock = new Mocker(proxyWithErrorResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .proxy(new ProxySel(proxyMock.getPort())) .authenticator(p) .build()) { - var badCreds = "user:wrong"; - var encoded1 = java.util.Base64.getEncoder().encodeToString(badCreds.getBytes(US_ASCII)); + var authHeaderValue = authHeaderValue("user", "wrong"); var request = HttpRequest.newBuilder().uri(URI.create("http://127.0.0.1/some_url")) .setHeader("User-Agent", "myUserAgent") - .setHeader("Proxy-Authorization", AUTH_PREFIX + encoded1) + .setHeader("Proxy-Authorization", authHeaderValue) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); - var proxyStr = proxyMock.getRequest(0); + var proxyStr = proxyMock.requests.getFirst(); assertEquals(407, response.statusCode()); - assertPattern(".*^Proxy-Authorization:.*Basic " + encoded1 + ".*", proxyStr); - assertTrue(!p.wasCalled(), "Proxy Auth should not have been called"); - System.out.println("testServerWithProxyError: OK"); - } finally { - proxyMock.stopMocker(); + assertPattern(".*^Proxy-Authorization:.*\\Q" + authHeaderValue + "\\E.*", proxyStr); + assertFalse(p.called, "Proxy Auth should not have been called"); } } - static void testServerOnly() throws IOException, InterruptedException { - Mocker serverMock = new Mocker(serverResponses); - serverMock.start(); - try (var client = HttpClient.newBuilder() + @Test + void h1TestServerOnly() throws IOException, InterruptedException { + try (var serverMock = new Mocker(serverResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .build()) { - var plainCreds = "user:pwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); + var authHeaderValue = authHeaderValue("user", "pwd"); var request = HttpRequest.newBuilder().uri(URI.create(serverMock.baseURL() + "/some_serv_url")) .setHeader("User-Agent", "myUserAgent") - .setHeader("Authorization", AUTH_PREFIX + encoded) + .setHeader("Authorization", authHeaderValue) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); assertEquals(data1, response.body()); - var serverStr = serverMock.getRequest(0); + var serverStr = serverMock.requests.getFirst(); assertContains(serverStr, "/some_serv_url"); assertPattern(".*^User-Agent:.*myUserAgent.*", serverStr); - assertPattern(".*^Authorization:.*Basic " + encoded + ".*", serverStr); - System.out.println("testServerOnly: OK"); - } finally { - serverMock.stopMocker(); + assertPattern(".*^Authorization:.*\\Q" + authHeaderValue + "\\E.*", serverStr); } } - // This is effectively a regression test for existing behavior - static void testServerOnlyAuthenticator() throws IOException, InterruptedException { - Mocker serverMock = new Mocker(authenticatorResponses); - serverMock.start(); - try (var client = HttpClient.newBuilder() + /** + * A regression test for existing behavior. + */ + @Test + void h1TestServerOnlyAuthenticator() throws IOException, InterruptedException { + try (var serverMock = new Mocker(authenticatorResponses); + var client = HttpClient.newBuilder() .version(java.net.http.HttpClient.Version.HTTP_1_1) .authenticator(new ServerAuth()) .build()) { // credentials set in the server authenticator - var plainCreds = "serverUser:serverPwd"; - var encoded = java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); var request = HttpRequest.newBuilder().uri(URI.create(serverMock.baseURL() + "/some_serv_url")) .setHeader("User-Agent", "myUserAgent") .build(); @@ -341,42 +330,43 @@ public class UserAuthWithAuthenticator { assertEquals(200, response.statusCode()); assertEquals(data1, response.body()); - var serverStr = serverMock.getRequest(1); + var serverStr = serverMock.requests.get(1); assertContains(serverStr, "/some_serv_url"); assertPattern(".*^User-Agent:.*myUserAgent.*", serverStr); - assertPattern(".*^Authorization:.*Basic " + encoded + ".*", serverStr); - System.out.println("testServerOnlyAuthenticator: OK"); - } finally { - serverMock.stopMocker(); + assertPattern(".*^Authorization:.*\\Q" + authHeaderValue("serverUser", "serverPwd") + "\\E.*", serverStr); } } - static void close(Closeable... clarray) { - for (Closeable c : clarray) { - try { - c.close(); - } catch (Exception e) {} - } - } + private static final class Mocker extends Thread implements AutoCloseable { + private final ServerSocket ss; + private final String[] responses; + private final List requests; + private volatile InputStream in; + private volatile OutputStream out; + private volatile Socket s = null; - static class Mocker extends Thread { - final ServerSocket ss; - final String[] responses; - volatile List requests; - volatile InputStream in; - volatile OutputStream out; - volatile Socket s = null; - - public Mocker(String[] responses) throws IOException { + private Mocker(String[] responses) throws IOException { this.ss = new ServerSocket(0, 0, InetAddress.getLoopbackAddress()); this.responses = responses; this.requests = new LinkedList<>(); + start(); } - public void stopMocker() { + @Override + public void close() { close(ss, s, in, out); } + private static void close(Closeable... clarray) { + for (Closeable c : clarray) { + try { + c.close(); + } catch (Exception e) { + // Do nothing + } + } + } + public int getPort() { return ss.getLocalPort(); } @@ -404,15 +394,12 @@ public class UserAuthWithAuthenticator { in = s.getInputStream(); out = s.getOutputStream(); } - req += (char)x; + // noinspection StringConcatenationInLoop + req += (char) x; } return req; } - public String getRequest(int i) { - return requests.get(i); - } - public void run() { try { int index=0; @@ -424,15 +411,17 @@ public class UserAuthWithAuthenticator { out.write(responses[index++].getBytes(US_ASCII)); } } catch (Exception e) { + // noinspection CallToPrintStackTrace e.printStackTrace(); } } } - static class ProxySel extends ProxySelector { - final int port; + private static final class ProxySel extends ProxySelector { - ProxySel(int port) { + private final int port; + + private ProxySel(int port) { this.port = port; } @Override @@ -446,7 +435,8 @@ public class UserAuthWithAuthenticator { } - static class ProxyAuth extends Authenticator { + private static final class ProxyAuth extends Authenticator { + private volatile boolean called = false; @Override @@ -455,16 +445,17 @@ public class UserAuthWithAuthenticator { return new PasswordAuthentication("proxyUser", "proxyPwd".toCharArray()); } - boolean wasCalled() { - return called; - } } - static class ServerAuth extends Authenticator { + private static final class ServerAuth extends Authenticator { + private volatile boolean called = false; - private static String USER = "serverUser"; - private static String PASS = "serverPwd"; + private static final String USER = "serverUser"; + + private static final String PASS = "serverPwd"; + + private static final String AUTH_HEADER_VALUE = authHeaderValue(USER, PASS); @Override protected PasswordAuthentication getPasswordAuthentication() { @@ -476,49 +467,21 @@ public class UserAuthWithAuthenticator { return new PasswordAuthentication(USER, PASS.toCharArray()); } - String authValue() { - var plainCreds = USER + ":" + PASS; - return java.util.Base64.getEncoder().encodeToString(plainCreds.getBytes(US_ASCII)); - } - - boolean wasCalled() { - return called; - } } - static void assertTrue(boolean assertion, String failMsg) { - if (!assertion) { - throw new RuntimeException(failMsg); - } + private static String authHeaderValue(String username, String password) { + String credentials = username + ':' + password; + return "Basic " + java.util.Base64.getEncoder().encodeToString(credentials.getBytes(US_ASCII)); } - static void assertEquals(int a, int b) { - if (a != b) { - String msg = String.format("Error: expected %d Got %d", a, b); - throw new RuntimeException(msg); - } + private static void assertContains(String container, String containee) { + assertTrue(container.contains(containee), String.format("Error: expected %s Got %s", container, containee)); } - static void assertEquals(String s1, String s2) { - if (!s1.equals(s2)) { - String msg = String.format("Error: expected %s Got %s", s1, s2); - throw new RuntimeException(msg); - } - } - - static void assertContains(String container, String containee) { - if (!container.contains(containee)) { - String msg = String.format("Error: expected %s Got %s", container, containee); - throw new RuntimeException(msg); - } - } - - static void assertPattern(String pattern, String candidate) { + private static void assertPattern(String pattern, String candidate) { Pattern pat = Pattern.compile(pattern, Pattern.DOTALL | Pattern.MULTILINE); Matcher matcher = pat.matcher(candidate); - if (!matcher.matches()) { - String msg = String.format("Error: expected %s Got %s", pattern, candidate); - throw new RuntimeException(msg); - } + assertTrue(matcher.matches(), String.format("Error: expected %s Got %s", pattern, candidate)); } + } diff --git a/test/jdk/java/net/httpclient/UserCookieTest.java b/test/jdk/java/net/httpclient/UserCookieTest.java index b8de5f97955..50a13e48322 100644 --- a/test/jdk/java/net/httpclient/UserCookieTest.java +++ b/test/jdk/java/net/httpclient/UserCookieTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -66,11 +66,7 @@ import java.util.stream.Stream; import javax.net.ServerSocketFactory; import javax.net.ssl.SSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; @@ -80,22 +76,27 @@ import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; public class UserCookieTest implements HttpServerAdapters { SSLContext sslContext; - HttpTestServer httpTestServer; // HTTP/1.1 [ 6 servers ] + HttpTestServer httpTestServer; // HTTP/1.1 [ 7 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + HttpTestServer http3TestServer; // HTTP/3 ( h3 ) DummyServer httpDummyServer; DummyServer httpsDummyServer; String httpURI; String httpsURI; String http2URI; String https2URI; + String http3URI; String httpDummy; String httpsDummy; @@ -113,6 +114,7 @@ public class UserCookieTest implements HttpServerAdapters { @DataProvider(name = "positive") public Object[][] positive() { return new Object[][] { + { http3URI, HTTP_3 }, { httpURI, HTTP_1_1 }, { httpsURI, HTTP_1_1 }, { httpDummy, HTTP_1_1 }, @@ -134,7 +136,10 @@ public class UserCookieTest implements HttpServerAdapters { ConcurrentHashMap> cookieHeaders = new ConcurrentHashMap<>(); CookieHandler cookieManager = new TestCookieHandler(cookieHeaders); - HttpClient client = HttpClient.newBuilder() + var builder = version == HTTP_3 + ? newClientBuilderForH3() + : HttpClient.newBuilder(); + HttpClient client = builder .followRedirects(Redirect.ALWAYS) .cookieHandler(cookieManager) .sslContext(sslContext) @@ -159,6 +164,9 @@ public class UserCookieTest implements HttpServerAdapters { .header("Cookie", userCookie); if (version != null) { requestBuilder.version(version); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } } HttpRequest request = requestBuilder.build(); out.println("Initial request: " + request.uri()); @@ -181,9 +189,13 @@ public class UserCookieTest implements HttpServerAdapters { .header("Cookie", userCookie); if (version != null) { requestBuilder.version(version); + if (version == HTTP_3) { + requestBuilder.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + } } request = requestBuilder.build(); } + client.close(); } // -- Infrastructure @@ -208,6 +220,10 @@ public class UserCookieTest implements HttpServerAdapters { https2TestServer.addHandler(new CookieValidationHandler(), "/https2/cookie/"); https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/cookie/retry"; + http3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new CookieValidationHandler(), "/http3/cookie/"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/cookie/retry"; + InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); // DummyServer httpDummyServer = DummyServer.create(sa); @@ -219,6 +235,7 @@ public class UserCookieTest implements HttpServerAdapters { httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); + http3TestServer.start(); httpDummyServer.start(); httpsDummyServer.start(); } @@ -229,6 +246,7 @@ public class UserCookieTest implements HttpServerAdapters { httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); + http3TestServer.stop(); httpsDummyServer.stopServer(); httpsDummyServer.stopServer(); } diff --git a/test/jdk/java/net/httpclient/VersionTest.java b/test/jdk/java/net/httpclient/VersionTest.java index d09ce0354d2..ff864202a9a 100644 --- a/test/jdk/java/net/httpclient/VersionTest.java +++ b/test/jdk/java/net/httpclient/VersionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,6 +49,7 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; public class VersionTest { static HttpServer s1 ; @@ -80,6 +81,8 @@ public class VersionTest { test(HTTP_1_1, false); test(HTTP_2, false); test(HTTP_2, true); + test(HTTP_3, false); + test(HTTP_3, true); } finally { s1.stop(0); executor.shutdownNow(); diff --git a/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/Http3ConnectionAccess.java b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/Http3ConnectionAccess.java new file mode 100644 index 00000000000..3e58990fb90 --- /dev/null +++ b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/Http3ConnectionAccess.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.http3.ConnectionSettings; + +public final class Http3ConnectionAccess { + + private Http3ConnectionAccess() { + throw new AssertionError(); + } + + static HttpClientImpl impl(HttpClient client) { + if (client instanceof HttpClientImpl impl) return impl; + if (client instanceof HttpClientFacade facade) return facade.impl; + return null; + } + + static HttpRequestImpl impl(HttpRequest request) { + if (request instanceof HttpRequestImpl impl) return impl; + return null; + } + + public static CompletableFuture peerSettings(HttpClient client, HttpResponse resp) { + try { + Http3Connection conn = impl(client) + .client3() + .get() + .findPooledConnectionFor(impl(resp.request()), null); + if (conn == null) { + return MinimalFuture.failedFuture(new NoSuchElementException("no connection found")); + } + return conn.peerSettingsCF(); + } catch (Exception ex) { + return MinimalFuture.failedFuture(ex); + } + } +} diff --git a/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/common/ImmutableSSLSessionAccess.java b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/common/ImmutableSSLSessionAccess.java new file mode 100644 index 00000000000..ab915428dc9 --- /dev/null +++ b/test/jdk/java/net/httpclient/access/java.net.http/jdk/internal/net/http/common/ImmutableSSLSessionAccess.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.common; + +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SSLSession; + +public final class ImmutableSSLSessionAccess { + + private ImmutableSSLSessionAccess() { + throw new AssertionError(); + } + + public static ImmutableSSLSession immutableSSLSession(SSLSession session) { + return new ImmutableSSLSession(session); + } + + public static ImmutableExtendedSSLSession immutableExtendedSSLSession(ExtendedSSLSession session) { + return new ImmutableExtendedSSLSession(session); + } + +} diff --git a/test/jdk/java/net/httpclient/altsvc/AltServiceReasonableAssurance.java b/test/jdk/java/net/httpclient/altsvc/AltServiceReasonableAssurance.java new file mode 100644 index 00000000000..0da1b238f60 --- /dev/null +++ b/test/jdk/java/net/httpclient/altsvc/AltServiceReasonableAssurance.java @@ -0,0 +1,688 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import jdk.httpclient.test.lib.common.DynamicKeyStoreUtil; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.ServerNameMatcher; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.Test; +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateCert; +import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateKeyStore; +import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateRSAKeyPair; +import static jdk.httpclient.test.lib.http3.Http3TestServer.quicServerBuilder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary verifies the HttpClient's usage of alternate services + * @comment The goal of this test class is to run various tests to verify that the HttpClient + * (and the underlying layers) use an alternate server for HTTP request(s) IF AND ONLY IF such an + * advertised alternate server satisfies "reasonable assurance" expectations as noted in the + * alternate service RFC-7838. Reasonable assurance can be summarized as: + * - The origin server which advertised the alternate service, MUST be running on TLS + * - The certificate presented by origin server during TLS handshake must be valid (and trusted) + * for the origin server + * - The certificate presented by alternate server (when subsequently a connection attempt is + * made to it) MUST be valid (and trusted) for the ORIGIN server + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.common.DynamicKeyStoreUtil + * jdk.test.lib.net.URIBuilder + * @modules java.base/sun.net.www.http + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * @modules java.base/sun.security.x509 + * java.base/jdk.internal.util + * @run junit/othervm -Djdk.net.hosts.file=${test.src}/altsvc-dns-hosts.txt + * -Djdk.internal.httpclient.debug=true -Djavax.net.debug=all + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * AltServiceReasonableAssurance + */ +public class AltServiceReasonableAssurance implements HttpServerAdapters { + + private static final String ORIGIN_SERVER_HOSTNAME = "origin.server"; + private static final String ALT_SERVER_HOSTNAME = "altservice.server"; + + private static final String ALT_SERVER_RESPONSE_MESSAGE = "Hello from an alt server"; + private static final String ORIGIN_SERVER_RESPONSE_MESSAGE = "Hello from an origin server"; + + private record TestInput(HttpTestServer originServer, HttpTestServer altServer, + URI requestURI, String expectedAltSvcHeader) { + } + + /** + * Creates and starts a origin server and an alternate server. The passed (same) SSLContext + * is used by both the origin server and the alternate server. + */ + private static TestInput startOriginAndAltServer(final SSLContext sslContext) + throws Exception { + Objects.requireNonNull(sslContext); + return startOriginAndAltServer(sslContext, sslContext); + } + + /** + * Creates and starts a origin server and an alternate server. The origin server will use + * the {@code originSrvSSLCtx} and the alternate server will use the {@code altSrvSSLCtx} + */ + private static TestInput startOriginAndAltServer(final SSLContext originSrvSSLCtx, + final SSLContext altSrvSSLCtx) + throws Exception { + Objects.requireNonNull(originSrvSSLCtx); + Objects.requireNonNull(altSrvSSLCtx); + final String requestPath = "/hello"; + final QuicServer quicServer = quicServerBuilder() + .sslContext(altSrvSSLCtx) + // the client sends a SNI for origin server. this alt server should be capable + // of matching/accepting that SNI name of the origin + .sniMatcher(new ServerNameMatcher(ORIGIN_SERVER_HOSTNAME)) + .build(); + // Alt server only supports H3 + final HttpTestServer altServer = HttpTestServer.of(new Http3TestServer(quicServer)); + altServer.addHandler(new Handler(ALT_SERVER_RESPONSE_MESSAGE), requestPath); + altServer.start(); + System.out.println("Alt server started at " + altServer.getAddress()); + + // H2 server which has a (application level) handler which advertises H3 alt service + final HttpTestServer originServer = HttpTestServer.of( + new Http2TestServer(ORIGIN_SERVER_HOSTNAME, true, originSrvSSLCtx)); + final int altServerPort = altServer.getAddress().getPort(); + final String altSvcHeaderVal = "h3=\"" + ALT_SERVER_HOSTNAME + ":" + altServerPort + "\""; + originServer.addHandler(new Handler(ORIGIN_SERVER_RESPONSE_MESSAGE, altSvcHeaderVal), + requestPath); + originServer.start(); + System.out.println("Origin server started at " + originServer.getAddress()); + // request URI should be directed to the origin server + final URI requestURI = URIBuilder.newBuilder() + .scheme("https") + .host(ORIGIN_SERVER_HOSTNAME) + .port(originServer.getAddress().getPort()) + .path(requestPath) + .build(); + return new TestInput(originServer, altServer, requestURI, altSvcHeaderVal); + } + + private TestInput startHttpOriginHttpsAltServer(final SSLContext altServerSSLCtx) + throws Exception { + Objects.requireNonNull(altServerSSLCtx); + final String requestPath = "/foo"; + // Alt server only supports H3 + final HttpTestServer altServer = HttpTestServer.create(HTTP_3_URI_ONLY, altServerSSLCtx); + altServer.addHandler(new Handler(ALT_SERVER_RESPONSE_MESSAGE), requestPath); + altServer.start(); + System.out.println("Alt server (HTTPS) started at " + altServer.getAddress()); + + // supports only HTTP server and uses a (application level) handler which advertises a H3 + // alternate service + final HttpTestServer originServer = HttpTestServer.create(HTTP_2); + final int altServerPort = altServer.getAddress().getPort(); + final String altSvcHeaderVal = "h3=\"" + ALT_SERVER_HOSTNAME + ":" + altServerPort + "\""; + originServer.addHandler(new Handler(ORIGIN_SERVER_RESPONSE_MESSAGE, altSvcHeaderVal), + requestPath); + originServer.start(); + System.out.println("Origin server (HTTP) started at " + originServer.getAddress()); + // request URI should be against (HTTP) origin server + final URI requestURI = URIBuilder.newBuilder() + .scheme("http") + .host(ORIGIN_SERVER_HOSTNAME) + .port(originServer.getAddress().getPort()) + .path(requestPath) + .build(); + return new TestInput(originServer, altServer, requestURI, altSvcHeaderVal); + } + + /** + * Stop the server (and ignore any exception) + */ + private static void safeStop(final HttpTestServer server) { + if (server == null) { + return; + } + final InetSocketAddress serverAddr = server.getAddress(); + try { + System.out.println("Stopping server " + serverAddr); + server.stop(); + } catch (Exception e) { + System.err.println("Ignoring exception: " + e.getMessage() + " that occurred " + + "during stop of server: " + serverAddr); + } + } + + /** + * Returns back a 200 HTTP response with a response body containing a response message + * that was used to construct the Handler instance. Additionally, if the Handler was constructed + * with a non-null {@code altSvcHeaderVal} then that value is sent back as a header value. in + * the response, for the {@code alt-svc} header + */ + private static final class Handler implements HttpTestHandler { + private final String responseMessage; + private final byte[] responseBytes; + private final String altSvcHeaderVal; + + private Handler(final String responseMessage) { + this(responseMessage, null); + } + + private Handler(final String responseMessage, final String altSvcHeaderVal) { + Objects.requireNonNull(responseMessage); + this.responseMessage = responseMessage; + this.responseBytes = responseMessage.getBytes(StandardCharsets.UTF_8); + this.altSvcHeaderVal = altSvcHeaderVal; + } + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + System.out.println("Handling request " + exchange.getRequestURI()); + if (this.altSvcHeaderVal != null) { + System.out.println("Responding with alt-svc header: " + this.altSvcHeaderVal); + exchange.getResponseHeaders().addHeader("alt-svc", this.altSvcHeaderVal); + } + System.out.println("Responding with body: " + this.responseMessage); + exchange.sendResponseHeaders(200, this.responseBytes.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(this.responseBytes); + } + } + } + + /** + * - Keystore K1 is constructed with a certificate whose subject is origin server hostname and + * subject alternative name is alternate server hostname + * - K1 is used to construct a SSLContext and thus the SSLContext uses the keys and trusted + * certificate from this keystore + * - The constructed SSLContext instance is used by the HttpClient, the origin server and the + * alternate server + * - During TLS handshake with origin server, the origin server is expected to present the + * certificate from this K1 keystore. + * - During TLS handshake with alternate server, the alternate server is expected to present + * this same certificate from K1 keystore. + * - Since the certificate is valid (and trusted by the client) for both origin server + * and alternate server (because of the valid subject name and subject alternate name), + * the TLS handshake between the HttpClient and the origin and alternate server is expected + * to pass + *

        + * Once the servers are started, this test method does the following: + *

        + * - Client constructs a HTTP_3 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the *same* request again + * - The request is expected to be handled by the alternate server + */ + @Test + public void testOriginAltSameCert() throws Exception { + // create a keystore which contains a PrivateKey entry and a certificate associated with + // that key. the certificate's subject will be origin server's hostname and will + // additionally have the alt server hostname as a subject alternate name. Thus, the + // certificate is valid for both origin server and alternate server + final KeyStore keyStore = generateKeyStore(ORIGIN_SERVER_HOSTNAME, ALT_SERVER_HOSTNAME); + System.out.println("Generated a keystore with certificate: " + + keyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create a SSLContext that will be used by the servers and the HttpClient and will be + // backed by the keystore we just created. Thus, the HttpClient will trust the certificate + // belonging to that keystore + final SSLContext sslContext = DynamicKeyStoreUtil.createSSLContext(keyStore); + // start the servers + final TestInput testInput = startOriginAndAltServer(sslContext); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Issuing request " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue the same request again and this time expect it to be handled + // by the alt-service + System.out.println("Again issuing request " + requestURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, secondResponse.statusCode(), "Unexpected response code"); + // expect the alt service to respond + assertEquals(ALT_SERVER_RESPONSE_MESSAGE, secondResponse.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_3, secondResponse.version(), "Unexpected HTTP version in response"); + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + /** + * - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is + * origin server hostname + * - Keystore K2 is constructed with the same PrivateKey PK1 and certificate whose subject is + * alternate server hostname AND has a subject alternate name of origin server hostname + * - K1 is used to construct a SSLContext S1 and that S1 is used by origin server + * - K2 is used to construct a SSLContext S2 and that S2 is used by alternate server + * - SSLContext S3 is constructed with both the certificate of origin server and + * the certificate of alternate server as trusted certificates. HttpClient uses S3 + * - During TLS handshake with origin server, the origin server is expected to present the + * certificate from this K1 keystore, with subject as origin server hostname + * - During TLS handshake with alternate server, the alternate server is expected to present + * the certificate from K2 keystore, with subject as alternate server hostname AND a subject + * alternate name of origin server + * - HttpClient (through S3 SSLContext) trusts both these certs. The cert presented + * by alt server, is valid (even) for origin server (since its subject alternate name is + * origin server hostname). Thus, the client must consider the alternate service as valid and + * use it. + *

        + * Once the servers are started, this test method does the following: + *

        + * - Client constructs a HTTP_3 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the *same* request again + * - The request is expected to be handled by the alternate server + */ + @Test + public void testOriginAltDifferentCert() throws Exception { + final SecureRandom secureRandom = new SecureRandom(); + final KeyPair keyPair = generateRSAKeyPair(secureRandom); + + // generate a certificate for origin server, with origin server hostname as the subject + final X509Certificate originServerCert = generateCert(keyPair, secureRandom, + ORIGIN_SERVER_HOSTNAME); + // create a keystore with the private key and the cert. this keystore will then be + // used by the SSLContext of origin server + final KeyStore originServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{originServerCert}); + System.out.println("Generated a keystore, for origin server, with certificate: " + + originServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create the SSLContext for the origin server + final SSLContext originServerSSLCtx = DynamicKeyStoreUtil.createSSLContext( + originServerKeyStore); + + // create a cert for the alternate server, with alternate server hostname as the subject + // AND origin server hostname as a subject alternate name + final X509Certificate altServerCert = generateCert(keyPair, secureRandom, + ALT_SERVER_HOSTNAME, ORIGIN_SERVER_HOSTNAME); + // create keystore with the private key and the alt server's cert. this keystore will then + // be used by the SSLContext of alternate server + final KeyStore altServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{altServerCert}); + System.out.println("Generated a keystore, for alt server, with certificate: " + + altServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create SSLContext of alternate server + final SSLContext altServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(altServerKeyStore); + + // now create a SSLContext for the HttpClient. This SSLContext will contain no key manager + // and will have a trust manager which trusts origin server certificate and the alternate + // server certificate + final SSLContext clientSSLCtx = sslCtxWithTrustedCerts(List.of(originServerCert, + altServerCert)); + // start the servers + final TestInput testInput = startOriginAndAltServer(originServerSSLCtx, altServerSSLCtx); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(clientSSLCtx) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Issuing request " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue the same request again and this time expect it to be handled + // by the alt-service + System.out.println("Again issuing request " + requestURI); + final HttpResponse secondResponse = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, secondResponse.statusCode(), "Unexpected response code"); + // expect the alt service to respond + assertEquals(ALT_SERVER_RESPONSE_MESSAGE, secondResponse.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_3, secondResponse.version(), "Unexpected HTTP version in response"); + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + + /** + * - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is + * origin server hostname + * - Keystore K2 is constructed with the same PrivateKey PK1 and certificate whose subject is + * alternate server hostname + * - K1 is used to construct a SSLContext S1 and that S1 is used by origin server + * - K2 is used to construct a SSLContext S2 and that S2 is used by alternate server + * - SSLContext S3 is constructed with both the certificate of origin server and + * the certificate of alternate server as trusted certificates. HttpClient uses S3 + * - During TLS handshake with origin server, the origin server is expected to present the + * certificate from this K1 keystore, with subject as origin server hostname + * - During TLS handshake with alternate server, the alternate server is expected to present + * the certificate from K2 keystore, with subject as alternate server hostname + * - HttpClient (through S3 SSLContext) trusts both these certs, but the cert presented + * by alt server, although valid for the alt server, CANNOT/MUST NOT be valid for origin + * server (since it's subject nor subject alternate name is origin server hostname). + * Reasonable assurance expects that the alt server present a certificate that is valid for + * origin server host and since it doesn't, the alt server must not be used by the HttpClient. + *

        + * Once the servers are started, this test method does the following: + *

        + * - Client constructs a HTTP_3 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the *same* request again + * - The request is expected to be handled by the origin server again and the advertised + * alternate service MUST NOT be used (due to reasons noted above) + */ + @Test + public void testAltServerWrongCert() throws Exception { + final SecureRandom secureRandom = new SecureRandom(); + final KeyPair keyPair = generateRSAKeyPair(secureRandom); + + // generate a certificate for origin server, with origin server hostname as the subject + final X509Certificate originServerCert = generateCert(keyPair, secureRandom, + ORIGIN_SERVER_HOSTNAME); + // create a keystore with the private key and the cert. this keystore will then be + // used by the SSLContext of origin server + final KeyStore originServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{originServerCert}); + System.out.println("Generated a keystore, for origin server, with certificate: " + + originServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create the SSLContext for the origin server + final SSLContext originServerSSLCtx = DynamicKeyStoreUtil.createSSLContext( + originServerKeyStore); + + // create a cert for the alternate server, with alternate server hostname as the subject + final X509Certificate altServerCert = generateCert(keyPair, secureRandom, + ALT_SERVER_HOSTNAME); + // create keystore with the private key and the alt server's cert. this keystore will then + // be used by the SSLContext of alternate server + final KeyStore altServerKeyStore = generateKeyStore(keyPair.getPrivate(), + new Certificate[]{altServerCert}); + System.out.println("Generated a keystore, for alt server, with certificate: " + + altServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create SSLContext of alternate server + final SSLContext altServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(altServerKeyStore); + + // now create a SSLContext for the HttpClient. This SSLContext will contain no key manager + // and will have a trust manager which trusts origin server certificate and the alternate + // server certificate + final SSLContext clientSSLCtx = sslCtxWithTrustedCerts(List.of(originServerCert, + altServerCert)); + // start the servers + final TestInput testInput = startOriginAndAltServer(originServerSSLCtx, altServerSSLCtx); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(clientSSLCtx) + .version(HTTP_3) + .build(); + // send a HTTP3 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Issuing request " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue the same request again (a few times). Expect each of these requests too, + // to be handled by the origin server (since the advertised alt server isn't expected + // to satisfy the "reasonable assurances" expectations + for (int i = 1; i <= 3; i++) { + System.out.println("Again(" + i + ") issuing request " + requestURI); + final HttpResponse rsp = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, rsp.statusCode(), "Unexpected response code"); + // expect the alt service to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, rsp.body(), + "Unexpected response body"); + assertEquals(HTTP_2, rsp.version(), "Unexpected HTTP version in response"); + } + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + + /** + * - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is + * alternate server hostname + * - K1 is used to construct a SSLContext S1 and that S1 is used by alternate server + * - The same SSLContext S1 is used by the HttpClient (and thus will trust the alternate + * server's certificate) + * - Origin server runs only on HTTP + * - Any alt-svc advertised by origin server MUST NOT be used by the client, because origin + * server runs on HTTP and as a result the "reasonable assurance" for the origin server + * cannot be satisfied. + *

        + * Once the servers are started, this test method does the following: + *

        + * - Client constructs a HTTP2 request addressed to origin server + * - Origin server responds with a 200 response and also with alt-svc header pointing to + * an alternate server + * - Client verifies the response as well as presence of the alt-svc header value + * - Client issues the request again, to the origin server, this time with HTTP3 as the request + * version + * - The request is expected to be handled by the origin server again and the advertised + * alternate service MUST NOT be used (due to reasons noted above) + */ + @Test + public void testAltServiceAdvertisedByHTTPOrigin() throws Exception { + // create a keystore which contains a PrivateKey entry and a certificate associated with + // that key. the certificate's subject will be alternate server's hostname. Thus, the + // certificate is valid for alternate server + final KeyStore keyStore = generateKeyStore(ALT_SERVER_HOSTNAME); + System.out.println("Generated a keystore with certificate: " + + keyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS)); + // create a SSLContext that will be used by the alternate server and the HttpClient and + // will be backed by the keystore we just created. Thus, the HttpClient will trust the + // certificate belonging to that keystore + final SSLContext sslContext = DynamicKeyStoreUtil.createSSLContext(keyStore); + + // start the servers + final TestInput testInput = startHttpOriginHttpsAltServer(sslContext); + try { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + // send a HTTP2 request to a server which is expected to respond back + // with a 200 response and an alt-svc header pointing to another/different H3 server + final URI requestURI = testInput.requestURI; + final HttpRequest request = HttpRequest.newBuilder() + .version(HTTP_2).GET() + .uri(requestURI).build(); + System.out.println("Issuing " + request.version() + " request to " + requestURI); + final HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, response.statusCode(), "Unexpected response code"); + assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response"); + // the origin server is expected to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" + + " body"); + + // verify the origin server sent back a alt-svc header + final Optional altSvcHeader = response.headers().firstValue("alt-svc"); + assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response"); + final String actualAltSvcHeader = altSvcHeader.get(); + System.out.println("Received alt-svc header value: " + actualAltSvcHeader); + assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader), + "Unexpected alt-svc header value: " + actualAltSvcHeader + + ", was expected to contain: " + testInput.expectedAltSvcHeader); + + // now issue few more requests to the same address, but as a HTTP3 version. Expect each + // of these requests too, to be handled by the origin server (since the previously + // advertised alt server isn't expected to satisfy the "reasonable assurances" + // expectations) + for (int i = 1; i <= 3; i++) { + final HttpRequest h3Request = HttpRequest.newBuilder() + .version(HTTP_3).GET().uri(requestURI) + .setOption(H3_DISCOVERY, ALT_SVC) + .build(); + System.out.println("Again(" + i + ") issuing " + h3Request.version() + + " request to " + requestURI); + final HttpResponse rsp = client.send(h3Request, + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + assertEquals(200, rsp.statusCode(), "Unexpected response code"); + // even though the request is HTTP_3 version, the client will fall back to HTTP_2 + // since the origin server does run on TLS and thus cannot use HTTP_3 (which + // requires TLS) + assertEquals(HTTP_2, rsp.version(), "Unexpected HTTP version in response"); + // expect the alt service to respond + assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, rsp.body(), + "Unexpected response body"); + } + } finally { + safeStop(testInput.originServer); + safeStop(testInput.altServer); + } + } + + private static SSLContext sslCtxWithTrustedCerts(final List trustedCerts) + throws Exception { + Objects.requireNonNull(trustedCerts); + // start with a blank keystore + final KeyStore keyStore = DynamicKeyStoreUtil.generateBlankKeyStore(); + final String aliasPrefix = "trusted-certs-alias-"; + int i = 1; + for (final Certificate cert : trustedCerts) { + // add the cert as a trusted certificate to the keystore + keyStore.setCertificateEntry(aliasPrefix + i, cert); + i++; + } + System.out.println("Generated a keystore with (only) trusted certs: "); + for (--i; i > 0; i--) { + System.out.println(keyStore.getCertificate(aliasPrefix + i)); + } + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + // use the generated keystore for this trust manager + tmf.init(keyStore); + + final String protocol = "TLS"; + final SSLContext ctx = SSLContext.getInstance(protocol); + // initialize the SSLContext with the trust manager which trusts the passed certificates + ctx.init(null, tmf.getTrustManagers(), null); + return ctx; + } +} diff --git a/test/jdk/java/net/httpclient/altsvc/altsvc-dns-hosts.txt b/test/jdk/java/net/httpclient/altsvc/altsvc-dns-hosts.txt new file mode 100644 index 00000000000..67445f3b278 --- /dev/null +++ b/test/jdk/java/net/httpclient/altsvc/altsvc-dns-hosts.txt @@ -0,0 +1,23 @@ +## Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. +## DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +## +## This code is free software; you can redistribute it and/or modify it +## under the terms of the GNU General Public License version 2 only, as +## published by the Free Software Foundation. +## +## This code is distributed in the hope that it will be useful, but WITHOUT +## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +## version 2 for more details (a copy is included in the LICENSE file that +## accompanied this code). +## +## You should have received a copy of the GNU General Public License version +## 2 along with this work; if not, write to the Free Software Foundation, +## Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +## +## Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +## or visit www.oracle.com if you need additional information or have any +## questions. +## +127.0.0.1 origin.server +127.0.0.1 altservice.server diff --git a/test/jdk/java/net/httpclient/debug/java.net.http/jdk/internal/net/http/common/TestLoggerUtil.java b/test/jdk/java/net/httpclient/debug/java.net.http/jdk/internal/net/http/common/TestLoggerUtil.java new file mode 100644 index 00000000000..a5617d85127 --- /dev/null +++ b/test/jdk/java/net/httpclient/debug/java.net.http/jdk/internal/net/http/common/TestLoggerUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.net.http.common; + +import java.util.function.Supplier; + +import jdk.internal.net.http.common.DebugLogger.LoggerConfig; + +public class TestLoggerUtil { + + public static Logger getStdoutLogger(Supplier dbgTag) { + var logLevel = Utils.DEBUG_CONFIG.logLevel(); + LoggerConfig config = Utils.DEBUG + ? LoggerConfig.STDOUT.withLogLevel(logLevel) + : LoggerConfig.OFF; + return DebugLogger.createHttpLogger(dbgTag, config); + } + + public static Logger getErrOutLogger(Supplier dbgTag) { + var logLevel = Utils.DEBUG_CONFIG.logLevel(); + LoggerConfig config = Utils.DEBUG + ? LoggerConfig.ERROUT.withLogLevel(logLevel) + : LoggerConfig.OFF; + return DebugLogger.createHttpLogger(dbgTag, config); + } +} diff --git a/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java b/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java index 73cc12ce478..c7edfd5fa8f 100644 --- a/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java +++ b/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java @@ -168,7 +168,7 @@ public class BadPushPromiseTest { } } - private void pushPromise(HttpServerAdapters.HttpTestExchange exchange) { + private void pushPromise(HttpServerAdapters.HttpTestExchange exchange) throws IOException { URI requestURI = exchange.getRequestURI(); String query = exchange.getRequestURI().getQuery(); int badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1)); diff --git a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java index 04ce8f4f4a4..f27fdb580dd 100644 --- a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java +++ b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java @@ -46,6 +46,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; + import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.frame.ContinuationFrame; import jdk.internal.net.http.frame.HeaderFrame; @@ -154,6 +155,22 @@ public class ContinuationFrameTest { static final int ITERATION_COUNT = 20; + HttpClient sharedClient; + HttpClient httpClient(boolean shared) { + if (!shared || sharedClient == null) { + var client = HttpClient.newBuilder() + .proxy(HttpClient.Builder.NO_PROXY) + .sslContext(sslContext) + .build(); + if (sharedClient == null) { + sharedClient = client; + } + TRACKER.track(client); + return client; + } + return sharedClient; + } + @Test(dataProvider = "variants") void test(String uri, boolean sameClient, @@ -165,11 +182,7 @@ public class ContinuationFrameTest { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { - client = HttpClient.newBuilder() - .proxy(HttpClient.Builder.NO_PROXY) - .sslContext(sslContext) - .build(); - TRACKER.track(client); + client = httpClient(sameClient); } HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) @@ -229,6 +242,7 @@ public class ContinuationFrameTest { @AfterTest public void teardown() throws Exception { + sharedClient = null; AssertionError fail = TRACKER.check(500); try { http2TestServer.stop(); diff --git a/test/jdk/java/net/httpclient/http2/ErrorTest.java b/test/jdk/java/net/httpclient/http2/ErrorTest.java index 061fd5cd350..e8613b9efa8 100644 --- a/test/jdk/java/net/httpclient/http2/ErrorTest.java +++ b/test/jdk/java/net/httpclient/http2/ErrorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,12 +25,26 @@ * @test * @bug 8157105 * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.net.SimpleSSLContext * @modules java.base/sun.net.www.http * java.net.http/jdk.internal.net.http.common * java.net.http/jdk.internal.net.http.frame * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers * java.security.jgss + * @modules java.base/jdk.internal.util * @run testng/othervm/timeout=60 -Djavax.net.debug=ssl -Djdk.httpclient.HttpClient.log=all ErrorTest * @summary check exception thrown when bad TLS parameters selected */ @@ -47,7 +61,6 @@ import javax.net.ssl.SSLParameters; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2EchoHandler; import jdk.test.lib.net.SimpleSSLContext; diff --git a/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java b/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java index 7d5424163ba..4c63e863fee 100644 --- a/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java +++ b/test/jdk/java/net/httpclient/http2/HpackBinaryTestDriver.java @@ -29,6 +29,6 @@ * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.BinaryPrimitivesTest + * @run testng/othervm/timeout=240 java.net.http/jdk.internal.net.http.hpack.BinaryPrimitivesTest */ public class HpackBinaryTestDriver { } diff --git a/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java b/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java index d2b515c6d63..650ed706c51 100644 --- a/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java +++ b/test/jdk/java/net/httpclient/http2/HpackHuffmanDriver.java @@ -29,6 +29,6 @@ * @compile/module=java.net.http jdk/internal/net/http/hpack/SpecHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/TestHelper.java * @compile/module=java.net.http jdk/internal/net/http/hpack/BuffersTestingKit.java - * @run testng/othervm java.net.http/jdk.internal.net.http.hpack.HuffmanTest + * @run testng/othervm/timeout=300 java.net.http/jdk.internal.net.http.hpack.HuffmanTest */ public class HpackHuffmanDriver { } diff --git a/test/jdk/java/net/httpclient/http2/IdleConnectionTimeoutTest.java b/test/jdk/java/net/httpclient/http2/IdleConnectionTimeoutTest.java deleted file mode 100644 index ac060e8722b..00000000000 --- a/test/jdk/java/net/httpclient/http2/IdleConnectionTimeoutTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* - * @test - * @bug 8288717 - * @summary Tests that when the idleConnectionTimeoutEvent is configured in HTTP/2, - * an HTTP/2 connection will close within the specified interval if there - * are no active streams on the connection. - * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.http2.Http2TestServer - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout=1 - * IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout=2 - * IdleConnectionTimeoutTest - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=1 - * IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=2 - * IdleConnectionTimeoutTest - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=1 - * -Djdk.httpclient.keepalive.timeout=2 - * IdleConnectionTimeoutTest - * - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors -Djdk.httpclient.keepalive.timeout.h2=-1 - * IdleConnectionTimeoutTest - * @run testng/othervm -Djdk.httpclient.HttpClient.log=errors,trace -Djdk.httpclient.keepalive.timeout.h2=abc - * IdleConnectionTimeoutTest - */ - -import jdk.httpclient.test.lib.http2.BodyOutputStream; -import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; -import jdk.httpclient.test.lib.http2.Http2TestServerConnection; -import jdk.internal.net.http.common.HttpHeadersBuilder; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.concurrent.CompletableFuture; -import jdk.httpclient.test.lib.http2.Http2TestServer; -import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; - -import javax.net.ssl.SSLSession; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.net.http.HttpClient.Version.HTTP_2; -import static org.testng.Assert.assertEquals; - -public class IdleConnectionTimeoutTest { - - static Http2TestServer http2TestServer; - URI timeoutUri; - URI noTimeoutUri; - final String IDLE_CONN_PROPERTY = "jdk.httpclient.keepalive.timeout.h2"; - final String KEEP_ALIVE_PROPERTY = "jdk.httpclient.keepalive.timeout"; - final String TIMEOUT_PATH = "/serverTimeoutHandler"; - final String NO_TIMEOUT_PATH = "/noServerTimeoutHandler"; - static final PrintStream testLog = System.err; - - @BeforeTest - public void setup() throws Exception { - http2TestServer = new Http2TestServer(false, 0); - http2TestServer.addHandler(new ServerTimeoutHandler(), TIMEOUT_PATH); - http2TestServer.addHandler(new ServerNoTimeoutHandler(), NO_TIMEOUT_PATH); - http2TestServer.setExchangeSupplier(TestExchangeSupplier::new); - - http2TestServer.start(); - int port = http2TestServer.getAddress().getPort(); - timeoutUri = new URI("http://localhost:" + port + TIMEOUT_PATH); - noTimeoutUri = new URI("http://localhost:" + port + NO_TIMEOUT_PATH); - } - - /* - If the InetSocketAddress of the first remote connection is not equal to the address of the - second remote connection, then the idleConnectionTimeoutEvent has occurred and a new connection - was made to carry out the second request by the client. - */ - @Test - public void test() throws InterruptedException { - String timeoutVal = System.getProperty(IDLE_CONN_PROPERTY); - String keepAliveVal = System.getProperty(KEEP_ALIVE_PROPERTY); - testLog.println("Test run for " + IDLE_CONN_PROPERTY + "=" + timeoutVal); - - int sleepTime = 0; - HttpClient hc = HttpClient.newBuilder().version(HTTP_2).build(); - HttpRequest hreq; - HttpResponse hresp; - if (timeoutVal != null) { - if (keepAliveVal != null) { - // In this case, specified h2 timeout should override keep alive timeout. - // Timeout should occur - hreq = HttpRequest.newBuilder(timeoutUri).version(HTTP_2).GET().build(); - sleepTime = 2000; - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was expected but did not occur"); - } else if (timeoutVal.equals("1")) { - // Timeout should occur - hreq = HttpRequest.newBuilder(timeoutUri).version(HTTP_2).GET().build(); - sleepTime = 2000; - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was expected but did not occur"); - } else if (timeoutVal.equals("2")) { - // Timeout should not occur - hreq = HttpRequest.newBuilder(noTimeoutUri).version(HTTP_2).GET().build(); - sleepTime = 1000; - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); - } else if (timeoutVal.equals("abc") || timeoutVal.equals("-1")) { - // Timeout should not occur - hreq = HttpRequest.newBuilder(noTimeoutUri).version(HTTP_2).GET().build(); - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent was not expected but occurred"); - } - } else { - // When no value is specified then no timeout should occur (default keep alive value of 600 used) - hreq = HttpRequest.newBuilder(noTimeoutUri).version(HTTP_2).GET().build(); - hresp = runRequest(hc, hreq, sleepTime); - assertEquals(hresp.statusCode(), 200, "idleConnectionTimeoutEvent should not occur, no value was specified for this property"); - } - } - - private HttpResponse runRequest(HttpClient hc, HttpRequest req, int sleepTime) throws InterruptedException { - CompletableFuture> request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); - HttpResponse hresp = request.join(); - assertEquals(hresp.statusCode(), 200); - - Thread.sleep(sleepTime); - request = hc.sendAsync(req, HttpResponse.BodyHandlers.ofString(UTF_8)); - return request.join(); - } - - static class ServerTimeoutHandler implements Http2Handler { - - volatile Object firstConnection = null; - - @Override - public void handle(Http2TestExchange exchange) throws IOException { - if (exchange instanceof TestExchangeSupplier exch) { - if (firstConnection == null) { - firstConnection = exch.getTestConnection(); - exch.sendResponseHeaders(200, 0); - } else { - var secondConnection = exch.getTestConnection(); - - if (firstConnection != secondConnection) { - testLog.println("ServerTimeoutHandler: New Connection was used, idleConnectionTimeoutEvent fired." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(200, 0); - } else { - testLog.println("ServerTimeoutHandler: Same Connection was used, idleConnectionTimeoutEvent did not fire." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(400, 0); - } - } - } - } - } - - static class ServerNoTimeoutHandler implements Http2Handler { - - volatile Object firstConnection = null; - - @Override - public void handle(Http2TestExchange exchange) throws IOException { - if (exchange instanceof TestExchangeSupplier exch) { - if (firstConnection == null) { - firstConnection = exch.getTestConnection(); - exch.sendResponseHeaders(200, 0); - } else { - var secondConnection = exch.getTestConnection(); - - if (firstConnection == secondConnection) { - testLog.println("ServerTimeoutHandler: Same Connection was used, idleConnectionTimeoutEvent did not fire." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(200, 0); - } else { - testLog.println("ServerTimeoutHandler: Different Connection was used, idleConnectionTimeoutEvent fired." - + " First Connection Hash: " + firstConnection + ", Second Connection Hash: " + secondConnection); - exch.sendResponseHeaders(400, 0); - } - } - } - } - } - - static class TestExchangeSupplier extends Http2TestExchangeImpl { - - public TestExchangeSupplier(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { - super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed); - } - - public Http2TestServerConnection getTestConnection() { - return this.conn; - } - } -} \ No newline at end of file diff --git a/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java b/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java index 0f4b204fda8..907afc28fa6 100644 --- a/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java +++ b/test/jdk/java/net/httpclient/http2/IdlePooledConnectionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -56,8 +56,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; * @summary verify that the HttpClient's HTTP2 idle connection management doesn't close a connection * when that connection has been handed out from the pool to a caller * @library /test/jdk/java/net/httpclient/lib + * /test/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters - * + * jdk.test.lib.Asserts * @run junit/othervm -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.keepalive.timeout.h2=3 * IdlePooledConnectionTest diff --git a/test/jdk/java/net/httpclient/http2/ProxyTest2.java b/test/jdk/java/net/httpclient/http2/ProxyTest2.java index 733c21ffe68..2fdb1b360e1 100644 --- a/test/jdk/java/net/httpclient/http2/ProxyTest2.java +++ b/test/jdk/java/net/httpclient/http2/ProxyTest2.java @@ -21,20 +21,15 @@ * questions. */ -import com.sun.net.httpserver.HttpContext; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsParameters; -import com.sun.net.httpserver.HttpsServer; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; -import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; @@ -42,9 +37,7 @@ import java.net.ProxySelector; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; @@ -72,11 +65,6 @@ public class ProxyTest2 { static { try { - HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { - return true; - } - }); SSLContext.setDefault(new SimpleSSLContext().get()); } catch (IOException ex) { throw new ExceptionInInitializerError(ex); @@ -336,15 +324,4 @@ public class ProxyTest2 { } - static class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) { - super(ctx); - } - - @Override - public void configure (HttpsParameters params) { - params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); - } - } - } diff --git a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java index 1f9340c5c08..e9c6447e600 100644 --- a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java +++ b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -233,18 +233,18 @@ public class PushPromiseContinuation { @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream content) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) { HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder(); headersBuilder.setHeader(":method", "GET"); headersBuilder.setHeader(":scheme", uri.getScheme()); headersBuilder.setHeader(":authority", uri.getAuthority()); headersBuilder.setHeader(":path", uri.getPath()); - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) headersBuilder.addHeader(entry.getKey(), value); } HttpHeaders combinedHeaders = headersBuilder.build(); - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content); // Indicates to the client that a continuation should be expected pp.setFlag(0x0); try { @@ -292,7 +292,7 @@ public class PushPromiseContinuation { } @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream content) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) { pushPromiseHeadersBuilder = new HttpHeadersBuilder(); testHeadersBuilder = new HttpHeadersBuilder(); cfs = new ArrayList<>(); @@ -301,7 +301,7 @@ public class PushPromiseContinuation { setPushHeaders(":scheme", uri.getScheme()); setPushHeaders(":authority", uri.getAuthority()); setPushHeaders(":path", uri.getPath()); - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) { setPushHeaders(entry.getKey(), value); } @@ -318,7 +318,7 @@ public class PushPromiseContinuation { HttpHeaders pushPromiseHeaders = pushPromiseHeadersBuilder.build(); testHeaders = testHeadersBuilder.build(); // Create the Push Promise Frame - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, pushPromiseHeaders, content, cfs); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, pushPromiseHeaders, rspHeaders, content, cfs); // Indicates to the client that a continuation should be expected pp.setFlag(0x0); diff --git a/test/jdk/java/net/httpclient/http2/RedirectTest.java b/test/jdk/java/net/httpclient/http2/RedirectTest.java index 2d7701c5e37..1d7b894bc40 100644 --- a/test/jdk/java/net/httpclient/http2/RedirectTest.java +++ b/test/jdk/java/net/httpclient/http2/RedirectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,9 +25,12 @@ * @test * @bug 8156514 * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @build jdk.httpclient.test.lib.http2.Http2TestExchange + * jdk.httpclient.test.lib.http2.Http2TestServer * jdk.httpclient.test.lib.http2.Http2EchoHandler * jdk.httpclient.test.lib.http2.Http2RedirectHandler + * jdk.test.lib.Asserts + * jdk.test.lib.net.SimpleSSLContext * @run testng/othervm * -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors * -Djdk.internal.httpclient.debug=true @@ -47,7 +50,6 @@ import java.util.Arrays; import java.util.Iterator; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; -import jdk.httpclient.test.lib.http2.Http2Handler; import jdk.httpclient.test.lib.http2.Http2EchoHandler; import jdk.httpclient.test.lib.http2.Http2RedirectHandler; import org.testng.annotations.Test; diff --git a/test/jdk/java/net/httpclient/http2/SimpleGet.java b/test/jdk/java/net/httpclient/http2/SimpleGet.java new file mode 100644 index 00000000000..8a65292a65d --- /dev/null +++ b/test/jdk/java/net/httpclient/http2/SimpleGet.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm -XX:+CrashOnOutOfMemoryError SimpleGet + * @run testng/othervm -XX:+CrashOnOutOfMemoryError + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * SimpleGet + * @run testng/othervm -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.redirects.retrylimit=5 + * -Djdk.httpclient.HttpClient.log=errors + * -XX:+CrashOnOutOfMemoryError + * -XX:+HeapDumpOnOutOfMemoryError + * SimpleGet + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; + +public class SimpleGet implements HttpServerAdapters { + static HttpTestServer httpsServer; + static HttpClient client = null; + static SSLContext sslContext; + static String httpsURIString; + static ExecutorService serverExec = Executors.newVirtualThreadPerTaskExecutor(); + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + httpsServer = HttpTestServer.create(HTTP_2, sslContext, serverExec); + httpsServer.addHandler(new TestHandler(), "/"); + httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + + httpsServer.start(); + warmup(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + private static void warmup() throws Exception { + SimpleSSLContext sslct = new SimpleSSLContext(); + var sslContext = sslct.get(); + + // warmup server + try (var client2 = createClient(sslContext)) { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_2) + .HEAD().build(); + client2.send(request, BodyHandlers.discarding()); + } + + // warmup client + var httpsServer2 = HttpTestServer.create(HTTP_2, sslContext, + Executors.newVirtualThreadPerTaskExecutor()); + httpsServer2.addHandler(new TestHandler(), "/"); + var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/"; + httpsServer2.start(); + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2)) + .version(HTTP_2) + .HEAD().build(); + client.send(request, BodyHandlers.discarding()); + } finally { + httpsServer2.stop(); + } + } + + public static void main(String[] args) throws Exception { + test(); + } + + @Test + public static void test() throws Exception { + try { + long prestart = System.nanoTime(); + initialize(); + long done = System.nanoTime(); + System.out.println("Stat: Initialization and warmup took " + TimeUnit.NANOSECONDS.toMillis(done - prestart) + " millis"); + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_2) + .GET().build(); + long start = System.nanoTime(); + var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b -> {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("Stat: First request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = property("simpleget.requests", 50); + ; + List>> list = new ArrayList<>(max); + Set connections = new ConcurrentSkipListSet<>(); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b -> {})) + .whenComplete((r, t) -> Optional.ofNullable(r) + .flatMap(HttpResponse::connectionLabel) + .ifPresent(connections::add)); + list.add(cf); + //cf.get(); // uncomment to test with serial instead of concurrent requests + } + try { + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + } finally { + long elapsed2 = System.nanoTime() - start2; + long completed = list.stream().filter(CompletableFuture::isDone) + .filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count(); + connections.forEach(System.out::println); + if (completed > 0) { + System.out.println("Stat: Next " + completed + " requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): " + + elapsed2 / completed + " nanos per request (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + " ms) on " + + connections.size() + " connections"); + } + } + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext) { + return HttpClient.newBuilder() + .sslContext(sslContext) + .version(HTTP_2) + .proxy(Builder.NO_PROXY) + .executor(Executors.newVirtualThreadPerTaskExecutor()) + .build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext); + } + return client; + } + + static int property(String name, int defaultValue) { + return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); + } + + // 32 * 32 * 1024 * 10 chars = 10Mb responses + // 50 requests => 500Mb + // 100 requests => 1Gb + // 1000 requests => 10Gb + private final static int REPEAT = property("simpleget.repeat", 32); + private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32)); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + try (var in = t.getRequestBody()) { + byte[] input = in.readAllBytes(); + t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT); + try (var out = t.getResponseBody()) { + if (t.getRequestMethod().equals("HEAD")) return; + for (int i=0; i> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) headersBuilder.addHeader(entry.getKey(), value); } HttpHeaders combinedHeaders = headersBuilder.build(); - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content); pp.setFlag(HeaderFrame.END_HEADERS); try { @@ -311,7 +309,7 @@ public class TrailingHeadersTest { static final BiPredicate ACCEPT_ALL = (x, y) -> true; - private void pushPromise(Http2TestExchange exchange) { + private void pushPromise(Http2TestExchange exchange) throws IOException { URI requestURI = exchange.getRequestURI(); URI uri = requestURI.resolve("/promise"); InputStream is = new ByteArrayInputStream("Sample_Push_Data".getBytes(UTF_8)); diff --git a/test/jdk/java/net/httpclient/http2/UserInfoTest.java b/test/jdk/java/net/httpclient/http2/UserInfoTest.java index 7dafda0c1f8..69cded67297 100644 --- a/test/jdk/java/net/httpclient/http2/UserInfoTest.java +++ b/test/jdk/java/net/httpclient/http2/UserInfoTest.java @@ -32,6 +32,7 @@ import javax.net.ssl.SSLContext; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import jdk.httpclient.test.lib.http2.Http2TestServer; @@ -46,13 +47,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; * @test * @bug 8292876 * @library /test/lib /test/jdk/java/net/httpclient/lib - * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestExchange + * @compile ../ReferenceTracker.java * @run junit UserInfoTest */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class UserInfoTest { + static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; Http2TestServer server; int port; SSLContext sslContext; @@ -96,6 +101,7 @@ public class UserInfoTest { .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .build(); + TRACKER.track(client); URI uri = URIBuilder.newBuilder() .scheme("https") @@ -112,5 +118,10 @@ public class UserInfoTest { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode(), "Test Failed : " + response.uri().getAuthority()); + + client = null; + System.gc(); + var error = TRACKER.check(500); + if (error != null) throw error; } } diff --git a/test/jdk/java/net/httpclient/http3/BadCipherSuiteErrorTest.java b/test/jdk/java/net/httpclient/http3/BadCipherSuiteErrorTest.java new file mode 100644 index 00000000000..abe6889e497 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/BadCipherSuiteErrorTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8157105 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm/timeout=60 -Djavax.net.debug=ssl -Djdk.httpclient.HttpClient.log=all BadCipherSuiteErrorTest + * @summary check exception thrown when bad TLS parameters selected + */ + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; + +import jdk.test.lib.Asserts; +import jdk.test.lib.net.SimpleSSLContext; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +import org.testng.annotations.Test; + +/** + * When selecting an unacceptable cipher suite the TLS handshake will fail. + * But, the exception that was thrown was not being returned up to application + * causing hang problems + */ +public class BadCipherSuiteErrorTest implements HttpServerAdapters { + + static final String[] CIPHER_SUITES = new String[]{ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" }; + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + //@Test(timeOut=5000) + @Test + public void test() throws Exception { + SSLContext sslContext = (new SimpleSSLContext()).get(); + ExecutorService exec = Executors.newCachedThreadPool(); + var builder = newClientBuilderForH3() + .executor(exec) + .sslContext(sslContext) + .version(HTTP_3); + var goodclient = builder.build(); + var badclient = builder + .sslParameters(new SSLParameters(CIPHER_SUITES)) + .build(); + + + + HttpTestServer httpsServer = null; + try { + SSLContext serverContext = (new SimpleSSLContext()).get(); + SSLParameters p = serverContext.getSupportedSSLParameters(); + p.setApplicationProtocols(new String[]{"h3"}); + httpsServer = HttpTestServer.create(HTTP_3_URI_ONLY, serverContext); + httpsServer.addHandler(new HttpTestEchoHandler(), "/"); + String httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + System.out.println("HTTP/3 Server started on: " + httpsServer.serverAuthority()); + + httpsServer.start(); + URI uri = URI.create(httpsURIString); + + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + + System.out.println("Sending request with good client to " + uri); + HttpResponse response = goodclient.send(req, BodyHandlers.ofString()); + Asserts.assertEquals(response.statusCode(), 200); + Asserts.assertEquals(response.version(), HTTP_3); + Asserts.assertEquals(response.body(), SIMPLE_STRING); + System.out.println("Expected response successfully received"); + try { + System.out.println("Sending request with bad client to " + uri); + response = badclient.send(req, BodyHandlers.discarding()); + throw new RuntimeException("Unexpected response: " + response); + } catch (IOException e) { + System.out.println("Caught Expected IOException: " + e); + } + System.out.println("DONE"); + } finally { + if (httpsServer != null ) { httpsServer.stop(); } + exec.close(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/FramesDecoderTest.java b/test/jdk/java/net/httpclient/http3/FramesDecoderTest.java new file mode 100644 index 00000000000..d406ee491cb --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/FramesDecoderTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.http3 + * @modules java.net.http/jdk.internal.net.http.http3.frames + * @modules java.net.http/jdk.internal.net.http.quic.streams + * @run junit/othervm FramesDecoderTest + * @summary Tests to check HTTP3 methods decode frames correctly + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class FramesDecoderTest { + // frames with arbitrary content, interpreted as PartialFrames + byte[][] vlframes() { + return new byte[][]{ + {0, 2, 0, 0}, // DATA frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // DATA frame, 8-byte VL encoding, 2 bytes = 0,0 + {1, 2, 0, 0}, // HEADERS frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 1, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // HEADERS frame, 8-byte VL encoding, 2 bytes = 0,0 + {5, 3, 0, 0, 0}, // PUSH_PROMISE frame, Push ID = 0, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 10, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // PUSH_PROMISE frame, 8-byte VL encoding, Push ID = 0, 2 bytes = 0,0 + {33, 2, 0, 0}, // RESERVED frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 33, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // RESERVED frame, 8-byte VL encoding, 2 bytes = 0,0 + {32, 2, 0, 0}, // UNKNOWN frame, 2 bytes = 0,0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 32, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 2, 0, 0}, // UNKNOWN frame, 8-byte VL encoding, 2 bytes = 0,0 + }; + } + + // frames with predefined content, correct + byte[][] fixedframes() { + return new byte[][]{ + {3, 1, 0}, // CANCEL_PUSH frame, Push ID = 0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 3, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 8, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // CANCEL_PUSH frame, 8-byte VL encoding, Push ID = 0 + {7, 1, 0}, // GOAWAY frame, Push ID = 0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 8, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // GOAWAY frame, 8-byte VL encoding, Push ID = 0 + {13, 1, 0}, // MAX_PUSH_ID frame, Push ID = 0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 13, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 8, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // MAX_PUSH_ID frame, 8-byte VL encoding, Push ID = 0 + {4, 0}, // SETTINGS frame, empty + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 4, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // SETTINGS frame, 8-byte VL encoding, empty + {4, 2, 31, 0}, // SETTINGS frame, 31(reserved)->0 + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 4, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 16, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 33, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 0}, // SETTINGS frame, 8-byte VL encoding, 33(reserved)->0 + {4, 3, 0x40, 33, 0}, // SETTINGS frame, 33(reserved)->0 + }; + } + + // incorrect frames + byte[][] badframes() { + return new byte[][]{ + {3, 2, 0, 0}, // CANCEL_PUSH frame, Push ID = 0, extra byte + {3, 2, (byte) 0xC0, 0}, // CANCEL_PUSH frame, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 3, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0x80, 0, 0, 0, 0}, // CANCEL_PUSH frame, 8-byte VL encoding, Push ID = 0, extra byte + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 3, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0}, // CANCEL_PUSH frame, 8-byte VL encoding, Push ID = truncated VL + {7, 2, 0, 0}, // GOAWAY frame, Push ID = 0, extra byte + {7, 2, (byte) 0xC0, 0}, // GOAWAY frame, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0x80, 0, 0, 0, 0}, // GOAWAY frame, 8-byte VL encoding, Push ID = 0, extra byte + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0}, // GOAWAY frame, 8-byte VL encoding, Push ID = truncated VL + {13, 2, 0, 0}, // MAX_PUSH_ID frame, Push ID = 0, extra byte + {13, 2, (byte) 0xC0, 0}, // MAX_PUSH_ID frame, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 13, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0x80, 0, 0, 0, 0}, // MAX_PUSH_ID frame, 8-byte VL encoding, Push ID = 0, extra byte + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 13, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 7, (byte) 0xC0, 0, 0, 0, 0, 0, 0}, // MAX_PUSH_ID frame, 8-byte VL encoding, Push ID = truncated VL + {(byte) 0xC0, 0, 0, 0, 0, 0, 0, 5, (byte) 0xC0, 0, 0, 0, 0, 0, 0, 1, (byte) 0xC0 }, // PUSH_PROMISE frame, 8-byte VL encoding, Push ID = truncated VL + {4, 5, 33, 0, 0x41, 0, 0x40}, // SETTINGS frame, 33(reserved)->0, 64 -> truncated VL + {4, 4, 33, 0, 0x41, 0}, // SETTINGS frame, 33(reserved)->0, 64 -> ? + {4, 3, 33, 0, 0x41}, // SETTINGS frame, 33(reserved)->0, truncated VL + {4, 2, 33, 0x40}, // SETTINGS frame, 33(reserved)-> truncated VL + {4, 1, 33}, // SETTINGS frame, 33(reserved)->? + }; + } + + private static int bufLength(List bufs) { + return bufs.stream().mapToInt(ByteBuffer::remaining).sum(); + } + + @ParameterizedTest + @MethodSource("vlframes") + public void testFullVLFrames(byte[] frame) { + // offer the entire frame at once + FramesDecoder fd = new FramesDecoder("test"); + fd.submit(ByteBuffer.wrap(frame)); + fd.submit(QuicStreamReader.EOF); + Http3Frame h3frame = fd.poll(); + assertEquals(2, h3frame.streamingLength()); + assertEquals(h3frame, fd.poll()); + assertFalse(fd.eof()); + List bufs = fd.readPayloadBytes(); + assertEquals(2, bufLength(bufs)); + assertNull(fd.poll()); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("vlframes") + public void testSplitVLFrames(byte[] frame) { + // offer the frame one byte at a time + FramesDecoder fd = new FramesDecoder("test"); + ByteBuffer buffer = ByteBuffer.wrap(frame); + for (int i = 1; i <= frame.length; i++) { + buffer.position(i-1); + buffer.limit(i); + fd.submit(buffer.asReadOnlyBuffer()); + if (i < frame.length - 2) { + assertNull(fd.poll()); + } else { + Http3Frame h3frame = fd.poll(); + assertEquals(2, h3frame.streamingLength()); + } + } + Http3Frame h3frame = fd.poll(); + assertEquals(2, h3frame.streamingLength()); + assertEquals(h3frame, fd.poll()); + assertFalse(fd.eof()); + List bufs = fd.readPayloadBytes(); + assertEquals(2, bufLength(bufs)); + assertNull(fd.poll()); + assertFalse(fd.eof()); + fd.submit(QuicStreamReader.EOF); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("fixedframes") + public void testFullGoodFrames(byte[] frame) { + // offer the entire frame at once + FramesDecoder fd = new FramesDecoder("test"); + fd.submit(ByteBuffer.wrap(frame)); + fd.submit(QuicStreamReader.EOF); + Http3Frame h3frame = fd.poll(); + assertEquals(0, h3frame.streamingLength()); + assertTrue(fd.eof()); + List bufs = fd.readPayloadBytes(); + assertNull(bufs); + assertNull(fd.poll()); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("fixedframes") + public void testSplitGoodFrames(byte[] frame) { + // offer the frame one byte at a time + FramesDecoder fd = new FramesDecoder("test"); + ByteBuffer buffer = ByteBuffer.wrap(frame); + for (int i = 1; i <= frame.length; i++) { + buffer.position(i-1); + buffer.limit(i); + fd.submit(buffer.asReadOnlyBuffer()); + if (i < frame.length) { + assertNull(fd.poll()); + } else { + Http3Frame h3frame = fd.poll(); + assertEquals(0, h3frame.streamingLength()); + } + } + assertNull(fd.poll()); + assertFalse(fd.eof()); + fd.submit(QuicStreamReader.EOF); + assertTrue(fd.eof()); + } + + @ParameterizedTest + @MethodSource("badframes") + public void testFullBadFrames(byte[] frame) { + // offer the entire frame at once + FramesDecoder fd = new FramesDecoder("test"); + fd.submit(ByteBuffer.wrap(frame)); + fd.submit(QuicStreamReader.EOF); + Http3Frame h3frame = fd.poll(); + assertInstanceOf(MalformedFrame.class, h3frame); + assertEquals(Http3Error.H3_FRAME_ERROR.code(), ((MalformedFrame)h3frame).getErrorCode()); + } + + @ParameterizedTest + @MethodSource("badframes") + public void testSplitBadFrames(byte[] frame) { + // offer the frame one byte at a time + FramesDecoder fd = new FramesDecoder("test"); + ByteBuffer buffer = ByteBuffer.wrap(frame); + for (int i = 1; i <= frame.length; i++) { + buffer.position(i-1); + buffer.limit(i); + fd.submit(buffer.asReadOnlyBuffer()); + if (i < frame.length) { + assertNull(fd.poll()); + } else { + Http3Frame h3frame = fd.poll(); + assertInstanceOf(MalformedFrame.class, h3frame); + assertEquals(Http3Error.H3_FRAME_ERROR.code(), ((MalformedFrame)h3frame).getErrorCode()); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java b/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java new file mode 100644 index 00000000000..a67710c485f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +import static java.lang.System.out; + + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm/timeout=60 -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * GetHTTP3Test + * @summary Basic HTTP/3 GET test + */ +// -Djdk.httpclient.http3.maxDirectConnectionTimeout=2500 +public class GetHTTP3Test implements HttpServerAdapters { + + // The response body + static final String BODY = """ + May the road rise up to meet you. + May the wind be always at your back. + May the sun shine warm upon your face; + """; + + SSLContext sslContext; + HttpTestServer h3TestServer; // HTTP/2 ( h2 + h3) + String h3URI; + + static final int ITERATION_COUNT = 4; + // a shared executor helps reduce the amount of threads created by the test + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + final Set sharedClientHasH3 = ConcurrentHashMap.newKeySet(); + private volatile HttpClient sharedClient; + private boolean directQuicConnectionSupported; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + final void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + h3URI, + }; + } + + @DataProvider(name = "variants") + public Object[][] variants(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + String[] uris = uris(); + Object[][] result = new Object[uris.length * 2 * 2 * 2][]; + int i = 0; + for (var version : List.of(Optional.empty(), Optional.of(HTTP_3))) { + for (Version firstRequestVersion : List.of(HTTP_2, HTTP_3)) { + for (boolean sameClient : List.of(false, true)) { + for (String uri : uris()) { + result[i++] = new Object[]{uri, firstRequestVersion, sameClient, version}; + } + } + } + } + assert i == result.length; + return result; + } + + @DataProvider(name = "uris") + public Object[][] uris(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + + @Test(dataProvider = "variants") + public void testAsync(String uri, Version firstRequestVersion, boolean sameClient, Optional version) throws Exception { + System.out.println("Request to " + uri +"/Async/*" + + ", firstRequestVersion=" + firstRequestVersion + + ", sameclient=" + sameClient + ", version=" + version); + + HttpClient client = newHttpClient(sameClient); + final URI headURI = URI.create(uri + "/Async/First/HEAD"); + final Builder headBuilder = HttpRequest.newBuilder(headURI) + .version(firstRequestVersion) + .HEAD(); + Http3DiscoveryMode config = null; + if (firstRequestVersion == HTTP_3 && !directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + config = ALT_SVC; + } + if (config != null) { + out.println("first request will use " + config); + headBuilder.setOption(H3_DISCOVERY, config); + config = null; + } + + HttpResponse response1 = client.send(headBuilder.build(), BodyHandlers.ofString()); + assertEquals(response1.statusCode(), 200, "Unexpected first response code"); + assertEquals(response1.body(), "", "Unexpected first response body"); + boolean expectH3 = sameClient && sharedClientHasH3.contains(headURI.getRawAuthority()); + if (firstRequestVersion == HTTP_3) { + if (expectH3) { + out.println("Expecting HEAD response over HTTP_3"); + assertEquals(response1.version(), HTTP_3, "Unexpected first response version"); + } + } else { + out.println("Expecting HEAD response over HTTP_2"); + assertEquals(response1.version(), HTTP_2, "Unexpected first response version"); + } + out.println("HEAD response version: " + response1.version()); + if (response1.version() == HTTP_2) { + if (sameClient) { + sharedClientHasH3.add(headURI.getRawAuthority()); + } + expectH3 = version.isEmpty() && client.version() == HTTP_3; + if (version.orElse(null) == HTTP_3 && !directQuicConnectionSupported) { + config = ALT_SVC; + expectH3 = true; + } + // we can expect H3 only if the (default) config is not ANY + if (expectH3) { + out.println("first response came over HTTP/2, so we should expect all responses over HTTP/3"); + } + } else if (response1.version() == HTTP_3) { + expectH3 = directQuicConnectionSupported && version.orElse(null) == HTTP_3; + if (expectH3) { + out.println("first response came over HTTP/3, direct connection supported: expect HTTP/3"); + } else if (firstRequestVersion == HTTP_3 && version.isEmpty() + && config == null && directQuicConnectionSupported) { + config = ANY; + expectH3 = true; + } + } + out.printf("request version: %s, directConnectionSupported: %s, first response: %s," + + " config: %s, expectH3: %s%n", + version, directQuicConnectionSupported, response1.version(), config, expectH3); + if (expectH3) { + out.println("All responses should now come through HTTP/3"); + } + + Builder builder = HttpRequest.newBuilder() + .GET(); + version.ifPresent(builder::version); + if (config != null) { + builder.setOption(H3_DISCOVERY, config); + } + Map>> responses = new HashMap<>(); + for (int i = 0; i < ITERATION_COUNT; i++) { + HttpRequest request = builder.uri(URI.create(uri+"/Async/GET/"+i)).build(); + System.out.println("Iteration: " + request.uri()); + responses.put(request.uri(), client.sendAsync(request, BodyHandlers.ofString())); + } + int h3Count = 0; + while (!responses.isEmpty()) { + CompletableFuture.anyOf(responses.values().toArray(CompletableFuture[]::new)).join(); + var done = responses.entrySet().stream() + .filter((e) -> e.getValue().isDone()).toList(); + for (var e : done) { + URI u = e.getKey(); + responses.remove(u); + out.println("Checking response: " + u); + var response = e.getValue().get(); + out.println("Response is: " + response + ", [version: " + response.version() + "]"); + assertEquals(response.statusCode(), 200,"status for " + u); + assertEquals(response.body(), BODY,"body for " + u); + if (expectH3) { + assertEquals(response.version(), HTTP_3, "version for " + u); + } + if (response.version() == HTTP_3) { + h3Count++; + } + } + } + if (client.version() == HTTP_3 || version.orElse(null) == HTTP_3) { + if (h3Count == 0) { + throw new AssertionError("No request used HTTP/3"); + } + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + System.out.println("test: DONE"); + } + + @Test(dataProvider = "uris") + public void testSync(String h3URI) throws Exception { + HttpClient client = makeNewClient(); + Builder builder = HttpRequest.newBuilder(URI.create(h3URI + "/Sync/GET/1")) + .version(HTTP_3) + .GET(); + if (!directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + builder.setOption(H3_DISCOVERY, ALT_SVC); + } + + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + if (directQuicConnectionSupported) { + // TODO unreliable assertion + //assertEquals(response.version(), HTTP_3, "Unexpected first response version"); + } else { + assertEquals(response.version(), HTTP_2, "Unexpected first response version"); + } + assertEquals(response.body(), BODY, "first response body"); + + request = builder.uri(URI.create(h3URI + "/Sync/GET/2")).build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_3, "second response version"); + assertEquals(response.body(), BODY, "second response body"); + + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + final Http2TestServer h2WithAltService = new Http2TestServer("localhost", true, + sslContext).enableH3AltServiceOnSamePort(); + h3TestServer = HttpTestServer.of(h2WithAltService); + h3TestServer.addHandler(new Handler(), "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/GET"; + serverCount.addAndGet(1); + h3TestServer.start(); + directQuicConnectionSupported = h2WithAltService.supportsH3DirectConnection(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + h3TestServer.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Server sending %d - %s length\n", 200, BODY.length()); + t.sendResponseHeaders(200, BODY.length()); + } + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + assertEquals(is.readAllBytes().length, 0); + if (!"HEAD".equals(t.getRequestMethod())) { + String[] body = BODY.split("\n"); + for (String line : body) { + os.write(line.getBytes(StandardCharsets.UTF_8)); + os.write('\n'); + os.flush(); + } + } + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3BadHeadersTest.java b/test/jdk/java/net/httpclient/http3/H3BadHeadersTest.java new file mode 100644 index 00000000000..3bc59a4d67f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3BadHeadersTest.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.internal.httpclient.debug=true H3BadHeadersTest + * @summary this test verifies the behaviour of the HttpClient when presented + * with bad headers + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; + +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.util.List.of; +import static java.util.Map.entry; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +public class H3BadHeadersTest implements HttpServerAdapters { + + private static final List>> BAD_HEADERS = of( + of(entry(":status", "200"), entry(":hello", "GET")), // Unknown pseudo-header + of(entry(":status", "200"), entry("hell o", "value")), // Space in the name + of(entry(":status", "200"), entry("hello", "line1\r\n line2\r\n")), // Multiline value + of(entry(":status", "200"), entry("hello", "DE" + ((char) 0x7F) + "L")) // Bad byte in value + // Not easily testable with H3, because we use a HttpHeadersBuilders which sorts headers... + // of(entry("hello", "world!"), entry(":status", "200")) // Pseudo header is not the first one + ); + + static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + + SSLContext sslContext; + HttpTestServer http3TestServer; // HTTP/3 ( h3 only ) + HttpTestServer https2TestServer; // HTTP/2 ( h2 + h3 ) + String http3URI; + String https2URI; + + + @DataProvider(name = "variants") + public Object[][] variants() { + return new Object[][] { + { http3URI, false}, + { https2URI, false}, + { http3URI, true}, + { https2URI, true}, + }; + } + + + @Test(dataProvider = "variants") + void test(String uri, + boolean sameClient) + throws Exception + { + System.out.printf("%ntest %s, %s, STARTING%n%n", uri, sameClient); + System.err.printf("%ntest %s, %s, STARTING%n%n", uri, sameClient); + var config = uri.startsWith(http3URI) + ? Http3DiscoveryMode.HTTP_3_URI_ONLY + : https2TestServer.supportsH3DirectConnection() + ? Http3DiscoveryMode.ANY + : Http3DiscoveryMode.ALT_SVC; + + boolean sendHeadRequest = config != Http3DiscoveryMode.HTTP_3_URI_ONLY; + + HttpClient client = null; + for (int i=0; i< BAD_HEADERS.size(); i++) { + boolean needsHeadRequest = false; + if (!sameClient || client == null) { + needsHeadRequest = sendHeadRequest; + client = newClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(sslContext) + .build(); + } + + if (needsHeadRequest) { + URI simpleURI = URI.create(uri); + HttpRequest head = HttpRequest.newBuilder(simpleURI) + .version(Version.HTTP_2) + .HEAD().setOption(H3_DISCOVERY, config).build(); + System.out.println("\nSending HEAD request: " + head); + var headResponse = client.send(head, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i); + HttpRequest request = HttpRequest.newBuilder(uriWithQuery) + .POST(BodyPublishers.ofString("Hello there!")) + .setOption(H3_DISCOVERY, config) + .version(Version.HTTP_3) + .build(); + System.out.println("\nSending request:" + uriWithQuery); + try { + HttpResponse response = client.send(request, BodyHandlers.ofString()); + fail("Expected exception, got :" + response + ", " + response.body()); + } catch (IOException ioe) { + System.out.println("Got EXPECTED: " + ioe); + assertDetailMessage(ioe, i); + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + } + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + System.out.printf("%ntest %s, %s, DONE%n%n", uri, sameClient); + System.err.printf("%ntest %s, %s, DONE%n%n", uri, sameClient); + } + + @Test(dataProvider = "variants") + void testAsync(String uri, + boolean sameClient) throws Exception + { + + System.out.printf("%ntestAsync %s, %s, STARTING%n%n", uri, sameClient); + System.err.printf("%ntestAsync %s, %s, STARTING%n%n", uri, sameClient); + var config = uri.startsWith(http3URI) + ? Http3DiscoveryMode.HTTP_3_URI_ONLY + : https2TestServer.supportsH3DirectConnection() + ? Http3DiscoveryMode.ANY + : Http3DiscoveryMode.ALT_SVC; + + boolean sendHeadRequest = config != Http3DiscoveryMode.HTTP_3_URI_ONLY; + + HttpClient client = null; + for (int i=0; i< BAD_HEADERS.size(); i++) { + boolean needsHeadRequest = false; + if (!sameClient || client == null) { + needsHeadRequest = sendHeadRequest; + client = newClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(sslContext) + .build(); + } + + if (needsHeadRequest) { + URI simpleURI = URI.create(uri); + HttpRequest head = HttpRequest.newBuilder(simpleURI) + .version(Version.HTTP_2) + .HEAD() + .setOption(H3_DISCOVERY, config) + .build(); + System.out.println("\nSending HEAD request: " + head); + + var headResponse = client.send(head, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + URI uriWithQuery = URI.create(uri + "?BAD_HEADERS=" + i); + HttpRequest request = HttpRequest.newBuilder(uriWithQuery) + .POST(BodyPublishers.ofString("Hello there!")) + .setOption(H3_DISCOVERY, config) + .version(Version.HTTP_3) + .build(); + System.out.println("\nSending request:" + uriWithQuery); + + Throwable t = null; + try { + HttpResponse response = client.sendAsync(request, BodyHandlers.ofString()).get(); + fail("Expected exception, got :" + response + ", " + response.body()); + } catch (Throwable t0) { + System.out.println("Got EXPECTED: " + t0); + if (t0 instanceof ExecutionException) { + t0 = t0.getCause(); + } + t = t0; + } + assertDetailMessage(t, i); + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + } + if (client != null) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker, 1500); + if (error != null) throw error; + } + System.out.printf("%ntestAsync %s, %s, DONE%n%n", uri, sameClient); + System.err.printf("%ntestAsync %s, %s, DONE%n%n", uri, sameClient); + } + + // Assertions based on implementation specific detail messages. Keep in + // sync with implementation. + static void assertDetailMessage(Throwable throwable, int iterationIndex) { + try { + assertTrue(throwable instanceof IOException, + "Expected IOException, got, " + throwable); + assertNotNull(throwable.getMessage(), "No message for " + throwable); + assertTrue(throwable.getMessage().contains("malformed response"), + "Expected \"malformed response\" in: " + throwable.getMessage()); + + if (iterationIndex == 0) { // unknown + assertTrue(throwable.getMessage().contains("Unknown pseudo-header"), + "Expected \"Unknown pseudo-header\" in: " + throwable.getMessage()); + } else if (iterationIndex == 4) { // unexpected + assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"), + "Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage()); + } else { + assertTrue(throwable.getMessage().contains("Bad header"), + "Expected \"Bad header\" in: " + throwable.getMessage()); + } + } catch (AssertionError e) { + System.out.println("Exception does not match expectation: " + throwable); + throwable.printStackTrace(System.out); + throw e; + } + } + + @BeforeTest + public void setup() throws Exception { + System.out.println("creating servers"); + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + http3TestServer.addHandler(new BadHeadersHandler(), "/http3/echo"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/echo"; + + https2TestServer = HttpTestServer.create(Http3DiscoveryMode.ANY, sslContext); + https2TestServer.addHandler(new BadHeadersHandler(), "/https2/echo"); + https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo"; + + http3TestServer.start(); + https2TestServer.start(); + System.out.println("server started"); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("\n\n**** stopping servers\n"); + System.out.println("stopping servers"); + http3TestServer.stop(); + https2TestServer.stop(); + System.out.println("servers stopped"); + } + + static class BadHeadersHandler implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange t) throws IOException { + var uri = t.getRequestURI(); + String query = uri.getRawQuery(); + if (query != null && !query.isEmpty()) { + int badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1)); + assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() : + "Unexpected badHeadersIndex value: " + badHeadersIndex; + List> headers = BAD_HEADERS.get(badHeadersIndex); + var responseHeaders = t.getResponseHeaders(); + for (var e : headers) { + responseHeaders.addHeader(e.getKey(), e.getValue()); + } + } + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + byte[] bytes = is.readAllBytes(); + t.sendResponseHeaders(200, bytes.length); + if (t.getRequestMethod().equals("HEAD")) { + os.close(); + } else { + os.write(bytes); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3BasicTest.java b/test/jdk/java/net/httpclient/http3/H3BasicTest.java new file mode 100644 index 00000000000..b99520ab830 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3BasicTest.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key randomness + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @compile ../ReferenceTracker.java + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors + * -Djdk.internal.httpclient.debug=true + * H3BasicTest + */ +// -Dseed=-163464189156654174 + +import java.io.IOException; +import java.net.*; +import javax.net.ssl.*; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.file.*; +import java.util.Random; +import java.util.concurrent.*; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2EchoHandler; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.RandomFactory; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static jdk.test.lib.Asserts.assertFileContentsEqual; +import static jdk.test.lib.Utils.createTempFile; +import static jdk.test.lib.Utils.createTempFileOfSize; + +public class H3BasicTest implements HttpServerAdapters { + + private static final Random RANDOM = RandomFactory.getRandom(); + + private static final String CLASS_NAME = H3BasicTest.class.getSimpleName(); + + static int http3Port, https2Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static HttpClient client = null; + static ExecutorService clientExec; + static ExecutorService serverExec; + static SSLContext sslContext; + static volatile String http3URIString, pingURIString, https2URIString; + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + // server that only supports HTTP/3 + http3OnlyServer = new Http3TestServer(sslContext, serverExec); + http3OnlyServer.addHandler("/", new Http2EchoHandler()); + http3OnlyServer.addHandler("/ping", new EchoWithPingHandler()); + http3Port = http3OnlyServer.getAddress().getPort(); + System.out.println("HTTP/3 server started at localhost:" + http3Port); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, 0, serverExec, sslContext); + if (RANDOM.nextBoolean()) { + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } else { + https2AltSvcServer.enableH3AltServiceOnSamePort(); + } + https2AltSvcServer.addHandler(new Http2EchoHandler(), "/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + if (https2AltSvcServer.supportsH3DirectConnection()) { + System.out.println("HTTP/2 server (same HTTP/3 origin) started at localhost:" + https2Port); + } else { + System.out.println("HTTP/2 server (different HTTP/3 origin) started at localhost:" + https2Port); + } + + http3URIString = "https://localhost:" + http3Port + "/foo/"; + pingURIString = "https://localhost:" + http3Port + "/ping/"; + https2URIString = "https://localhost:" + https2Port + "/bar/"; + + http3OnlyServer.start(); + https2AltSvcServer.start(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + static final List> cfs = Collections + .synchronizedList( new LinkedList<>()); + + static CompletableFuture currentCF; + + static class EchoWithPingHandler extends Http2EchoHandler { + private final Object lock = new Object(); + + @Override + public void handle(Http2TestExchange exchange) throws IOException { + // for now only one ping active at a time. don't want to saturate + System.out.println("PING handler invoked for " + exchange.getRequestURI()); + synchronized(lock) { + CompletableFuture cf = currentCF; + if (cf == null || cf.isDone()) { + cf = exchange.sendPing(); + assert cf != null; + cfs.add(cf); + currentCF = cf; + } + } + super.handle(exchange); + } + } + + @Test + public static void test() throws Exception { + try { + initialize(); + System.out.println("servers initialized"); + warmup(false); + warmup(true); + System.out.println("warmup finished"); + simpleTest(false, false); + System.out.println("simpleTest(false, false): done"); + simpleTest(false, true); + System.out.println("simpleTest(false, true): done"); + simpleTest(true, false); + System.out.println("simpleTest(true, false): done"); + System.out.println("simple tests finished"); + streamTest(false); + streamTest(true); + System.out.println("stream tests finished"); + paramsTest(); + System.out.println("params test finished"); + CompletableFuture.allOf(cfs.toArray(new CompletableFuture[0])).join(); + synchronized (cfs) { + for (CompletableFuture cf : cfs) { + System.out.printf("Ping ack received in %d millisec\n", cf.get()); + } + } + System.out.println("closing client"); + if (client != null) { + var tracker = ReferenceTracker.INSTANCE; + tracker.track(client); + client = null; + System.gc(); + var error = tracker.check(1500); + clientExec.close(); + if (error != null) throw error; + } + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + serverExec.close(); + } + } + + static HttpClient getClient() { + if (client == null) { + serverExec = Executors.newCachedThreadPool(); + clientExec = Executors.newCachedThreadPool(); + client = HttpServerAdapters.createClientBuilderForH3() + .executor(clientExec) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI(boolean altSvc) { + return getURI(altSvc, false); + } + + static URI getURI(boolean altsvc, boolean ping) { + if (altsvc) + return URI.create(https2URIString); + else + return URI.create(ping ? pingURIString: http3URIString); + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static final AtomicInteger count = new AtomicInteger(); + static Http3DiscoveryMode config(boolean http3only) { + if (http3only) return HTTP_3_URI_ONLY; + // if the server supports H3 direct connection, we can + // additionally use HTTP_3_URI_ONLY; Otherwise we can + // only use ALT_SVC - or ANY (given that we should have + // preloaded an ALT_SVC in warmup) + int bound = https2AltSvcServer.supportsH3DirectConnection() ? 4 : 3; + int rand = RANDOM.nextInt(bound); + count.getAndIncrement(); + return switch (rand) { + case 1 -> ANY; + case 2 -> ALT_SVC; + case 3 -> HTTP_3_URI_ONLY; + default -> null; + }; + } + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + static final int LOOPS = 13; + static final int FILESIZE = 64 * 1024 + 200; + + static void streamTest(boolean altSvc) throws Exception { + URI uri = getURI(altSvc); + System.err.printf("streamTest %b to %s\n" , altSvc, uri); + System.out.printf("streamTest %b to %s\n" , altSvc, uri); + + HttpClient client = getClient(); + Path src = createTempFileOfSize(CLASS_NAME, ".dat", FILESIZE * 4); + var http3Only = altSvc == false; + var config = config(http3Only); + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofFile(src)) + .setOption(H3_DISCOVERY, config) + .build(); + + Path dest = Paths.get("streamtest.txt"); + dest.toFile().delete(); + CompletableFuture response = client.sendAsync(req, BodyHandlers.ofFile(dest)) + .thenApply(resp -> { + if (resp.statusCode() != 200) + throw new RuntimeException(); + if (resp.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + resp.version()); + } + return resp.body(); + }); + response.join(); + assertFileContentsEqual(src, dest); + System.err.println("streamTest: DONE"); + } + + static void paramsTest() throws Exception { + URI u = new URI("https://localhost:"+https2Port+"/foo"); + System.out.println("paramsTest: Request to " + u); + https2AltSvcServer.addHandler((t -> { + SSLSession s = t.getSSLSession(); + String prot = s.getProtocol(); + if (prot.equals("TLSv1.3")) { + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Protocols =%s\n", prot); + t.sendResponseHeaders(500, -1); + } + }), "/"); + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(u).build(); + HttpResponse resp = client.send(req, BodyHandlers.ofString()); + int stat = resp.statusCode(); + if (stat != 200) { + throw new RuntimeException("paramsTest failed " + stat); + } + if (resp.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + resp.version()); + } + System.err.println("paramsTest: DONE"); + } + + static void warmup(boolean altSvc) throws Exception { + URI uri = getURI(altSvc); + System.out.println("Warmup: Request to " + uri); + System.err.println("Warmup: Request to " + uri); + + // Do a simple warmup request + + HttpClient client = getClient(); + var http3Only = altSvc == false; + var config = config(http3Only); + + // in the warmup phase, we want to make sure + // to preload the ALT_SVC, otherwise the first + // request that uses ALT_SVC might go through HTTP/2 + if (altSvc) config = ALT_SVC; + + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .setOption(H3_DISCOVERY, config) + .build(); + HttpResponse response = client.send(req, BodyHandlers.ofString()); + checkStatus(200, response.statusCode()); + String responseBody = response.body(); + HttpHeaders h = response.headers(); + checkStrings(SIMPLE_STRING, responseBody); + checkStrings(h.firstValue("x-hello").get(), "world"); + checkStrings(h.firstValue("x-bye").get(), "universe"); + } + + static T logExceptionally(String desc, Throwable t) { + System.out.println(desc + " failed: " + t); + System.err.println(desc + " failed: " + t); + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + throw new CompletionException(t); + } + + static void simpleTest(boolean altSvc, boolean ping) throws Exception { + URI uri = getURI(altSvc, ping); + System.err.printf("simpleTest(altSvc:%s, ping:%s) Request to %s%n", + altSvc, ping, uri); + System.out.printf("simpleTest(altSvc:%s, ping:%s) Request to %s%n", + altSvc, ping, uri); + String type = altSvc ? "altSvc" : (ping ? "ping" : "http3"); + + // Do loops asynchronously + + CompletableFuture>[] responses = new CompletableFuture[LOOPS]; + final Path source = createTempFileOfSize(H3BasicTest.class.getSimpleName(), ".dat", FILESIZE); + var http3Only = altSvc == false; + for (int i = 0; i < LOOPS; i++) { + var config = config(http3Only); + HttpRequest request = HttpRequest.newBuilder(uri) + .header("X-Compare", source.toString()) + .POST(BodyPublishers.ofFile(source)) + .setOption(H3_DISCOVERY, config) + .build(); + String desc = type + ": Loop " + i; + System.out.printf("%s simpleTest(altSvc:%s, ping:%s) config(%s) Request to %s%n", + desc, altSvc, ping, config, uri); + System.err.printf("%s simpleTest(altSvc:%s, ping:%s) config(%s) Request to %s%n", + desc, altSvc, ping, config, uri); + Path requestBodyFile = createTempFile(CLASS_NAME, ".dat"); + responses[i] = client.sendAsync(request, BodyHandlers.ofFile(requestBodyFile)) + //.thenApply(resp -> assertFileContentsEqual(resp.body(), source)); + .exceptionally((t) -> logExceptionally(desc, t)) + .thenApply(resp -> { + System.out.printf("Resp %s status %d body size %d\n", + resp.version(), resp.statusCode(), + resp.body().toFile().length() + ); + assertFileContentsEqual(resp.body(), source); + if (resp.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + resp.version()); + } + return resp; + }); + Thread.sleep(100); + System.out.println(type + ": Loop " + i + " done"); + } + CompletableFuture.allOf(responses).join(); + System.err.println(type + " simpleTest: DONE"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ConcurrentPush.java b/test/jdk/java/net/httpclient/http3/H3ConcurrentPush.java new file mode 100644 index 00000000000..f0a9a947d13 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ConcurrentPush.java @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * -Djdk.httpclient.http3.maxConcurrentPushStreams=45 + * H3ConcurrentPush + * @summary This test exercises some of the HTTP/3 specifities for PushPromises. + * It sends several concurrent requests, and the server sends a bunch of + * identical push promise frames to all of them. That is, there will be + * a push promise frame with the same push ID sent to each exchange. + * The one (and only one) of the handlers will open a push stream for + * that push id. The client checks that the expected HTTP/3 specific + * methods are invoked on the PushPromiseHandler. + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class H3ConcurrentPush implements HttpServerAdapters { + + // dummy hack to prevent the IDE complaining that calling + // println will throw NPE + static final PrintStream err = System.err; + static final PrintStream out = System.out; + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + + HttpTestServer server; + URI uri; + URI headURI; + ServerPushHandler pushHandler; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + static final class TestPushPromiseHandler implements PushPromiseHandler { + record NotifiedPromise(PushId pushId, HttpRequest initiatingRequest) {} + final Map requestToPushId = new ConcurrentHashMap<>(); + final Map pushIdToRequest = new ConcurrentHashMap<>(); + final List errors = new CopyOnWriteArrayList<>(); + final List notified = new CopyOnWriteArrayList<>(); + final ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + final Supplier> bodyHandlerSupplier; + final PushPromiseHandler pph; + TestPushPromiseHandler(Supplier> bodyHandlerSupplier) { + this.bodyHandlerSupplier = bodyHandlerSupplier; + this.pph = PushPromiseHandler.of((r) -> bodyHandlerSupplier.get(), promises); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + errors.add(new AssertionError("no pushID provided for: " + pushPromiseRequest)); + } + + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushid) { + notified.add(new NotifiedPromise(pushid, initiatingRequest)); + out.println("notifyPushPromise: pushId=" + pushid); + pph.notifyAdditionalPromise(initiatingRequest, pushid); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function, CompletableFuture>> acceptor) { + out.println("applyPushPromise: " + pushPromiseRequest + ", pushId=" + pushid); + requestToPushId.putIfAbsent(pushPromiseRequest, pushid); + if (pushIdToRequest.putIfAbsent(pushid, pushPromiseRequest) != null) { + errors.add(new AssertionError("pushId already used: " + pushid)); + } + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, pushid, acceptor); + } + + } + + @Test + public void testConcurrentPushes() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + TestPushPromiseHandler custom = new TestPushPromiseHandler<>(BodyHandlers::ofString); + var promises = custom.promises; + + for (int j=0; j < 2; j++) { + if (j == 0) out.println("\ntestCancel: First time around"); + else out.println("\ntestCancel: Second time around: should be a new connection"); + + // now make sure there's an HTTP/3 connection + client.send(HttpRequest.newBuilder(headURI).version(Version.HTTP_3) + .setOption(H3_DISCOVERY, ALT_SVC).HEAD().build(), BodyHandlers.discarding()); + + int waitForPushId; + List>> responses = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + waitForPushId = Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + CompletableFuture> main = client.sendAsync( + HttpRequest.newBuilder(uri.resolve("?i=%s,j=%s".formatted(i, j))) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + custom); + responses.add(main); + } + CompletableFuture.allOf(responses.toArray(CompletableFuture[]::new)).join(); + responses.forEach(cf -> { + var main = cf.join(); + var old = promises.put(main.request(), CompletableFuture.completedFuture(main)); + assertNull(old, "unexpected mapping for: " + old); + }); + + promises.forEach((key, value) -> out.println(key + ":" + value.join().body())); + + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + + int expectedPushes = Math.min(PUSH_PROMISES.size(), maxPushes) + 5; + assertEquals(promises.size(), expectedPushes); + + promises.clear(); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3ConcurrentPush::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3ConcurrentPush::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes))) { + success = true; + } + if (success) { + out.println("Got expected IOException: " + io); + } else throw io; + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection, + // so we can restart from scratch + pushHandler.reset(); + } + var errors = custom.errors; + errors.forEach(t -> t.printStackTrace(System.out)); + var error = errors.stream().findFirst().orElse(null); + if (error != null) throw error; + var notified = custom.notified; + assertEquals(notified.size(), 9*4*2, "Unexpected notification: " + notified); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + private final ReentrantLock lock = new ReentrantLock(); + private final Map sentPromises = new ConcurrentHashMap<>(); + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + // The assumption is that there will be several concurrent + // exchanges, but all on the same connection + // The first exchange that emits a PushPromise sends + // a push promise frame + open the push response stream. + // The other exchanges will simply send a push promise + // frame, with the pushId allocated by the previous exchange. + // The sentPromises map is used to store that pushId. + // This obviously only works if we have a single HTTP/3 connection. + final AtomicInteger count = new AtomicInteger(); + public void handle(HttpTestExchange exchange) throws IOException { + long count = -1; + try { + count = this.count.incrementAndGet(); + err.println("Server: handle " + exchange + + " on " + exchange.getConnectionKey()); + out.println("Server: handle " + exchange.getRequestURI() + + " on " + exchange.getConnectionKey()); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } catch (ClosedChannelException ex) { + out.printf("handling exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + out.printf("Got closed channel exception sending response after sent=%s allowed=%s%n", + sent, allowed); + } + } finally { + out.printf("handled exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + } + } + + volatile long allowed = -1; + volatile int sent = 0; + volatile int nsent = 0; + void reset() { + lock.lock(); + try { + allowed = -1; + sent = 0; + nsent = 0; + sentPromises.clear(); + } finally { + lock.unlock(); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long usePushId = exchange.getRequestHeaders() + .firstValueAsLong("X-UsePushId").orElse(-1); + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + for (Map.Entry promise : promises.entrySet()) { + // if usePushId != -1 we send a single push promise, + // without checking that it's allowed. + // Otherwise, we stop sending promises when we have consumed + // the whole window + if (usePushId == -1 && allowed > 0 && sent >= allowed) { + err.println("Server: sent all allowed promises: " + sent); + break; + } + + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + if (usePushId == -1) { + long pushId; + boolean send = false; + lock.lock(); + try { + Long usedPushId = sentPromises.get(promise.getKey()); + if (usedPushId == null) { + pushId = exchange.sendHttp3PushPromiseFrame(-1, uri, headers); + waitForPushId = pushId + 1; + sentPromises.put(promise.getKey(), pushId); + sent += 1; + send = true; + } else { + pushId = usedPushId; + exchange.sendHttp3PushPromiseFrame(pushId, uri, headers); + } + } finally { + lock.unlock(); + } + if (send) { + exchange.sendHttp3PushResponse(pushId, uri, headers, headers, is); + err.println("Server: Sent push promise with response: " + pushId); + } else { + err.println("Server: Sent push promise frame: " + pushId); + } + if (pushId >= waitForPushId) waitForPushId = pushId + 1; + } else { + exchange.sendHttp3PushPromiseFrame(usePushId, uri, headers); + err.println("Server: Sent push promise frame: " + usePushId); + exchange.sendHttp3PushResponse(usePushId, uri, headers, headers, is); + err.println("Server: Sent push promise response: " + usePushId); + lock.lock(); + sent += 1; + lock.unlock(); + return; + } + } + err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ConnectionPoolTest.java b/test/jdk/java/net/httpclient/http3/H3ConnectionPoolTest.java new file mode 100644 index 00000000000..719067a7c3c --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ConnectionPoolTest.java @@ -0,0 +1,581 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors,http3,quic:hs + * -Djdk.internal.httpclient.debug=false + * H3ConnectionPoolTest + */ + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Supplier; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2EchoHandler; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertNotEquals; +import static jdk.test.lib.Asserts.assertTrue; + +public class H3ConnectionPoolTest implements HttpServerAdapters { + + private static final String CLASS_NAME = H3ConnectionPoolTest.class.getSimpleName(); + + static int altsvcPort, https2Port, http3Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static volatile HttpClient client = null; + static SSLContext sslContext; + static volatile String http3OnlyURIString, https2URIString, http3AltSvcURIString, http3DirectURIString; + + static void initialize(boolean samePort) throws Exception { + initialize(samePort, Http2EchoHandler::new); + } + + static void initialize(boolean samePort, Supplier handlers) throws Exception { + System.out.println("\nConfiguring for advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = null; + client = getClient(); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, sslContext); + if (samePort) { + System.out.println("Attempting to enable advertised HTTP/3 service on same port"); + https2AltSvcServer.enableH3AltServiceOnSamePort(); + System.out.println("Advertised AltSvc on same port " + + (https2AltSvcServer.supportsH3DirectConnection() ? "enabled" : " not enabled")); + } else { + System.out.println("Attempting to enable advertised HTTP/3 service on different port"); + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/https2/"); + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/h2h3/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + altsvcPort = https2AltSvcServer.getH3AltService() + .map(Http3TestServer::getAddress).stream() + .mapToInt(InetSocketAddress::getPort).findFirst() + .getAsInt(); + // server that only supports HTTP/3 - we attempt to use the same port + // as the HTTP/2 server so that we can pretend that the H2 server as two H3 endpoints: + // one advertised (the alt service endpoint og the HTTP/2 server) + // one non advertised (the direct endpoint, at the same authority as HTTP/2, but which + // is in fact our http3OnlyServer) + try { + http3OnlyServer = new Http3TestServer(sslContext, samePort ? 0 : https2Port); + System.out.println("Unadvertised service enabled on " + + (samePort ? "ephemeral port" : "same port")); + } catch (IOException ex) { + System.out.println("Can't create HTTP/3 server on same port: " + ex); + http3OnlyServer = new Http3TestServer(sslContext, 0); + } + http3OnlyServer.addHandler("/" + CLASS_NAME + "/http3/", handlers.get()); + http3OnlyServer.addHandler("/" + CLASS_NAME + "/h2h3/", handlers.get()); + http3OnlyServer.start(); + http3Port = http3OnlyServer.getQuicServer().getAddress().getPort(); + + if (http3Port == https2Port) { + System.out.println("HTTP/3 server enabled on same port than HTTP/2 server"); + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for HTTP/3 server"); + } + } else { + System.out.println("HTTP/3 server enabled on a different port than HTTP/2 server"); + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for HTTP/3 server"); + } + } + if (altsvcPort == https2Port) { + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for advertised AltSvc"); + } + } else { + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for advertised AltSvc"); + } + } + + http3OnlyURIString = "https://" + http3OnlyServer.serverAuthority() + "/" + CLASS_NAME + "/http3/foo/"; + https2URIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/https2/bar/"; + http3DirectURIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/h2h3/direct/"; + http3AltSvcURIString = https2URIString + .replace(":" + https2Port + "/", ":" + altsvcPort + "/") + .replace("/https2/bar/", "/h2h3/altsvc/"); + System.out.println("HTTP/2 server started at: " + https2AltSvcServer.serverAuthority()); + System.out.println(" with advertised HTTP/3 endpoint at: " + + URI.create(http3AltSvcURIString).getRawAuthority()); + System.out.println("HTTP/3 server started at:" + http3OnlyServer.serverAuthority()); + + https2AltSvcServer.start(); + } catch (Throwable e) { + System.out.println("Configuration failed: " + e); + System.err.println("Throwing now: " + e); + e.printStackTrace(); + throw e; + } + } + + @Test + public static void testH3Only() throws Exception { + System.out.println("\nTesting HTTP/3 only"); + initialize(true); + try (HttpClient client = getClient()) { + var reqBuilder = HttpRequest.newBuilder() + .uri(URI.create(http3OnlyURIString)) + .version(HTTP_3) + .GET(); + HttpRequest request1 = reqBuilder.copy() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + HttpResponse response1 = client.send(request1, BodyHandlers.ofString()); + System.out.printf("First response: (%s): %s%n", response1.connectionLabel(), response1); + response1.headers().map().entrySet().forEach((e) -> { + System.out.printf(" %s: %s%n", e.getKey(), e.getValue()); + }); + // ANY should reuse the same connection + HttpRequest request2 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + HttpRequest request3 = reqBuilder.copy() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + // ANY should reuse the same connection + HttpRequest request4 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + HttpResponse response4 = client.send(request4, BodyHandlers.ofString()); + assertEquals(response1.connectionLabel().get(), response2.connectionLabel().get()); + assertEquals(response2.connectionLabel().get(), response3.connectionLabel().get()); + assertEquals(response3.connectionLabel().get(), response4.connectionLabel().get()); + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testH2H3WithTwoAltSVC() throws Exception { + testH2H3(false); + } + + @Test + public static void testH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3(true); + } + + private static void testH2H3(boolean samePort) throws Exception { + System.out.println("\nTesting with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + HttpResponse response1 = client.send(request1, BodyHandlers.ofString()); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // second request with HTTP3_URI_ONLY should reuse a created connection + // It should reuse the advertised connection (from response2) if same + // origin + HttpRequest request3 = req1Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response1.connectionLabel().get(), response3.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request4 = req2Builder.copy().build(); + HttpResponse response4 = client.send(request4, BodyHandlers.ofString()); + assertEquals(HTTP_3, response4.version()); + checkStatus(200, response4.statusCode()); + assertEquals(response4.connectionLabel().get(), response2.connectionLabel().get()); + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first - make a direct connection + HttpRequest request1 = req1Builder.copy().build(); + HttpResponse response1 = client.send(request1, BodyHandlers.ofString()); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + + // second, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request3 = req2Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response1.connectionLabel().get()); + + // fourth request with HTTP_3_URI_ONLY should reuse the first connection, + // and not reuse the second. + HttpRequest request4 = req1Builder.copy().build(); + HttpResponse response4 = client.send(request1, BodyHandlers.ofString()); + assertEquals(HTTP_3, response4.version()); + assertEquals(response4.connectionLabel().get(), response1.connectionLabel().get()); + assertNotEquals(response4.connectionLabel().get(), response3.connectionLabel().get()); + checkStatus(200, response1.statusCode()); + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request3 = req2Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testParallelH2H3WithTwoAltSVC() throws Exception { + testH2H3Concurrent(false); + } + + @Test + public static void testParallelH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3Concurrent(true); + } + + private static void testH2H3Concurrent(boolean samePort) throws Exception { + System.out.println("\nTesting concurrent connections with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + HttpRequest request2 = req2Builder.copy().build(); + List>> directResponses = new ArrayList<>(); + for (int i=0; i<3; i++) { + directResponses.add(client.sendAsync(request1, BodyHandlers.ofString())); + } + // can't send requests in parallel here because if any establishes + // a connection before the H3 direct are established, then the H3 + // direct might reuse the H3 alt since the service is with same origin + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + String c1Label = null; + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s]: %s%n", i, + response1.connectionLabel(), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + if (i == 0) { + c1Label = response1.connectionLabel().get(); + } + assertEquals(c1Label, response1.connectionLabel().orElse(null)); + } + // first request with ALT_SVC is to get alt service, should be H2 + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + assertNotEquals(c1Label, h2resp2.connectionLabel().orElse(null)); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + List>> altResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + altResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + String c2Label = null; + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s]: %s%n", i, + response2.connectionLabel(), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), c1Label); + if (i == 0) { + c2Label = response2.connectionLabel().get(); + } + assertEquals(c2Label, response2.connectionLabel().orElse(null)); + } + + // second set of requests should reuse a created connection + HttpRequest request3 = req1Builder.copy().build(); + List>> mixResponses = new ArrayList<>(); + for (int i=0; i < 3; i++) { + mixResponses.add(client.sendAsync(request3, BodyHandlers.ofString())); + mixResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + for (int i=0; i < mixResponses.size(); i++) { + HttpResponse response3 = mixResponses.get(i).get(); + System.out.printf("mixed response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + if (response3.request().getOption(H3_DISCOVERY).orElse(null) == ALT_SVC) { + assertEquals(c2Label, response3.connectionLabel().get()); + } else { + assertEquals(c1Label, response3.connectionLabel().get()); + } + } + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first - make a direct connection + HttpRequest request1 = req1Builder.copy().build(); + + // second, use the alt service + HttpRequest request2 = req2Builder.copy().build(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // third, use ANY + HttpRequest request3 = req2Builder.copy().setOption(H3_DISCOVERY, ANY).build(); + + List>> directResponses = new ArrayList<>(); + List>> altResponses = new ArrayList<>(); + List>> anyResponses = new ArrayList<>(); + checkStatus(200, h2resp2.statusCode()); + for (int i=0; i<3; i++) { + anyResponses.add(client.sendAsync(request3, BodyHandlers.ofString())); + directResponses.add(client.sendAsync(request1, BodyHandlers.ofString())); + altResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + String c1Label = null; + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s] %s: %s%n", i, + response1.connectionLabel(), + response1.request().getOption(H3_DISCOVERY), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + if (i == 0) { + c1Label = response1.connectionLabel().get(); + } + assertEquals(c1Label, response1.connectionLabel().orElse(null)); + } + String c2Label = null; + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s] %s: %s%n", i, + response2.connectionLabel(), + response2.request().getOption(H3_DISCOVERY), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + if (i == 0) { + c2Label = response2.connectionLabel().get(); + } + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), c1Label); + assertEquals(c2Label, response2.connectionLabel().orElse(null)); + } + var expectedLabels = Set.of(c1Label, c2Label); + for (int i = 0; i < anyResponses.size(); i++) { + HttpResponse response3 = anyResponses.get(i).get(); + System.out.printf("any response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), h2resp2.connectionLabel().get()); + var label = response3.connectionLabel().orElse(""); + assertTrue(expectedLabels.contains(label), "Unexpected label: %s not in %s" + .formatted(label, expectedLabels)); + } + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + HttpResponse response2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + HttpRequest request3 = req2Builder.copy().build(); + HttpResponse response3 = client.send(request3, BodyHandlers.ofString()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + static HttpClient getClient() { + if (client == null) { + client = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + + static T logExceptionally(String desc, Throwable t) { + System.out.println(desc + " failed: " + t); + System.err.println(desc + " failed: " + t); + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + throw new CompletionException(t); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3DataLimitsTest.java b/test/jdk/java/net/httpclient/http3/H3DataLimitsTest.java new file mode 100644 index 00000000000..0bee94e6e8a --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3DataLimitsTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.System.out; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.assertEquals; + + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.quic.QuicStandaloneServer + * @run testng/othervm/timeout=480 -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djavax.net.debug=all + * H3DataLimitsTest + * @summary Verify handling of MAX_DATA / MAX_STREAM_DATA frames + */ +public class H3DataLimitsTest implements HttpServerAdapters { + + SSLContext sslContext; + HttpTestServer h3TestServer; + String h3URI; + + static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + @DataProvider(name = "h3URIs") + public Object[][] versions(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return client; + } + + @Test(dataProvider = "h3URIs") + public void testHugeResponse(final String h3URI) throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI + "?16000000"); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + HttpRequest request = builder.build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_3, "first response version"); + + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_3, "second response version"); + } + + @Test(dataProvider = "h3URIs") + public void testManySmallResponses(final String h3URI) throws Exception { + HttpClient client = makeNewClient(); + URI uri = URI.create(h3URI + "?160000"); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + HttpRequest request = builder.build(); + for (int i=0; i<102; i++) { // more than 100 to exercise MAX_STREAMS + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #" + i + ": " + response); + out.println("Version #" + i + ": " + response.version()); + assertEquals(response.statusCode(), 200, "response status"); + assertEquals(response.version(), HTTP_3, "response version"); + } + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + + // An HTTP/3 server that only supports HTTP/3 + h3TestServer = HttpTestServer.of(new Http3TestServer(sslContext)); + final HttpTestHandler h3Handler = new Handler(); + h3TestServer.addHandler(h3Handler, "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/x"; + + serverCount.addAndGet(1); + h3TestServer.start(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + h3TestServer.stop(); + } + + static class Handler implements HttpTestHandler { + + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + try (InputStream is = t.getRequestBody()) { + is.readAllBytes(); + } + System.out.println("Query: "+uri.getQuery()); + int bytesToProduce = Integer.parseInt(uri.getQuery()); + if ((invocation++ % 2) == 1) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Server sending %d - 0 length\n", 200); + t.sendResponseHeaders(200, bytesToProduce); + } + try (OutputStream os = t.getResponseBody()) { + os.write(new byte[bytesToProduce]); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java b/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java new file mode 100644 index 00000000000..10a3a3af8f2 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java @@ -0,0 +1,1063 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.IRetryAnalyzer; +import org.testng.ITestResult; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.OutputStream; +import java.net.ProtocolException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.time.Duration; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +/* + * @test + * @key intermittent + * @comment testResetControlStream may fail if the client doesn't read the stream type + * before the stream is reset, + * testConnectionCloseXXX may fail because connection_close frame is not retransmitted + * @summary Verifies that the HTTP client responds with the right error codes and types + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors H3ErrorHandlingTest + */ +public class H3ErrorHandlingTest implements HttpServerAdapters { + + private SSLContext sslContext; + private QuicStandaloneServer server; + private String requestURIBase; + + @DataProvider + public static Object[][] controlStreams() { + // control / encoder / decoder + return new Object[][] {{(byte)0}, {(byte)2}, {(byte)3}}; + } + + static final byte[] data = new byte[]{(byte)0,(byte)0}; + static final byte[] headers = new byte[]{(byte)1,(byte)0}; + static final byte[] reserved1 = new byte[]{(byte)2,(byte)0}; + static final byte[] cancel_push = new byte[]{(byte)3,(byte)1,(byte)0}; + static final byte[] settings = new byte[]{(byte)4,(byte)0}; + static final byte[] push_promise = new byte[]{(byte)5,(byte)1,(byte)0}; + // 48 bytes, ID 0, 47 byte headers + static final byte[] valid_push_promise = HexFormat.of().parseHex( + "0530000000"+ // push promise, length 48, id 0, section prefix + "508b089d5c0b8170dc702fbce7"+ // :authority + "d1"+ // :method:get + "51856272d141ff"+ // :path + "d7"+ // :scheme:https + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"); //user-agent + static final byte[] reserved2 = new byte[]{(byte)6,(byte)0}; + static final byte[] goaway = new byte[]{(byte)7,(byte)1,(byte)4}; + static final byte[] reserved3 = new byte[]{(byte)8,(byte)0}; + static final byte[] reserved4 = new byte[]{(byte)9,(byte)0}; + static final byte[] max_push_id = new byte[]{(byte)13,(byte)1,(byte)0}; + static final byte[] huge_id_push_promise = new byte[]{(byte)5,(byte)10, + (byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255, + (byte)0, (byte)0}; + + /* + Truncates or expands the frame to the specified length + */ + private static Object[][] chopFrame(byte[] frame, int... lengths) { + var result = new Object[lengths.length][]; + for (int i = 0; i< lengths.length; i++) { + int length = lengths[i]; + byte[] choppedFrame = Arrays.copyOf(frame, length + 2); + choppedFrame[1] = (byte)length; + result[i] = new Object[] {choppedFrame, lengths[i]}; + } + return result; + } + + /* + Truncates or expands the byte array to the specified length + */ + private static Object[][] chopBytes(byte[] bytes, int... lengths) { + var result = new Object[lengths.length][]; + for (int i = 0; i< lengths.length; i++) { + int length = lengths[i]; + byte[] choppedBytes = Arrays.copyOf(bytes, length); + result[i] = new Object[] {choppedBytes, lengths[i]}; + } + return result; + } + + @DataProvider + public static Object[][] malformedSettingsFrames() { + // 2-byte ID, 2-byte value + byte[] settingsFrame = new byte[]{(byte)4,(byte)4,(byte)0x40, (byte)6, (byte)0x40, (byte)6}; + return chopFrame(settingsFrame, 1, 2, 3); + } + + @DataProvider + public static Object[][] malformedCancelPushFrames() { + byte[] cancelPush = new byte[]{(byte)3,(byte)2, (byte)0x40, (byte)0}; + return chopFrame(cancelPush, 0, 1, 3, 9); + } + + @DataProvider + public static Object[][] malformedGoawayFrames() { + byte[] goaway = new byte[]{(byte)7,(byte)2, (byte)0x40, (byte)0}; + return chopFrame(goaway, 0, 1, 3, 9); + } + + @DataProvider + public static Object[][] malformedResponseHeadersFrames() { + byte[] responseHeaders = HexFormat.of().parseHex( + "011a0000"+ // headers, length 26, section prefix + "d9"+ // :status:200 + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"); //user-agent + return chopFrame(responseHeaders, 0, 1, 4, 5, 6, 7); + } + + @DataProvider + public static Object[][] truncatedResponseFrames() { + byte[] response = HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "210100" // reserved, 1 byte + ); + return chopBytes(response, 1, 2, 3, 4, 6, 7, 9, 10); + } + + @DataProvider + public static Object[][] truncatedControlFrames() { + byte[] response = HexFormat.of().parseHex( + "00"+ // stream type: control + "04022100"+ //settings, reserved + "070104"+ //goaway, 4 + "210100" // reserved, 1 byte + ); + return chopBytes(response, 2, 3, 4, 6, 7, 9, 10); + } + + @DataProvider + public static Object[][] malformedPushPromiseFrames() { + return chopFrame(valid_push_promise, 0, 1, 2, 4, 5, 6); + } + + @DataProvider + public static Object[][] invalidControlFrames() { + // frames not valid on the server control stream (after settings) + // all except cancel_push / goaway (max_push_id is client-only) + return new Object[][] {{data}, {headers}, {settings}, {push_promise}, {max_push_id}, + {reserved1}, {reserved2}, {reserved3}, {reserved4}}; + } + + @DataProvider + public static Object[][] invalidResponseFrames() { + // frames not valid on the response stream + // all except headers / push_promise + // data is not valid as the first frame + return new Object[][] {{data}, {cancel_push}, {settings}, {goaway}, {max_push_id}, + {reserved1}, {reserved2}, {reserved3}, {reserved4}}; + } + + @DataProvider + public static Object[][] invalidPushFrames() { + // frames not valid on the push promise stream + // all except headers + // data is not valid as the first frame + return new Object[][] {{data}, {cancel_push}, {settings}, {push_promise}, {goaway}, {max_push_id}, + {reserved1}, {reserved2}, {reserved3}, {reserved4}}; + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .alpn("h3") + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + } + + /** + * Server sends a non-settings frame on the control stream + */ + @Test + public void testNonSettingsFrame() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, reserved frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 0x21, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_MISSING_SETTINGS); + } + + /** + * Server opens 2 control streams + */ + @Test(dataProvider = "controlStreams") + public void testTwoControlStreams(byte type) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + + QuicSenderStream controlStream, controlStream2; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + controlStream2 = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + var writer2 = controlStream2.connectWriter(scheduler); + // control stream + byte[] bytesToWrite = new byte[] { type }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + writer2.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_STREAM_CREATION_ERROR); + } + + /** + * Server closes control stream + */ + @Test(dataProvider = "controlStreams") + public void testCloseControlStream(byte type) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); + var writer = controlStream.connectWriter(controlscheduler); + + byte[] bytesToWrite = new byte[] { type }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), true); + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_CLOSED_CRITICAL_STREAM); + } + + public static class RetryOnce implements IRetryAnalyzer { + boolean retried; + + @Override + public boolean retry(ITestResult iTestResult) { + if (!retried) { + retried = true; + return true; + } + return false; + } + } + + /** + * Server resets control stream + */ + @Test(dataProvider = "controlStreams", retryAnalyzer = RetryOnce.class) + public void testResetControlStream(byte type) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); + var writer = controlStream.connectWriter(controlscheduler); + + byte[] bytesToWrite = new byte[] { type }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // wait for the stream data to be sent before resetting + System.out.println("Server: sending first ping"); + c.requestSendPing().join(); + // sometimes the first ping succeeds before the stream frame is delivered. + // Send another one just in case. + System.out.println("Server: sending second ping"); + c.requestSendPing().join(); + System.out.println("Server: resetting control stream " + writer.stream().streamId()); + // the test may fail if the stream type byte is not processed by HTTP3 + // before the reset is received. + writer.reset(0); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_CLOSED_CRITICAL_STREAM); + } + + /** + * Server sends unexpected frame on control stream + */ + @Test(dataProvider = "invalidControlFrames") + public void testUnexpectedControlFrame(byte[] frame) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 4, 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); + } + + /** + * Server sends malformed settings frame + */ + @Test(dataProvider = "malformedSettingsFrames") + public void testMalformedSettingsFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream + byte[] bytesToWrite = new byte[] { 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends malformed goaway frame + */ + @Test(dataProvider = "malformedGoawayFrames") + public void testMalformedGoawayFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 4, 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends malformed cancel push frame + */ + @Test(dataProvider = "malformedCancelPushFrames") + public void testMalformedCancelPushFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0 + byte[] bytesToWrite = new byte[] { 0, 4, 0 }; + ByteBuffer buf = ByteBuffer.allocate(3 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends invalid GOAWAY frame sequence + */ + @Test + public void testInvalidGoAwaySequence() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0, GOAWAY, id = 4, GOAWAY, id = 8 + byte[] bytesToWrite = new byte[] { 0, 4, 0, 7, 1, 4, 7, 1, 8}; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends invalid GOAWAY stream ID + */ + @Test + public void testInvalidGoAwayId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0, GOAWAY, id = 7 + byte[] bytesToWrite = new byte[] { 0, 4, 0, 7, 1, 7}; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends invalid CANCEL_PUSH stream ID + */ + @Test + public void testInvalidCancelPushId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 0, CANCEL_PUSH, id = MAX_VL_INTEGER + byte[] bytesToWrite = new byte[] { 0, 4, 0, 3, 8, (byte)255, (byte)255, + (byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255}; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends unexpected frame on push stream + */ + @Test(dataProvider = "invalidPushFrames") + public void testUnexpectedPushFrame(byte[] frame) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); + } + + /** + * Server sends malformed frame on push stream + */ + @Test(dataProvider = "malformedResponseHeadersFrames") + public void testMalformedPushStreamFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, frame.length == 2 + ? Http3Error.H3_FRAME_ERROR + : Http3Error.QPACK_DECOMPRESSION_FAILED); + } + + /** + * Server sends malformed frame on push stream + */ + @Test(dataProvider = "malformedPushPromiseFrames") + public void testMalformedPushPromiseFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + // write PUSH_PROMISE frame + s.outputStream().write(frame); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, frame.length <= 3 + ? Http3Error.H3_FRAME_ERROR + : Http3Error.QPACK_DECOMPRESSION_FAILED); + } + + /** + * Server reuses push stream ID + */ + @Test + public void testDuplicatePushStream() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream, pushStream2; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + pushStream2 = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + + writer = pushStream2.connectWriter(scheduler); + // push stream, id 0 + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends push promise with ID > MAX_PUSH_ID + */ + @Test + public void testInvalidPushPromiseId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + // write PUSH_PROMISE frame + s.outputStream().write(huge_id_push_promise); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server opens a push stream ID > MAX_PUSH_ID + */ + @Test + public void testInvalidPushStreamId() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = pushStream.connectWriter(scheduler); + // push stream, id MAX_VL_INTEGER + byte[] bytesToWrite = new byte[] { 1, + (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255, (byte)255 }; + ByteBuffer buf = ByteBuffer.wrap(bytesToWrite); + writer.scheduleForWriting(buf, false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_ID_ERROR); + } + + /** + * Server sends unexpected frame on response stream + */ + @Test(dataProvider = "invalidResponseFrames") + public void testUnexpectedResponseFrame(byte[] frame) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + s.outputStream().write(frame); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_UNEXPECTED); + } + + /** + * Server sends malformed headers frame on response stream + */ + @Test(dataProvider = "malformedResponseHeadersFrames") + public void testMalformedResponseFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + s.outputStream().write(frame); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, frame.length == 2 + ? Http3Error.H3_FRAME_ERROR + : Http3Error.QPACK_DECOMPRESSION_FAILED); + } + + /** + * Server truncates a frame on the response stream + */ + @Test(dataProvider = "truncatedResponseFrames") + public void testTruncatedResponseFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(frame); + } + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server truncates a frame on the control stream + */ + @Test(dataProvider = "truncatedControlFrames") + public void testTruncatedControlFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var controlscheduler = SequentialScheduler.lockingScheduler(() -> {}); + var writer = controlStream.connectWriter(controlscheduler); + + writer.scheduleForWriting(ByteBuffer.wrap(frame), true); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + // H3_CLOSED_CRITICAL_STREAM is also acceptable here + triggerError(errorCF, Http3Error.H3_FRAME_ERROR, Http3Error.H3_CLOSED_CRITICAL_STREAM); + } + + /** + * Server truncates a frame on the push stream + */ + @Test(dataProvider = "truncatedResponseFrames") + public void testTruncatedPushStreamFrame(byte[] frame, int bytes) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream pushStream; + pushStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + // write PUSH_PROMISE frame + s.outputStream().write(valid_push_promise); + var writer = pushStream.connectWriter(scheduler); + // push stream, id 0 + byte[] bytesToWrite = new byte[] { 1, 0 }; + ByteBuffer buf = ByteBuffer.allocate(2 + frame.length); + buf.put(bytesToWrite); + buf.put(frame); + buf.flip(); + writer.scheduleForWriting(buf, true); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerPushError(errorCF, Http3Error.H3_FRAME_ERROR); + } + + /** + * Server sends a settings frame with reserved HTTP2 settings + */ + @Test + public void testReservedSettingsFrames() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream controlStream; + controlStream = c.openNewLocalUniStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = controlStream.connectWriter(scheduler); + // control stream, settings frame, length 2, setting 4 = 0 + byte[] bytesToWrite = new byte[] { 0, 4, 2, 4, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_SETTINGS_ERROR); + } + + /** + * Server sends a stateless reset + */ + @Test + public void testStatelessReset() throws Exception { + server.addHandler((c,s)-> { + // stateless reset + QuicConnectionId localConnId = c.localConnectionId(); + ByteBuffer resetDatagram = c.endpoint().idFactory().statelessReset(localConnId.asReadOnlyBuffer(), 43); + ((DatagramChannel)c.channel()).send(resetDatagram, c.peerAddress()); + // ignore the request stream; we're expecting the client to close the connection. + // The server won't receive any notification from the client here. + // The connection will leak. + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (Exception e) { + final String expectedMsg = "stateless reset from peer"; + if (e.getMessage() != null && e.getMessage().contains(expectedMsg)) { + // got the expected exception + return; + } + // unexpected exception, throw it back + throw e; + } finally { + client.shutdownNow(); + } + } + + /** + * Server opens a bidi stream + */ + @Test + @Ignore("BiDi streams are rejected by H3 client at QUIC level") + public void testBidiStream() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + QuicSenderStream bidiStream; + bidiStream = c.openNewLocalBidiStream(Duration.ZERO).resultNow(); + var scheduler = SequentialScheduler.lockingScheduler(() -> { + }); + var writer = bidiStream.connectWriter(scheduler); + // some data + byte[] bytesToWrite = new byte[] { 0, 4, 2, 4, 0 }; + writer.scheduleForWriting(ByteBuffer.wrap(bytesToWrite), false); + // ignore the request stream; we're expecting the client to close the connection + completeUponTermination(c, errorCF); + }); + triggerError(errorCF, Http3Error.H3_STREAM_CREATION_ERROR); + } + + /** + * Server closes the connection with a known QUIC error + */ + @Test + public void testConnectionCloseQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.INTERNAL_ERROR) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("INTERNAL_ERROR", "testtest"); + } + + /** + * Server closes the connection with a known crypto error + */ + @Test + public void testConnectionCloseCryptoQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.CRYPTO_ERROR.from() + 80 /*Alert.INTERNAL_ERROR.id*/, null) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("CRYPTO_ERROR", "internal_error", "testtest"); + } + + /** + * Server closes the connection with an unknown crypto error + */ + @Test + public void testConnectionCloseUnknownCryptoQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.CRYPTO_ERROR.from() + 5, null) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("CRYPTO_ERROR", "5", "testtest"); + } + + /** + * Server closes the connection with an unknown QUIC error + */ + @Test + public void testConnectionCloseUnknownQUIC() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.forException( + new QuicTransportException("ignored", null, 0, + QuicTransportErrors.CRYPTO_ERROR.to() + 1 /*0x200*/, null) + ); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("200", "testtest"); + } + + /** + * Server closes the connection with a known H3 error + */ + @Test + public void testConnectionCloseH3() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.appLayerClose(Http3Error.H3_EXCESSIVE_LOAD.code()); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("H3_EXCESSIVE_LOAD", "testtest"); + } + + /** + * Server closes the connection with an unknown H3 error + */ + @Test + public void testConnectionCloseH3Unknown() throws Exception { + server.addHandler((c,s)-> { + TerminationCause tc = TerminationCause.appLayerClose(0x1f21); + tc.peerVisibleReason("testtest"); + c.connectionTerminator().terminate(tc); + + }); + triggerClose("1F21", "testtest"); + } + + + private void triggerClose(String... reasons) throws Exception { + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof IOException, "Expected IOException"); + for (String reason : reasons) { + assertTrue(cause.getMessage().contains(reason), + cause.getMessage() + " does not contain " + reason); + } + } finally { + client.shutdownNow(); + } + } + + + private void triggerError(CompletableFuture errorCF, Http3Error expected) throws Exception { + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); + TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); + final long actual = terminationCause.getCloseCode(); + // expected + assertEquals(actual, expected.code(), "Expected " + toHexString(expected) + " got 0x" + Long.toHexString(actual)); + } finally { + client.shutdownNow(); + } + } + + private void triggerError(CompletableFuture errorCF, Http3Error... expected) throws Exception { + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); + TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); + final long actual = terminationCause.getCloseCode(); + // expected + Optional h3Actual = Http3Error.fromCode(actual); + assertTrue(h3Actual.isPresent(), "Expected HTTP3 error, got 0x" + Long.toHexString(actual)); + Set expectedErrors = Set.of(expected); + assertTrue(expectedErrors.contains(h3Actual.get()), "Expected "+expectedErrors+ + ", got: "+h3Actual); + } finally { + client.shutdownNow(); + } + } + + private void triggerPushError(CompletableFuture errorCF, Http3Error http3Error) throws Exception { + HttpClient client = getHttpClient(); + // close might block; use shutdownNow instead + try { + HttpRequest request = getRequest(); + final HttpResponse response = client.sendAsync( + request, + BodyHandlers.discarding(), + (initiatingRequest, pushPromiseRequest, acceptor) -> + acceptor.apply(BodyHandlers.discarding()) + ).get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response); + } catch (ExecutionException e) { + System.out.println("Client exception [expected]: " + e); + var cause = e.getCause(); + assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); + TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); + final long actual = terminationCause.getCloseCode(); + // expected + assertEquals(actual, http3Error.code(), "Expected " + toHexString(http3Error) + " got 0x" + Long.toHexString(actual)); + } finally { + client.shutdownNow(); + } + } + + private HttpRequest getRequest() throws URISyntaxException { + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + return reqBuilder.build(); + } + + private HttpClient getHttpClient() { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(sslContext).build(); + return client; + } + + private static String toHexString(final Http3Error error) { + return error.name() + "(0x" + Long.toHexString(error.code()) + ")"; + } + + private static void completeUponTermination(final QuicServerConnection serverConnection, + final CompletableFuture cf) { + serverConnection.futureTerminationCause().handle( + (r,t) -> t != null ? cf.completeExceptionally(t) : cf.complete(r)); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3FixedThreadPoolTest.java b/test/jdk/java/net/httpclient/http3/H3FixedThreadPoolTest.java new file mode 100644 index 00000000000..9e801ac2ea4 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3FixedThreadPoolTest.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8087112 8177935 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.internal.httpclient.debug=err + * -Djdk.httpclient.HttpClient.log=ssl,headers,requests,responses,errors + * H3FixedThreadPoolTest + */ + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.test.lib.Asserts.assertFileContentsEqual; +import static jdk.test.lib.Utils.createTempFileOfSize; + +import org.testng.annotations.Test; + +public class H3FixedThreadPoolTest implements HttpServerAdapters { + + private static final String CLASS_NAME = H3FixedThreadPoolTest.class.getSimpleName(); + + static int http3Port, https2Port; + static HttpTestServer http3Server, https2Server; + static volatile HttpClient client = null; + static ExecutorService exec; + static SSLContext sslContext; + + static String http3URIString, https2URIString; + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + exec = Executors.newCachedThreadPool(); + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, exec); + http3Server.addHandler(new HttpTestFileEchoHandler(), "/H3FixedThreadPoolTest/http3-only/"); + http3Port = http3Server.getAddress().getPort(); + + https2Server = HttpTestServer.create(ALT_SVC, sslContext, exec); + https2Server.addHandler(new HttpTestFileEchoHandler(), "/H3FixedThreadPoolTest/http3-alt-svc/"); + https2Server.addHandler((t) -> { + t.getRequestBody().readAllBytes(); + t.sendResponseHeaders(200, 0); + }, "/H3FixedThreadPoolTest/http3-alt-svc/bar/head"); + https2Port = https2Server.getAddress().getPort(); + http3URIString = "https://" + http3Server.serverAuthority() + "/H3FixedThreadPoolTest/http3-only/foo/"; + https2URIString = "https://" + https2Server.serverAuthority() + "/H3FixedThreadPoolTest/http3-alt-svc/bar/"; + + http3Server.start(); + https2Server.start(); + + // warmup client to populate AltServiceRegistry + var head = HttpRequest.newBuilder(URI.create(https2URIString + "head")) + .setOption(H3_DISCOVERY, ALT_SVC).build(); + var resp = client.send(head, BodyHandlers.ofString()); + assert resp.statusCode() == 200; + + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + @Test + public static void test() throws Exception { + try { + initialize(); + simpleTest(false); + simpleTest(true); + streamTest(false); + streamTest(true); + paramsTest(); + if (client != null) { + ReferenceTracker.INSTANCE.track(client); + client = null; + System.gc(); + var error = ReferenceTracker.INSTANCE.check(4000); + if (error != null) throw error; + } + } catch (Exception | Error tt) { + tt.printStackTrace(); + throw tt; + } finally { + http3Server.stop(); + https2Server.stop(); + exec.shutdownNow(); + } + } + + static HttpClient getClient() { + if (client == null) { + // Executor e1 = Executors.newFixedThreadPool(1); + // Executor e = (Runnable r) -> e1.execute(() -> { + // System.out.println("[" + Thread.currentThread().getName() + // + "] Executing: " + // + r.getClass().getName()); + // r.run(); + // }); + client = HttpServerAdapters.createClientBuilderForH3() + .executor(Executors.newFixedThreadPool(2)) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI(boolean http3Only) { + if (http3Only) + return URI.create(http3URIString); + else + return URI.create(https2URIString); + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string \"%s\" != \"%s\"%n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + static final int LOOPS = 32; + static final int FILESIZE = 64 * 1024 + 200; + + static void streamTest(boolean http3only) throws Exception { + URI uri = getURI(http3only); + System.out.printf("%nstreamTest %b to %s%n" , http3only, uri); + System.err.printf("%nstreamTest %b to %s%n" , http3only, uri); + var config = http3only ? HTTP_3_URI_ONLY : ALT_SVC; + + HttpClient client = getClient(); + Path src = createTempFileOfSize(CLASS_NAME, ".dat", FILESIZE * 4); + HttpRequest req = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, config) + .POST(BodyPublishers.ofFile(src)) + .build(); + + Path dest = Path.of("streamtest.txt"); + dest.toFile().delete(); + CompletableFuture response = client.sendAsync(req, BodyHandlers.ofFile(dest)) + .thenApply(resp -> { + if (resp.statusCode() != 200) + throw new RuntimeException(); + return resp.body(); + }); + response.join(); + assertFileContentsEqual(src, dest); + System.err.println("DONE"); + } + + // expect highest supported version we know about + static String expectedTLSVersion(SSLContext ctx) { + SSLParameters params = ctx.getSupportedSSLParameters(); + String[] protocols = params.getProtocols(); + for (String prot : protocols) { + if (prot.equals("TLSv1.3")) + return "TLSv1.3"; + } + return "TLSv1.2"; + } + + static void paramsTest() throws Exception { + System.out.println("\nparamsTest"); + System.err.println("\nparamsTest"); + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + server.addHandler((t -> { + SSLSession s = t.getSSLSession(); + String prot = s.getProtocol(); + System.err.println("Server: paramsTest: " + prot); + if (prot.equals(expectedTLSVersion(sslContext))) { + t.sendResponseHeaders(200, 0); + } else { + System.err.printf("Protocols =%s%n", prot); + t.sendResponseHeaders(500, 0); + } + }), "/"); + server.start(); + try { + URI u = new URI("https://" + server.serverAuthority() + "/paramsTest"); + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(u).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + HttpResponse resp = client.sendAsync(req, BodyHandlers.ofString()).get(); + int stat = resp.statusCode(); + if (stat != 200) { + throw new RuntimeException("paramsTest failed " + stat); + } + } finally { + server.stop(); + } + } + + static void simpleTest(boolean http3only) throws Exception { + System.out.println("\nsimpleTest http3-only=" + http3only); + System.err.println("\nsimpleTest http3-only=" + http3only); + URI uri = getURI(http3only); + var config = http3only ? HTTP_3_URI_ONLY : ALT_SVC; + System.err.println("Request to " + uri); + + // Do a simple warmup request + + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .setOption(H3_DISCOVERY, config) + .build(); + HttpResponse response = client.sendAsync(req, BodyHandlers.ofString()).get(); + HttpHeaders h = response.headers(); + + checkStatus(200, response.statusCode()); + + String responseBody = response.body(); + checkStrings(SIMPLE_STRING, responseBody); + + checkStrings(h.firstValue("x-hello").get(), "world"); + checkStrings(h.firstValue("x-bye").get(), "universe"); + + // Do loops asynchronously + + CompletableFuture[] responses = new CompletableFuture[LOOPS]; + final Path source = createTempFileOfSize(CLASS_NAME, ".dat", FILESIZE); + HttpRequest request = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, config) + .POST(BodyPublishers.ofFile(source)) + .build(); + for (int i = 0; i < LOOPS; i++) { + Path requestPayloadFile = Utils.createTempFile(CLASS_NAME, ".dat"); + responses[i] = client.sendAsync(request, BodyHandlers.ofFile(requestPayloadFile)) + //.thenApply(resp -> compareFiles(resp.body(), source)); + .thenApply(resp -> { + System.out.printf("Resp status %d body size %d\n", + resp.statusCode(), resp.body().toFile().length()); + assertFileContentsEqual(resp.body(), source); + return null; + }); + } + CompletableFuture.allOf(responses).join(); + System.err.println("DONE"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3GoAwayTest.java b/test/jdk/java/net/httpclient/http3/H3GoAwayTest.java new file mode 100644 index 00000000000..fc6166cb66c --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3GoAwayTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/* + * @test + * @summary verifies that when the server sends a GOAWAY frame then + * the client correctly handles it and retries unprocessed requests + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * @run junit/othervm + * -Djdk.httpclient.HttpClient.log=errors,headers,quic:hs,http3 + * H3GoAwayTest + */ +public class H3GoAwayTest { + + private static String REQ_URI_BASE; + private static SSLContext sslCtx; + private static Http3TestServer server; + + @BeforeAll + static void beforeAll() throws Exception { + sslCtx = new SimpleSSLContext().get(); + assertNotNull(sslCtx, "SSLContext couldn't be created"); + server = new Http3TestServer(sslCtx); + final RequestApprover reqApprover = new RequestApprover(); + server.setRequestApprover(reqApprover::allowNewRequest); + server.addHandler("/test", new Handler()); + server.start(); + System.out.println("Server started at " + server.getAddress()); + REQ_URI_BASE = URIBuilder.newBuilder().scheme("https") + .loopback() + .port(server.getAddress().getPort()) + .path("/test") + .build().toString(); + } + + @AfterAll + static void afterAll() throws Exception { + if (server != null) { + System.out.println("stopping server at " + server.getAddress()); + server.close(); + } + } + + /** + * Verifies that when several requests are sent using send() and the server + * connection is configured to send a GOAWAY after processing only a few requests, then + * the remaining requests are retried on a different connection + */ + @Test + public void testSequential() throws Exception { + try (final HttpClient client = HttpServerAdapters + .createClientBuilderFor(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(sslCtx).build()) { + final String[] reqMethods = {"HEAD", "GET", "POST"}; + for (final String reqMethod : reqMethods) { + final int numReqs = RequestApprover.MAX_REQS_PER_CONN + 3; + final Set connectionKeys = new LinkedHashSet<>(); + for (int i = 1; i <= numReqs; i++) { + final URI reqURI = new URI(REQ_URI_BASE + "?" + reqMethod + "=" + i); + final HttpRequest req = HttpRequest.newBuilder() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .uri(reqURI) + .method(reqMethod, BodyPublishers.noBody()) + .build(); + System.out.println("initiating request " + req); + final HttpResponse resp = client.send(req, BodyHandlers.ofString()); + final String respBody = resp.body(); + System.out.println("received response: " + respBody); + assertEquals(200, resp.statusCode(), + "unexpected status code for request " + resp.request()); + // response body is the logical key of the connection on which the + // request was handled + connectionKeys.add(respBody); + } + System.out.println("connections involved in handling the requests: " + + connectionKeys); + // all requests have finished, we now just do a basic check that + // more than one connection was involved in processing these requests + assertEquals(2, connectionKeys.size(), + "unexpected number of connections " + connectionKeys); + } + } + } + + private static final class RequestApprover { + private static final int MAX_REQS_PER_CONN = 6; + private final Map numApproved = + new ConcurrentHashMap<>(); + private final Map numDisapproved = + new ConcurrentHashMap<>(); + + public boolean allowNewRequest(final String connKey) { + final AtomicInteger approved = numApproved.computeIfAbsent(connKey, + (k) -> new AtomicInteger()); + int curr = approved.get(); + while (curr < MAX_REQS_PER_CONN) { + if (approved.compareAndSet(curr, curr + 1)) { + return true; // new request allowed + } + curr = approved.get(); + } + final AtomicInteger disapproved = numDisapproved.computeIfAbsent(connKey, + (k) -> new AtomicInteger()); + final int numUnprocessed = disapproved.incrementAndGet(); + System.out.println(approved.get() + " processed, " + + numUnprocessed + " unprocessed requests so far," + + " sending GOAWAY on connection " + connKey); + return false; + } + } + + private static final class Handler implements Http2Handler { + @Override + public void handle(final Http2TestExchange exchange) throws IOException { + final String connectionKey = exchange.getConnectionKey(); + System.out.println(connectionKey + " responding to request: " + exchange.getRequestURI()); + final byte[] response = connectionKey.getBytes(UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (final OutputStream os = exchange.getResponseBody()) { + os.write(response); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3HeaderSizeLimitTest.java b/test/jdk/java/net/httpclient/http3/H3HeaderSizeLimitTest.java new file mode 100644 index 00000000000..7369bb9190e --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3HeaderSizeLimitTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.net.ProtocolException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.ExecutionException; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.internal.net.http.Http3ConnectionAccess; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.test.lib.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Verifies that the HTTP client respects the SETTINGS_MAX_FIELD_SECTION_SIZE setting on HTTP3 connection + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors H3HeaderSizeLimitTest + */ +public class H3HeaderSizeLimitTest implements HttpServerAdapters { + + private static final long HEADER_SIZE_LIMIT_BYTES = 1024; + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURIBase; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + final QuicServer quicServer = Http3TestServer.quicServerBuilder() + .sslContext(sslContext) + .build(); + h3Server = HttpTestServer.of(new Http3TestServer(quicServer) + .setConnectionSettings(new ConnectionSettings(HEADER_SIZE_LIMIT_BYTES, 0, 0))); + h3Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(h3Server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + /** + * Issues a HTTP3 request with combined request headers size exceeding the limit set by the + * test server. Verifies that such requests fail. + */ + @Test + public void testLargeHeaderSize() throws Exception { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + // the server drops 1 packet out of two! + .connectTimeout(Duration.ofSeconds(Utils.adjustTimeout(10))) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + // issue a few requests so that enough time has passed to allow the SETTINGS frame from the + // server to have reached the client. + for (int i = 1; i <= 3; i++) { + System.out.println("Issuing warmup request " + i + " to " + reqURI); + final HttpResponse response = client.send( + reqBuilder.build(), + BodyHandlers.discarding()); + Assert.assertEquals(response.statusCode(), 200, "Unexpected status code"); + if (i == 3) { + var cf = Http3ConnectionAccess.peerSettings(client, response); + if (!cf.isDone()) { + System.out.println("Waiting for peer settings"); + cf.join(); + } + System.out.println("Got peer settings: " + cf.get()); + } + } + // at this point the client should have processed the SETTINGS frame the server + // and we expect it to start honouring those settings. We start the real testing now + // create headers that are larger than the headers size limit that has been configured on + // the server by this test + final String headerValue = headerValueOfLargeSize(); + for (int i = 0; i < 10; i++) { + reqBuilder.setHeader("header-" + i, headerValue); + } + final HttpRequest request = reqBuilder.build(); + System.out.println("Issuing request to " + reqURI); + final IOException thrown = Assert.expectThrows(ProtocolException.class, + () -> client.send(request, BodyHandlers.discarding())); + if (!thrown.getMessage().equals("Request headers size exceeds limit set by peer")) { + throw thrown; + } + // test same with async + System.out.println("Issuing async request to " + reqURI); + final ExecutionException asyncThrown = Assert.expectThrows(ExecutionException.class, + () -> client.sendAsync(request, BodyHandlers.discarding()).get()); + if (!(asyncThrown.getCause() instanceof ProtocolException)) { + System.err.println("Received unexpected cause"); + throw asyncThrown; + } + if (!asyncThrown.getCause().getMessage().equals("Request headers size exceeds limit set by peer")) { + System.err.println("Received unexpected message in cause"); + throw asyncThrown; + } + } + + private static String headerValueOfLargeSize() { + return "abcdefgh".repeat(250); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java b/test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java new file mode 100644 index 00000000000..e63ae88ed32 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3HeadersEncoding.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.httpclient.qpack.encoderTableCapacityLimit=4096 + * -Djdk.httpclient.qpack.decoderMaxTableCapacity=4096 + * -Dhttp3.test.server.encoderAllowedHeaders=* + * -Dhttp3.test.server.decoderMaxTableCapacity=4096 + * -Dhttp3.test.server.encoderTableCapacityLimit=4096 + * -Djdk.internal.httpclient.qpack.log.level=NORMAL + * H3HeadersEncoding + * @summary this test verifies that when QPACK dynamic table is enabled multiple + * random headers can be encoded/decoded correctly + */ + +import jdk.test.lib.RandomFactory; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.httpclient.test.lib.common.HttpServerAdapters.*; + +public class H3HeadersEncoding { + + private static final int REQUESTS_COUNT = 500; + private static final int HEADERS_PER_REQUEST = 20; + SSLContext sslContext; + HttpTestServer http3TestServer; + HeadersHandler serverHeadersHandler; + String http3URI; + + @BeforeTest + public void setup() throws Exception { + System.out.println("Creating servers"); + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + + http3TestServer = HttpTestServer.create(Http3DiscoveryMode.HTTP_3_URI_ONLY, sslContext); + serverHeadersHandler = new HeadersHandler(); + http3TestServer.addHandler(serverHeadersHandler, "/http3/headers"); + http3URI = "https://" + http3TestServer.serverAuthority() + "/http3/headers"; + + http3TestServer.start(); + } + + @AfterTest + public void tearDown() { + http3TestServer.stop(); + } + + @Test + public void serialRequests() throws Exception { + try (HttpClient client = newClient()) { + for (int i = 0; i < REQUESTS_COUNT; i++) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(http3URI)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + List rndHeaders = TestHeader.randomHeaders(HEADERS_PER_REQUEST); + String[] requestHeaders = rndHeaders.stream() + .flatMap(th -> Stream.of(th.name(), th.value())) + .toArray(String[]::new); + requestBuilder.headers(requestHeaders); + + // Send client request headers in request body for further check + // on the server handler side + requestBuilder.POST(HttpRequest.BodyPublishers.ofString( + TestHeader.headersToBodyContent(rndHeaders))); + + HttpResponse response = + client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + // Headers received by the client + var serverHeadersClientSide = TestHeader.fromHttpHeaders(response.headers()); + // Headers sent by the server handler + var serverHeadersServerSide = TestHeader.bodyContentToHeaders(response.body()); + + // Check that all headers that server sent are received on client side + checkHeaders(serverHeadersServerSide, serverHeadersClientSide, "client"); + } + } + } + + @Test + public void asyncRequests() throws Exception { + try (HttpClient client = newClient()) { + ArrayList> requestsHeaders = new ArrayList<>(REQUESTS_COUNT); + ArrayList requests = new ArrayList<>(REQUESTS_COUNT); + CopyOnWriteArrayList>> futureReplies + = new CopyOnWriteArrayList<>(); + + // Prepare all requests first + for (int i = 0; i < REQUESTS_COUNT; i++) { + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(http3URI)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + List rndHeaders = TestHeader.randomHeaders(HEADERS_PER_REQUEST); + String[] requestHeaders = rndHeaders.stream() + .flatMap(th -> Stream.of(th.name(), th.value())) + .toArray(String[]::new); + requestBuilder.headers(requestHeaders); + requestsHeaders.add(i, rndHeaders); + + // Send client request headers in request body for further check + // on the server handler side + requestBuilder.POST(HttpRequest.BodyPublishers.ofString( + TestHeader.headersToBodyContent(rndHeaders))); + requests.add(i, requestBuilder.build()); + } + + // Send async request + for (int i = 0; i < REQUESTS_COUNT; i++) { + CompletableFuture> cf = + client.sendAsync(requests.get(i), + HttpResponse.BodyHandlers.ofString()); + futureReplies.add(i, cf); + } + + // Join on all CF responses + for (int i = 0; i < REQUESTS_COUNT; i++) { + futureReplies.get(i).join(); + } + + // Check the responses + for (int i = 0; i < REQUESTS_COUNT; i++) { + HttpResponse reply = futureReplies.get(i).get(); + // Headers received by the client + var serverHeadersClientSide = TestHeader.fromHttpHeaders(reply.headers()); + // Headers sent by the server handler + var serverHeadersServerSide = TestHeader.bodyContentToHeaders(reply.body()); + // Check that all headers that server sent are received on client side + checkHeaders(serverHeadersServerSide, serverHeadersClientSide, "client"); + } + } + } + + + private static void checkHeaders(List mustPresent, + List allHeaders, + String sideDescription) { + List notFound = new ArrayList<>(); + for (var header : mustPresent) { + if (!allHeaders.contains(header)) { + notFound.add(header); + } + } + if (!notFound.isEmpty()) { + System.err.println("The following headers was not found on " + + sideDescription + " side: " + notFound); + throw new RuntimeException("Headers not found: " + notFound); + } + } + + HttpClient newClient() { + var builder = createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY); + return builder.build(); + } + + private static final Random RND = RandomFactory.getRandom(); + + record TestHeader(String name, String value) { + static TestHeader randomHeader() { + // It is better to have same id generated two or more + // times during the test run, therefore the range below + int headerId = RND.nextInt(10, 10 + HEADERS_PER_REQUEST * 3); + return new TestHeader("test_header" + headerId, "TestValue"); + } + + static List randomHeaders(int count) { + return IntStream + .range(0, count) + .boxed() + .map(ign -> TestHeader.randomHeader()) + .toList(); + } + + static List fromHttpHeaders(HttpHeaders httpHeaders) { + var headersMap = httpHeaders.map(); + return fromHeadersEntrySet(headersMap.entrySet()); + } + + static List fromTestHttpHeaders(HttpTestRequestHeaders requestHeaders) { + var headersSet = requestHeaders.entrySet(); + return fromHeadersEntrySet(headersSet); + } + + private static List fromHeadersEntrySet(Set>> entrySet) { + return entrySet.stream() + .flatMap(entry -> { + var name = entry.getKey(); + return entry.getValue() + .stream() + .map(value -> new TestHeader(name, value)); + }).toList(); + } + + public static String headersToBodyContent(List rndHeaders) { + return rndHeaders.stream() + .map(TestHeader::toString) + .collect(Collectors.joining(System.lineSeparator())); + } + + public static List bodyContentToHeaders(String bodyContent) { + return Arrays.stream(bodyContent.split(System.lineSeparator())) + .filter(Predicate.not(String::isBlank)) + .map(String::strip) + .map(TestHeader::fromBodyHeaderLine) + .toList(); + } + + public static TestHeader fromBodyHeaderLine(String headerLine) { + String[] parts = headerLine.split(":"); + if (parts.length != 2) { + throw new RuntimeException("Internal test error"); + } + return new TestHeader(parts[0], parts[1]); + } + + public String toString() { + return name + ":" + value; + } + } + + + private class HeadersHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + + var clientHeadersServerSide = TestHeader.fromTestHttpHeaders(t.getRequestHeaders()); + + String requestBody; + try (InputStream is = t.getRequestBody()) { + byte[] body = is.readAllBytes(); + requestBody = new String(body); + } + var clientHeadersClientSide = TestHeader.bodyContentToHeaders(requestBody); + + // Check that all headers that client sent are received on tne server side + checkHeaders(clientHeadersClientSide, clientHeadersServerSide, + "server handler"); + + // Response back with a set of random headers + var responseHeaders = t.getResponseHeaders(); + List serverResp = new ArrayList<>(); + for (TestHeader h : TestHeader.randomHeaders(HEADERS_PER_REQUEST)) { + serverResp.add(h); + responseHeaders.addHeader(h.name(), h.value()); + } + + String responseBody = TestHeader.headersToBodyContent(serverResp); + try (OutputStream os = t.getResponseBody()) { + byte[] responseBodyBytes = responseBody.getBytes(); + t.sendResponseHeaders(200, responseBodyBytes.length); + if (!t.getRequestMethod().equals("HEAD")) { + os.write(responseBodyBytes); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3ImplicitPushCancel.java b/test/jdk/java/net/httpclient/http3/H3ImplicitPushCancel.java new file mode 100644 index 00000000000..735a68c88cf --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ImplicitPushCancel.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * H3ImplicitPushCancel + * @summary This is a clone of http2/ImplicitPushCancel but for HTTP/3 + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; + +public class H3ImplicitPushCancel implements HttpServerAdapters { + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + + HttpTestServer server; + URI uri; + URI headURI; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + HttpTestHandler pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, + PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + System.err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + /* + * With a handler not capable of accepting push promises, then all push + * promises should be rejected / cancelled, without interfering with the + * main response. + */ + @Test + public void test() throws Exception { + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri) + .build(), BodyHandlers.ofString()) + .thenApply(H3ImplicitPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + System.out.println("Got result before error was raised"); + throw new AssertionError("should have failed"); + } catch (CompletionException c) { + Throwable cause = Utils.getCompletionCause(c); + if (cause.getMessage().contains("Max pushId exceeded")) { + System.out.println("Got expected exception: " + cause); + } else throw new AssertionError(cause); + } + + // Send with promise handler + ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + PushPromiseHandler pph = PushPromiseHandler + .of((r) -> BodyHandlers.ofString(), promises); + + HttpResponse main; + try { + main = client.sendAsync( + HttpRequest.newBuilder(uri) + .header("X-WaitForPushId", String.valueOf(1)) + .build(), + BodyHandlers.ofString(), + pph) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + promises.forEach((key, value) -> System.out.println(key + ":" + value.join().body())); + + promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main)); + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + assertEquals(promises.size(), PUSH_PROMISES.size() + 1); + + promises.clear(); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3ImplicitPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + assertEquals(promises.size(), 0); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + public void handle(HttpTestExchange exchange) throws IOException { + System.err.println("Server: handle " + exchange); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long allowed = -1; + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + System.err.println("Got maxPushId: " + allowed); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + for (Map.Entry promise : promises.entrySet()) { + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + exchange.serverPush(uri, headers, is); + } + System.err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3InsertionsLimitTest.java b/test/jdk/java/net/httpclient/http3/H3InsertionsLimitTest.java new file mode 100644 index 00000000000..7ca07a30ab9 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3InsertionsLimitTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.test.lib.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Verifies that the HTTP client respects the maxLiteralWithIndexing + * system property value. + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm -Djdk.httpclient.qpack.encoderTableCapacityLimit=4096 + * -Djdk.internal.httpclient.qpack.allowBlockingEncoding=true + * -Djdk.httpclient.qpack.decoderMaxTableCapacity=4096 + * -Djdk.httpclient.qpack.decoderBlockedStreams=1024 + * -Dhttp3.test.server.encoderAllowedHeaders=* + * -Dhttp3.test.server.decoderMaxTableCapacity=4096 + * -Dhttp3.test.server.encoderTableCapacityLimit=4096 + * -Djdk.httpclient.maxLiteralWithIndexing=32 + * -Djdk.internal.httpclient.qpack.log.level=EXTRA + * H3InsertionsLimitTest + */ +public class H3InsertionsLimitTest implements HttpServerAdapters { + + private static final long HEADER_SIZE_LIMIT_BYTES = 8192; + private static final long MAX_SERVER_DT_CAPACITY = 4096; + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURIBase; + public static final long MAX_LITERALS_WITH_INDEXING = 32L; + private static final CountDownLatch WAIT_FOR_FAILURE = new CountDownLatch(1); + + private static void handle(HttpTestExchange exchange) throws IOException { + String handlerMsg = "Server handler: " + exchange.getRequestURI(); + long unusedStreamID = 1111; + System.out.println(handlerMsg); + System.err.println(handlerMsg); + + try { + ConnectionSettings settings = exchange.clientHttp3Settings().get(); + System.err.println("Received client connection settings: " + settings); + } catch (Exception e) { + throw new RuntimeException("Test issue: failure awaiting for HTTP/3" + + " connection settings from the client", e); + } + + // Set encoder table capacity explicitly + // to avoid waiting client's settings frame + // that triggers the same DT configuration + Encoder encoder = exchange.qpackEncoder(); + encoder.setTableCapacity(MAX_SERVER_DT_CAPACITY); + // Mimic entry insertions on the server-side + try (Encoder.EncodingContext context = encoder.newEncodingContext( + unusedStreamID, 0, encoder.newHeaderFrameWriter())) { + for (int i = 0; i <= MAX_LITERALS_WITH_INDEXING; i++) { + var entry = new TableEntry("n" + i, "v" + i); + var insertedEntry = context.tryInsertEntry(entry); + if (insertedEntry.index() == -1L) { + throw new RuntimeException("Test issue: cannot insert" + + " entry to the encoder dynamic table"); + } + } + } + try { + WAIT_FOR_FAILURE.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Test Issue: handler interrupted", e); + } + // Send a response + exchange.sendResponseHeaders(200, 0); + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + final QuicServer quicServer = Http3TestServer.quicServerBuilder() + .bindAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)) + .sslContext(sslContext) + .build(); + var http3TestServer = new Http3TestServer(quicServer) + .setConnectionSettings(new ConnectionSettings( + HEADER_SIZE_LIMIT_BYTES, MAX_SERVER_DT_CAPACITY, 0)); + + h3Server = HttpTestServer.of(http3TestServer); + h3Server.addHandler(H3InsertionsLimitTest::handle, "/insertions"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(h3Server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + @Test + public void multipleTableInsertions() throws Exception { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + // the server drops 1 packet out of two! + .connectTimeout(Duration.ofSeconds(Utils.adjustTimeout(10))) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURIBase + "/insertions"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + final HttpRequest request = + reqBuilder.POST(HttpRequest.BodyPublishers.ofString("Hello")).build(); + System.out.println("Issuing request to " + reqURI); + try { + client.send(request, BodyHandlers.discarding()); + Assert.fail("IOException expected"); + } catch (IOException ioe) { + System.out.println("Got IOException: " + ioe); + Assert.assertTrue(ioe.getMessage() + .contains("Too many literal with indexing")); + } finally { + WAIT_FOR_FAILURE.countDown(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java b/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java new file mode 100644 index 00000000000..06b12b6a477 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java @@ -0,0 +1,437 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.HexFormat; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +/* + * @test + * @summary Verifies that the HTTP client correctly handles malformed responses + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors H3MalformedResponseTest + */ +public class H3MalformedResponseTest implements HttpServerAdapters { + + private SSLContext sslContext; + private QuicStandaloneServer server; + private String requestURIBase; + + // These responses are malformed and should not be accepted by the client, + // but they should not cause connection closure + @DataProvider + public static Object[][] malformedResponse() { + return new Object[][]{ + new Object[] {"empty", HexFormat.of().parseHex( + "" + )}, + new Object[] {"non-final response", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00" // :status:100 + )}, + new Object[] {"uppercase header name", HexFormat.of().parseHex( + "01090000"+ // headers, length 9, section prefix + "d9"+ // :status:200 + "234147450130"+ // AGE:0 + "000100" // data, 1 byte + )}, + new Object[] {"content too long", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "d9"+ // :status:200 + "c4"+ // content-length:0 + "000100" // data, 1 byte + )}, + new Object[] {"content too short", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "540132"+ // content-length:2 + "000100" // data, 1 byte + )}, + new Object[] {"text in content-length", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "540161"+ // content-length:a + "000100" // data, 1 byte + )}, + new Object[] {"connection: close", HexFormat.of().parseHex( + "01150000"+ // headers, length 21, section prefix + "d9"+ // :status:200 + "2703636F6E6E656374696F6E05636C6F7365"+ // connection:close + "000100" // data, 1 byte + )}, + // request pseudo-headers in response + new Object[] {":method in response", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "d9"+ // :status:200 + "d1"+ // :method:get + "000100" // data, 1 byte + )}, + new Object[] {":authority in response", HexFormat.of().parseHex( + "01100000"+ // headers, length 16, section prefix + "d9"+ // :status:200 + "508b089d5c0b8170dc702fbce7"+ // :authority + "000100" // data, 1 byte + )}, + new Object[] {":path in response", HexFormat.of().parseHex( + "010a0000"+ // headers, length 10, section prefix + "d9"+ // :status:200 + "51856272d141ff"+ // :path + "000100" // data, 1 byte + )}, + new Object[] {":scheme in response", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "d9"+ // :status:200 + "d7"+ // :scheme:https + "000100" // data, 1 byte + )}, + new Object[] {"undefined pseudo-header", HexFormat.of().parseHex( + "01080000"+ // headers, length 8, section prefix + "d9"+ // :status:200 + "223A6D0130"+ // :m:0 + "000100" // data, 1 byte + )}, + new Object[] {"pseudo-header after regular", HexFormat.of().parseHex( + "011a0000"+ // headers, length 26, section prefix + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"+ //user-agent + "d9"+ // :status:200 + "000100" // data, 1 byte + )}, + new Object[] {"trailer", HexFormat.of().parseHex( + "01020000" // headers, length 2, section prefix + )}, + new Object[] {"trailer+data", HexFormat.of().parseHex( + "01020000"+ // headers, length 2, section prefix + "000100" // data, 1 byte + )}, + // valid characters include \t, 0x20-0x7e, 0x80-0xff (RFC 9110, section 5.5) + new Object[] {"invalid character in field value 00", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "570100"+ // etag:\0 + "000100" // data, 1 byte + )}, + new Object[] {"invalid character in field value 0a", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "57010a"+ // etag:\n + "000100" // data, 1 byte + )}, + new Object[] {"invalid character in field value 0d", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "57010d"+ // etag:\r + "000100" // data, 1 byte + )}, + new Object[] {"invalid character in field value 7f", HexFormat.of().parseHex( + "01060000"+ // headers, length 6, section prefix + "d9"+ // :status:200 + "57017f"+ // etag: 0x7f + "000100" // data, 1 byte + )}, + }; + } + + // These responses are malformed and should not be accepted by the client. + // They might or might not cause connection closure (H3_FRAME_UNEXPECTED) + @DataProvider + public static Object[][] malformedResponse2() { + // data before headers is covered by H3ErrorHandlingTest + return new Object[][]{ + new Object[] {"100+data", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00"+ // :status:100 + "000100" // data, 1 byte + )}, + new Object[] {"100+data+200", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00"+ // :status:100 + "000100"+ // data, 1 byte + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+data+200", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+data+100", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01040000"+ // headers, length 4, section prefix + "ff00" // :status:100 + )}, + new Object[] {"200+data+trailers+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01020000"+ // trailers, length 2, section prefix + "000100" // data, 1 byte + )}, + new Object[] {"200+trailers+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01020000"+ // trailers, length 2, section prefix + "000100" // data, 1 byte + )}, + new Object[] {"200+200", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+100", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01040000"+ // headers, length 4, section prefix + "ff00" // :status:100 + )}, + }; + } + + @DataProvider + public static Object[][] wellformedResponse() { + return new Object[][]{ + new Object[] {"100+200+data+reserved", HexFormat.of().parseHex( + "01040000"+ // headers, length 4, section prefix + "ff00"+ // :status:100 + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "210100" // reserved, 1 byte + )}, + new Object[] {"200+data+reserved", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "210100" // reserved, 1 byte + )}, + new Object[] {"200+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100" // data, 1 byte + )}, + new Object[] {"200+user-agent+data", HexFormat.of().parseHex( + "011a0000"+ // headers, length 26, section prefix + "d9"+ // :status:200 + "5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747"+ //user-agent + "000100" // data, 1 byte + )}, + new Object[] {"200", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9" // :status:200 + )}, + new Object[] {"200+data+data", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "000100" // data, 1 byte + )}, + new Object[] {"200+data+trailers", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "000100"+ // data, 1 byte + "01020000" // trailers, length 2, section prefix + )}, + new Object[] {"200+trailers", HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "01020000" // trailers, length 2, section prefix + )}, + }; + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .alpn("h3") + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + } + + /** + * Server sends a well-formed response + */ + @Test(dataProvider = "wellformedResponse") + public void testWellFormedResponse(String desc, byte[] response) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + } + // verify that the connection stays open + completeUponTermination(c, errorCF); + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + assertEquals(response1.statusCode(), 200); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + } finally { + client.shutdownNow(); + } + } + + + /** + * Server sends a malformed response that should not close connection + */ + @Test(dataProvider = "malformedResponse") + public void testMalformedResponse(String desc, byte[] response) throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + } + // verify that the connection stays open + completeUponTermination(c, errorCF); + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response1); + } catch (TimeoutException e) { + throw e; + } catch (Exception e) { + System.out.println("Got expected exception: " +e); + e.printStackTrace(); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + } finally { + client.shutdownNow(); + } + } + + /** + * Server sends a malformed response that might close connection + */ + @Test(dataProvider = "malformedResponse2") + public void testMalformedResponse2(String desc, byte[] response) throws Exception { + server.addHandler((c,s)-> { + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + } + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.sendAsync( + request, + BodyHandlers.discarding()) + .get(10, TimeUnit.SECONDS); + fail("Expected the request to fail, got " + response1); + } catch (TimeoutException e) { + throw e; + } catch (Exception e) { + System.out.println("Got expected exception: " +e); + e.printStackTrace(); + } finally { + client.shutdownNow(); + } + } + + private HttpRequest getRequest() throws URISyntaxException { + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + return reqBuilder.build(); + } + + private HttpClient getHttpClient() { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(sslContext).build(); + return client; + } + + private static String toHexString(final Http3Error error) { + return error.name() + "(0x" + Long.toHexString(error.code()) + ")"; + } + + private static void completeUponTermination(final QuicServerConnection serverConnection, + final CompletableFuture cf) { + serverConnection.futureTerminationCause().handle( + (r,t) -> t != null ? cf.completeExceptionally(t) : cf.complete(r)); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3MaxInitialTimeoutTest.java b/test/jdk/java/net/httpclient/http3/H3MaxInitialTimeoutTest.java new file mode 100644 index 00000000000..30f48c56454 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MaxInitialTimeoutTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.channels.DatagramChannel; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.ITestContext; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static java.lang.System.out; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.assertEquals; + + +/* + * @test + * @bug 8342954 + * @summary Verify jdk.httpclient.quic.maxInitialTimeout is taken into account. + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.quic.QuicStandaloneServer + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,quic:controls + * -Djdk.httpclient.quic.maxInitialTimeout=1 + * H3MaxInitialTimeoutTest + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,quic:controls + * -Djdk.httpclient.quic.maxInitialTimeout=2 + * H3MaxInitialTimeoutTest + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors,quic:controls + * -Djdk.httpclient.quic.maxInitialTimeout=2147483647 + * H3MaxInitialTimeoutTest + */ +public class H3MaxInitialTimeoutTest implements HttpServerAdapters { + + SSLContext sslContext; + DatagramChannel receiver; + String h3URI; + + static final Executor executor = new TestExecutor(Executors.newVirtualThreadPerTaskExecutor()); + static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + @DataProvider(name = "h3URIs") + public Object[][] versions(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient(long connectionTimeout) { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(connectionTimeout)) + .build(); + return client; + } + + @Test(dataProvider = "h3URIs") + public void testTimeout(final String h3URI) throws Exception { + long timeout = Long.getLong("jdk.httpclient.quic.maxInitialTimeout", 30); + long connectionTimeout = timeout == Integer.MAX_VALUE ? 2 : 10 * timeout; + + try (HttpClient client = makeNewClient(connectionTimeout)) { + URI uri = URI.create(h3URI); + Builder builder = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + HttpRequest request = builder.build(); + try { + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + assertEquals(response.statusCode(), 200, "first response status"); + assertEquals(response.version(), HTTP_3, "first response version"); + throw new AssertionError("Expected ConnectException not thrown"); + } catch (ConnectException c) { + String msg = c.getMessage(); + if (timeout != Integer.MAX_VALUE) { + if (msg != null && msg.contains("No response from peer")) { + out.println("Got expected exception: " + c); + } else throw c; + } else throw c; + } catch (HttpConnectTimeoutException hc) { + String msg = hc.getMessage(); + if (timeout == Integer.MAX_VALUE) { + if (msg != null && msg.contains("No response from peer")) { + throw new AssertionError("Unexpected message: " + msg, hc); + } else { + out.println("Got expected exception: " + hc); + return; + } + } else throw hc; + } + } + + } + + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + receiver = DatagramChannel.open(); + receiver.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + h3URI = URIBuilder.newBuilder() + .scheme("https") + .loopback() + .port(((InetSocketAddress)receiver.getLocalAddress()).getPort()) + .path("/") + .build() + .toString(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + receiver.close(); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3MemoryHandlingTest.java b/test/jdk/java/net/httpclient/http3/H3MemoryHandlingTest.java new file mode 100644 index 00000000000..f270297d85c --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MemoryHandlingTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.HexFormat; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +/* + * @test + * @summary Verifies that the HTTP client does not buffer excessive amounts of data + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @library ../access + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @build java.net.http/jdk.internal.net.http.Http3ConnectionAccess + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djdk.httpclient.quic.maxStreamInitialData=16384 + * -Djdk.httpclient.quic.streamBufferSize=2048 H3MemoryHandlingTest + */ +public class H3MemoryHandlingTest implements HttpServerAdapters { + + private SSLContext sslContext; + private QuicStandaloneServer server; + private String requestURIBase; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .alpn("h3") + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(server.getAddress().getPort()).build().toString(); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + } + + /** + * Server sends a large response, and the user code does not read from the input stream. + * Writing on the server side should block once the buffers are full. + */ + @Test + public void testOfInputStreamBlocks() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + byte[] response = HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "00ffffffffffffffff"); // data, 2^62 - 1 bytes + byte[] kilo = new byte[1024]; + final CompletableFuture serverAllWritesDone = new CompletableFuture<>(); + server.addHandler((c,s)-> { + // verify that the connection stays open + completeUponTermination(c, errorCF); + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + for (int i = 0; i < 20; i++) { + // 18 writes should succeed, 19th should block + outputStream.write(kilo); + System.out.println("Wrote "+(i+1)+"KB"); + } + // all 20 writes unexpectedly completed + serverAllWritesDone.complete(true); + } catch(IOException ex) { + System.out.println("Got expected exception: " + ex); + serverAllWritesDone.complete(false); + } + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.send( + request, BodyHandlers.ofInputStream()); + assertEquals(response1.statusCode(), 200); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + assertFalse(serverAllWritesDone.isDone()); + response1.body().close(); + final boolean done = serverAllWritesDone.get(10, TimeUnit.SECONDS); + assertFalse(done, "Too much data was buffered by the client"); + } finally { + client.close(); + } + } + + /** + * Server sends a large response, and the user code does not read from the input stream. + * Writing on the server side should unblock once the client starts receiving. + */ + @Test + public void testOfInputStreamUnblocks() throws Exception { + CompletableFuture errorCF = new CompletableFuture<>(); + CompletableFuture handlerCF = new CompletableFuture<>(); + byte[] response = HexFormat.of().parseHex( + "01030000"+ // headers, length 3, section prefix + "d9"+ // :status:200 + "0080008000"); // data, 32 KB + byte[] kilo = new byte[1024]; + CountDownLatch writerBlocked = new CountDownLatch(1); + + server.addHandler((c,s)-> { + // verify that the connection stays open + completeUponTermination(c, errorCF); + QuicBidiStream qs = s.underlyingBidiStream(); + + try (OutputStream outputStream = s.outputStream()) { + outputStream.write(response); + for (int i = 0;i < 32;i++) { + // 18 writes should succeed, 19th should block + if (i == 18) { + writerBlocked.countDown(); + } + outputStream.write(kilo); + System.out.println("Wrote "+(i+1)+"KB"); + } + handlerCF.complete(true); + } catch (IOException e) { + handlerCF.completeExceptionally(e); + } + }); + HttpClient client = getHttpClient(); + try { + HttpRequest request = getRequest(); + final HttpResponse response1 = client.send( + request, BodyHandlers.ofInputStream()); + assertEquals(response1.statusCode(), 200); + assertFalse(errorCF.isDone(), "Expected the connection to be open"); + assertFalse(handlerCF.isDone()); + assertTrue(writerBlocked.await(10, TimeUnit.SECONDS), + "write of 18 KB should have succeeded"); + System.out.println("Wait completed, receiving response"); + byte[] receivedResponse; + try (InputStream body = response1.body()) { + receivedResponse = body.readAllBytes(); + } + assertEquals(receivedResponse.length, 32768, + "Unexpected response length"); + } finally { + client.close(); + } + assertTrue(handlerCF.get(10, TimeUnit.SECONDS), + "Unexpected result"); + } + + private HttpRequest getRequest() throws URISyntaxException { + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + return reqBuilder.build(); + } + + private HttpClient getHttpClient() { + final HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(sslContext).build(); + return client; + } + + private static String toHexString(final Http3Error error) { + return error.name() + "(0x" + Long.toHexString(error.code()) + ")"; + } + + private static void completeUponTermination(final QuicServerConnection serverConnection, + final CompletableFuture cf) { + serverConnection.futureTerminationCause().handle( + (r,t) -> t != null ? cf.completeExceptionally(t) : cf.complete(r)); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java b/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java new file mode 100644 index 00000000000..45024c58e1f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3MultipleConnectionsToSameHost.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test id=with-continuations + * @bug 8087112 + * @requires os.family != "windows" | ( os.name != "Windows 10" & os.name != "Windows Server 2016" + * & os.name != "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=360 -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.quic.minPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoffTime=90 + * -Djdk.httpclient.quic.maxPtoBackoff=10 + * -Djdk.internal.httpclient.quic.useNioSelector=false + * -Djdk.internal.httpclient.quic.poller.usePlatformThreads=false + * -Djdk.httpclient.quic.maxEndpoints=-1 + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.quic.maxBidiStreams=2 + * -Djdk.httpclient.retryOnStreamlimit=50 + * -Djdk.httpclient.HttpClient.log=errors,http3,quic:retransmit + * -Dsimpleget.requests=100 + * H3MultipleConnectionsToSameHost + * @summary test multiple connections and concurrent requests with blocking IO and virtual threads + */ +/* + * @test id=without-continuations + * @bug 8087112 + * @requires os.family == "windows" & ( os.name == "Windows 10" | os.name == "Windows Server 2016" + * | os.name == "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=360 -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.quic.minPtoBackoffTime=45 + * -Djdk.httpclient.quic.maxPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * -Djdk.internal.httpclient.quic.useNioSelector=false + * -Djdk.internal.httpclient.quic.poller.usePlatformThreads=false + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * -Djdk.httpclient.quic.maxEndpoints=-1 + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.quic.maxBidiStreams=2 + * -Djdk.httpclient.retryOnStreamlimit=50 + * -Djdk.httpclient.HttpClient.log=errors,http3,quic:retransmit + * -Dsimpleget.requests=100 + * H3MultipleConnectionsToSameHost + * @summary test multiple connections and concurrent requests with blocking IO and virtual threads + * on windows 10 and windows 2016 - but with -XX:-VMContinuations + */ +/* + * @test id=useNioSelector + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=360 -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.quic.idleTimeout=120 + * -Djdk.httpclient.keepalive.timeout.h3=120 + * -Djdk.test.server.quic.idleTimeout=90 + * -Djdk.httpclient.quic.minPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoffTime=120 + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * -Djdk.internal.httpclient.quic.useNioSelector=true + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.quic.maxEndpoints=1 + * -Djdk.httpclient.quic.maxBidiStreams=2 + * -Djdk.httpclient.retryOnStreamlimit=50 + * -Djdk.httpclient.HttpClient.log=errors,http3,quic:hs:retransmit + * -Dsimpleget.requests=100 + * H3MultipleConnectionsToSameHost + * @summary Send 100 large concurrent requests, with connections whose max stream + * limit is artificially low, in order to cause concurrent connections + * to the same host to be created, with non-blocking IO and selector + */ + +// Interesting additional settings for debugging and manual testing: +// ----------------------------------------------------------------- +// -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations +// -Djdk.httpclient.HttpClient.log=errors,requests,http3,quic +// -Djdk.httpclient.HttpClient.log=requests,errors,quic:retransmit:control,http3 +// -Djdk.httpclient.HttpClient.log=errors,requests,quic:all +// -Djdk.httpclient.quic.defaultMTU=64000 +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.httpclient.quic.defaultMTU=4096 +// -Djdk.httpclient.http3.maxStreamLimitTimeout=1375 +// -Xmx16g +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.internal.httpclient.debug=err +// -XX:+HeapDumpOnOutOfMemoryError +// -Djdk.httpclient.HttpClient.log=errors,quic:cc +// -Djdk.httpclient.quic.sendBufferSize=16384 +// -Djdk.httpclient.quic.receiveBufferSize=16384 + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static jdk.internal.net.http.Http3ClientProperties.MAX_STREAM_LIMIT_WAIT_TIMEOUT; + +public class H3MultipleConnectionsToSameHost implements HttpServerAdapters { + static HttpTestServer httpsServer; + static HttpClient client = null; + static SSLContext sslContext; + static String httpsURIString; + static ExecutorService serverExec = + Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("server-vt-worker-", 1).factory()); + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + httpsServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, serverExec); + httpsServer.addHandler(new TestHandler(), "/"); + httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + + httpsServer.start(); + warmup(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + private static void warmup() throws Exception { + SimpleSSLContext sslct = new SimpleSSLContext(); + var sslContext = sslct.get(); + + // warmup server + try (var client2 = createClient(sslContext, Executors.newVirtualThreadPerTaskExecutor())) { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client2.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } + + // warmup client + var httpsServer2 = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, + Executors.newVirtualThreadPerTaskExecutor()); + httpsServer2.addHandler(new TestHandler(), "/"); + var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/"; + httpsServer2.start(); + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } finally { + httpsServer2.stop(); + } + } + + public static void main(String[] args) throws Exception { + test(); + } + + @Test + public static void test() throws Exception { + try { + long prestart = System.nanoTime(); + initialize(); + long done = System.nanoTime(); + System.out.println("Initialization and warmup took "+ TimeUnit.NANOSECONDS.toMillis(done-prestart)+" millis"); + // Thread.sleep(30000); + int maxBidiStreams = Utils.getIntegerNetProperty("jdk.httpclient.quic.maxBidiStreams", 100); + long timeout = MAX_STREAM_LIMIT_WAIT_TIMEOUT; + + Set connections = new ConcurrentSkipListSet<>(); + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET().build(); + long start = System.nanoTime(); + var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("First request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = property("simpleget.requests", 50); + List>> list = new ArrayList<>(max); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + int rqNum = i; + var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b-> {})) + .whenComplete((r, t) -> { + Optional.ofNullable(r) + .flatMap(HttpResponse::connectionLabel) + .ifPresent(connections::add); + if (r != null) { + System.out.println(rqNum + " completed: " + r.connectionLabel()); + } else { + System.out.println(rqNum + " failed: " + t); + } + }); + list.add(cf); + //cf.get(); // uncomment to test with serial instead of concurrent requests + } + try { + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + } finally { + long elapsed2 = System.nanoTime() - start2; + long completed = list.stream().filter(CompletableFuture::isDone) + .filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count(); + if (completed > 0) { + System.out.println("Next " + completed + " requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): " + + elapsed2 / completed + " nanos per request (" + TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + + " ms) on " + connections.size() + " connections"); + } + if (completed == list.size()) { + long msPerRequest = TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed; + if (timeout == 0 || timeout < msPerRequest) { + int expectedCount = max / maxBidiStreams; + if (expectedCount > 2) { + if (connections.size() < expectedCount - Math.max(1, expectedCount/5)) { + throw new AssertionError( + "Too few connections: %s for %s requests with %s streams/connection (timeout %s ms)" + .formatted(connections.size(), max, maxBidiStreams, timeout)); + } + } + } + if (connections.size() > max - Math.max(1, max/5)) { + throw new AssertionError( + "Too few connections: %s for %s requests with %s streams/connection (timeout %s ms)" + .formatted(connections.size(), max, maxBidiStreams, timeout)); + } + + } + } + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + client.close(); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext, ExecutorService clientExec) { + var builder = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(Builder.NO_PROXY); + if (clientExec != null) { + builder = builder.executor(clientExec); + } + return builder.build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext, null); + } + return client; + } + + static int property(String name, int defaultValue) { + return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); + } + + // 32 * 32 * 1024 * 10 chars = 10Mb responses + // 50 requests => 500Mb + // 100 requests => 1Gb + // 1000 requests => 10Gb + private final static int REPEAT = property("simpleget.repeat", 32); + private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32)); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + try (var in = t.getRequestBody()) { + byte[] input = in.readAllBytes(); + t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT); + try (var out = t.getResponseBody()) { + if (t.getRequestMethod().equals("HEAD")) return; + for (int i=0; i { + he.getResponseHeaders().addHeader("encoding", "UTF-8"); + he.sendResponseHeaders(200, RESPONSE.length()); + if (!he.getRequestMethod().equals("HEAD")) { + he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8)); + } + he.close(); + }, PATH); + + return server; + } + + static HttpTestServer createHttp3Server() throws Exception { + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault()); + server.addHandler(he -> { + he.getResponseHeaders().addHeader("encoding", "UTF-8"); + he.sendResponseHeaders(200, RESPONSE.length()); + if (!he.getRequestMethod().equals("HEAD")) { + he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8)); + } + he.close(); + }, PATH); + + return server; + } + + public static void main(String[] args) + throws Exception + { + HttpTestServer server = createHttps2Server(); + HttpTestServer server3 = createHttp3Server(); + server.start(); + server3.start(); + try { + if (server.supportsH3DirectConnection()) { + test(server, ANY); + try { + test(server, HTTP_3_URI_ONLY); + throw new AssertionError("expected UnsupportedProtocolVersionException not raised"); + } catch (UnsupportedProtocolVersionException upve) { + System.out.printf("%nGot expected exception: %s%n%n", upve); + } + } + test(server, ALT_SVC); + try { + test(server3, HTTP_3_URI_ONLY); + throw new AssertionError("expected UnsupportedProtocolVersionException not raised"); + } catch (UnsupportedProtocolVersionException upve) { + System.out.printf("%nGot expected exception: %s%n%n", upve); + } + } finally { + server.stop(); + server3.stop(); + System.out.println("Server stopped"); + } + } + + public static void test(HttpTestServer server, + Http3DiscoveryMode config) + throws Exception + { + System.out.println(""" + + # -------------------------------------------------- + # Server is %s + # Config is %s + # -------------------------------------------------- + """.formatted(server.getAddress(), config)); + + URI uri = new URI("https://" + server.serverAuthority() + PATH + "x"); + TunnelingProxy proxy = new TunnelingProxy(server); + proxy.start(); + try { + System.out.println("Proxy started"); + System.out.println("\nSetting up request with HttpClient for version: " + + config.name() + " URI=" + uri); + ProxySelector ps = ProxySelector.of( + InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort())); + HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .proxy(ps) + .build(); + try (client) { + if (config == ALT_SVC) { + System.out.println("\nSending HEAD request to preload AltServiceRegistry"); + HttpRequest head = HttpRequest.newBuilder() + .uri(uri) + .HEAD() + .version(Version.HTTP_2) + .build(); + var headResponse = client.send(head, BodyHandlers.ofString()); + System.out.println("Got head response: " + headResponse); + if (headResponse.statusCode() != 200) { + throw new AssertionError("bad status code: " + headResponse); + } + if (!headResponse.version().equals(Version.HTTP_2)) { + throw new AssertionError("bad protocol version: " + headResponse.version()); + } + + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(uri) + .GET() + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, config) + .build(); + + System.out.println("\nSending request with HttpClient: " + config); + HttpResponse response + = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("Got response"); + if (response.statusCode() != 200) { + throw new AssertionError("bad status code: " + response); + } + if (!response.version().equals(Version.HTTP_2)) { + throw new AssertionError("bad protocol version: " + response.version()); + } + + String resp = response.body(); + System.out.println("Received: " + resp); + if (!RESPONSE.equals(resp)) { + throw new AssertionError("Unexpected response"); + } + } + } catch (Throwable t) { + System.out.println("Error: " + t); + throw t; + } finally { + System.out.println("Stopping proxy"); + proxy.stop(); + System.out.println("Proxy stopped"); + } + } + + static class TunnelingProxy { + final Thread accept; + final ServerSocket ss; + final boolean DEBUG = false; + final HttpTestServer serverImpl; + final CopyOnWriteArrayList> connectionCFs + = new CopyOnWriteArrayList<>(); + private volatile boolean stopped; + TunnelingProxy(HttpTestServer serverImpl) throws IOException { + this.serverImpl = serverImpl; + ss = new ServerSocket(); + accept = new Thread(this::accept); + accept.setDaemon(true); + } + + void start() throws IOException { + ss.setReuseAddress(false); + ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + accept.start(); + } + + // Pipe the input stream to the output stream. + private synchronized Thread pipe(InputStream is, OutputStream os, + char tag, CompletableFuture end) { + return new Thread("TunnelPipe("+tag+")") { + @Override + public void run() { + try { + try (os) { + int c; + while ((c = is.read()) != -1) { + os.write(c); + os.flush(); + // if DEBUG prints a + or a - for each transferred + // character. + if (DEBUG) System.out.print(tag); + } + is.close(); + } + } catch (IOException ex) { + if (DEBUG) ex.printStackTrace(System.out); + } finally { + end.complete(null); + } + } + }; + } + + public InetSocketAddress getAddress() { + return new InetSocketAddress( InetAddress.getLoopbackAddress(), ss.getLocalPort()); + } + + // This is a bit shaky. It doesn't handle continuation + // lines, but our client shouldn't send any. + // Read a line from the input stream, swallowing the final + // \r\n sequence. Stops at the first \n, doesn't complain + // if it wasn't preceded by '\r'. + // + String readLine(InputStream r) throws IOException { + StringBuilder b = new StringBuilder(); + int c; + while ((c = r.read()) != -1) { + if (c == '\n') break; + b.appendCodePoint(c); + } + if (b.codePointAt(b.length() -1) == '\r') { + b.delete(b.length() -1, b.length()); + } + return b.toString(); + } + + public void accept() { + Socket clientConnection; + try { + while (!stopped) { + System.out.println("Tunnel: Waiting for client"); + Socket toClose; + try { + toClose = clientConnection = ss.accept(); + } catch (IOException io) { + if (DEBUG) io.printStackTrace(System.out); + break; + } + System.out.println("Tunnel: Client accepted"); + Socket targetConnection; + InputStream ccis = clientConnection.getInputStream(); + OutputStream ccos = clientConnection.getOutputStream(); + Writer w = new OutputStreamWriter(ccos, StandardCharsets.UTF_8); + PrintWriter pw = new PrintWriter(w); + System.out.println("Tunnel: Reading request line"); + String requestLine = readLine(ccis); + System.out.println("Tunnel: Request status line: " + requestLine); + if (requestLine.startsWith("CONNECT ")) { + // We should probably check that the next word following + // CONNECT is the host:port of our HTTPS serverImpl. + // Some improvement for a followup! + + // Read all headers until we find the empty line that + // signals the end of all headers. + while(!requestLine.equals("")) { + System.out.println("Tunnel: Reading header: " + + (requestLine = readLine(ccis))); + } + + // Open target connection + targetConnection = new Socket( + InetAddress.getLoopbackAddress(), + serverImpl.getAddress().getPort()); + + // Then send the 200 OK response to the client + System.out.println("Tunnel: Sending " + + "HTTP/1.1 200 OK\r\n\r\n"); + pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + pw.flush(); + } else { + // This should not happen. If it does then just print an + // error - both on out and err, and close the accepted + // socket + System.out.println("WARNING: Tunnel: Unexpected status line: " + + requestLine + " received by " + + ss.getLocalSocketAddress() + + " from " + + toClose.getRemoteSocketAddress() + + " - closing accepted socket"); + // Print on err + System.err.println("WARNING: Tunnel: Unexpected status line: " + + requestLine + " received by " + + ss.getLocalSocketAddress() + + " from " + + toClose.getRemoteSocketAddress()); + // close accepted socket. + toClose.close(); + System.err.println("Tunnel: accepted socket closed."); + continue; + } + + // Pipe the input stream of the client connection to the + // output stream of the target connection and conversely. + // Now the client and target will just talk to each other. + System.out.println("Tunnel: Starting tunnel pipes"); + CompletableFuture end, end1, end2; + Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+', + end1 = new CompletableFuture<>()); + Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-', + end2 = new CompletableFuture<>()); + end = CompletableFuture.allOf(end1, end2); + end.whenComplete( + (r,t) -> { + try { toClose.close(); } catch (IOException x) { } + finally {connectionCFs.remove(end);} + }); + connectionCFs.add(end); + t1.start(); + t2.start(); + } + } catch (Throwable ex) { + try { + ss.close(); + } catch (IOException ex1) { + ex.addSuppressed(ex1); + } + ex.printStackTrace(System.err); + } finally { + System.out.println("Tunnel: exiting (stopped=" + stopped + ")"); + connectionCFs.forEach(cf -> cf.complete(null)); + } + } + + public void stop() throws IOException { + stopped = true; + ss.close(); + try { + if (accept != Thread.currentThread()) accept.join(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3PushCancel.java b/test/jdk/java/net/httpclient/http3/H3PushCancel.java new file mode 100644 index 00000000000..a5293444ade --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3PushCancel.java @@ -0,0 +1,508 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * -Djdk.httpclient.http3.maxConcurrentPushStreams=5 + * H3PushCancel + * @summary This test checks that not accepting one of the push promise + * will cancel it. It also verifies that receiving a pushId bigger + * than the max push ID allowed on the connection will cause + * the exchange to fail and the connection to get closed. + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.nio.channels.ClosedChannelException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class H3PushCancel implements HttpServerAdapters { + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + + HttpTestServer server; + URI uri; + URI headURI; + ServerPushHandler pushHandler; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + System.err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + @Test + public void testNoCancel() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + System.out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + PushPromiseHandler pph = PushPromiseHandler + .of((r) -> BodyHandlers.ofString(), promises); + + for (int j=0; j < 2; j++) { + if (j == 0) System.out.println("\ntestNoCancel: First time around"); + else System.out.println("\ntestNoCancel: Second time around: should be a new connection"); + + int waitForPushId; + for (int i = 0; i < 3; i++) { + HttpResponse main; + waitForPushId = i * Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + try { + main = client.sendAsync( + HttpRequest.newBuilder(uri) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + pph) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + promises.forEach((key, value1) -> System.out.println(key + ":" + value1.join().body())); + + promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main)); + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + assertEquals(promises.size(), Math.min(PUSH_PROMISES.size(), maxPushes) + 1); + + promises.clear(); + } + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes * 3))) { + success = true; + } + if (success) { + System.out.println("Got expected IOException: " + io); + } else throw io; + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection + // so we can restart from scratch + pushHandler.reset(); + } + } + } + + @Test + public void testCancel() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + System.out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(Version.HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + PushPromiseHandler pph = PushPromiseHandler + .of((r) -> BodyHandlers.ofString(), promises); + record NotifiedPromise(PushId pushId, HttpRequest initiatingRequest) {} + final Map requestToPushId = new ConcurrentHashMap<>(); + final Map pushIdToRequest = new ConcurrentHashMap<>(); + final List errors = new CopyOnWriteArrayList<>(); + final List notified = new CopyOnWriteArrayList<>(); + PushPromiseHandler custom = new PushPromiseHandler<>() { + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, acceptor); + } + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushid) { + notified.add(new NotifiedPromise(pushid, initiatingRequest)); + pph.notifyAdditionalPromise(initiatingRequest, pushid); + } + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function, CompletableFuture>> acceptor) { + System.out.println("applyPushPromise: " + pushPromiseRequest + ", pushId=" + pushid); + requestToPushId.putIfAbsent(pushPromiseRequest, pushid); + if (pushIdToRequest.putIfAbsent(pushid, pushPromiseRequest) != null) { + errors.add(new AssertionError("pushId already used: " + pushid)); + } + if (pushid instanceof Http3PushId http3PushId) { + if (http3PushId.pushId() == 1) { + System.out.println("Cancelling: " + http3PushId); + return; // cancel pushId == 1 + } + } + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, pushid, acceptor); + } + }; + + for (int j=0; j < 2; j++) { + if (j == 0) System.out.println("\ntestCancel: First time around"); + else System.out.println("\ntestCancel: Second time around: should be a new connection"); + + int waitForPushId; + for (int i = 0; i < 3; i++) { + HttpResponse main; + waitForPushId = i * Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + try { + main = client.sendAsync( + HttpRequest.newBuilder(uri) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + custom) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + + promises.forEach((key, value) -> System.out.println(key + ":" + value.join().body())); + + promises.putIfAbsent(main.request(), CompletableFuture.completedFuture(main)); + promises.forEach((request, value) -> { + HttpResponse response = value.join(); + assertEquals(response.statusCode(), 200); + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + assertEquals(response.body(), PUSH_PROMISES.get(request.uri().getPath())); + } else { + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + int expectedPushes = Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + if (i == 0) expectedPushes--; // pushId == 1 was cancelled + assertEquals(promises.size(), expectedPushes); + + promises.clear(); + } + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3PushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body ->assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes * 3))) { + success = true; + } + if (success) { + System.out.println("Got expected IOException: " + io); + } else throw io; + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection + // so we can restart from scratch + pushHandler.reset(); + } + errors.forEach(t -> t.printStackTrace(System.out)); + var error = errors.stream().findFirst().orElse(null); + if (error != null) throw error; + assertEquals(notified.size(), 0, "Unexpected notification: " + notified); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + private final ReentrantLock lock = new ReentrantLock(); + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + final AtomicInteger count = new AtomicInteger(); + public void handle(HttpTestExchange exchange) throws IOException { + long count = -1; + lock.lock(); + try { + count = this.count.incrementAndGet(); + System.err.println("Server: handle " + exchange); + System.out.println("Server: handle " + exchange.getRequestURI()); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } catch (ClosedChannelException ex) { + System.out.printf("handling exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + System.out.printf("Got closed channel exception sending response after sent=%s allowed=%s%n", + sent, allowed); + } + } finally { + lock.unlock(); + System.out.printf("handled exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + } + } + + volatile long allowed = -1; + volatile int sent = 0; + void reset() { + lock.lock(); + try { + allowed = -1; + sent = 0; + } finally { + lock.unlock(); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long usePushId = exchange.getRequestHeaders() + .firstValueAsLong("X-UsePushId").orElse(-1); + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + System.err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + System.err.println("Server: Got maxPushId: " + allowed); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + for (Map.Entry promise : promises.entrySet()) { + // if usePushId != -1 we send a single push promise, + // without checking that's it's allowed. + // Otherwise, we stop sending promises when we have consumed + // the whole window + if (usePushId == -1 && allowed > 0 && sent >= allowed) { + System.err.println("Server: sent all allowed promises: " + sent); + break; + } + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + System.err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + System.err.println("Server: Got maxPushId: " + allowed); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + if (usePushId == -1) { + long pushId = exchange.http3ServerPush(uri, headers, headers, is); + System.err.println("Server: Sent push promise with response: " + pushId); + waitForPushId = pushId + 1; // assuming no concurrent requests... + sent += 1; + } else { + exchange.sendHttp3PushPromiseFrame(usePushId, uri, headers); + System.err.println("Server: Sent push promise frame: " + usePushId); + exchange.sendHttp3PushResponse(usePushId, uri, headers, headers, is); + System.err.println("Server: Sent push promise response: " + usePushId); + sent += 1; + return; + } + } + System.err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3QuicTLSConnection.java b/test/jdk/java/net/httpclient/http3/H3QuicTLSConnection.java new file mode 100644 index 00000000000..cd699e7774d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3QuicTLSConnection.java @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.UnsupportedProtocolVersionException; +import java.util.List; +import java.util.Optional; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.test.lib.net.SimpleSSLContext; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; + + +/* + * @test + * @summary verifies that the SSLParameters configured with specific cipher suites + * and TLS protocol versions gets used by the HttpClient for HTTP/3 + * @library /test/jdk/java/net/httpclient/lib /test/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @run main/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=all + * H3QuicTLSConnection + */ +public class H3QuicTLSConnection { + + private static final SSLParameters DEFAULT_SSL_PARAMETERS = new SSLParameters(); + + // expect highest supported version we know about + private static String expectedTLSVersion(SSLContext ctx) throws Exception { + if (ctx == null) { + ctx = SSLContext.getDefault(); + } + SSLParameters params = ctx.getSupportedSSLParameters(); + String[] protocols = params.getProtocols(); + for (String prot : protocols) { + if (prot.equals("TLSv1.3")) + return "TLSv1.3"; + } + return "TLSv1.2"; + } + + public static void main(String[] args) throws Exception { + // create and set the default SSLContext + SSLContext context = new SimpleSSLContext().get(); + SSLContext.setDefault(context); + + Handler handler = new Handler(); + + try (HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, SSLContext.getDefault())) { + server.addHandler(handler, "/"); + server.start(); + + String uriString = "https://" + server.serverAuthority(); + + // run test cases + boolean success = true; + + SSLParameters parameters = null; + success &= expectFailure( + "---\nTest #1: SSL parameters is null, expect NPE", + () -> connect(uriString, parameters), + NullPointerException.class, + Optional.empty()); + + success &= expectSuccess( + "---\nTest #2: default SSL parameters, " + + "expect successful connection", + () -> connect(uriString, DEFAULT_SSL_PARAMETERS)); + success &= checkProtocol(handler.getSSLSession(), expectedTLSVersion(null)); + + success &= expectFailure( + "---\nTest #3: SSL parameters with " + + "TLS_AES_128_GCM_SHA256 cipher suite, but TLSv1.2 " + + "expect UnsupportedProtocolVersionException", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_128_GCM_SHA256"}, + new String[]{"TLSv1.2"})), + UnsupportedProtocolVersionException.class, + Optional.empty()); + + // set TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 and expect it to fail since + // it's not supported with TLS v1.3 + success &= expectFailure( + "---\nTest #4: SSL parameters with " + + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 cipher suite, " + + "expect No appropriate protocol " + + "(protocol is disabled or cipher suites are inappropriate)", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}, + new String[]{"TLSv1.3"})), + SSLHandshakeException.class, + Optional.of("protocol is disabled or cipher suites are inappropriate")); + + // set TLS_AES_128_CCM_8_SHA256 cipher suite + // which is not supported by the (default) SunJSSE provider + // and forbidden in QUIC in general + success &= expectFailure( + "---\nTest #5: SSL parameters with " + + "TLS_AES_128_CCM_8_SHA256 cipher suite, " + + "expect IllegalArgumentException: Unsupported CipherSuite", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_128_CCM_8_SHA256"}, + new String[]{"TLSv1.3"})), + IllegalArgumentException.class, + Optional.of("Unsupported CipherSuite")); + + // set TLS_AES_128_GCM_SHA256 and TLS_AES_256_GCM_SHA384 cipher suite + var suites = List.of("TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256"); + success &= expectSuccess( + "---\nTest #6: SSL parameters with " + + "TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, " + + "and TLS_CHACHA20_POLY1305_SHA256 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + suites.toArray(new String[0]), + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), suites); + + // set TLS_AES_128_GCM_SHA256 cipher suite + success &= expectSuccess( + "---\nTest #7: SSL parameters with " + + "TLS_AES_128_GCM_SHA256 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_128_GCM_SHA256"}, + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), + "TLS_AES_128_GCM_SHA256"); + + // set TLS_AES_256_GCM_SHA384 cipher suite + success &= expectSuccess( + "---\nTest #8: SSL parameters with " + + "TLS_AES_256_GCM_SHA384 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_AES_256_GCM_SHA384"}, + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), + "TLS_AES_256_GCM_SHA384"); + + // set TLS_CHACHA20_POLY1305_SHA256 cipher suite + success &= expectSuccess( + "---\nTest #9: SSL parameters with " + + "TLS_CHACHA20_POLY1305_SHA256 cipher suites," + + " expect successful connection", + () -> connect(uriString, new SSLParameters( + new String[]{"TLS_CHACHA20_POLY1305_SHA256"}, + new String[]{"TLSv1.3"}))); + success &= checkProtocol(handler.getSSLSession(), "TLSv1.3"); + success &= checkCipherSuite(handler.getSSLSession(), + "TLS_CHACHA20_POLY1305_SHA256"); + + if (success) { + System.out.println("Test passed"); + } else { + throw new RuntimeException("At least one test case failed"); + } + } + } + + private interface Test { + void run() throws Exception; + } + + private static class Handler implements HttpTestHandler { + + private static final byte[] BODY = "Test response".getBytes(); + + private volatile SSLSession sslSession; + + @Override + public void handle(HttpTestExchange t) throws IOException { + System.out.println("Handler: received request to " + + t.getRequestURI()); + + try (InputStream is = t.getRequestBody()) { + byte[] body = is.readAllBytes(); + System.out.println("Handler: read " + body.length + + " bytes of body: "); + System.out.println(new String(body)); + } + + sslSession = t.getSSLSession(); + + try (OutputStream os = t.getResponseBody()) { + t.sendResponseHeaders(200, BODY.length); + os.write(BODY); + } + + } + + SSLSession getSSLSession() { + return sslSession; + } + } + + private static void connect(String uriString, SSLParameters sslParameters) + throws URISyntaxException, IOException, InterruptedException { + HttpClient.Builder builder = HttpServerAdapters.createClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(HttpClient.Version.HTTP_3); + if (sslParameters != DEFAULT_SSL_PARAMETERS) + builder.sslParameters(sslParameters); + try (final HttpClient client = builder.build()) { + HttpRequest request = HttpRequest.newBuilder(new URI(uriString)) + .POST(BodyPublishers.ofString("body")) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .version(Version.HTTP_3) + .build(); + String body = client.send(request, BodyHandlers.ofString()).body(); + System.out.println("Response: " + body); + } catch (UncheckedIOException uio) { + throw uio.getCause(); + } + } + + private static boolean checkProtocol(SSLSession session, String protocol) { + if (session == null) { + System.out.println("Check protocol: no session provided"); + return false; + } + + System.out.println("Check protocol: negotiated protocol: " + + session.getProtocol()); + System.out.println("Check protocol: expected protocol: " + + protocol); + if (!protocol.equals(session.getProtocol())) { + System.out.println("Check protocol: unexpected negotiated protocol"); + return false; + } + + return true; + } + + private static boolean checkCipherSuite(SSLSession session, String ciphersuite) { + if (session == null) { + System.out.println("Check protocol: no session provided"); + return false; + } + + System.out.println("Check protocol: negotiated ciphersuite: " + + session.getCipherSuite()); + System.out.println("Check protocol: expected ciphersuite: " + + ciphersuite); + if (!ciphersuite.equals(session.getCipherSuite())) { + System.out.println("Check protocol: unexpected negotiated ciphersuite"); + return false; + } + + return true; + } + + private static boolean checkCipherSuite(SSLSession session, List ciphersuites) { + if (session == null) { + System.out.println("Check protocol: no session provided"); + return false; + } + + System.out.println("Check protocol: negotiated ciphersuite: " + + session.getCipherSuite()); + System.out.println("Check protocol: expected ciphersuite in: " + + ciphersuites); + if (!ciphersuites.contains(session.getCipherSuite())) { + System.out.println("Check protocol: unexpected negotiated ciphersuite"); + return false; + } + + return true; + } + + private static boolean expectSuccess(String message, Test test) { + System.out.println(message); + try { + test.run(); + System.out.println("Passed"); + return true; + } catch (Exception e) { + System.out.println("Failed: unexpected exception:"); + e.printStackTrace(System.out); + return false; + } + } + + private static boolean expectFailure(String message, Test test, + Class expectedException, + Optional exceptionMsg) { + + System.out.println(message); + try { + test.run(); + System.out.println("Failed: unexpected successful connection"); + return false; + } catch (Exception e) { + System.out.println("Got an exception:"); + e.printStackTrace(System.out); + if (expectedException != null + && !expectedException.isAssignableFrom(e.getClass())) { + System.out.printf("Failed: expected %s, but got %s%n", + expectedException.getName(), + e.getClass().getName()); + return false; + } + if (exceptionMsg.isPresent()) { + final String actualMsg = e.getMessage(); + if (actualMsg == null || !actualMsg.contains(exceptionMsg.get())) { + System.out.printf("Failed: exception message was expected" + + " to contain \"%s\", but got \"%s\"%n", + exceptionMsg.get(), actualMsg); + return false; + } + } + System.out.println("Passed: expected exception"); + return true; + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3RedirectTest.java b/test/jdk/java/net/httpclient/http3/H3RedirectTest.java new file mode 100644 index 00000000000..4f6fbaeef6f --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3RedirectTest.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8156514 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm + * -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors + * -Djdk.internal.httpclient.debug=true + * H3RedirectTest + */ + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +public class H3RedirectTest implements HttpServerAdapters { + static int httpPort; + static SSLContext sslContext; + static HttpTestServer http3Server; + static HttpClient client; + + static String httpURIString, altURIString1, altURIString2; + static URI httpURI, altURI1, altURI2; + + static Supplier sup(String... args) { + Iterator i = Arrays.asList(args).iterator(); + // need to know when to stop calling it. + return i::next; + } + + static class Redirector extends HttpTestRedirectHandler { + private InetSocketAddress remoteAddr; + private boolean error = false; + + Redirector(Supplier supplier) { + super(supplier); + } + + @Override + protected synchronized void examineExchange(HttpTestExchange ex) { + InetSocketAddress addr = ex.getRemoteAddress(); + if (remoteAddr == null) { + remoteAddr = addr; + return; + } + // check that the client addr/port stays the same, proving + // that the connection didn't get dropped. + if (!remoteAddr.equals(addr)) { + System.err.printf("Error %s/%s\n", remoteAddr, + addr.toString()); + error = true; + } + } + + @Override + protected int redirectCode() { + return 308; // we need to use a code that preserves the body + } + + public synchronized boolean error() { + return error; + } + } + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + http3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + httpPort = http3Server.getAddress().getPort(); + String serverAuth = http3Server.serverAuthority(); + + // urls are accessed in sequence below. The first two are on + // different servers. Third on same server as second. So, the + // client should use the same http connection. + httpURIString = "https://" + serverAuth + "/foo/"; + httpURI = URI.create(httpURIString); + altURIString1 = "https://" + serverAuth + "/redir"; + altURI1 = URI.create(altURIString1); + altURIString2 = "https://" + serverAuth + "/redir_again"; + altURI2 = URI.create(altURIString2); + + Redirector r = new Redirector(sup(altURIString1, altURIString2)); + http3Server.addHandler(r, "/foo"); + http3Server.addHandler(r, "/redir"); + http3Server.addHandler(new HttpTestEchoHandler(), "/redir_again"); + + http3Server.start(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + @Test + public static void test() throws Exception { + try { + initialize(); + simpleTest(); + ReferenceTracker.INSTANCE.track(client); + client = null; + System.gc(); + var error = ReferenceTracker.INSTANCE.check(1500); + if (error != null) throw error; + } finally { + http3Server.stop(); + } + } + + static HttpClient getClient() { + if (client == null) { + client = HttpServerAdapters.createClientBuilderForH3() + .followRedirects(HttpClient.Redirect.ALWAYS) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI() { + return URI.create(httpURIString); + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkURIs(URI expected, URI found) throws Exception { + System.out.printf ("Expected: %s, Found: %s\n", expected, found); + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong URI %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void check(boolean cond, Object... msg) { + if (cond) + return; + StringBuilder sb = new StringBuilder(); + for (Object o : msg) + sb.append(o); + throw new RuntimeException(sb.toString()); + } + + static final String SIMPLE_STRING = "Hello world Goodbye world"; + + static void simpleTest() throws Exception { + URI uri = getURI(); + System.err.println("Request to " + uri); + + HttpClient client = getClient(); + HttpRequest req = HttpRequest.newBuilder(uri) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .POST(BodyPublishers.ofString(SIMPLE_STRING)) + .build(); + CompletableFuture> cf = client.sendAsync(req, BodyHandlers.ofString()); + HttpResponse response = cf.join(); + + checkStatus(200, response.statusCode()); + String responseBody = response.body(); + checkStrings(SIMPLE_STRING, responseBody); + checkURIs(response.uri(), altURI2); + + // check two previous responses + HttpResponse prev = response.previousResponse() + .orElseThrow(() -> new RuntimeException("no previous response")); + checkURIs(prev.uri(), altURI1); + + prev = prev.previousResponse() + .orElseThrow(() -> new RuntimeException("no previous response")); + checkURIs(prev.uri(), httpURI); + + checkPreviousRedirectResponses(req, response); + + System.err.println("DONE"); + } + + static void checkPreviousRedirectResponses(HttpRequest initialRequest, + HttpResponse finalResponse) { + // there must be at least one previous response + finalResponse.previousResponse() + .orElseThrow(() -> new RuntimeException("no previous response")); + + HttpResponse response = finalResponse; + do { + URI uri = response.uri(); + response = response.previousResponse().get(); + check(300 <= response.statusCode() && response.statusCode() <= 309, + "Expected 300 <= code <= 309, got:" + response.statusCode()); + check(response.body() == null, "Unexpected body: " + response.body()); + String locationHeader = response.headers().firstValue("Location") + .orElseThrow(() -> new RuntimeException("no previous Location")); + check(uri.toString().endsWith(locationHeader), + "URI: " + uri + ", Location: " + locationHeader); + } while (response.previousResponse().isPresent()); + + // initial + check(initialRequest.equals(response.request()), + "Expected initial request [%s] to equal last prev req [%s]", + initialRequest, response.request()); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPush.java b/test/jdk/java/net/httpclient/http3/H3ServerPush.java new file mode 100644 index 00000000000..4b271320b0d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPush.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8087112 8159814 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.httpclient.test.lib.http2.PushHandler + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm/timeout=960 + * -Djdk.httpclient.HttpClient.log=errors,requests,headers + * -Djdk.internal.httpclient.debug=false + * H3ServerPush + * @summary This is a clone of http2/ServerPush but for HTTP/3 + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.BodySubscribers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.PushHandler; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.test.lib.Utils.createTempFileOfSize; +import static org.testng.Assert.assertEquals; + +public class H3ServerPush implements HttpServerAdapters { + + private static final String CLASS_NAME = H3ServerPush.class.getSimpleName(); + + static final int LOOPS = 13; + static final int FILE_SIZE = 512 * 1024 + 343; + + static Path tempFile; + + HttpTestServer server; + URI uri; + URI headURI; + + @BeforeTest + public void setup() throws Exception { + tempFile = createTempFileOfSize(CLASS_NAME, ".dat", FILE_SIZE); + var sslContext = new SimpleSSLContext().get(); + var h2Server = new Http2TestServer(true, sslContext); + h2Server.enableH3AltServiceOnSamePort(); + h2Server.addHandler(new PushHandler(tempFile, LOOPS), "/foo/"); + System.out.println("Using temp file:" + tempFile); + server = HttpTestServer.of(h2Server); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + uri = new URI("https://" + server.serverAuthority() + "/foo/a/b/c"); + + System.err.println("Server listening at " + server.serverAuthority()); + server.start(); + + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + // Test 1 - custom written push promise handler, everything as a String + @Test + public void testTypeString() throws Exception { + System.out.println("\n**** testTypeString\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> + resultMap = new ConcurrentHashMap<>(); + + PushPromiseHandler pph = (initial, pushRequest, acceptor) -> { + BodyHandler s = BodyHandlers.ofString(UTF_8); + CompletableFuture> cf = acceptor.apply(s); + resultMap.put(pushRequest, cf); + }; + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET() + .build(); + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph); + resultMap.put(request, cf); + System.out.println("waiting for response"); + var resp = cf.join(); + assertEquals(resp.version(), Version.HTTP_3); + var seen = new HashSet<>(); + resultMap.forEach((k, v) -> { + if (seen.add(k)) { + System.out.println("Got " + v.join()); + } + }); + + // waiting for all promises to reach us + System.out.println("waiting for promises"); + System.out.println("results.size: " + resultMap.size()); + for (HttpRequest r : resultMap.keySet()) { + System.out.println("Checking " + r); + HttpResponse response = resultMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + assertEquals(response.body(), tempFileAsString); + } + resultMap.forEach((k, v) -> { + if (seen.add(k)) { + System.out.println("Got " + v.join()); + } + }); + assertEquals(resultMap.size(), LOOPS + 1); + } + } + + // Test 2 - of(...) populating the given Map, everything as a String + @Test + public void testTypeStringOfMap() throws Exception { + System.out.println("\n**** testTypeStringOfMap\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> + resultMap = new ConcurrentHashMap<>(); + + PushPromiseHandler pph = + PushPromiseHandler.of(pushPromise -> BodyHandlers.ofString(UTF_8), resultMap); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofString(UTF_8), pph); + cf.join(); + resultMap.put(request, cf); + System.err.println("results.size: " + resultMap.size()); + for (HttpRequest r : resultMap.keySet()) { + HttpResponse response = resultMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + assertEquals(response.body(), tempFileAsString); + } + assertEquals(resultMap.size(), LOOPS + 1); + } + } + + // --- Path --- + + static final Path dir = Paths.get(".", "serverPush"); + static BodyHandler requestToPath(HttpRequest req) { + URI u = req.uri(); + Path path = Paths.get(dir.toString(), u.getPath()); + try { + Files.createDirectories(path.getParent()); + } catch (IOException ee) { + throw new UncheckedIOException(ee); + } + return BodyHandlers.ofFile(path); + } + + // Test 3 - custom written push promise handler, everything as a Path + @Test + public void testTypePath() throws Exception { + System.out.println("\n**** testTypePath\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + + PushPromiseHandler pushPromiseHandler = (initial, pushRequest, acceptor) -> { + BodyHandler pp = requestToPath(pushRequest); + CompletableFuture> cf = acceptor.apply(pp); + resultsMap.put(pushRequest, cf); + }; + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + CompletableFuture> cf = + client.sendAsync(request, requestToPath(request), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + String fileAsString = Files.readString(response.body()); + assertEquals(fileAsString, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } + + // Test 4 - of(...) populating the given Map, everything as a Path + @Test + public void testTypePathOfMap() throws Exception { + System.out.println("\n**** testTypePathOfMap\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + + PushPromiseHandler pushPromiseHandler = + PushPromiseHandler.of(H3ServerPush::requestToPath, resultsMap); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + CompletableFuture> cf = + client.sendAsync(request, requestToPath(request), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + String fileAsString = Files.readString(response.body()); + assertEquals(fileAsString, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } + + // --- Consumer --- + + static class ByteArrayConsumer implements Consumer> { + volatile List listByteArrays = new ArrayList<>(); + volatile byte[] accumulatedBytes; + + public byte[] getAccumulatedBytes() { return accumulatedBytes; } + + @Override + public void accept(Optional optionalBytes) { + assert accumulatedBytes == null; + if (optionalBytes.isEmpty()) { + int size = listByteArrays.stream().mapToInt(ba -> ba.length).sum(); + ByteBuffer bb = ByteBuffer.allocate(size); + listByteArrays.forEach(bb::put); + accumulatedBytes = bb.array(); + } else { + listByteArrays.add(optionalBytes.get()); + } + } + } + + // Test 5 - custom written handler, everything as a consumer of optional byte[] + @Test + public void testTypeByteArrayConsumer() throws Exception { + System.out.println("\n**** testTypeByteArrayConsumer\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + Map byteArrayConsumerMap + = new ConcurrentHashMap<>(); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + ByteArrayConsumer bac = new ByteArrayConsumer(); + byteArrayConsumerMap.put(request, bac); + + PushPromiseHandler pushPromiseHandler = (initial, pushRequest, acceptor) -> { + CompletableFuture> cf = acceptor.apply( + (info) -> { + ByteArrayConsumer bc = new ByteArrayConsumer(); + byteArrayConsumerMap.put(pushRequest, bc); + return BodySubscribers.ofByteArrayConsumer(bc); + }); + resultsMap.put(pushRequest, cf); + }; + + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes(); + String result = new String(ba, UTF_8); + assertEquals(result, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } + + // Test 6 - of(...) populating the given Map, everything as a consumer of optional byte[] + @Test + public void testTypeByteArrayConsumerOfMap() throws Exception { + System.out.println("\n**** testTypeByteArrayConsumerOfMap\n"); + String tempFileAsString = Files.readString(tempFile); + ConcurrentMap>> resultsMap + = new ConcurrentHashMap<>(); + Map byteArrayConsumerMap + = new ConcurrentHashMap<>(); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(new SimpleSSLContext().get()) + .version(Version.HTTP_3).build()) { + sendHeadRequest(client); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + ByteArrayConsumer bac = new ByteArrayConsumer(); + byteArrayConsumerMap.put(request, bac); + + PushPromiseHandler pushPromiseHandler = + PushPromiseHandler.of( + pushRequest -> { + ByteArrayConsumer bc = new ByteArrayConsumer(); + byteArrayConsumerMap.put(pushRequest, bc); + return BodyHandlers.ofByteArrayConsumer(bc); + }, + resultsMap); + + CompletableFuture> cf = + client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(bac), pushPromiseHandler); + cf.join(); + resultsMap.put(request, cf); + for (HttpRequest r : resultsMap.keySet()) { + HttpResponse response = resultsMap.get(r).join(); + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), Version.HTTP_3); + byte[] ba = byteArrayConsumerMap.get(r).getAccumulatedBytes(); + String result = new String(ba, UTF_8); + assertEquals(result, tempFileAsString); + } + assertEquals(resultsMap.size(), LOOPS + 1); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPushCancel.java b/test/jdk/java/net/httpclient/http3/H3ServerPushCancel.java new file mode 100644 index 00000000000..32ca87e20f5 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPushCancel.java @@ -0,0 +1,607 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace + * -Djdk.httpclient.http3.maxConcurrentPushStreams=45 + * H3ServerPushCancel + * @summary This test checks that the client deals correctly with a + * CANCEL_PUSH frame sent by the server + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.net.http.HttpResponse.PushPromiseHandler.PushId; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.Utils; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class H3ServerPushCancel implements HttpServerAdapters { + + // dummy hack to prevent the IDE complaining that calling + // println will throw NPE + static final PrintStream err = System.err; + static final PrintStream out = System.out; + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eight push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + static final String MAIN_RESPONSE_BODY = "the main response body"; + static final int REQUESTS = 5; + + HttpTestServer server; + URI uri; + URI headURI; + ServerPushHandler pushHandler; + + @BeforeTest + public void setup() throws Exception { + server = HttpTestServer.create(ANY, new SimpleSSLContext().get()); + pushHandler = new ServerPushHandler(MAIN_RESPONSE_BODY, PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + server.start(); + err.println("Server listening on port " + server.serverAuthority()); + uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + } + + @AfterTest + public void teardown() { + server.stop(); + } + + static HttpResponse assert200ResponseCode(HttpResponse response) { + assertEquals(response.statusCode(), 200); + assertEquals(response.version(), HTTP_3); + return response; + } + + private void sendHeadRequest(HttpClient client) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), HTTP_2); + } + + static final class TestPushPromiseHandler implements PushPromiseHandler { + record NotifiedPromise(PushId pushId, HttpRequest initiatingRequest) {} + final Map requestToPushId = new ConcurrentHashMap<>(); + final Map pushIdToRequest = new ConcurrentHashMap<>(); + final List errors = new CopyOnWriteArrayList<>(); + final List notified = new CopyOnWriteArrayList<>(); + final ConcurrentMap>> promises + = new ConcurrentHashMap<>(); + final Supplier> bodyHandlerSupplier; + final PushPromiseHandler pph; + TestPushPromiseHandler(Supplier> bodyHandlerSupplier) { + this.bodyHandlerSupplier = bodyHandlerSupplier; + this.pph = PushPromiseHandler.of((r) -> bodyHandlerSupplier.get(), promises); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + errors.add(new AssertionError("no pushID provided for: " + pushPromiseRequest)); + } + + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushid) { + notified.add(new NotifiedPromise(pushid, initiatingRequest)); + out.println("notifyPushPromise: pushId=" + pushid); + pph.notifyAdditionalPromise(initiatingRequest, pushid); + } + + @Override + public void applyPushPromise(HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushid, + Function, CompletableFuture>> acceptor) { + out.println("applyPushPromise: " + pushPromiseRequest + ", pushId=" + pushid); + requestToPushId.putIfAbsent(pushPromiseRequest, pushid); + if (pushIdToRequest.putIfAbsent(pushid, pushPromiseRequest) != null) { + errors.add(new AssertionError("pushId already used: " + pushid)); + } + pph.applyPushPromise(initiatingRequest, pushPromiseRequest, pushid, acceptor); + } + + } + + T join(CompletableFuture cf) { + try { + return cf.join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } catch (Throwable t) { + throw new AssertionError(t); + } + } + + String describeBody(CompletableFuture> cf) { + return cf.thenApply(HttpResponse::body).exceptionally(Throwable::toString).join(); + } + + String describeKey(TestPushPromiseHandler pph, HttpRequest request) { + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + var pushId = pph.requestToPushId.get(request); + if (pushId instanceof Http3PushId h3id) { + return "[push=%s]".formatted(h3id.pushId()) + request; + } else return "[push=%s]".formatted(pushId) + request; + } else { + return "[main] " + request; + } + } + + @Test + public void testServerCancelPushes() throws Exception { + int maxPushes = Utils.getIntegerProperty("jdk.httpclient.http3.maxConcurrentPushStreams", -1); + out.println("maxPushes: " + maxPushes); + assertTrue(maxPushes > 0); + try (HttpClient client = newClientBuilderForH3() + .proxy(Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(new SimpleSSLContext().get()) + .build()) { + + sendHeadRequest(client); + + // Send with promise handler + TestPushPromiseHandler custom = new TestPushPromiseHandler<>(BodyHandlers::ofString); + var promises = custom.promises; + Set expectedPushIds = new HashSet<>(); + + for (int j=0; j < 2; j++) { + if (j == 0) out.println("\ntestCancel: First time around"); + else out.println("\ntestCancel: Second time around: should be a new connection"); + + // now make sure there's an HTTP/3 connection + client.send(HttpRequest.newBuilder(headURI).version(HTTP_3) + .setOption(H3_DISCOVERY, ALT_SVC) + .HEAD().build(), BodyHandlers.discarding()); + + int waitForPushId; + List>> responses = new ArrayList<>(); + for (int i = 0; i < REQUESTS; i++) { + waitForPushId = Math.min(PUSH_PROMISES.size(), maxPushes) + 1; + CompletableFuture> main = client.sendAsync( + HttpRequest.newBuilder(uri.resolve("?i=%s,j=%s".formatted(i, j))) + .header("X-WaitForPushId", String.valueOf(waitForPushId)) + .build(), + BodyHandlers.ofString(), + custom); + responses.add(main); + } + + join(CompletableFuture.allOf(responses.toArray(CompletableFuture[]::new))); + + responses.forEach(cf -> { + var main = join(cf); + var old = promises.put(main.request(), CompletableFuture.completedFuture(main)); + assertNull(old, "unexpected mapping for: " + old); + }); + + promises.forEach((key, value) -> out.println(describeKey(custom, key) + ":" + describeBody(value))); + + promises.forEach((request, value) -> { + if (PUSH_PROMISES.containsKey(request.uri().getPath())) { + var pushId = custom.requestToPushId.get(request); + assertNotNull(pushId, "no pushId for " + request); + if (pushId instanceof Http3PushId h3id) { + long id = h3id.pushId(); + long mod = id % PUSH_PROMISES.size(); + if (mod > 0 && mod < 4) { + // should have been cancelled by server, so + // we should have an IO for these + Throwable ex = value.exceptionNow(); + var msg = ex.getMessage(); + var expected = "Push promise cancelled: %s".formatted(id); + if (!(ex instanceof IOException)) { + throw new AssertionError(ex); + } else if (!msg.contains(expected)) { + throw new AssertionError("Unexpected message: " + msg, ex); + } + } else { + assertEquals(join(value).body(), PUSH_PROMISES.get(request.uri().getPath())); + } + expectedPushIds.add(pushId); + } else assertEquals(pushId.getClass(), Http3PushId.class); + } else { + HttpResponse response = join(value); + assertEquals(response.statusCode(), 200); + assertEquals(response.body(), MAIN_RESPONSE_BODY); + } + }); + + int maxExpectedPushes = Math.min(PUSH_PROMISES.size(), maxPushes); + int minExpectedPushes = maxExpectedPushes - 3; + int countpushes = promises.size() - REQUESTS; + assert countpushes >= 0; + if (maxExpectedPushes < countpushes || minExpectedPushes > countpushes) { + throw new AssertionError("unexpected number of pushes %s should be in [%s,%s]" + .formatted(countpushes, minExpectedPushes, maxExpectedPushes)); + } + + promises.clear(); + custom.requestToPushId.clear(); + + // Send with no promise handler + try { + client.sendAsync(HttpRequest.newBuilder(uri).build(), BodyHandlers.ofString()) + .thenApply(H3ServerPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + } catch (CompletionException c) { + throw new AssertionError(c.getCause()); + } + assertEquals(promises.size(), 0); + + // Send with no promise handler, but use pushId bigger than allowed. + // This should cause the connection to get closed + long usePushId = maxPushes * 3 + 10; + try { + HttpRequest bigger = HttpRequest.newBuilder(uri) + .header("X-UsePushId", String.valueOf(usePushId)) + .build(); + client.sendAsync(bigger, BodyHandlers.ofString()) + .thenApply(H3ServerPushCancel::assert200ResponseCode) + .thenApply(HttpResponse::body) + .thenAccept(body -> assertEquals(body, MAIN_RESPONSE_BODY)) + .join(); + throw new AssertionError("Expected IOException not thrown"); + } catch (CompletionException c) { + boolean success = false; + if (c.getCause() instanceof IOException io) { + if (io.getMessage() != null && + io.getMessage().contains("Max pushId exceeded (%s >= %s)" + .formatted(usePushId, maxPushes))) { + success = true; + } + if (success) { + out.println("Got expected IOException: " + io); + } else throw new AssertionError(io); + } + if (!success) { + throw new AssertionError("Unexpected exception: " + c.getCause(), c.getCause()); + } + } + assertEquals(promises.size(), 0); + + // the next time around we should have a new connection, + // so we can restart from scratch + pushHandler.reset(); + } + var errors = custom.errors; + errors.forEach(t -> t.printStackTrace(System.out)); + var error = errors.stream().findFirst().orElse(null); + if (error != null) throw error; + var notified = custom.notified; + int count = 0; + Set uniqueIds = new HashSet<>(); + Set cIds = new HashSet<>(); + for (var npp : notified) { + uniqueIds.add(npp.pushId); + if (npp.pushId instanceof Http3PushId h3id) { + long id = h3id.pushId(); + cIds.add(h3id.connectionLabel()); + long mod = id % PUSH_PROMISES.size(); + // we can't count the cancelled pushes as + // how many notifs we might get for those is racy. + if (mod == 0 || mod >= 4) { + // was not cancelled + count++; + } + } + } + + if (!uniqueIds.equals(expectedPushIds)) { + int problems = 0; + int missed = 0; + for (var id : uniqueIds) { + if (!expectedPushIds.contains(id)) { + problems++; + out.printf("%s was not expected%n", id); + } + } + for (var id : expectedPushIds) { + if (!uniqueIds.contains(id)) { + if (id instanceof Http3PushId h3id) { + long mod = h3id.pushId() % PUSH_PROMISES.size(); + if (mod > 0 && mod < 4) { + // this one was cancelled, so it might not + // have been notified + missed++; + continue; + } + } + problems++; + out.printf("%s was expected but not notified%n", id); + } + } + if (problems > 0) { + throw new AssertionError("%s unexpected problems with ids have been found" + .formatted(problems)); + } + } + // excluding those that got cancelled, + // we should have received REQUEST-1 notifications + // per push promise and per connection + assertEquals(count, (PUSH_PROMISES.size()-3)*2*(REQUESTS-1), + "Unexpected notification: " + notified); + } + } + + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + private final ReentrantLock lock = new ReentrantLock(); + private final Map sentPromises = new ConcurrentHashMap<>(); + record PendingPromise(long pushId, CountDownLatch latch) { + PendingPromise(long pushId) { + this(pushId, new CountDownLatch(REQUESTS)); + } + } + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + // The assumption is that there will be several concurrent + // exchanges, but all on the same connection + // The first exchange that emits a PushPromise sends + // a push promise frame + open the push response stream. + // The other exchanges will simply send a push promise + // frame, with the pushId allocated by the previous exchange. + // The sentPromises map is used to store that pushId. + // This obviously only works if we have a single HTTP/3 connection. + final AtomicInteger count = new AtomicInteger(); + public void handle(HttpTestExchange exchange) throws IOException { + long count = -1; + try { + count = this.count.incrementAndGet(); + err.println("Server: handle " + exchange + + " on " + exchange.getConnectionKey()); + out.println("Server: handle " + exchange.getRequestURI() + + " on " + exchange.getConnectionKey()); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } catch (ClosedChannelException ex) { + out.printf("handling exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + out.printf("Got closed channel exception sending response after sent=%s allowed=%s%n", + sent, allowed); + } + } finally { + out.printf("handled exchange %s, %s: %s%n", count, + exchange.getRequestURI(), exchange.getRequestHeaders()); + } + } + + volatile long allowed = -1; + volatile int sent = 0; + volatile int nsent = 0; + void reset() { + lock.lock(); + try { + allowed = -1; + sent = 0; + nsent = 0; + sentPromises.clear(); + } finally { + lock.unlock(); + } + } + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + long waitForPushId = exchange.getRequestHeaders() + .firstValueAsLong("X-WaitForPushId").orElse(-1); + long usePushId = exchange.getRequestHeaders() + .firstValueAsLong("X-UsePushId").orElse(-1); + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + for (Map.Entry promise : promises.entrySet()) { + // if usePushId != -1 we send a single push promise, + // without checking that it's allowed. + // Otherwise, we stop sending promises when we have consumed + // the whole window + if (usePushId == -1 && allowed > 0 && sent >= allowed) { + err.println("Server: sent all allowed promises: " + sent); + break; + } + + if (waitForPushId >= 0) { + while (allowed <= waitForPushId) { + try { + err.printf("Server: waiting for pushId sent=%s allowed=%s: %s%n", + sent, allowed, waitForPushId); + var allowed = exchange.waitForHttp3MaxPushId(waitForPushId); + err.println("Server: Got maxPushId: " + allowed); + out.println("Server: Got maxPushId: " + allowed); + lock.lock(); + if (allowed > this.allowed) this.allowed = allowed; + lock.unlock(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + HttpHeaders headers = HttpHeaders.of(Collections.emptyMap(), (x, y) -> true); + if (usePushId == -1) { + long pushId; + boolean send = false; + lock.lock(); + PendingPromise pendingPromise; + try { + pendingPromise = sentPromises.get(promise.getKey()); + if (pendingPromise == null) { + pushId = exchange.sendHttp3PushPromiseFrame(-1, uri, headers); + waitForPushId = pushId + 1; + pendingPromise = new PendingPromise(pushId); + sentPromises.put(promise.getKey(), pendingPromise); + sent += 1; + send = true; + } else { + pushId = pendingPromise.pushId; + exchange.sendHttp3PushPromiseFrame(pushId, uri, headers); + } + } finally { + lock.unlock(); + } + pendingPromise.latch.countDown(); + long mod = pushId % promises.size(); + if (send) { + if (mod == 1) { + // var stream = exchange.openPushIdStream(pushId); + // err.println("Server: Opened push stream: " + pushId); + } + if (mod > 0 && mod < 4) { + try { + pendingPromise.latch.await(); + } catch (InterruptedException e) { + throw new InterruptedIOException("" + e); + } + exchange.sendHttp3CancelPushFrame(pendingPromise.pushId); + err.println("Server: Cancelled push promise: " + pushId); + out.println("Server: Cancelled push promise: " + pushId); + } else { + exchange.sendHttp3PushResponse(pushId, uri, headers, headers, is); + out.println("Server: Sent push promise with response: " + pushId); + err.println("Server: Sent push promise with response: " + pushId); + } + } else { + err.println("Server: Sent push promise frame: " + pushId); + } + if (pushId >= waitForPushId) waitForPushId = pushId + 1; + } else { + exchange.sendHttp3PushPromiseFrame(usePushId, uri, headers); + err.println("Server: Sent push promise frame: " + usePushId); + exchange.sendHttp3PushResponse(usePushId, uri, headers, headers, is); + err.println("Server: Sent push promise response: " + usePushId); + lock.lock(); + sent += 1; + lock.unlock(); + return; + } + } + err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java b/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java new file mode 100644 index 00000000000..9b4858f50c9 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java @@ -0,0 +1,1224 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; + +import javax.net.ssl.SSLContext; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.PushPromiseHandler.PushId.Http3PushId; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/* + * @test + * @summary Verifies the HTTP/3 server push handling of the HTTP client + * @library /test/jdk/java/net/httpclient/lib + * /test/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.net.SimpleSSLContext + * @run junit H3ServerPushTest + */ + +/** + * Verifies the HTTP/3 server push handling of {@link HttpClient}. + * + * @implNote + * Some tests deliberately corrupt the HTTP/3 stream state. Hence, instead of + * creating a single {@link HttpTestServer}-{@link HttpClient} pair attached + * to the class, and sharing it across tests, each test creates its own + * server/client pair. + */ +@TestMethodOrder(OrderAnnotation.class) +class H3ServerPushTest { + + private static final HttpHeaders EMPTY_HEADERS = HttpHeaders.of(Map.of(), (_, _) -> false); + + private static final SSLContext SSL_CONTEXT = createSslContext(); + + private static SSLContext createSslContext() { + try { + return new SimpleSSLContext().get(); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + @Test + @Order(1) + void testBasicRequestResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler( + exchange -> { + try (exchange) { + exchange.sendResponseHeaders(200, 0); + } + }, + uri.getPath()); + + // Send the request and verify its response + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.discarding()); + assertEquals(200, response.statusCode()); + + } + } + + @Test + @Order(2) + void testTwoConsecutiveRequestsToSameServer(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler(new PushSender(), uri.getPath()); + + // Send the 1st request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response1 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 1st response + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + String connectionLabel = response1.connectionLabel().orElseThrow(); + final long initialPushId; + { + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response, since this is the very first request + ReceivedPush.Promise[] push3Ref = {null}; // 3. Push(initialPushId+1) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref, push3Ref); + initialPushId = push1Ref[0].pushId.pushId(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + assertEquals(initialPushId + 1, push3Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push3Ref[0].pushId.connectionLabel()); + } + + // Send the 2nd request + log("requesting `%s`...", request.uri()); + HttpResponse response2 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 2nd response + assertEquals(200, response2.statusCode()); + assertEquals("response1", response2.body()); + { + ReceivedPush.AdditionalPromise[] push1Ref = {null}; // 1. Push(initialPushId) additional promise + ReceivedPush.Promise[] push2Ref = {null}; // 2. Push(initialPushId+2) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref); + assertEquals(initialPushId, push1Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId + 2, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + } + + } + } + + @Test + @Order(3) + void testTwoConsecutiveRequestsToDifferentServers(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server1 = createServer(); + HttpTestServer server2 = createServer()) { + + // Configure the server handlers + URI uri1 = createUri(server1, testInfo); + server1.addHandler(new PushSender(), uri1.getPath()); + URI uri2 = createUri(server2, testInfo); + server2.addHandler(new PushSender(), uri2.getPath()); + + // Send a request to the 1st server + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request1 = createRequest(uri1); + log("requesting `%s`...", request1.uri()); + HttpResponse response1 = client + .sendAsync(request1, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response from the 1st server + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + String connectionLabel1 = response1.connectionLabel().orElseThrow(); + { + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response, since this is the very first request + ReceivedPush.Promise[] push3Ref = {null}; // 3. Push(initialPushId+1) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref, push3Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + assertEquals(connectionLabel1, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel1, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + assertEquals(initialPushId + 1, push3Ref[0].pushId.pushId()); + assertEquals(connectionLabel1, push3Ref[0].pushId.connectionLabel()); + } + + // Send a request to the 2nd server + HttpRequest request2 = createRequest(uri2); + log("requesting `%s`...", request2.uri()); + HttpResponse response2 = client + .sendAsync(request2, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response from the 2nd server + assertEquals(200, response2.statusCode()); + assertEquals("response0", response2.body()); + String connectionLabel2 = response2.connectionLabel().orElseThrow(); + { + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response, since this is the very first request + ReceivedPush.Promise[] push3Ref = {null}; // 3. Push(initialPushId+1) promise, the orphan one + pushReceiver.consume(push1Ref, push2Ref, push3Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + assertEquals(connectionLabel2, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel2, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + assertEquals(initialPushId + 1, push3Ref[0].pushId.pushId()); + assertEquals(connectionLabel2, push3Ref[0].pushId.connectionLabel()); + } + + // Verify that connection labels differ + assertNotEquals(connectionLabel1, connectionLabel2); + + } + } + + /** + * A server handler responding to all requests as follows: + *

          + *
        1. push(initialPushId) promise
        2. + *
        3. push(initialPushId) response: "pushResponse" + responseIndex,
          + * iff responseIndex == 0
        4. + *
        5. push(pushId++) promise (an orphan push promise)
        6. + *
        7. response: "response" + responseIndex
        8. + *
        + */ + private static final class PushSender implements HttpTestHandler { + + private int responseIndex = 0; + + private long initialPushId = -1; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Start with the push promise + assertTrue(exchange.serverPushAllowed()); + log(">>> sending push promise (responseIndex=%d, pushId=%d)", responseIndex, initialPushId); + long newInitialPushId = exchange.sendHttp3PushPromiseFrame( + initialPushId, + exchange.getRequestURI(), + EMPTY_HEADERS); + if (initialPushId != newInitialPushId) { + log(">>> updated initial pushId=%d (responseIndex=%d)", newInitialPushId, responseIndex); + initialPushId = newInitialPushId; + } + + // Send the push response iff it is the very first request + if (responseIndex == 0) { + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, initialPushId); + byte[] pushResponseBody = "pushResponse%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + initialPushId, + exchange.getRequestURI(), + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + + // Send the orphan push promise + log(">>> sending an orphan push promise (responseIndex=%d)", responseIndex); + long orphanPushId = exchange.sendHttp3PushPromiseFrame(-1, exchange.getRequestURI(), EMPTY_HEADERS); + log(">>> sent the orphan push promise (responseIndex=%d, pushId=%d)", responseIndex, orphanPushId); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + } + + @Test + @Order(4) + void testTwoPushPromisesWithSameIdInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + HttpTestHandler pushSender = new HttpTestHandler() { + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the 1st push promise and receive the push ID + log(">>> sending push promise (responseIndex=%d)", responseIndex); + long pushId = exchange.sendHttp3PushPromiseFrame(-1, uri, EMPTY_HEADERS); + + // Send the 2nd push promise using the same ID + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId); + exchange.sendHttp3PushPromiseFrame(pushId, uri, EMPTY_HEADERS); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.AdditionalPromise[] push2Ref = {null}; // 2. Push(initialPushId) promise, again + pushReceiver.consume(push1Ref, push2Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + String connectionLabel = response.connectionLabel().orElseThrow(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + + } + } + + @Test + @Order(5) + void testTwoPushPromisesWithSameIdButDifferentHeadersInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + CountDownLatch responseBodyWriteLatch = new CountDownLatch(1); + var pushSender = new HttpTestHandler() { + + private long pushId = -1; + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the 1st push promise and receive the push ID + log(">>> sending push promise (responseIndex=%d)", responseIndex); + pushId = exchange.sendHttp3PushPromiseFrame(pushId, uri, EMPTY_HEADERS); + + // Send the 2nd push promise using the same ID, but different headers + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId); + HttpHeaders nonEmptyHeaders = HttpHeaders.of(Map.of("Foo", List.of("Bar")), (_, _) -> true); + assertNotEquals(EMPTY_HEADERS, nonEmptyHeaders); + exchange.sendHttp3PushPromiseFrame(pushId, uri, nonEmptyHeaders); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + // Block to ensure the bad push stream is received before the response + awaitLatch(responseBodyWriteLatch); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + ExecutionException exception = assertThrows(ExecutionException.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + responseBodyWriteLatch.countDown(); + Throwable cause = exception.getCause(); + assertNotNull(cause); + assertInstanceOf(IOException.class, cause); + String actualMessage = cause.getMessage(); + String expectedMessage = "push headers do not match with previous promise for %d".formatted(pushSender.pushId); + assertEquals(expectedMessage, actualMessage); + + } + } + + @Test + @Order(6) + void testTwoPushPromisesWithSameIdInTwoResponsesOverOneConnection(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + HttpTestHandler pushSender = new HttpTestHandler() { + + private long pushId = -1; + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the push promise, and receive the push ID, if necessary + log(">>> sending push promise (responseIndex=%d, pushId=%d)", responseIndex, pushId); + pushId = exchange.sendHttp3PushPromiseFrame(pushId, uri, EMPTY_HEADERS); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the 1st request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response1 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 1st response + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + String connectionLabel = response1.connectionLabel().orElseThrow(); + final long initialPushId; + { + ReceivedPush.Promise[] pushRef = {null}; + pushReceiver.consume(pushRef); + initialPushId = pushRef[0].pushId.pushId(); + assertEquals(connectionLabel, pushRef[0].pushId.connectionLabel()); + } + + // Send the 2nd request + log("requesting `%s`...", request.uri()); + HttpResponse response2 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 2nd request + assertEquals(200, response2.statusCode()); + assertEquals("response1", response2.body()); + assertEquals(connectionLabel, response2.connectionLabel().orElseThrow()); + { + ReceivedPush.AdditionalPromise[] pushRef = {null}; + pushReceiver.consume(pushRef); + assertEquals(initialPushId, pushRef[0].pushId.pushId()); + assertEquals(connectionLabel, pushRef[0].pushId.connectionLabel()); + } + + } + } + + @Test + @Order(7) + void testTwoPushResponsesWithSameIdInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + long[] pushId = {-1}; + CountDownLatch responseBodyWriteLatch = new CountDownLatch(1); + HttpTestHandler pushSender = new HttpTestHandler() { + + private int responseIndex = 0; + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the 1st push promise and receive the push ID + log(">>> sending push promise (responseIndex=%d)", responseIndex); + pushId[0] = exchange.sendHttp3PushPromiseFrame(-1, uri, EMPTY_HEADERS); + + // Send two push responses + for (int trialIndex = 0; trialIndex < 2; trialIndex++) { + log( + ">>> sending push response (responseIndex=%d, pushId=%d, trialIndex=%d)", + responseIndex, pushId[0], trialIndex); + byte[] pushResponseBody = "pushResponse%d-%d" + .formatted(responseIndex, trialIndex) + .getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushId[0], + uri, + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + // Block to ensure the bad push stream is received before the response + awaitLatch(responseBodyWriteLatch); + exchange.getResponseBody().write(responseBody); + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + ExecutionException exception = assertThrows(ExecutionException.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + responseBodyWriteLatch.countDown(); + Throwable cause = exception.getCause(); + assertNotNull(cause); + assertInstanceOf(IOException.class, cause); + String actualMessage = cause.getMessage(); + String expectedMessage = "HTTP/3 pushId %d already used on this connection".formatted(pushId[0]); + assertEquals(expectedMessage, actualMessage); + } + } + + private static void awaitLatch(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); // Restore the interrupt + throw new RuntimeException(ie); + } + } + + @Test + @Order(8) + void testTwoPushResponsesWithSameIdInTwoResponsesOverOneConnection(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + long[] pushId = {-1}; + HttpTestHandler pushSender = new HttpTestHandler() { + + private int responseIndex = 0; + + private final Semaphore responseBodyWriteSem = new Semaphore(1); + + @Override + public synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + + // Send the push promise and receive the push ID, iff this is the very first response + if (responseIndex == 0) { + log(">>> sending push promise (responseIndex=%d)", responseIndex); + pushId[0] = exchange.sendHttp3PushPromiseFrame(-1, uri, EMPTY_HEADERS); + } + + // Send the push response + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId[0]); + byte[] pushResponseBody = "pushResponse%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushId[0], + uri, + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + + // Send the response + log(">>> sending response (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendResponseHeaders(200, responseBody.length); + try { + // The second request will block here, ensuring the + // bad push stream is received before the response. + responseBodyWriteSem.acquire(); + } catch (InterruptedException x) { + Thread.currentThread().interrupt(); + } + exchange.getResponseBody().write(responseBody); + + } finally { + responseIndex++; + } + } + + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the 1st request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response1 = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the 1st response + assertEquals(200, response1.statusCode()); + assertEquals("response0", response1.body()); + ReceivedPush.Promise[] push1Ref = {null}; // 1. Push(initialPushId) promise + ReceivedPush.Response[] push2Ref = {null}; // 2. Push(initialPushId) response + pushReceiver.consume(push1Ref, push2Ref); + long initialPushId = push1Ref[0].pushId.pushId(); + String connectionLabel = response1.connectionLabel().orElseThrow(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(initialPushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + + // Send the 2nd request and verify the failure + log("requesting `%s`...", request.uri()); + ExecutionException exception = assertThrows(ExecutionException.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + pushReceiver) + .get()); + Throwable cause = exception.getCause(); + assertNotNull(cause); + assertInstanceOf(IOException.class, cause); + String actualMessage = cause.getMessage(); + String expectedMessage = "HTTP/3 pushId %d already used on this connection".formatted(pushId[0]); + assertEquals(expectedMessage, actualMessage); + + } + } + + @Test + @Order(9) + void testPushPromiseBeforeHeader(TestInfo testInfo) throws Exception { + testPositionalPushPromise(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange); + sendHeaders(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(10) + void testPushPromiseAfterHeaderAndBeforeBody(TestInfo testInfo) throws Exception { + testPositionalPushPromise(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendPushPromise(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(11) + void testPushPromiseAfterBody(TestInfo testInfo) throws Exception { + testPositionalPushPromise(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendBody(exchange); + sendPushPromise(exchange); + } + }); + } + + private static void testPositionalPushPromise(TestInfo testInfo, PositionalPushSender pushSender) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush.Promise[] pushRef = {null}; + pushReceiver.consume(pushRef); + assertEquals(pushSender.pushId, pushRef[0].pushId.pushId()); + + } + } + + @Test + @Order(12) + void testPushPromiseAndResponseBeforeHeader(TestInfo testInfo) throws Exception { + testPositionalPushPromiseAndResponse(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange); + sendPushResponse(exchange); + sendHeaders(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(13) + void testPushPromiseAndResponseAfterHeaderAndBeforeBody(TestInfo testInfo) throws Exception { + testPositionalPushPromiseAndResponse(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendPushPromise(exchange); + sendPushResponse(exchange); + sendBody(exchange); + } + }); + } + + @Test + @Order(14) + void testPushPromiseAndResponseAfterBody(TestInfo testInfo) throws Exception { + testPositionalPushPromiseAndResponse(testInfo, new PositionalPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendHeaders(exchange); + sendBody(exchange); + sendPushPromise(exchange); + sendPushResponse(exchange); + } + }); + } + + private static void testPositionalPushPromiseAndResponse(TestInfo testInfo, PositionalPushSender pushSender) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush.Promise[] push1Ref = {null}; + ReceivedPush.Response[] push2Ref = {null}; + pushReceiver.consume(push1Ref, push2Ref); + assertEquals(pushSender.pushId, push1Ref[0].pushId.pushId()); + String connectionLabel = response.connectionLabel().orElseThrow(); + assertEquals(connectionLabel, push1Ref[0].pushId.connectionLabel()); + assertEquals(pushSender.pushId, push2Ref[0].pushId.pushId()); + assertEquals(connectionLabel, push2Ref[0].pushId.connectionLabel()); + assertEquals("pushResponse0", push2Ref[0].responseBody); + + } + } + + /** + * A server providing helper methods to send header, body, push promise & response. + * Subclasses can use these methods to inject custom server push behaviour at certain positions of the response assembly. + */ + private static abstract class PositionalPushSender implements HttpTestHandler { + + private long pushId = -1; + + private int responseIndex = 0; + + @Override + public final synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + handle0(exchange); + } finally { + responseIndex++; + } + } + + abstract void handle0(HttpTestExchange exchange) throws IOException; + + void sendHeaders(HttpTestExchange exchange) throws IOException { + log(">>> sending headers (responseIndex=%d)", responseIndex); + exchange.sendResponseHeaders( + 200, + // Use `-1` to avoid generating a single DataFrame. + // Otherwise, server closes the stream after writing + // the response body, and this makes it impossible to test + // server pushes delivered after the response body. + -1); + } + + void sendBody(HttpTestExchange exchange) throws IOException { + log(">>> sending body (responseIndex=%d)", responseIndex); + byte[] responseBody = "response%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.getResponseBody().write(responseBody); + } + + void sendPushResponse(HttpTestExchange exchange) throws IOException { + log(">>> sending push response (responseIndex=%d, pushId=%d)", responseIndex, pushId); + byte[] pushResponseBody = "pushResponse%d".formatted(responseIndex).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushId, + exchange.getRequestURI(), + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + + void sendPushPromise(HttpTestExchange exchange) throws IOException { + log(">>> sending push promise (responseIndex=%d, pushId=%d)", responseIndex, pushId); + pushId = exchange.sendHttp3PushPromiseFrame(pushId, exchange.getRequestURI(), EMPTY_HEADERS); + } + + } + + /** + * The maximum number of distinct push promise IDs allowed in a single response. + */ + private static final int MAX_ALLOWED_PUSH_ID_COUNT_PER_RESPONSE = 100; + + /** + * A value slightly more than {@link #MAX_ALLOWED_PUSH_ID_COUNT_PER_RESPONSE} to intentionally violate limits. + */ + private static final int EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE = + Math.addExact(10, MAX_ALLOWED_PUSH_ID_COUNT_PER_RESPONSE); + + @Test + @Order(15) + void testExcessivePushPromisesWithSameIdInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + int pushCount = EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE; + HttpTestHandler pushSender = new ManyPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange, pushCount, () -> pushId); + } + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request + PushReceiver pushReceiver = new PushReceiver(); + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + HttpResponse response = client + .sendAsync(request, HttpResponse.BodyHandlers.ofString(US_ASCII), pushReceiver) + .get(); + + // Verify the response + assertEquals(200, response.statusCode()); + assertEquals("response0", response.body()); + ReceivedPush[][] pushRefs = new ReceivedPush[pushCount][1]; + for (int i = 0; i < pushCount; i++) { + pushRefs[i] = i == 0 ? new ReceivedPush.Promise[1] : new ReceivedPush.AdditionalPromise[1]; + } + pushReceiver.consume(pushRefs); + long initialPushId = ((ReceivedPush.Promise[]) pushRefs[0])[0].pushId.pushId(); + for (int i = 1; i < pushCount; i++) { + assertEquals( + initialPushId, ((ReceivedPush.AdditionalPromise[]) pushRefs[i])[0].pushId.pushId(), + "push ID mismatch for received server push at index %d".formatted(i)); + } + + } + } + + @Test + @Order(16) + void testExcessivePushPromisesWithDistinctIdsInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + HttpTestHandler pushSender = new ManyPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + sendPushPromise(exchange, EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE, () -> -1L); + } + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + Exception exception = assertThrows(Exception.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + String exceptionMessage = exception.getMessage(); + assertTrue( + exceptionMessage.contains("Max pushId exceeded"), + "Unexpected exception message: `%s`".formatted(exceptionMessage)); + + } + } + + @Test + @Order(17) + void testExcessivePushResponsesWithDistinctIdsInOneResponse(TestInfo testInfo) throws Exception { + try (HttpClient client = createClient(); + HttpTestServer server = createServer()) { + + // Configure the server handler + URI uri = createUri(server, testInfo); + int pushCount = EXCESSIVE_PUSH_ID_COUNT_PER_RESPONSE; + HttpTestHandler pushSender = new ManyPushSender() { + @Override + void handle0(HttpTestExchange exchange) throws IOException { + long[] returnedPushIds = sendPushPromise(exchange, pushCount, () -> -1L); + Queue returnedPushIdQueue = Arrays + .stream(returnedPushIds) + .boxed() + .collect(Collectors.toCollection(LinkedList::new)); + sendPushResponse(exchange, pushCount, returnedPushIdQueue::poll); + } + }; + server.addHandler(pushSender, uri.getPath()); + + // Send the request and verify the failure + HttpRequest request = createRequest(uri); + log("requesting `%s`...", request.uri()); + Exception exception = assertThrows(Exception.class, () -> client + .sendAsync( + request, + HttpResponse.BodyHandlers.ofString(US_ASCII), + // Push receiver has no semantic purpose here, but provide logs to aid troubleshooting. + new PushReceiver()) + .get()); + String exceptionMessage = exception.getMessage(); + assertTrue( + exceptionMessage.contains("Max pushId exceeded"), + "Unexpected exception message: `%s`".formatted(exceptionMessage)); + + } + } + + /** + * A server providing helper methods subclasses can extend to send multiple push promises & responses. + */ + private static abstract class ManyPushSender implements HttpTestHandler { + + long pushId = -1; + + private int responseIndex = 0; + + @Override + public final synchronized void handle(HttpTestExchange exchange) throws IOException { + try (exchange) { + handle0(exchange); + sendHeaders(exchange); + sendBody(exchange); + } finally { + responseIndex++; + } + } + + abstract void handle0(HttpTestExchange exchange) throws IOException; + + private void sendHeaders(HttpTestExchange exchange) throws IOException { + log(">>> sending headers (responseIndex=%d)", responseIndex); + byte[] responseBody = responseBody(); + exchange.sendResponseHeaders(200, responseBody.length); + } + + private void sendBody(HttpTestExchange exchange) throws IOException { + log(">>> sending body (responseIndex=%d)", responseIndex); + byte[] responseBody = responseBody(); + exchange.getResponseBody().write(responseBody); + } + + private byte[] responseBody() { + return "response%d".formatted(responseIndex).getBytes(US_ASCII); + } + + long[] sendPushPromise(HttpTestExchange exchange, int count, Supplier pushIdProvider) throws IOException { + long[] returnedPushIds = new long[count]; + for (int i = 0; i < count; i++) { + long pushPromiseId = pushIdProvider.get(); + log( + ">>> sending push promise (responseIndex=%d, pushId=%d, i=%d/%d)", + responseIndex, pushPromiseId, i, count); + pushId = returnedPushIds[i] = + exchange.sendHttp3PushPromiseFrame(pushPromiseId, exchange.getRequestURI(), EMPTY_HEADERS); + } + return returnedPushIds; + } + + void sendPushResponse(HttpTestExchange exchange, int count, Supplier pushIdProvider) throws IOException { + for (int i = 0; i < count; i++) { + long pushResponseId = pushIdProvider.get(); + log( + ">>> sending push response (responseIndex=%d, pushId=%d, i=%d/%d)", + responseIndex, pushResponseId, i, count); + byte[] pushResponseBody = "pushResponse%d-%d".formatted(responseIndex, i).getBytes(US_ASCII); + exchange.sendHttp3PushResponse( + pushResponseId, + exchange.getRequestURI(), + EMPTY_HEADERS, + EMPTY_HEADERS, + new ByteArrayInputStream(pushResponseBody)); + } + } + + } + + private static URI createUri(HttpTestServer server, TestInfo testInfo) { + String uri = "https://%s/%s/%s".formatted( + server.serverAuthority(), + testInfo.getTestClass().map(Class::getSimpleName).orElse("UnknownClass"), + testInfo.getTestMethod().map(Method::getName).orElse("UnknownMethod")); + return URI.create(uri); + } + + private static HttpRequest createRequest(URI uri) { + return HttpRequest.newBuilder(uri).HEAD().setOption(H3_DISCOVERY, HTTP_3_URI_ONLY).build(); + } + + private static final class PushReceiver implements HttpResponse.PushPromiseHandler { + + private final BlockingQueue buffer = new LinkedBlockingQueue<>(); + + @Override + public void applyPushPromise( + HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + Function, CompletableFuture>> acceptor) { + fail("`applyPushPromise(...,PushId,...)` should have been called instead"); + } + + @Override + public void applyPushPromise( + HttpRequest initiatingRequest, + HttpRequest pushPromiseRequest, + PushId pushId, + Function, CompletableFuture>> acceptor) { + Http3PushId http3PushId = (Http3PushId) pushId; + buffer(new ReceivedPush.Promise(http3PushId)); + acceptor.apply(HttpResponse.BodyHandlers.ofString(US_ASCII)).thenAccept(response -> { + assertEquals(200, response.statusCode()); + String responseBody = response.body(); + buffer(new ReceivedPush.Response(http3PushId, responseBody)); + }); + } + + @Override + public void notifyAdditionalPromise(HttpRequest initiatingRequest, PushId pushId) { + Http3PushId http3PushId = (Http3PushId) pushId; + buffer(new ReceivedPush.AdditionalPromise(http3PushId)); + } + + private void buffer(ReceivedPush push) { + log("<<< received push: `%s`", push); + try { + buffer.put(push); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); // Restore the interrupt + throw new RuntimeException(ie); + } + } + + @SuppressWarnings("rawtypes") + private void consume(ReceivedPush[]... pushRefs) { + int n = pushRefs.length; + Class[] pushTypes = Arrays + .stream(pushRefs) + .map(pushRef -> pushRef.getClass().componentType()) + .toArray(Class[]::new); + boolean[] foundIndices = new boolean[n]; + for (int i = 0; i < n; i++) { + ReceivedPush push; + try { + push = buffer.take(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); // Restore the interrupt + throw new RuntimeException(ie); + } + boolean found = false; + for (int j = 0; j < n; j++) { + if (!foundIndices[j] && pushTypes[j].isInstance(push)) { + pushRefs[j][0] = push; + foundIndices[j] = true; + found = true; + break; + } + } + if (!found) { + log("pushRefs: %s", List.of(pushRefs)); + log("foundIndices: %s", List.of(foundIndices)); + log("n: %d", n); + log("i: %d", i); + log("push: %s", push); + fail("received push does not match with the expected types"); + } + } + } + + } + + private sealed interface ReceivedPush { + + record Promise(Http3PushId pushId) implements ReceivedPush {} + + record Response(Http3PushId pushId, String responseBody) implements ReceivedPush {} + + record AdditionalPromise(Http3PushId pushId) implements ReceivedPush {} + + } + + private static HttpTestServer createServer() throws IOException { + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, SSL_CONTEXT); + server.start(); + return server; + } + + private static HttpClient createClient() { + return HttpServerAdapters + .createClientBuilderFor(HTTP_3) + .proxy(NO_PROXY) + .version(HTTP_3) + .sslContext(SSL_CONTEXT) + .build(); + } + + private static void log(String format, Object... args) { + String text = format.formatted(args); + System.err.printf( + "%s [%25s] %s%n", + LocalTime.now(), + Thread.currentThread().getName(), + text); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPushWithDiffTypes.java b/test/jdk/java/net/httpclient/http3/H3ServerPushWithDiffTypes.java new file mode 100644 index 00000000000..af22651f0a0 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3ServerPushWithDiffTypes.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,responses + * H3ServerPushWithDiffTypes + * @summary This is a clone of http2/ServerPushWithDiffTypes but for HTTP/3 + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.BodySubscribers; +import java.net.http.HttpResponse.PushPromiseHandler; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Flow; +import java.util.function.BiPredicate; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; + +public class H3ServerPushWithDiffTypes implements HttpServerAdapters { + + static Map PUSH_PROMISES = Map.of( + "/x/y/z/1", "the first push promise body", + "/x/y/z/2", "the second push promise body", + "/x/y/z/3", "the third push promise body", + "/x/y/z/4", "the fourth push promise body", + "/x/y/z/5", "the fifth push promise body", + "/x/y/z/6", "the sixth push promise body", + "/x/y/z/7", "the seventh push promise body", + "/x/y/z/8", "the eighth push promise body", + "/x/y/z/9", "the ninth push promise body" + ); + + private void sendHeadRequest(HttpClient client, URI headURI) throws IOException, InterruptedException { + HttpRequest headRequest = HttpRequest.newBuilder(headURI) + .HEAD().version(Version.HTTP_2).build(); + var headResponse = client.send(headRequest, BodyHandlers.ofString()); + assertEquals(headResponse.statusCode(), 200); + assertEquals(headResponse.version(), Version.HTTP_2); + } + + @Test + public void test() throws Exception { + var sslContext = new SimpleSSLContext().get(); + try (HttpTestServer server = HttpTestServer.create(ANY, sslContext)) { + HttpTestHandler pushHandler = + new ServerPushHandler("the main response body", + PUSH_PROMISES); + server.addHandler(pushHandler, "/push/"); + server.addHandler(new HttpHeadOrGetHandler(), "/head/"); + + server.start(); + System.err.println("Server listening on port " + server.serverAuthority()); + + // use multi-level path + URI uri = new URI("https://" + server.serverAuthority() + "/push/a/b/c"); + URI headURI = new URI("https://" + server.serverAuthority() + "/head/x"); + + try (HttpClient client = newClientBuilderForH3().proxy(Builder.NO_PROXY) + .sslContext(sslContext).version(Version.HTTP_3).build()) { + + sendHeadRequest(client, headURI); + + HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); + + ConcurrentMap>>> + results = new ConcurrentHashMap<>(); + PushPromiseHandler> bh = PushPromiseHandler.of( + BodyAndTypeHandler::new, results); + + CompletableFuture>> cf = + client.sendAsync(request, new BodyAndTypeHandler(request), bh); + results.put(request, cf); + cf.join(); + + assertEquals(results.size(), PUSH_PROMISES.size() + 1); + + for (HttpRequest r : results.keySet()) { + URI u = r.uri(); + var resp = results.get(r).get(); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.version(), Version.HTTP_3); + BodyAndType body = resp.body(); + String result; + // convert all body types to String for easier comparison + if (body.type() == String.class) { + result = (String) body.body(); + } else if (body.type() == byte[].class) { + byte[] bytes = (byte[]) body.body(); + result = new String(bytes, UTF_8); + } else if (Path.class.isAssignableFrom(body.type())) { + Path path = (Path) body.body(); + result = Files.readString(path); + } else { + throw new AssertionError("Unknown:" + body.type()); + } + + System.err.printf("%s -> %s\n", u.toString(), result); + String expected = PUSH_PROMISES.get(r.uri().getPath()); + if (expected == null) + expected = "the main response body"; + assertEquals(result, expected); + } + } + } + } + + interface BodyAndType { + Class type(); + T body(); + } + + static final Path WORK_DIR = Paths.get("."); + + static class BodyAndTypeHandler implements BodyHandler> { + int count; + final HttpRequest request; + + BodyAndTypeHandler(HttpRequest request) { + this.request = request; + } + + @Override + @SuppressWarnings("rawtypes,unchecked") + public BodySubscriber> apply(HttpResponse.ResponseInfo info) { + int whichType = count++ % 3; // real world may base this on the request metadata + switch (whichType) { + case 0: // String + return new BodyAndTypeSubscriber(BodySubscribers.ofString(UTF_8)); + case 1: // byte[] + return new BodyAndTypeSubscriber(BodySubscribers.ofByteArray()); + case 2: // Path + URI u = request.uri(); + Path path = Paths.get(WORK_DIR.toString(), u.getPath()); + try { + Files.createDirectories(path.getParent()); + } catch (IOException ee) { + throw new UncheckedIOException(ee); + } + return new BodyAndTypeSubscriber(BodySubscribers.ofFile(path)); + default: + throw new AssertionError("Unexpected " + whichType); + } + } + } + + static class BodyAndTypeSubscriber + implements BodySubscriber> + { + private record BodyAndTypeImpl(Class type, T body) implements BodyAndType { } + + private final BodySubscriber bodySubscriber; + private final CompletableFuture> cf; + + @SuppressWarnings("unchecked") + BodyAndTypeSubscriber(BodySubscriber bodySubscriber) { + this.bodySubscriber = bodySubscriber; + cf = new CompletableFuture<>(); + bodySubscriber.getBody().whenComplete( + (r,t) -> cf.complete(new BodyAndTypeImpl<>((Class) r.getClass(), r))); + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + bodySubscriber.onSubscribe(subscription); + } + + @Override + public void onNext(List item) { + bodySubscriber.onNext(item); + } + + @Override + public void onError(Throwable throwable) { + bodySubscriber.onError(throwable); + cf.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + bodySubscriber.onComplete(); + } + + @Override + public CompletionStage> getBody() { + return cf; + } + } + + // --- server push handler --- + static class ServerPushHandler implements HttpTestHandler { + + private final String mainResponseBody; + private final Map promises; + + public ServerPushHandler(String mainResponseBody, + Map promises) + throws Exception + { + Objects.requireNonNull(promises); + this.mainResponseBody = mainResponseBody; + this.promises = promises; + } + + public void handle(HttpTestExchange exchange) throws IOException { + System.err.println("Server: handle " + exchange); + try (InputStream is = exchange.getRequestBody()) { + is.readAllBytes(); + } + + if (exchange.serverPushAllowed()) { + pushPromises(exchange); + } + + // response data for the main response + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = mainResponseBody.getBytes(UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } + + static final BiPredicate ACCEPT_ALL = (x, y) -> true; + + private void pushPromises(HttpTestExchange exchange) throws IOException { + URI requestURI = exchange.getRequestURI(); + for (Map.Entry promise : promises.entrySet()) { + URI uri = requestURI.resolve(promise.getKey()); + InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); + Map> map = Map.of("X-Promise", List.of(promise.getKey())); + HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL); + // TODO: add some check on headers, maybe + exchange.serverPush(uri, headers, is); + } + System.err.println("Server: All pushes sent"); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3SimpleGet.java b/test/jdk/java/net/httpclient/http3/H3SimpleGet.java new file mode 100644 index 00000000000..bccd77e7e1d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3SimpleGet.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test id=with-continuations + * @bug 8087112 + * @requires os.family != "windows" | ( os.name != "Windows 10" & os.name != "Windows Server 2016" + * & os.name != "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.httpclient.retryOnStreamlimit=20 + * -Djdk.httpclient.redirects.retrylimit=21 + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.retryOnStreamlimit=5 + * -Djdk.httpclient.redirects.retrylimit=6 + * -Djdk.httpclient.quic.defaultMTU=16336 + * H3SimpleGet + */ + +/* + * @test id=without-continuation + * @bug 8087112 + * @requires os.family == "windows" & ( os.name == "Windows 10" | os.name == "Windows Server 2016" + * | os.name == "Windows Server 2019" ) + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * -Djdk.httpclient.retryOnStreamlimit=20 + * -Djdk.httpclient.redirects.retrylimit=21 + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations + * -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.retryOnStreamlimit=5 + * -Djdk.httpclient.redirects.retrylimit=6 + * -Djdk.httpclient.quic.defaultMTU=16336 + * H3SimpleGet + */ + +/* + * @test id=useNioSelector + * @bug 8087112 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.http2.Http2TestServer + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.internal.httpclient.quic.useNioSelector=true + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.internal.httpclient.quic.useNioSelector=true + * -Djdk.httpclient.retryOnStreamlimit=20 + * -Djdk.httpclient.redirects.retrylimit=21 + * -Dsimpleget.repeat=1 -Dsimpleget.chunks=1 -Dsimpleget.requests=1000 + * H3SimpleGet + * @run testng/othervm/timeout=480 -XX:+HeapDumpOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError + * -Djdk.internal.httpclient.quic.useNioSelector=true + * -Dsimpleget.requests=150 + * -Dsimpleget.chunks=16384 + * -Djdk.httpclient.retryOnStreamlimit=5 + * -Djdk.httpclient.redirects.retrylimit=6 + * -Djdk.httpclient.quic.defaultMTU=16336 + * H3SimpleGet + */ + +// Interesting additional settings for debugging and manual testing: +// ----------------------------------------------------------------- +// -Djdk.httpclient.HttpClient.log=requests,errors,quic:retransmit:control,http3 +// -Djdk.httpclient.HttpClient.log=errors,requests,quic:all +// -Djdk.httpclient.quic.defaultMTU=64000 +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.httpclient.quic.defaultMTU=4096 +// -Djdk.httpclient.http3.maxStreamLimitTimeout=1375 +// -Xmx16g +// -Djdk.httpclient.quic.defaultMTU=16384 +// -Djdk.internal.httpclient.debug=err +// -XX:+HeapDumpOnOutOfMemoryError +// -Xmx768m -XX:MaxRAMPercentage=12.5 +// -Djdk.httpclient.HttpClient.log=errors,requests,http3 +// -Djdk.httpclient.HttpClient.log=errors,http3,quic:retransmit:control + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +public class H3SimpleGet implements HttpServerAdapters { + static HttpTestServer httpsServer; + static HttpClient client = null; + static SSLContext sslContext; + static String httpsURIString; + static ExecutorService serverExec = + Executors.newThreadPerTaskExecutor(Thread.ofVirtual() + .name("server-vt-worker-", 1).factory()); + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + httpsServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, serverExec); + httpsServer.addHandler(new TestHandler(), "/"); + httpsURIString = "https://" + httpsServer.serverAuthority() + "/bar/"; + + httpsServer.start(); + warmup(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(); + throw e; + } + } + + private static void warmup() throws Exception { + SimpleSSLContext sslct = new SimpleSSLContext(); + var sslContext = sslct.get(); + + // warmup server + try (var client2 = createClient(sslContext, Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("client-2-vt-worker", 1).factory()))) { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client2.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } + + // warmup client + var httpsServer2 = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext, + Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("server-2-vt-worker", 1).factory())); + httpsServer2.addHandler(new TestHandler(), "/"); + var httpsURIString2 = "https://" + httpsServer2.serverAuthority() + "/bar/"; + httpsServer2.start(); + try { + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString2)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .HEAD().build(); + client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + } finally { + httpsServer2.stop(); + } + } + + public static void main(String[] args) throws Exception { + test(); + } + + static volatile boolean waitBeforeTest = false; + + @Test + public static void test() throws Exception { + try { + if (waitBeforeTest) { + Thread.sleep(20000); + } + long prestart = System.nanoTime(); + initialize(); + long done = System.nanoTime(); + System.out.println("Stat: Initialization and warmup took " + + TimeUnit.NANOSECONDS.toMillis(done-prestart)+" millis"); + HttpRequest request = HttpRequest.newBuilder(URI.create(httpsURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET().build(); + long start = System.nanoTime(); + var resp = client.send(request, BodyHandlers.ofByteArrayConsumer(b-> {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("Stat: First request took: " + elapsed + + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = property("simpleget.requests", 50); + List>> list = new ArrayList<>(max); + Set connections = new ConcurrentSkipListSet<>(); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + var cf = client.sendAsync(request, BodyHandlers.ofByteArrayConsumer(b-> {})) + .whenComplete((r, t) -> Optional.ofNullable(r) + .flatMap(HttpResponse::connectionLabel) + .ifPresent(connections::add)); + list.add(cf); + //cf.get(); // uncomment to test with serial instead of concurrent requests + } + try { + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + } finally { + long elapsed2 = System.nanoTime() - start2; + long completed = list.stream().filter(CompletableFuture::isDone) + .filter(Predicate.not(CompletableFuture::isCompletedExceptionally)).count(); + connections.forEach(System.out::println); + if (completed > 0) { + System.out.println("Stat: Next " + completed + " requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + completed + " requests): " + + elapsed2 / completed + " nanos per request (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) / completed + " ms) on " + + connections.size() + " connections"); + } + } + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext, ExecutorService clientExec) { + var builder = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(Builder.NO_PROXY); + if (clientExec != null) { + builder = builder.executor(clientExec); + } + return builder.build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext, null); + } + return client; + } + + static int property(String name, int defaultValue) { + return Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))); + } + + // 32 * 32 * 1024 * 10 chars = 10Mb responses + // 50 requests => 500Mb + // 100 requests => 1Gb + // 1000 requests => 10Gb + private final static int REPEAT = property("simpleget.repeat", 32); + private final static String RESPONSE = "abcdefghij".repeat(property("simpleget.chunks", 1024*32)); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + try (var in = t.getRequestBody()) { + byte[] input = in.readAllBytes(); + t.sendResponseHeaders(200, RESPONSE_BYTES.length * REPEAT); + try (var out = t.getResponseBody()) { + if (t.getRequestMethod().equals("HEAD")) return; + for (int i=0; i {})); + Assert.assertEquals(resp.statusCode(), 200); + long elapsed = System.nanoTime() - start; + System.out.println("First GET request took: " + elapsed + " nanos (" + TimeUnit.NANOSECONDS.toMillis(elapsed) + " ms)"); + final int max = 50; + List>> list = new ArrayList<>(max); + long start2 = System.nanoTime(); + for (int i = 0; i < max; i++) { + list.add(client.sendAsync(postRequest, BodyHandlers.ofByteArrayConsumer(b -> { + }))); + } + CompletableFuture.allOf(list.toArray(new CompletableFuture[0])).join(); + long elapsed2 = System.nanoTime() - start2; + System.out.println("Next " + max + " POST requests took: " + elapsed2 + " nanos (" + + TimeUnit.NANOSECONDS.toMillis(elapsed2) + "ms for " + max + " requests): " + + elapsed2 / max + " nanos per request (" + TimeUnit.NANOSECONDS.toMillis(elapsed2) / max + " ms)"); + list.forEach((cf) -> Assert.assertEquals(cf.join().statusCode(), 200)); + } catch (Throwable tt) { + System.err.println("tt caught"); + tt.printStackTrace(); + throw tt; + } finally { + httpsServer.stop(); + } + } + + static HttpClient createClient(SSLContext sslContext, ExecutorService clientExec) { + var builder = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .proxy(Builder.NO_PROXY); + if (clientExec != null) { + builder = builder.executor(clientExec); + } + return builder.build(); + } + + static HttpClient getClient() { + if (client == null) { + client = createClient(sslContext, null); + } + return client; + } + + private final static int REPEAT = 32; + private final static String RESPONSE = "abcdefghij".repeat(1024*32); + private final static byte[] RESPONSE_BYTES = RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static class TestHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange t) throws IOException { + // consume all input bytes, + try (var in = t.getRequestBody()) { + in.skip(Integer.MAX_VALUE); + t.sendResponseHeaders(200, 0); + t.getResponseBody().close(); + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3SimpleTest.java b/test/jdk/java/net/httpclient/http3/H3SimpleTest.java new file mode 100644 index 00000000000..73c766f1eab --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3SimpleTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Basic test to verify that simple GET/POST/HEAD + * requests work as expected with HTTP/3, using IPv4 + * or IPv6 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * H3SimpleTest + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djava.net.preferIPv6Addresses=true + * H3SimpleTest + * @run testng/othervm + * -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors + * -Djava.net.preferIPv4Stack=true + * H3SimpleTest + */ +// -Djava.security.debug=all +public class H3SimpleTest implements HttpServerAdapters { + + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURI; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + // create an H3 only server + h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3Server.addHandler((exchange) -> exchange.sendResponseHeaders(200, 0), "/hello"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURI = "https://" + h3Server.serverAuthority() + "/hello"; + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + /** + * Issues various HTTP3 requests and verifies the responses are received + */ + @Test + public void testBasicRequests() throws Exception { + final HttpClient client = newClientBuilderForH3() + .proxy(NO_PROXY) + .version(HTTP_3) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURI); + final HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(reqURI) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); + + // GET + final HttpRequest req1 = reqBuilder.copy().GET().build(); + System.out.println("Issuing request: " + req1); + final HttpResponse resp1 = client.send(req1, BodyHandlers.discarding()); + Assert.assertEquals(resp1.statusCode(), 200, "unexpected response code for GET request"); + + // POST + final HttpRequest req2 = reqBuilder.copy().POST(BodyPublishers.ofString("foo")).build(); + System.out.println("Issuing request: " + req2); + final HttpResponse resp2 = client.send(req2, BodyHandlers.discarding()); + Assert.assertEquals(resp2.statusCode(), 200, "unexpected response code for POST request"); + + // HEAD + final HttpRequest req3 = reqBuilder.copy().HEAD().build(); + System.out.println("Issuing request: " + req3); + final HttpResponse resp3 = client.send(req3, BodyHandlers.discarding()); + Assert.assertEquals(resp3.statusCode(), 200, "unexpected response code for HEAD request"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3StopSendingTest.java b/test/jdk/java/net/httpclient/http3/H3StopSendingTest.java new file mode 100644 index 00000000000..1855cdba6ff --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3StopSendingTest.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Verifies that the client reacts correctly to the receipt of a STOP_SENDING frame. + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm/timeout=40 -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=trace,errors,headers + * H3StopSendingTest + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.internal.net.http.http3.Http3Error; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.concurrent.ExecutionException; + +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.testng.Assert.*; + +public class H3StopSendingTest { + + HttpTestServer h3TestServer; + HttpRequest postRequestNoError, postRequestError; + HttpRequest postRequestNoErrorWithData, postRequestErrorWithData; + URI h3TestServerUriNoError, h3TestServerUriError; + SSLContext sslContext; + + static final String TEST_ROOT_PATH = "/h3_stop_sending_test"; + static final String NO_ERROR_PATH = TEST_ROOT_PATH + "/no_error_path"; + static final String ERROR_PATH = TEST_ROOT_PATH + "/error_path"; + static final String WITH_RESPONSE_BODY_QUERY = "?withbody"; + + static final PrintStream err = System.err; + + + @Test + public void test() throws ExecutionException, InterruptedException { + HttpClient.Builder clientBuilder = HttpServerAdapters.createClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext); + try (HttpClient client = clientBuilder.build()) { + HttpResponse resp = client.sendAsync(postRequestNoError, HttpResponse.BodyHandlers.ofString()).get(); + err.println(resp.headers()); + err.println(resp.body()); + err.println(resp.statusCode()); + assertEquals(resp.statusCode(), 200); + resp = client.sendAsync(postRequestNoErrorWithData, HttpResponse.BodyHandlers.ofString()).get(); + err.println(resp.headers()); + err.println(resp.body()); + err.println(resp.statusCode()); + assertEquals(resp.statusCode(), 200); + assertEquals(resp.body(), RESPONSE_MESSAGE.repeat(MESSAGE_REPEAT)); + } + } + + @Test + public void testError() { + Throwable caught = null; + HttpClient.Builder clientBuilder = HttpServerAdapters.createClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext); + try (HttpClient client = clientBuilder.build()) { + try { + client.sendAsync(postRequestError, HttpResponse.BodyHandlers.ofString()).get(); + } catch (Throwable throwable) { + caught = throwable; + } + assertRequestCancelled(caught); + try { + client.sendAsync(postRequestErrorWithData, HttpResponse.BodyHandlers.ofString()).get(); + } catch (Throwable throwable) { + caught = throwable; + } + assertRequestCancelled(caught); + } + } + + private static void assertRequestCancelled(Throwable caught) { + assertNotNull(caught); + if (!caught.getMessage().contains(Long.toString(Http3Error.H3_REQUEST_CANCELLED.code())) + && !caught.getMessage().contains(Http3Error.H3_REQUEST_CANCELLED.name())) { + throw new AssertionError(caught.getMessage() + " does not contain " + + Http3Error.H3_REQUEST_CANCELLED.code() + " or " + + Http3Error.H3_REQUEST_CANCELLED.name(), caught); + } else { + System.out.println("Got expected exception: " + caught); + } + } + + @BeforeTest + public void setup() throws IOException { + + sslContext = new SimpleSSLContext().get(); + h3TestServer = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3TestServer.addHandler(new ServerRequestStopSendingHandler(), TEST_ROOT_PATH); + h3TestServerUriError = URI.create("https://" + h3TestServer.serverAuthority() + ERROR_PATH); + h3TestServerUriNoError = URI.create("https://" + h3TestServer.serverAuthority() + NO_ERROR_PATH); + h3TestServer.start(); + + Iterable iterable = EndlessDataChunks::new; + HttpRequest.BodyPublisher testPub = HttpRequest.BodyPublishers.ofByteArrays(iterable); + postRequestNoError = HttpRequest.newBuilder() + .POST(testPub) + .uri(h3TestServerUriNoError) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + postRequestError = HttpRequest.newBuilder() + .POST(testPub) + .uri(h3TestServerUriError) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + postRequestNoErrorWithData = HttpRequest.newBuilder() + .POST(testPub) + .uri(URI.create(h3TestServerUriNoError.toString() + WITH_RESPONSE_BODY_QUERY)) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + postRequestErrorWithData = HttpRequest.newBuilder() + .POST(testPub) + .uri(URI.create(h3TestServerUriError.toString() + WITH_RESPONSE_BODY_QUERY)) + .version(HttpClient.Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + } + + @AfterTest + public void afterTest() { + h3TestServer.stop(); + } + + static final String RESPONSE_MESSAGE = "May the road rise up to meet you "; + static final String ERROR_MESSAGE = "Forbidden: the data won't be sent "; + static final String NO_BODY = ""; + + static final int MESSAGE_REPEAT = 5; + + static class ServerRequestStopSendingHandler implements HttpTestHandler { + + @Override + public void handle(HttpServerAdapters.HttpTestExchange t) throws IOException { + String query = t.getRequestURI().getQuery(); + System.out.println("Query is: " + query); + boolean withData = WITH_RESPONSE_BODY_QUERY.substring(1).equals(query); + switch (t.getRequestURI().getPath()) { + case NO_ERROR_PATH -> { + final String RESP = withData ? RESPONSE_MESSAGE : NO_BODY; + byte[] resp = RESP.getBytes(StandardCharsets.UTF_8); + System.err.println("Replying with 200 to " + t.getRequestURI().getPath() + + " with data " + resp.length * MESSAGE_REPEAT); + t.sendResponseHeaders(200, resp.length * MESSAGE_REPEAT); + t.requestStopSending(Http3Error.H3_NO_ERROR.code()); + if (resp.length == 0) return; + try (var os = t.getResponseBody()) { + for (int i = 0; i { + final String RESP = withData ? ERROR_MESSAGE : NO_BODY; + byte[] resp = RESP.getBytes(StandardCharsets.UTF_8); + System.err.println("Replying with 403 to " + t.getRequestURI().getPath() + + " with data " + resp.length * MESSAGE_REPEAT); + if (resp.length == 0) { + t.requestStopSending(Http3Error.H3_EXCESSIVE_LOAD.code()); + sleep(100); + t.resetStream(Http3Error.H3_REQUEST_CANCELLED.code()); + } else { + t.sendResponseHeaders(403, resp.length * MESSAGE_REPEAT); + t.requestStopSending(Http3Error.H3_EXCESSIVE_LOAD.code()); + try (var os = t.getResponseBody()) { + for (int i = 0; i < MESSAGE_REPEAT; i++) { + os.write(resp); + os.flush(); + sleep(10); + if (i == MESSAGE_REPEAT - 1) { + t.resetStream(Http3Error.H3_REQUEST_CANCELLED.code()); + } + } + } + } + } + } + } + } + + private static void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + + static public class EndlessDataChunks implements Iterator { + + byte[] data = new byte[32]; + boolean hasNext = true; + @Override + public boolean hasNext() { + return hasNext; + } + @Override + public byte[] next() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return data; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3StreamLimitReachedTest.java b/test/jdk/java/net/httpclient/http3/H3StreamLimitReachedTest.java new file mode 100644 index 00000000000..916a6a5221d --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3StreamLimitReachedTest.java @@ -0,0 +1,971 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test id=with-default-wait + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors,http3,quic:control + * -Djdk.internal.httpclient.debug=false + * -Djdk.httpclient.quic.maxBidiStreams=1 + * H3StreamLimitReachedTest + */ + +/* + * @test id=with-no-wait + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.http2.Http2TestServer + * jdk.test.lib.Asserts + * jdk.test.lib.Utils + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors,http3,quic:control + * -Djdk.internal.httpclient.debug=false + * -Djdk.httpclient.quic.maxBidiStreams=1 + * -Djdk.httpclient.http3.maxStreamLimitTimeout=0 + * -Djdk.httpclient.retryOnStreamlimit=9 + * H3StreamLimitReachedTest + */ + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpOption; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertNotEquals; +import static jdk.test.lib.Asserts.assertTrue; +import static org.testng.Assert.assertFalse; + +public class H3StreamLimitReachedTest implements HttpServerAdapters { + + private static final String CLASS_NAME = H3ConnectionPoolTest.class.getSimpleName(); + + static int altsvcPort, https2Port, http3Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static volatile HttpClient client = null; + static SSLContext sslContext; + static volatile String http3OnlyURIString, https2URIString, http3AltSvcURIString, http3DirectURIString; + + static void initialize(boolean samePort) throws Exception { + BlockingHandler.GATE.drainPermits(); + BlockingHandler.IN_HANDLER.drainPermits(); + BlockingHandler.PERMITS.set(0); + initialize(samePort, BlockingHandler::new); + } + + static void initialize(boolean samePort, Supplier handlers) throws Exception { + System.out.println("\nConfiguring for advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = null; + client = getClient(); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, sslContext); + if (samePort) { + System.out.println("Attempting to enable advertised HTTP/3 service on same port"); + https2AltSvcServer.enableH3AltServiceOnSamePort(); + System.out.println("Advertised AltSvc on same port " + + (https2AltSvcServer.supportsH3DirectConnection() ? "enabled" : " not enabled")); + } else { + System.out.println("Attempting to enable advertised HTTP/3 service on different port"); + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/https2/"); + https2AltSvcServer.addHandler(handlers.get(), "/" + CLASS_NAME + "/h2h3/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + altsvcPort = https2AltSvcServer.getH3AltService() + .map(Http3TestServer::getAddress).stream() + .mapToInt(InetSocketAddress::getPort).findFirst() + .getAsInt(); + // server that only supports HTTP/3 - we attempt to use the same port + // as the HTTP/2 server so that we can pretend that the H2 server as two H3 endpoints: + // one advertised (the alt service endpoint og the HTTP/2 server) + // one non advertised (the direct endpoint, at the same authority as HTTP/2, but which + // is in fact our http3OnlyServer) + try { + http3OnlyServer = new Http3TestServer(sslContext, samePort ? 0 : https2Port); + System.out.println("Unadvertised service enabled on " + + (samePort ? "ephemeral port" : "same port")); + } catch (IOException ex) { + System.out.println("Can't create HTTP/3 server on same port: " + ex); + http3OnlyServer = new Http3TestServer(sslContext, 0); + } + http3OnlyServer.addHandler("/" + CLASS_NAME + "/http3/", handlers.get()); + http3OnlyServer.addHandler("/" + CLASS_NAME + "/h2h3/", handlers.get()); + http3OnlyServer.start(); + http3Port = http3OnlyServer.getQuicServer().getAddress().getPort(); + + if (http3Port == https2Port) { + System.out.println("HTTP/3 server enabled on same port than HTTP/2 server"); + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for HTTP/3 server"); + } + } else { + System.out.println("HTTP/3 server enabled on a different port than HTTP/2 server"); + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for HTTP/3 server"); + } + } + if (altsvcPort == https2Port) { + if (!samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used same port for advertised AltSvc"); + } + } else { + if (samePort) { + System.out.println("WARNING: configuration could not be respected," + + " should have used ephemeral port for advertised AltSvc"); + } + } + + http3OnlyURIString = "https://" + http3OnlyServer.serverAuthority() + "/" + CLASS_NAME + "/http3/foo/"; + https2URIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/https2/bar/"; + http3DirectURIString = "https://" + https2AltSvcServer.serverAuthority() + "/" + CLASS_NAME + "/h2h3/direct/"; + http3AltSvcURIString = https2URIString + .replace(":" + https2Port + "/", ":" + altsvcPort + "/") + .replace("/https2/bar/", "/h2h3/altsvc/"); + System.out.println("HTTP/2 server started at: " + https2AltSvcServer.serverAuthority()); + System.out.println(" with advertised HTTP/3 endpoint at: " + + URI.create(http3AltSvcURIString).getRawAuthority()); + System.out.println("HTTP/3 server started at:" + http3OnlyServer.serverAuthority()); + + https2AltSvcServer.start(); + } catch (Throwable e) { + System.out.println("Configuration failed: " + e); + System.err.println("Throwing now: " + e); + e.printStackTrace(); + throw e; + } + } + + static class BlockingHandler implements Http2Handler { + static final AtomicLong REQCOUNT = new AtomicLong(); + static final Semaphore IN_HANDLER = new Semaphore(0); + static final Semaphore GATE = new Semaphore(0); + static final AtomicInteger PERMITS = new AtomicInteger(); + + static void acquireGate() throws InterruptedException { + GATE.acquire(); + System.out.println("GATE acquired: remaining permits: " + + PERMITS.decrementAndGet() + + " (actual: " + GATE.availablePermits() +")"); + } + static void releaseGate() throws InterruptedException { + int permits = PERMITS.incrementAndGet(); + GATE.release(); + System.out.println("GATE released: remaining permits: " + + permits + " (actual: " + GATE.availablePermits() +")"); + } + + static void releaseGate(int permits) throws InterruptedException { + int npermits = PERMITS.addAndGet(permits); + GATE.release(npermits); + System.out.println("GATE released (" + permits + "): remaining permits: " + + npermits + " (actual: " + GATE.availablePermits() +")"); + } + + @Override + public void handle(Http2TestExchange t) throws IOException { + long count = REQCOUNT.incrementAndGet(); + byte[] resp; + int status; + try { + try { + IN_HANDLER.release(); + System.out.printf("*** Server [%s] waiting for GATE: %s%n", + count, t.getRequestURI()); + System.err.printf("*** Server [%s] waiting for GATE: %s%n", + count, t.getRequestURI()); + acquireGate(); + System.err.printf("*** Server [%s] GATE acquired: %s%n", + count, t.getRequestURI()); + status = 200; + resp = "Request %s OK".formatted(count) + .getBytes(StandardCharsets.UTF_8); + } catch (InterruptedException x) { + status = 500; + resp = "Request %s interrupted: %s" + .formatted(count, x) + .getBytes(StandardCharsets.UTF_8); + } + System.out.printf("*** Server [%s] headers for: %s%n\t%s%n", + count, t.getRequestURI(), t.getRequestHeaders()); + System.out.printf("*** Server [%s] reading body for: %s%n", + count, t.getRequestURI()); + t.getRequestBody().readAllBytes(); + System.out.printf("*** Server [%s] sending headers for: %s%n", + count, t.getRequestURI()); + t.sendResponseHeaders(status, resp.length); + System.out.printf("*** Server [%s] sending body %s (%s bytes) for: %s%n", + count, status, resp.length, t.getRequestURI()); + try (var body = t.getResponseBody()) { + body.write(resp); + } + System.out.printf("*** Server [%s] response %s sent for: %s%n", + count, status, t.getRequestURI()); + } catch (Throwable throwable) { + var msg = String.format("Server [%s] response failed for: %s", + count, t.getRequestURI()); + System.out.printf("*** %s%n\t%s%n", msg, throwable); + var error = new IOException(msg,throwable); + error.printStackTrace(System.out); + System.err.printf("*** %s%n\t%s%n", msg, throwable); + //GATE.release(); + throw error; + } + } + } + + private static void printResponse(String name, + HttpOption option, + HttpResponse response) { + printResponse("%s %s".formatted( + name, response.request().getOption(option)), + response); + } + + private static void printResponse(String name, HttpResponse response) { + System.out.printf("%s: (%s): %s%n", + name, response.connectionLabel(), response); + response.headers().map().entrySet().forEach((e) -> { + System.out.printf(" %s: %s%n", e.getKey(), e.getValue()); + }); + System.out.printf(" :body: \"%s\"%n%n", response.body()); + } + + @Test + public static void testH3Only() throws Exception { + System.out.println("\nTesting HTTP/3 only"); + initialize(true); + try (HttpClient client = getClient()) { + var reqBuilder = HttpRequest.newBuilder() + .uri(URI.create(http3OnlyURIString)) + .version(HTTP_3) + .GET(); + HttpRequest request1 = reqBuilder.copy() + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + var responseCF1 = client.sendAsync(request1, BodyHandlers.ofString()); + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // ANY should reuse the same connection, get stream limit reached, + // and open a new connection + HttpRequest request2 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // release both + BlockingHandler.GATE.release(2); + + var response1 = responseCF1.get(); + printResponse("First response", response1); + var response2 = responseCF2.get(); + printResponse("Second response", response2); + + // set a timeout to make sure we wait long enough for + // the MAX_STREAMS update to reach us before attempting + // to create the HTTP/3 exchange. + HttpRequest request3 = reqBuilder.copy() + .timeout(Duration.ofSeconds(30)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + // request3 should now reuse the connection opened by request2 + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // ANY should reuse the same connection as request3, + // get stream limit exception, and open a new connection + HttpRequest request4 = reqBuilder.copy() + .setOption(H3_DISCOVERY, ANY) + .build(); + var responseCF4 = client.sendAsync(request4, BodyHandlers.ofString()); + // wait until blocked in the handler + BlockingHandler.IN_HANDLER.acquire(); + + // release both response 3 and response 4 + BlockingHandler.GATE.release(2); + + var response3 = responseCF3.get(); + printResponse("Third response", response3); + var response4 = responseCF4.get(); + printResponse("Fourth response", response4); + + assertNotEquals(response1.connectionLabel().get(), response2.connectionLabel().get()); + assertEquals(response2.connectionLabel().get(), response3.connectionLabel().get()); + assertNotEquals(response1.connectionLabel().get(), response4.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), response4.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response4.connectionLabel().get()); + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testH2H3WithTwoAltSVC() throws Exception { + testH2H3(false); + } + + @Test + public static void testH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3(true); + } + + private static void testH2H3(boolean samePort) throws Exception { + System.out.println("\nTesting with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + var responseCF1 = client.sendAsync(request1, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + var h2resp2CF = client.sendAsync(request2, BodyHandlers.ofString()); + // Blocking until the handler is invoked should be enough + // to ensure we get the AltSvc frame before the next request. + BlockingHandler.IN_HANDLER.acquire(); + BlockingHandler.GATE.release(3); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + // There's still a potential race here - to avoid it + // we need h2resp2 = h2resp2CF.get(); before sending request2 + var h2resp2 = h2resp2CF.get(); + System.out.printf("Got expected h2 response: %s [%s]%n", + h2resp2, h2resp2.version()); + + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + + var response1 = responseCF1.get(); + printResponse("response 1", H3_DISCOVERY, response1); + var response2 = responseCF2.get(); + printResponse("response 2", H3_DISCOVERY, response2); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // second request with HTTP3_URI_ONLY should reuse a created connection + // It should reuse the advertised connection (from response2) if same + // origin + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request3 = req1Builder.copy() + .timeout(Duration.ofSeconds(30)).build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + // It should not reuse the connection from response1, it should + // not invoke reconnection on the connection used by response 3 + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request4 = req2Builder.copy() + .timeout(Duration.ofSeconds(30)).build(); + var responseCF4 = client.sendAsync(request4, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // release both + BlockingHandler.GATE.release(2); + + var response3 = responseCF3.get(); + printResponse("response 3", H3_DISCOVERY, response3); + var response4 = responseCF4.get(); + printResponse("response 4", H3_DISCOVERY, response4); + + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response1.connectionLabel().get(), response3.connectionLabel().get()); + + assertEquals(HTTP_3, response4.version()); + checkStatus(200, response4.statusCode()); + assertEquals(response2.connectionLabel().get(), response4.connectionLabel().get()); + + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + BlockingHandler.GATE.release(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + System.out.printf("Got expected h2 response: %s [%s]%n", + h2resp2, h2resp2.version()); + + // second - make a direct connection + HttpRequest request1 = req1Builder.copy() + .uri(URI.create(http3DirectURIString+"?request1")).build(); + var responseCF1 = client.sendAsync(request1, BodyHandlers.ofString()); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + var req2 = req2Builder.copy() + .uri(URI.create(http3DirectURIString + "?request2")) + .build(); + var responseCF2 = client.sendAsync(req2, BodyHandlers.ofString()); + + BlockingHandler.IN_HANDLER.acquire(2); + BlockingHandler.GATE.release(2); + + var response1 = responseCF1.get(); + printResponse("response 1", H3_DISCOVERY, response1); + var response2 = responseCF2.get(); + printResponse("response 2", H3_DISCOVERY, response2); + + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response2.connectionLabel().get(), response1.connectionLabel().get()); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin... + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request3 = req2Builder.copy() + .uri(URI.create(http3DirectURIString + "?request3")) + .timeout(Duration.ofSeconds(30)).build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + + // fourth request with HTTP_3_URI_ONLY should reuse the first connection, + // and not reuse the second. + // Specify a request timeout here to make sure we wait long enough + // for the MAX_STREAMS frame to arrive before creating a new + // connection + HttpRequest request4 = req1Builder.copy() + .uri(URI.create(http3DirectURIString + "?request4")) + .timeout(Duration.ofSeconds(30)).build(); + var responseCF4 = client.sendAsync(request4, BodyHandlers.ofString()); + + BlockingHandler.IN_HANDLER.acquire(2); + BlockingHandler.GATE.release(2); + + var response3 = responseCF3.get(); + printResponse("response 3", H3_DISCOVERY, response3); + var response4 = responseCF4.get(); + printResponse("response 4", H3_DISCOVERY, response4); + + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertEquals(response2.connectionLabel().get(), response3.connectionLabel().get()); + assertNotEquals(response1.connectionLabel().get(), response3.connectionLabel().get()); + assertEquals(HTTP_3, response4.version()); + assertEquals(response1.connectionLabel().get(), response4.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response4.connectionLabel().get()); + checkStatus(200, response1.statusCode()); + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + BlockingHandler.GATE.release(1); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin, get + // StreamLimitReached exception, and create a new connection + HttpRequest request3 = req2Builder.copy().build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + BlockingHandler.GATE.release(2); + + var response2 = responseCF2.get(); + printResponse("response 2", H3_DISCOVERY, response2); + + var response3 = responseCF3.get(); + printResponse("response 3", H3_DISCOVERY, response3); + + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + + @Test + public static void testParallelH2H3WithTwoAltSVC() throws Exception { + testH2H3Concurrent(false); + } + + @Test + public static void testParallelH2H3WithAltSVCOnSamePort() throws Exception { + testH2H3Concurrent(true); + } + + + private static void testH2H3Concurrent(boolean samePort) throws Exception { + System.out.println("\nTesting concurrent reconnections with advertised AltSvc on " + + (samePort ? "same port" : "ephemeral port")); + initialize(samePort); + try (HttpClient client = getClient()) { + var req1Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .version(HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .GET(); + var req2Builder = HttpRequest.newBuilder() + .uri(URI.create(http3DirectURIString)) + .setOption(H3_DISCOVERY, ALT_SVC) + .version(HTTP_3) + .GET(); + + if (altsvcPort == https2Port) { + System.out.println("Testing reconnections with alt service on same port"); + + // first request with HTTP3_URI_ONLY should create H3 connection + HttpRequest request1 = req1Builder.copy().build(); + HttpRequest request2 = req2Builder.copy().build(); + List>> directResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + HttpRequest req1 = req1Builder.copy() + .uri(URI.create(http3DirectURIString+"?dir="+i)).build(); + directResponses.add(client.sendAsync(req1, BodyHandlers.ofString())); + BlockingHandler.IN_HANDLER.acquire(); + } + // can't send requests in parallel here because if any establishes + // a connection before the H3 direct are established, then the H3 + // direct might reuse the H3 alt since the service is with same origin + BlockingHandler.releaseGate(directResponses.size() + 1); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + CompletableFuture.allOf(directResponses.stream() + .toArray(CompletableFuture[]::new)).exceptionally((t) -> null) + .join(); + + Set c1Label = new HashSet<>(); + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s]: %s%n", i, + response1.connectionLabel(), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + var cLabel = response1.connectionLabel().get(); + assertFalse(c1Label.contains(cLabel), + "%s contained in %s".formatted(cLabel, c1Label)); + c1Label.add(cLabel); + } + + // first request with ALT_SVC is to get alt service, should be H2 + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + assertFalse(c1Label.contains(h2resp2.connectionLabel().get()), + "%s contained in %s!".formatted(h2resp2.connectionLabel().get(), c1Label)); + + // second request should have ALT_SVC and create new connection with H3 + // it should not reuse the non-advertised connection + List>> altResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + HttpRequest req2 = req2Builder.copy() + .uri(URI.create(http3DirectURIString+"?alt="+i)).build(); + altResponses.add(client.sendAsync(req2, BodyHandlers.ofString())); + } + + BlockingHandler.releaseGate(altResponses.size()); + BlockingHandler.IN_HANDLER.acquire(altResponses.size()); + + CompletableFuture.allOf(altResponses.stream().toArray(CompletableFuture[]::new)) + .exceptionally((t) -> null) + .join(); + + Set c2Label = new HashSet<>(); + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s]: %s%n", i, + response2.connectionLabel(), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + var cLabel = response2.connectionLabel().get(); + if (c2Label.contains(cLabel)) { + System.out.printf("Connection %s reused%n", cLabel); + } + c2Label.add(cLabel); + assertFalse(c1Label.contains(cLabel), + "%s contained in %s".formatted(cLabel, c1Label)); + // cLabel could already be in c2Label, if the previous + // request finished before the next one. + } + + System.out.println("Sending mixed requests"); + + // second set of requests should reuse a created connection + HttpRequest request3 = req1Builder.copy().build(); + List>> mixResponses = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + var builder1 = req1Builder.copy() + .uri(URI.create(http3DirectURIString+"?mix1="+i)); + if (i == 0) { + // make sure to give time for MAX_STREAMS to arrive + builder1 = builder1.timeout(Duration.ofSeconds(30)); + } + HttpRequest req1 = builder1.build(); + mixResponses.add(client.sendAsync(req1, BodyHandlers.ofString())); + if (i == 0) BlockingHandler.IN_HANDLER.acquire(); + var builder2 = req2Builder.copy() + .uri(URI.create(http3DirectURIString+"?mix2="+i)); + if (i == 0) { + // make sure to give time for MAX_STREAMS to arrive + builder2 = builder2.timeout(Duration.ofSeconds(30)); + } + HttpRequest req2 = builder2.build(); + mixResponses.add(client.sendAsync(req2, BodyHandlers.ofString())); + if (i == 0) BlockingHandler.IN_HANDLER.acquire(); + } + + System.out.println("IN_HANDLER.acquire(" + + (mixResponses.size() - 2) + ") - available: " + + BlockingHandler.IN_HANDLER.availablePermits()); + BlockingHandler.IN_HANDLER.acquire(mixResponses.size() - 2); + BlockingHandler.releaseGate(mixResponses.size()); + + System.out.println("Getting mixed responses"); + + CompletableFuture.allOf(mixResponses.stream().toArray(CompletableFuture[]::new)) + .exceptionally((t) -> null) + .join(); + + System.out.println("All mixed responses received"); + + Set mixC1Label = new HashSet<>(); + Set mixC2Label = new HashSet<>(); + for (int i = 0; i < mixResponses.size(); i++) { + HttpResponse response3 = mixResponses.get(i).get(); + System.out.printf("mixed response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + var cLabel = response3.connectionLabel().get(); + if (response3.request().getOption(H3_DISCOVERY).orElse(null) == ALT_SVC) { + if (i == 0 || i == 1) { + assertTrue(c2Label.contains(cLabel), + "%s not in %s".formatted(cLabel, c2Label)); + System.out.printf("first ALTSVC connection reused %s from %s%n", cLabel, c2Label); + } else { + assertFalse(c2Label.contains(cLabel), + "%s in %s".formatted(cLabel, c2Label)); + } + assertFalse(c1Label.contains(cLabel), + "%s in %s".formatted(cLabel, c1Label)); + assertFalse(mixC1Label.contains(cLabel), + "%s in %s".formatted(cLabel, mixC1Label)); + if (mixC2Label.contains(cLabel)) { + System.out.printf("ALTSVC connection reused %s from %s%n", cLabel, mixC2Label); + } + mixC2Label.add(cLabel); + } else { + if (i == 0 || i == 1) { + assertTrue(c1Label.contains(cLabel), + "%s not in %s".formatted(cLabel, c1Label)); + System.out.printf("first ALTSVC connection reused %s from %s%n", cLabel, c1Label); + } else { + assertFalse(c1Label.contains(cLabel), + "%s in %s".formatted(cLabel, c1Label)); + } + assertFalse(c2Label.contains(cLabel), + "%s in %s".formatted(cLabel, c2Label)); + assertFalse(mixC2Label.contains(cLabel), + "%s in %s".formatted(cLabel, mixC2Label)); + if (mixC1Label.contains(cLabel)) { + System.out.printf("ALTSVC connection reused %s from %s%n", cLabel, mixC1Label); + } + mixC1Label.add(cLabel); + } + } + System.out.println("All done"); + } else if (http3Port == https2Port) { + System.out.println("Testing with two alt services"); + // first - make a direct connection + HttpRequest request1 = req1Builder.copy().build(); + + // second, use the alt service + HttpRequest request2 = req2Builder.copy().build(); + BlockingHandler.GATE.release(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + + // third, use ANY + HttpRequest request3 = req2Builder.copy().setOption(H3_DISCOVERY, ANY).build(); + + List>> directResponses = new ArrayList<>(); + List>> altResponses = new ArrayList<>(); + List>> anyResponses = new ArrayList<>(); + checkStatus(200, h2resp2.statusCode()); + + // We're going to send nine requests here. We could get + // "No more stream available on connection" when we run with + // a stream limit timeout of 0, unless we raise the retry on + // stream limit to at least 6 + for (int i = 0; i < 3; i++) { + anyResponses.add(client.sendAsync(request3, BodyHandlers.ofString())); + directResponses.add(client.sendAsync(request1, BodyHandlers.ofString())); + altResponses.add(client.sendAsync(request2, BodyHandlers.ofString())); + } + + var all = new ArrayList<>(directResponses); + all.addAll(altResponses); + all.addAll(anyResponses); + int requestCount = all.size(); + + BlockingHandler.GATE.release(requestCount); + CompletableFuture.allOf(all.stream().toArray(CompletableFuture[]::new)) + .exceptionally((t) -> null) + .join(); + + Set c1Label = new HashSet<>(); + for (int i = 0; i < directResponses.size(); i++) { + HttpResponse response1 = directResponses.get(i).get(); + System.out.printf("direct response [%s][%s] %s: %s%n", i, + response1.connectionLabel(), + response1.request().getOption(H3_DISCOVERY), + response1); + assertEquals(HTTP_3, response1.version()); + checkStatus(200, response1.statusCode()); + + var cLabel = response1.connectionLabel().get(); + if (c1Label.contains(cLabel)) { + System.out.printf(" connection %s reused from %s%n", cLabel, c1Label); + } + c1Label.add(cLabel); + } + Set c2Label = new HashSet<>(); + for (int i = 0; i < altResponses.size(); i++) { + HttpResponse response2 = altResponses.get(i).get(); + System.out.printf("alt response [%s][%s] %s: %s%n", i, + response2.connectionLabel(), + response2.request().getOption(H3_DISCOVERY), + response2); + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + + var cLabel = response2.connectionLabel().get(); + if (c2Label.contains(cLabel)) { + System.out.printf(" connection %s reused from %s%n", cLabel, c2Label); + } + assertNotEquals(cLabel, h2resp2.connectionLabel().get()); + assertFalse(c1Label.contains(cLabel), + "%s found in %s".formatted(cLabel, c1Label)); + c2Label.add(cLabel); + } + + var diff = new HashSet<>(c2Label); + diff.retainAll(c1Label); + assertTrue(diff.isEmpty()); + + var anyLabels = new HashSet(Set.of(c1Label, c2Label)); + for (int i = 0; i < anyResponses.size(); i++) { + HttpResponse response3 = anyResponses.get(i).get(); + System.out.printf("any response [%s][%s] %s: %s%n", i, + response3.connectionLabel(), + response3.request().getOption(H3_DISCOVERY), + response3); + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), h2resp2.connectionLabel().get()); + var label = response3.connectionLabel().orElse(""); + if (anyLabels.contains(label)) { + System.out.printf(" connection %s reused from %s%n", label, anyLabels); + } + } + BlockingHandler.IN_HANDLER.acquire(requestCount); + } else { + System.out.println("WARNING: Couldn't create HTTP/3 server on same port! Can't test all..."); + // Get, get the alt service + HttpRequest request2 = req2Builder.copy().build(); + // first request with ALT_SVC is to get alt service, should be H2 + BlockingHandler.GATE.release(); + HttpResponse h2resp2 = client.send(request2, BodyHandlers.ofString()); + assertEquals(HTTP_2, h2resp2.version()); + checkStatus(200, h2resp2.statusCode()); + BlockingHandler.IN_HANDLER.acquire(); + + // second request should have ALT_SVC and create new connection with H3 + var responseCF2 = client.sendAsync(request2, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + // third request with ALT_SVC should reuse the same advertised + // connection (from response2), regardless of same origin, get + // stream limit, and create a new connection + HttpRequest request3 = req2Builder.copy().build(); + var responseCF3 = client.sendAsync(request3, BodyHandlers.ofString()); + BlockingHandler.IN_HANDLER.acquire(); + + BlockingHandler.GATE.release(2); + + CompletableFuture.allOf(responseCF2, responseCF3) + .exceptionally((t) -> null) + .join(); + + var response2 = responseCF2.get(); + printResponse("first HTTP/3 request", H3_DISCOVERY, response2); + var response3 = responseCF3.get(); + printResponse("second HTTP/3 request", H3_DISCOVERY, response2); + + assertEquals(HTTP_3, response2.version()); + checkStatus(200, response2.statusCode()); + assertNotEquals(response2.connectionLabel().get(), h2resp2.connectionLabel().get()); + + assertEquals(HTTP_3, response3.version()); + checkStatus(200, response3.statusCode()); + assertNotEquals(response3.connectionLabel().get(), h2resp2.connectionLabel().get()); + assertNotEquals(response3.connectionLabel().get(), response2.connectionLabel().get()); + } + } catch (Throwable t) { + t.printStackTrace(System.out); + throw t; + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + } + } + static HttpClient getClient() { + if (client == null) { + client = HttpServerAdapters.createClientBuilderForH3() + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + + static T logExceptionally(String desc, Throwable t) { + System.out.println(desc + " failed: " + t); + System.err.println(desc + " failed: " + t); + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + throw new CompletionException(t); + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3Timeout.java b/test/jdk/java/net/httpclient/http3/H3Timeout.java new file mode 100644 index 00000000000..81a0f87467b --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3Timeout.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.net.http.HttpTimeoutException; +import java.time.Duration; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.common.OperationTrackers.Tracker; +import jdk.test.lib.net.SimpleSSLContext; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @bug 8156710 + * @summary Check if HttpTimeoutException is thrown if a server doesn't reply + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run main/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors H3Timeout + */ +public class H3Timeout implements HttpServerAdapters { + + private static final int TIMEOUT = 2 * 1000; // in millis + private static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + + public static void main(String[] args) throws Exception { + SSLContext context = new SimpleSSLContext().get(); + testConnect(context, false); + testConnect(context, true); + testTimeout(context, false); + testTimeout(context, true); + } + + public static void testConnect(SSLContext context, boolean async) throws Exception { + + InetSocketAddress loopback = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + try (DatagramSocket socket = new DatagramSocket(loopback)) { + String address = socket.getLocalAddress().getHostAddress(); + if (address.indexOf(':') >= 0) { + if (!address.startsWith("[") || !address.endsWith("]")) { + address = "[" + address + "]"; + } + } + String serverAuth = address + ":" + socket.getLocalPort(); + String uri = "https://" + serverAuth + "/"; + HttpTimeoutException expected; + if (async) { + System.out.println(uri + ": Trying to connect asynchronously"); + expected = connectAsync(context, uri); + } else { + System.out.println(uri + ": Trying to connect synchronously"); + expected = connect(context, uri); + } + if (!(expected instanceof HttpConnectTimeoutException)) { + throw new AssertionError("expected HttpConnectTimeoutException, got: " + + expected, expected); + } + } + } + + public static void testTimeout(SSLContext context, boolean async) throws Exception { + + CountDownLatch latch = new CountDownLatch(1); + HttpTestServer server = HttpTestServer.create(HTTP_3_URI_ONLY, context); + server.addHandler((exch) -> { + try { + System.err.println("server reading request"); + byte[] req = exch.getRequestBody().readAllBytes(); + System.err.printf("server got request: %s bytes", req.length); + latch.await(); + exch.sendResponseHeaders(500, 0); + } catch (Exception e) { + System.err.println("server exception: " + e); + } + }, "/"); + server.start(); + try { + String serverAuth = server.serverAuthority(); + String uri = "https://" + serverAuth + "/"; + HttpTimeoutException expected; + if (async) { + System.out.println(uri + ": Trying to connect asynchronously"); + expected = connectAsync(context, uri); + } else { + System.out.println(uri + ": Trying to connect synchronously"); + expected = connect(context, uri); + } + assert expected instanceof HttpTimeoutException; + } finally { + latch.countDown(); + server.stop(); + } + } + + private static HttpTimeoutException connect(SSLContext context, String server) throws Exception { + HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .version(HTTP_3) + .sslContext(context) + .build(); + try { + HttpRequest request = HttpRequest.newBuilder(new URI(server)) + .timeout(Duration.ofMillis(TIMEOUT)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .POST(BodyPublishers.ofString("body")) + .build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + System.out.println("Received unexpected reply: " + response.statusCode()); + throw new RuntimeException("unexpected successful connection"); + } catch (HttpTimeoutException e) { + System.out.println("expected exception: " + e); + return e; + } finally { + client.shutdown(); + if (!client.awaitTermination(Duration.ofSeconds(5))) { + Tracker tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 5000); + if (error != null) throw error; + } + } + } + + private static HttpTimeoutException connectAsync(SSLContext context, String server) throws Exception { + try (HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .version(HTTP_3) + .sslContext(context) + .build()) { + try { + HttpRequest request = HttpRequest.newBuilder(new URI(server)) + .timeout(Duration.ofMillis(TIMEOUT)) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .POST(BodyPublishers.ofString("body")) + .build(); + HttpResponse response = client.sendAsync(request, BodyHandlers.ofString()).join(); + System.out.println("Received unexpected reply: " + response.statusCode()); + throw new RuntimeException("unexpected successful connection"); + } catch (CompletionException e) { + var cause = e.getCause(); + if (cause instanceof HttpTimeoutException timeout) { + System.out.println("expected exception: " + e.getCause()); + return timeout; + } else { + throw new RuntimeException("Unexpected exception received: " + e.getCause(), e); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/http3/H3UnsupportedSSLParametersTest.java b/test/jdk/java/net/httpclient/http3/H3UnsupportedSSLParametersTest.java new file mode 100644 index 00000000000..090dec2c189 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3UnsupportedSSLParametersTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +import java.io.UncheckedIOException; +import java.net.http.HttpClient; +import java.net.http.UnsupportedProtocolVersionException; + +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary Tests that a HttpClient configured with SSLParameters that doesn't include TLSv1.3 + * cannot be used for HTTP3 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @run junit H3UnsupportedSSLParametersTest + */ +public class H3UnsupportedSSLParametersTest { + + /** + * Configures a HttpClient builder to use a SSLParameter which doesn't list TLSv1.3 + * as one of the supported protocols. The method then uses this builder + * to create a HttpClient for HTTP3 and expects that build() to fail with + * UnsupportedProtocolVersionException + */ + @Test + public void testNoTLSv13() throws Exception { + final SSLParameters params = new SSLParameters(); + params.setProtocols(new String[]{"TLSv1.2"}); + final UncheckedIOException uioe = assertThrows(UncheckedIOException.class, + () -> HttpServerAdapters.createClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(HttpClient.Version.HTTP_3) + .sslParameters(params) + .build()); + assertTrue(uioe.getCause() instanceof UnsupportedProtocolVersionException, + "Unexpected cause " + uioe.getCause() + " in HttpClient build failure"); + } + + /** + * Builds a HttpClient with SSLParameters which explicitly lists TLSv1.3 as one of the supported + * protocol versions and expects the build() to succeed and return a HttpClient instance + */ + @Test + public void testExplicitTLSv13() throws Exception { + final SSLParameters params = new SSLParameters(); + params.setProtocols(new String[]{"TLSv1.2", "TLSv1.3"}); + final HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .sslParameters(params) + .version(HttpClient.Version.HTTP_3).build(); + assertNotNull(client, "HttpClient is null"); + } +} diff --git a/test/jdk/java/net/httpclient/http3/H3UserInfoTest.java b/test/jdk/java/net/httpclient/http3/H3UserInfoTest.java new file mode 100644 index 00000000000..15e25b865cd --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/H3UserInfoTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.Arguments; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.stream.Stream; + + +import static java.net.http.HttpOption.H3_DISCOVERY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static org.junit.jupiter.api.Assertions.fail; + +/* + * @test + * @bug 8292876 + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.net.SimpleSSLContext + * @compile ../ReferenceTracker.java + * @run junit/othervm -Djdk.httpclient.HttpClient.log=quic,errors + * -Djdk.httpclient.http3.maxDirectConnectionTimeout=4000 + * -Djdk.internal.httpclient.debug=true H3UserInfoTest + */ + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class H3UserInfoTest implements HttpServerAdapters { + + static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + static HttpTestServer server; + static HttpTestServer server3; + static String serverURI; + static String server3URI; + static SSLContext sslContext; + + @BeforeAll + static void before() throws Exception { + sslContext = new SimpleSSLContext().get(); + HttpTestHandler handler = new HttpHandler(); + + server = HttpTestServer.create(ANY, sslContext); + server.addHandler(handler, "/"); + serverURI = "https://" + server.serverAuthority() +"/http3-any/"; + server.start(); + + server3 = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + server3.addHandler(handler, "/"); + server3URI = "https://" + server3.serverAuthority() +"/http3-only/"; + server3.start(); + } + + @AfterAll + static void after() throws Exception { + server.stop(); + server3.stop(); + } + + static class HttpHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange e) throws IOException { + String authorityHeader = e.getRequestHeaders() + .firstValue(":authority") + .orElse(null); + if (authorityHeader == null || authorityHeader.contains("user@")) { + e.sendResponseHeaders(500, 0); + } else { + e.sendResponseHeaders(200, 0); + } + } + } + + public static Stream servers() { + return Stream.of( + Arguments.arguments(serverURI, server), + Arguments.arguments(server3URI, server3) + ); + } + + @ParameterizedTest + @MethodSource("servers") + public void testAuthorityHeader(String serverURI, HttpTestServer server) throws Exception { + try (HttpClient client = newClientBuilderForH3() + .proxy(HttpClient.Builder.NO_PROXY) + .version(HTTP_3) + .sslContext(sslContext) + .build()) { + TRACKER.track(client); + + URI origURI = URI.create(serverURI); + URI uri = URIBuilder.newBuilder() + .scheme("https") + .userInfo("user") + .host(origURI.getHost()) + .port(origURI.getPort()) + .path(origURI.getRawPath()) + .build(); + var config = server.h3DiscoveryConfig(); + + int numRetries = 0; + while (true) { + if (config == ALT_SVC) { + // send head request + System.out.printf("Sending head request (%s) to %s%n", config, origURI); + System.err.printf("Sending head request (%s) to %s%n", config, origURI); + HttpRequest head = HttpRequest.newBuilder(origURI) + .HEAD() + .version(HTTP_2) + .build(); + var headResponse = client.send(head, BodyHandlers.ofString()); + assertEquals(200, headResponse.statusCode()); + assertEquals(HTTP_2, headResponse.version()); + assertEquals("", headResponse.body()); + } + + HttpRequest request = HttpRequest + .newBuilder(uri) + .setOption(H3_DISCOVERY, config) + .version(HTTP_3) + .GET() + .build(); + + System.out.printf("Sending GET request (%s) to %s%n", config, origURI); + System.err.printf("Sending GET request (%s) to %s%n", config, origURI); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), + "Test Failed : " + response.uri().getAuthority()); + assertEquals("", response.body()); + if (config != ANY) { + assertEquals(HTTP_3, response.version()); + } else if (response.version() != HTTP_3) { + // the request went through HTTP/2 - the next + // should go through HTTP/3 + if (numRetries++ < 3) { + System.out.printf("Received GET response (%s) to %s with version %s: " + + "repeating request once more%n", config, origURI, response.version()); + System.err.printf("Received GET response (%s) to %s with version %s: " + + "repeating request once more%n", config, origURI, response.version()); + assertEquals(HTTP_2, response.version()); + continue; + } else { + fail("Did not receive the expected HTTP3 response"); + } + } + break; + } + } + // the client should already be closed, but its facade ref might + // not have been cleared by GC yet. + System.gc(); + var error = TRACKER.checkClosed(1500); + if (error != null) throw error; + } +} diff --git a/test/jdk/java/net/httpclient/http3/HTTP3NoBodyTest.java b/test/jdk/java/net/httpclient/http3/HTTP3NoBodyTest.java new file mode 100644 index 00000000000..f04588e9362 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/HTTP3NoBodyTest.java @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key randomness + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.http3.Http3TestServer + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors + * -Djdk.internal.httpclient.debug=true + * HTTP3NoBodyTest + * @summary this is a copy of http2/NoBodyTest over HTTP/3 + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.*; +import javax.net.ssl.*; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Random; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.Test; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC; +import static java.net.http.HttpOption.Http3DiscoveryMode.ANY; +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +@Test +public class HTTP3NoBodyTest { + private static final Random RANDOM = RandomFactory.getRandom(); + + static int http3Port, https2Port; + static Http3TestServer http3OnlyServer; + static Http2TestServer https2AltSvcServer; + static HttpClient client = null; + static ExecutorService clientExec; + static ExecutorService serverExec; + static SSLContext sslContext; + static final String TEST_STRING = "The quick brown fox jumps over the lazy dog "; + + static volatile String http3URIString, https2URIString; + + static void initialize() throws Exception { + try { + SimpleSSLContext sslct = new SimpleSSLContext(); + sslContext = sslct.get(); + client = getClient(); + + // server that only supports HTTP/3 + http3OnlyServer = new Http3TestServer(sslContext, serverExec); + http3OnlyServer.addHandler("/", new Handler()); + http3Port = http3OnlyServer.getAddress().getPort(); + System.out.println("HTTP/3 server started at localhost:" + http3Port); + + // server that supports both HTTP/2 and HTTP/3, with HTTP/3 on an altSvc port. + https2AltSvcServer = new Http2TestServer(true, 0, serverExec, sslContext); + if (RANDOM.nextBoolean()) { + https2AltSvcServer.enableH3AltServiceOnEphemeralPort(); + } else { + https2AltSvcServer.enableH3AltServiceOnSamePort(); + } + https2AltSvcServer.addHandler(new Handler(), "/"); + https2Port = https2AltSvcServer.getAddress().getPort(); + if (https2AltSvcServer.supportsH3DirectConnection()) { + System.out.println("HTTP/2 server (same HTTP/3 origin) started at localhost:" + https2Port); + } else { + System.out.println("HTTP/2 server (different HTTP/3 origin) started at localhost:" + https2Port); + } + + http3URIString = "https://localhost:" + http3Port + "/foo/"; + https2URIString = "https://localhost:" + https2Port + "/bar/"; + + http3OnlyServer.start(); + https2AltSvcServer.start(); + } catch (Throwable e) { + System.err.println("Throwing now"); + e.printStackTrace(System.err); + throw e; + } + } + + @Test + public static void runtest() throws Exception { + try { + initialize(); + warmup(false); + warmup(true); + test(false); + test(true); + if (client != null) { + var tracker = ReferenceTracker.INSTANCE; + tracker.track(client); + client = null; + System.gc(); + var error = tracker.check(1500); + if (error != null) throw error; + } + } catch (Throwable tt) { + System.err.println("Unexpected Throwable caught"); + tt.printStackTrace(System.err); + throw tt; + } finally { + http3OnlyServer.stop(); + https2AltSvcServer.stop(); + serverExec.close(); + clientExec.close(); + } + } + + static HttpClient getClient() { + if (client == null) { + serverExec = Executors.newCachedThreadPool(); + clientExec = Executors.newCachedThreadPool(); + client = HttpServerAdapters.createClientBuilderForH3() + .executor(clientExec) + .sslContext(sslContext) + .version(HTTP_3) + .build(); + } + return client; + } + + static URI getURI(boolean altSvc) { + return getURI(altSvc, -1); + } + + static URI getURI(boolean altSvc, int step) { + return URI.create(getURIString(altSvc, step)); + } + + static String getURIString(boolean altSvc, int step) { + var uriStr = altSvc ? https2URIString : http3URIString; + return step >= 0 ? (uriStr + step) : uriStr; + } + + static void checkStatus(int expected, int found) throws Exception { + if (expected != found) { + System.err.printf ("Test failed: wrong status code %d/%d\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static void checkStrings(String expected, String found) throws Exception { + if (!expected.equals(found)) { + System.err.printf ("Test failed: wrong string %s/%s\n", + expected, found); + throw new RuntimeException("Test failed"); + } + } + + static final AtomicInteger count = new AtomicInteger(); + static Http3DiscoveryMode config(boolean http3only) { + if (http3only) return HTTP_3_URI_ONLY; + // if the server supports H3 direct connection, we can + // additionally use HTTP_3_URI_ONLY; Otherwise we can + // only use ALT_SVC - or ANY (given that we should have + // preloaded an ALT_SVC in warmup) + int bound = https2AltSvcServer.supportsH3DirectConnection() ? 4 : 3; + int rand = RANDOM.nextInt(bound); + count.getAndIncrement(); + return switch (rand) { + case 1 -> ANY; + case 2 -> ALT_SVC; + case 3 -> HTTP_3_URI_ONLY; + default -> null; + }; + } + + static final int LOOPS = 13; + + static void warmup(boolean altSvc) throws Exception { + URI uri = getURI(altSvc); + String type = altSvc ? "http2" : "http3"; + System.out.println("warmup: " + type); + System.err.println("Request to " + uri); + var http3Only = altSvc == false; + var config = config(http3Only); + + // in the warmup phase, we want to make sure + // to preload the ALT_SVC, otherwise the first + // request that uses ALT_SVC might go through HTTP/2 + if (altSvc) config = ALT_SVC; + + // Do a simple warmup request + + HttpClient client = getClient(); + var builder = HttpRequest.newBuilder(uri); + HttpRequest req = builder + .POST(BodyPublishers.ofString("Random text")) + .setOption(H3_DISCOVERY, config) + .build(); + HttpResponse response = client.send(req, BodyHandlers.ofString()); + checkStatus(200, response.statusCode()); + String responseBody = response.body(); + HttpHeaders h = response.headers(); + checkStrings(TEST_STRING + type, responseBody); + System.out.println("warmup: " + type + " done"); + System.err.println("warmup: " + type + " done"); + } + + static void test(boolean http2) throws Exception { + URI uri = getURI(http2); + String type = http2 ? "http2" : "http3"; + System.err.println("Request to " + uri); + var http3Only = http2 == false; + for (int i = 0; i < LOOPS; i++) { + var config = config(http3Only); + URI uri2 = getURI(http2, i); + HttpRequest request = HttpRequest.newBuilder(uri2) + .POST(BodyPublishers.ofString(TEST_STRING)) + .setOption(H3_DISCOVERY, config) + .build(); + System.out.println(type + ": Loop " + i + ", config: " + config + ", uri: " + uri2); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + int expectedResponse = (i % 2) == 0 ? 200 : 204; + if (response.statusCode() != expectedResponse) + throw new RuntimeException("wrong response code " + response.statusCode()); + if (expectedResponse == 200 && !response.body().equals(TEST_STRING + type)) { + System.err.printf(type + " response received/expected %s/%s\n", response.body(), TEST_STRING + type); + throw new RuntimeException("wrong response body"); + } + if (response.version() != HTTP_3) { + throw new RuntimeException("wrong response version: " + response.version()); + } + System.out.println(type + ": Loop " + i + " done"); + } + System.err.println("test: " + type + " DONE"); + } + + static URI base(URI uri) { + var uriStr = uri.toString(); + if (uriStr.startsWith(http3URIString)) { + if (uriStr.equals(http3URIString)) return uri; + return URI.create(http3URIString); + } else if (uri.toString().startsWith(https2URIString)) { + if (uriStr.equals(https2URIString)) return uri; + return URI.create(https2URIString); + } else return uri; + } + + static class Handler implements Http2Handler { + + public Handler() {} + + volatile int invocation = 0; + + @Override + public void handle(Http2TestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request to %s from %s\n", + uri, t.getRemoteAddress()); + String type = uri.toString().startsWith(http3URIString) + ? "http3" : "http2"; + InputStream is = t.getRequestBody(); + while (is.read() != -1); + is.close(); + + // every second response is 204. + var base = base(uri); + int step = base == uri ? 0 : Integer.parseInt(base.relativize(uri).toString()); + invocation++; + + if ((step++ % 2) == 1) { + System.err.println("Server sending 204"); + t.sendResponseHeaders(204, -1); + } else { + System.err.println("Server sending 200"); + String body = TEST_STRING + type; + t.sendResponseHeaders(200, body.length()); + OutputStream os = t.getResponseBody(); + os.write(body.getBytes()); + os.close(); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/Http3ExpectContinueTest.java b/test/jdk/java/net/httpclient/http3/Http3ExpectContinueTest.java new file mode 100644 index 00000000000..2052671b697 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/Http3ExpectContinueTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Tests Http3 expect continue + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @compile ../ReferenceTracker.java + * @build jdk.httpclient.test.lib.common.HttpServerAdapters + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=errors,requests,headers + * Http3ExpectContinueTest + */ + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; +import org.testng.TestException; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; +import java.net.http.HttpRequest; +import java.net.http.HttpOption.Http3DiscoveryMode; +import java.net.http.HttpOption; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static java.net.http.HttpClient.Version.HTTP_3; +import static org.testng.Assert.*; + +public class Http3ExpectContinueTest implements HttpServerAdapters { + + ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + + Http3TestServer http3TestServer; + + URI h3postUri, h3forcePostUri, h3hangUri; + + static PrintStream err = new PrintStream(System.err); + static PrintStream out = new PrintStream(System.out); + static final String EXPECTATION_FAILED_417 = "417 Expectation Failed"; + static final String CONTINUE_100 = "100 Continue"; + static final String RESPONSE_BODY= "Verify response received"; + static final String BODY = "Post body"; + private SSLContext sslContext; + + @DataProvider(name = "uris") + public Object[][] urisData() { + return new Object[][]{ + // URI, Expected Status Code, Will finish with Exception + { h3postUri, 200, false }, + { h3forcePostUri, 200, false }, + { h3hangUri, 417, false }, + }; + } + + @Test(dataProvider = "uris") + public void test(URI uri, int expectedStatusCode, boolean exceptionally) + throws CancellationException, InterruptedException, ExecutionException, IOException { + + err.printf("\nTesting URI: %s, exceptionally: %b\n", uri, exceptionally); + out.printf("\nTesting URI: %s, exceptionally: %b\n", uri, exceptionally); + HttpClient client = newClientBuilderForH3(). + proxy(Builder.NO_PROXY) + .version(HTTP_3).sslContext(sslContext) + .build(); + AssertionError failed = null; + TRACKER.track(client); + try { + HttpResponse resp = null; + Throwable testThrowable = null; + + HttpRequest postRequest = HttpRequest.newBuilder(uri) + .version(HTTP_3) + .setOption(HttpOption.H3_DISCOVERY, + Http3DiscoveryMode.HTTP_3_URI_ONLY) + .POST(HttpRequest.BodyPublishers.ofString(BODY)) + .expectContinue(true) + .build(); + + err.printf("Sending request: %s%n", postRequest); + CompletableFuture> cf = client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString()); + try { + resp = cf.get(); + } catch (Exception e) { + testThrowable = e.getCause(); + } + verifyRequest(uri.getPath(), expectedStatusCode, resp, exceptionally, testThrowable); + } catch (Throwable x) { + failed = new AssertionError("Unexpected exception:" + x, x); + } finally { + client.shutdown(); + if (!client.awaitTermination(Duration.ofMillis(1000))) { + var tracker = TRACKER.getTracker(client); + client = null; + var error = TRACKER.check(tracker, 2000); + if (error != null || failed != null) { + var ex = failed == null ? error : failed; + err.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, ex); + out.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, ex); + } + if (error != null) { + if (failed != null) { + failed.addSuppressed(error); + throw failed; + } + throw error; + } + } + } + if (failed != null) { + err.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, failed); + out.printf("FAILED URI: %s, exceptionally: %b, error: %s\n", uri, exceptionally, failed); + throw failed; + } + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + final QuicServer quicServer = Http3TestServer.quicServerBuilder() + .sslContext(sslContext) + .build(); + http3TestServer = new Http3TestServer(quicServer); + http3TestServer.addHandler("/http3/post", new PostHandler().toHttp2Handler()); + http3TestServer.addHandler("/http3/forcePost", new ForcePostHandler().toHttp2Handler()); + http3TestServer.addHandler("/http3/hang", new PostHandlerCantContinue().toHttp2Handler()); + + h3postUri = new URI("https://" + http3TestServer.serverAuthority() + "/http3/post"); + h3forcePostUri = URI.create("https://" + http3TestServer.serverAuthority() + "/http3/forcePost"); + h3hangUri = URI.create("https://" + http3TestServer.serverAuthority() + "/http3/hang"); + out.printf("HTTP/3 server listening at: %s", http3TestServer.getAddress()); + + http3TestServer.start(); + } + + @AfterTest + public void teardown() throws IOException { + var error = TRACKER.check(500); + if (error != null) throw error; + http3TestServer.stop(); + } + + static class PostHandler implements HttpTestHandler { + + @java.lang.Override + public void handle(HttpTestExchange exchange) throws IOException { + System.out.printf("Server version %s and exchange version %s", exchange.getServerVersion(), exchange.getExchangeVersion()); + + if(exchange.getExchangeVersion().equals(HTTP_3)){ + // send 100 header + byte[] ContinueResponseBytes = CONTINUE_100.getBytes(); + err.println("Server send 100 (length="+ContinueResponseBytes.length+")"); + exchange.sendResponseHeaders(100, ContinueResponseBytes.length); + } + + // Read body from client and acknowledge with 200 + try (InputStream is = exchange.getRequestBody()) { + err.println("Server reading body"); + var bytes = is.readAllBytes(); + String responseBody = new String(bytes); + assert responseBody.equals(BODY); + byte[] responseBodyBytes = RESPONSE_BODY.getBytes(); + err.println("Server send 200 (length="+responseBodyBytes.length+")"); + exchange.sendResponseHeaders(200, responseBodyBytes.length); + exchange.getResponseBody().write(responseBodyBytes); + } + } + } + + static class ForcePostHandler implements HttpTestHandler { + @Override + public void handle(HttpTestExchange exchange) throws IOException { + try (InputStream is = exchange.getRequestBody()) { + err.println("Server reading body inside the force Post"); + is.readAllBytes(); + err.println("Server send 200 (length=0) in the force post"); + exchange.sendResponseHeaders(200, 0); + } + } + } + + static class PostHandlerCantContinue implements HttpTestHandler { + @java.lang.Override + public void handle(HttpTestExchange exchange) throws IOException { + //Send 417 Headers, tell client to not send body + try (OutputStream os = exchange.getResponseBody()) { + byte[] bytes = EXPECTATION_FAILED_417.getBytes(); + err.println("Server send 417 (length="+bytes.length+")"); + exchange.sendResponseHeaders(417, bytes.length); + err.println("Server sending Response Body"); + os.write(bytes); + } + } + } + + private void verifyRequest(String path, int expectedStatusCode, HttpResponse resp, boolean exceptionally, Throwable testThrowable) { + if (!exceptionally) { + err.printf("Response code %s received for path %s %n", resp.statusCode(), path); + } + if (exceptionally && testThrowable != null) { + err.println("Finished exceptionally Test throwable: " + testThrowable); + assertEquals(IOException.class, testThrowable.getClass()); + } else if (exceptionally) { + throw new TestException("Expected case to finish with an IOException but testException is null"); + } else if (resp != null) { + assertEquals(resp.statusCode(), expectedStatusCode); + err.println("Request completed successfully for path " + path); + err.println("Response Headers: " + resp.headers()); + err.println("Response Status Code: " + resp.statusCode()); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/PeerUniStreamDispatcherTest.java b/test/jdk/java/net/httpclient/http3/PeerUniStreamDispatcherTest.java new file mode 100644 index 00000000000..be1b8304bf1 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/PeerUniStreamDispatcherTest.java @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @run testng/othervm + * -Djdk.internal.httpclient.debug=out + * PeerUniStreamDispatcherTest + * @summary Unit test for the PeerUniStreamDispatcher + */ + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.http3.streams.PeerUniStreamDispatcher; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreams; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +public class PeerUniStreamDispatcherTest { + + final Logger debug = Utils.getDebugLogger(() -> "PeerUniStreamDispatcherStub"); + + enum DISPATCHED_STREAM { + CONTROL, ENCODER, DECODER, PUSH, RESERVED, UNKNOWN + } + + sealed interface DispatchedStream { + record StandardStream(DISPATCHED_STREAM type, String description, QuicReceiverStream stream) + implements DispatchedStream { } + record PushStream(DISPATCHED_STREAM type, String description, QuicReceiverStream stream, long pushId) + implements DispatchedStream { } + record UnknownStream(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) + implements DispatchedStream { } + record ReservedStream(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) + implements DispatchedStream { } + + static DispatchedStream of(DISPATCHED_STREAM type, String description, QuicReceiverStream stream) { + return new StandardStream(type, description, stream); + } + static DispatchedStream of(DISPATCHED_STREAM type, String description, QuicReceiverStream stream, long pushId) { + return new PushStream(type, description, stream, pushId); + } + static DispatchedStream reserved(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) { + return new ReservedStream(type, code, stream); + } + static DispatchedStream unknown(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) { + return new UnknownStream(type, code, stream); + } + } + + class PeerUniStreamDispatcherStub extends PeerUniStreamDispatcher { + + final List dispatched = new CopyOnWriteArrayList<>(); + + PeerUniStreamDispatcherStub(QuicReceiverStream stream) { + super(stream); + } + + private void dispatched(DISPATCHED_STREAM type, String description, QuicReceiverStream stream) { + dispatched.add(DispatchedStream.of(type, description, stream)); + } + + private void dispatched(DISPATCHED_STREAM type, String description, QuicReceiverStream stream, long pushId) { + dispatched.add(DispatchedStream.of(type, description, stream, pushId)); + } + + private void dispatched(DISPATCHED_STREAM type, long code, QuicReceiverStream stream) { + dispatched.add(switch (type) { + case UNKNOWN -> DispatchedStream.unknown(type, code, stream); + case RESERVED -> DispatchedStream.reserved(type, code, stream); + default -> throw new IllegalArgumentException(String.valueOf(type)); + }); + } + + @Override + protected Logger debug() { + return debug; + } + + @Override + protected void onControlStreamCreated(String description, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.CONTROL, description, stream); + } + + @Override + protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.ENCODER, description, stream); + } + + @Override + protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.DECODER, description, stream); + } + + @Override + protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) { + dispatched(DISPATCHED_STREAM.PUSH, description, stream, pushId); + } + + @Override + protected void onReservedStreamType(long code, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.RESERVED, code, stream); + super.onReservedStreamType(code, stream); + } + + @Override + protected void onUnknownStreamType(long code, QuicReceiverStream stream) { + dispatched(DISPATCHED_STREAM.UNKNOWN, code, stream); + super.onUnknownStreamType(code, stream); + } + + @Override + public void start() { + super.start(); + } + } + + static class QuicReceiverStreamStub implements QuicReceiverStream { + + class QuicStreamReaderStub extends QuicStreamReader { + + volatile boolean connected, started; + QuicStreamReaderStub(SequentialScheduler scheduler) { + super(scheduler); + } + + @Override + public ReceivingStreamState receivingState() { + return QuicReceiverStreamStub.this.receivingState(); + } + + @Override + public ByteBuffer poll() throws IOException { + return buffers.poll(); + } + + @Override + public ByteBuffer peek() throws IOException { + return buffers.peek(); + } + + @Override + public QuicReceiverStream stream() { + return QuicReceiverStreamStub.this; + } + + @Override + public boolean connected() { + return connected; + } + + @Override + public boolean started() { + return started; + } + + @Override + public void start() { + started = true; + if (!buffers.isEmpty()) scheduler.runOrSchedule(); + } + } + + volatile QuicStreamReaderStub reader; + volatile SequentialScheduler scheduler; + volatile long errorCode; + final long streamId; + ConcurrentLinkedQueue buffers = new ConcurrentLinkedQueue<>(); + + QuicReceiverStreamStub(long streamId) { + this.streamId = streamId; + } + + + @Override + public ReceivingStreamState receivingState() { + return ReceivingStreamState.RECV; + } + + @Override + public QuicStreamReader connectReader(SequentialScheduler scheduler) { + this.scheduler = scheduler; + var reader = this.reader + = new QuicStreamReaderStub(scheduler); + reader.connected = true; + return reader; + } + + @Override + public void disconnectReader(QuicStreamReader reader) { + this.scheduler = null; + this.reader = null; + ((QuicStreamReaderStub) reader).connected = false; + } + + @Override + public void requestStopSending(long errorCode) { + this.errorCode = errorCode; + } + + @Override + public long dataReceived() { + return 0; + } + + @Override + public long maxStreamData() { + return 0; + } + + @Override + public long rcvErrorCode() { + return errorCode; + } + + @Override + public long streamId() { + return streamId; + } + + @Override + public StreamMode mode() { + return StreamMode.READ_ONLY; + } + + @Override + public boolean isClientInitiated() { + return QuicStreams.isClientInitiated(streamId); + } + + @Override + public boolean isServerInitiated() { + return QuicStreams.isServerInitiated(streamId); + } + + @Override + public boolean isBidirectional() { + return QuicStreams.isBidirectional(streamId); + } + + @Override + public boolean isLocalInitiated() { + return isClientInitiated(); + } + + @Override + public boolean isRemoteInitiated() { + return !isClientInitiated(); + } + + @Override + public int type() { + return QuicStreams.streamType(streamId); + } + + @Override + public StreamState state() { + return ReceivingStreamState.RECV; + } + + } + + private void simpleStreamType(DISPATCHED_STREAM type, long code) { + System.out.println("Testing " + type + " with " + code); + QuicReceiverStreamStub stream = new QuicReceiverStreamStub(QuicStreams.UNI_MASK + QuicStreams.SRV_MASK); + PeerUniStreamDispatcherStub dispatcher = new PeerUniStreamDispatcherStub(stream); + QuicStreamReader reader = stream.reader; + SequentialScheduler scheduler = stream.scheduler; + assertTrue(reader.connected()); + int size = VariableLengthEncoder.getEncodedSize(code); + ByteBuffer buffer = ByteBuffer.allocate(size); + assertEquals(buffer.remaining(), size); + VariableLengthEncoder.encode(buffer, code); + buffer.flip(); + stream.buffers.add(buffer); + scheduler.runOrSchedule(); + dispatcher.start(); + if (type == DISPATCHED_STREAM.PUSH) { + // we want to encode the pushId in multiple buffers, but call + // the scheduler only once to check that the dispatcher + // will loop correctly. + size = VariableLengthEncoder.getEncodedSize(1L << 62 - 5); + ByteBuffer buffer2 = ByteBuffer.allocate(size); + assertEquals(buffer2.remaining(), size); + VariableLengthEncoder.encode(buffer2, 1L << 62 - 5); + buffer2.flip(); + stream.buffers.add(ByteBuffer.wrap(new byte[] {buffer2.get()})); + scheduler.runOrSchedule(); // call runOrSchedule after supplying the first byte. + assertTrue(reader.connected()); + assert buffer2.remaining() > 1; // should always be true + while (buffer2.hasRemaining()) { + stream.buffers.add(ByteBuffer.wrap(new byte[] {buffer2.get()})); + } + } + scheduler.runOrSchedule(); + assertFalse(reader.connected()); + assertFalse(dispatcher.dispatched.isEmpty()); + assertTrue(stream.buffers.isEmpty()); + assertEquals(dispatcher.dispatched.size(), 1); + var dispatched = dispatcher.dispatched.get(0); + checkDispatched(type, code, stream, dispatched); + } + + private void checkDispatched(DISPATCHED_STREAM type, + long code, + QuicReceiverStream stream, + DispatchedStream dispatched) { + var streamClass = switch (type) { + case CONTROL, ENCODER, DECODER -> DispatchedStream.StandardStream.class; + case PUSH -> DispatchedStream.PushStream.class; + case RESERVED -> DispatchedStream.ReservedStream.class; + case UNKNOWN -> DispatchedStream.UnknownStream.class; + }; + assertEquals(dispatched.getClass(), streamClass, + "unexpected dispatched class " + dispatched + " for " + type); + if (dispatched instanceof DispatchedStream.StandardStream st) { + System.out.println("Got expected stream: " + st); + assertEquals(st.type(), type); + assertEquals(st.stream, stream); + } else if (dispatched instanceof DispatchedStream.ReservedStream res) { + System.out.println("Got expected stream: " + res); + assertEquals(res.type(), type); + assertEquals(res.stream, stream); + assertEquals(res.code(), code); + assertTrue(Http3Streams.isReserved(res.code())); + } else if (dispatched instanceof DispatchedStream.UnknownStream unk) { + System.out.println("Got expected stream: " + unk); + assertEquals(unk.type(), type); + assertEquals(unk.stream, stream); + assertEquals(unk.code(), code); + assertFalse(Http3Streams.isReserved(unk.code())); + } else if (dispatched instanceof DispatchedStream.PushStream push) { + System.out.println("Got expected stream: " + push); + assertEquals(push.type(), type); + assertEquals(push.stream, stream); + assertEquals(push.pushId, 1L << 62 - 5); + assertEquals(push.type(), DISPATCHED_STREAM.PUSH); + } + + } + @Test + public void simpleControl() { + simpleStreamType(DISPATCHED_STREAM.CONTROL, Http3Streams.CONTROL_STREAM_CODE); + } + @Test + public void simpleDecoder() { + simpleStreamType(DISPATCHED_STREAM.DECODER, Http3Streams.QPACK_DECODER_STREAM_CODE); + } + @Test + public void simpleEncoder() { + simpleStreamType(DISPATCHED_STREAM.ENCODER, Http3Streams.QPACK_ENCODER_STREAM_CODE); + } + @Test + public void simplePush() { + simpleStreamType(DISPATCHED_STREAM.PUSH, Http3Streams.PUSH_STREAM_CODE); + } + @Test + public void simpleUknown() { + simpleStreamType(DISPATCHED_STREAM.UNKNOWN, VariableLengthEncoder.MAX_ENCODED_INTEGER); + } + @Test + public void simpleReserved() { + simpleStreamType(DISPATCHED_STREAM.RESERVED, 31 * 256 + 2); + } + + @Test + public void multyBytes() { + DISPATCHED_STREAM type = DISPATCHED_STREAM.UNKNOWN; + long code = VariableLengthEncoder.MAX_ENCODED_INTEGER; + System.out.println("Testing multi byte " + type + " with " + code); + QuicReceiverStreamStub stream = new QuicReceiverStreamStub(QuicStreams.UNI_MASK + QuicStreams.SRV_MASK); + PeerUniStreamDispatcherStub dispatcher = new PeerUniStreamDispatcherStub(stream); + QuicStreamReader reader = stream.reader; + SequentialScheduler scheduler = stream.scheduler; + assertTrue(reader.connected()); + int size = VariableLengthEncoder.getEncodedSize(code); + assertEquals(size, 8); + ByteBuffer buffer = ByteBuffer.allocate(size); + assertEquals(buffer.remaining(), size); + VariableLengthEncoder.encode(buffer, code); + buffer.flip(); + dispatcher.start(); + for (int i=0; i FAILURES = new ConcurrentHashMap<>(); + static volatile boolean tasksFailed; + static final AtomicLong serverCount = new AtomicLong(); + static final AtomicLong clientCount = new AtomicLong(); + static final long start = System.nanoTime(); + public static String now() { + long now = System.nanoTime() - start; + long secs = now / 1000_000_000; + long mill = (now % 1000_000_000) / 1000_000; + long nan = now % 1000_000; + return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); + } + + final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + final Set sharedClientHasH3 = ConcurrentHashMap.newKeySet(); + private volatile HttpClient sharedClient; + private boolean directQuicConnectionSupported; + + static class TestExecutor implements Executor { + final AtomicLong tasks = new AtomicLong(); + Executor executor; + TestExecutor(Executor executor) { + this.executor = executor; + } + + @java.lang.Override + public void execute(Runnable command) { + long id = tasks.incrementAndGet(); + executor.execute(() -> { + try { + command.run(); + } catch (Throwable t) { + tasksFailed = true; + System.out.printf(now() + "Task %s failed: %s%n", id, t); + System.err.printf(now() + "Task %s failed: %s%n", id, t); + FAILURES.putIfAbsent("Task " + id, t); + throw t; + } + }); + } + } + + protected boolean stopAfterFirstFailure() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); + } + + @BeforeMethod + void beforeMethod(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + var x = new SkipException("Skipping: some test failed"); + x.setStackTrace(new StackTraceElement[0]); + throw x; + } + } + + @AfterClass + static void printFailedTests() { + out.println("\n========================="); + try { + out.printf("%n%sCreated %d servers and %d clients%n", + now(), serverCount.get(), clientCount.get()); + if (FAILURES.isEmpty()) return; + out.println("Failed tests: "); + FAILURES.forEach((key, value) -> { + out.printf("\t%s: %s%n", key, value); + value.printStackTrace(out); + value.printStackTrace(); + }); + if (tasksFailed) { + System.out.println("WARNING: Some tasks failed"); + } + } finally { + out.println("\n=========================\n"); + } + } + + private String[] uris() { + return new String[] { + h3URI, + }; + } + + @DataProvider(name = "variants") + public Object[][] variants(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + String[] uris = uris(); + Object[][] result = new Object[uris.length * 2 * 2 * 2][]; + int i = 0; + for (var version : List.of(Optional.empty(), Optional.of(HTTP_3))) { + for (Version firstRequestVersion : List.of(HTTP_2, HTTP_3)) { + for (boolean sameClient : List.of(false, true)) { + for (String uri : uris()) { + result[i++] = new Object[]{uri, firstRequestVersion, sameClient, version}; + } + } + } + } + assert i == result.length; + return result; + } + + @DataProvider(name = "uris") + public Object[][] uris(ITestContext context) { + if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { + return new Object[0][]; + } + Object[][] result = {{h3URI}}; + return result; + } + + private HttpClient makeNewClient() { + clientCount.incrementAndGet(); + HttpClient client = newClientBuilderForH3() + .version(HTTP_3) + .proxy(HttpClient.Builder.NO_PROXY) + .executor(executor) + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + return TRACKER.track(client); + } + + HttpClient newHttpClient(boolean share) { + if (!share) return makeNewClient(); + HttpClient shared = sharedClient; + if (shared != null) return shared; + synchronized (this) { + shared = sharedClient; + if (shared == null) { + shared = sharedClient = makeNewClient(); + } + return shared; + } + } + + BodyPublisher oflines(boolean streaming, String ...lines) { + if (streaming) { + return BodyPublishers.fromPublisher(BodyPublishers.concat( + Stream.of(lines) + .map(s -> s + '\n') + .map(BodyPublishers::ofString) + .toArray(BodyPublisher[]::new))); + } else { + return BodyPublishers.fromPublisher(BodyPublishers.concat( + Stream.of(lines) + .map(s -> s + '\n') + .map(BodyPublishers::ofString) + .toArray(BodyPublisher[]::new)), + Stream.of(lines).mapToLong(String::length) + .map((l) -> l+1) + .sum()); + } + } + + + @Test(dataProvider = "variants") + public void testAsync(String uri, Version firstRequestVersion, boolean sameClient, Optional version) throws Exception { + System.out.println("Request to " + uri +"/Async/*" + + ", firstRequestVersion=" + firstRequestVersion + + ", sameclient=" + sameClient + ",version=" + version); + + HttpClient client = newHttpClient(sameClient); + final URI headURI = URI.create(uri + "/Async/First/HEAD"); + final Builder headBuilder = HttpRequest.newBuilder(headURI) + .version(firstRequestVersion) + .HEAD(); + Http3DiscoveryMode config = null; + if (firstRequestVersion == HTTP_3 && !directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + config = ALT_SVC; + } + if (config != null) { + out.println("first request will use " + config); + headBuilder.setOption(H3_DISCOVERY, config); + config = null; + } + + HttpResponse response1 = client.send(headBuilder.build(), BodyHandlers.ofString()); + assertEquals(response1.statusCode(), 200, "Unexpected first response code"); + assertEquals(response1.body(), "", "Unexpected first response body"); + boolean expectH3 = sameClient && sharedClientHasH3.contains(headURI.getRawAuthority()); + if (firstRequestVersion == HTTP_3) { + if (expectH3) { + out.println("Expecting HEAD response over HTTP_3"); + assertEquals(response1.version(), HTTP_3, "Unexpected first response version"); + } + } else { + out.println("Expecting HEAD response over HTTP_2"); + assertEquals(response1.version(), HTTP_2, "Unexpected first response version"); + } + out.println("HEAD response version: " + response1.version()); + if (response1.version() == HTTP_2) { + if (sameClient) { + sharedClientHasH3.add(headURI.getRawAuthority()); + } + expectH3 = version.isEmpty() && client.version() == HTTP_3; + if (version.orElse(null) == HTTP_3 && !directQuicConnectionSupported) { + config = ALT_SVC; + expectH3 = true; + } + // we can expect H3 only if the (default) config is not ANY + if (expectH3) { + out.println("first response came over HTTP/2, so we should expect all responses over HTTP/3"); + } + } else if (response1.version() == HTTP_3) { + expectH3 = directQuicConnectionSupported && version.orElse(null) == HTTP_3; + if (expectH3) { + out.println("first response came over HTTP/3, direct connection supported: expect HTTP/3"); + } else if (firstRequestVersion == HTTP_3 && version.isEmpty() + && config == null && directQuicConnectionSupported) { + config = ANY; + expectH3 = true; + } + } + out.printf("request version: %s, directConnectionSupported: %s, first response: %s," + + " config: %s, expectH3: %s%n", + version, directQuicConnectionSupported, response1.version(), config, expectH3); + if (expectH3) { + out.println("All responses should now come through HTTP/3"); + } + + Builder builder = HttpRequest.newBuilder(); + version.ifPresent(builder::version); + if (config != null) { + builder.setOption(H3_DISCOVERY, config); + } + Map>> responses = new HashMap<>(); + boolean streaming = false; + int h3Count = 0; + for (int i = 0; i < ITERATION_COUNT; i++) { + streaming = !streaming; + HttpRequest request = builder.uri(URI.create(uri+"/Async/"+i)) + .POST(oflines(streaming, BODY.split("\n"))) + .build(); + System.out.println("Iteration: " + request.uri()); + responses.put(request.uri(), client.sendAsync(request, BodyHandlers.ofString())); + } + while (!responses.isEmpty()) { + CompletableFuture.anyOf(responses.values().toArray(CompletableFuture[]::new)).join(); + var done = responses.entrySet().stream() + .filter((e) -> e.getValue().isDone()).toList(); + for (var e : done) { + URI u = e.getKey(); + responses.remove(u); + out.println("Checking response: " + u); + var response = e.getValue().get(); + out.println("Response is: " + response + ", [version: " + response.version() + "]"); + assertEquals(response.statusCode(), 200,"status for " + u); + assertEquals(response.body(), BODY,"body for " + u); + if (expectH3) { + assertEquals(response.version(), HTTP_3, "version for " + u); + } + if (response.version() == HTTP_3) { + h3Count++; + } + } + } + if (client.version() == HTTP_3 || version.orElse(null) == HTTP_3) { + if (h3Count == 0) { + throw new AssertionError("No request used HTTP/3"); + } + } + if (!sameClient) { + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + System.out.println("test: DONE"); + } + + @Test(dataProvider = "uris") + public void testSync(String h3URI) throws Exception { + HttpClient client = makeNewClient(); + Builder builder = HttpRequest.newBuilder(URI.create(h3URI + "/Sync/1")) + .version(HTTP_3); + if (!directQuicConnectionSupported) { + // if the server doesn't listen for HTTP/3 on the same port than TCP, then + // do not attempt to connect to the URI host:port through UDP - as we might + // be connecting to some other server. Once the first request has gone + // through, there should be an AltService record for the server, so + // we should be able to safely use any default config (except + // HTTP_3_URI_ONLY) + builder.setOption(H3_DISCOVERY, ALT_SVC); + } + + HttpRequest request = builder + .POST(oflines(true, BODY.split("\n"))) + .build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + out.println("Response #1: " + response); + out.println("Version #1: " + response.version()); + out.println("Body #1:\n" + response.body().indent(4)); + assertEquals(response.statusCode(), 200, "first response status"); + if (directQuicConnectionSupported) { + // TODO unreliable assertion + //assertEquals(response.version(), HTTP_3, "Unexpected first response version"); + } else { + assertEquals(response.version(), HTTP_2, "Unexpected first response version"); + } + assertEquals(response.body(), BODY, "first response body"); + + request = builder.uri(URI.create(h3URI + "/Sync/2")) + .POST(oflines(true, BODY.split("\n"))) + .build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #2: " + response); + out.println("Version #2: " + response.version()); + out.println("Body #2:\n" + response.body().indent(4)); + assertEquals(response.statusCode(), 200, "second response status"); + assertEquals(response.version(), HTTP_3, "second response version"); + assertEquals(response.body(), BODY, "second response body"); + + request = builder.uri(URI.create(h3URI + "/Sync/3")) + .POST(oflines(true, BODY.split("\n"))) + .build(); + response = client.send(request, BodyHandlers.ofString()); + out.println("Response #3: " + response); + out.println("Version #3: " + response.version()); + out.println("Body #3:\n" + response.body().indent(4)); + assertEquals(response.statusCode(), 200, "third response status"); + assertEquals(response.version(), HTTP_3, "third response version"); + assertEquals(response.body(), BODY, "third response body"); + + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + AssertionError error = TRACKER.check(tracker, 1000); + if (error != null) throw error; + } + + @BeforeTest + public void setup() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) + throw new AssertionError("Unexpected null sslContext"); + final Http2TestServer h2WithAltService = new Http2TestServer("localhost", true, sslContext) + .enableH3AltServiceOnSamePort(); + h3TestServer = HttpTestServer.of(h2WithAltService); + h3TestServer.addHandler(new Handler(), "/h3/testH3/"); + h3URI = "https://" + h3TestServer.serverAuthority() + "/h3/testH3/POST"; + + serverCount.addAndGet(1); + h3TestServer.start(); + directQuicConnectionSupported = h2WithAltService.supportsH3DirectConnection(); + } + + @AfterTest + public void teardown() throws Exception { + System.err.println("======================================================="); + System.err.println(" Tearing down test"); + System.err.println("======================================================="); + String sharedClientName = + sharedClient == null ? null : sharedClient.toString(); + sharedClient = null; + Thread.sleep(100); + AssertionError fail = TRACKER.check(500); + try { + h3TestServer.stop(); + } finally { + if (fail != null) { + if (sharedClientName != null) { + System.err.println("Shared client name is: " + sharedClientName); + } + throw fail; + } + } + } + + static class Handler implements HttpTestHandler { + public Handler() {} + + volatile int invocation = 0; + + @java.lang.Override + public void handle(HttpTestExchange t) + throws IOException { + try { + URI uri = t.getRequestURI(); + System.err.printf("Handler received request for %s\n", uri); + + boolean head = "HEAD".equals(t.getRequestMethod()); + if ((invocation++ % 2) == 1 && !head) { + System.err.printf("Server sending %d - chunked\n", 200); + t.sendResponseHeaders(200, -1); + } else { + System.err.printf("Server sending %d - %s length\n", 200, BODY.length()); + t.sendResponseHeaders(200, BODY.length()); + } + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + reader.lines().forEach((line) -> { + try { + if (head) return; + os.write(line.getBytes(StandardCharsets.UTF_8)); + os.write('\n'); + os.flush(); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + }); + } + } catch (Throwable e) { + e.printStackTrace(System.err); + throw new IOException(e); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/StopSendingTest.java b/test/jdk/java/net/httpclient/http3/StopSendingTest.java new file mode 100644 index 00000000000..2a029e176a2 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/StopSendingTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.internal.net.http.ResponseSubscribers; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary Exercises the HTTP3 client to send a STOP_SENDING frame + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * @compile ../ReferenceTracker.java + * @run testng/othervm -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.HttpClient.log=requests,responses,errors StopSendingTest + */ +public class StopSendingTest implements HttpServerAdapters { + + private SSLContext sslContext; + private HttpTestServer h3Server; + private String requestURIBase; + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + h3Server = HttpTestServer.create(HTTP_3_URI_ONLY, sslContext); + h3Server.addHandler(new Handler(), "/hello"); + h3Server.start(); + System.out.println("Server started at " + h3Server.getAddress()); + requestURIBase = URIBuilder.newBuilder().scheme("https").loopback() + .port(h3Server.getAddress().getPort()).build().toString(); + + } + + @AfterClass + public void afterClass() throws Exception { + if (h3Server != null) { + System.out.println("Stopping server " + h3Server.getAddress()); + h3Server.stop(); + } + } + + private static final class Handler implements HttpTestHandler { + private static final byte[] DUMMY_BODY = "foo bar hello world".getBytes(StandardCharsets.UTF_8); + private static volatile boolean stop; + private static final CountDownLatch stopped = new CountDownLatch(1); + + /** + * Keeps writing out response data (bytes) until asked to stop + */ + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + System.out.println("Handling request: " + exchange.getRequestURI()); + exchange.sendResponseHeaders(200, -1); + try (final OutputStream os = exchange.getResponseBody()) { + while (!stop) { + os.write(DUMMY_BODY); + os.flush(); + System.out.println("Wrote response data of size " + DUMMY_BODY.length); + try { + Thread.sleep(5); + } catch (InterruptedException e) { + // ignore + } + } + System.out.println("Stopped writing response"); + } catch (IOException io) { + System.out.println("Got expected exception: " + io); + } finally { + stopped.countDown(); + } + } + } + + /** + * Issues a HTTP3 request to a server handler which keeps sending data. When some amount of + * data is received on the client side, the request is cancelled by the test method. This + * internally is expected to trigger a STOP_SENDING frame from the HTTP client to the server. + */ + @Test + public void testStopSending() throws Exception { + HttpClient client = newClientBuilderForH3() + .version(Version.HTTP_3) + .sslContext(sslContext).build(); + final URI reqURI = new URI(requestURIBase + "/hello"); + final HttpRequest req = HttpRequest.newBuilder(reqURI) + .version(Version.HTTP_3) + .setOption(H3_DISCOVERY, HTTP_3_URI_ONLY) + .build(); + // used to wait and trigger a request cancellation + final CountDownLatch cancellationTrigger = new CountDownLatch(1); + System.out.println("Issuing request to " + reqURI); + final CompletableFuture> futureResp = client.sendAsync(req, + BodyHandlers.fromSubscriber(new CustomBodySubscriber(cancellationTrigger))); + // wait for the subscriber to receive some amount of response data before we trigger + // the request cancellation + System.out.println("Awaiting some response data to arrive"); + cancellationTrigger.await(); + System.out.println("Cancelling request"); + // cancel the request which will internal trigger a STOP_SENDING frame from the HTTP + // client to the server + final boolean cancelled = futureResp.cancel(true); + System.out.println("Cancelled request: " + cancelled); + try { + // we expect a CancellationException for a cancelled request, + // but due to a bug (race condition) in the HttpClient's implementation + // of the Future instance, sometimes the Future.cancel(true) results + // in an ExecutionException which wraps the CancellationException. + // TODO: fix the actual race condition and then expect only CancellationException here + final Exception actualException = Assert.expectThrows(Exception.class, futureResp::get); + if (actualException instanceof CancellationException) { + // expected + System.out.println("Received the expected CancellationException"); + } else if (actualException instanceof ExecutionException + && actualException.getCause() instanceof CancellationException) { + System.out.println("Received CancellationException wrapped as ExecutionException"); + } else { + // unexpected + throw actualException; + } + } catch (Exception | Error e) { + Handler.stop = true; + System.err.println("Unexpected exception: " + e); + e.printStackTrace(); + throw e; + } finally { + // wait until the handler stops sending + Handler.stopped.await(10, TimeUnit.SECONDS); + } + var TRACKER = ReferenceTracker.INSTANCE; + var tracker = TRACKER.getTracker(client); + client = null; + System.gc(); + var error = TRACKER.check(tracker,1000); + if (error != null) throw error; + } + + /** + * A {@link java.net.http.HttpResponse.BodySubscriber} which informs any interested parties + * whenever it receives any data in {@link #onNext(List)} + */ + private static final class CustomBodySubscriber extends ResponseSubscribers.ByteArraySubscriber { + // the latch used to inform any interested parties about data being received + private final CountDownLatch latch; + + private CustomBodySubscriber(final CountDownLatch latch) { + // a finisher which just returns the bytes back + super((bytes) -> bytes); + this.latch = latch; + } + + @Override + public void onNext(final List items) { + super.onNext(items); + long totalSize = 0; + for (final ByteBuffer bb : items) { + totalSize += bb.remaining(); + } + System.out.println("Subscriber got response data of size " + totalSize); + // inform interested party that we received some data + latch.countDown(); + } + } +} diff --git a/test/jdk/java/net/httpclient/http3/StreamLimitTest.java b/test/jdk/java/net/httpclient/http3/StreamLimitTest.java new file mode 100644 index 00000000000..d8a920c8544 --- /dev/null +++ b/test/jdk/java/net/httpclient/http3/StreamLimitTest.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.SocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestExchange; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestHandler; +import jdk.httpclient.test.lib.common.HttpServerAdapters.HttpTestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static java.net.http.HttpClient.Version.HTTP_3; +import static java.net.http.HttpOption.H3_DISCOVERY; + +/* + * @test + * @summary verifies that when the Quic stream limit is reached + * then HTTP3 requests are retried on newer connection + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.test.lib.net.SimpleSSLContext + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http3.Http3TestServer + * @run testng/othervm -Djdk.internal.httpclient.debug=true StreamLimitTest + */ +public class StreamLimitTest { + + private SSLContext sslContext; + private HttpTestServer server; + private QuicServer quicServer; + private URI requestURI; + private volatile QuicServerConnection latestServerConn; + + private final class Handler implements HttpTestHandler { + + @Override + public void handle(HttpTestExchange exchange) throws IOException { + final String handledBy = latestServerConn.logTag(); + System.out.println(handledBy + " handling request " + exchange.getRequestURI()); + final byte[] respBody; + if (handledBy == null) { + respBody = new byte[0]; + } else { + respBody = handledBy.getBytes(StandardCharsets.UTF_8); + } + exchange.sendResponseHeaders(200, respBody.length == 0 ? -1 : respBody.length); + // write out the server's connection id as a response + if (respBody.length > 0) { + try (OutputStream os = exchange.getResponseBody()) { + os.write(respBody); + } + } + exchange.close(); + } + } + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + quicServer = Http3TestServer.quicServerBuilder().sslContext(sslContext).build(); + final Http3TestServer h3Server = new Http3TestServer(quicServer) { + @Override + public boolean acceptIncoming(SocketAddress source, QuicServerConnection quicConn) { + final boolean accepted = super.acceptIncoming(source, quicConn); + if (accepted) { + // keep track of the latest server connection + latestServerConn = quicConn; + } + return accepted; + } + }; + server = HttpTestServer.of(h3Server); + server.addHandler(new Handler(), "/foo"); + server.start(); + System.out.println("Server started at " + server.serverAuthority()); + requestURI = new URI("https://" + server.serverAuthority() + "/foo"); + } + + @AfterClass + public void afterClass() throws Exception { + latestServerConn = null; + if (server != null) { + server.stop(); + } + } + + /** + * Configures different limits for max bidi stream creation by HTTP client and then verifies + * the expected behaviour by sending HTTP3 requests + */ + @Test + public void testBidiMaxStreamLimit() throws Exception { + final QuicTransportParameters transportParameters = new QuicTransportParameters(); + final int intialMaxStreamLimit = 3; + final AtomicInteger maxStreamLimit = new AtomicInteger(intialMaxStreamLimit); + transportParameters.setIntParameter(ParameterId.initial_max_streams_bidi, + maxStreamLimit.get()); + // set the limit so that any new server connections created with advertise this limit + // to the peer (client) + quicServer.setTransportParameters(transportParameters); + // also set a MAX_STREAMS limit computer for this server so that the created + // connections use this computer for deciding MAX_STREAMS limit + quicServer.setMaxStreamLimitComputer((ignore) -> maxStreamLimit.longValue()); + final HttpClient client = HttpServerAdapters.createClientBuilderForH3() + .proxy(NO_PROXY) + .sslContext(sslContext) + .build(); + final HttpRequest req = HttpRequest.newBuilder().version(HTTP_3) + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, server.h3DiscoveryConfig()) + .build(); + String requestHandledBy = null; + System.out.println("Server has been configured with a limit for max" + + " bidi streams: " + intialMaxStreamLimit); + // issue N number of requests where N == the max bidi stream creation limit that is + // advertised to the peer. All these N requests are expected to be handled by the same + // server connection + for (int i = 1; i <= intialMaxStreamLimit; i++) { + System.out.println("Sending request " + i + " to " + requestURI); + final HttpResponse resp = client.send(req, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(resp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(resp.statusCode(), 200, "Unexpected response code"); + final String respBody = resp.body(); + System.out.println("Request " + i + " was handled by server connection: " + respBody); + if (i == 1) { + // first request; keep track the server connection id which responded + // to this request + requestHandledBy = respBody; + } else { + Assert.assertEquals(respBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } + } + // at this point the limit for bidi stream creation has reached on the client. + // now issue a request so that: + // - the HTTP client implementation notices that it has hit the limit + // - HTTP client sends a STREAMS_BLOCKED frame and waits for a while (timeout derived + // internally based on request timeout) for server to increase the limit. But this server + // connection will not send any MAX_STREAMS frame upon receipt of STREAMS_BLOCKED frame + // - client notices that server connection hasn't increased the stream limit, so internally + // retries the request, which should trigger a new server connection and thus this + // request should be handled by a different server connection than the last N requests + final HttpRequest reqWithTimeout = HttpRequest.newBuilder().version(HTTP_3) + .GET().uri(requestURI) + .setOption(H3_DISCOVERY, server.h3DiscoveryConfig()) + .timeout(Duration.of(10, ChronoUnit.SECONDS)) + .build(); + for (int i = 1; i <= intialMaxStreamLimit; i++) { + System.out.println("Sending request " + i + " (configured with timeout) to " + + requestURI); + final HttpResponse resp = client.send(reqWithTimeout, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(resp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(resp.statusCode(), 200, "Unexpected response code"); + final String respBody = resp.body(); + System.out.println("Request " + i + " was handled by server connection: " + respBody); + if (i == 1) { + // first request after the limit was hit. + // verify that it was handled by a new connection and not the one that handled + // the previous N requests + Assert.assertNotEquals(respBody, requestHandledBy, "Request was expected to be" + + " handled by a new server connection, but wasn't"); + // keep track this new server connection id which responded to this request + requestHandledBy = respBody; + } else { + Assert.assertEquals(respBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } + } + // at this point the limit for bidi stream creation has reached on the client, for + // this new server connection. we now configure this current server connection to + // increment the limit to new higher limit + maxStreamLimit.set(intialMaxStreamLimit + 2); + System.out.println("Server connection " + latestServerConn + " has now been configured to" + + " increment the bidi stream creation limit to " + maxStreamLimit.get()); + // we now issue new requests, with timeout specified + // - the HTTP client implementation notices that it has hit the limit + // - HTTP client sends a STREAMS_BLOCKED frame and waits for a while (timeout derived + // internally based on request timeout) for server to increase the limit. This server + // connection, since it is configured to increment the stream limit, will send + // MAX_STREAMS frame upon receipt of STREAMS_BLOCKED frame + // - client receives the MAX_STREAMS frame (hopefully within the timeout) and + // notices that server connection has increased the stream limit, so opens a new bidi + // stream and lets the request move forward (on the same server connection) + final int numNewRequests = maxStreamLimit.get() - intialMaxStreamLimit; + for (int i = 1; i <= numNewRequests; i++) { + System.out.println("Sending request " + i + " (after stream limit has been increased)" + + " to " + requestURI); + final HttpResponse resp = client.send(reqWithTimeout, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(resp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(resp.statusCode(), 200, "Unexpected response code"); + final String respBody = resp.body(); + System.out.println("Request " + i + " was handled by server connection: " + respBody); + // all these requests should be handled by the same server connection which handled + // the previous requests + Assert.assertEquals(respBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } + // at this point the newer limit for bidi stream creation has reached on the client. + // we now issue a new request without any timeout configured on the request, so that the + // client internally just immediately retries and uses a different connection on noticing + // that the stream creation limit for the current server connection has been reached. + System.out.println("Server connection " + latestServerConn + " has now been configured to" + + " not increase max stream limit for bidi streams created by the client"); + final HttpRequest finalReq = HttpRequest.newBuilder() + .version(HTTP_3) + .setOption(H3_DISCOVERY, server.h3DiscoveryConfig()) + .GET().uri(requestURI) + .build(); + System.out.println("Sending request, without timeout, to " + requestURI); + final HttpResponse finalResp = client.send(finalReq, + HttpResponse.BodyHandlers.ofString()); + Assert.assertEquals(finalResp.version(), HTTP_3, "Unexpected response version"); + Assert.assertEquals(finalResp.statusCode(), 200, "Unexpected response code"); + final String finalRespBody = finalResp.body(); + System.out.println("Request was handled by server connection: " + finalRespBody); + // this request should have been handled by a new server connection + Assert.assertNotEquals(finalRespBody, requestHandledBy, "Request was handled by an" + + " unexpected server connection"); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/DynamicKeyStoreUtil.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/DynamicKeyStoreUtil.java new file mode 100644 index 00000000000..5b96d4c0b76 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/DynamicKeyStoreUtil.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.httpclient.test.lib.common; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Objects; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import sun.security.x509.CertificateExtensions; +import sun.security.x509.CertificateSerialNumber; +import sun.security.x509.CertificateValidity; +import sun.security.x509.CertificateVersion; +import sun.security.x509.CertificateX509Key; +import sun.security.x509.DNSName; +import sun.security.x509.GeneralName; +import sun.security.x509.GeneralNames; +import sun.security.x509.SubjectAlternativeNameExtension; +import sun.security.x509.X500Name; +import sun.security.x509.X509CertImpl; +import sun.security.x509.X509CertInfo; + +/** + * A utility for generating dynamic {@link java.security.KeyStore}s in tests. The keystores + * generated by this utility are dynamic in the sense that they are generated as necessary and + * don't require keys/certificates to be present on the filesystem. The generated keystores too + * aren't saved to the filesystem, by this utility. + */ +public class DynamicKeyStoreUtil { + + private static final String PKCS12_KEYSTORE_TYPE = "PKCS12"; + + /** + * The default alias that will be used in the KeyStore generated by + * {@link #generateKeyStore(String, String...)} + */ + public static final String DEFAULT_ALIAS = "foobar-key-alias"; + + private static final String DEFAULT_SUBJECT_OU = "foobar-org-unit"; + private static final String DEFAULT_SUBJECT_ON = "foobar-org-name"; + private static final String DEFAULT_SUBJECT_COUNTRY = "US"; + + + /** + * Generates a PKCS12 type {@link KeyStore} which has one + * {@link KeyStore#getKey(String, char[]) key entry} which corresponds to a newly generated + * {@link java.security.PrivateKey} and accompanied by a newly generated self-signed certificate, + * certifying the corresponding public key. + *

        + * The newly generated {@link X509Certificate} certificate will use the {@code certSubject} + * as the certificate's subject. If the {@code certSubjectAltNames} is non-null then + * the certificate will be created with a + * {@link sun.security.x509.SubjectAlternativeNameExtension subject alternative name extension} + * which will include the {@code certSubject} and each of the {@code certSubjectAltNames} as + * subject alternative names (represented as DNS names) + *

        + * The generated KeyStore won't be password protected + * + * @param certSubject The subject to be used for the newly generated certificate + * @param certSubjectAltNames Optional subject alternative names to be used in the generated + * certificate + * @return The newly generated KeyStore + * @throws NullPointerException If {@code certSubject} is null + */ + public static KeyStore generateKeyStore(final String certSubject, final String... certSubjectAltNames) + throws Exception { + Objects.requireNonNull(certSubject); + final SecureRandom secureRandom = new SecureRandom(); + final KeyPair keyPair = generateRSAKeyPair(secureRandom); + final X509Certificate cert = generateCert(keyPair, secureRandom, certSubject, certSubjectAltNames); + final KeyStore.Builder keystoreBuilder = KeyStore.Builder.newInstance(PKCS12_KEYSTORE_TYPE, + null, new KeyStore.PasswordProtection(null)); + final KeyStore keyStore = keystoreBuilder.getKeyStore(); + // write a private key (with cert chain for the public key) + final char[] keyPassword = null; + keyStore.setKeyEntry(DEFAULT_ALIAS, keyPair.getPrivate(), keyPassword, new Certificate[]{cert}); + return keyStore; + } + + /** + * Generates a PKCS12 type {@link KeyStore} which has one + * {@link KeyStore#getKey(String, char[]) key entry} which corresponds to the {@code privateKey} + * and accompanied by the {@code certChain} certifying the corresponding public key. + *

        + * The generated KeyStore won't be password protected + * + * @param privateKey The private key to include in the keystore + * @param certChain The certificate chain + * @return The newly generated KeyStore + * @throws NullPointerException If {@code privateKey} or {@code certChain} is null + */ + public static KeyStore generateKeyStore(final PrivateKey privateKey, final Certificate[] certChain) + throws Exception { + Objects.requireNonNull(privateKey); + Objects.requireNonNull(certChain); + final KeyStore.Builder keystoreBuilder = KeyStore.Builder.newInstance(PKCS12_KEYSTORE_TYPE, + null, new KeyStore.PasswordProtection(null)); + final KeyStore keyStore = keystoreBuilder.getKeyStore(); + // write a private key (with cert chain for the public key) + final char[] keyPassword = null; + keyStore.setKeyEntry(DEFAULT_ALIAS, privateKey, keyPassword, certChain); + return keyStore; + } + + /** + * Creates and returns a new PKCS12 type keystore without any entries in the keystore + * + * @return The newly created keystore + * @throws Exception if any exception occurs during keystore generation + */ + public static KeyStore generateBlankKeyStore() throws Exception { + final KeyStore.Builder keystoreBuilder = KeyStore.Builder.newInstance(PKCS12_KEYSTORE_TYPE, + null, new KeyStore.PasswordProtection(null)); + return keystoreBuilder.getKeyStore(); + } + + /** + * Generates a {@link KeyPair} using the {@code secureRandom} + * + * @param secureRandom The SecureRandom + * @return The newly generated KeyPair + * @throws NoSuchAlgorithmException + */ + public static KeyPair generateRSAKeyPair(final SecureRandom secureRandom) + throws NoSuchAlgorithmException { + final String keyType = "RSA"; + final int defaultRSAKeySize = 3072; + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyType); + keyPairGenerator.initialize(defaultRSAKeySize, secureRandom); + //final String sigAlgName = "SHA384withRSA"; + final KeyPair pair = keyPairGenerator.generateKeyPair(); + final PublicKey publicKey = pair.getPublic(); + // publicKey's format must be X.509 otherwise + if (!"X.509".equalsIgnoreCase(publicKey.getFormat())) { + throw new IllegalArgumentException("Public key format " + publicKey.getFormat() + + ", isn't X.509"); + } + return pair; + } + + /** + * Generates a X509 certificate using the passed {@code keyPair} + * + * @param keyPair The KeyPair to use + * @param secureRandom The SecureRandom + * @param subjectName The subject to use in the certificate + * @param subjectAltNames Any subject alternate names to use in the certificate + * @return The newly generated certificate + * @throws Exception + */ + public static X509Certificate generateCert(final KeyPair keyPair, final SecureRandom secureRandom, + final String subjectName, final String... subjectAltNames) + throws Exception { + final X500Name subject = new X500Name(subjectName, DEFAULT_SUBJECT_OU, DEFAULT_SUBJECT_ON, + DEFAULT_SUBJECT_COUNTRY); + final X500Name issuer = subject; // self-signed cert + final GeneralNames generalNames; + if (subjectAltNames == null) { + generalNames = null; + } else { + generalNames = new GeneralNames(); + generalNames.add(new GeneralName(new DNSName(subjectName, true))); + for (final String san : subjectAltNames) { + if (san == null) { + continue; + } + final DNSName dnsName = new DNSName(san, true); + generalNames.add(new GeneralName(dnsName)); + } + } + return generateCert(keyPair, secureRandom, subject, issuer, generalNames); + } + + private static X509Certificate generateCert(final KeyPair keyPair, final SecureRandom secureRandom, + final X500Name subjectName, final X500Name issuerName, + final GeneralNames subjectAltNames) + throws Exception { + final X509CertInfo certInfo = new X509CertInfo(); + certInfo.setVersion(new CertificateVersion(CertificateVersion.V3)); + certInfo.setSerialNumber(CertificateSerialNumber.newRandom64bit(secureRandom)); + certInfo.setSubject(subjectName); + final PublicKey publicKey = keyPair.getPublic(); + certInfo.setKey(new CertificateX509Key(publicKey)); + certInfo.setValidity(certDuration()); + certInfo.setIssuer(issuerName); + if (subjectAltNames != null && !subjectAltNames.isEmpty()) { + final SubjectAlternativeNameExtension sanExtn = new SubjectAlternativeNameExtension( + true, subjectAltNames); + final CertificateExtensions certExtensions = new CertificateExtensions(); + certExtensions.setExtension(sanExtn.getId(), sanExtn); + certInfo.setExtensions(certExtensions); + } + final PrivateKey privateKey = keyPair.getPrivate(); + final String sigAlgName = "SHA384withRSA"; + return X509CertImpl.newSigned(certInfo, privateKey, sigAlgName); + } + + private static CertificateValidity certDuration() { + // create a cert with 1 day validity, starting from 1 minute back + final long currentTime = System.currentTimeMillis(); + final long oneMinuteInThePast = currentTime - (60 * 1000); + final long oneDayInTheFuture = currentTime + (24 * 60 * 60 * 1000); + final Date startDate = new Date(oneMinuteInThePast); + final Date expiryDate = new Date(oneDayInTheFuture); + return new CertificateValidity(startDate, expiryDate); + } + + /** + * Creates a {@link SSLContext} which is + * {@link SSLContext#init(KeyManager[], TrustManager[], SecureRandom) initialized} using the + * {@link javax.net.ssl.KeyManager}s and {@link javax.net.ssl.TrustManager}s available in the + * {@code keyStore}. The {@code keyStore} is expected to be password-less. + * + * @param keyStore The password-less keystore + * @return the SSLContext + * @throws Exception + */ + public static SSLContext createSSLContext(final KeyStore keyStore) throws Exception { + Objects.requireNonNull(keyStore); + final char[] password = null; + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX"); + kmf.init(keyStore, password); + + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(keyStore); + + final String protocol = "TLS"; + final SSLContext ctx = SSLContext.getInstance(protocol); + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return ctx; + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java index 93a93ad25d2..10633340a66 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java @@ -34,31 +34,46 @@ import com.sun.net.httpserver.HttpsServer; import jdk.httpclient.test.lib.http2.Http2Handler; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.qpack.Encoder; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.net.InetAddress; import java.io.ByteArrayInputStream; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Builder; import java.net.http.HttpClient.Version; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; -import java.io.UncheckedIOException; -import java.math.BigInteger; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpHeaders; +import java.net.http.HttpOption.Http3DiscoveryMode; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Base64; +import java.util.HashSet; +import java.util.HexFormat; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,9 +81,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; +import static jdk.test.lib.Asserts.assertFileContentsEqual; /** * Defines an adaptation layers so that a test server handlers and filters @@ -93,24 +111,14 @@ import static java.net.http.HttpClient.Version.HTTP_2; */ public interface HttpServerAdapters { - static final boolean PRINTSTACK = - Boolean.getBoolean("jdk.internal.httpclient.debug"); - - static void uncheckedWrite(ByteArrayOutputStream baos, byte[] ba) { - try { - baos.write(ba); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + static final boolean PRINTSTACK = getPrintStack(); + private static boolean getPrintStack() { + return Boolean.getBoolean("jdk.internal.httpclient.debug"); } + static final HexFormat HEX_FORMAT = HexFormat.ofDelimiter(":").withUpperCase(); static void printBytes(PrintStream out, String prefix, byte[] bytes) { - int padding = 4 + 4 - (bytes.length % 4); - padding = padding > 4 ? padding - 4 : 4; - byte[] bigbytes = new byte[bytes.length + padding]; - System.arraycopy(bytes, 0, bigbytes, padding, bytes.length); - out.println(prefix + bytes.length + " " - + new BigInteger(bigbytes).toString(16)); + out.println(prefix + bytes.length + " " + HEX_FORMAT.formatHex(bytes)); } /** @@ -122,6 +130,7 @@ public interface HttpServerAdapters { public abstract Set>> entrySet(); public abstract List get(String name); public abstract boolean containsKey(String name); + public abstract OptionalLong firstValueAsLong(String name); @Override public boolean equals(Object o) { if (this == o) return true; @@ -166,6 +175,11 @@ public interface HttpServerAdapters { return headers.containsKey(name); } @Override + public OptionalLong firstValueAsLong(String name) { + return Optional.ofNullable(headers.getFirst(name)) + .stream().mapToLong(Long::parseLong).findFirst(); + } + @Override public String toString() { return String.valueOf(headers); } @@ -192,6 +206,10 @@ public interface HttpServerAdapters { return headers.firstValue(name).isPresent(); } @Override + public OptionalLong firstValueAsLong(String name) { + return headers.firstValueAsLong(name); + } + @Override public String toString() { return String.valueOf(headers); } @@ -244,18 +262,169 @@ public interface HttpServerAdapters { public abstract String getRequestMethod(); public abstract void close(); public abstract InetSocketAddress getRemoteAddress(); - public abstract String getConnectionKey(); public abstract InetSocketAddress getLocalAddress(); - public void serverPush(URI uri, HttpHeaders headers, byte[] body) { + public abstract String getConnectionKey(); + public abstract SSLSession getSSLSession(); + public void serverPush(URI uri, HttpHeaders reqHeaders, byte[] body) throws IOException { ByteArrayInputStream bais = new ByteArrayInputStream(body); - serverPush(uri, headers, bais); + serverPush(uri, reqHeaders, bais); } - public void serverPush(URI uri, HttpHeaders headers, InputStream body) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, byte[] body) throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(body); + serverPush(uri, reqHeaders, rspHeaders, bais); + } + public void serverPush(URI uri, HttpHeaders reqHeaders, InputStream body) + throws IOException { + serverPush(uri, reqHeaders, HttpHeaders.of(Map.of(), (n,v) -> true), body); + } + + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) + throws IOException { throw new UnsupportedOperationException("serverPush with " + getExchangeVersion()); } + + public void requestStopSending(long errorCode) { + throw new UnsupportedOperationException("sendHttp3ConnectionClose with " + getExchangeVersion()); + } + + /** + * Sends an HTTP/3 PUSH_PROMISE frame, for the given {@code uri}, + * with the given request {@code reqHeaders}, and opens a push promise + * stream to send the given response {@code rspHeaders} and {@code body}. + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param uri the push promise URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise request headers + * @param body the push response body + * + * @return the pushId used to push the promise + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public long http3ServerPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) + throws IOException { + throw new UnsupportedOperationException("serverPushWithId with " + getExchangeVersion()); + } + /** + * Sends an HTTP/3 PUSH_PROMISE frame, for the given {@code uri}, + * with the given request {@code headers}, and with the given + * {@code pushId}. This method only sends the PUSH_PROMISE frame + * and doesn't open any push stream. + * + * @apiNote + * This method can be used to send a PUSH_PROMISE whose body has + * already been promised by calling {@link + * #http3ServerPush(URI, HttpHeaders, HttpHeaders, InputStream)}. In that case + * the {@code pushId} returned by {@link + * #http3ServerPush(URI, HttpHeaders, HttpHeaders, InputStream)} should be passed + * as parameter. Otherwise, if {@code pushId=-1} is passed as parameter, + * a new pushId will be allocated. The push response headers and body + * can be later sent using {@link + * #sendHttp3PushResponse(long, URI, HttpHeaders, HttpHeaders, InputStream)}. + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the pushId to use, or {@code -1} if a new + * pushId should be allocated. + * @param uri the push promise URI + * @param headers the push promise request headers + * @return the given pushId, if positive, otherwise the new allocated pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public long sendHttp3PushPromiseFrame(long pushId, URI uri, HttpHeaders headers) + throws IOException { + throw new UnsupportedOperationException("serverPushId with " + getExchangeVersion()); + } + /** + * Opens an HTTP/3 PUSH_STREAM to send a push promise response headers + * and body. + * + * @apiNote + * No check is performed on the provided pushId + * + * @param pushId a positive pushId obtained from {@link + * #sendHttp3PushPromiseFrame(long, URI, HttpHeaders)} + * @param uri the push request URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise response headers + * @param body the push response body + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + */ + public void sendHttp3PushResponse(long pushId, URI uri, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream body) + throws IOException { + throw new UnsupportedOperationException("serverPushWithId with " + getExchangeVersion()); + } + /** + * Sends an HTTP/3 CANCEL_PUSH frame to cancel a push that has been + * promised by either {@link #http3ServerPush(URI, HttpHeaders, HttpHeaders, InputStream)} + * or {@link #sendHttp3PushPromiseFrame(long, URI, HttpHeaders)}. + * + * This method doesn't cancel the push stream but just sends + * a CANCEL_PUSH frame. + * Note that if the push stream has already been opened this + * method may not have any effect. + * + * @apiNote + * No check is performed on the provided pushId + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the cancelled pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public void sendHttp3CancelPushFrame(long pushId) + throws IOException { + throw new UnsupportedOperationException("cancelPushId with " + getExchangeVersion()); + } + /** + * Waits until the given {@code pushId} is allowed by the HTTP/3 peer + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId a pushId + * + * @return the maximum pushId allowed (exclusive) + * + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + public long waitForHttp3MaxPushId(long pushId) + throws InterruptedException { + throw new UnsupportedOperationException("waitForMaxPushId with " + getExchangeVersion()); + } public boolean serverPushAllowed() { return false; } + public Encoder qpackEncoder() { + throw new UnsupportedOperationException("qpackEncoder with " + getExchangeVersion()); + } + public CompletableFuture clientHttp3Settings() { + throw new UnsupportedOperationException("HTTP/3 client connection settings with " + + getExchangeVersion()); + } public static HttpTestExchange of(HttpExchange exchange) { return new Http1TestExchange(exchange); } @@ -265,6 +434,10 @@ public interface HttpServerAdapters { abstract void doFilter(Filter.Chain chain) throws IOException; + public void resetStream(long code) throws IOException { + throw new UnsupportedOperationException(String.valueOf(this.getServerVersion())); + } + // implementations... private static final class Http1TestExchange extends HttpTestExchange { private final HttpExchange exchange; @@ -303,7 +476,8 @@ public interface HttpServerAdapters { } @Override public void close() { exchange.close(); } - + @Override + public SSLSession getSSLSession() { return null; } @Override public InetSocketAddress getRemoteAddress() { return exchange.getRemoteAddress(); @@ -334,9 +508,9 @@ public interface HttpServerAdapters { this.exchange = exch; } @Override - public Version getServerVersion() { return HTTP_2; } + public Version getServerVersion() { return exchange.getServerVersion(); } @Override - public Version getExchangeVersion() { return HTTP_2; } + public Version getExchangeVersion() { return exchange.getServerVersion(); } @Override public InputStream getRequestBody() { return exchange.getRequestBody(); @@ -365,15 +539,61 @@ public interface HttpServerAdapters { return exchange.serverPushAllowed(); } @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream body) { - exchange.serverPush(uri, headers, body); + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) + throws IOException { + exchange.serverPush(uri, reqHeaders, rspHeaders, body); } + @Override + public void requestStopSending(long errorCode) { + exchange.requestStopSending(errorCode); + } + @Override + public void resetStream(long code) throws IOException { + exchange.resetStream(code); + } + + @Override + public long http3ServerPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream body) throws IOException { + return exchange.serverPushWithId(uri, reqHeaders, rspHeaders, body); + } + @Override + public long sendHttp3PushPromiseFrame(long pushId, URI uri, HttpHeaders reqHeaders) throws IOException { + return exchange.sendPushId(pushId, uri, reqHeaders); + } + @Override + public void sendHttp3CancelPushFrame(long pushId) throws IOException { + exchange.cancelPushId(pushId); + } + @Override + public void sendHttp3PushResponse(long pushId, + URI uri, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream body) throws IOException { + exchange.sendPushResponse(pushId, uri, reqHeaders, rspHeaders, body); + } + @Override + public long waitForHttp3MaxPushId(long pushId) throws InterruptedException { + return exchange.waitForMaxPushId(pushId); + } + @Override + public Encoder qpackEncoder() { + return exchange.qpackEncoder(); + } + + @Override + public CompletableFuture clientHttp3Settings() { + return exchange.clientHttp3Settings(); + } + + @Override void doFilter(Filter.Chain filter) throws IOException { throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server"); } @Override public void close() { exchange.close();} - + @Override + public SSLSession getSSLSession() { return exchange.getSSLSession();} @Override public InetSocketAddress getRemoteAddress() { return exchange.getRemoteAddress(); @@ -400,31 +620,6 @@ public interface HttpServerAdapters { } - - /** - * A version agnostic adapter class for HTTP Server Handlers. - */ - public interface HttpTestHandler { - void handle(HttpTestExchange t) throws IOException; - - default HttpHandler toHttpHandler() { - return (t) -> doHandle(HttpTestExchange.of(t)); - } - default Http2Handler toHttp2Handler() { - return (t) -> doHandle(HttpTestExchange.of(t)); - } - private void doHandle(HttpTestExchange t) throws IOException { - try { - handle(t); - } catch (Throwable x) { - System.out.println("WARNING: exception caught in HttpTestHandler::handle " + x); - System.err.println("WARNING: exception caught in HttpTestHandler::handle " + x); - if (PRINTSTACK && !expectException(t)) x.printStackTrace(System.out); - throw x; - } - } - } - /** * An {@link HttpTestHandler} that handles only HEAD and GET * requests. If another method is used 405 is returned with @@ -473,24 +668,192 @@ public interface HttpServerAdapters { } + /** + * A version agnostic adapter class for HTTP Server Handlers. + */ + public interface HttpTestHandler { + void handle(HttpTestExchange t) throws IOException; + + default HttpHandler toHttpHandler() { + return (t) -> doHandle(HttpTestExchange.of(t)); + } + default Http2Handler toHttp2Handler() { + return (t) -> doHandle(HttpTestExchange.of(t)); + } + + default void handleFailure(final HttpTestExchange exchange, Throwable failure) { + System.out.println("WARNING: exception caught in HttpTestHandler::handle " + failure); + System.err.println("WARNING: exception caught in HttpTestHandler::handle " + failure); + if (PRINTSTACK && !expectException(exchange)) { + failure.printStackTrace(System.out); + } + } + + private void doHandle(HttpTestExchange exchange) throws IOException { + try { + handle(exchange); + } catch (Throwable failure) { + handleFailure(exchange, failure); + throw failure; + } + } + } + + /** + * An echo handler that can be used to transfer large amount of data, and + * uses file on the file system to download the input. + */ + // TODO: it would be good if we could merge this with the Http2EchoHandler, + // from which this code was copied and adapted. + public static class HttpTestFileEchoHandler implements HttpTestHandler { + static final Path CWD = Paths.get("."); + + @Override + public void handle(HttpTestExchange t) throws IOException { + try { + System.err.printf("EchoHandler received request to %s from %s (version %s)%n", + t.getRequestURI(), t.getRemoteAddress(), t.getExchangeVersion()); + InputStream is = t.getRequestBody(); + var requestHeaders = t.getRequestHeaders(); + var responseHeaders = t.getResponseHeaders(); + responseHeaders.addHeader("X-Hello", "world"); + responseHeaders.addHeader("X-Bye", "universe"); + String fixedrequest = requestHeaders.firstValue("XFixed").orElse(null); + File outfile = Files.createTempFile(CWD, "foo", "bar").toFile(); + //System.err.println ("QQQ = " + outfile.toString()); + FileOutputStream fos = new FileOutputStream(outfile); + long count = is.transferTo(fos); + System.err.printf("EchoHandler read %s bytes\n", count); + is.close(); + fos.close(); + InputStream is1 = new FileInputStream(outfile); + OutputStream os = null; + + Path check = requestHeaders.firstValue("X-Compare") + .map((String s) -> Path.of(s)).orElse(null); + if (check != null) { + System.err.println("EchoHandler checking file match: " + check); + try { + assertFileContentsEqual(check, outfile.toPath()); + } catch (Throwable x) { + System.err.println("Files do not match: " + x); + t.sendResponseHeaders(500, -1); + outfile.delete(); + os.close(); + return; + } + } + + // return the number of bytes received (no echo) + String summary = requestHeaders.firstValue("XSummary").orElse(null); + if (fixedrequest != null && summary == null) { + t.sendResponseHeaders(200, count); + os = t.getResponseBody(); + if (!t.getRequestMethod().equals("HEAD")) { + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes%n", count1); + } else { + System.err.printf("EchoHandler HEAD received, no bytes sent%n"); + } + } else { + t.sendResponseHeaders(200, -1); + os = t.getResponseBody(); + if (!t.getRequestMethod().equals("HEAD")) { + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes\n", count1); + + if (summary != null) { + String s = Long.toString(count); + os.write(s.getBytes()); + } + } else { + System.err.printf("EchoHandler HEAD received, no bytes sent%n"); + } + } + outfile.delete(); + os.close(); + is1.close(); + } catch (Throwable e) { + e.printStackTrace(); + throw new IOException(e); + } + } + } + public static class HttpTestEchoHandler implements HttpTestHandler { + + private final boolean printBytes; + public HttpTestEchoHandler() { + this(true); + } + + public HttpTestEchoHandler(boolean printBytes) { + this.printBytes = printBytes; + } + @Override public void handle(HttpTestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); - printBytes(System.out,"Echo server got " - + t.getExchangeVersion() + " bytes: ", bytes); + if (printBytes) { + printBytes(System.out, "Echo server got " + + t.getExchangeVersion() + " bytes: ", bytes); + } if (t.getRequestHeaders().firstValue("Content-type").isPresent()) { t.getResponseHeaders().addHeader("Content-type", t.getRequestHeaders().firstValue("Content-type").get()); } t.sendResponseHeaders(200, bytes.length); - os.write(bytes); + if (!t.getRequestMethod().equals("HEAD")) { + os.write(bytes); + } } } } + public static class HttpTestRedirectHandler implements HttpTestHandler { + + final Supplier supplier; + + public HttpTestRedirectHandler(Supplier redirectSupplier) { + supplier = redirectSupplier; + } + + @Override + public void handle(HttpTestExchange t) throws IOException { + examineExchange(t); + try (InputStream is = t.getRequestBody()) { + is.readAllBytes(); + String location = supplier.get(); + System.err.printf("RedirectHandler request to %s from %s\n", + t.getRequestURI().toString(), t.getRemoteAddress().toString()); + System.err.println("Redirecting to: " + location); + var headersBuilder = t.getResponseHeaders(); + headersBuilder.addHeader("Location", location); + byte[] bb = getResponseBytes(); + t.sendResponseHeaders(redirectCode(), bb.length); + OutputStream os = t.getResponseBody(); + os.write(bb); + os.close(); + t.close(); + } + } + + protected byte[] getResponseBytes() { + return new byte[1024]; + } + + protected int redirectCode() { + return 301; + } + + // override in sub-class to examine the exchange, but don't + // alter transaction state by reading the request body etc. + protected void examineExchange(HttpTestExchange t) { + } + } + public static boolean expectException(HttpTestExchange e) { HttpTestRequestHeaders h = e.getRequestHeaders(); Optional expectException = h.firstValue("X-expect-exception"); @@ -780,6 +1143,41 @@ public interface HttpServerAdapters { public abstract HttpTestContext addHandler(HttpTestHandler handler, String root); public abstract InetSocketAddress getAddress(); public abstract Version getVersion(); + + /** + * {@return the HTTP3 test server which is acting as an alt-service for this server, + * if any} + */ + public Optional getH3AltService() { + return Optional.empty(); + } + + /** + * {@return true if any HTTP3 test server is acting as an alt-service for this server and the + * HTTP3 test server listens on the same host and port as this server. + * Returns false otherwise} + */ + public boolean supportsH3DirectConnection() { + return false; + } + + public Http3DiscoveryMode h3DiscoveryConfig() { + return null; + } + + @Override + public String toString() { + var conf = Optional.ofNullable(h3DiscoveryConfig()).orElse(getVersion()); + return "HttpTestServer(%s: %s)".formatted(conf, serverAuthority()); + } + + /** + * @param version the HTTP version + * @param more additional HTTP versions + * {@return true if the handlers registered with this server can be accessed (through + * request URIs) using all of the passed HTTP versions. Returns false otherwise} + */ + public abstract boolean canHandle(Version version, Version... more); public abstract void setRequestApprover(final Predicate approver); @Override @@ -799,18 +1197,26 @@ public interface HttpServerAdapters { return hostString + ":" + address.getPort(); } - public static HttpTestServer of(HttpServer server) { + public static HttpTestServer of(final HttpServer server) { + Objects.requireNonNull(server); return new Http1TestServer(server); } - public static HttpTestServer of(HttpServer server, ExecutorService executor) { + public static HttpTestServer of(final HttpServer server, ExecutorService executor) { + Objects.requireNonNull(server); return new Http1TestServer(server, executor); } - public static HttpTestServer of(Http2TestServer server) { + public static HttpTestServer of(final Http2TestServer server) { + Objects.requireNonNull(server); return new Http2TestServerImpl(server); } + public static HttpTestServer of(final Http3TestServer server) { + Objects.requireNonNull(server); + return new H3ServerAdapter(server); + } + /** * Creates a {@link HttpTestServer} which supports the {@code serverVersion}. The server * will only be available on {@code http} protocol. {@code https} will not be supported @@ -841,7 +1247,7 @@ public interface HttpServerAdapters { public static HttpTestServer create(Version serverVersion, SSLContext sslContext) throws IOException { Objects.requireNonNull(serverVersion); - return create(serverVersion, sslContext, null); + return create(serverVersion, sslContext, null, null); } /** @@ -860,7 +1266,130 @@ public interface HttpServerAdapters { public static HttpTestServer create(Version serverVersion, SSLContext sslContext, ExecutorService executor) throws IOException { Objects.requireNonNull(serverVersion); + return create(serverVersion, sslContext, null, executor); + } + + /** + * Creates a {@link HttpTestServer} which supports HTTP_3 version. + * + * @param h3DiscoveryCfg Discovery config for HTTP_3 connection creation. Can be null + * @param sslContext SSLContext. Cannot be null + * @return The newly created server + * @throws IOException if any exception occurs during the server creation + */ + public static HttpTestServer create(Http3DiscoveryMode h3DiscoveryCfg, + SSLContext sslContext) + throws IOException { + Objects.requireNonNull(sslContext, "SSLContext"); + return create(h3DiscoveryCfg, sslContext, null); + } + + /** + * Creates a {@link HttpTestServer} which supports HTTP_3 version. + * + * @param h3DiscoveryCfg Discovery config for HTTP_3 connection creation. Can be null + * @param sslContext SSLContext. Cannot be null + * @param executor The executor to be used by the server. Can be null + * @return The newly created server + * @throws IOException if any exception occurs during the server creation + */ + public static HttpTestServer create(Http3DiscoveryMode h3DiscoveryCfg, + SSLContext sslContext, ExecutorService executor) + throws IOException { + Objects.requireNonNull(sslContext, "SSLContext"); + return create(HTTP_3, sslContext, h3DiscoveryCfg, executor); + } + + + /** + * Creates a {@link HttpTestServer} which supports the {@code serverVersion}. If the + * {@code sslContext} is null, then only {@code http} protocol will be supported by the + * server. Else, the server will be configured with the {@code sslContext} and will support + * {@code https} protocol. + * + * If {@code serverVersion} is {@link Version#HTTP_3 HTTP_3}, then a {@code h3DiscoveryCfg} + * can be passed to decide how the HTTP_3 server will be created. The following table + * summarizes how {@code h3DiscoveryCfg} is used: + *
          + *
        • HTTP3_ONLY - A server which only supports HTTP_3 is created
        • + *
        • HTTP3_ALTSVC - A HTTP_2 server is created and a HTTP_3 server is created. + * The HTTP_2 server advertises the HTTP_3 server as an alternate service. When + * creating the HTTP_3 server, an ephemeral port is used and thus the alternate + * service will be advertised on a different port than the HTTP_2 server's port
        • + *
        • ANY - A HTTP_2 server is created and a HTTP_3 server is created. + * The HTTP_2 server advertises the HTTP_3 server as an alternate service. When + * creating the HTTP_3 server, the same port as that of the HTTP_2 server is used + * to bind the HTTP_3 server. If that bind attempt fails, then an ephemeral port + * is used to bind the HTTP_3 server
        • + *
        + * + * @param serverVersion The HTTP version of the server + * @param sslContext The SSLContext to use. Can be null + * @param h3DiscoveryCfg The Http3DiscoveryMode for HTTP_3 server. Can be null, + * in which case it defaults to {@code ALT_SVC} for HTTP_3 + * server + * @param executor The executor to be used by the server. Can be null + * @return The newly created server + * @throws IllegalArgumentException if {@code serverVersion} is not supported by this method + * @throws IllegalArgumentException if {@code h3DiscoveryCfg} is not null when + * {@code serverVersion} is not {@code HTTP_3} + * @throws IOException if any exception occurs during the server creation + */ + private static HttpTestServer create(final Version serverVersion, final SSLContext sslContext, + final Http3DiscoveryMode h3DiscoveryCfg, + final ExecutorService executor) throws IOException { + Objects.requireNonNull(serverVersion); + if (h3DiscoveryCfg != null && serverVersion != HTTP_3) { + // Http3DiscoveryMode is only supported when version of HTTP_3 + throw new IllegalArgumentException("Http3DiscoveryMode" + + " isn't allowed for " + serverVersion + " version"); + } switch (serverVersion) { + case HTTP_3 -> { + if (sslContext == null) { + throw new IllegalArgumentException("SSLContext cannot be null when" + + " constructing a HTTP_3 server"); + } + final Http3DiscoveryMode effectiveDiscoveryCfg = h3DiscoveryCfg == null + ? Http3DiscoveryMode.ALT_SVC + : h3DiscoveryCfg; + switch (effectiveDiscoveryCfg) { + case HTTP_3_URI_ONLY -> { + // create only a HTTP3 server + return HttpTestServer.of(new Http3TestServer(sslContext, executor)); + } + case ALT_SVC -> { + // create a HTTP2 server which advertises an HTTP3 alternate service. + // that alternate service will be using an ephemeral port for the server + final Http2TestServer h2WithAltService; + try { + h2WithAltService = new Http2TestServer( + "localhost", true, 0, executor, sslContext) + .enableH3AltServiceOnEphemeralPort(); + } catch (Exception e) { + throw new IOException(e); + } + return HttpTestServer.of(h2WithAltService); + } + case ANY -> { + // create a HTTP2 server which advertises an HTTP3 alternate service. + // that alternate service will first attempt to use the same port as the + // HTTP2 server and if binding to that port fails, then will attempt + // to use a ephemeral port. + final Http2TestServer h2WithAltService; + try { + h2WithAltService = new Http2TestServer( + "localhost", true, 0, executor, sslContext) + .enableH3AltServiceOnSamePort(); + } catch (Exception e) { + throw new IOException(e); + } + return HttpTestServer.of(h2WithAltService); + } + default -> throw new IllegalArgumentException("Unsupported" + + " Http3DiscoveryMode: " + effectiveDiscoveryCfg); + } + } case HTTP_2 -> { Http2TestServer underlying; try { @@ -874,7 +1403,7 @@ public interface HttpServerAdapters { } return HttpTestServer.of(underlying); } - case HTTP_1_1 -> { + case HTTP_1_1 -> { InetAddress loopback = InetAddress.getLoopbackAddress(); InetSocketAddress sa = new InetSocketAddress(loopback, 0); HttpServer underlying; @@ -933,8 +1462,22 @@ public interface HttpServerAdapters { return new InetSocketAddress(InetAddress.getLoopbackAddress(), impl.getAddress().getPort()); } + @Override public Version getVersion() { return HTTP_1_1; } + @Override + public boolean canHandle(final Version version, final Version... more) { + if (version != HTTP_1_1) { + return false; + } + for (var v : more) { + if (v != HTTP_1_1) { + return false; + } + } + return true; + } + @Override public void setRequestApprover(final Predicate approver) { throw new UnsupportedOperationException("not supported"); @@ -996,8 +1539,41 @@ public interface HttpServerAdapters { return new InetSocketAddress(InetAddress.getLoopbackAddress(), impl.getAddress().getPort()); } + + @Override + public Optional getH3AltService() { + return impl.getH3AltService(); + } + + @Override + public boolean supportsH3DirectConnection() { + return impl.supportsH3DirectConnection(); + } + + public Http3DiscoveryMode h3DiscoveryConfig() { + return supportsH3DirectConnection() + ? Http3DiscoveryMode.ANY + : Http3DiscoveryMode.ALT_SVC; + } + public Version getVersion() { return HTTP_2; } + @Override + public boolean canHandle(final Version version, final Version... more) { + final Set supported = new HashSet<>(); + supported.add(HTTP_2); + impl.getH3AltService().ifPresent((unused)-> supported.add(HTTP_3)); + if (!supported.contains(version)) { + return false; + } + for (var v : more) { + if (!supported.contains(v)) { + return false; + } + } + return true; + } + @Override public void setRequestApprover(final Predicate approver) { this.impl.setRequestApprover(approver); @@ -1036,6 +1612,119 @@ public interface HttpServerAdapters { } @Override public Version getVersion() { return HTTP_2; } } + + private static final class H3ServerAdapter extends HttpTestServer { + private final Http3TestServer underlyingH3Server; + + private H3ServerAdapter(final Http3TestServer server) { + this.underlyingH3Server = server; + } + + @Override + public void start() { + underlyingH3Server.start(); + } + + @Override + public void stop() { + underlyingH3Server.stop(); + } + + @Override + public HttpTestContext addHandler(final HttpTestHandler handler, final String path) { + Objects.requireNonNull(path); + Objects.requireNonNull(handler); + final H3RootCtx h3Ctx = new H3RootCtx(path, handler); + this.underlyingH3Server.addHandler(path, h3Ctx::doHandle); + return h3Ctx; + } + + @Override + public InetSocketAddress getAddress() { + return underlyingH3Server.getAddress(); + } + + @Override + public Version getVersion() { + return HTTP_3; + } + + @Override + public Http3DiscoveryMode h3DiscoveryConfig() { + return Http3DiscoveryMode.HTTP_3_URI_ONLY; + } + + @Override + public boolean canHandle(Version version, Version... more) { + if (version != HTTP_3) { + return false; + } + for (var v : more) { + if (v != HTTP_3) { + return false; + } + } + return true; + } + + @Override + public void setRequestApprover(final Predicate approver) { + underlyingH3Server.setRequestApprover(approver); + } + + } + + private static final class H3RootCtx extends HttpTestContext implements HttpTestHandler { + private final String path; + private final HttpTestHandler handler; + private final List filters = new CopyOnWriteArrayList<>(); + + private H3RootCtx(final String path, final HttpTestHandler handler) { + this.path = path; + this.handler = handler; + } + + @Override + public String getPath() { + return this.path; + } + + @Override + public void addFilter(final HttpTestFilter filter) { + Objects.requireNonNull(filter); + this.filters.add(filter); + } + + @Override + public Version getVersion() { + return HTTP_3; + } + + @Override + public void setAuthenticator(final Authenticator authenticator) { + if (authenticator instanceof BasicAuthenticator basicAuth) { + addFilter(new HttpBasicAuthFilter(basicAuth)); + } else { + throw new UnsupportedOperationException( + "Only BasicAuthenticator is supported on an H3 context"); + } + } + + @Override + public void handle(final HttpTestExchange exchange) throws IOException { + HttpChain.of(this.filters, this.handler).doFilter(exchange); + } + + private void doHandle(final Http2TestExchange exchange) throws IOException { + final HttpTestExchange adapted = HttpTestExchange.of(exchange); + try { + H3RootCtx.this.handle(adapted); + } catch (Throwable failure) { + handleFailure(adapted, failure); + throw failure; + } + } + } } public static void enableServerLogging() { @@ -1044,4 +1733,73 @@ public interface HttpServerAdapters { HttpTestServer.ServerLogging.enableLogging(); } + public default HttpClient.Builder newClientBuilderForH3() { + return createClientBuilderForH3(); + } + + /** + * {@return a client builder suitable for interacting with the specified + * version} + * The builder's {@linkplain HttpClient.Builder#version(Version) version}, + * {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy selector} + * and {@linkplain HttpClient.Builder#sslContext(SSLContext) SSL context} + * are not set. + * @apiNote This method sets the {@linkplain HttpClient.Builder#localAddress(InetAddress) + * bind address} to the {@linkplain InetAddress#getLoopbackAddress() loopback address} + * if version is HTTP/3, the OS is Mac, and the OS version is 10.X, in order to + * avoid conflicting with system allocated ephemeral UDP ports. + * @param version the highest version the client is assumed to interact with. + */ + public static HttpClient.Builder createClientBuilderFor(Version version) { + var builder = HttpClient.newBuilder(); + return switch (version) { + case HTTP_3 -> configureForH3(builder); + default -> builder; + }; + } + + /** + * {@return a client builder suitable for interacting with HTTP/3} + * The builder's {@linkplain HttpClient.Builder#version(Version) version}, + * {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy selector} + * and {@linkplain HttpClient.Builder#sslContext(SSLContext) SSL context} + * are not set. + * @apiNote This method sets the {@linkplain HttpClient.Builder#localAddress(InetAddress) + * bind address} to the {@linkplain InetAddress#getLoopbackAddress() loopback address} + * if version is HTTP/3, the OS is Mac, and the OS version is 10.X, in order to + * avoid conflicting with system allocated ephemeral UDP ports. + * @implSpec This is identical to calling {@link #createClientBuilderFor(Version) + * newClientBuilderFor(Version.HTTP_3)} or {@link #configureForH3(Builder) + * configureForH3(HttpClient.newBuilder())} + */ + public static HttpClient.Builder createClientBuilderForH3() { + return configureForH3(HttpClient.newBuilder()); + } + + /** + * Configure a builder to be suitable for a client that may send requests + * through HTTP/3. + * The builder's {@linkplain HttpClient.Builder#version(Version) version}, + * {@linkplain HttpClient.Builder#proxy(ProxySelector) proxy selector} + * and {@linkplain HttpClient.Builder#sslContext(SSLContext) SSL context} + * are not set. + * @apiNote This method sets the {@linkplain HttpClient.Builder#localAddress(InetAddress) + * bind address} to the {@linkplain InetAddress#getLoopbackAddress() loopback address} + * if the OS is Mac, and the OS version is 10.X, in order to + * avoid conflicting with system allocated ephemeral UDP ports. + * @return a client builder suitable for interacting with HTTP/3 + */ + public static HttpClient.Builder configureForH3(HttpClient.Builder builder) { + if (TestUtil.sysPortsMayConflict()) { + return builder.localAddress(InetAddress.getLoopbackAddress()); + } + return builder; + } + + public static InetAddress clientLocalBindAddress() { + if (TestUtil.sysPortsMayConflict()) { + return InetAddress.getLoopbackAddress(); + } + return new InetSocketAddress(0).getAddress(); + } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/RequestPathMatcherUtil.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/RequestPathMatcherUtil.java new file mode 100644 index 00000000000..e732f45efe4 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/RequestPathMatcherUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.common; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Utility which parses a request path and finds a best match registered handler + */ +public class RequestPathMatcherUtil { + + public record Resolved(String bestMatchedPath, T handler) { + } + + /** + * Matches the {@code path} against the registered {@code pathHandlers} and returns the best + * matched handler. + * + * @param path The request path + * @param pathHandlers The handlers for each of the registered paths + * @param + * @return The resolved result or an {@linkplain Optional#empty() empty Optional} if no + * handler could be found for the {@code path} + * @throws NullPointerException if {@code pathHandlers} is null + */ + public static Optional> findHandler(final String path, + final Map pathHandlers) { + Objects.requireNonNull(pathHandlers, "pathHandlers is null"); + final String fpath = (path == null || path.isEmpty()) ? "/" : path; + final AtomicReference bestMatch = new AtomicReference<>(""); + final AtomicReference result = new AtomicReference<>(); + pathHandlers.forEach((key, value) -> { + if (fpath.startsWith(key) && key.length() > bestMatch.get().length()) { + bestMatch.set(key); + result.set(value); + } + }); + final T handler = result.get(); + if (handler == null) { + System.err.println("No handler found for path: " + path); + return Optional.empty(); + } + return Optional.of(new Resolved(bestMatch.get(), handler)); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java index a471f3ce07f..0d525a4f2c8 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestServerConfigurator.java @@ -57,11 +57,22 @@ public final class TestServerConfigurator extends HttpsConfigurator { @Override public void configure(final HttpsParameters params) { final SSLParameters sslParams = getSSLContext().getDefaultSSLParameters(); - final String hostname = serverAddr.getHostName(); - - final List sniMatchers = List.of(new ServerNameMatcher(hostname)); - sslParams.setSNIMatchers(sniMatchers); + addSNIMatcher(serverAddr, sslParams); // configure the server with these custom SSLParameters params.setSSLParameters(sslParams); } + + public static void addSNIMatcher(final InetAddress serverAddr, final SSLParameters sslParams) { + final String hostname; + if (serverAddr.isLoopbackAddress()) { + // when it's loopback address, don't rely on InetAddress.getHostName() to get us the + // hostname, since it has been observed on Windows setups that InetAddress.getHostName() + // can return an IP address (127.0.0.1) instead of the hostname for loopback address + hostname = "localhost"; + } else { + hostname = serverAddr.getHostName(); + } + final List sniMatchers = List.of(new ServerNameMatcher(hostname)); + sslParams.setSNIMatchers(sniMatchers); + } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestUtil.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestUtil.java new file mode 100644 index 00000000000..37ce49cd901 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/TestUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.httpclient.test.lib.common; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Optional; + +import jdk.internal.util.OperatingSystem; + +public final class TestUtil { + + private TestUtil() {} + + public static boolean sysPortsMayConflict() { + if (OperatingSystem.isMacOS()) { + // syslogd udp_in module may be dynamically started and opens an udp4 port + // on the wildcard address. In addition, macOS will allow different processes + // to bind to the same port on the wildcard, if one uses udp4 and the other + // binds using udp46 (dual IPv4 IPv6 socket). + // Binding to the loopback (or a specific interface) instead of binding + // to the wildcard can prevent such conflicts. + return true; + } + return false; + } + + public static Optional chooseClientBindAddress() { + if (!TestUtil.sysPortsMayConflict()) { + return Optional.empty(); + } + final InetSocketAddress address = new InetSocketAddress( + InetAddress.getLoopbackAddress(), 0); + return Optional.of(address); + } + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java index c6eee5afabf..33cca60ff38 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/BodyOutputStream.java @@ -26,6 +26,7 @@ package jdk.httpclient.test.lib.http2; import java.io.*; import java.nio.ByteBuffer; import java.util.Objects; +import java.util.concurrent.Semaphore; import jdk.internal.net.http.frame.DataFrame; import jdk.internal.net.http.frame.ResetFrame; @@ -39,7 +40,8 @@ public class BodyOutputStream extends OutputStream { final static byte[] EMPTY_BARRAY = new byte[0]; final int streamid; - int window; + // stream level send window, permits = bytes + final Semaphore window; volatile boolean closed; volatile BodyInputStream bis; volatile int resetErrorCode; @@ -48,7 +50,7 @@ public class BodyOutputStream extends OutputStream { final Queue outputQ; BodyOutputStream(int streamid, int initialWindow, Http2TestServerConnection conn) { - this.window = initialWindow; + this.window = new Semaphore(initialWindow); this.streamid = streamid; this.conn = conn; this.outputQ = conn.outputQ; @@ -57,9 +59,8 @@ public class BodyOutputStream extends OutputStream { // called from connection reader thread as all incoming window // updates are handled there. - synchronized void updateWindow(int update) { - window += update; - notifyAll(); + void updateWindow(int update) { + window.release(update); } void waitForWindow(int demand) throws InterruptedException { @@ -69,23 +70,8 @@ public class BodyOutputStream extends OutputStream { conn.obtainConnectionWindow(demand); } - public void waitForStreamWindow(int amount) throws InterruptedException { - int demand = amount; - try { - synchronized (this) { - while (amount > 0) { - int n = Math.min(amount, window); - amount -= n; - window -= n; - if (amount > 0) { - wait(); - } - } - } - } catch (Throwable t) { - window += (demand - amount); - throw t; - } + public void waitForStreamWindow(int demand) throws InterruptedException { + window.acquire(demand); } public void goodToGo() { @@ -176,7 +162,7 @@ public class BodyOutputStream extends OutputStream { sendEndStream(); if (bis!= null && bis.unconsumed()) { // Send a reset if there is still unconsumed data in the input stream - sendReset(EMPTY_BARRAY, 0, 0, ResetFrame.NO_ERROR); + sendReset(ResetFrame.NO_ERROR); } } catch (IOException ex) { ex.printStackTrace(); @@ -187,12 +173,18 @@ public class BodyOutputStream extends OutputStream { send(EMPTY_BARRAY, 0, 0, DataFrame.END_STREAM); } - public void sendReset(byte[] buf, int offset, int len, int flags) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(len); - buffer.put(buf, offset, len); - buffer.flip(); + public void sendReset(int resetErrorCode) throws IOException { assert streamid != 0; - ResetFrame rf = new ResetFrame(streamid, flags); + ResetFrame rf = new ResetFrame(streamid, resetErrorCode); outputQ.put(rf); } + + public void reset(int resetErrorCode) throws IOException { + if (closed) return; + synchronized (this) { + if (closed) return; + this.closed = true; + } + sendReset(resetErrorCode); + } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java index 24ac59d96dc..047b52b3699 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/EchoHandler.java @@ -24,11 +24,12 @@ package jdk.httpclient.test.lib.http2; import java.io.*; +import java.net.http.HttpHeaders; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import jdk.internal.net.http.common.HttpHeadersImpl; +import jdk.internal.net.http.common.HttpHeadersBuilder; public class EchoHandler implements Http2Handler { static final Path CWD = Paths.get("."); @@ -41,8 +42,8 @@ public class EchoHandler implements Http2Handler { try { System.err.println("EchoHandler received request to " + t.getRequestURI()); InputStream is = t.getRequestBody(); - HttpHeadersImpl map = t.getRequestHeaders(); - HttpHeadersImpl map1 = t.getResponseHeaders(); + HttpHeaders map = t.getRequestHeaders(); + HttpHeadersBuilder map1 = t.getResponseHeaders(); map1.addHeader("X-Hello", "world"); map1.addHeader("X-Bye", "universe"); String fixedrequest = map.firstValue("XFixed").orElse(null); diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java index 7b13724ac51..fd0b03ac691 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2EchoHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,8 +27,11 @@ import java.net.http.HttpHeaders; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; + import jdk.internal.net.http.common.HttpHeadersBuilder; +import static jdk.test.lib.Asserts.assertFileContentsEqual; + public class Http2EchoHandler implements Http2Handler { static final Path CWD = Paths.get("."); @@ -49,27 +52,42 @@ public class Http2EchoHandler implements Http2Handler { File outfile = Files.createTempFile(CWD, "foo", "bar").toFile(); //System.err.println ("QQQ = " + outfile.toString()); FileOutputStream fos = new FileOutputStream(outfile); - int count = (int) is.transferTo(fos); - System.err.printf("EchoHandler read %d bytes\n", count); + long count = is.transferTo(fos); + System.err.printf("EchoHandler read %s bytes\n", count); is.close(); fos.close(); InputStream is1 = new FileInputStream(outfile); OutputStream os = null; + + Path check = map.firstValue("X-Compare").map((String s) -> Path.of(s)).orElse(null); + if (check != null) { + System.err.println("EchoHandler checking file match: " + check); + try { + assertFileContentsEqual(check, outfile.toPath()); + } catch (Throwable x) { + System.err.println("Files do not match: " + x); + t.sendResponseHeaders(500, -1); + outfile.delete(); + os.close(); + return; + } + } + // return the number of bytes received (no echo) String summary = map.firstValue("XSummary").orElse(null); if (fixedrequest != null && summary == null) { t.sendResponseHeaders(200, count); os = t.getResponseBody(); - int count1 = (int)is1.transferTo(os); - System.err.printf("EchoHandler wrote %d bytes\n", count1); + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes\n", count1); } else { t.sendResponseHeaders(200, 0); os = t.getResponseBody(); - int count1 = (int)is1.transferTo(os); - System.err.printf("EchoHandler wrote %d bytes\n", count1); + long count1 = is1.transferTo(os); + System.err.printf("EchoHandler wrote %s bytes\n", count1); if (summary != null) { - String s = Integer.toString(count); + String s = Long.toString(count); os.write(s.getBytes()); } } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java index 8871f0c2cd5..c1a5538b95d 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2Handler.java @@ -37,6 +37,5 @@ public interface Http2Handler { * client and used to send the response * @throws NullPointerException if exchange is null */ - void handle (Http2TestExchange exchange) throws IOException; + void handle(Http2TestExchange exchange) throws IOException; } - diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java index 69e4344aff2..ab817d0c79d 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2RedirectHandler.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.function.Supplier; + import jdk.internal.net.http.common.HttpHeadersBuilder; public class Http2RedirectHandler implements Http2Handler { @@ -48,8 +49,8 @@ public class Http2RedirectHandler implements Http2Handler { System.err.println("Redirecting to: " + location); HttpHeadersBuilder headersBuilder = t.getResponseHeaders(); headersBuilder.addHeader("Location", location); - t.sendResponseHeaders(redirectCode(), 1024); - byte[] bb = new byte[1024]; + byte[] bb = getResponseBytes(); + t.sendResponseHeaders(redirectCode(), bb.length == 0 ? -1 : bb.length); OutputStream os = t.getResponseBody(); os.write(bb); os.close(); @@ -57,6 +58,10 @@ public class Http2RedirectHandler implements Http2Handler { } } + protected byte[] getResponseBytes() { + return new byte[1024]; + } + protected int redirectCode() { return 301; } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java index 828c939f53f..77c9e831fb0 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,12 +28,18 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.InetSocketAddress; +import java.net.http.HttpClient.Version; import java.net.http.HttpHeaders; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiPredicate; import javax.net.ssl.SSLSession; + import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.quic.VariableLengthEncoder; import jdk.internal.net.http.frame.Http2Frame; public interface Http2TestExchange { @@ -72,19 +78,177 @@ public interface Http2TestExchange { boolean serverPushAllowed(); - void serverPush(URI uri, HttpHeaders headers, InputStream content); + default void serverPush(URI uri, HttpHeaders headers, InputStream content) + throws IOException { + serverPush(uri, headers, HttpHeaders.of(Map.of(), (n,v) -> true), content); + } + + void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException; + + // For HTTP/3 only: send push promise + push stream, returns pushId + // + + /** + * For HTTP/3 only: send push promise + push stream, returns pushId. + * The pushId can be promised again using {@link + * #sendPushId(long, URI, HttpHeaders)} + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param uri the push promise URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise response headers + * @param content the push response body + * + * @return the pushId used to push the promise + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default long serverPushWithId(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + throw new UnsupportedOperationException("serverPushWithId " + getExchangeVersion()); + } + + /** + * For HTTP/3 only: only sends a push promise frame. If a positive + * pushId is provided, uses the provided pushId and returns it. + * Otherwise, a new pushId will be allocated and returned. + * This allows to send an additional promise after {@linkplain + * #serverPushWithId(URI, HttpHeaders, HttpHeaders, InputStream) sending the first}, + * or to send one or several push promise frames before {@linkplain + * #sendPushResponse(long, URI, HttpHeaders, HttpHeaders, InputStream) sending + * the response}. + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the pushId to use, or {@code -1} if a new + * pushId should be allocated. + * @param uri the push promise URI + * @param headers the push promise request headers + * + * @return the given pushId, if positive, otherwise the new allocated pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default long sendPushId(long pushId, URI uri, HttpHeaders headers) throws IOException { + throw new UnsupportedOperationException("sendPushId with " + getExchangeVersion()); + } + + /** + * For HTTP/3 only: sends an HTTP/3 CANCEL_PUSH frame to cancel + * a push that has been promised by either {@link + * #serverPushWithId(URI, HttpHeaders, HttpHeaders, InputStream)} or {@link + * #sendPushId(long, URI, HttpHeaders)}. + * + * This method just sends a CANCEL_PUSH frame. + * Note that if the push stream has already been opened this + * sending a CANCEL_PUSH frame may have no effect. + * + * @apiNote + * No check is performed on the provided pushId + * + * @implSpec + * The default implementation of this method throws {@link + * UnsupportedOperationException} + * + * @param pushId the cancelled pushId + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default void cancelPushId(long pushId) throws IOException { + throw new UnsupportedOperationException("cancelPush with " + getExchangeVersion()); + } + + /** + * For HTTP/3 only: opens an HTTP/3 PUSH_STREAM to send a + * push promise response headers and body. + * + * @apiNote + * No check is performed on the provided pushId + * + * @param pushId a positive pushId obtained from {@link + * #sendPushId(long, URI, HttpHeaders)} + * @param uri the push request URI + * @param reqHeaders the push promise request headers + * @param rspHeaders the push promise response headers + * @param content the push response body + * + * @throws IOException if an error occurs + * @throws UnsupportedOperationException if the exchange is not {@link + */ + default void sendPushResponse(long pushId, URI uri, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream content) + throws IOException { + throw new UnsupportedOperationException("sendPushResponse with " + getExchangeVersion()); + } + + default void requestStopSending(long errorCode) { + throw new UnsupportedOperationException("sendStopSendingFrame with " + getExchangeVersion()); + } default void sendFrames(List frames) throws IOException { throw new UnsupportedOperationException("not implemented"); } /** - * Send a PING on this exchanges connection, and completes the returned CF + * For HTTP/3 only: waits until the given {@code pushId} is allowed by + * the HTTP/3 peer. + * + * @implSpec + * The default implementation of this method returns the larger + * possible variable length integer. + * + * @param pushId a pushId + * + * @return the upper bound pf the maximum pushId allowed (exclusive) + * + * @throws UnsupportedOperationException if the exchange is not {@link + * #getExchangeVersion() HTTP_3} + */ + default long waitForMaxPushId(long pushId) throws InterruptedException { + return VariableLengthEncoder.MAX_ENCODED_INTEGER; + } + + default Encoder qpackEncoder() { + throw new UnsupportedOperationException("QPack encoder not supported: " + getExchangeVersion()); + } + + default CompletableFuture clientHttp3Settings() { + throw new UnsupportedOperationException("HTTP/3 client connection settings not supported: " + getExchangeVersion()); + } + + /** + * Send a PING on this exchange connection, and completes the returned CF * with the number of milliseconds it took to get a valid response. * It may also complete exceptionally */ CompletableFuture sendPing(); + default void close(IOException closed) throws IOException { + close(); + } + + default Version getServerVersion() { return Version.HTTP_2; } + + default Version getExchangeVersion() { return Version.HTTP_2; } + + default void resetStream(long code) throws IOException { + throw new UnsupportedOperationException("resetStream with " + getExchangeVersion()); + } + /** * {@return the identification of the connection on which this exchange is being * processed} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java index 12324b3ba0b..8bc0fd2473c 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -149,12 +149,12 @@ public class Http2TestExchangeImpl implements Http2TestExchange { long clen = responseLength > 0 ? responseLength : 0; rspheadersBuilder.setHeader("Content-length", Long.toString(clen)); } - - rspheadersBuilder.setHeader(":status", Integer.toString(rCode)); - HttpHeaders headers = rspheadersBuilder.build(); - + final HttpHeadersBuilder pseudoHeadersBuilder = new HttpHeadersBuilder(); + pseudoHeadersBuilder.setHeader(":status", Integer.toString(rCode)); + final HttpHeaders pseudoHeaders = pseudoHeadersBuilder.build(); + final HttpHeaders headers = rspheadersBuilder.build(); ResponseHeaders response - = new ResponseHeaders(headers, insertionPolicy); + = new ResponseHeaders(pseudoHeaders, headers, insertionPolicy); response.streamid(streamid); response.setFlag(HeaderFrame.END_HEADERS); @@ -184,6 +184,13 @@ public class Http2TestExchangeImpl implements Http2TestExchange { conn.sendFrames(frames); } + @Override + public void resetStream(long code) throws IOException { + // will close the os if not closed. + // reset will be sent only if the os is not closed. + os.sendReset((int) code); + } + @Override public InetSocketAddress getRemoteAddress() { return (InetSocketAddress) conn.socket.getRemoteSocketAddress(); @@ -210,18 +217,18 @@ public class Http2TestExchangeImpl implements Http2TestExchange { } @Override - public void serverPush(URI uri, HttpHeaders headers, InputStream content) { + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) { HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder(); headersBuilder.setHeader(":method", "GET"); headersBuilder.setHeader(":scheme", uri.getScheme()); headersBuilder.setHeader(":authority", uri.getAuthority()); headersBuilder.setHeader(":path", uri.getPath()); - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : reqHeaders.map().entrySet()) { for (String value : entry.getValue()) headersBuilder.addHeader(entry.getKey(), value); } HttpHeaders combinedHeaders = headersBuilder.build(); - OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, content); + OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, combinedHeaders, rspHeaders, content); pp.setFlag(HeaderFrame.END_HEADERS); try { diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java index a4d696ee53c..543c4079c14 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServer.java @@ -24,22 +24,39 @@ package jdk.httpclient.test.lib.http2; import java.io.IOException; -import java.net.*; -import java.util.*; +import java.net.BindException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import javax.net.ServerSocketFactory; +import javax.net.ssl.SNIMatcher; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocket; +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil; +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil.Resolved; +import jdk.httpclient.test.lib.common.ServerNameMatcher; +import jdk.httpclient.test.lib.http3.Http3TestServer; import jdk.internal.net.http.frame.ErrorFrame; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.quic.QuicVersion; +import jdk.httpclient.test.lib.quic.QuicServer; /** * Waits for incoming TCP connections from a client and establishes @@ -48,7 +65,10 @@ import jdk.internal.net.http.frame.ErrorFrame; * Http2Handler on additional threads. All threads * obtained from the supplied ExecutorService. */ -public class Http2TestServer implements AutoCloseable { +public final class Http2TestServer implements AutoCloseable { + + record AltSvcAddr(String host, int port, InetSocketAddress original) {} + static final AtomicLong IDS = new AtomicLong(); final long id = IDS.incrementAndGet(); final ServerSocket server; @@ -61,7 +81,12 @@ public class Http2TestServer implements AutoCloseable { final String serverName; final Set connections; final Properties properties; + volatile Http3TestServer h3Server; + volatile AltSvcAddr h3AltSvcAddr; final String name; + private final SNIMatcher sniMatcher; + volatile boolean altSvcAsRespHeader; + private final ReentrantLock serverLock = new ReentrantLock(); // request approver which takes the server connection key as the input private volatile Predicate newRequestApprover; @@ -179,7 +204,6 @@ public class Http2TestServer implements AutoCloseable { throws Exception { this.name = "TestServer(%d)".formatted(id); - this.serverName = serverName; this.supportsHTTP11 = supportsHTTP11; if (secure) { if (context != null) @@ -192,12 +216,166 @@ public class Http2TestServer implements AutoCloseable { server = initPlaintext(port, backlog); } this.secure = secure; + this.serverName = serverName; + this.sniMatcher = serverName == null + ? new ServerNameMatcher(localAddr.getHostName()) + : new ServerNameMatcher(this.serverName); this.exec = exec == null ? createExecutor(name) : exec; this.handlers = Collections.synchronizedMap(new HashMap<>()); this.properties = properties == null ? new Properties() : properties; this.connections = ConcurrentHashMap.newKeySet(); } + /** + * {@return the {@link SNIMatcher} configured for this server. Returns {@code null} + * if none is configured} + */ + public SNIMatcher getSniMatcher() { + return this.sniMatcher; + } + + /** + * Creates a H3 server which will attempt to use the same host/port as the one used + * by this current H2 server (except that the H3 server will use UDP). If that host/port + * isn't available for the H3 server then it uses an ephemeral port on loopback address. + * That H3 server then acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + */ + public Http2TestServer enableH3AltServiceOnSamePort() throws IOException { + this.enableH3AltService(false); + return this; + } + + /** + * Creates a H3 server that acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + *

        + * The H3 server will be created using an ephemeral port on loopback address. + *

        + */ + public Http2TestServer enableH3AltServiceOnEphemeralPort() throws IOException { + return enableH3AltService(true); + } + + /** + * Creates a H3 server that acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + *

        + * The H3 server will be created using an ephemeral port on loopback address. + *

        + * The server will switch to selected QUIC version using compatible or incompatible negotiation. + */ + public Http2TestServer enableH3AltServiceOnEphemeralPortWithVersion(QuicVersion version, boolean compatible) throws IOException { + return enableH3AltService(0, new QuicVersion[]{version}, compatible); + } + + public Http2TestServer enableH3AltServiceOnPort(int port) throws IOException { + return enableH3AltService(port, new QuicVersion[]{QuicVersion.QUIC_V1}); + } + + /** + * Creates a H3 server that acts as the alternate service for this H2 server and will be advertised + * as such when this H2 server responds to any HTTP requests. + *

        + * If {@code useEphemeralAddr} is {@code true} then the H3 server will be created using an + * ephemeral port on loopback address. Otherwise, an attempt will be made by this current H2 + * server (except that the H3 server will use UDP). If that attempt fails then this method + * implementation will fallback to using an ephemeral port for creating the H3 server. + *

        + * @param useEphemeralAddr If true then the H3 server will be created using an ephemeral port + */ + Http2TestServer enableH3AltService(final boolean useEphemeralAddr) throws IOException { + return enableH3AltService(useEphemeralAddr ? 0 : getAddress().getPort(), new QuicVersion[]{QuicVersion.QUIC_V1}); + } + + Http2TestServer enableH3AltService(final int port, QuicVersion[] quicVersions) throws IOException { + return enableH3AltService(port, quicVersions, false); + } + + Http2TestServer enableH3AltService(final int port, QuicVersion[] quicVersions, boolean compatible) throws IOException { + if (this.h3Server != null) { + // already enabled + // TODO: throw exception instead? + return this; + } + if (!secure) { + throw new IllegalStateException("Cannot enable H3 alt service for a non-secure H2 server"); + } + serverLock.lock(); + try { + if (this.h3Server != null) { + return this; + } + QuicServer.Builder quicServerBuilder = Http3TestServer.quicServerBuilder(); + quicServerBuilder.sslContext(this.sslContext).serverId("h2-server-" + id) + .executor(this.exec) + .sniMatcher(this.sniMatcher) + .availableVersions(quicVersions) + .compatibleNegotiation(compatible) + .appErrorCodeToString(Http3Error::stringForCode) + .bindAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), port)); + try { + this.h3Server = new Http3TestServer(quicServerBuilder.build(), this::getHandlerFor); + } catch (BindException be) { + if (port == 0) { + // this means that we already attempted to bind with an ephemeral port + // and it failed, so no need to attempt again. Just throw back the original + // exception + throw be; + } + // try with an ephemeral port + quicServerBuilder.bindAddress(new InetSocketAddress( + InetAddress.getLoopbackAddress(), 0)); + this.h3Server = new Http3TestServer(quicServerBuilder.build(), this::getHandlerFor); + } + // we keep track of the InetSocketAddress.getHostString() when the alt service address + // was created and keep using the same host string irrespective of whether the + // underlying/original InetSocketAddress' hostname resolution could potentially have + // changed the value returned by getHostString(). This allows us to use a consistent + // host in the alt-svc that we advertise. + this.h3AltSvcAddr = new AltSvcAddr(h3Server.getAddress().getHostString(), + h3Server.getAddress().getPort(), h3Server.getAddress()); + } finally { + serverLock.unlock(); + } + return this; + } + + public Optional getH3AltService() { + return Optional.ofNullable(h3Server); + } + + /** + * {@return true if this H2 server is configured with an H3 alternate service and that + * H3 alternate service listens on the same host and port as that of this H2 server (except + * that H3 uses UDP). Returns false otherwise} + */ + public boolean supportsH3DirectConnection() { + final AltSvcAddr h3Addr = this.h3AltSvcAddr; + if (h3Addr == null) { + return false; + } + final InetSocketAddress h2Addr = this.getAddress(); + return h2Addr.equals(h3Addr.original); + } + + /** + * Controls whether this H2 server sends a alt-svc response header or an AltSvc frame + * when H3 alternate service is enable on this server. The alt-svc header or the frame + * will be sent whenever this server responds next to an HTTP request. + * + * @param enable If {@code true} then the alt-svc response header is sent. Else AltSvc frame + * is sent. + * @return The current Http2TestServer + */ + public Http2TestServer advertiseAltSvcResponseHeader(final boolean enable) { + // TODO: this is only set as a flag currently. We need to implement the logic + // which sends the alt-svc response header. we currently send a alt-svc frame + // whenever a alt-svc is present. + this.altSvcAsRespHeader = enable; + return this; + } + /** * Adds the given handler for the given path */ @@ -218,24 +396,14 @@ public class Http2TestServer implements AutoCloseable { } Http2Handler getHandlerFor(String path) { - if (path == null || path.equals("")) - path = "/"; - - final String fpath = path; - AtomicReference bestMatch = new AtomicReference<>(""); - AtomicReference href = new AtomicReference<>(); - - handlers.forEach((key, value) -> { - if (fpath.startsWith(key) && key.length() > bestMatch.get().length()) { - bestMatch.set(key); - href.set(value); - } - }); - Http2Handler handler = href.get(); - if (handler == null) - throw new RuntimeException("No handler found for path " + path); - System.err.println(name + ": Using handler for: " + bestMatch.get()); - return handler; + final Optional> match = RequestPathMatcherUtil.findHandler(path, handlers); + if (match.isEmpty()) { + // no handler available for the path + return null; + } + final Resolved resolved = match.get(); + System.err.println(name + ": Using handler for: " + resolved.bestMatchedPath()); + return resolved.handler(); } final ServerSocket initPlaintext(int port, int backlog) throws Exception { @@ -245,7 +413,16 @@ public class Http2TestServer implements AutoCloseable { return ss; } - public synchronized void stop() { + public void stop() { + serverLock.lock(); + try { + implStop(); + } finally { + serverLock.unlock(); + } + } + + private void implStop() { // TODO: clean shutdown GoAway stopping = true; System.err.printf("%s: stopping %d connections\n", name, connections.size()); @@ -255,6 +432,12 @@ public class Http2TestServer implements AutoCloseable { try { server.close(); } catch (IOException e) {} + try { + var h3Server = this.h3Server; + if (h3Server != null) { + h3Server.close(); + } + } catch (IOException e) {} exec.shutdownNow(); } @@ -284,6 +467,16 @@ public class Http2TestServer implements AutoCloseable { return serverName; } + private void putConnection(InetSocketAddress addr, Http2TestServerConnection c) { + serverLock.lock(); + try { + if (!stopping) + connections.add(c); + } finally { + serverLock.unlock(); + } + } + public void setRequestApprover(final Predicate approver) { this.newRequestApprover = approver; } @@ -292,13 +485,13 @@ public class Http2TestServer implements AutoCloseable { return this.newRequestApprover; } - private synchronized void putConnection(InetSocketAddress addr, Http2TestServerConnection c) { - if (!stopping) - connections.add(c); - } - - private synchronized void removeConnection(InetSocketAddress addr, Http2TestServerConnection c) { - connections.remove(c); + private void removeConnection(InetSocketAddress addr, Http2TestServerConnection c) { + serverLock.lock(); + try { + connections.remove(c); + } finally { + serverLock.unlock(); + } } record AcceptedConnection(Http2TestServer server, @@ -350,6 +543,10 @@ public class Http2TestServer implements AutoCloseable { * Starts a thread which waits for incoming connections. */ public void start() { + var h3Server = this.h3Server; + if (h3Server != null) { + h3Server.start(); + } exec.submit(() -> { try { while (!stopping) { diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java index deec3ec2c24..20668d281c8 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java @@ -41,25 +41,19 @@ import jdk.internal.net.http.frame.SettingsFrame; import jdk.internal.net.http.frame.WindowUpdateFrame; import jdk.internal.net.http.hpack.Decoder; import jdk.internal.net.http.hpack.DecodingCallback; -import jdk.internal.net.http.hpack.Encoder; import sun.net.www.http.ChunkedInputStream; import sun.net.www.http.HttpClient; -import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIMatcher; -import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import javax.net.ssl.StandardConstants; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.net.InetAddress; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; @@ -74,20 +68,25 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Random; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiPredicate; import java.util.function.Consumer; + +import jdk.internal.net.http.frame.AltSvcFrame; + import java.util.function.Predicate; import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import static jdk.internal.net.http.frame.ErrorFrame.REFUSED_STREAM; import static jdk.internal.net.http.frame.SettingsFrame.DEFAULT_MAX_FRAME_SIZE; @@ -125,7 +124,6 @@ public class Http2TestServerConnection { private final AtomicInteger goAwayRequestStreamId = new AtomicInteger(-1); final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); - final static byte[] EMPTY_BARRAY = new byte[0]; final Random random; final static byte[] clientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(); @@ -175,7 +173,7 @@ public class Http2TestServerConnection { if (socket instanceof SSLSocket) { SSLSocket sslSocket = (SSLSocket)socket; - handshake(server.serverName(), sslSocket); + handshake(server.getSniMatcher(), sslSocket); if (!server.supportsHTTP11 && !"h2".equals(sslSocket.getApplicationProtocol())) { throw new IOException("Unexpected ALPN: [" + sslSocket.getApplicationProtocol() + "]"); } @@ -312,39 +310,12 @@ public class Http2TestServerConnection { outputQ.put(frame); } - private static boolean compareIPAddrs(InetAddress addr1, String host) { - try { - InetAddress addr2 = InetAddress.getByName(host); - return addr1.equals(addr2); - } catch (IOException e) { - throw new UncheckedIOException(e); + private static void handshake(final SNIMatcher sniMatcher, final SSLSocket sock) throws IOException { + if (sniMatcher != null) { + final SSLParameters params = sock.getSSLParameters(); + params.setSNIMatchers(List.of(sniMatcher)); + sock.setSSLParameters(params); } - } - - private static void handshake(String name, SSLSocket sock) throws IOException { - if (name == null) { - sock.startHandshake(); // blocks until handshake done - return; - } else if (name.equals("localhost")) { - name = "localhost"; - } - final String fname = name; - final InetAddress addr1 = InetAddress.getByName(name); - SSLParameters params = sock.getSSLParameters(); - SNIMatcher matcher = new SNIMatcher(StandardConstants.SNI_HOST_NAME) { - public boolean matches (SNIServerName n) { - String host = ((SNIHostName)n).getAsciiName(); - if (host.equals("localhost")) - host = "localhost"; - boolean cmp = host.equalsIgnoreCase(fname); - if (cmp) - return true; - return compareIPAddrs(addr1, host); - } - }; - List list = List.of(matcher); - params.setSNIMatchers(list); - sock.setSSLParameters(params); sock.startHandshake(); // blocks until handshake done } @@ -356,8 +327,9 @@ public class Http2TestServerConnection { if (stopping) return; stopping = true; - System.err.printf(server.name + ": Server connection to %s stopping. %d streams\n", - socket.getRemoteSocketAddress().toString(), streams.size()); + System.err.printf(server.name + ": Server connection to %s stopping (%s). %d streams\n", + socket.getRemoteSocketAddress().toString(), + (error == -1 ? "no error" : ("error="+error)), streams.size()); streams.forEach((i, q) -> { q.orderlyClose(); }); @@ -374,15 +346,20 @@ public class Http2TestServerConnection { private void readPreface() throws IOException { int len = clientPreface.length; byte[] bytes = new byte[len]; + System.err.println("reading preface"); int n = is.readNBytes(bytes, 0, len); - if (Arrays.compare(clientPreface, bytes) != 0) { - String msg = String.format("Invalid preface: read %s/%s bytes", n, len); - System.err.println(server.name + ": " + msg); - throw new IOException(msg +": \"" + - new String(bytes, 0, n, ISO_8859_1) - .replace("\r", "\\r") - .replace("\n", "\\n") - + "\""); + if (n >= 0) { + if (Arrays.compare(clientPreface, bytes) != 0) { + String msg = String.format("Invalid preface: read %s/%s bytes", n, len); + System.err.println(server.name + ": " + msg); + throw new IOException(msg +": \"" + + new String(bytes, 0, n, ISO_8859_1) + .replace("\r", "\\r") + .replace("\n", "\\n") + + "\""); + } + } else { + throw new IOException("EOF while reading preface"); } } @@ -686,7 +663,7 @@ public class Http2TestServerConnection { // all other streams created here @SuppressWarnings({"rawtypes","unchecked"}) - void createStream(HeaderFrame frame) throws IOException { + private boolean createStream(HeaderFrame frame, Http2TestServer.AltSvcAddr altSvcAddr) throws IOException { List frames = new LinkedList<>(); frames.add(frame); int streamid = frame.streamid(); @@ -725,7 +702,7 @@ public class Http2TestServerConnection { if (disallowedHeader.isPresent()) { throw new IOException("Unexpected HTTP2-Settings in headers:" + headers); } - + boolean altSvcSent = false; // skip processing the request if the server is configured to do so final String connKey = connectionKey(); final String path = headers.firstValue(":path").orElse(""); @@ -734,10 +711,21 @@ public class Http2TestServerConnection { + " and sending GOAWAY on server connection " + connKey + ", for request: " + path); sendGoAway(ErrorFrame.NO_ERROR); - return; + return altSvcSent; } + Queue q = new Queue(sentinel); streams.put(streamid, q); + + if (altSvcAddr != null) { + String originHost = headers.firstValue("host") + .or(() -> headers.firstValue(":authority")) + .orElse(null); + if (originHost != null) { + altSvcSent = sendAltSvc(originHost, altSvcAddr); + } + } + // keep track of the largest request id that we have processed int currentLargest = maxProcessedRequestStreamId.get(); while (streamid > currentLargest) { @@ -749,6 +737,7 @@ public class Http2TestServerConnection { exec.submit(() -> { handleRequest(headers, q, streamid, endStreamReceived); }); + return altSvcSent; } // runs in own thread. Handles request from start to finish. Incoming frames @@ -793,14 +782,19 @@ public class Http2TestServerConnection { headers, rspheadersBuilder, uri, bis, getSSLSession(), bos, this, pushAllowed); - // give to user - Http2Handler handler = server.getHandlerFor(uri.getPath()); - - // Need to pass the BodyInputStream reference to the BodyOutputStream, so it can determine if the stream - // must be reset due to the BodyInputStream not being consumed by the handler when invoked. - if (bis instanceof BodyInputStream bodyInputStream) bos.bis = bodyInputStream; - + final String reqPath = uri.getPath(); + // locate a handler for the request + final Http2Handler handler = server.getHandlerFor(reqPath); try { + // no handler available for the request path, respond with 404 + if (handler == null) { + respondForMissingHandler(exchange); + return; + } + // Need to pass the BodyInputStream reference to the BodyOutputStream, so it can determine if the stream + // must be reset due to the BodyInputStream not being consumed by the handler when invoked. + if (bis instanceof BodyInputStream bodyInputStream) bos.bis = bodyInputStream; + handler.handle(exchange); } catch (IOException closed) { if (bos.closed) { @@ -821,6 +815,16 @@ public class Http2TestServerConnection { close(-1); } } + private void respondForMissingHandler(final Http2TestExchange exchange) + throws IOException { + final byte[] responseBody = (this.getClass().getSimpleName() + + " - No handler available to handle request " + + exchange.getRequestURI()).getBytes(US_ASCII); + try (final OutputStream os = exchange.getResponseBody()) { + exchange.sendResponseHeaders(404, responseBody.length); + os.write(responseBody); + } + } public void sendFrames(List frames) throws IOException { synchronized (outputQ) { @@ -845,6 +849,7 @@ public class Http2TestServerConnection { @SuppressWarnings({"rawtypes","unchecked"}) void readLoop() { try { + boolean altSvcSent = false; while (!stopping) { Http2Frame frame = readFrameImpl(); if (frame == null) { @@ -885,7 +890,9 @@ public class Http2TestServerConnection { outputQ.put(rst); continue; } - createStream((HeadersFrame) frame); + final Http2TestServer.AltSvcAddr altSvcAddr = server.h3AltSvcAddr; + final boolean sendAltSvc = secure && !altSvcSent && altSvcAddr != null; + altSvcSent = createStream((HeadersFrame) frame, sendAltSvc ? altSvcAddr : null); } } else { if (q == null && !pushStreams.contains(stream)) { @@ -896,9 +903,12 @@ public class Http2TestServerConnection { } if (frame.type() == WindowUpdateFrame.TYPE) { WindowUpdateFrame wup = (WindowUpdateFrame) frame; - synchronized (updaters) { + updatersLock.lock(); + try { Consumer r = updaters.get(stream); r.accept(wup.getUpdate()); + } finally { + updatersLock.unlock(); } } else if (frame.type() == ResetFrame.TYPE) { // do orderly close on input q @@ -949,6 +959,28 @@ public class Http2TestServerConnection { } } + boolean sendAltSvc(final String originHost, final Http2TestServer.AltSvcAddr altSvcAddr) { + Objects.requireNonNull(originHost); + System.err.printf("TestServer: AltSvcFrame for: %s%n", originHost); + try { + URI url = new URI("https://" + originHost); + String origin = url.toASCIIString(); + String svc = "h3=\"" + altSvcAddr.host() + ":" + altSvcAddr.port() + "\""; + svc = "fooh2=\":443\"; ma=2592000; persist=1, " + svc; + svc = svc + ", bar3=\":446\"; ma=2592000; persist=1"; + svc = svc + ", h3-34=\"" + altSvcAddr.host() + ":" + altSvcAddr.port() + +"\"; ma=2592000; persist=1"; + AltSvcFrame frame = new AltSvcFrame(0, 0, Optional.of(origin), svc); + System.err.printf("TestServer: Sending AltSvcFrame for: %s [%s]%n", origin, svc); + outputQ.put(frame); + return true; + } catch (IOException | URISyntaxException x) { + System.err.println("TestServer: Failed to send AltSvcFrame: " + x); + x.printStackTrace(); + } + return false; + } + static boolean isClientStreamId(int streamid) { return (streamid & 0x01) == 0x01; } @@ -967,12 +999,13 @@ public class Http2TestServerConnection { public List encodeHeaders(HttpHeaders headers, BiPredicate insertionPolicy) { List buffers = new LinkedList<>(); - + var entrySet = headers.map().entrySet(); + if (entrySet.isEmpty()) return buffers; ByteBuffer buf = getBuffer(); boolean encoded; headersLock.lock(); try { - for (Map.Entry> entry : headers.map().entrySet()) { + for (Map.Entry> entry : entrySet) { List values = entry.getValue(); String key = entry.getKey().toLowerCase(); for (String value : values) { @@ -998,6 +1031,7 @@ public class Http2TestServerConnection { /** Encodes an ordered list of headers. */ public List encodeHeadersOrdered(List> headers) { List buffers = new LinkedList<>(); + if (headers.isEmpty()) return buffers; ByteBuffer buf = getBuffer(); boolean encoded; @@ -1045,7 +1079,9 @@ public class Http2TestServerConnection { } else throw x; } if (frame instanceof ResponseHeaders rh) { - var buffers = encodeHeaders(rh.headers, rh.insertionPolicy); + // order of headers matters - pseudo headers first followed by rest of the headers + final List encodedHeaders = new ArrayList(encodeHeaders(rh.pseudoHeaders, rh.insertionPolicy)); + encodedHeaders.addAll(encodeHeaders(rh.headers, rh.insertionPolicy)); int maxFrameSize = Math.min(rh.getMaxFrameSize(), getMaxFrameSize() - 64); int next = 0; int cont = 0; @@ -1054,9 +1090,9 @@ public class Http2TestServerConnection { // size we need to split the headers into one // HeadersFrame + N x ContinuationFrames int remaining = maxFrameSize; - var list = new ArrayList(buffers.size()); - for (; next < buffers.size(); next++) { - var b = buffers.get(next); + var list = new ArrayList(encodedHeaders.size()); + for (; next < encodedHeaders.size(); next++) { + var b = encodedHeaders.get(next); var len = b.remaining(); if (!b.hasRemaining()) continue; if (len <= remaining) { @@ -1072,7 +1108,7 @@ public class Http2TestServerConnection { } } int flags = rh.getFlags(); - if (next != buffers.size()) { + if (next != encodedHeaders.size()) { flags = flags & ~HeadersFrame.END_HEADERS; } if (cont > 0) { @@ -1087,7 +1123,7 @@ public class Http2TestServerConnection { } writeFrame(hf); cont++; - } while (next < buffers.size()); + } while (next < encodedHeaders.size()); } else if (frame instanceof OutgoingPushPromise) { handlePush((OutgoingPushPromise)frame); } else @@ -1109,7 +1145,7 @@ public class Http2TestServerConnection { PushPromiseFrame pp = new PushPromiseFrame(op.parentStream, op.getFlags(), promisedStreamid, - encodeHeaders(op.headers), + encodeHeaders(op.reqHeaders), 0); pushStreams.add(promisedStreamid); nextPushStreamId += 2; @@ -1140,7 +1176,7 @@ public class Http2TestServerConnection { oo.goodToGo(); exec.submit(() -> { try { - ResponseHeaders oh = getPushResponse(promisedStreamid); + ResponseHeaders oh = getPushResponse(promisedStreamid, op.rspHeaders); outputQ.put(oh); ii.transferTo(oo); @@ -1162,10 +1198,10 @@ public class Http2TestServerConnection { // returns a minimal response with status 200 // that is the response to the push promise just sent - private ResponseHeaders getPushResponse(int streamid) { - HttpHeadersBuilder hb = createNewHeadersBuilder(); - hb.addHeader(":status", "200"); - ResponseHeaders oh = new ResponseHeaders(hb.build()); + private ResponseHeaders getPushResponse(int streamid, HttpHeaders rspHeaders) { + HttpHeadersBuilder pseudoHeaders = createNewHeadersBuilder(); + pseudoHeaders.addHeader(":status", "200"); + ResponseHeaders oh = new ResponseHeaders(pseudoHeaders.build(), rspHeaders); oh.streamid(streamid); oh.setFlag(HeaderFrame.END_HEADERS); return oh; @@ -1187,7 +1223,9 @@ public class Http2TestServerConnection { try { byte[] buf = new byte[9]; int ret; + // System.err.println("TestServer: reading frame headers"); ret=is.readNBytes(buf, 0, 9); + // System.err.println("TestServer: got frame headers"); if (ret == 0) { return null; } else if (ret != 9) { @@ -1200,6 +1238,7 @@ public class Http2TestServerConnection { len = (len << 8) + n; } byte[] rest = new byte[len]; + // System.err.println("TestServer: reading frame body"); int n = is.readNBytes(rest, 0, len); if (n != len) throw new IOException("Error reading frame"); @@ -1364,82 +1403,60 @@ public class Http2TestServerConnection { // window updates done in main reader thread because they may // be used to unblock BodyOutputStreams waiting for WUPs - HashMap> updaters = new HashMap<>(); + final HashMap> updaters = new HashMap<>(); + final ReentrantLock updatersLock = new ReentrantLock(); void registerStreamWindowUpdater(int streamid, Consumer r) { - synchronized(updaters) { + updatersLock.lock(); + try { updaters.put(streamid, r); + } finally { + updatersLock.unlock(); } } - int sendWindow = 64 * 1024 - 1; // connection level send window + // connection level send window, permits = bytes + final Semaphore sendWindow = new Semaphore(64 * 1024 - 1); /** * BodyOutputStreams call this to get the connection window first. * * @param amount */ - public synchronized void obtainConnectionWindow(int amount) throws InterruptedException { - int demand = amount; - try { - int waited = 0; - while (amount > 0) { - int n = Math.min(amount, sendWindow); - amount -= n; - sendWindow -= n; - if (amount > 0) { - // Do not include this print line on a version that does not have - // JDK-8337395 - System.err.printf("%s: blocked waiting for %s connection window, obtained %s%n", - server.name, amount, demand - amount); - waited++; - wait(); - } - } - if (waited > 0) { - // Do not backport this print line on a version that does not have - // JDK-8337395 - System.err.printf("%s: obtained %s connection window, remaining %s%n", - server.name, demand, sendWindow); - } - assert amount == 0; - } catch (Throwable t) { - sendWindow += (demand - amount); - throw t; - } + public void obtainConnectionWindow(int amount) throws InterruptedException { + sendWindow.acquire(amount); } public void updateConnectionWindow(int amount) { - synchronized (this) { - // Do not backport this print line on a version that does not have - // JDK-8337395 - System.err.printf(server.name + ": update sendWindow (window=%s, amount=%s) is now: %s%n", - sendWindow, amount, sendWindow + amount); - sendWindow += amount; - notifyAll(); - } + System.err.printf("%s: sendWindow (available:%s, released amount=%s) is now: %s%n", + server.name, + sendWindow.availablePermits(), amount, sendWindow.availablePermits() + amount); + sendWindow.release(amount); } // simplified output headers class. really just a type safe container // for the hashmap. public static class ResponseHeaders extends Http2Frame { + final HttpHeaders pseudoHeaders; final HttpHeaders headers; final BiPredicate insertionPolicy; final int maxFrameSize; - public ResponseHeaders(HttpHeaders headers) { - this(headers, (n,v) -> false); + public ResponseHeaders(HttpHeaders pseudoHeaders, HttpHeaders headers) { + this(pseudoHeaders, headers, (n,v) -> false); } - public ResponseHeaders(HttpHeaders headers, BiPredicate insertionPolicy) { - this(headers, insertionPolicy, Integer.MAX_VALUE); + public ResponseHeaders(HttpHeaders pseudoHeaders, HttpHeaders headers, BiPredicate insertionPolicy) { + this(pseudoHeaders, headers, insertionPolicy, Integer.MAX_VALUE); } - public ResponseHeaders(HttpHeaders headers, + public ResponseHeaders(HttpHeaders pseudoHeaders, + HttpHeaders headers, BiPredicate insertionPolicy, int maxFrameSize) { super(0, 0); + this.pseudoHeaders = pseudoHeaders; this.headers = headers; this.insertionPolicy = insertionPolicy; this.maxFrameSize = maxFrameSize; diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java index 908a901133a..ace975e94fb 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/OutgoingPushPromise.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,7 +34,8 @@ import jdk.internal.net.http.frame.Http2Frame; // will be converted to a PushPromiseFrame in the writeLoop // a thread is then created to produce the DataFrames from the InputStream public class OutgoingPushPromise extends Http2Frame { - final HttpHeaders headers; + final HttpHeaders reqHeaders; + final HttpHeaders rspHeaders; final URI uri; final InputStream is; final int parentStream; // not the pushed streamid @@ -42,19 +43,22 @@ public class OutgoingPushPromise extends Http2Frame { public OutgoingPushPromise(int parentStream, URI uri, - HttpHeaders headers, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, InputStream is) { - this(parentStream, uri, headers, is, List.of()); + this(parentStream, uri, reqHeaders, rspHeaders, is, List.of()); } public OutgoingPushPromise(int parentStream, URI uri, - HttpHeaders headers, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, InputStream is, List continuations) { super(0,0); this.uri = uri; - this.headers = headers; + this.reqHeaders = reqHeaders; + this.rspHeaders = rspHeaders; this.is = is; this.parentStream = parentStream; this.continuations = List.copyOf(continuations); diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerConnection.java new file mode 100644 index 00000000000..4b0ea351a09 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerConnection.java @@ -0,0 +1,801 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.http3; + +import java.io.IOException; +import java.net.SocketAddress; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.CancelPushFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.GoAwayFrame; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.MaxPushIdFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.http3.streams.PeerUniStreamDispatcher; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +import static jdk.internal.net.http.http3.Http3Error.H3_STREAM_CREATION_ERROR; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS; +import static jdk.internal.net.http.http3.frames.SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY; +import static jdk.internal.net.http.quic.TerminationCause.appLayerClose; + +public class Http3ServerConnection { + private final Http3TestServer server; + private final QuicServerConnection quicConnection; + private final ConnectionTerminator quicConnTerminator; + private final SocketAddress peerAddress; + private final String dbgTag; + private final Logger debug; + private final UniStreamPair controlStreams; + private final UniStreamPair encoderStreams; + private final UniStreamPair decoderStreams; + private final Encoder qpackEncoder; + private final Decoder qpackDecoder; + private final FramesDecoder controlFramesDecoder; + private final AtomicLong nextPushId = new AtomicLong(); + private volatile long maxPushId = 0; + private final ReentrantLock pushIdLock = new ReentrantLock(); + private final Condition pushIdChanged = pushIdLock.newCondition(); + private final ConcurrentHashMap> requests = + new ConcurrentHashMap<>(); + private volatile boolean closeRequested; + // the max stream id of a processed H3 request. -1 implies none were processed. + private final AtomicLong maxProcessedRequestStreamId = new AtomicLong(-1); + // the stream id that was sent in a GOAWAY frame. -1 implies no GOAWAY frame was sent. + private final AtomicLong goAwayRequestStreamId = new AtomicLong(-1); + + private final CompletableFuture afterSettings = new MinimalFuture<>(); + private final CompletableFuture clientSettings = new MinimalFuture<>(); + + private final ConcurrentLinkedQueue lcsWriterQueue = + new ConcurrentLinkedQueue<>(); + + // A class used to dispatch peer initiated unidirectional streams + // according to their type. + private final class Http3StreamDispatcher extends PeerUniStreamDispatcher { + Http3StreamDispatcher(QuicReceiverStream stream) { + super(stream); + } + + @Override + protected Logger debug() { + return debug; + } + + @Override + protected void onStreamAbandoned(QuicReceiverStream stream) { + if (debug.on()) debug.log("Stream " + stream.streamId() + " abandoned!"); + qpackDecoder.cancelStream(stream.streamId()); + } + + @Override + protected void onControlStreamCreated(String description, QuicReceiverStream stream) { + if (debug.on()) { + debug.log("peerControlStream %s dispatched", stream.streamId()); + } + complete(description, stream, controlStreams.futureReceiverStream()); + } + + @Override + protected void onEncoderStreamCreated(String description, QuicReceiverStream stream) { + if (debug.on()) debug.log("peer opened QPack encoder stream"); + complete(description, stream, decoderStreams.futureReceiverStream()); + } + + @Override + protected void onDecoderStreamCreated(String description, QuicReceiverStream stream) { + if (debug.on()) debug.log("peer opened QPack decoder stream"); + complete(description, stream, encoderStreams.futureReceiverStream()); + } + + @Override + protected void onPushStreamCreated(String description, QuicReceiverStream stream, long pushId) { + // From RFC 9114: + // Only servers can push; if a server receives a client-initiated push stream, + // this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR. + close(H3_STREAM_CREATION_ERROR.code(), + "Push Stream %s opened by client" + .formatted(stream.streamId())); + } + + // completes the given completable future with the given stream + private void complete(String description, QuicReceiverStream stream, + CompletableFuture cf) { + if (debug.on()) { + debug.log("completing CF for %s with stream %s", description, stream.streamId()); + } + boolean completed = cf.complete(stream); + if (!completed) { + if (!cf.isCompletedExceptionally()) { + debug.log("CF for %s already completed with stream %s!", description, cf.resultNow().streamId()); + close(Http3Error.H3_STREAM_CREATION_ERROR, + "%s already created".formatted(description)); + } else { + debug.log("CF for %s already completed exceptionally!", description); + } + } + } + + static CompletableFuture dispatch(Http3ServerConnection conn, + QuicReceiverStream stream) { + var dispatcher = conn.new Http3StreamDispatcher(stream); + dispatcher.start(); + return dispatcher.dispatchCF(); + } + } + + /** + * Creates a new {@code Http3ServerConnection}. + * Once created, the connection must be {@linkplain #start()} started. + * @param server the HTTP/3 server creating this connection + * @param connection the underlying Quic connection + */ + Http3ServerConnection(Http3TestServer server, + QuicServerConnection connection, + SocketAddress peerAddress) { + this.server = server; + this.quicConnection = connection; + this.quicConnTerminator = connection.connectionTerminator(); + this.peerAddress = peerAddress; + var qtag = connection.dbgTag(); + dbgTag = "H3-Server(" + qtag + ")"; + debug = Utils.getDebugLogger(this::dbgTag); + controlFramesDecoder = new FramesDecoder("H3-Server-control("+qtag+")", + FramesDecoder::isAllowedOnClientControlStream); + controlStreams = new UniStreamPair(StreamType.CONTROL, + quicConnection, this::receiveControlBytes, + this::lcsWriterLoop, + this::onControlStreamError, debug); + qpackEncoder = new Encoder(this::qpackInsertionPolicy, + this::createEncoderStreams, + this::connectionError); + encoderStreams = qpackEncoder.encoderStreams(); + qpackDecoder = new Decoder(this::createDecoderStreams, + this::connectionError); + decoderStreams = qpackDecoder.decoderStreams(); + } + + boolean qpackInsertionPolicy(TableEntry entry) { + List allowedHeaders = Http3TestServer.ENCODER_ALLOWED_HEADERS; + if (allowedHeaders.isEmpty()) { + return false; + } + if (allowedHeaders.contains(Http3TestServer.ALL_ALLOWED)) { + return true; + } + return allowedHeaders.contains(entry.name()); + } + + /** + * Starts this {@code Http3ServerConnection}. + */ + public void start() { + quicConnection.addRemoteStreamListener(this::onNewRemoteStream); + quicConnection.onHandshakeCompletion(this::handshakeDone); + } + + // push bytes to the local control stream queue + void writeControlStream(ByteBuffer buffer) { + lcsWriterQueue.add(buffer); + controlStreams.localWriteScheduler().runOrSchedule(); + } + + QuicConnectionImpl quicConnection() { + return this.quicConnection; + } + + Http3TestServer server() { + return this.server; + } + + String connectionKey() { + // assuming the localConnectionId never changes; + // this will return QuicServerConnectionId(NNN), which should + // be enough to detect whether two exchanges are made on the + // same connection + return quicConnection.logTag(); + } + + // The local control stream write loop + private void lcsWriterLoop() { + var controlStreams = this.controlStreams; + if (controlStreams == null) return; + var writer = controlStreams.localWriter(); + if (writer == null) return; + ByteBuffer buffer; + if (debug.on()) + debug.log("start control writing loop: credit=" + writer.credit()); + while (writer.credit() > 0 && (buffer = lcsWriterQueue.poll()) != null) { + try { + if (debug.on()) + debug.log("schedule %s bytes for writing on control stream", buffer.remaining()); + writer.scheduleForWriting(buffer, buffer == QuicStreamReader.EOF); + } catch (Throwable t) { + var stream = writer.stream(); + Http3Streams.debugErrorCode(debug, stream, "Control stream"); + if (!closeRequested && quicConnection.isOpen()) { + if (!Http3Error.isNoError(stream.sndErrorCode())) { + if (debug.on()) debug.log("Failed to write to control stream", t); + } + close(Http3Error.H3_CLOSED_CRITICAL_STREAM, "Failed to write to control stream"); + return; + } + } + } + } + + private boolean hasHttp3Error(QuicStream stream) { + if (stream instanceof QuicReceiverStream rcvs) { + var code = rcvs.rcvErrorCode(); + if (code > 0 && !Http3Error.isNoError(code)) return true; + } + if (stream instanceof QuicSenderStream snds) { + var code = snds.sndErrorCode(); + if (code > 0 && !Http3Error.isNoError(code)) return true; + } + return false; + } + + + private void onControlStreamError(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + // TODO: implement this! + try { + Http3Streams.debugErrorCode(debug, stream, "Control stream"); + if (!closeRequested && quicConnection.isOpen()) { + if (hasHttp3Error(stream)) { + if (debug.on()) { + debug.log("control stream " + stream.mode() + " failed", throwable); + } + } + close(Http3Error.H3_CLOSED_CRITICAL_STREAM, + "Control stream " + stream.mode() + " failed"); + } + } catch (Throwable t) { + if (debug.on() && !closeRequested) { + debug.log("onControlStreamError: handling ", throwable); + debug.log("onControlStreamError: exception while handling error: ", t); + } + } + } + + private void receiveControlBytes(ByteBuffer buffer) { + if (debug.on()) debug.log("received client control: %s bytes", buffer.remaining()); + controlFramesDecoder.submit(buffer); + Http3Frame frame; + while ((frame = controlFramesDecoder.poll()) != null) { + if (debug.on()) debug.log("client control frame: %s", frame); + if (frame instanceof MalformedFrame malformed) { + var cause = malformed.getCause(); + if (cause != null && debug.on()) { + debug.log(malformed.toString(), cause); + } + close(malformed.getErrorCode(), malformed.getMessage()); + controlStreams.stopSchedulers(); + controlFramesDecoder.clear(); + return; + } else if (frame instanceof PartialFrame) { + var payloadBytes = controlFramesDecoder.readPayloadBytes(); + if (debug.on()) { + debug.log("added %s bytes to %s", + Utils.remaining(payloadBytes), + frame); + } + } else if (frame instanceof CancelPushFrame cpf) { + cancelPushReceived(cpf.getPushId()); + } else if (frame instanceof MaxPushIdFrame mpf) { + maxPushIdReceived(mpf.getMaxPushId()); + } else if (frame instanceof SettingsFrame sf) { + ConnectionSettings clientSettings = ConnectionSettings.createFrom(sf); + // Set max and current capacity of the QPack encoder + qpackEncoder.configure(clientSettings); + long clientMaxTableCapacity = clientSettings.qpackMaxTableCapacity(); + long capacity = Math.min(Http3TestServer.ENCODER_CAPACITY_LIMIT, + clientMaxTableCapacity); + // RFC9204 3.2.3. Maximum Dynamic Table Capacity: + // "When the maximum table capacity is zero, the encoder MUST NOT + // insert entries into the dynamic table and MUST NOT send any encoder + // instructions on the encoder stream." + if (clientMaxTableCapacity != 0) { + qpackEncoder.setTableCapacity(capacity); + } + this.clientSettings.complete(clientSettings); + } + if (controlFramesDecoder.eof()) break; + } + if (controlFramesDecoder.eof()) { + close(Http3Error.H3_CLOSED_CRITICAL_STREAM, + "EOF reached while reading client control stream"); + } + } + + private void handshakeDone(Throwable t) { + if (t == null) { + controlStreams.futureSenderStream() + .thenApply(this::sendSettings) + .exceptionally(this::exceptionallyAndClose) + .thenApply(afterSettings::complete); + } else { + if (debug.on()) debug.log("Handshake failed: " + t, t); + // the connection is probably closed already, but just in case... + close(Http3Error.H3_INTERNAL_ERROR, "Handshake failed"); + } + } + + private T exceptionallyAndClose(Throwable t) { + try { + return exceptionally(t); + } finally { + // TODO: should we distinguish close due to + // exception from graceful close? + close(Http3Error.H3_INTERNAL_ERROR, message(t)); + } + } + + String message(Throwable t) { + return t == null ? "No Error" : t.getClass().getSimpleName(); + } + + private T exceptionally(Throwable t) { + try { + if (debug.on()) debug.log(t.getMessage(), t); + throw t; + } catch (RuntimeException | Error r) { + throw r; + } catch (ExecutionException x) { + throw new CompletionException(x.getMessage(), x.getCause()); + } catch (Throwable e) { + throw new CompletionException(e.getMessage(), e); + } + } + + private QuicSenderStream sendSettings(final QuicSenderStream localControlStream) { + final ConnectionSettings settings = server.getConfiguredConnectionSettings(); + final SettingsFrame settingsFrame = new SettingsFrame(); + + settingsFrame.setParameter(SETTINGS_MAX_FIELD_SECTION_SIZE, settings.maxFieldSectionSize()); + settingsFrame.setParameter(SETTINGS_QPACK_MAX_TABLE_CAPACITY, settings.qpackMaxTableCapacity()); + settingsFrame.setParameter(SETTINGS_QPACK_BLOCKED_STREAMS, settings.qpackBlockedStreams()); + qpackDecoder.configure(settings); + + if (debug.on()) { + debug.log("sending server settings %s for connection %s", settingsFrame, this); + } + final long size = settingsFrame.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + var buf = ByteBuffer.allocate((int)settingsFrame.size()); + settingsFrame.writeFrame(buf); + buf.flip(); + writeControlStream(buf); + return localControlStream; + } + + + private boolean onNewRemoteStream(QuicReceiverStream stream) { + boolean closeRequested = this.closeRequested; + if (closeRequested) return false; + + if (stream instanceof QuicBidiStream bidiStream) { + onNewHttpRequest(bidiStream); + } else { + Http3StreamDispatcher.dispatch(this, stream).whenComplete((r, t) -> { + if (t != null) dispatchFailed(t); + }); + } + if (debug.on()) { + debug.log("New stream %s accepted", stream.streamId()); + } + return true; + } + + private void onNewHttpRequest(QuicBidiStream stream) { + if (!this.server.shouldProcessNewHTTPRequest(this)) { + if (debug.on()) { + debug.log("Rejecting HTTP request on stream %s of connection %s", + stream.streamId(), this); + } + // consider the request as unprocessed and send a GOAWAY on the connection + try { + sendGoAway(); + } catch (IOException ioe) { + System.err.println("Failed to send GOAWAY on connection " + this + + " due to: " + ioe); + ioe.printStackTrace(); + } + return; + } + var streamId = stream.streamId(); + // keep track of the largest request id that we have processed + long currentLargest = maxProcessedRequestStreamId.get(); + while (streamId > currentLargest) { + if (maxProcessedRequestStreamId.compareAndSet(currentLargest, streamId)) { + break; + } + currentLargest = maxProcessedRequestStreamId.get(); + } + if (debug.on()) { + debug.log("new incoming HTTP request on stream %s", streamId); + } + if (requests.containsKey(stream.streamId())) { + if (debug.on()) { + debug.log("Stream %s already created!", streamId); + } + quicConnTerminator.terminate(appLayerClose(H3_STREAM_CREATION_ERROR.code()) + .loggedAs("stream already created")); + return; + } + // creation of the Http3ServerExchangeImpl involves connecting its reader, which + // takes a fair amount of (JIT?) time. Since this method is called from + // within the decrypt loop, it prevents decrypting the following ONERTT packets, + // which can unnecessarily delay the processing of ACKs and cause excessive + // retransmission + MinimalFuture exchCf = new MinimalFuture<>(); + requests.put(stream.streamId(), exchCf); + if (debug.on()) { + debug.log("HTTP/3 exchange future for stream %s registered", streamId); + } + server.getQuicServer().executor().execute(() -> createExchange(exchCf, stream)); + if (debug.on()) { + debug.log("HTTP/3 exchange creation for stream %s triggered", streamId); + } + } + + private void createExchange(CompletableFuture exchCf, + QuicBidiStream stream) { + var streamId = stream.streamId(); + if (debug.on()) { + debug.log("Completing HTTP/3 exchange future for stream %s", streamId); + } + exchCf.complete(new Http3ServerStreamImpl(this, stream)); + if (debug.on()) { + debug.log("HTTP/3 exchange future for stream %s Completed", streamId); + } + } + + public final String dbgTag() { return dbgTag; } + + private void dispatchFailed(Throwable throwable) { + // TODO: anything to do? + if (debug.on()) debug.log("dispatch failed: " + throwable); + } + + QueuingStreamPair createEncoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_ENCODER, quicConnection, + encoderReceiver, this::onEncoderStreamsFailed, debug); + } + + private void onEncoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + // TODO: implement this! + // close connection here. + if (!closeRequested) { + String message = stream != null ? stream.mode() + " failed" : "is null"; + if (quicConnection().isOpen()) { + if (debug.on()) { + debug.log("QPack encoder stream " + message, throwable); + } + } else { + if (debug.on()) { + debug.log("QPack encoder stream " + message + ": " + throwable); + } + } + } + } + + QueuingStreamPair createDecoderStreams(Consumer encoderReceiver) { + return new QueuingStreamPair(StreamType.QPACK_DECODER, quicConnection, + encoderReceiver, this::onDecoderStreamsFailed, debug); + } + + private void onDecoderStreamsFailed(final QuicStream stream, final UniStreamPair uniStreamPair, + final Throwable throwable) { + // TODO: implement this! + // close connection here. + if (!closeRequested) { + String message = stream != null ? stream.mode() + " failed" : "is null"; + if (quicConnection().isOpen()) { + if (debug.on()) { + debug.log("QPack decoder stream " + message, throwable); + } + } else { + debug.log("QPack decoder stream " + message + ": " + throwable); + } + } + } + + // public, to allow invocations from within tests + public void sendGoAway() throws IOException { + final QuicStreamWriter writer = controlStreams.localWriter(); + if (writer == null || !quicConnection.isOpen()) { + return; + } + // RFC-9114, section 5.2: + // Requests ... with the indicated identifier or greater + // are rejected ... by the sender of the GOAWAY. + final long maxProcessedStreamId = maxProcessedRequestStreamId.get(); + // adding 4 gets us the next stream id for the stream type + final long streamIdToReject = maxProcessedStreamId == -1 ? 0 : maxProcessedStreamId + 4; + // An endpoint MAY send multiple GOAWAY frames indicating different + // identifiers, but the identifier in each frame MUST NOT be greater + // than the identifier in any previous frame, since clients might + // already have retried unprocessed requests on another HTTP connection. + long currentGoAwayReqStrmId = goAwayRequestStreamId.get(); + while (currentGoAwayReqStrmId != -1 && streamIdToReject < currentGoAwayReqStrmId) { + if (goAwayRequestStreamId.compareAndSet(currentGoAwayReqStrmId, streamIdToReject)) { + break; + } + currentGoAwayReqStrmId = goAwayRequestStreamId.get(); + } + final GoAwayFrame frame = new GoAwayFrame(streamIdToReject); + final long size = frame.size(); + assert size >= 0 && size < Integer.MAX_VALUE; + final var buf = ByteBuffer.allocate((int) size); + frame.writeFrame(buf); + buf.flip(); + if (debug.on()) { + debug.log("Sending GOAWAY frame %s from server connection %s", frame, this); + } + writer.scheduleForWriting(buf, false); + } + + public void close(Http3Error error, String reason) { + close(error.code(), reason); + } + + void connectionError(Throwable throwable, Http3Error error) { + close(error, throwable.getMessage()); + } + + private boolean markCloseRequested() { + var closeRequested = this.closeRequested; + if (!closeRequested) { + synchronized (this) { + closeRequested = this.closeRequested; + if (!closeRequested) { + return this.closeRequested = true; + } + } + } + assert closeRequested; + if (debug.on()) debug.log("close already requested"); + return false; + } + + public void close(long error, String reason) { + if (markCloseRequested()) { + try { + sendGoAway(); + } catch (IOException e) { + // it's OK if we couldn't send a GOAWAY + if (debug.on()) { + debug.log("ignoring failure to send GOAWAY from server connection " + + this + " due to " + e); + } + } + if (quicConnection.isOpen()) { + if (debug.on()) debug.log("closing quic connection: " + reason); + quicConnTerminator.terminate(appLayerClose(error).loggedAs(reason)); + } else { + if (debug.on()) debug.log("quic connection already closed"); + } + } + } + + HeaderFrameReader newHeaderFrameReader(DecodingCallback decodingCallback) { + return qpackDecoder.newHeaderFrameReader(decodingCallback); + } + + void exchangeClosed(Http3ServerStreamImpl http3ServerExchange) { + requests.remove(http3ServerExchange.streamId()); + } + + sealed interface PushPromise permits CancelledPush, CompletedPush, PendingPush { + long pushId(); + } + + record CancelledPush(long pushId) implements PushPromise {} + record CompletedPush(long pushId, HttpHeaders headers) implements PushPromise {} + record PendingPush(long pushId, + CompletableFuture stream, + HttpHeaders headers, + Http3ServerExchange exchange) implements PushPromise { + } + + private final Map promiseMap = new ConcurrentHashMap<>(); + + PushPromise addPendingPush(long pushId, + CompletableFuture stream, + HttpHeaders headers, + Http3ServerExchange exchange) { + var push = new PendingPush(pushId, stream, headers, exchange); + expungePromiseMap(); + var previous = promiseMap.putIfAbsent(pushId, push); + if (previous == null || !(previous instanceof CancelledPush)) { + // allow to open multiple streams for the same pushId + // in order to test client behavior. We will return + // push even if the map contains a pending or completed + // push; + return push; + } + return previous; + } + + void addPushPromise(final long promiseId, final PushPromise promise) { + this.promiseMap.put(promiseId, promise); + } + + PushPromise getPushPromise(final long promiseId) { + return this.promiseMap.get(promiseId); + } + + void cancelPush(long pushId) { + expungePromiseMap(); + var push = promiseMap.putIfAbsent(pushId, new CancelledPush(pushId)); + if (push == null || push instanceof CancelledPush) return; + if (push instanceof CompletedPush) return; + if (push instanceof PendingPush pp) { + promiseMap.put(pushId, new CancelledPush(pushId)); + var ps = pp.stream(); + if (ps == null) { + try { + sendCancelPush(pushId); + } catch (IOException io) { + if (debug.on()) { + debug.log("Failed to send CANCEL_PUSH pushId=%s: %s", pushId, io); + } + } + } else { + ps.thenAccept(s -> { + try { + s.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } catch (IOException io) { + if (debug.on()) { + debug.log("Failed to reset push stream pushId=%s, stream=%s: %s", + pushId, s.streamId(), io); + } + } + }); + } + } + } + + void sendCancelPush(long pushId) throws IOException { + CancelPushFrame cancelPushFrame = new CancelPushFrame(pushId); + ByteBuffer buf = ByteBuffer.allocate((int)cancelPushFrame.size()); + cancelPushFrame.writeFrame(buf); + buf.flip(); + // need to wait until after settings are sent. + afterSettings.thenAccept((s) -> writeControlStream(buf)); + } + + + void cancelPushReceived(long pushId) { + cancelPush(pushId); + } + + void maxPushIdReceived(long pushId) { + pushIdLock.lock(); + try { + if (pushId > maxPushId) { + if (debug.on()) debug.log("max pushId: " + pushId); + maxPushId = pushId; + pushIdChanged.signalAll(); + } + } finally { + pushIdLock.unlock(); + } + } + + final AtomicLong minPush = new AtomicLong(); + static final int MAX_PUSH_HISTORY = 100; + void expungePromiseMap() { + assert MAX_PUSH_HISTORY > 0; + while (promiseMap.size() >= MAX_PUSH_HISTORY) { + long lowest = minPush.getAndIncrement(); + var pp = promiseMap.remove(lowest); + if (pp instanceof PendingPush ppp) { + cancelPush(ppp.pushId); + } + } + } + + List encodeHeaders(int bufferSize, long streamId, HttpHeaders... headers) { + HeaderFrameWriter writer = qpackEncoder.newHeaderFrameWriter(); + return qpackEncoder.encodeHeaders(writer, streamId, bufferSize, headers); + } + + void decodeHeaders(final HeadersFrame partialHeadersFrame, final ByteBuffer buffer, + final HeaderFrameReader headersReader) throws IOException { + ByteBuffer received = partialHeadersFrame.nextPayloadBytes(buffer); + boolean done = partialHeadersFrame.remaining() == 0; + this.qpackDecoder.decodeHeader(received, done, headersReader); + } + + long nextPushId() { + return this.nextPushId.getAndIncrement(); + } + + long waitForMaxPushId(long pushId) throws InterruptedException { + long maxPushId = this.maxPushId; + if (maxPushId > pushId) return maxPushId; + do { + this.pushIdLock.lock(); + try { + maxPushId = this.maxPushId; + if (maxPushId > pushId) return maxPushId; + this.pushIdChanged.await(); + } finally { + this.pushIdLock.unlock(); + } + } while (true); + } + + public Encoder qpackEncoder() { + return qpackEncoder; + } + + public CompletableFuture clientHttp3Settings() { + return clientSettings; + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerExchange.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerExchange.java new file mode 100644 index 00000000000..9a39ba4358d --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerExchange.java @@ -0,0 +1,801 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.http3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.LongSupplier; + +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.PushPromiseFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +public final class Http3ServerExchange implements Http2TestExchange { + + private final Http3ServerStreamImpl serverStream; + private final Http3ServerConnection serverConn; + private final Logger debug; + final HttpHeaders requestHeaders; + final String method; + final String scheme; + final String authority; + final String path; + final URI uri; + final HttpHeadersBuilder rspheadersBuilder; + final SSLSession sslSession; + volatile long responseLength = 0; // 0 is unknown, -1 is 0 + volatile int responseCode; + final Http3ServerStreamImpl.RequestBodyInputStream is; + final ResponseBodyOutputStream os; + private boolean unknownReservedFrameAlreadySent; + + Http3ServerExchange(Http3ServerStreamImpl serverStream, HttpHeaders requestHeaders, + Http3ServerStreamImpl.RequestBodyInputStream is, SSLSession sslSession) { + this.serverStream = serverStream; + this.serverConn = serverStream.serverConnection(); + this.debug = Utils.getDebugLogger(this.serverConn::dbgTag); + this.requestHeaders = requestHeaders; + this.sslSession = sslSession; + this.is = is; + this.os = new ResponseBodyOutputStream(connectionTag(), debug, serverStream.writer, serverStream.writeLock, + serverStream.writeEnabled, this::getResponseLength); + method = requestHeaders.firstValue(":method").orElse(""); + //System.out.println("method = " + method); + path = requestHeaders.firstValue(":path").orElse(""); + //System.out.println("path = " + path); + scheme = requestHeaders.firstValue(":scheme").orElse(""); + //System.out.println("scheme = " + scheme); + authority = requestHeaders.firstValue(":authority").orElse(""); + if (!path.isEmpty() && !path.startsWith("/")) { + throw new IllegalArgumentException("Path is not absolute: " + path); + } + uri = URI.create(scheme + "://" + authority + path); + rspheadersBuilder = new HttpHeadersBuilder(); + } + + String connectionTag() { + return serverConn.quicConnection().logTag(); + } + + long getResponseLength() { + return responseLength; + } + + @Override + public String toString() { + return "H3Server Http3ServerExchange(%s)".formatted(serverStream.streamId()); + } + + @Override + public HttpHeaders getRequestHeaders() { + return requestHeaders; + } + + @Override + public HttpHeadersBuilder getResponseHeaders() { + return rspheadersBuilder; + } + + @Override + public URI getRequestURI() { + return uri; + } + + @Override + public String getRequestMethod() { + return method; + } + + @Override + public SSLSession getSSLSession() { + return sslSession; + } + + @Override + public void close() { + try { + is.close(); + os.close(); + serverStream.close(); + } catch (IOException e) { + if (debug.on()) { + debug.log(this + ".close exception: " + e, e); + } + } + } + + @Override + public void close(IOException io) throws IOException { + if (debug.on()) { + debug.log(this + " closed with exception: " + io); + } + if (serverStream.writer.sendingState().isSending()) { + if (debug.on()) { + debug.log(this + " resetting writer with H3_INTERNAL_ERROR"); + } + serverStream.writer.reset(Http3Error.H3_INTERNAL_ERROR.code()); + } + is.close(io); + os.closeInternal(); + close(); + } + + public Http3ServerExchange streamResetByPeer(IOException io) { + try { + if (debug.on()) + debug.log("H3 Server closing exchange: " + io); + close(io); + } catch (IOException e) { + if (debug.on()) + debug.log("Failed to close stream %s", serverStream.streamId()); + } + return this; + } + + @Override + public InputStream getRequestBody() { + return is; + } + + @Override + public OutputStream getResponseBody() { + return os; + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + // occasionally send an unknown/reserved HTTP3 frame to exercise the case + // where the client is expected to ignore such frames + try { + optionallySendUnknownOrReservedFrame(); + this.responseLength = responseLength; + sendResponseHeaders(serverStream.streamId(), serverStream.writer, isHeadRequest(), + rCode, responseLength, rspheadersBuilder, os); + } catch (Exception ex) { + throw new IOException("failed to send headers: " + ex, ex); + } + } + + // WARNING: this method is also called for PushStreams, which has + // a different writer, streamId, request etc... + // The only fields that can be safely used in this method is debug and + // http3ServerConnection + private void sendResponseHeaders(long streamId, + QuicStreamWriter writer, + boolean isHeadRequest, + int rCode, + long responseLength, + HttpHeadersBuilder rspheadersBuilder, + ResponseBodyOutputStream os) + throws IOException { + String tag = "streamId=" + streamId + " "; + // in case of HEAD request the caller is supposed to set Content-Length + // directly - and the responseLength passed here is supposed to be -1 + if (responseLength != 0 && rCode != 204 && !isHeadRequest) { + long clen = responseLength > 0 ? responseLength : 0; + rspheadersBuilder.setHeader("Content-length", Long.toString(clen)); + } + final HttpHeadersBuilder pseudoHeadersBuilder = new HttpHeadersBuilder(); + pseudoHeadersBuilder.setHeader(":status", Integer.toString(rCode)); + final HttpHeaders pseudoHeaders = pseudoHeadersBuilder.build(); + final HttpHeaders headers = rspheadersBuilder.build(); + // order of headers matters - pseudo headers first followed by rest of the headers + var payload = serverConn.encodeHeaders(1024, streamId, pseudoHeaders, headers); + if (debug.on()) + debug.log(tag + "headers payload: " + Utils.remaining(payload)); + HeadersFrame frame = new HeadersFrame(Utils.remaining(payload)); + ByteBuffer buffer = ByteBuffer.allocate(frame.headersSize()); + frame.writeHeaders(buffer); + buffer.flip(); + if (debug.on()) { + debug.log(tag + "Writing HeaderFrame headers: " + Utils.asHexString(buffer)); + } + boolean noBody = rCode >= 200 && (responseLength < 0 || rCode == 204); + boolean last = frame.length() == 0 && noBody; + if (last) { + if (debug.on()) { + debug.log(tag + "last payload sent: empty headers, no body"); + } + writer.scheduleForWriting(buffer, true); + } else { + writer.queueForWriting(buffer); + } + int size = payload.size(); + for (int i = 0; i < size; i++) { + last = i == size - 1; + var buf = payload.get(i); + if (debug.on()) { + debug.log(tag + "Writing HeaderFrame payload: " + Utils.asHexString(buf)); + } + if (last) { + if (debug.on()) { + debug.log(tag + "last headers bytes sent, %s", + noBody ? "no body" : "body should follow"); + } + writer.scheduleForWriting(buf, noBody); + } else { + writer.queueForWriting(buf); + } + } + if (noBody) { + if (debug.on()) { + debug.log(tag + "no body: closing os"); + } + os.closeInternal(); + } + os.goodToGo(); + if (debug.on()) { + debug.log(this + " Sent response headers " + tag + rCode); + } + } + + private void optionallySendUnknownOrReservedFrame() { + if (this.unknownReservedFrameAlreadySent) { + // don't send it more than once + return; + } + UnknownOrReservedFrame.tryGenerateFrame().ifPresent((f) -> { + if (debug.on()) { + debug.log("queueing to send an unknown/reserved HTTP3 frame: " + f); + } + try { + serverStream.writer.queueForWriting(f.toByteBuffer()); + } catch (IOException e) { + // ignore + if (debug.on()) { + debug.log("failed to queue unknown/reserved HTTP3 frame: " + f, e); + } + } + this.unknownReservedFrameAlreadySent = true; + }); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return serverConn.quicConnection().peerAddress(); + } + + @Override + public int getResponseCode() { + return responseCode; + } + + @Override + public InetSocketAddress getLocalAddress() { + return (InetSocketAddress) serverConn.quicConnection().localAddress(); + } + + @Override + public String getConnectionKey() { + return serverConn.connectionKey(); + } + + @Override + public String getProtocol() { + return "HTTP/3"; + } + + @Override + public HttpClient.Version getServerVersion() { + return HttpClient.Version.HTTP_3; + } + + @Override + public HttpClient.Version getExchangeVersion() { + return HttpClient.Version.HTTP_3; + } + + @Override + public boolean serverPushAllowed() { + return true; + } + + @Override + public void serverPush(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + try { + serverPushWithId(uri, reqHeaders, rspHeaders, content); + } catch (IOException io) { + if (debug.on()) + debug.log("Failed to push " + uri + ": " + io); + throw io; + } + } + + @Override + public long serverPushWithId(URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + HttpHeaders combinePromiseHeaders = combinePromiseHeaders(uri, reqHeaders); + long pushId = serverConn.nextPushId(); + if (debug.on()) { + debug.log("Server sending serverPushWithId(" + pushId + "): " + uri); + } + // send PUSH_PROMISE frame + sendPushPromiseFrame(pushId, uri, combinePromiseHeaders); + if (debug.on()) + debug.log("Server sent PUSH_PROMISE(" + pushId + ")"); + // now open push stream and send response headers + body + Http3ServerConnection.PushPromise pp = sendPushResponse(pushId, combinePromiseHeaders, rspHeaders, content); + assert pushId == pp.pushId(); + return pp.pushId(); + } + + @Override + public long sendPushId(long pushId, URI uri, HttpHeaders headers) throws IOException { + HttpHeaders combinePromiseHeaders = combinePromiseHeaders(uri, headers); + return sendPushPromiseFrame(pushId, uri, combinePromiseHeaders); + } + + @Override + public void sendPushResponse(long pushId, URI uri, HttpHeaders reqHeaders, HttpHeaders rspHeaders, InputStream content) + throws IOException { + HttpHeaders combinePromiseHeaders = combinePromiseHeaders(uri, reqHeaders); + Http3ServerConnection.PushPromise pp = sendPushResponse(pushId, combinePromiseHeaders, rspHeaders, content); + assert pushId == pp.pushId(); + } + + @Override + public void resetStream(long code) throws IOException { + os.resetStream(code); + } + + @Override + public void cancelPushId(long pushId) throws IOException { + serverConn.sendCancelPush(pushId); + } + + @Override + public long waitForMaxPushId(long pushId) throws InterruptedException { + return serverConn.waitForMaxPushId(pushId); + } + + @Override + public Encoder qpackEncoder() { + return serverConn.qpackEncoder(); + } + + @Override + public CompletableFuture clientHttp3Settings() { + return serverConn.clientHttp3Settings(); + } + + private long sendPushPromiseFrame(long pushId, URI uri, HttpHeaders headers) + throws IOException { + if (pushId == -1) pushId = serverConn.nextPushId(); + List payload = serverConn.encodeHeaders(1024, serverStream.streamId(), headers); + PushPromiseFrame frame = new PushPromiseFrame(pushId, Utils.remaining(payload)); + ByteBuffer buffer = ByteBuffer.allocate(frame.headersSize()); + frame.writeHeaders(buffer); + buffer.flip(); + boolean last = frame.length() == 0; + if (last) { + if (debug.on()) { + debug.log("last payload sent: empty headers, no body"); + } + serverStream.writer.scheduleForWriting(buffer, false); + } else { + serverStream.writer.queueForWriting(buffer); + } + int size = payload.size(); + for (int i = 0; i < size; i++) { + last = i == size - 1; + var buf = payload.get(i); + if (last) { + serverStream.writer.scheduleForWriting(buf, false); + } else { + serverStream.writer.queueForWriting(buf); + } + } + return pushId; + } + + private static HttpHeaders combinePromiseHeaders(URI uri, HttpHeaders headers) { + HttpHeadersBuilder headersBuilder = new HttpHeadersBuilder(); + headersBuilder.setHeader(":method", "GET"); + headersBuilder.setHeader(":scheme", uri.getScheme()); + headersBuilder.setHeader(":authority", uri.getAuthority()); + headersBuilder.setHeader(":path", uri.getPath()); + for (Map.Entry> entry : headers.map().entrySet()) { + for (String value : entry.getValue()) + headersBuilder.addHeader(entry.getKey(), value); + } + return headersBuilder.build(); + } + + @Override + public void requestStopSending(long errorCode) { + serverStream.reader.stream().requestStopSending(errorCode); + } + + private QuicSenderStream cancel(QuicSenderStream s) { + try { + switch (s.sendingState()) { + case READY, SEND, DATA_SENT -> s.reset(Http3Error.H3_REQUEST_CANCELLED.code()); + } + } catch (IOException io) { + throw new UncheckedIOException(io); + } + return s; + } + + private Http3ServerConnection.PushPromise sendPushResponse(long pushId, + HttpHeaders reqHeaders, + HttpHeaders rspHeaders, + InputStream body) { + var stream = serverConn.quicConnection() + .openNewLocalUniStream(Duration.ofSeconds(10)); + final Http3ServerConnection.PushPromise promise = + serverConn.addPendingPush(pushId, stream, reqHeaders, this); + if (!(promise instanceof Http3ServerConnection.PendingPush)) { + stream.thenApply(this::cancel); + return promise; + } + stream.thenApplyAsync(s -> { + if (debug.on()) { + debug.log("Server open(streamId=" + s.streamId() + ", pushId=" + pushId + ")"); + } + String tag = "streamId=" + s.streamId() + ": "; + var push = serverConn.getPushPromise(pushId); + if (push instanceof Http3ServerConnection.CancelledPush) { + this.cancel(s); + return push; + } + // no write loop: just buffer everything + final ReentrantLock pushLock = new ReentrantLock(); + final Condition writePushEnabled = pushLock.newCondition(); + final Runnable writeLoop = () -> { + pushLock.lock(); + try { + writePushEnabled.signalAll(); + } finally { + pushLock.unlock(); + } + }; + var pushw = s.connectWriter(SequentialScheduler.lockingScheduler(writeLoop)); + int tlen = VariableLengthEncoder.getEncodedSize(Http3Streams.PUSH_STREAM_CODE); + int plen = VariableLengthEncoder.getEncodedSize(pushId); + ByteBuffer buf = ByteBuffer.allocate(tlen + plen); + VariableLengthEncoder.encode(buf, Http3Streams.PUSH_STREAM_CODE); + VariableLengthEncoder.encode(buf, pushId); + buf.flip(); + try { + pushw.queueForWriting(buf); + if (debug.on()) { + debug.log(tag + "Server queued push stream type pushId=" + pushId + + " 0x" + Utils.asHexString(buf)); + } + ResponseBodyOutputStream os = new ResponseBodyOutputStream(connectionTag(), + debug, pushw, pushLock, writePushEnabled, () -> 0); + sendResponseHeaders(s.streamId(), pushw, false, 200, 0, + new HttpHeadersBuilder(rspHeaders), os); + if (debug.on()) { + debug.log(tag + "Server push response headers sent pushId=" + pushId); + } + switch (s.sendingState()) { + case SEND, READY -> { + if (!s.stopSendingReceived()) { + body.transferTo(os); + serverConn.addPushPromise(pushId, new Http3ServerConnection.CompletedPush(pushId, reqHeaders)); + os.close(); + if (debug.on()) { + debug.log(tag + "Server push response body sent pushId=" + pushId); + } + } else { + if (debug.on()) { + debug.log(tag + "Server push response body cancelled pushId=" + pushId); + } + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + cancel(s); + } + } + case RESET_SENT, RESET_RECVD -> { + if (debug.on()) { + debug.log(tag + "Server push response body cancelled pushId=" + pushId); + } + // benign race if already cancelled, stateless marker + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + } + default -> { + if (debug.on()) { + debug.log(tag + "Server push response body cancelled pushId=" + pushId); + } + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + cancel(s); + } + } + body.close(); + } catch (IOException io) { + if (debug.on()) { + debug.log(tag + "Server failed to send pushId=" + pushId + ": " + io); + } + throw new UncheckedIOException(io); + } + return serverConn.getPushPromise(pushId); + }, serverConn.server().getQuicServer().executor()).exceptionally(t -> { + if (debug.on()) { + debug.log("Server failed to send PushPromise(pushId=" + pushId + "): " + t); + } + serverConn.addPushPromise(pushId, new Http3ServerConnection.CancelledPush(pushId)); + try { + body.close(); + } catch (IOException io) { + if (debug.on()) { + debug.log("Failed to close PushPromise stream(pushId=" + + pushId + "): " + io); + } + } + return serverConn.getPushPromise(pushId); + }); + return promise; + } + + @Override + public CompletableFuture sendPing() { + final QuicConnectionImpl quicConn = serverConn.quicConnection(); + var executor = quicConn.quicInstance().executor(); + return quicConn.requestSendPing() + // ensure that dependent actions will not be executed in the + // thread that completes the CF + .thenApplyAsync(Function.identity(), executor) + .exceptionallyAsync(this::rethrow, executor); + } + + private T rethrow(Throwable t) { + if (t instanceof RuntimeException r) throw r; + if (t instanceof Error e) throw e; + if (t instanceof ExecutionException x) return rethrow(x.getCause()); + throw new CompletionException(t); + } + + private boolean isHeadRequest() { + return "HEAD".equals(method); + } + + static class ResponseBodyOutputStream extends OutputStream { + + volatile boolean closed; + volatile boolean goodToGo; + boolean headersWritten; + long sent; + private final QuicStreamWriter osw; + private final ReentrantLock writeLock; + private final Condition writeEnabled; + private final LongSupplier responseLength; + private final Logger debug; + private final String connectionTag; + + ResponseBodyOutputStream(String connectionTag, + Logger debug, + QuicStreamWriter writer, + ReentrantLock writeLock, + Condition writeEnabled, + LongSupplier responseLength) { + this.debug = debug; + this.writeLock = writeLock; + this.writeEnabled = writeEnabled; + this.responseLength = responseLength; + this.osw = writer; + this.connectionTag = connectionTag; + } + + private void writeHeadersIfNeeded(ByteBuffer buffer) + throws IOException { + assert writeLock.isHeldByCurrentThread(); + long responseLength = this.responseLength.getAsLong(); + boolean streaming = responseLength == 0; + if (streaming) { + if (buffer.hasRemaining()) { + int len = buffer.remaining(); + if (debug.on()) { + debug.log("Streaming BodyResponse: streamId=%s writing DataFrame(%s)", + osw.stream().streamId(), len); + } + var data = new DataFrame(len); + var headers = ByteBuffer.allocate(data.headersSize()); + data.writeHeaders(headers); + headers.flip(); + osw.queueForWriting(headers); + } + } else if (!headersWritten) { + long len = responseLength > 0 ? responseLength : 0; + if (debug.on()) { + debug.log("BodyResponse: streamId=%s writing DataFrame(%s)", + osw.stream().streamId(), len); + } + var data = new DataFrame(len); + var headers = ByteBuffer.allocate(data.headersSize()); + data.writeHeaders(headers); + headers.flip(); + osw.queueForWriting(headers); + headersWritten = true; + } + } + + @Override + public void write(int b) throws IOException { + var buffer = ByteBuffer.allocate(1); + buffer.put((byte) b); + buffer.flip(); + submit(buffer); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + // the data is not written immediately, and therefore + // it needs to be copied. + // maybe we should find a way to wait until the data + // has been written, but that sounds complex. + ByteBuffer buffer = ByteBuffer.wrap(b.clone(), off, len); + submit(buffer); + } + + String logTag() { + return connectionTag + " streamId=" + osw.stream().streamId(); + } + + /** + * Schedule the ByteBuffer for writing. The buffer must never + * be reused. + * + * @param buffer response data + * @throws IOException if the channel is closed + */ + public void submit(ByteBuffer buffer) throws IOException { + writeLock.lock(); + try { + if (closed && buffer.hasRemaining()) { + throw new ClosedChannelException(); + } + if (osw.credit() <= 0) { + if (Log.requests()) { + Log.logResponse(() -> logTag() + ": HTTP/3 Server waiting for credits"); + } + writeEnabled.awaitUninterruptibly(); + if (Log.requests()) { + Log.logResponse(() -> logTag() + ": HTTP/3 Server unblocked - credits: " + + osw.credit() + ", closed: " + closed); + } + } + if (closed) { + if (buffer.hasRemaining()) { + throw new ClosedChannelException(); + } else return; + } + int len = buffer.remaining(); + sent = sent + len; + writeHeadersIfNeeded(buffer); + long responseLength = this.responseLength.getAsLong(); + boolean streaming = responseLength == 0; + boolean last = !streaming && (sent == responseLength + || (sent == 0 && responseLength == -1)); + osw.scheduleForWriting(buffer, last); + if (last) closeInternal(); + if (!streaming && sent != 0 && sent > responseLength) { + throw new IOException("sent more bytes than expected"); + } + } finally { + writeLock.unlock(); + } + } + + public void closeInternal() { + if (debug.on()) { + debug.log("BodyResponse: streamId=%s closeInternal", osw.stream().streamId()); + } + if (closed) return; + writeLock.lock(); + try { + closed = true; + } finally { + writeLock.unlock(); + } + } + + public void close() throws IOException { + if (debug.on()) { + debug.log("BodyResponse: streamId=%s close", osw.stream().streamId()); + } + if (closed) return; + writeLock.lock(); + try { + if (closed) return; + closed = true; + switch (osw.sendingState()) { + case READY, SEND -> { + if (debug.on()) { + debug.log("BodyResponse: streamId=%s sending EOF", + osw.stream().streamId()); + } + osw.scheduleForWriting(QuicStreamReader.EOF, true); + writeEnabled.signalAll(); + } + default -> { + } + } + } catch (IOException io) { + throw new IOException(io); + } finally { + writeLock.unlock(); + } + } + + public void goodToGo() { + this.goodToGo = true; + } + + public void resetStream(long code) throws IOException { + if (closed) return; + writeLock.lock(); + try { + if (closed) return; + closed = true; + switch (osw.sendingState()) { + case READY, SEND, DATA_SENT: + osw.reset(code); + default: + break; + } + } catch (IOException io) { + throw new IOException(io); + } finally { + writeLock.unlock(); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerStreamImpl.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerStreamImpl.java new file mode 100644 index 00000000000..c5a8709346c --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3ServerStreamImpl.java @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.http3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.UncheckedIOException; +import java.net.http.HttpHeaders; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.common.ValidatingHeadersConsumer; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.CancelPushFrame; +import jdk.internal.net.http.http3.frames.DataFrame; +import jdk.internal.net.http.http3.frames.FramesDecoder; +import jdk.internal.net.http.http3.frames.HeadersFrame; +import jdk.internal.net.http.http3.frames.Http3Frame; +import jdk.internal.net.http.http3.frames.MalformedFrame; +import jdk.internal.net.http.http3.frames.PartialFrame; +import jdk.internal.net.http.http3.frames.UnknownFrame; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +final class Http3ServerStreamImpl { + private final Http3ServerConnection serverConn; + private final Logger debug; + final QuicBidiStream stream; + final SequentialScheduler readScheduler = SequentialScheduler.lockingScheduler(this::readLoop); + final SequentialScheduler writeScheduler = SequentialScheduler.lockingScheduler(this::writeLoop); + final QuicStreamReader reader; + final QuicStreamWriter writer; + final BuffersReader.ListBuffersReader incoming = BuffersReader.list(); + final DecodingCallback headersConsumer; + final HeaderFrameReader headersReader; + final HttpHeadersBuilder requestHeadersBuilder; + final ReentrantLock writeLock = new ReentrantLock(); + final Condition writeEnabled = writeLock.newCondition(); + final CompletableFuture requestHeadersCF = new MinimalFuture<>(); + final CompletableFuture exchangeCF; + final BlockingQueue requestBodyQueue = new LinkedBlockingQueue<>(); + private volatile boolean eof; + + volatile PartialFrame partialFrame; + + Http3ServerStreamImpl(Http3ServerConnection http3ServerConnection, QuicBidiStream stream) { + this.serverConn = http3ServerConnection; + this.debug = Utils.getDebugLogger(this.serverConn::dbgTag); + this.stream = stream; + requestHeadersBuilder = new HttpHeadersBuilder(); + headersConsumer = new HeadersConsumer(); + headersReader = http3ServerConnection.newHeaderFrameReader(headersConsumer); + writer = stream.connectWriter(writeScheduler); + reader = stream.connectReader(readScheduler); + exchangeCF = requestHeadersCF.thenApply(this::startExchange); + // TODO: add a start() method that calls reader.start(), and + // call it outside of the constructor + reader.start(); + } + + Http3ServerConnection serverConnection() { + return this.serverConn; + } + + private void readLoop() { + try { + readLoop0(); + } catch (QPackException qe) { + boolean isConnectionError = qe.isConnectionError(); + Http3Error error = qe.http3Error(); + Throwable cause = qe.getCause(); + if (isConnectionError) { + headersConsumer.onConnectionError(cause, error); + } else { + headersConsumer.onStreamError(cause, error); + } + } + } + + private void readLoop0() { + ByteBuffer buffer; + + // reader can be null if the readLoop is invoked + // before reader is assigned. + if (reader == null) return; + + if (debug.on()) { + debug.log("H3Server: entering readLoop(stream=%s)", stream.streamId()); + } + try { + while (!reader.isReset() && (buffer = reader.poll()) != null) { + if (buffer == QuicStreamReader.EOF) { + if (debug.on()) { + debug.log("H3Server: EOF on stream=" + stream.streamId()); + } + if (!eof) requestBodyQueue.add(buffer); + eof = true; + return; + } + if (debug.on()) { + debug.log("H3Server: got %s bytes on stream %s", buffer.remaining(), stream.streamId()); + } + + var partialFrame = this.partialFrame; + if (partialFrame != null && partialFrame.remaining() == 0) { + this.partialFrame = partialFrame = null; + } + if (partialFrame instanceof HeadersFrame partialHeaders) { + serverConn.decodeHeaders(partialHeaders, buffer, headersReader); + } else if (partialFrame instanceof DataFrame partialData) { + receiveData(partialData, buffer); + } else if (partialFrame != null) { + partialFrame.nextPayloadBytes(buffer); + } + if (!buffer.hasRemaining()) { + continue; + } + + incoming.add(buffer); + Http3Frame frame = Http3Frame.decode(incoming, FramesDecoder::isAllowedOnRequestStream, debug); + if (frame == null) continue; + if (frame instanceof PartialFrame partial) { + this.partialFrame = partialFrame = partial; + if (frame instanceof HeadersFrame partialHeaders) { + if (debug.on()) { + debug.log("H3Server Got headers: " + frame + " on stream=" + stream.streamId()); + } + long remaining = partial.remaining(); + long available = incoming.remaining(); + long read = Math.min(remaining, available); + if (read > 0) { + for (ByteBuffer buf : incoming.getAndRelease(read)) { + serverConn.decodeHeaders(partialHeaders, buf, headersReader); + } + } + } else if (frame instanceof DataFrame partialData) { + if (debug.on()) { + debug.log("H3Server Got request body: " + frame + " on stream=" + stream.streamId()); + } + long remaining = partial.remaining(); + long available = incoming.remaining(); + long read = Math.min(remaining, available); + if (read > 0) { + for (ByteBuffer buf : incoming.getAndRelease(read)) { + receiveData(partialData, buf); + } + } + + } else if (frame instanceof UnknownFrame unknown) { + unknown.nextPayloadBytes(incoming); + } else { + if (debug.on()) { + debug.log("H3Server Got unexpected partial frame: " + + frame + " on stream=" + stream.streamId()); + } + serverConn.close(Http3Error.H3_FRAME_UNEXPECTED, + "unexpected frame type=" + frame.type() + + " on stream=" + stream.streamId()); + readScheduler.stop(); + writeScheduler.stop(); + return; + } + } else if (frame instanceof MalformedFrame malformed) { + if (debug.on()) { + debug.log("H3Server Got frame: " + frame + " on stream=" + stream.streamId()); + } + serverConn.close(malformed.getErrorCode(), + malformed.getMessage()); + readScheduler.stop(); + writeScheduler.stop(); + return; + } else { + if (debug.on()) { + debug.log("H3Server Got frame: " + frame + " on stream=" + stream.streamId()); + } + } + } + if (reader.isReset()) { + if (debug.on()) + debug.log("H3 Server: stream %s reset", reader.stream().streamId()); + readScheduler.stop(); + resetReceived(); + } + if (debug.on()) + debug.log("H3 Server: exiting read loop"); + } catch (Throwable t) { + if (debug.on()) + debug.log("H3 Server: read loop failed: " + t); + if (reader.isReset()) { + if (debug.on()) { + debug.log("H3 Server: stream %s reset", reader.stream()); + } + readScheduler.stop(); + resetReceived(); + } else { + if (debug.on()) { + debug.log("H3 Server: closing connection due to: " + t, t); + } + serverConn.close(Http3Error.H3_INTERNAL_ERROR, serverConn.message(t)); + readScheduler.stop(); + writeScheduler.stop(); + } + } + } + + String readErrorString(String defVal) { + return Http3Streams.errorCodeAsString(reader.stream()).orElse(defVal); + } + + void resetReceived() { + // If stop_sending sent and reset received (implied by this method being called) + // then exit normally and don't send a reset + if (debug.on()) { + debug.log("resetReceived: stream:%s, isStopSendingRequested:%s, errorCode:%s, isNoError:%s", + stream.streamId(), stream.isStopSendingRequested(), stream.rcvErrorCode(), + Http3Error.isNoError(reader.stream().rcvErrorCode())); + } + + if (reader.stream().isStopSendingRequested() + && requestHeadersCF.isDone()) { + // we can only request stop sending in the handler after having + // parsed the headers, therefore, if requestHeadersCF is not + // completed when we reach here we should reset the stream. + + // We have requested stopSending and received a reset in response: + // nothing to do - let the response be sent to the client, but throw an + // exception if `is` is used again. + exchangeCF.thenApply(en -> { + en.is.close(new IOException("stopSendingRequested")); + return en; + }); + return; + } + + String msg = "Stream %s reset by peer: %s" + .formatted(streamId(), readErrorString("no error code")); + if (debug.on()) + debug.log("H3 Server: reset received: " + msg); + var io = new IOException(msg); + requestHeadersCF.completeExceptionally(io); + exchangeCF.thenApply((e) -> e.streamResetByPeer(io)); + } + + void receiveData(DataFrame partialDataFrame, ByteBuffer buffer) { + if (debug.on()) { + debug.log("receiving data: " + buffer.remaining() + " on stream=" + stream.streamId()); + } + ByteBuffer received = partialDataFrame.nextPayloadBytes(buffer); + requestBodyQueue.add(received); + } + + void cancelPushFrameReceived(CancelPushFrame cancel) { + serverConn.cancelPush(cancel.getPushId()); + } + + class RequestBodyInputStream extends InputStream { + volatile IOException error; + volatile boolean closed; + // uses an unbounded blocking queue in which the readrLoop + // publishes the DataFrames payload... + ByteBuffer current; + // Use lock to avoid pinned threads on the blocking queue + final ReentrantLock lock = new ReentrantLock(); + + ByteBuffer current() throws IOException { + lock.lock(); + try { + while (true) { + if (current != null && current.hasRemaining()) { + return current; + } + if (current == QuicStreamReader.EOF) return current; + try { + if (debug.on()) + debug.log("Taking buffer from queue"); + // Blocking call + current = requestBodyQueue.take(); + } catch (InterruptedException e) { + var io = new InterruptedIOException(); + Thread.currentThread().interrupt(); + io.initCause(e); + close(io); + var error = this.error; + if (error != null) throw error; + } + } + } finally { + lock.unlock(); + } + } + + @Override + public int read() throws IOException { + ByteBuffer buffer = current(); + if (buffer == QuicStreamReader.EOF) { + var error = this.error; + if (error == null) return -1; + throw error; + } + return buffer.get() & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + int remaining = len; + while (remaining > 0) { + ByteBuffer buffer = current(); + if (buffer == QuicStreamReader.EOF) { + if (len == remaining) { + var error = this.error; + if (error == null) return -1; + throw error; + } else return len - remaining; + } + int count = Math.min(buffer.remaining(), remaining); + buffer.get(b, off + (len - remaining), count); + remaining -= count; + } + return len - remaining; + } + + @Override + public void close() throws IOException { + lock.lock(); + try { + if (closed) return; + closed = true; + + } finally { + lock.unlock(); + } + if (debug.on()) + debug.log("Closing request body input stream"); + requestBodyQueue.add(QuicStreamReader.EOF); + } + + void close(IOException io) { + lock.lock(); + try { + if (closed) return; + closed = true; + error = io; + } finally { + lock.unlock(); + } + if (debug.on()) { + debug.log("Closing request body input stream: " + io); + } + requestBodyQueue.clear(); + requestBodyQueue.add(QuicStreamReader.EOF); + } + } + + Http3ServerExchange startExchange(HttpHeaders headers) { + Http3ServerExchange exchange = new Http3ServerExchange(this, headers, + new RequestBodyInputStream(), + serverConn.quicConnection().getTLSEngine().getSession()); + try { + serverConn.server().submitExchange(exchange); + } catch (Exception e) { + try { + exchange.close(new IOException(e)); + } catch (IOException ex) { + if (debug.on()) + debug.log("Failed to close exchange: " + ex); + } + } + return exchange; + } + + long streamId() { + return stream.streamId(); + } + + private void writeLoop() { + writeLock.lock(); + try { + writeEnabled.signalAll(); + } finally { + writeLock.unlock(); + } + } + + void close() { + serverConn.exchangeClosed(this); + } + + private final class HeadersConsumer extends ValidatingHeadersConsumer + implements DecodingCallback { + + private HeadersConsumer() { + super(Context.REQUEST); + } + + @Override + public void reset() { + super.reset(); + requestHeadersBuilder.clear(); + if (debug.on()) { + debug.log("Response builder cleared, ready to receive new headers."); + } + } + + @Override + public void onDecoded(CharSequence name, CharSequence value) + throws UncheckedIOException { + String n = name.toString(); + String v = value.toString(); + super.onDecoded(n, v); + requestHeadersBuilder.addHeader(n, v); + if (Log.headers() && Log.trace()) { + Log.logTrace("RECEIVED HEADER (streamid={0}): {1}: {2}", + streamId(), n, v); + } + } + + @Override + public void onComplete() { + HttpHeaders requestHeaders = requestHeadersBuilder.build(); + headersReader.reset(); + requestHeadersCF.complete(requestHeaders); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + try { + stream.reset(http3Error.code()); + serverConn.connectionError(throwable, http3Error); + } catch (IOException ioe) { + serverConn.close(http3Error.code(), + ioe.getMessage()); + } + } + + @Override + public void onStreamError(Throwable throwable, Http3Error http3Error) { + try { + stream.reset(http3Error.code()); + } catch (IOException ioe) { + serverConn.close(http3Error.code(), + ioe.getMessage()); + } + } + + @Override + public long streamId() { + return stream.streamId(); + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3TestServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3TestServer.java new file mode 100644 index 00000000000..ff76c67ed7e --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/Http3TestServer.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.http3; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.System.Logger.Level; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.net.ssl.SSLContext; + +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil; +import jdk.httpclient.test.lib.common.RequestPathMatcherUtil.Resolved; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import static java.nio.charset.StandardCharsets.US_ASCII; + +public class Http3TestServer implements QuicServer.ConnectionAcceptor, AutoCloseable { + private static final AtomicLong IDS = new AtomicLong(); + + static final long DECODER_MAX_CAPACITY = + Utils.getLongProperty("http3.test.server.decoderMaxTableCapacity", 4096); + static final long ENCODER_CAPACITY_LIMIT = + Utils.getLongProperty("http3.test.server.encoderTableCapacityLimit", 4096); + + private static final String ALLOWED_HEADERS_PROP_NAME = "http3.test.server.encoderAllowedHeaders"; + static final String ALL_ALLOWED = "*"; + static final List ENCODER_ALLOWED_HEADERS = readEncoderAllowedHeadersProp(); + + private static List readEncoderAllowedHeadersProp() { + String properties = Utils.getProperty(ALLOWED_HEADERS_PROP_NAME); + if (properties == null) { + // If the system property is not set all headers are allowed to be encoded + return List.of(ALL_ALLOWED); + } else if(properties.isBlank()) { + // If system property value is a blank string - no headers are + // allowed to be encoded + return List.of(); + } + var headers = Arrays.stream(properties.split(",")) + .filter(Predicate.not(String::isBlank)) + .toList(); + if (headers.contains(ALL_ALLOWED)) { + return List.of(ALL_ALLOWED); + } + return headers; + } + + private final QuicServer quicServer; + private volatile boolean stopping; + private final Map handlers = new ConcurrentHashMap<>(); + private final Function handlerProvider; + private final Logger debug; + private final InetSocketAddress serverAddr; + private volatile ConnectionSettings ourSettings; + // request approver which takes the server connection key as the input + private volatile Predicate newRequestApprover; + + private static String nextName() { + return "h3-server-" + IDS.incrementAndGet(); + } + + /** + * Same as calling {@code Http3TestServer(sslContext, null)} + * + * @param sslContext SSLContext + * @throws IOException if the server could not be created + */ + public Http3TestServer(final SSLContext sslContext) throws IOException { + this(sslContext, null); + } + + /** + * Same as calling {@code Http3TestServer(sslContext, + * new InetSocketAddress(InetAddress.getLoopbackAddress(), port), null)} + * + * @param sslContext SSLContext + * @throws IOException if the server could not be created + */ + public Http3TestServer(final SSLContext sslContext, int port) throws IOException { + this(sslContext, new InetSocketAddress(InetAddress.getLoopbackAddress(), port), null); + } + + public Http3TestServer(final SSLContext sslContext, InetSocketAddress address, final ExecutorService executor) throws IOException { + this(quicServerBuilder().sslContext(sslContext).executor(executor).bindAddress(address).build(), null); + } + + public Http3TestServer(final SSLContext sslContext, final ExecutorService executor) throws IOException { + this(quicServerBuilder().sslContext(sslContext).executor(executor).build(), null); + } + + public Http3TestServer(final QuicServer quicServer) throws IOException { + this(quicServer, null); + } + + public Http3TestServer(final QuicServer quicServer, + final Function handlerProvider) + throws IOException { + Objects.requireNonNull(quicServer); + this.debug = Utils.getDebugLogger(quicServer::name); + this.quicServer = quicServer; + this.handlerProvider = handlerProvider; + this.quicServer.setConnectionAcceptor(this); + this.serverAddr = bindServer(this.quicServer); + debug.log(Level.INFO, "H3 server is listening at " + this.serverAddr); + } + + public void start() { + quicServer.start(); + } + + public void stop() { + try { + close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public QuicServer getQuicServer() { + return this.quicServer; + } + + public InetSocketAddress getAddress() { + return this.serverAddr; + } + + public String serverAuthority() { + final InetSocketAddress inetSockAddr = getAddress(); + final String hostIP = inetSockAddr.getAddress().getHostAddress(); + // escape for ipv6 + final String h = hostIP.contains(":") ? "[" + hostIP + "]" : hostIP; + return h + ":" + inetSockAddr.getPort(); + } + + public void addHandler(final String path, final Http2Handler handler) { + if (this.handlerProvider != null) { + throw new IllegalStateException("Cannot add handler to H3 server which uses a handler provider"); + } + Objects.requireNonNull(path); + Objects.requireNonNull(handler); + this.handlers.put(path, handler); + } + + public void setRequestApprover(final Predicate approver) { + this.newRequestApprover = approver; + } + + /** + * Sets the connection settings that will be used by this server to generate a SETTINGS frame + * to send to client peers + * + * @param connectionSettings The connection settings + * @return The instance of this server + */ + public Http3TestServer setConnectionSettings(final ConnectionSettings connectionSettings) { + Objects.requireNonNull(connectionSettings); + this.ourSettings = connectionSettings; + return this; + } + + /** + * {@return the connection settings of this server, which will be sent to + * client peers in a SETTINGS frame. If none have been configured then this method returns + * {@link Optional#empty() empty}} + */ + public ConnectionSettings getConfiguredConnectionSettings() { + if (this.ourSettings == null) { + SettingsFrame settings = SettingsFrame.defaultRFCSettings(); + settings.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DECODER_MAX_CAPACITY); + return ConnectionSettings.createFrom(settings); + } + return this.ourSettings; + } + + private static InetSocketAddress bindServer(final QuicServer server) throws IOException { + // bind the quic server to the socket + final SocketAddress addr = server.getEndpoint().getLocalAddress(); + if (addr instanceof InetSocketAddress inetsaddr) { + return inetsaddr; + } + throw new IOException(new IOException("Unexpected socket address type " + + addr.getClass().getName())); + } + + void submitExchange(final Http2TestExchange exchange) { + debug.log("H3 server handling exchange for: %s%n\t\t" + + "(Memory: max=%s, free=%s, total=%s)%n", + exchange.getRequestURI(), Runtime.getRuntime().maxMemory(), + Runtime.getRuntime().freeMemory(), Runtime.getRuntime().totalMemory()); + final String reqPath = exchange.getRequestURI().getPath(); + final Http2Handler handler; + if (this.handlerProvider != null) { + handler = this.handlerProvider.apply(reqPath); + } else { + Optional> match = + RequestPathMatcherUtil.findHandler(reqPath, this.handlers); + handler = match.isPresent() ? match.get().handler() : null; + } + // The server Http3ServerExchange uses a BlockingQueue to + // read data so handling the exchange in the current thread would + // wedge it. The executor must have at least one thread and must not + // execute inline - otherwise, we'd be wedged. + Thread currentThread = Thread.currentThread(); + this.quicServer.executorService().execute(() -> { + try { + // if no handler was located, we respond with a 404 + if (handler == null) { + respondForMissingHandler(exchange); + return; + } + // This assertion is too strong: there are cases + // where the calling task might terminate before + // the submitted task is executed. In which case + // it can safely run on the same thread. + // assert Thread.currentThread() != currentThread + // : "HTTP/3 server executor must have at least one thread"; + handler.handle(exchange); + } catch (Throwable failure) { + System.err.println("Failed to handle exchange: " + failure); + failure.printStackTrace(); + final var ioException = (failure instanceof IOException) + ? (IOException) failure + : new IOException(failure); + try { + exchange.close(ioException); + } catch (IOException x) { + System.err.println("Failed to close exchange: " + x); + } + } + }); + } + + private void respondForMissingHandler(final Http2TestExchange exchange) + throws IOException { + final byte[] responseBody = (this.getClass().getSimpleName() + + " - No handler available to handle request " + + exchange.getRequestURI()).getBytes(US_ASCII); + try (final OutputStream os = exchange.getResponseBody()) { + exchange.sendResponseHeaders(404, responseBody.length); + os.write(responseBody); + } + } + + /** + * Called by the {@link QuicServer} when a new connection has been added to the endpoint's + * connection map. + * + * @param source The client address + * @param quicConn the new connection + * @return true if the new connection should be accepted, false + * if it should be closed + */ + @Override + public boolean acceptIncoming(final SocketAddress source, final QuicServerConnection quicConn) { + if (stopping) { + return false; + } + debug.log("New connection %s accepted from %s", quicConn.dbgTag(), source); + quicConn.onSuccessfulHandshake(() -> { + var http3Connection = new Http3ServerConnection(this, quicConn, source); + http3Connection.start(); + }); + return true; + } + + boolean shouldProcessNewHTTPRequest(final Http3ServerConnection serverConn) { + final Predicate approver = this.newRequestApprover; + if (approver == null) { + // by the default the server will process new requests + return true; + } + final String connKey = serverConn.connectionKey(); + return approver.test(connKey); + } + + @Override + public void close() throws IOException { + stopping = true; + if (debug.on()) { + debug.log("Stopping H3 server " + this.serverAddr); + } + // TODO: should we close the quic server only if we "own" it + if (this.quicServer != null) { + this.quicServer.close(); + } + } + + public static QuicServer.Builder quicServerBuilder() { + return new H3QuicBuilder(); + } + + private static final class H3QuicBuilder extends QuicServer.Builder { + + public H3QuicBuilder() { + alpn = "h3"; + } + + @Override + public QuicServer build() throws IOException { + QuicVersion[] versions = availableQuicVersions; + if (versions == null) { + // default to v1 and v2 + versions = new QuicVersion[]{QuicVersion.QUIC_V1, QuicVersion.QUIC_V2}; + } + if (versions.length == 0) { + throw new IllegalStateException("Empty available QUIC versions"); + } + InetSocketAddress addr = bindAddress; + if (addr == null) { + // default to loopback address and ephemeral port + addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + } + SSLContext ctx = sslContext; + if (ctx == null) { + try { + ctx = SSLContext.getDefault(); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } + } + final QuicTLSContext quicTLSContext = new QuicTLSContext(ctx); + final String name = serverId == null ? nextName() : serverId; + return new QuicServer(name, addr, executor, versions, compatible, quicTLSContext, sniMatcher, + incomingDeliveryPolicy, outgoingDeliveryPolicy, alpn, Http3Error::stringForCode); + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/UnknownOrReservedFrame.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/UnknownOrReservedFrame.java new file mode 100644 index 00000000000..eddad466daa --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http3/UnknownOrReservedFrame.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.http3; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.Random; + +import jdk.internal.net.http.http3.frames.AbstractHttp3Frame; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +/** + * A test-only HTTP3 frame used to exercise the case where (client) implementation + * is expected to ignore unknown/reserved frames, as expected by RFC-9114, section + * 7.2.8 and section 9 + */ +final class UnknownOrReservedFrame extends AbstractHttp3Frame { + + private static final Random random = new Random(getSeed()); + + private final int payloadLength; + private final byte[] payload; + + public UnknownOrReservedFrame() { + super(generateRandomFrameType()); + this.payloadLength = random.nextInt(13); // arbitrary upper bound + this.payload = new byte[this.payloadLength]; + random.nextBytes(this.payload); + } + + @Override + public long length() { + return this.payloadLength; + } + + ByteBuffer toByteBuffer() { + final int frameSize = (int) this.size(); // cast is OK - value expected to be within range + final ByteBuffer buf = ByteBuffer.allocate(frameSize); + // write the type of the frame + VariableLengthEncoder.encode(buf, this.type()); + // write the length of the payload + VariableLengthEncoder.encode(buf, this.payloadLength); + // write the payload + buf.put(this.payload); + buf.flip(); + return buf; + } + + private static long generateRandomFrameType() { + final boolean useReservedFrameType = random.nextBoolean(); + if (useReservedFrameType) { + final int N = random.nextInt(100); // arbitrary upper bound + // RFC-9114, section 7.2.8: Frame types of the format 0x1f * N + 0x21 for non-negative + // integer values of N are reserved to exercise the requirement + // that unknown types be ignored + return 0x1F * N + 0x21; + } + // arbitrary lower bound of 0x45 + return random.nextLong(0x45, VariableLengthEncoder.MAX_ENCODED_INTEGER); + } + + private static long getSeed() { + Long seed = Long.getLong("seed"); + return seed != null ? seed : System.nanoTime() ^ new Random().nextLong(); + } + + static Optional tryGenerateFrame() { + // an arbitrary decision to create a new unknown/reserved frame + if (random.nextInt() % 8 == 0) { + return Optional.of(new UnknownOrReservedFrame()); + } + return Optional.empty(); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ClientConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ClientConnection.java new file mode 100644 index 00000000000..96bb04a4ac7 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ClientConnection.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.NO_ERROR; + +/** + * A client initiated QUIC connection to a server + */ +public final class ClientConnection implements AutoCloseable { + + private static final Logger debug = Utils.getDebugLogger(() -> ClientConnection.class.getName()); + + private static final ByteBuffer EOF = ByteBuffer.allocate(0); + private static final String ALPN = QuicStandaloneServer.ALPN; + + private final QuicConnection connection; + private final QuicEndpoint endpoint; + + /** + * Establishes a connection between a Quic client and a target Quic server. This includes completing + * the handshake between the client and the server. + * + * @param client The Quic client + * @param serverAddr the target server address + * @return a ClientConnection + * @throws IOException If there was any exception while establishing the connection + */ + public static ClientConnection establishConnection(final QuicClient client, + final InetSocketAddress serverAddr) + throws IOException { + Objects.requireNonNull(client); + Objects.requireNonNull(serverAddr); + final QuicConnection conn = client.createConnectionFor(serverAddr, new String[]{ALPN}); + assert conn instanceof QuicConnectionImpl : "unexpected QUIC connection type: " + + conn.getClass(); + final CompletableFuture handshakeCf = + conn.startHandshake(); + final QuicEndpoint endpoint; + try { + endpoint = handshakeCf.get(); + } catch (InterruptedException e) { + throw new IOException(e); + } catch (ExecutionException e) { + throw new IOException(e.getCause()); + } + assert endpoint != null : "null endpoint after handshake completion"; + if (debug.on()) { + debug.log("Quic connection established for client: " + client.name() + + ", local addr: " + conn.localAddress() + + ", peer addr: " + serverAddr + + ", endpoint: " + endpoint); + } + return new ClientConnection(conn, endpoint); + } + + private ClientConnection(final QuicConnection connection, final QuicEndpoint endpoint) { + this.connection = Objects.requireNonNull(connection); + this.endpoint = endpoint; + } + + /** + * Creates a new client initiated bidirectional stream to the server. The returned + * {@code ConnectedBidiStream} will have the reader and writer tasks started, thus + * allowing the caller of this method to then use the returned {@code ConnectedBidiStream} + * for sending or received data through the {@link ConnectedBidiStream#outputStream() output stream} + * or {@link ConnectedBidiStream#inputStream() input stream} respectively. + * + * @return + * @throws IOException + */ + public ConnectedBidiStream initiateNewBidiStream() throws IOException { + final QuicBidiStream quicBidiStream; + try { + // TODO: review the duration being passed and whether it needs to be something + // that should be taken as an input to the initiateNewBidiStream() method + quicBidiStream = this.connection.openNewLocalBidiStream(Duration.ZERO).get(); + } catch (InterruptedException e) { + throw new IOException(e); + } catch (ExecutionException e) { + throw new IOException(e.getCause()); + } + return new ConnectedBidiStream(quicBidiStream); + } + + public QuicConnection underlyingQuicConnection() { + return this.connection; + } + + public QuicEndpoint endpoint() { + return this.endpoint; + } + + @Override + public void close() throws Exception { + this.connection.connectionTerminator().terminate(forTransportError(NO_ERROR)); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ConnectedBidiStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ConnectedBidiStream.java new file mode 100644 index 00000000000..9aa2e9040e2 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/ConnectedBidiStream.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Semaphore; + +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +/** + * A {@code ConnectedBidiStream} represents a {@link + * jdk.internal.net.http.quic.streams.QuicBidiStream} + * which has a reader and writer task/loop started for it + */ +public class ConnectedBidiStream implements AutoCloseable { + + private final QuicBidiStream bidiStream; + private final QuicStreamReader quicStreamReader; + private final QuicStreamWriter quicStreamWriter; + private final BlockingQueue incomingData; + private final Semaphore writeSemaphore = new Semaphore(1); + private final OutputStream outputStream; + private final QueueInputStream inputStream; + private final SequentialScheduler readScheduler; + private volatile boolean closed; + + ConnectedBidiStream(final QuicBidiStream bidiStream) { + Objects.requireNonNull(bidiStream); + this.bidiStream = bidiStream; + incomingData = new ArrayBlockingQueue<>(1024, true); + this.quicStreamReader = bidiStream.connectReader( + readScheduler = SequentialScheduler.lockingScheduler(new ReaderLoop())); + this.inputStream = new QueueInputStream(this.incomingData, QuicStreamReader.EOF, quicStreamReader); + this.quicStreamWriter = bidiStream.connectWriter( + SequentialScheduler.lockingScheduler(() -> { + System.out.println("Server writer task called"); + writeSemaphore.release(); + })); + this.outputStream = new OutStream(this.quicStreamWriter, writeSemaphore); + // TODO: start the reader when inputStream() is called instead? + this.quicStreamReader.start(); + } + + public InputStream inputStream() { + return this.inputStream; + } + + public OutputStream outputStream() { + return this.outputStream; + } + + public QuicBidiStream underlyingBidiStream() { + return this.bidiStream; + } + + @Override + public void close() throws Exception { + this.closed = true; + // TODO: use runOrSchedule(executor)? + this.readScheduler.runOrSchedule(); + } + + + private final class ReaderLoop implements Runnable { + + private volatile boolean alreadyLogged; + + @Override + public void run() { + try { + if (quicStreamReader == null) return; + while (true) { + final var bb = quicStreamReader.poll(); + if (closed) { + return; + } + if (bb == null) { + return; + } + incomingData.add(bb); + if (bb == QuicStreamReader.EOF) { + break; + } + } + } catch (Throwable e) { + if (closed && e instanceof IOException) { + // the stream has been closed so we ignore any IOExceptions + return; + } + System.err.println("Error in " + getClass()); + e.printStackTrace(); + var in = inputStream; + if (in != null) { + in.error(e); + } + } + } + } + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/DatagramDeliveryPolicy.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/DatagramDeliveryPolicy.java new file mode 100644 index 00000000000..277e0b289a5 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/DatagramDeliveryPolicy.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.text.ParseException; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import jdk.internal.net.http.quic.packets.QuicPacket; + +/** + * Used by the {@link QuicServer Quic server} and the {@link QuicServerConnection Quic server + * connection} to decide whether an incoming datagram needs to be dropped + */ +public interface DatagramDeliveryPolicy { + + /** + * System property for configuring the incoming datagram delivery policy for Quic server + */ + public static final String SYS_PROP_INCOMING_DELIVERY_POLICY = + "jdk.internal.httpclient.test.quic.incoming"; + + /** + * System property for configuring the outgoing datagram delivery policy for Quic server + */ + public static final String SYS_PROP_OUTGOING_DELIVERY_POLICY = + "jdk.internal.httpclient.test.quic.outgoing"; + + /** + * Will be called to decide if an incoming or an outgoing datagram should be dropped + * + * @param address The source or the destination address for the datagram + * @param payload The datagram payload + * @param connection The connection which was chosen to handle the Quic packet + * @return true if the datagram should be dropped, false otherwise + */ + boolean shouldDrop(SocketAddress address, ByteBuffer payload, QuicServerConnection connection, + QuicPacket.HeadersType headersType); + + /** + * Will be called to decide if an incoming datagram, which wasn't matched against + * any specific Quic connection, should be dropped + * + * @param source The source address which transmitted the datagram + * @param payload The datagram payload + * @return true if the datagram should be dropped, false otherwise + */ + boolean shouldDrop(SocketAddress source, ByteBuffer payload, QuicPacket.HeadersType headersType); + + static final DatagramDeliveryPolicy ALWAYS_DELIVER = new DatagramDeliveryPolicy() { + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + return false; + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + return false; + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=always deliver]"; + } + }; + + static final DatagramDeliveryPolicy NEVER_DELIVER = new DatagramDeliveryPolicy() { + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + return true; + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + return true; + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=never deliver]"; + } + }; + + static final class FixedRate implements DatagramDeliveryPolicy { + private final int n; + private final AtomicLong counter = new AtomicLong(); + + FixedRate(final int n) { + if (n <= 0) { + throw new IllegalArgumentException("n should be greater than 0"); + } + this.n = n; + } + + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + final long current = counter.incrementAndGet(); + return current % n == 0; // drop every nth + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + final long current = counter.incrementAndGet(); + return current % n == 0; // drop every nth + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=drop every " + n + "]"; + } + } + + static final class RandomDrop implements DatagramDeliveryPolicy { + private final long seed; + private final Random random; + + RandomDrop() { + Long s = null; + try { + // note that Long.valueOf(null) also throws a + // NumberFormatException so if the property is undefined this + // will still work correctly + s = Long.valueOf(System.getProperty("seed")); + } catch (NumberFormatException e) { + // do nothing: seed is still null + } + this.seed = s != null ? s : new Random().nextLong(); + this.random = new Random(seed); + } + + @Override + public boolean shouldDrop(final SocketAddress address, + final ByteBuffer payload, + final QuicServerConnection connection, + final QuicPacket.HeadersType headersType) { + return this.random.nextLong() % 42 == 0; + } + + @Override + public boolean shouldDrop(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType) { + return this.random.nextLong() % 42 == 0; + } + + @Override + public String toString() { + return "[DatagramDeliveryPolicy=drop randomly, seed=" + seed + "]"; + } + } + + /** + * {@return a DatagramDeliveryPolicy which always returns false from the {@code shouldDrop} + * methods} + */ + public static DatagramDeliveryPolicy alwaysDeliver() { + return ALWAYS_DELIVER; + } + + /** + * {@return a DatagramDeliveryPolicy which always returns true from the {@code shouldDrop} + * methods} + */ + public static DatagramDeliveryPolicy neverDeliver() { + return NEVER_DELIVER; + } + + /** + * @param n the repeat count at which the datagram will be dropped + * @return a DatagramDeliveryPolicy which will return true on every {@code n}th call to + * either of the {@code shouldDrop} methods + */ + public static DatagramDeliveryPolicy dropEveryNth(final int n) { + return new FixedRate(n); + } + + /** + * @return a DatagramDeliveryPolicy which will randomly return true from the {@code shouldDrop} + * methods. If the {@code seed} system property is set then the {@code Random} instance used by + * this policy will use that seed. + */ + public static DatagramDeliveryPolicy dropRandomly() { + return new RandomDrop(); + } + + private static String privilegedGetProperty(String property) { + return privilegedGetProperty(property, null); + } + + private static String privilegedGetProperty(String property, String defval) { + return System.getProperty(property, defval); + } + + /** + * Reads the system property {@code sysPropName} and parses the value into a + * {@link DatagramDeliveryPolicy}. If the {@code sysPropName} system property isn't set or + * is set to a value of {@link String#isBlank() blank}, then this method returns a + * {@link DatagramDeliveryPolicy#alwaysDeliver() always deliver policy}. + *

        + * The {@code sysPropName} if set is expected to have either of the following values: + *

          + *
        • {@code always} - this returns a {@link DatagramDeliveryPolicy#alwaysDeliver() + * always deliver policy}
        • + *
        • {@code never} - this returns a {@link DatagramDeliveryPolicy#neverDeliver() + * never deliver policy}
        • + *
        • {@code fixed=} - where n is a positive integer, this returns a + * {@link DatagramDeliveryPolicy#dropEveryNth(int) dropEveryNth policy}
        • + *
        • {@code random} - this returns a + * {@link DatagramDeliveryPolicy#dropRandomly() dropRandomly policy}
        • + *
        + *

        + * + * @param sysPropName The system property name to use + * @return a DatagramDeliveryPolicy + * @throws ParseException If the system property value cannot be parsed into a + * DatagramDeliveryPolicy + */ + private static DatagramDeliveryPolicy fromSystemProperty(final String sysPropName) + throws ParseException { + String val = privilegedGetProperty(sysPropName); + if (val == null || val.isBlank()) { + return ALWAYS_DELIVER; + } + val = val.trim(); + if (val.startsWith("fixed=")) { + // read the characters following "fixed=" + String rateVal = val.substring("fixed=".length()); + final int n; + try { + n = Integer.parseInt(rateVal); + } catch (NumberFormatException nfe) { + throw new ParseException("Unexpected value: " + val, "fixed=".length()); + } + return dropEveryNth(n); + } else if (val.equals("random")) { + return dropRandomly(); + } else if (val.equals("always")) { + return ALWAYS_DELIVER; + } else if (val.equals("never")) { + return NEVER_DELIVER; + } else { + throw new ParseException("Unexpected value: " + val, 0); + } + } + + /** + * Returns the default incoming datagram delivery policy. This takes into account the + * {@link DatagramDeliveryPolicy#SYS_PROP_INCOMING_DELIVERY_POLICY} system property to decide + * the default policy + * + * @return the default incoming datagram delivery policy + * @throws ParseException If the {@link DatagramDeliveryPolicy#SYS_PROP_INCOMING_DELIVERY_POLICY} + * was configured and there was a problem parsing its value + */ + public static DatagramDeliveryPolicy defaultIncomingPolicy() throws ParseException { + try { + return fromSystemProperty(SYS_PROP_INCOMING_DELIVERY_POLICY); + } catch (Throwable t) { + t.printStackTrace(); + throw t; + } + } + + /** + * Returns the default outgoing datagram delivery policy. This takes into account the + * {@link DatagramDeliveryPolicy#SYS_PROP_OUTGOING_DELIVERY_POLICY} system property to decide + * the default policy + * + * @return the default outgoing datagram delivery policy + * @throws ParseException If the {@link DatagramDeliveryPolicy#SYS_PROP_OUTGOING_DELIVERY_POLICY} + * was configured and there was a problem parsing its value + */ + + public static DatagramDeliveryPolicy defaultOutgoingPolicy() throws ParseException { + try { + return fromSystemProperty(SYS_PROP_OUTGOING_DELIVERY_POLICY); + } catch (Throwable t) { + t.printStackTrace(); + throw t; + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/OutStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/OutStream.java new file mode 100644 index 00000000000..659c68682ef --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/OutStream.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.Semaphore; + +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; + +/** + * An {@link OutputStream} which writes using a {@link QuicStreamWriter} + */ +final class OutStream extends OutputStream { + + private final QuicStreamWriter quicStreamWriter; + private final Semaphore writeSemaphore; + + OutStream(final QuicStreamWriter quicStreamWriter, Semaphore writeSemaphore) { + Objects.requireNonNull(quicStreamWriter); + this.quicStreamWriter = quicStreamWriter; + this.writeSemaphore = Objects.requireNonNull(writeSemaphore); + } + + @Override + public void write(final int b) throws IOException { + this.write(new byte[]{(byte) (b & 0xff)}); + } + + @Override + public void write(final byte[] b, final int off, final int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + while (quicStreamWriter.credit() < 0 + && !quicStreamWriter.stopSendingReceived()) { + try { + writeSemaphore.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + // the data will be queued and won't be written immediately, and therefore + // it needs to be copied. + final ByteBuffer data = ByteBuffer.wrap(b.clone(), off, len); + quicStreamWriter.scheduleForWriting(data, false); + } + + @Override + public void close() throws IOException { + quicStreamWriter.scheduleForWriting(QuicStreamReader.EOF, true); + super.close(); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QueueInputStream.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QueueInputStream.java new file mode 100644 index 00000000000..2756fa732fa --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QueueInputStream.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; + +import jdk.internal.net.http.quic.streams.QuicStreamReader; +import jdk.internal.net.quic.QuicTransportErrors; + +/** + * An {@code InputStream} which reads its data from a {@link BlockingQueue} + */ +final class QueueInputStream extends InputStream { + private final BlockingQueue incomingData; + private final ByteBuffer eofIndicator; + private final QuicStreamReader streamReader; + // error needs volatile access as it is set by a different thread + private volatile Throwable error; + // current might not need volatile access as it should only + // be read/set by the reading thread. However available() + // might conceivably be called by multiple threads. + private volatile ByteBuffer current; + + QueueInputStream(final BlockingQueue incomingData, + final ByteBuffer eofIndicator, + QuicStreamReader streamReader) { + this.incomingData = incomingData; + this.eofIndicator = eofIndicator; + this.streamReader = streamReader; + } + + private ByteBuffer current() throws InterruptedException { + ByteBuffer current = this.current; + // if eof, there should no more byte buffer + if (current == eofIndicator) return eofIndicator; + if (current == null || !current.hasRemaining()) { + return (current = this.current = incomingData.take()); + } + return current; + } + + private boolean eof() { + ByteBuffer current = this.current; + return current == eofIndicator; + } + + @Override + public int read() throws IOException { + final byte[] data = new byte[1]; + final int numRead = this.read(data, 0, data.length); + // can't be 0, since we block till we receive at least 1 byte of data + assert numRead != 0 : "No data read"; + if (numRead == -1) { + return -1; + } + return data[0]; + } + + // concurrent calls to read() should not and are not supported + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + int totalRead = 0; + while (totalRead < len) { + ByteBuffer bb = null; + checkError(); + try { + bb = current(); + } catch (InterruptedException e) { + streamReader.stream().requestStopSending(QuicTransportErrors.NO_ERROR.code()); + // TODO: should close here + error(e); + Thread.currentThread().interrupt(); + throw toIOException(e); + } + if (bb == eofIndicator) { + return totalRead == 0 ? -1 : totalRead; + } + final int available = bb.remaining(); + if (available > 0) { + final int numToTransfer = Math.min(available, (len - totalRead)); + bb.get(b, off + totalRead, numToTransfer); + totalRead += numToTransfer; + } + // if more data is available, take more, else if we have read at least 1 byte + // then return back + if (totalRead > 0 && incomingData.peek() == null) { + return totalRead; + } + } + if (totalRead > 0) return totalRead; + // if we reach here then len must be 0 + checkError(); + assert len == 0; + return eof() ? -1 : 0; + } + + @Override + public int available() throws IOException { + var bb = current; + if (bb == null || !bb.hasRemaining()) bb = incomingData.peek(); + if (bb == null || bb == eofIndicator) return 0; + return bb.remaining(); + } + + // we only check for errors after the incoming data queue + // has been emptied - except for interrupt. + private void checkError() throws IOException { + var error = this.error; + if (error == null) return; + if (error instanceof InterruptedException) + throw new IOException("closed by interrupt"); + var bb = current; + if (bb == eofIndicator || (bb != null && bb.hasRemaining())) return; + // we create a new exception to have the caller in the + // stack trace. + if (incomingData.isEmpty()) throw toIOException(error); + } + + // called if an error comes from upstream + void error(Throwable error) { + boolean firstError = false; + // only keep the first error + synchronized (this) { + var e = this.error; + if (e == null) { + e = this.error = error; + firstError = true; + } + } + // unblock read if needed + if (firstError) { + incomingData.add(ByteBuffer.allocate(0)); + } + } + + static IOException toIOException(Throwable error) { + return new IOException(error); + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServer.java new file mode 100644 index 00000000000..ae600a7a592 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServer.java @@ -0,0 +1,913 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.security.AlgorithmConstraints; +import java.security.AlgorithmParameters; +import java.security.CryptoPrimitive; +import java.security.Key; +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.LongFunction; + +import javax.net.ssl.SNIMatcher; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.ServerNameMatcher; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.QuicEndpoint.QuicEndpointFactory; +import jdk.internal.net.http.quic.QuicInstance; +import jdk.internal.net.http.quic.QuicSelector; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.packets.LongHeader; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.CONNECTION_REFUSED; + +/** + * This class represents a QuicServer. + */ +public sealed class QuicServer implements QuicInstance, AutoCloseable permits QuicStandaloneServer { + + public interface ConnectionAcceptor { + boolean acceptIncoming(SocketAddress source, QuicServerConnection quicConnection); + } + + final Logger debug = Utils.getDebugLogger(this::name); + + private final String serverId; + private final String name; + private final ExecutorService executor; + private final boolean ownExecutor; + private volatile ConnectionAcceptor newConnectionAcceptor; + private final String alpn; + private final InetSocketAddress bindAddress; + private final SNIMatcher sniMatcher; + private volatile InetSocketAddress listenAddress; + private final QuicTLSContext quicTLSContext; + private volatile boolean started; + private volatile boolean sendRetry; + protected final List availableQuicVersions; + private final QuicVersion preferredVersion; + private volatile QuicEndpoint endpoint; + private volatile QuicSelector selector; + private volatile boolean closed; + private volatile QuicTransportParameters transportParameters; + private final byte[] retryTokenPrefixBytes = new byte[4]; + private final byte[] newTokenPrefixBytes = new byte[4]; + private final DatagramDeliveryPolicy incomingDeliveryPolicy; + private final DatagramDeliveryPolicy outgoingDeliveryPolicy; + private boolean wantClientAuth; + private boolean needClientAuth; + // set of KeyAgreement algorithms to reject; used to force a HelloRetryRequest + private Set rejectedKAAlgos; + // used to compute MAX_STREAMS limit by connections created on this server instance. + // if null, then an internal algorithm is used to compute the limit. + // The Function takes a boolean argument whose value is true if the computation is for bidi + // streams and false for uni streams. The returned value from this function is expected + // to be the MAX_STREAMS limit to be imposed on the peer for that particular stream type + private volatile Function maxStreamLimitComputer; + + private final ReentrantLock quickServerLock = new ReentrantLock(); + private final LongFunction appErrorCodeToString; + + record RetryData(QuicConnectionId originalServerConnId, + QuicConnectionId serverChosenConnId) { + } + + public static abstract class Builder { + protected SSLContext sslContext; + protected InetSocketAddress bindAddress; + protected DatagramDeliveryPolicy incomingDeliveryPolicy; + protected DatagramDeliveryPolicy outgoingDeliveryPolicy; + protected String serverId; + protected SNIMatcher sniMatcher; + protected ExecutorService executor; + protected QuicVersion[] availableQuicVersions; + protected boolean compatible; + protected LongFunction appErrorCodeToString; + + protected ConnectionAcceptor connAcceptor = + (source, conn) -> { + System.err.println("Rejecting connection " + conn + " attempt from source " + + source); + return false; + }; + + protected String alpn; + + protected Builder() { + try { + incomingDeliveryPolicy = DatagramDeliveryPolicy.defaultIncomingPolicy(); + outgoingDeliveryPolicy = DatagramDeliveryPolicy.defaultOutgoingPolicy(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public Builder availableVersions(final QuicVersion[] available) { + Objects.requireNonNull(available); + if (available.length == 0) { + throw new IllegalArgumentException("Empty available versions"); + } + this.availableQuicVersions = available; + return this; + } + + public Builder appErrorCodeToString(LongFunction appErrorCodeToString) { + this.appErrorCodeToString = appErrorCodeToString; + return this; + } + + public Builder compatibleNegotiation(boolean compatible) { + this.compatible = compatible; + return this; + } + + public Builder sslContext(final SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public Builder sniMatcher(final SNIMatcher sniMatcher) { + this.sniMatcher = sniMatcher; + return this; + } + + public Builder bindAddress(final InetSocketAddress addr) { + Objects.requireNonNull(addr); + this.bindAddress = addr; + return this; + } + + public Builder serverId(final String serverId) { + Objects.requireNonNull(serverId); + this.serverId = serverId; + return this; + } + + public Builder incomingDeliveryPolicy(final DatagramDeliveryPolicy policy) { + Objects.requireNonNull(policy); + this.incomingDeliveryPolicy = policy; + return this; + } + + public Builder outgoingDeliveryPolicy(final DatagramDeliveryPolicy policy) { + Objects.requireNonNull(policy); + this.outgoingDeliveryPolicy = policy; + return this; + } + + public Builder executor(final ExecutorService executor) { + this.executor = executor; + return this; + } + + public Builder alpn(String alpn) { + this.alpn = alpn; + return this; + } + + public abstract T build() throws IOException; + } + + private final QuicEndpointFactory endpointFactory; + public QuicServer(final String serverId, final InetSocketAddress bindAddress, + final ExecutorService executor, final QuicVersion[] availableQuicVersions, + boolean compatible, final QuicTLSContext quicTLSContext, final SNIMatcher sniMatcher, + final DatagramDeliveryPolicy incomingDeliveryPolicy, + final DatagramDeliveryPolicy outgoingDeliveryPolicy, String alpn, + final LongFunction appErrorCodeToString) { + this.bindAddress = bindAddress; + this.sniMatcher = sniMatcher == null + ? new ServerNameMatcher(this.bindAddress.getHostName()) + : sniMatcher; + this.alpn = Objects.requireNonNull(alpn); + this.appErrorCodeToString = appErrorCodeToString == null + ? QuicInstance.super::appErrorToString + : appErrorCodeToString; + if (executor != null) { + this.executor = executor; + this.ownExecutor = false; + } else { + this.executor = Utils.safeExecutor( + createExecutor(serverId), + (_, t) -> debug.log("rejected task - using ASYNC_POOL", t)); + this.ownExecutor = true; + } + this.serverId = serverId; + this.quicTLSContext = quicTLSContext; + this.name = "QuicServer(%s)".formatted(serverId); + if (compatible) { + this.availableQuicVersions = Arrays.asList(QuicVersion.values()); + } else { + this.availableQuicVersions = Arrays.asList(availableQuicVersions); + } + this.preferredVersion = availableQuicVersions[0]; + this.incomingDeliveryPolicy = incomingDeliveryPolicy; + this.outgoingDeliveryPolicy = outgoingDeliveryPolicy; + final Random random = new Random(); + random.nextBytes(retryTokenPrefixBytes); + random.nextBytes(newTokenPrefixBytes); + this.endpointFactory = newQuicEndpointFactory(); + if (debug.on()) { + debug.log("server created, incoming delivery policy %s, outgoing delivery policy %s", + this.incomingDeliveryPolicy, this.outgoingDeliveryPolicy); + } + } + + private static ExecutorService createExecutor(String name) { + String threadNamePrefix = "%s-quic-pool".formatted(name); + ThreadFactory threadFactory = Thread.ofPlatform().name(threadNamePrefix, 0).factory(); + return Executors.newCachedThreadPool(threadFactory); + } + + @Override + public String appErrorToString(long errorCode) { + return appErrorCodeToString.apply(errorCode); + } + + static QuicEndpointFactory newQuicEndpointFactory() { + return new QuicEndpointFactory(); + } + + public void setConnectionAcceptor(final ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor); + quickServerLock.lock(); + try { + var current = this.newConnectionAcceptor; + if (current != null) { + throw new IllegalStateException("An connection acceptor already exists for" + + " this quic server " + this); + } + this.newConnectionAcceptor = acceptor; + } finally { + quickServerLock.unlock(); + } + } + + public void setWantClientAuth(boolean wantClientAuth) { + this.wantClientAuth = wantClientAuth; + } + + public void setNeedClientAuth(boolean needClientAuth) { + this.needClientAuth = needClientAuth; + } + + public void setRejectKeyAgreement(Set rejectedKAAlgos) { + this.rejectedKAAlgos = rejectedKAAlgos; + } + + Function getMaxStreamLimitComputer() { + return this.maxStreamLimitComputer; + } + + /** + * Sets a new MAX_STREAMS limit computer for this server. + * @param computer the limit computer. can be null, in which case an internal computation + * algorithm with decide the MAX_STREAMS limit for connections on this server + * instance + */ + public void setMaxStreamLimitComputer(final Function computer) { + this.maxStreamLimitComputer = computer; + } + + @Override + public String instanceId() { + return serverId; + } + + @Override + public QuicTLSContext getQuicTLSContext() { + return quicTLSContext; + } + + @Override + public boolean isVersionAvailable(QuicVersion quicVersion) { + return availableQuicVersions.contains(quicVersion); + } + + @Override + public List getAvailableVersions() { + return availableQuicVersions; + } + + public void sendRetry(final boolean enable) { + this.sendRetry = enable; + } + + public void start() { + this.started = true; + try { + final QuicEndpoint endpoint = getEndpoint(); + final InetSocketAddress addr = this.listenAddress = (InetSocketAddress) endpoint.getLocalAddress(); + if (debug.on()) { + debug.log("Quic server listening at: " + addr + + " supported versions " + this.availableQuicVersions); + } + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + /** + * {@return the address on which the server is listening on} + * + * @throws IllegalStateException If server hasn't yet started + */ + public InetSocketAddress getAddress() { + final var addr = this.listenAddress; + if (addr == null) { + throw new IllegalArgumentException("Server hasn't started"); + } + return addr; + } + + /** + * The name identifying this QuicServer, used in debug traces. + * + * @return the name identifying this QuicServer. + * @implNote This is {@code "QuicServer()"}. + */ + @Override + public String name() { + return name; + } + + /** + * The executor used by this QuicServer when a task needs to + * be offloaded to a separate thread. + * + * @return the executor used by this QuicServer. + * @implNote This is the server internal executor. + */ + @Override + public Executor executor() { + return executor; + } + + public ExecutorService executorService() { + return this.executor; + } + + /** + * Get the QuicEndpoint for the given transport. + * + * @return the QuicEndpoint for the given transport. + * @throws IOException if an error occurs when setting up the endpoint + * or linking the transport with the endpoint. + * @throws IllegalStateException if the server is closed. + */ + @Override + public QuicEndpoint getEndpoint() throws IOException { + var endpoint = this.endpoint; + if (endpoint != null) return endpoint; + var selector = getSelector(); + quickServerLock.lock(); + try { + if (closed) throw new IllegalStateException("QuicServer is closed"); + endpoint = this.endpoint; + if (endpoint != null) return endpoint; + final String endpointName = "QuicEndpoint(" + serverId + ")"; + endpoint = this.endpoint = switch (QuicEndpoint.CONFIGURED_CHANNEL_TYPE) { + case NON_BLOCKING_WITH_SELECTOR -> + endpointFactory.createSelectableEndpoint(this, endpointName, + bindAddress, selector.timer()); + case BLOCKING_WITH_VIRTUAL_THREADS -> + endpointFactory.createVirtualThreadedEndpoint(this, endpointName, + bindAddress, selector.timer()); + }; + } finally { + quickServerLock.unlock(); + } + // register the newly created endpoint with the selector + QuicEndpoint.registerWithSelector(endpoint, selector, debug); + return endpoint; + } + + /** + * Gets the QuicSelector for the transport. + * + * @return the QuicSelector for the given transport. + * @throws IOException if an error occurs when setting up the selector + * or linking the transport with the selector. + * @throws IllegalStateException if the server is closed. + */ + private QuicSelector getSelector() throws IOException { + var selector = this.selector; + if (selector != null) return selector; + quickServerLock.lock(); + try { + if (closed) throw new IllegalStateException("QuicServer is closed"); + selector = this.selector; + if (selector != null) return selector; + final String selectorName = "QuicSelector(" + serverId + ")"; + selector = this.selector = switch (QuicEndpoint.CONFIGURED_CHANNEL_TYPE) { + case NON_BLOCKING_WITH_SELECTOR -> + QuicSelector.createQuicNioSelector(this, selectorName); + case BLOCKING_WITH_VIRTUAL_THREADS -> + QuicSelector.createQuicVirtualThreadPoller(this, selectorName); + }; + } finally { + quickServerLock.unlock(); + } + // we may be closed when we reach here. It doesn't matter though. + // if the selector is closed before it's started the thread will + // immediately exit (or exit after the first wakeup) + debug.log("starting selector"); + selector.start(); + return selector; + } + + @Override + public void unmatchedQuicPacket(SocketAddress source, QuicPacket.HeadersType type, ByteBuffer buffer) { + if (debug.on()) { + debug.log("Received datagram %s(src=%s, payload(%d))", type, source, buffer.remaining()); + } + // consult the delivery policy to see if we should silently drop this packet + if (this.incomingDeliveryPolicy.shouldDrop(source, buffer, type)) { + silentIgnorePacket(source, buffer, type, false); + return; + } + // check packet type. If Initial, it may be a connection attempt + int pos = buffer.position(); + if (type != QuicPacket.HeadersType.LONG) { + if (debug.on()) { + debug.log("Dropping unmatched datagram %s(src=%s, payload(%d))", + type, source, buffer.remaining()); + } + return; + } + // INITIAL packet + // decode packet here + // TODO: FIXME + // Transport: is this needed? + // ALPN, etc... + // Move this to a dedicated method + // Double check how the serverId provided by the client should + // be replaced + // Should the new connection have 2 connections id for a time? + // => the initial one that the client sent, and that will + // be used until the client receives our response, + // => the new connection id that we are sending back to the + // client? + LongHeader header = QuicPacketDecoder.peekLongHeader(buffer); + if (header == null) { + if (debug.on()) { + debug.log("Dropping invalid datagram %s(src=%s, payload(%d))", + type, source, buffer.remaining()); + } + return; + } + // need to assert that dest.remaining() >= 8 and drop the packet + // if this is not the case. + if (header.destinationId().length() < 8) { + debug.log("destination connection id has not enough bytes: %d", + header.destinationId().length()); + return; + } + + final QuicVersion version = QuicVersion.of(header.version()).orElse(null); + try { + // check that the server supports the version, send a version + if (header.version() == 0) { + if (debug.on()) { + debug.log("Stray version negotiation packet"); + } + return; + } + if (version == null || !availableQuicVersions.contains(version)) { + if (debug.on()) { + debug.log("Unsupported version number 0x%x in incoming packet (len=%d)", header.version(), buffer.remaining()); + } + if (buffer.remaining() >= QuicConnectionImpl.SMALLEST_MAXIMUM_DATAGRAM_SIZE) { + // A server might not send a Version Negotiation packet if the datagram it receives is smaller than the minimum size specified in a different version + int[] supported = availableQuicVersions.stream().mapToInt(QuicVersion::versionNumber).toArray(); + var negotiate = QuicPacketEncoder + .newVersionNegotiationPacket(header.destinationId(), + header.sourceId(), supported); + ByteBuffer datagram = ByteBuffer.allocateDirect(negotiate.size()); + QuicPacketEncoder.of(QuicVersion.QUIC_V1).encode(negotiate, datagram, null); + datagram.flip(); + sendDatagram(source, datagram); + } + return; + } + } catch (Throwable t) { + debug.log("Failed to decode packet", t); + return; + } + assert availableQuicVersions.contains(version); + final InetSocketAddress peerAddress = (InetSocketAddress) source; + final ByteBuffer token = QuicPacketDecoder.peekInitialPacketToken(buffer); + if (token == null) { + // packet is malformed: token will be an empty ByteBuffer if + // the packet doesn't contain a token. + debug.log("failed to read connection token"); + return; + } + var localAddress = this.getAddress(); + var conflict = Utils.addressConflict(localAddress, peerAddress); + if (conflict != null) { + String msg = "%s: %s (local:%s == peer:%s)!"; + System.out.println(msg.formatted(this, conflict, localAddress, peerAddress)); + debug.log(msg, "WARNING", conflict, localAddress, peerAddress); + Log.logError(msg.formatted(this, conflict, localAddress, peerAddress)); + } + final QuicServerConnection connection; + if (token.hasRemaining()) { + // the INITIAL packet contains a token. This token is then expected to either match + // the RETRY packet token that this server might have sent (if any) or a NEW_TOKEN frame + // token that this server might have sent (if any). If the token doesn't match either + // of these expectations then drop the packet (or send CLOSE_CONNECTION frame) + final RetryData retryData = isRetryToken(token.asReadOnlyBuffer()); + if (retryData != null) { + // the token matches one that this server could have sent as a RETRY token. verify + // that this server was indeed configured to send a RETRY token. + if (!sendRetry) { + // although the token looks like a RETRY token, this server wasn't configured + // to send a RETRY token, so consider this an invalid token and drop the packet + // (or send CLOSE_CONNECTION frame) + debug.log("Server dropping INITIAL packet due to token " + + "(which looks like an unexpected retry token) from " + peerAddress); + return; + } + // verify the dest connection id in the INITIAL packet is the one that we had asked + // the client to use through our RETRY packet + if (!retryData.serverChosenConnId.equals(header.destinationId())) { + // drop the packet + debug.log("Invalid dest connection id in INITIAL packet," + + " expected the one sent in RETRY packet " + retryData.serverChosenConnId + + " but found a different one " + header.destinationId()); + return; + } + // at this point we have verified that the token is a valid retry token that this server + // sent. We can now create a connection + final SSLParameters sslParameters = createSSLParameters(peerAddress); + final byte[] clientInitialToken = new byte[token.remaining()]; + token.get(clientInitialToken); + connection = new QuicServerConnection(this, version, preferredVersion, + peerAddress, header.destinationId(), + sslParameters, clientInitialToken, retryData); + debug.log("Created new server connection " + connection + " (with a retry token) " + + "to client " + peerAddress); + } else { + // token doesn't match a RETRY token. check if it is a NEW_TOKEN that this server + // sent + final boolean isNewToken = isNewToken(token.asReadOnlyBuffer()); + if (!isNewToken) { + // invalid token in the INITIAL packet. drop packet (or send CLOSE_CONNECTION + // frame) + debug.log("Server dropping INITIAL packet due to unexpected token from " + + peerAddress); + return; + } + // matches a NEW_TOKEN token. create the connection + final SSLParameters sslParameters = createSSLParameters(peerAddress); + final byte[] clientInitialToken = new byte[token.remaining()]; + token.get(clientInitialToken); + connection = new QuicServerConnection(this, version, preferredVersion, + peerAddress, header.destinationId(), + sslParameters, clientInitialToken); + debug.log("Created new server connection " + connection + + " (with NEW_TOKEN initial token) to client " + peerAddress); + } + } else { + // token is empty in INITIAL packet. send a RETRY packet if the server is configured + // to do so. The spec allows us to send the RETRY packet more than once to the same + // client, so we don't have to maintain any state to check if we already have sent one. + if (sendRetry) { + // send RETRY packet + final QuicConnectionId serverConnId = this.endpoint.idFactory().newConnectionId(); + final byte[] retryToken = buildRetryToken(header.destinationId(), serverConnId); + QuicPacketEncoder encoder = QuicPacketEncoder.of(version); + final var retry = encoder + .newRetryPacket(serverConnId, header.sourceId(), retryToken); + final ByteBuffer datagram = ByteBuffer.allocateDirect(retry.size()); + try { + encoder.encode(retry, datagram, new RetryCodingContext(header.destinationId(), quicTLSContext)); + } catch (Throwable t) { + // TODO: should we throw exception? + debug.log("Failed to encode packet", t); + return; + } + datagram.flip(); + debug.log("Sending RETRY packet to client " + peerAddress); + sendDatagram(source, datagram); + return; + } + // no token in INITIAL frame and the server isn't configured to send a RETRY packet. + // we are now ready to create the connection + final SSLParameters sslParameters = createSSLParameters(peerAddress); + connection = new QuicServerConnection(this, version, preferredVersion, + peerAddress, header.destinationId(), + sslParameters, null); + debug.log("Created new server connection " + connection + + " (without any initial token) to client " + peerAddress); + } + assert connection.quicVersion() == version; + + // TODO: maybe we should coalesce some dummy packet in the datagram + // to make sure the client will ignore it + // => this might require slightly altering the algorithm for + // encoding packets: + // we may need to build a packet, and then only encode it + // instead of having a toByteBuffer() method. + try { + endpoint.registerNewConnection(connection); + } catch (IOException io) { + if (closed) { + debug.log("Can't register new connection: server closed"); + } else if (debug.on()) { + debug.log("Can't register new connection", io); + } + // drop all bytes in the payload + buffer.position(buffer.limit()); + connection.connectionTerminator().terminate( + forTransportError(CONNECTION_REFUSED).loggedAs(io.getMessage())); + return; + } + connection.processIncoming(source, header.destinationId().asReadOnlyBuffer(), type, buffer); + + final ConnectionAcceptor connAcceptor = this.newConnectionAcceptor; + if (connAcceptor == null || !connAcceptor.acceptIncoming(source, connection)) { + buffer.position(buffer.limit()); + final String msg = "Quic server " + this.serverId + " refused connection"; + connection.connectionTerminator().terminate( + forTransportError(CONNECTION_REFUSED).loggedAs(msg)); + return; + } + } + + void sendDatagram(final SocketAddress dest, final ByteBuffer datagram) { + final QuicPacket.HeadersType headersType = QuicPacketDecoder.peekHeaderType(datagram, + datagram.position()); + if (this.outgoingDeliveryPolicy.shouldDrop(dest, datagram, headersType)) { + silentIgnorePacket(dest, datagram, headersType, true); + return; + } + endpoint.pushDatagram(null, dest, datagram); + return; + } + + private SSLParameters createSSLParameters(final InetSocketAddress peerAddress) { + final SSLParameters sslParameters = Utils.copySSLParameters(this.getSSLParameters()); + sslParameters.setApplicationProtocols(new String[]{this.alpn}); + sslParameters.setProtocols(new String[] {"TLSv1.3"}); + if (this.sniMatcher != null) { + sslParameters.setSNIMatchers(List.of(this.sniMatcher)); + } + if (wantClientAuth) { + sslParameters.setWantClientAuth(true); + } else if (needClientAuth) { + sslParameters.setNeedClientAuth(true); + } + if (rejectedKAAlgos != null) { + sslParameters.setAlgorithmConstraints(new TestAlgorithmConstraints(rejectedKAAlgos)); + } + return sslParameters; + } + + private byte[] buildRetryToken(final QuicConnectionId originalServerConnId, + final QuicConnectionId serverChosenNewConnId) { + // TODO this token is too simple to provide authenticity guarantee; use for testing only + final int NUM_BYTES_FOR_CONN_ID_LENGTH = 1; + final byte[] result = new byte[retryTokenPrefixBytes.length + + NUM_BYTES_FOR_CONN_ID_LENGTH + originalServerConnId.length() + + NUM_BYTES_FOR_CONN_ID_LENGTH + serverChosenNewConnId.length()]; + // copy the retry token prefix + System.arraycopy(retryTokenPrefixBytes, 0, result, 0, retryTokenPrefixBytes.length); + int currentIndex = retryTokenPrefixBytes.length; + // copy over the length of the original dest conn id + result[currentIndex++] = (byte) originalServerConnId.length(); + // copy over the original dest connection id sent by the client + originalServerConnId.asReadOnlyBuffer().get(0, result, currentIndex, originalServerConnId.length()); + currentIndex += originalServerConnId.length(); + // copy over the length of the server chosen dest conn id + result[currentIndex++] = (byte) serverChosenNewConnId.length(); + // copy over the connection id that the server has chosen and expects clients to use as new dest conn id + serverChosenNewConnId.asReadOnlyBuffer().get(0, result, currentIndex, serverChosenNewConnId.length()); + return result; + } + + private RetryData isRetryToken(final ByteBuffer token) { + Objects.requireNonNull(token); + if (!token.hasRemaining()) { + return null; + } + final int NUM_BYTES_FOR_CONN_ID_LENGTH = 1; + // we expect the retry token prefix and 2 connection ids. so expected length = retry token prefix + // length plus the length of each of the connection ids + final int expectedLength = retryTokenPrefixBytes.length + NUM_BYTES_FOR_CONN_ID_LENGTH + + NUM_BYTES_FOR_CONN_ID_LENGTH; + if (token.remaining() <= expectedLength) { + return null; + } + final byte[] tokenPrefixBytes = new byte[retryTokenPrefixBytes.length]; + token.get(tokenPrefixBytes); + int mismatchIndex = Arrays.mismatch(retryTokenPrefixBytes, 0, retryTokenPrefixBytes.length, + tokenPrefixBytes, 0, retryTokenPrefixBytes.length); + if (mismatchIndex != -1) { + // token doesn't start with the expected retry token prefix. Not a valid retry token + return null; + } + // now find the length of the original connection id + final int originalServerConnIdLen = token.get(); + final byte[] originalServerConnId = new byte[originalServerConnIdLen]; + // read the original dest conn id + token.get(originalServerConnId); + + // now find the length of the server generated dest connection id + final int serverChosenDestConnIdLen = token.get(); + final byte[] serverChosenDestConnId = new byte[serverChosenDestConnIdLen]; + // read the server chosen dest conn id + token.get(serverChosenDestConnId); + + // TODO: the use of PeerConnectionId is only for convenience + return new RetryData(new PeerConnectionId(originalServerConnId), new PeerConnectionId(serverChosenDestConnId)); + } + + DatagramDeliveryPolicy incomingDeliveryPolicy() { + return this.incomingDeliveryPolicy; + } + + DatagramDeliveryPolicy outgoingDeliveryPolicy() { + return this.outgoingDeliveryPolicy; + } + + byte[] buildNewToken() { + // TODO this token is too simple to provide authenticity guarantee; use for testing only + final byte[] token = new byte[newTokenPrefixBytes.length]; + // copy the new_token prefix + System.arraycopy(newTokenPrefixBytes, 0, token, 0, newTokenPrefixBytes.length); + return token; + } + + private boolean isNewToken(final ByteBuffer token) { + Objects.requireNonNull(token); + if (!token.hasRemaining()) { + return false; + } + if (token.remaining() != newTokenPrefixBytes.length) { + return false; + } + final byte[] tokenBytes = new byte[newTokenPrefixBytes.length]; + token.get(tokenBytes); + int mismatchIndex = Arrays.mismatch(newTokenPrefixBytes, 0, newTokenPrefixBytes.length, + tokenBytes, 0, newTokenPrefixBytes.length); + if (mismatchIndex != -1) { + // token doesn't start with the expected new_token prefix. Not a valid token + return false; + } + return true; + } + + private void silentIgnorePacket(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType, final boolean outgoing) { + if (debug.on()) debug.log("silently dropping %s packet %s %s", headersType, + (outgoing ? "to dest" : "from source"), source); + } + + @Override + public void close() throws IOException { + // TODO: ignore exceptions while closing? + quickServerLock.lock(); + try { + if (closed) return; + closed = true; + } finally { + quickServerLock.unlock(); + } + //http3Server.stop(); + var endpoint = this.endpoint; + if (endpoint != null) endpoint.close(); + debug.log("endpoint closed"); + var selector = this.selector; + if (selector != null) selector.close(); + debug.log("selector closed"); + if (ownExecutor && executor != null) { + debug.log("shutting down executor"); + this.executor.shutdown(); + try { + debug.log("awaiting termination"); + this.executor.awaitTermination(QuicSelector.IDLE_PERIOD_MS, + TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + this.executor.shutdownNow(); + } + } + } + + /** + * The transport parameters that will be sent to the peer by any new subsequent server connections + * that are created by this server + * + * @param params transport parameters. Can be null, in which case the new server connection + * (whenever it is created) will use internal defaults + */ + public void setTransportParameters(QuicTransportParameters params) { + this.transportParameters = params; + } + + /** + * {@return the current configured transport parameters for new server connections. null if none + * configured} + */ + @Override + public QuicTransportParameters getTransportParameters() { + final QuicTransportParameters qtp = this.transportParameters; + if (qtp == null) { + return null; + } + // return a copy + return new QuicTransportParameters(qtp); + } + + /** + * Called + * + * @param connection + * @param originalConnectionId + * @param localConnectionId + */ + void connectionAcknowledged(QuicConnection connection, + QuicConnectionId originalConnectionId, + QuicConnectionId localConnectionId) { + // endpoint.removeConnectionId(originalConnectionId); + } + + private static class TestAlgorithmConstraints implements AlgorithmConstraints { + private final Set rejectedKAAlgos; + + public TestAlgorithmConstraints(Set rejectedKAAlgos) { + this.rejectedKAAlgos = rejectedKAAlgos; + } + + @Override + public boolean permits(Set primitives, String algorithm, AlgorithmParameters parameters) { + if (primitives.contains(CryptoPrimitive.KEY_AGREEMENT) && + rejectedKAAlgos.contains(algorithm)) { + return false; + } + return true; + } + + @Override + public boolean permits(Set primitives, Key key) { + return true; + } + + @Override + public boolean permits(Set primitives, String algorithm, Key key, AlgorithmParameters parameters) { + return true; + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerConnection.java new file mode 100644 index 00000000000..53a9129d452 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerConnection.java @@ -0,0 +1,570 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import javax.net.ssl.SSLParameters; + +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.QuicTransportParameters.VersionInformation; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.QuicTransportParameters; +import jdk.internal.net.http.quic.QuicTransportParameters.ParameterId; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.NewTokenFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.http.quic.packets.OneRttPacket; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTLSEngine.HandshakeState; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_data; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_local; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_stream_data_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_bidi; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_max_streams_uni; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.initial_source_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.max_idle_timeout; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.original_destination_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.retry_source_connection_id; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.stateless_reset_token; +import static jdk.internal.net.http.quic.QuicTransportParameters.ParameterId.version_information; +import static jdk.internal.net.quic.QuicTransportErrors.PROTOCOL_VIOLATION; + +public final class QuicServerConnection extends QuicConnectionImpl { + private static final AtomicLong CONNECTIONS = new AtomicLong(); + private final QuicVersion preferredQuicVersion; + private volatile boolean connectionIdAcknowledged; + private final QuicServer server; + private final byte[] clientInitialToken; + private final QuicConnectionId clientSentDestConnId; + private final QuicConnectionId originalServerConnId; + private final QuicServer.RetryData retryData; + private final AtomicBoolean firstHandshakePktProcessed = new AtomicBoolean(); + + public static final boolean FILTER_SENDER_ADDRESS = Utils.getBooleanProperty( + "test.quic.server.filterSenderAddress", true); + + QuicServerConnection(QuicServer server, + QuicVersion quicVersion, + QuicVersion preferredQuicVersion, + InetSocketAddress peerAddress, + QuicConnectionId clientSentDestConnId, + SSLParameters sslParameters, + byte[] initialToken) { + this(server, quicVersion, preferredQuicVersion, peerAddress, clientSentDestConnId, + sslParameters, initialToken, null); + + } + + QuicServerConnection(QuicServer server, + QuicVersion quicVersion, + QuicVersion preferredQuicVersion, + InetSocketAddress peerAddress, + QuicConnectionId clientSentDestConnId, + SSLParameters sslParameters, + byte[] initialToken, + QuicServer.RetryData retryData) { + super(quicVersion, server, peerAddress, null, -1, sslParameters, + "QuicServerConnection(%s)", CONNECTIONS.incrementAndGet()); + this.preferredQuicVersion = preferredQuicVersion; + // this should have been first statement in this constructor but compiler doesn't allow it + Objects.requireNonNull(quicVersion, "quic version"); + this.clientInitialToken = initialToken; + this.server = server; + this.clientSentDestConnId = clientSentDestConnId; + this.retryData = retryData; + this.originalServerConnId = retryData == null ? clientSentDestConnId : retryData.originalServerConnId(); + handshakeFlow().handshakeCF().thenAccept((hs) -> { + try { + onHandshakeCompletion(hs); + } catch (Exception e) { + // TODO: consider if this needs to be propagated somehow. for now just log + System.err.println("onHandshakeCompletion() failed: " + e); + e.printStackTrace(); + } + }); + assert quicVersion == quicVersion() : "unexpected quic version on" + + " server connection, expected " + quicVersion + " but found " + quicVersion(); + try { + getTLSEngine().deriveInitialKeys(quicVersion, clientSentDestConnId.asReadOnlyBuffer()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected QuicConnectionId originalServerConnId() { + return this.originalServerConnId; + } + + @Override + protected boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return Arrays.equals(clientInitialToken, token); + } + + @Override + public List connectionIds() { + var connectionIds = super.connectionIds(); + // we can stop using the original connection id if we have + // received the ClientHello fully. + // TODO: find when/where to switch connectionIdAcknowledged to true + // TODO: what if the ClientHello is in 3 initial packets and the packet number 2 + // gets lost? How do we know? I guess we can assume that the client hello + // was fully receive when we send (or receive) the first handshake packet. + if (!connectionIdAcknowledged) { + // Add client's initial connection ID (original or retry) + QuicConnectionId initial = this.clientSentDestConnId; + connectionIds = Stream.concat(connectionIds.stream(), Stream.of(initial)).toList(); + } + return connectionIds; + } + + @Override + public Optional initialConnectionId() { + return Optional.ofNullable(clientSentDestConnId); + } + + @Override + public void processIncoming(final SocketAddress source, final ByteBuffer destConnId, + final QuicPacket.HeadersType headersType, final ByteBuffer buffer) { + // consult the delivery policy if this packet should be dropped + if (this.server.incomingDeliveryPolicy().shouldDrop(source, buffer, this, headersType)) { + silentIgnorePacket(source, buffer, headersType, false, "incoming delivery policy"); + return; + } + if (!connectionIdAcknowledged && localConnectionId().matches(destConnId)) { + debug.log("connection acknowledged"); + connectionIdAcknowledged = true; + server.connectionAcknowledged(this, clientSentDestConnId, localConnectionId()); + endpoint.removeConnectionId(clientSentDestConnId, this); + } + super.processIncoming(source, destConnId, headersType, buffer); + } + + @Override + public boolean accepts(SocketAddress source) { + if (FILTER_SENDER_ADDRESS && !source.equals(peerAddress())) { + // We do not support path migration yet, so we only accept + // packets from the endpoint to which we send them. + if (debug.on()) { + debug.log("unexpected sender %s, skipping packet", source); + } + return false; + } + return true; + } + + @Override + protected void processRetryPacket(final QuicPacket quicPacket) { + // server is not supposed to receive retry packet: + // ignore it? + Objects.requireNonNull(quicPacket); + if (quicPacket.packetType() != PacketType.RETRY) { + throw new IllegalArgumentException("Not a RETRY packet: " + quicPacket.packetType()); + } + if (Log.errors()) { + Log.logError("Server received RETRY packet - discarding it"); + } + } + + @Override + protected void incoming1RTTFrame(HandshakeDoneFrame frame) throws QuicTransportException { + // RFC-9000, section 19.20: A HANDSHAKE_DONE frame can only be sent by + // the server. ... A server MUST treat receipt of a HANDSHAKE_DONE frame + // as a connection error of type PROTOCOL_VIOLATION + throw new QuicTransportException("HANDSHAKE_DONE frame isn't allowed from clients", + null, + frame.getTypeField(), PROTOCOL_VIOLATION); + } + + @Override + protected void incoming1RTTFrame(NewTokenFrame frame) throws QuicTransportException { + // This is a server connection and as per RFC-9000, section 19.7, clients + // aren't supposed to send NEW_TOKEN frames and if a server receives such + // a frame then it is considered a connection error + // of type PROTOCOL_VIOLATION. + throw new QuicTransportException("NEW_TOKEN frame isn't allowed from clients", + null, + frame.getTypeField(), PROTOCOL_VIOLATION); + } + + @Override + protected void pushDatagram(final SocketAddress destination, final ByteBuffer datagram) { + final QuicPacket.HeadersType headersType = QuicPacketDecoder.peekHeaderType(datagram, + datagram.position()); + // consult the delivery policy if this packet should be dropped + if (this.server.outgoingDeliveryPolicy().shouldDrop(destination, datagram, + this, headersType)) { + silentIgnorePacket(destination, datagram, headersType, true, "outgoing delivery policy"); + return; + } + super.pushDatagram(destination, datagram); + } + + @Override + protected void processInitialPacket(final QuicPacket quicPacket) { + try { + if (!(quicPacket instanceof InitialPacket initialPacket)) { + throw new AssertionError("Bad packet type: " + quicPacket); + } + updatePeerConnectionId(initialPacket); + var initialPayloadLength = initialPacket.payloadSize(); + assert initialPayloadLength < Integer.MAX_VALUE; + if (debug.on()) { + debug.log("Initial payload (count=%d, remaining=%d)", + initialPacket.frames().size(), initialPayloadLength); + } + long total = processInitialPacketPayload(initialPacket); + assert total == initialPayloadLength; + if (initialPacket.frames().stream().anyMatch(f -> f instanceof CryptoFrame)) { + debug.log("ClientHello received"); + } + var hsState = getTLSEngine().getHandshakeState(); + debug.log("hsState: " + hsState); + if (hsState == QuicTLSEngine.HandshakeState.NEED_SEND_CRYPTO) { + debug.log("Continuing handshake"); + continueHandshake(); + } else if (quicPacket.isAckEliciting() && + getTLSEngine().getCurrentSendKeySpace() == QuicTLSEngine.KeySpace.HANDSHAKE) { + packetNumberSpaces().initial().fastRetransmit(); + } + } catch (Throwable t) { + debug.log("Unexpected exception handling initial packet", t); + connectionTerminator().terminate(TerminationCause.forException(t)); + } + } + + @Override + protected void processHandshakePacket(final QuicPacket quicPacket) { + super.processHandshakePacket(quicPacket); + if (this.firstHandshakePktProcessed.compareAndSet(false, true)) { + // close INITIAL packet space and discard INITIAL keys as expected by + // RFC-9001, section 4.9.1: ... a server MUST discard Initial keys when + // it first successfully processes a Handshake packet. Endpoints MUST NOT send + // Initial packets after this point. + if (debug.on()) { + debug.log("server processed first handshake packet, initiating close of" + + " INITIAL packet space"); + } + packetNumberSpaces().initial().close(); + } + QuicTLSEngine engine = getTLSEngine(); + switch (engine.getHandshakeState()) { + case NEED_SEND_HANDSHAKE_DONE -> { + // should ack handshake and possibly send HandshakeDoneFrame + // the HANDSHAKE space will be closed after sending the + // HANDSHAKE_DONE frame (see sendStreamData) + packetSpace(PacketNumberSpace.HANDSHAKE).runTransmitter(); + engine.tryMarkHandshakeDone(); + enqueue1RTTFrame(new HandshakeDoneFrame()); + debug.log("Adding HandshakeDoneFrame"); + completeHandshakeCF(); + packetSpace(PacketNumberSpace.APPLICATION).runTransmitter(); + } + } + } + + @Override + protected void completeHandshakeCF() { + completeHandshakeCF(null); + } + + @Override + protected void send1RTTPacket(final OneRttPacket packet) + throws QuicKeyUnavailableException, QuicTransportException { + boolean closeHandshake = false; + var handshakeSpace = packetNumberSpaces().handshake(); + if (!handshakeSpace.isClosed()) { + closeHandshake = packet.frames() + .stream() + .anyMatch(HandshakeDoneFrame.class::isInstance); + } + super.send1RTTPacket(packet); + if (closeHandshake) { + // close handshake space after sending + // HANDSHAKE_DONE + handshakeSpace.close(); + } + } + + /** + * This method can be invoked if a certain {@code action} needs to be performed, + * by this server connection, on a successful completion of Quic connection handshake + * (initiated by a client). + * + * @param action The action to be performed on successful completion of the handshake + */ + public void onSuccessfulHandshake(final Runnable action) { + this.handshakeFlow().handshakeCF().thenRun(action); + } + + /** + * This method can be invoked if a certain {@code action} needs to be performed, + * by this server connection, when a Quic connection handshake (initiated by a client), completes. + * The handshake could either have succeeded or failed. If the handshake succeeded, then the + * {@code Throwable} passed to the {@code action} will be {@code null}, else it will represent + * the handshake failure. + * + * @param action The action to be performed on completion of the handshake + */ + public void onHandshakeCompletion(final Consumer action) { + this.handshakeFlow().handshakeCF().handle((unused, failure) -> { + action.accept(failure); + return null; + }); + } + + + @Override + public QuicServer quicInstance() { + return server; + } + + @Override + protected ByteBuffer buildInitialParameters() { + final QuicTransportParameters params = new QuicTransportParameters(this.transportParams); + if (!params.isPresent(original_destination_connection_id)) { + params.setParameter(original_destination_connection_id, originalServerConnId.getBytes()); + } + if (!params.isPresent(initial_source_connection_id)) { + params.setParameter(initial_source_connection_id, localConnectionId().getBytes()); + } + if (!params.isPresent(stateless_reset_token)) { + params.setParameter(stateless_reset_token, + endpoint.idFactory().statelessTokenFor(localConnectionId())); + } + if (!params.isPresent(max_idle_timeout)) { + final long idleTimeoutMillis = TimeUnit.SECONDS.toMillis( + Utils.getLongProperty("jdk.test.server.quic.idleTimeout", 30)); + params.setIntParameter(max_idle_timeout, idleTimeoutMillis); + } + if (retryData != null && !params.isPresent(retry_source_connection_id)) { + // include the connection id that was directed by this server's RETRY packet + // for usage in INITIAL packets sent by client + params.setParameter(retry_source_connection_id, + retryData.serverChosenConnId().getBytes()); + } + setIntParamIfNotSet(params, initial_max_data, () -> DEFAULT_INITIAL_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_local, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_bidi_remote, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_stream_data_uni, () -> DEFAULT_INITIAL_STREAM_MAX_DATA); + setIntParamIfNotSet(params, initial_max_streams_bidi, () -> (long) DEFAULT_MAX_BIDI_STREAMS); + setIntParamIfNotSet(params, initial_max_streams_uni, () -> (long) DEFAULT_MAX_UNI_STREAMS); + // params.setParameter(QuicTransportParameters.ParameterId.stateless_reset_token, ...); // no token + // params.setIntParameter(QuicTransportParameters.ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(QuicTransportParameters.ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(QuicTransportParameters.ParameterId.disable_active_migration, false); + // params.setPreferedAddressParameter(QuicTransportParameters.ParameterId.preferred_address, ...); + // params.setIntParameter(QuicTransportParameters.ParameterId.active_connection_id_limit, 2); + if (!params.isPresent(version_information)) { + final VersionInformation vi = QuicTransportParameters.buildVersionInformation( + quicVersion(), quicInstance().getAvailableVersions()); + params.setVersionInformationParameter(version_information, vi); + } + final byte[] unsupportedTransportParam = encodeRandomUnsupportedTransportParameter(); + final int capacity = params.size() + unsupportedTransportParam.length; + final ByteBuffer buf = ByteBuffer.allocate(capacity); + params.encode(buf); + // add an unsupported transport param id so that we can exercise the case where endpoints + // are expected to ignore unsupported transport parameters (RFC-9000, section 7.4.2) + buf.put(unsupportedTransportParam); + buf.flip(); + newLocalTransportParameters(params); + return buf; + } + + // returns the encoded representation of a random unsupported transport parameter + private static byte[] encodeRandomUnsupportedTransportParameter() { + final int n = new Random().nextInt(1, 100); + final long unsupportedParamId = 31 * n + 27; // RFC-9000, section 18.1 + final int value = 42; + // Transport Parameter { + // Transport Parameter ID (i), + // Transport Parameter Length (i), + // Transport Parameter Value (..), + // } + int size = 0; + size += VariableLengthEncoder.getEncodedSize(unsupportedParamId); + final int paramLength = VariableLengthEncoder.getEncodedSize(value); + size += VariableLengthEncoder.getEncodedSize(paramLength); + size += paramLength; + final byte[] encoded = new byte[size]; + final ByteBuffer buf = ByteBuffer.wrap(encoded); + + VariableLengthEncoder.encode(buf, unsupportedParamId); // write out the id, as a variable length integer + VariableLengthEncoder.encode(buf, paramLength); // write out the len, as a variable length integer + VariableLengthEncoder.encode(buf, value); // write out the actual value + return encoded; + } + + @Override + protected void consumeQuicParameters(ByteBuffer byteBuffer) + throws QuicTransportException { + final QuicTransportParameters params = QuicTransportParameters.decode(byteBuffer); + if (debug.on()) { + debug.log("Received (from client) Quic transport params: " + params.toStringWithValues()); + } + if (params.isPresent(retry_source_connection_id)) { + throw new QuicTransportException("Retry connection ID not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(original_destination_connection_id)) { + throw new QuicTransportException("Original connection ID not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(stateless_reset_token)) { + throw new QuicTransportException("Reset token not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (params.isPresent(ParameterId.preferred_address)) { + throw new QuicTransportException("Preferred address not expected here", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (!params.matches(initial_source_connection_id, + getIncomingInitialPacketSourceId())) { + throw new QuicTransportException("Peer connection ID does not match", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + VersionInformation vi = + params.getVersionInformationParameter(version_information); + if (vi != null) { + boolean found = false; + for (int v: vi.availableVersions()) { + if (v == vi.chosenVersion()) { + found = true; + break; + } + } + if (!found) { + throw new QuicTransportException( + "[version_information] Chosen Version not in Available Versions", + null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + } + if (vi.chosenVersion() != quicVersion().versionNumber()) { + throw new QuicTransportException( + "[version_information] Chosen Version %s does not match version in use %s" + .formatted(vi.chosenVersion(), quicVersion().versionNumber()), + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + assert Arrays.stream(vi.availableVersions()).anyMatch(v -> v == preferredQuicVersion.versionNumber()); + if (preferredQuicVersion != quicVersion()) { + if (!switchVersion(preferredQuicVersion)) { + throw new QuicTransportException("Switching version failed", + null, 0, QuicTransportErrors.VERSION_NEGOTIATION_ERROR); + } + } + } else { + assert preferredQuicVersion == quicVersion(); + } + markVersionNegotiated(preferredQuicVersion.versionNumber()); + handleIncomingPeerTransportParams(params); + + // build our parameters + final ByteBuffer quicInitialParameters = buildInitialParameters(); + getTLSEngine().setLocalQuicTransportParameters(quicInitialParameters); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_data, DEFAULT_INITIAL_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_local, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_stream_data_bidi_remote, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_stream_data_uni, DEFAULT_INITIAL_STREAM_MAX_DATA); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_streams_bidi, DEFAULT_MAX_STREAMS); + // params.setIntParameter(QuicTransportParameters.ParameterId.initial_max_streams_uni, DEFAULT_MAX_STREAMS); + // params.setIntParameter(QuicTransportParameters.ParameterId.ack_delay_exponent, 3); // unit 2^3 microseconds + // params.setIntParameter(QuicTransportParameters.ParameterId.max_ack_delay, 25); //25 millis + // params.setBooleanParameter(QuicTransportParameters.ParameterId.disable_active_migration, false); + // params.setIntParameter(QuicTransportParameters.ParameterId.active_connection_id_limit, 2); + } + + @Override + protected void processVersionNegotiationPacket(final QuicPacket quicPacket) { + // ignore the packet: the server doesn't reply to version negotiation. + debug.log("Server ignores version negotiation packet: " + quicPacket); + } + + @Override + public boolean isClientConnection() { + return false; + } + + @Override + protected QuicEndpoint onHandshakeCompletion(HandshakeState result) { + super.onHandshakeCompletion(result); + // send a new token frame to the client, for use in new connection attempts. + sendNewToken(); + return this.endpoint; + } + + private void sendNewToken() { + final byte[] token = server.buildNewToken(); + final QuicFrame newTokenFrame = new NewTokenFrame(ByteBuffer.wrap(token)); + enqueue1RTTFrame(newTokenFrame); + } + + @Override + public long nextMaxStreamsLimit(final boolean bidi) { + final Function limitComputer = this.server.getMaxStreamLimitComputer(); + if (limitComputer != null) { + return limitComputer.apply(bidi); + } + return super.nextMaxStreamsLimit(bidi); + } + + private void silentIgnorePacket(final SocketAddress source, final ByteBuffer payload, + final QuicPacket.HeadersType headersType, + final boolean outgoing, + final String reason) { + if (debug.on()) { + debug.log("silently dropping %s packet %s %s, reason: %s", headersType, + (outgoing ? "to dest" : "from source"), source, reason); + } + } +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerHandler.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerHandler.java new file mode 100644 index 00000000000..886f15821a6 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicServerHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.SocketAddress; + +import jdk.internal.net.http.quic.streams.QuicBidiStream; + +/** + * Used by server side application code to handle incoming Quic connections and streams associated + * with those connections + * + * @see QuicStandaloneServer#addHandler(QuicServerHandler) + */ +// TODO: should we make this an abstract class instead of interface? +public interface QuicServerHandler { + + /** + * @param source The (client) source of the incoming connection + * @param serverConn The {@link QuicServerConnection}, constructed on the server side, + * representing in the incoming connection + * {@return true if the incoming connection should be accepted. false otherwise} + */ + default boolean acceptIncomingConnection(final SocketAddress source, + final QuicServerConnection serverConn) { + // by default accept new connections + return true; + } + + /** + * Called whenever a client initiated bidirectional stream has been received on the + * Quic connection which this {@code QuicServerHandler} previously + * {@link #acceptIncomingConnection(SocketAddress, QuicServerConnection) accepted} + * + * @param conn The connection on which the stream was created + * @param bidi The client initiated bidirectional stream + * @throws IOException + */ + default void onClientInitiatedBidiStream(final QuicServerConnection conn, + final QuicBidiStream bidi) throws IOException { + // start the reader/writer loops for this stream, by creating a ConnectedBidiStream + try (final ConnectedBidiStream connectedBidiStream = new ConnectedBidiStream(bidi)) { + // let the handler use this connected stream to read/write data, if it wants to + this.handleBidiStream(conn, connectedBidiStream); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(e); + } + + } + + /** + * Called whenever a client initiated bidirectional stream has been received on the connection + * previously accepted by this handler. This method is called from within + * {@link #onClientInitiatedBidiStream(QuicBidiStream)} with a {@code ConnectedBidiStream} which + * has the reader and writer tasks started. + * + * @param conn The connection on which the stream was created + * @param bidiStream The bidirectional stream which has the reader and writer tasks started + * @throws IOException + */ + void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException; + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicStandaloneServer.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicStandaloneServer.java new file mode 100644 index 00000000000..b1697064556 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/QuicStandaloneServer.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongFunction; + +import javax.net.ssl.SNIMatcher; +import javax.net.ssl.SSLContext; + +import jdk.internal.net.http.common.Logger; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; + +public final class QuicStandaloneServer extends QuicServer { + public static final String ALPN = "quic-standalone-test-alpn"; + + private static final AtomicLong IDS = new AtomicLong(); + + private final Logger debug = Utils.getDebugLogger(this::name); + private QuicServerHandler handler; + + private static String nextName() { + return "quic-standalone-server-" + IDS.incrementAndGet(); + } + + QuicStandaloneServer(String serverId, InetSocketAddress bindAddress, + ExecutorService executor, QuicVersion[] supportedQuicVersions, + boolean compatible, QuicTLSContext quicTLSContext, SNIMatcher sniMatcher, + DatagramDeliveryPolicy incomingDeliveryPolicy, + DatagramDeliveryPolicy outgoingDeliveryPolicy, String alpn, + LongFunction appErrorCodeToString) { + super(serverId, bindAddress, executor, supportedQuicVersions, compatible, quicTLSContext, sniMatcher, + incomingDeliveryPolicy, outgoingDeliveryPolicy, alpn, appErrorCodeToString); + // set a connection acceptor + setConnectionAcceptor(QuicStandaloneServer::acceptIncoming); + } + + public void addHandler(final QuicServerHandler handler) { + this.handler = handler; + } + + QuicServerHandler getHandler() { + return this.handler; + } + + static boolean acceptIncoming(final SocketAddress source, final QuicServerConnection serverConn) { + try { + final QuicStandaloneServer server = (QuicStandaloneServer) serverConn.quicInstance(); + final QuicServerHandler handler = server.getHandler(); + if (handler == null) { + if (server.debug.on()) { + server.debug.log("Handler absent - rejecting new connection " + + serverConn + " from source " + source); + } + return false; + } + if (!handler.acceptIncomingConnection(source, serverConn)) { + if (server.debug.on()) { + server.debug.log("Handler " + handler + " rejected new connection " + + serverConn + " from source " + source); + } + return false; + } + if (server.debug.on()) { + server.debug.log("New connection " + serverConn + " accepted from " + source); + } + serverConn.onSuccessfulHandshake(() -> { + if (server.debug.on()) { + server.debug.log("Registering a listener for remote streams on connection " + + serverConn); + } + // add a listener for streams that have been created by the remote side + // (i.e. initiated by the client) + serverConn.addRemoteStreamListener((stream) -> { + if (stream.isBidirectional()) { + // invoke the handler (application code) as a async work + server.asyncHandleBidiStream(source, serverConn, (QuicBidiStream) stream); + return true; // true implies that this listener wishes to acquire the stream + } else { + if (server.debug.on()) { + server.debug.log("Ignoring stream " + stream + " on connection " + serverConn); + } + return false; + } + }); + }); + return true; // true implies we wish to accept this incoming connection + } catch (Throwable t) { + // TODO: re-evaluate why this try/catch block is there. it's likely + // that in the absence of this block, the call just "disappears"/hangs when an exception + // occurs in this method + System.err.println("Exception while accepting incoming connection: " + t.getMessage()); + t.printStackTrace(); + return false; + } + } + + private void asyncHandleBidiStream(final SocketAddress source, final QuicServerConnection serverConn, + final QuicBidiStream stream) { + this.executor().execute(() -> { + try { + if (debug.on()) { + debug.log("Invoking handler " + handler + " for handling bidi stream " + + stream + " on connection " + serverConn); + } + handler.onClientInitiatedBidiStream(serverConn, stream); + } catch (Throwable t) { + System.err.println("Failed to handle client initiated" + + " bidi stream for connection " + serverConn + + " from source " + source); + t.printStackTrace(); + } + }); + } + + public static Builder newBuilder() { + return new StandaloneBuilder(); + } + + private static final class StandaloneBuilder extends Builder { + + public StandaloneBuilder() { + this.alpn = ALPN; + } + + @Override + public QuicStandaloneServer build() throws IOException { + QuicVersion[] versions = availableQuicVersions; + if (versions == null) { + // default to v1 and v2 + versions = new QuicVersion[]{QuicVersion.QUIC_V1, QuicVersion.QUIC_V2}; + } + if (versions.length == 0) { + throw new IllegalStateException("Empty supported QUIC versions"); + } + InetSocketAddress addr = bindAddress; + if (addr == null) { + // default to loopback address and ephemeral port + addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + } + SSLContext ctx = sslContext; + if (ctx == null) { + try { + ctx = SSLContext.getDefault(); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } + } + final QuicTLSContext quicTLSContext = new QuicTLSContext(ctx); + final String name = serverId == null ? nextName() : serverId; + return new QuicStandaloneServer(name, addr, executor, versions, compatible, quicTLSContext, + sniMatcher, incomingDeliveryPolicy, outgoingDeliveryPolicy, alpn, appErrorCodeToString); + } + } + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/RetryCodingContext.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/RetryCodingContext.java new file mode 100644 index 00000000000..c627ed52fab --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/quic/RetryCodingContext.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.httpclient.test.lib.quic; + +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class RetryCodingContext implements CodingContext { + private final QuicConnectionId connectionId; + private final QuicTLSEngine engine; + + public RetryCodingContext(QuicConnectionId connectionId, QuicTLSContext quicTLSContext) { + this.connectionId = connectionId; + engine = quicTLSContext.createEngine(); + } + + @Override + public long largestProcessedPN(QuicPacket.PacketNumberSpace packetSpace) { + throw new UnsupportedOperationException(); + } + + @Override + public long largestAckedPN(QuicPacket.PacketNumberSpace packetSpace) { + throw new UnsupportedOperationException(); + } + + @Override + public int connectionIdLength() { + throw new UnsupportedOperationException(); + } + + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public QuicConnectionId originalServerConnId() { + return connectionId; + } + + @Override + public QuicTLSEngine getTLSEngine() { + return engine; + } + + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + throw new UnsupportedOperationException(); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/BlockingDecodingTest.java b/test/jdk/java/net/httpclient/qpack/BlockingDecodingTest.java new file mode 100644 index 00000000000..c588f8117ae --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/BlockingDecodingTest.java @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertNotEquals; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA BlockingDecodingTest + */ + + +public class BlockingDecodingTest { + @Test + public void blockedStreamsSettingDefaultValueTest() throws Exception { + // Default SETTINGS_QPACK_BLOCKED_STREAMS value (0) doesn't allow blocked streams + var encoderEh = new TestErrorHandler(); + var decoderEh = new TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair pair = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, + streamError, -1L, 1); + + // Get Encoder and Decoder instances from a newly established connector + var encoder = pair.encoder(); + var decoder = pair.decoder(); + + // Create a decoding callback to check for completion and to log failures + TestDecodingCallback decodingCallback = new TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + var header = TestHeader.withId(0); + encoder.header(context, header.name(), header.value(), + false, IGNORE_RECEIVED_COUNT_CHECK); + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + + // It is expected to get QPACK_DECOMPRESSION_FAILED here since decoder is + // expected to be blocked due to missing entry with index 0 in the decoder table, + // and the default number of blocked streams (0) will be exceeded (1). + var lastHttp3Error = decodingCallback.lastHttp3Error.get(); + System.err.println("Last Http3Error: " + lastHttp3Error); + Assert.assertEquals(lastHttp3Error, Http3Error.QPACK_DECOMPRESSION_FAILED); + Assert.assertFalse(decodingCallback.completed.isDone()); + } + + @Test + public void noBlockedStreamsTest() throws Exception { + // No blocked streams - with default SETTINGS_QPACK_BLOCKED_STREAMS value + // Default SETTINGS_QPACK_BLOCKED_STREAMS value (0) doesn't allow blocked streams + var encoderEh = new TestErrorHandler(); + var decoderEh = new TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair pair = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, + streamError, -1L, 1); + + // Populate decoder table with an entry - there should be no blocked streams + // observed during decoding + prepopulateDynamicTable(pair.decoderTable(), 1); + + // Get Encoder and Decoder instances from a newly established connector + var encoder = pair.encoder(); + var decoder = pair.decoder(); + + // Create a decoding callback to check for completion and to log failures + TestDecodingCallback decodingCallback = new TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + var expectedHeader = TestHeader.withId(0); + encoder.header(context, expectedHeader.name, + expectedHeader.value, false, IGNORE_RECEIVED_COUNT_CHECK); + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + + // It is expected to get QPACK_DECOMPRESSION_FAILED here since decoder is + // expected to be blocked due to missing entry with index 0 in the decoder table, + // and the default number of blocked streams (0) will be exceeded (1). + var lastHttp3Error = decodingCallback.lastHttp3Error.get(); + System.err.println("Last Http3Error: " + lastHttp3Error); + Assert.assertNull(lastHttp3Error); + Assert.assertNull(decodingCallback.lastThrowable.get()); + Assert.assertTrue(decodingCallback.completed.isDone()); + // Check that onDecoded was called for the test entry + var decodedHeader = decodingCallback.decodedHeaders.get(0); + Assert.assertEquals(decodedHeader, expectedHeader); + } + + @Test + public void awaitBlockedStreamsTest() throws Exception { + // Max number of blocked streams is not exceeded + // No blocked streams - with default SETTINGS_QPACK_BLOCKED_STREAMS value + // Default SETTINGS_QPACK_BLOCKED_STREAMS value (0) doesn't allow blocked streams + final int numberOfMaxAllowedBlockedStreams = 5; + final int numberOfHeaders = 4; + final int base = 2; + var encoderEh = new TestErrorHandler(); + var decoderEh = new TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair pair = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, + streamError, numberOfMaxAllowedBlockedStreams, numberOfHeaders); + + // Create list of headers to encode for each thread + List expectedHeaders = Collections.synchronizedList(new ArrayList<>()); + for (int headerId = 0; headerId < numberOfHeaders; headerId++) { + expectedHeaders.add(TestHeader.withId(headerId)); + } + + // Create virtual threads executor + var vtExecutor = Executors.newVirtualThreadPerTaskExecutor(); + List> decodingTaskResults = new ArrayList<>(); + + // Create 10 blocked tasks + for (int taskCount = 0; taskCount < numberOfMaxAllowedBlockedStreams; taskCount++) { + var decodingTask = new Callable() { + final EncoderDecoderConnector.EncoderDecoderPair ed = pair; + @Override + public TestDecodingCallback call() throws Exception { + var encoder = ed.encoder(); + var decoder = ed.decoder(); + // Create a decoding callback to check for completion and to log failures + TestDecodingCallback decodingCallback = new TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = + encoder.newEncodingContext(0, base, headerFrameWriter); + + for (var header : expectedHeaders) { + encoder.header(context, header.name, header.value, false, + IGNORE_RECEIVED_COUNT_CHECK); + headerFrameWriter.write(headersBb); + } + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + return decodingCallback; + } + }; + decodingTaskResults.add(vtExecutor.submit(decodingTask)); + } + + // Schedule the delayed update to the decoders dynamic table + var delayedExecutor = CompletableFuture.delayedExecutor(100, TimeUnit.MILLISECONDS, + vtExecutor); + AtomicLong updateDoneTimestamp = new AtomicLong(); + delayedExecutor.execute(() -> { + updateDoneTimestamp.set(System.nanoTime()); + prepopulateDynamicTable(pair.decoderTable(), numberOfHeaders); + }); + + // Await completion of all tasks + for (var decodingResultFuture : decodingTaskResults) { + decodingResultFuture.get().completed.get(); + } + // Acquire the timestamp + long updateDoneTimeStamp = updateDoneTimestamp.get(); + + System.err.println("All decoding tasks are done"); + System.err.println("Decoder table update timestamp: " + updateDoneTimeStamp); + // Check results of each decoding task + for (var decodingResultFuture : decodingTaskResults) { + var taskCallback = decodingResultFuture.get(); + Assert.assertNull(taskCallback.lastHttp3Error.get()); + Assert.assertNull(taskCallback.lastThrowable.get()); + long decodingTaskCompleted = taskCallback.completedTimestamp.get(); + System.err.println("Decoding task completion timestamp: " + decodingTaskCompleted); + Assert.assertTrue(decodingTaskCompleted >= updateDoneTimeStamp); + var decodedHeaders = taskCallback.decodedHeaders; + Assert.assertEquals(decodedHeaders, expectedHeaders); + } + } + + private static EncoderDecoderConnector.EncoderDecoderPair newPreconfiguredEncoderDecoder( + TestErrorHandler encoderEh, + TestErrorHandler decoderEh, + AtomicReference streamError, + long maxBlockedStreams, + int numberOfEntriesInEncoderDT) { + EncoderDecoderConnector conn = new EncoderDecoderConnector(); + var pair = conn.newEncoderDecoderPair( + e -> false, + encoderEh::qpackErrorHandler, + decoderEh::qpackErrorHandler, + streamError::set); + // Create settings frame with dynamic table capacity and number of blocked streams + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + // 4k should be enough for storing dynamic table entries added by 'prepopulateDynamicTable' + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DT_CAPACITY); + if (maxBlockedStreams > 0) { + // Set max number of blocked decoder streams if the provided value is positive, otherwise + // use the default RFC setting which is 0 + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS, maxBlockedStreams); + } + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder with constructed ConnectionSettings + pair.encoder().configure(settings); + pair.decoder().configure(settings); + pair.encoderTable().setCapacity(DT_CAPACITY); + pair.decoderTable().setCapacity(DT_CAPACITY); + + // Prepopulate encoder dynamic table with test entries. Decoder dynamic table will be pre-populated with + // a test-case specific code to reproduce blocked decoding scenario + prepopulateDynamicTable(pair.encoderTable(), numberOfEntriesInEncoderDT); + + return pair; + } + + private static void prepopulateDynamicTable(DynamicTable dynamicTable, int numEntries) { + for (int count = 0; count < numEntries; count++) { + var header = TestHeader.withId(count); + dynamicTable.insert(header.name(), header.value()); + } + } + + private static class TestDecodingCallback implements DecodingCallback { + + final List decodedHeaders = new CopyOnWriteArrayList<>(); + final CompletableFuture completed = new CompletableFuture<>(); + final AtomicLong completedTimestamp = new AtomicLong(); + + final AtomicReference lastThrowable = new AtomicReference<>(); + final AtomicReference lastHttp3Error = new AtomicReference<>(); + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + var nameValue = new TestHeader(name.toString(), value.toString()); + decodedHeaders.add(nameValue); + System.err.println("Decoding callback 'onDecoded': " + nameValue); + } + + @Override + public void onComplete() { + System.err.println("Decoding callback 'onComplete'"); + completedTimestamp.set(System.nanoTime()); + completed.complete(null); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + System.err.println("Decoding callback 'onError': " + http3Error); + lastThrowable.set(throwable); + lastHttp3Error.set(http3Error); + } + + @Override + public long streamId() { + return 0; + } + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + throw new RuntimeException("http3 error: " + http3Error, error); + } + } + + record TestHeader(String name, String value) { + public static TestHeader withId(int id) { + return new TestHeader(NAME + id, VALUE + id); + } + } + + private static final String NAME = "test"; + private static final String VALUE = "valueTest"; + private static final long DT_CAPACITY = 4096L; + private static final long IGNORE_RECEIVED_COUNT_CHECK = -1L; +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderInstructionsReaderTest.java b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsReaderTest.java new file mode 100644 index 00000000000..e231d0dded3 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsReaderTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key randomness + * @library /test/lib + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL DecoderInstructionsReaderTest + */ + +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.DecoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.RepeatedTest; + +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DecoderInstructionsReaderTest { + + DecoderInstructionsReader decoderInstructionsReader; + private static final Random RANDOM = RandomFactory.getRandom(); + + @RepeatedTest(10) + public void acknowledgementTest() { + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Stream ID (7+) | + // +---+---------------------------+ + + TestDecoderInstructionsCallback callback = new TestDecoderInstructionsCallback(); + decoderInstructionsReader = new DecoderInstructionsReader(callback, QPACK.getLogger()); + + long streamId = RANDOM.nextLong(0, IntegerReader.QPACK_MAX_INTEGER_VALUE); + IntegerWriter writer = new IntegerWriter(); + int bufferSize = requiredBufferSize(7, streamId); + var payload = 0b1000_0000; + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + writer.configure(streamId, 7, payload); + writer.write(byteBuffer); + byteBuffer.flip(); + + decoderInstructionsReader.read(byteBuffer); + assertEquals(streamId, callback.lastSectionAckStreamId.get()); + } + + @RepeatedTest(10) + public void cancellationTest() { + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Stream ID (6+) | + // +---+---+-----------------------+ + + TestDecoderInstructionsCallback callback = new TestDecoderInstructionsCallback(); + decoderInstructionsReader = new DecoderInstructionsReader(callback, QPACK.getLogger()); + + long streamId = RANDOM.nextLong(0, IntegerReader.QPACK_MAX_INTEGER_VALUE); + IntegerWriter writer = new IntegerWriter(); + int bufferSize = requiredBufferSize(6, streamId); + var payload = 0b0100_0000; + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + writer.configure(streamId, 6, payload); + writer.write(byteBuffer); + byteBuffer.flip(); + + decoderInstructionsReader.read(byteBuffer); + assertEquals(streamId, callback.lastCancelStreamId.get()); + } + + @RepeatedTest(10) + public void incrementTest() { + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | Increment (6+) | + // +---+---+-----------------------+ + + TestDecoderInstructionsCallback callback = new TestDecoderInstructionsCallback(); + decoderInstructionsReader = new DecoderInstructionsReader(callback, QPACK.getLogger()); + + long increaseCountInc = RANDOM.nextLong(0, IntegerReader.QPACK_MAX_INTEGER_VALUE); + IntegerWriter writer = new IntegerWriter(); + int bufferSize = requiredBufferSize(6, increaseCountInc); + var payload = 0b0000_0000; + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + writer.configure(increaseCountInc, 6, payload); + writer.write(byteBuffer); + byteBuffer.flip(); + + decoderInstructionsReader.read(byteBuffer); + assertEquals(increaseCountInc, callback.lastInsertCountInc.get()); + + } + + static int requiredBufferSize(int N, long value) { + checkPrefix(N); + int size = 1; + int max = (2 << (N - 1)) - 1; + if (value < max) { + return size; + } + size++; + value -= max; + while (value >= 128) { + value /= 128; + size++; + } + return size; + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + private static class TestDecoderInstructionsCallback implements DecoderInstructionsReader.Callback { + final AtomicLong lastSectionAckStreamId = new AtomicLong(-1L); + final AtomicLong lastCancelStreamId = new AtomicLong(-1L); + final AtomicLong lastInsertCountInc = new AtomicLong(-1L); + + @Override + public void onSectionAck(long streamId) { + lastSectionAckStreamId.set(streamId); + } + + @Override + public void onStreamCancel(long streamId) { + lastCancelStreamId.set(streamId); + } + + @Override + public void onInsertCountIncrement(long increment) { + lastInsertCountInc.set(increment); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderInstructionsWriterTest.java b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsWriterTest.java new file mode 100644 index 00000000000..275dec34927 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderInstructionsWriterTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL DecoderInstructionsWriterTest + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.readers.DecoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.DecoderInstructionsWriter; +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class DecoderInstructionsWriterTest { + + @ParameterizedTest + @MethodSource("decoderInstructionsSource") + public void decoderInstructionsTest(DecoderInstruction instruction, long value) throws Exception { + testRunner(instruction, value); + } + + private static Stream decoderInstructionsSource() { + // "Section Acknowledgment" + Stream sectionAck = RANDOM.longs(10, + 0, IntegerReader.QPACK_MAX_INTEGER_VALUE) + .boxed() + .map(l -> Arguments.of(DecoderInstruction.SECTION_ACK, l)); + + // "Stream Cancellation" + Stream streamCancel = RANDOM.longs(10, + 0, IntegerReader.QPACK_MAX_INTEGER_VALUE) + .boxed() + .map(l -> Arguments.of(DecoderInstruction.STREAM_CANCEL, l)); + + // "Insert Count Increment" + Stream insertCountInc = RANDOM.longs(10, + 0, IntegerReader.QPACK_MAX_INTEGER_VALUE) + .boxed() + .map(l -> Arguments.of(DecoderInstruction.INSERT_COUNT_INC, l)); + + return Stream.concat(sectionAck, Stream.concat(streamCancel, insertCountInc)); + } + + + enum DecoderInstruction { + SECTION_ACK, + STREAM_CANCEL, + INSERT_COUNT_INC; + } + + private static void testRunner(DecoderInstruction instruction, long value) throws Exception { + var writer = new DecoderInstructionsWriter(); + int calculatedInstructionSize = configureWriter(writer, instruction, value); + var logger = QPACK.getLogger(); + var dynamicTable = new DynamicTable(logger); + + var buffers = new ArrayList(); + + boolean writeDone = false; + int writtenBytes = 0; + // Write instruction to a byte buffers of a random size + while (!writeDone) { + int allocSize = RANDOM.nextInt(1, 9); + var buffer = ByteBuffer.allocate(allocSize); + + writeDone = writer.write(buffer); + writtenBytes += buffer.position(); + buffer.flip(); + buffers.add(buffer); + } + // Check that instruction size calculated by the writer matches + // the number of written bytes + assertEquals(writtenBytes, calculatedInstructionSize); + + // Read back the data from byte buffers + var callback = new TestDecoderInstructionsCallback(); + var reader = new DecoderInstructionsReader(callback, logger); + for (var bb : buffers) { + reader.read(bb); + } + // Check that reader callback values match values supplied to the writer + long instructionValue = extractCallbackValue(instruction, callback); + assertEquals(value, instructionValue); + } + + private static long extractCallbackValue(DecoderInstruction instruction, + TestDecoderInstructionsCallback callback) { + return switch (instruction) { + case SECTION_ACK -> callback.lastSectionAckStreamId.get(); + case STREAM_CANCEL -> callback.lastCancelStreamId.get(); + case INSERT_COUNT_INC -> callback.lastInsertCountInc.get(); + }; + } + + + private static int configureWriter(DecoderInstructionsWriter writer, + DecoderInstruction instruction, + long instructionValue) { + return switch (instruction) { + case SECTION_ACK -> writer.configureForSectionAck(instructionValue); + case STREAM_CANCEL -> writer.configureForStreamCancel(instructionValue); + case INSERT_COUNT_INC -> writer.configureForInsertCountInc(instructionValue); + }; + } + + private static final Random RANDOM = RandomFactory.getRandom(); + + private static class TestDecoderInstructionsCallback implements DecoderInstructionsReader.Callback { + final AtomicLong lastSectionAckStreamId = new AtomicLong(-1L); + final AtomicLong lastCancelStreamId = new AtomicLong(-1L); + final AtomicLong lastInsertCountInc = new AtomicLong(-1L); + + @Override + public void onSectionAck(long streamId) { + lastSectionAckStreamId.set(streamId); + } + + @Override + public void onStreamCancel(long streamId) { + lastCancelStreamId.set(streamId); + } + + @Override + public void onInsertCountIncrement(long increment) { + lastInsertCountInc.set(increment); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderSectionSizeLimitTest.java b/test/jdk/java/net/httpclient/qpack/DecoderSectionSizeLimitTest.java new file mode 100644 index 00000000000..4b715a46e08 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderSectionSizeLimitTest.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL + * DecoderSectionSizeLimitTest + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import jdk.test.lib.RandomFactory; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; + +public class DecoderSectionSizeLimitTest { + @Test(dataProvider = "headerSequences") + public void fieldSectionSizeLimitExceeded(List headersSequence, + long maxFieldSectionSize) { + + boolean decoderErrorExpected = + maxFieldSectionSize > 0 && maxFieldSectionSize < REQUIRED_FIELD_SECTION_SIZE; + + System.err.println("=".repeat(50)); + System.err.println("Max Field Section Size = " + maxFieldSectionSize); + System.err.println("Max Field Section Size is" + (decoderErrorExpected ? " not" : "") + + " enough to encode headers"); + + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + DecoderSectionSizeLimitTest.TestErrorHandler encoderErrorHandler = + new DecoderSectionSizeLimitTest.TestErrorHandler(); + DecoderSectionSizeLimitTest.TestErrorHandler decoderErrorHandler = + new DecoderSectionSizeLimitTest.TestErrorHandler(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> false, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // This test emulates a scenario with an Encoder that doesn't respect + // the SETTINGS_MAX_FIELD_SECTION_SIZE setting value while encoding the headers frame + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 512L); + settingsFrame.setParameter(SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, maxFieldSectionSize); + ConnectionSettings decoderConnectionSetting = ConnectionSettings.createFrom(settingsFrame); + + // Encoder imposes no limit on the field section size + settingsFrame.setParameter(SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, -1L); + ConnectionSettings encoderConnectionSetting = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder + encoder.configure(encoderConnectionSetting); + decoder.configure(decoderConnectionSetting); + + // Configure dynamic tables + configureDynamicTable(conn.encoderTable()); + configureDynamicTable(conn.decoderTable()); + + // Encode headers + // Create header frame writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, BASE, headerFrameWriter); + + ByteBuffer buffer = ByteBuffer.allocate(RANDOM.nextInt(1, 65)); + List buffers = new ArrayList<>(); + for (TestHeader header : headersSequence) { + // Configures encoder for writing the header name:value pair + encoder.header(context, header.name, header.value, + false, -1L); + + // Write the header + while (!headerFrameWriter.write(buffer)) { + buffer.flip(); + buffers.add(buffer); + buffer = ByteBuffer.allocate(RANDOM.nextInt(1, 65)); + } + } + buffer.flip(); + buffers.add(buffer); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + System.err.println("Number of generated header buffers:" + buffers.size()); + + // Decode header buffers and check if expected HTTP/3 error is reported + // via decoding callback + var decodingCallback = new TestDecodingCallback(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + for (int bufferIdx = 0; bufferIdx < buffers.size(); bufferIdx++) { + headerFrameReader.read(buffers.get(bufferIdx), + bufferIdx == buffers.size() - 1); + Http3Error decodingError = decodingCallback.lastHttp3Error.get(); + if (decodingError != null) { + System.err.printf("Decoding error observed during buffer #%d processing: %s throwable: %s%n", + bufferIdx, decodingError, decodingCallback.lastThrowable.get()); + if (decoderErrorExpected) { + Assert.assertEquals(decodingError, Http3Error.QPACK_DECOMPRESSION_FAILED); + return; + } else { + Assert.fail("No HTTP/3 error was expected"); + } + } else { + System.err.println("Buffer #" + bufferIdx + " readout completed without errors"); + } + } + if (decoderErrorExpected) { + Assert.fail("HTTP/3 error was expected but was not observed"); + } + } + + @DataProvider + public Object[][] headerSequences() { + List testCases = new ArrayList<>(); + for (var sequence : generateHeaderSequences()) { + // Decoding should complete without failure + testCases.add(new Object[]{sequence, -1L}); + // No failure since it is enough bytes specified in the SETTINGS_MAX_FIELD_SECTION_SIZE + // setting value + testCases.add(new Object[]{sequence, REQUIRED_FIELD_SECTION_SIZE}); + // Failure is expected - not enough bytes specified in the SETTINGS_MAX_FIELD_SECTION_SIZE + // setting value + testCases.add(new Object[]{sequence, REQUIRED_FIELD_SECTION_SIZE - 1}); + } + return testCases.toArray(Object[][]::new); + } + + private static List> generateHeaderSequences() { + List> headersSequences = new ArrayList<>(); + headersSequences.add(TEST_HEADERS); + // startIndex == 0 - the TEST_HEADERS sequence that is already + // added to the sequences list + for (int startIndex = 1; startIndex < TEST_HEADERS.size(); startIndex++) { + List firstPart = TEST_HEADERS.subList(startIndex, TEST_HEADERS.size()); + List secondPart = TEST_HEADERS.subList(0, startIndex); + List sequence = new ArrayList<>(); + sequence.addAll(firstPart); + sequence.addAll(secondPart); + headersSequences.add(sequence); + } + return headersSequences; + } + + record TestHeader(String name, String value, long size) { + public TestHeader(String name, String value) { + this(name, value, name.length() + value.length() + 32L); + } + } + + private static void configureDynamicTable(DynamicTable table) { + table.setCapacity(512L); + table.insert(NAME_IN_TABLE, VALUE_IN_TABLE); + table.insert(NAME_IN_TABLE_POSTBASE, VALUE_IN_TABLE_POSTBASE); + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + } + } + + private static class TestDecodingCallback implements DecodingCallback { + + final AtomicReference lastHttp3Error = new AtomicReference<>(); + final AtomicReference lastThrowable = new AtomicReference<>(); + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + lastHttp3Error.set(http3Error); + lastThrowable.set(throwable); + } + + @Override + public long streamId() { + return 0; + } + } + + private static final String NAME_IN_TABLE = "HEADER_NAME_FROM_TABLE"; + private static final String VALUE_IN_TABLE = "HEADER_VALUE_FROM_TABLE"; + private static final String NAME_IN_TABLE_POSTBASE = "HEADER_NAME_FROM_TABLE_POSTBASE"; + private static final String VALUE_IN_TABLE_POSTBASE = "HEADER_VALUE_FROM_TABLE_POSTBASE"; + private static final String NAME_NOT_IN_TABLE = "NAME_NOT_IN_TABLE"; + private static final String VALUE_NOT_IN_TABLE = "VALUE_NOT_IN_TABLE"; + + private static List TEST_HEADERS = List.of( + // Relative index + new TestHeader(NAME_IN_TABLE, VALUE_IN_TABLE), + // Relative name index + new TestHeader(NAME_IN_TABLE, VALUE_NOT_IN_TABLE), + // Post-base index + new TestHeader(NAME_IN_TABLE_POSTBASE, VALUE_IN_TABLE_POSTBASE), + // Post-base name index + new TestHeader(NAME_IN_TABLE_POSTBASE, VALUE_NOT_IN_TABLE), + // Literal + new TestHeader(NAME_NOT_IN_TABLE, VALUE_NOT_IN_TABLE) + ); + + private static long REQUIRED_FIELD_SECTION_SIZE = TEST_HEADERS.stream() + .mapToLong(TestHeader::size) + .sum(); + private static final long BASE = 1L; + private static final Random RANDOM = RandomFactory.getRandom(); +} diff --git a/test/jdk/java/net/httpclient/qpack/DecoderTest.java b/test/jdk/java/net/httpclient/qpack/DecoderTest.java new file mode 100644 index 00000000000..37f87bdda59 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DecoderTest.java @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.qpack.writers.StringWriter; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static org.testng.Assert.*; + +/* + * @test + * @modules java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng/othervm DecoderTest + */ +public class DecoderTest { + + private final Random random = new Random(); + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter stringWriter = new StringWriter(); + + record DecoderWithReader(Decoder decoder, HeaderFrameReader reader) { + } + + private static DecoderWithReader newDecoderWithReader(DecodingCallback decodingCallback) throws IOException { + var decoder = new Decoder(DecoderTest::createDecoderStreams, DecoderTest::qpackErrorHandler); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + // Supply byte buffer with two bytes of zeroed Field Section Prefix + ByteBuffer prefix = ByteBuffer.allocate(2); + decoder.decodeHeader(prefix, true, headerFrameReader); + // Return record with decoder/reader tuple + return new DecoderWithReader(decoder, headerFrameReader); + } + + private static final int TEST_STR_MAX_LENGTH = 10; + + static QueuingStreamPair createDecoderStreams(Consumer receiver) { + return null; + } + + @DataProvider(name = "indexProvider") + public Object[][] indexProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(headerField -> List.of(tableIndex.getAndIncrement(), headerField)) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider(name = "nameReferenceProvider") + public Object[][] nameReferenceProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(h -> List.of(tableIndex.getAndIncrement(), h.name(), randomString())) + .map(List::toArray).toArray(Object[][]::new); + } + + @DataProvider(name = "literalProvider") + public Object[][] literalProvider() { + var output = new String[100][]; + for (int i = 0; i < 100; i++) { + output[i] = new String[]{ randomString(), randomString() }; + } + return output; + } + + @Test(dataProvider = "indexProvider") + public void testIndexedOnStaticTable(int index, HeaderField h) throws IOException { + var actual = writeIndex(index); + var callback = new TestingCallBack(index, h.name(), h.value()); + var dr = newDecoderWithReader(callback); + dr.decoder().decodeHeader(actual, true, dr.reader()); + } + + @Test(dataProvider = "nameReferenceProvider") + public void testLiteralWithNameReferenceOnStaticTable(int index, String name, String value) throws IOException { + boolean sensitive = random.nextBoolean(); + + var actual = writeNameRef(index, sensitive, value); + var callback = new TestingCallBack(index, sensitive, name, value); + var dr = newDecoderWithReader(callback); + dr.decoder().decodeHeader(actual, true, dr.reader()); + } + + @Test(dataProvider = "literalProvider") + public void testLiteralWithLiteralNameOnStaticTable(String name, String value) throws IOException { + boolean sensitive = random.nextBoolean(); + + var actual = writeLiteral(sensitive, name, value); + var callback = new TestingCallBack(sensitive, name, value); + var dr = newDecoderWithReader(callback); + dr.decoder().decodeHeader(actual, true, dr.reader()); + } + + @Test + public void stateCheckSingle() throws IOException { + boolean sensitive = random.nextBoolean(); + var name = "foo"; + var value = "bar"; + + var bb = writeLiteral(sensitive, name, value); + var callback = new TestingCallBack(sensitive, name, value); + + var dr = newDecoderWithReader(callback); + int len = bb.capacity(); + for (int i = 0; i < len; i++) { + var b = ByteBuffer.wrap(new byte[]{ bb.get() }); + dr.decoder().decodeHeader(b, (i == len - 1), dr.reader()); + } + } + + /* Test Methods */ + private void debug(ByteBuffer bb, String msg, boolean verbose) { + if (verbose) { + System.out.printf("DEBUG[%s]: pos=%d, limit=%d, remaining=%d\n", + msg, bb.position(), bb.limit(), bb.remaining()); + } + System.out.printf("DEBUG[%s]: ", msg); + for (byte b : bb.array()) { + System.out.printf("(%s,%d) ", Integer.toBinaryString(b & 0xFF), (int)(b & 0xFF)); + } + System.out.print("\n"); + } + + private ByteBuffer writeIndex(int index) { + int N = 6; + int payload = 0b1100_0000; // static table = true + var bb = ByteBuffer.allocate(2); + + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer writeNameRef(int index, boolean sensitive, String value) { + int N = 4; + int payload = 0b0101_0000; // static table = true + if (sensitive) + payload |= 0b0010_0000; + var bb = allocateNameRefBuffer(N, index, value); + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + boolean huffman = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffman) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffman); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer writeLiteral(boolean sensitive, String name, String value) { + int N = 3; + //boolean hasInSt = Sta; + int payload = 0b0010_0000; // static table = true + var bb = allocateLiteralBuffer(N, name, value); + + if (sensitive) + payload |= 0b0001_0000; + boolean huffmanName = QuickHuffman.isHuffmanBetterFor(name); + if (huffmanName) + payload |= 0b0000_1000; + stringWriter.configure(name, N, payload, huffmanName); + stringWriter.write(bb); + stringWriter.reset(); + + boolean huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffmanValue) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffmanValue); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer allocateIndexBuffer(int index) { + /* + * Note on Integer Representation used for storing the length of name and value strings. + * Taken from RFC 7541 Section 5.1 + * + * "An integer is represented in two parts: a prefix that fills the current octet and an + * optional list of octets that are used if the integer value does not fit within the + * prefix. The number of bits of the prefix (called N) is a parameter of the integer + * representation. If the integer value is small enough, i.e., strictly less than 2N-1, it + * is encoded within the N-bit prefix. + * + * ... + * + * Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2N-1, is + * encoded using a list of one or more octets. The most significant bit of each octet is + * used as a continuation flag: its value is set to 1 except for the last octet in the list. + * The remaining bits of the octets are used to encode the decreased value." + * + * Use "null" for name, if name isn't being provided (i.e. for a nameRef); otherwise, buffer + * will be too large. + * + */ + int N = 6; // bits available in first byte + int size = 1; + index -= Math.pow(2, N) - 1; // number that you can store in first N bits + while (index >= 0) { + index -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateNameRefBuffer(int N, int index, CharSequence value) { + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = 1 + vlen; + + index -= Math.pow(2, N) - 1; + while (index >= 0) { + index -= 127; + size++; + } + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateLiteralBuffer(int N, CharSequence name, CharSequence value) { + int nlen = Math.min(QuickHuffman.lengthOf(name), name.length()); + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = nlen + vlen; + + nlen -= Math.pow(2, N) - 1; + size++; + while (nlen >= 0) { + nlen -= 127; + size++; + } + + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum."""; + + private String randomString() { + int lower = random.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + /** + * The empty string ("") is a valid value String in the static table and the random + * String returned cannot refer to a entry in the table. Therefore, we set the upper + * bound below to a minimum of 1. + */ + return LOREM.substring(lower, 1 + lower + random.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestingCallBack implements DecodingCallback { + final int index; + final boolean huffmanName, huffmanValue; + final boolean sensitive; + final String name, value; + + // Indexed + TestingCallBack(int index, String name, String value) { + this(index, false, name, value); + } + // Literal w/Literal Name + TestingCallBack(boolean sensitive, String name, String value) { + this(-1, sensitive, name, value); + } + // Literal w/Name Reference + TestingCallBack(int index, boolean sensitive, String name, String value) { + this.index = index; + this.sensitive = sensitive; + this.name = name; + this.value = value; + this.huffmanName = QuickHuffman.isHuffmanBetterFor(name); + this.huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + } + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + fail("should not be called"); + } + + @Override + public void onIndexed(long index, CharSequence name, CharSequence value) { + assertEquals(this.index, index); + assertEquals(this.name, name.toString()); + assertEquals(this.value, value.toString()); + } + + @Override + public void onLiteralWithNameReference(long index, CharSequence name, + CharSequence value, boolean huffmanValue, + boolean sensitive) { + assertEquals(this.index, index); + assertEquals(this.value, value.toString()); + assertEquals(this.huffmanValue, huffmanValue); + assertEquals(this.sensitive, sensitive); + } + + @Override + public void onLiteralWithLiteralName(CharSequence name, boolean huffmanName, + CharSequence value, boolean huffmanValue, + boolean sensitive) { + assertEquals(this.name, name.toString()); + assertEquals(this.huffmanName, huffmanName); + assertEquals(this.value, value.toString()); + assertEquals(this.huffmanValue, huffmanValue); + assertEquals(this.sensitive, sensitive); + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + fail(http3Error + "Decoding error:" + http3Error, throwable); + } + + @Override + public long streamId() { + return 0; + } + } + + private static void qpackErrorHandler(Throwable error, Http3Error http3Error) { + fail("QPACK error:" + http3Error, error); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/DynamicTableFieldLineRepresentationTest.java b/test/jdk/java/net/httpclient/qpack/DynamicTableFieldLineRepresentationTest.java new file mode 100644 index 00000000000..cdf584d1854 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DynamicTableFieldLineRepresentationTest.java @@ -0,0 +1,404 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.*; + +/* + * @test + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA + * -Djdk.http.qpack.allowBlockingEncoding=true + * -Djdk.http.qpack.decoderBlockedStreams=4 + * DynamicTableFieldLineRepresentationTest + */ +public class DynamicTableFieldLineRepresentationTest { + + private static final Random RANDOM = RandomFactory.getRandom(); + private static final long DT_CAPACITY = 4096L; + + //4.5.2. Indexed Field Line + @Test + public void indexedFieldLineOnDynamicTable() throws IOException { + boolean sensitive = RANDOM.nextBoolean(); + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Configure settingsFrames and prepopulate table + configureConnector(conn,1); + + var name = conn.decoderTable().get(0).name(); + var value = conn.decoderTable().get(0).value(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack( name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 1, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastIndexedName,name); + } + + //4.5.3. Indexed Field Line with Post-Base Index + @Test + public void indexedFieldLineOnDynamicTablePostBase() throws IOException { + System.err.println("start indexedFieldLineOnDynamicTablePostBase"); + boolean sensitive = RANDOM.nextBoolean(); + + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create settings frame with dynamic table capacity and number of blocked streams + configureConnector(conn, 3); + + var name = conn.decoderTable().get(1).name(); + var value = conn.decoderTable().get(1).value(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack( name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 1, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastIndexedName,name); + assertEquals(callback.lastValue,value); + } + + // 4.5.4. Literal Field Line with Name Reference + // A literal field line with name reference representation encodes a field line + // where the field name matches the field name of an entry in the static table + // or the field name of an entry in the dynamic table with an absolute index + // less than the value of the Base. + @Test + public void literalFieldLineNameReference() throws IOException { + System.err.println("start literalFieldLineNameReference"); + boolean sensitive = RANDOM.nextBoolean(); + + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> false, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create settings frame with dynamic table capacity and number of blocked streams + configureConnector(conn, 3); + + var name = conn.decoderTable().get(1).name(); + var value = conn.decoderTable().get(2).value(); // don't want value to match + + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack(name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 3, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastReferenceName, name); + } + + //4.5.5. Literal Field Line with Post-Base Name Reference + @Test + public void literalFieldLineNameReferencePostBase() throws IOException { + System.err.println("start literalFieldLineNameReferencePostBase"); + boolean sensitive = RANDOM.nextBoolean(); + + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> false, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create settings frame with dynamic table capacity and number of blocked streams + configureConnector(conn,4); + + var name = conn.decoderTable().get(3).name(); + var value = conn.decoderTable().get(2).value(); // don't want value to match + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingDynamicCallBack(name, value); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext(0, 2, + headerFrameWriter); + ByteBuffer headersBb = ByteBuffer.allocate(2048); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive, -1); + + // Write the header + headerFrameWriter.write(headersBb); + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertEquals(callback.lastReferenceName, name); + assertEquals(callback.lastValue, value); + } + + private void configureConnector(EncoderDecoderConnector.EncoderDecoderPair connector, int numberOfEntries){ + // Create settings frame with dynamic table capacity and number of blocked streams + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + // 4k should be enough for storing dynamic table entries added by 'prepopulateDynamicTable' + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DT_CAPACITY); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_BLOCKED_STREAMS,2); + + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder with constructed ConnectionSettings + connector.encoder().configure(settings); + connector.decoder().configure(settings); + connector.encoderTable().setCapacity(DT_CAPACITY); + connector.decoderTable().setCapacity(DT_CAPACITY); + + // add basic matching entries to both + prepopulateDynamicTable(connector.encoderTable(), numberOfEntries); + prepopulateDynamicTable(connector.decoderTable(), numberOfEntries); + } + + private static void prepopulateDynamicTable(DynamicTable dynamicTable, int numEntries) { + for (int count = 0; count < numEntries; count++) { + var header = TestHeader.withId(count); + dynamicTable.insert(header.name(), header.value()); + } + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + } + } + + private static class TestingDynamicCallBack implements DecodingCallback { + final String name, value; + String lastLiteralName = null; + String lastReferenceName = null; + String lastValue = null; + String lastIndexedName = null; + + TestingDynamicCallBack(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void onDecoded(CharSequence actualName, CharSequence value) { + fail("onDecoded should not be called"); + } + + @Override + public void onComplete() { + System.out.println("completed it"); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + fail("Decoding error: " + http3Error, throwable); + } + + @Override + public long streamId() { + return 0; + } + + @Override + public void onIndexed(long actualIndex, CharSequence actualName, CharSequence actualValue) { + System.out.println("Indexed called"); + assertEquals(actualName, name); + assertEquals(actualValue, value); + lastValue = value; + lastIndexedName = name; + } + + @Override + public void onLiteralWithNameReference(long index, + CharSequence actualName, + CharSequence actualValue, + boolean valueHuffman, + boolean hideIntermediary) { + System.out.println("Literal with name reference called"); + assertEquals(actualName.toString(), name); + assertEquals(actualValue.toString(), value); + lastReferenceName = name; + lastValue = value; + } + + @Override + public void onLiteralWithLiteralName(CharSequence actualName, boolean nameHuffman, + CharSequence actualValue, boolean valueHuffman, + boolean hideIntermediary) { + System.out.println("Literal with literal name called"); + assertEquals(actualName.toString(), name); + assertEquals(actualValue.toString(), value); + lastLiteralName = name; + lastValue = value; + } + } + + record TestHeader(String name, String value) { + public static BlockingDecodingTest.TestHeader withId(int id) { + return new BlockingDecodingTest.TestHeader(NAME + id, VALUE + id); + } + } + + private static final String NAME = "test"; + private static final String VALUE = "valueTest"; +} diff --git a/test/jdk/java/net/httpclient/qpack/DynamicTableTest.java b/test/jdk/java/net/httpclient/qpack/DynamicTableTest.java new file mode 100644 index 00000000000..80765e41787 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/DynamicTableTest.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key randomness + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL DynamicTableTest + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.test.lib.RandomFactory; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.IntStream; + +public class DynamicTableTest { + + // Test for addition to the table and that indices are growing monotonically, + // and they can be used to retrieve previously added entries + @Test + public void monotonicIndexes() { + int tableMaxCapacityBytes = 2048; + int numberOfElementsToAdd = 1024; + int charsPerNumber = (int) Math.ceil(Math.log10(numberOfElementsToAdd)); + int oneElementSize = 32 + HEADER_NAME_PREFIX.length() + HEADER_VALUE_PREFIX.length() + charsPerNumber * 2; + + // Expected table capacity in elements + int maxElementsInTable = tableMaxCapacityBytes / oneElementSize; + + // Test element id counter + long lastAddedId; + var dynamicTable = new DynamicTable(QPACK.getLogger().subLogger("monotonicIndexes")); + dynamicTable.setMaxTableCapacity(2048); + dynamicTable.setCapacity(tableMaxCapacityBytes); + + for (lastAddedId = 0; lastAddedId < numberOfElementsToAdd; lastAddedId++) { + var name = generateHeaderString(lastAddedId, true, charsPerNumber); + var value = generateHeaderString(lastAddedId, false, charsPerNumber); + long addedId = dynamicTable.insert(name, value); + + // Check that dynamic table put gives back monotonically increasing indexes + Assert.assertEquals(addedId, lastAddedId); + + if (lastAddedId > maxElementsInTable) { + // Check that oldest element is available and not reclaimed + long oldestAliveId = lastAddedId - maxElementsInTable + 1; + dynamicTable.get(oldestAliveId); + + // Check that relative indexing can be used to get oldest and newest entry + dynamicTable.getRelative(maxElementsInTable - 1); + dynamicTable.getRelative(0); + + // Check that reverse lookup is working for a random index from not reclaimed region + long rid = RANDOM.nextLong(oldestAliveId, lastAddedId); + String rName = generateHeaderString(rid, true, charsPerNumber); + String rValue = generateHeaderString(rid, false, charsPerNumber); + + // The reverse lookup search result range is shifted by 1 to implement search result indexing: + // full match found in a table: searchResult = idx + 1 + // partial match (name) in a table: searchResult = -idx - 1 + // no match: 0 + long fullMatchSearchResult = dynamicTable.search(rName, rValue); + long onlyNameSearchResult = dynamicTable.search(rName, "notFoundInTable"); + long noMatchResult = dynamicTable.search(HEADER_NAME_PREFIX, HEADER_VALUE_PREFIX); + + Assert.assertEquals(fullMatchSearchResult - 1, rid); + Assert.assertEquals(-onlyNameSearchResult - 1, rid); + Assert.assertEquals(noMatchResult, 0); + } + } + } + + @Test(dataProvider = "randomTableResizeData") + public void randomTableResize(int initialSize, long tail, long head, int resizeTo) + throws Throwable { + HeaderField[] initial = generateHeadersArray(initialSize, tail, head); + resizeTestRunner(initial, tail, head, resizeTo); + } + + @DataProvider + public Object[][] randomTableResizeData() { + return IntStream.range(0, 1000) + .boxed() + .map(i -> newRandomTableConfiguration()) + .toArray(Object[][]::new); + } + + @Test + public void holderArrayLengthTest() { + // Test that holder array size for storing elements is increased according to demand on array + // elements, and that by default its length is set to 64 elements. + var dynamicTable = new DynamicTable(QPACK.getLogger().subLogger("tableResizeTests")); + + // Check that the initial array length is DynamicTable.INITIAL_HOLDER_ARRAY_SIZE + Assert.assertEquals(getElementsArrayLength(dynamicTable), + INITIAL_HOLDER_ARRAY_SIZE); + + // Update dynamic table capacity to maximum allowed value and check + // that holder array is not changed + dynamicTable.setMaxTableCapacity(IntegerReader.QPACK_MAX_INTEGER_VALUE); + dynamicTable.setCapacity(IntegerReader.QPACK_MAX_INTEGER_VALUE); + Assert.assertEquals(getElementsArrayLength(dynamicTable), + INITIAL_HOLDER_ARRAY_SIZE); + + // Add DynamicTable.INITIAL_HOLDER_ARRAY_SIZE + 1 element to the dynamic table + // and check that its length is increased 2 times + for (int i = 0; i <= INITIAL_HOLDER_ARRAY_SIZE; i++) { + dynamicTable.insert("name" + i, "value" + i); + } + Assert.assertEquals(getElementsArrayLength(dynamicTable), INITIAL_HOLDER_ARRAY_SIZE << 1); + } + + // Test for a simple resize that checks that unique indexes still reference the correct entry + @Test(dataProvider = "simpleTableResizeData") + public void simpleTableResize(HeaderField[] array, long tail, long head, int resizeTo) throws Throwable { + resizeTestRunner(array, tail, head, resizeTo); + } + + @DataProvider + public Object[][] simpleTableResizeData() { + return new Object[][]{ + tableResizeScenario1(), tableResizeScenario2(), + tableResizeScenario3(), tableResizeScenario4(), + tableResizeScenario5()}; + } + + private Object[] tableResizeScenario1() { + HeaderField[] elements = new HeaderField[8]; + elements[5] = new HeaderField("5", "5"); // Tail + elements[6] = new HeaderField("6", "6"); + elements[7] = new HeaderField("7", "7"); // Head + return new Object[]{elements, 21L, 24L, 4}; + } + + private Object[] tableResizeScenario2() { + HeaderField[] elements = new HeaderField[8]; + elements[2] = new HeaderField("2", "2"); // Tail + elements[3] = new HeaderField("3", "3"); + elements[4] = new HeaderField("4", "4"); + elements[5] = new HeaderField("5", "5"); // Head + return new Object[]{elements, 26L, 30L, 4}; + } + + private Object[] tableResizeScenario3() { + HeaderField[] elements = new HeaderField[8]; + elements[0] = new HeaderField("4", "4"); + elements[1] = new HeaderField("5", "5"); // Head + elements[6] = new HeaderField("2", "2"); // Tail + elements[7] = new HeaderField("3", "3"); + return new Object[]{elements, 30L, 34L, 64}; + } + + private Object[] tableResizeScenario4() { + HeaderField[] elements = new HeaderField[8]; + elements[0] = new HeaderField("4", "4"); + elements[1] = new HeaderField("5", "5"); // Head + elements[5] = new HeaderField("1", "1"); // Tail + elements[6] = new HeaderField("2", "2"); + elements[7] = new HeaderField("3", "3"); + return new Object[]{elements, 29L, 34L, 16}; + } + + private Object[] tableResizeScenario5() { + HeaderField[] elements = new HeaderField[64]; + elements[10] = new HeaderField("1", "1"); + return new Object[]{elements, 3977L, 3978L, 16}; + } + + private static void resizeTestRunner(HeaderField[] array, long tail, long head, int resizeTo) throws Throwable { + assert tail < head; + var dynamicTable = new DynamicTable(QPACK.getLogger().subLogger("tableResizeTests")); + dynamicTable.setMaxTableCapacity(2048); + dynamicTable.setCapacity(2048); + // Prepare dynamic table state for the resize operation + DT_ELEMENTS_VH.set(dynamicTable, array); + DT_HEAD_VH.set(dynamicTable, head); + DT_TAIL_VH.set(dynamicTable, tail); + + // Call resize + ReentrantReadWriteLock lock = (ReentrantReadWriteLock) DT_LOCK_VH.get(dynamicTable); + lock.writeLock().lock(); + HeaderField[] resizeResult; + try { + // Call DynamicTable.resize + DT_RESIZE_MH.invoke(dynamicTable, resizeTo); + // Acquire resize result + resizeResult = (HeaderField[]) DT_ELEMENTS_VH.get(dynamicTable); + } finally { + lock.writeLock().unlock(); + } + + // Check the resulting array by calculating the expected array + HeaderField[] expectedResult = calcResizeResult(array, tail, head, resizeTo); + + // Check the resulting of the resize operation + checkResizeResult(array, resizeResult, expectedResult); + } + + private static HeaderField[] generateHeadersArray(int size, long tail, long head) { + assert head > tail; + HeaderField[] res = new HeaderField[size]; + assert head > 0L; + int charsPerNumber = (int) (Math.log10(head) + 1); + for (long eid = tail; eid < head; eid++) { + int idx = (int) (eid % size); + res[idx] = new HeaderField(generateHeaderString(eid, true, charsPerNumber), + generateHeaderString(eid, false, charsPerNumber)); + } + return res; + } + + private static int getElementsArrayLength(DynamicTable dynamicTable) { + HeaderField[] array = (HeaderField[]) DT_ELEMENTS_VH.get(dynamicTable); + return array.length; + } + + private static Object[] newRandomTableConfiguration() { + boolean shrink = RANDOM.nextBoolean(); + int initialSize = pow2size(RANDOM.nextInt(2, 2048)); + int resizeTo = shrink ? pow2size(RANDOM.nextInt(1, initialSize)) : pow2size(RANDOM.nextInt(initialSize, 4096)); + int elementsCount = RANDOM.nextInt(0, Math.min(initialSize, resizeTo)); + long tail = RANDOM.nextLong(100000); + long head = tail + elementsCount + 1; + return new Object[]{initialSize, tail, head, resizeTo}; + } + + private static HeaderField[] calcResizeResult(HeaderField[] array, long tail, long head, int resizeTo) { + HeaderField[] result = new HeaderField[resizeTo]; + for (long p = tail; p < head; p++) { + int newIdx = (int) (p % resizeTo); + int oldIdx = (int) (p % array.length); + result[newIdx] = array[oldIdx]; + } + return result; + } + + private static void checkResizeResult(HeaderField[] initial, HeaderField[] resized, HeaderField[] expected) { + Assert.assertEquals(resized.length, expected.length); + for (int index = 0; index < expected.length; index++) { + if (!sameHeaderField(expected[index], resized[index])) { + System.err.println("Initial Array:" + Arrays.deepToString(initial)); + System.err.println("Resized Array:" + Arrays.deepToString(resized)); + System.err.println("Expected Array:" + Arrays.deepToString(expected)); + Assert.fail("DynamicTable.resize failed"); + } + } + } + + private static boolean sameHeaderField(HeaderField a, HeaderField b) { + // Check if one HeaderField is null and another is not null + if (a == null ^ b == null) { + return false; + } + // Given previous check, check if both HeaderField are null + if (a == null) { + return true; + } + // Both HFs are not null - will check name() and value() values + return a.name().equals(b.name()) && a.value().equals(b.value()); + } + + private static MethodHandle findDynamicTableResizeMH() { + try { + MethodType mt = MethodType.methodType(void.class, int.class); + return DT_LOOKUP.findVirtual(DynamicTable.class, "resize", mt); + } catch (Exception e) { + Assert.fail("Failed to initialize DynamicTable.resize MH", e); + return null; + } + } + + private static VarHandle findDynamicTableFieldVH(String fieldName, Class fieldType) { + try { + return DT_LOOKUP.findVarHandle(DynamicTable.class, fieldName, fieldType); + } catch (Exception e) { + Assert.fail("Failed to initialize DynamicTable private Lookup instance", e); + return null; + } + } + + private static T readDynamicTableStaticFieldValue(String fieldName, Class fieldType) { + try { + var vh = DT_LOOKUP.findStaticVarHandle(DynamicTable.class, fieldName, fieldType); + return (T) vh.get(); + } catch (Exception e) { + Assert.fail("Failed to read DynamicTable static field value", e); + return null; + } + } + + private static MethodHandles.Lookup initializeDtLookup() { + try { + return MethodHandles.privateLookupIn(DynamicTable.class, MethodHandles.lookup()); + } catch (IllegalAccessException e) { + Assert.fail("Failed to initialize DynamicTable private Lookup instance", e); + return null; + } + } + + + private static final MethodHandles.Lookup DT_LOOKUP; + private static final MethodHandle DT_RESIZE_MH; + private static final VarHandle DT_HEAD_VH; + private static final VarHandle DT_TAIL_VH; + private static final VarHandle DT_ELEMENTS_VH; + private static final VarHandle DT_LOCK_VH; + private static final int INITIAL_HOLDER_ARRAY_SIZE; + + static { + DT_LOOKUP = initializeDtLookup(); + DT_RESIZE_MH = findDynamicTableResizeMH(); + DT_HEAD_VH = findDynamicTableFieldVH("head", long.class); + DT_TAIL_VH = findDynamicTableFieldVH("tail", long.class); + DT_ELEMENTS_VH = findDynamicTableFieldVH("elements", HeaderField[].class); + DT_LOCK_VH = findDynamicTableFieldVH("lock", ReentrantReadWriteLock.class); + INITIAL_HOLDER_ARRAY_SIZE = readDynamicTableStaticFieldValue( + "INITIAL_HOLDER_ARRAY_LENGTH", int.class); + } + + private static String generateHeaderString(long id, boolean generateName, int charsPerNumber) { + return (generateName ? HEADER_NAME_PREFIX : HEADER_VALUE_PREFIX) + ("%0" + charsPerNumber + "d").formatted(id); + } + + private static int pow2size(int size) { + return 1 << (32 - Integer.numberOfLeadingZeros(size - 1)); + } + + private static final String HEADER_NAME_PREFIX = "HeaderName"; + private static final String HEADER_VALUE_PREFIX = "HeaderValue"; + private static final Random RANDOM = RandomFactory.getRandom(); +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnectionTest.java b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnectionTest.java new file mode 100644 index 00000000000..418af9be2ad --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnectionTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicReference; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA + * EncoderDecoderConnectionTest + */ +public class EncoderDecoderConnectionTest { + + @Test + public void capacityUpdateTest() { + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, error::set); + + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Set encoder and decoder maximum dynamic table capacity + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 2048L); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + decoder.configure(settings); + encoder.configure(settings); + + // Encoder - update DT capacity + final long capacityToSet = 1024L; + encoder.setTableCapacity(capacityToSet); + + // Check that no errors observed + Assert.assertNull(encoderErrorHandler.error.get()); + Assert.assertNull(encoderErrorHandler.http3Error.get()); + Assert.assertNull(decoderErrorHandler.error.get()); + Assert.assertNull(decoderErrorHandler.http3Error.get()); + + // Check that encoder's table capacity is updated + Assert.assertEquals(conn.encoderTable().capacity(), capacityToSet); + // Since encoder/decoder streams are cross-wired we expect see dynamic + // table capacity updated for the decoder too + Assert.assertEquals(conn.decoderTable().capacity(), + conn.encoderTable().capacity()); + } + + @Test + public void entryInsertionTest() { + AtomicReference error = new AtomicReference<>(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + + var conn = encoderDecoderConnector.newEncoderDecoderPair(entry -> true, + encoderErrorHandler::qpackErrorHandler, decoderErrorHandler::qpackErrorHandler, + error::set); + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Set encoder and decoder maximum dynamic table capacity + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 2048L); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + decoder.configure(settings); + encoder.configure(settings); + + // Update encoder and decoder DTs capacity - note that "Set Dynamic Table Capacity" + // is issued by the encoder that updates capacity on the decoder side + encoder.setTableCapacity(1024L); + + // Create table entry for insertion to the dynamic table + var entryToInsert = new TableEntry("test", "testValue"); + + // Create encoder instruction writer for generating "Insert with Literal Name" + // encoder instruction + var encoderInstructionWriter = new EncoderInstructionsWriter(); + + // Check that no errors observed + Assert.assertNull(encoderErrorHandler.error.get()); + Assert.assertNull(encoderErrorHandler.http3Error.get()); + Assert.assertNull(decoderErrorHandler.error.get()); + Assert.assertNull(decoderErrorHandler.http3Error.get()); + + // Issue the insert instruction on encoder stream + conn.encoderTable().insertWithEncoderStreamUpdate(entryToInsert, + encoderInstructionWriter, conn.encoderStreams(), + encoder.newEncodingContext(0, 0, new HeaderFrameWriter())); + var encoderHeader = conn.encoderTable().get(0); + var decoderHeader = conn.decoderTable().get(0); + Assert.assertEquals(encoderHeader.name(), decoderHeader.name()); + Assert.assertEquals(encoderHeader.value(), decoderHeader.value()); + } + + @Test + public void decoderErrorReportingTest() { + AtomicReference error = new AtomicReference<>(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(e -> false, + encoderErrorHandler::qpackErrorHandler, + decoderErrorHandler::qpackErrorHandler, + error::set); + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 2048L); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + conn.encoder().configure(settings); + conn.encoderTable().setCapacity(1024L); + conn.encoderTable().insertWithEncoderStreamUpdate( + new TableEntry("a", "b"), + new EncoderInstructionsWriter(), + conn.encoderStreams(), + conn.encoder().newEncodingContext(0, 0, new HeaderFrameWriter())); + + // QPACK_ENCODER_STREAM_ERROR is expected on the decoder side + // since the decoder dynamic table capacity was not updated + Assert.assertEquals(decoderErrorHandler.http3Error.get(), + Http3Error.QPACK_ENCODER_STREAM_ERROR); + + // It is expected that http3 error reported to + // the decoder error handler only + Assert.assertNull(encoderErrorHandler.http3Error.get()); + } + + @Test + public void overflowIntegerInInstructions() { + AtomicReference error = new AtomicReference<>(); + TestErrorHandler encoderErrorHandler = new TestErrorHandler(); + TestErrorHandler decoderErrorHandler = new TestErrorHandler(); + EncoderDecoderConnector encoderDecoderConnector = new EncoderDecoderConnector(); + var conn = encoderDecoderConnector.newEncoderDecoderPair(e -> false, + encoderErrorHandler::qpackErrorHandler, + decoderErrorHandler::qpackErrorHandler, + error::set); + + // Forge byte buffer with encoder instruction containing integer > + // QPACK_MAX_INTEGER_VALUE + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Capacity (5+) | + // +---+---+---+-------------------+ + var encoderInstBb = instructionWithOverflowInteger(5, 0b0010_0000); + conn.encoderStreams().submitData(encoderInstBb); + + // Send bad decoder instruction back to encoder + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Stream ID (6+) | + // +---+---+-----------------------+ + var buffer = instructionWithOverflowInteger(6, 0b0100_0000); + conn.decoderStreams().submitData(buffer); + + // Analyze errors for expected results + Throwable encoderError = encoderErrorHandler.error.get(); + Http3Error encoderHttp3Error = encoderErrorHandler.http3Error.get(); + Throwable decoderError = decoderErrorHandler.error.get(); + Http3Error decoderHttp3Error = decoderErrorHandler.http3Error.get(); + + System.err.println("Encoder Error: " + encoderError); + System.err.println("Encoder Http3 error: " + encoderHttp3Error); + System.err.println("Decoder Error: " + decoderError); + System.err.println("Decoder Http3 error: " + decoderHttp3Error); + + if (encoderError == null || !(encoderError instanceof IOException)) { + Assert.fail("Incorrect encoder error type", encoderError); + } + if (decoderError == null || !(decoderError instanceof IOException)) { + Assert.fail("Incorrect decoder error type", decoderError); + } + Assert.assertEquals(encoderHttp3Error, Http3Error.QPACK_DECODER_STREAM_ERROR); + Assert.assertEquals(decoderHttp3Error, Http3Error.QPACK_ENCODER_STREAM_ERROR); + } + + private static ByteBuffer instructionWithOverflowInteger(int N, int payload) { + var buffer = ByteBuffer.allocate(11); + int max = (2 << (N - 1)) - 1; + buffer.put((byte) (payload | max)); + for (int i = 0; i < 9; i++) { + buffer.put((byte) 128); + } + buffer.put((byte)10); + buffer.flip(); + return buffer; + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnector.java b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnector.java new file mode 100644 index 00000000000..3cf185ff8a7 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderDecoderConnector.java @@ -0,0 +1,509 @@ +/* + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.http3.streams.Http3Streams; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.Decoder; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.InsertionPolicy; +import jdk.internal.net.http.qpack.QPACK.QPACKErrorHandler; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.quic.QuicTLSEngine; +import org.testng.Assert; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * Instance of this class provides a stubbed Quic Connection implementation that + * cross-wires encoder/decoder streams, and also provides access to + * encoder and decoder dynamic tables. + */ +public class EncoderDecoderConnector { + /** + * Constructs test connector instance capable of instantiating cross-wired encoder/decoder pair. + * It is achieved by implementing stubs for Quic connection and writer classes. + */ + public EncoderDecoderConnector() { + this(null, null); + } + + /** + * Constructs test connector instance capable of instantiating encoder/decoder pair. + * The encoder/decoder connections are not cross-wired. The provided byte buffer consumers + * are used by the underlying Quic connection instead. + * + * @param encoderBytesConsumer consumer of the encoder byte buffers + * @param decoderBytesConsumer consumer of the decoder byte buffers + */ + public EncoderDecoderConnector(Consumer encoderBytesConsumer, Consumer decoderBytesConsumer) { + encoderReceiverFuture = new CompletableFuture<>(); + decoderReceiverFuture = new CompletableFuture<>(); + encoderConnection = new TestQuicConnection(decoderReceiverFuture, encoderBytesConsumer); + decoderConnection = new TestQuicConnection(encoderReceiverFuture, decoderBytesConsumer); + } + + /** + * Create new encoder/decoder pair and establish Quic stub connection between them. + * + * @param encoderInsertionPolicy encoder insertion policy + * @param encoderErrorHandler encoder stream error handler + * @param decoderErrorHandler decoder stream error handler + * @param streamsErrorHandler streams error handler + * @return encoder/decoder pair + */ + public EncoderDecoderConnector.EncoderDecoderPair + newEncoderDecoderPair(InsertionPolicy encoderInsertionPolicy, + QPACKErrorHandler encoderErrorHandler, + QPACKErrorHandler decoderErrorHandler, + Consumer streamsErrorHandler) { + // One instance of this class supports only one encoder/decoder pair + if (connectionCreated) { + throw new IllegalStateException("Encoder/decoder pair was already instantiated"); + } + connectionCreated = true; + + // Create encoder + var encoder = new Encoder(encoderInsertionPolicy, (receiver) -> + createEncoderStreams(receiver, streamsErrorHandler), + encoderErrorHandler); + + // Create decoder + var decoder = new Decoder((receiver) -> + createDecoderStreams(receiver, streamsErrorHandler), + decoderErrorHandler); + // Extract encoder and decoder dynamic tables + DynamicTable encoderTable = dynamicTable(encoder); + DynamicTable decoderTable = dynamicTable(decoder); + return new EncoderDecoderConnector.EncoderDecoderPair(encoder, decoder, + encoderTable, decoderTable, encoderStreamPair, decoderStreamPair); + } + + /** + * Record describing {@linkplain EncoderDecoderConnector#EncoderDecoderConnector() cross-wired} + * OR {@link EncoderDecoderConnector#EncoderDecoderConnector(Consumer, Consumer) decoupled} + * encoder and decoder pair. + * The references for encoder and decoder dynamic tables also provided for testing purposes. + * + * @param encoder encoder + * @param decoder decoder + * @param encoderTable encoder's dynamic table + * @param decoderTable decoder's dynamic table + * @param encoderStreams encoder streams + */ + record EncoderDecoderPair(Encoder encoder, Decoder decoder, + DynamicTable encoderTable, + DynamicTable decoderTable, + QueuingStreamPair encoderStreams, + QueuingStreamPair decoderStreams) { + } + + + private CompletableFuture> decoderReceiverFuture; + private CompletableFuture> encoderReceiverFuture; + private final QuicConnection encoderConnection; + private final QuicConnection decoderConnection; + private volatile QueuingStreamPair encoderStreamPair; + private volatile QueuingStreamPair decoderStreamPair; + + private volatile boolean connectionCreated; + + private static DynamicTable dynamicTable(Encoder encoder) { + return (DynamicTable) ENCODER_DT_VH.get(encoder); + } + + private static DynamicTable dynamicTable(Decoder decoder) { + return (DynamicTable) DECODER_DT_VH.get(decoder); + } + + private static final MethodHandles.Lookup ENCODER_LOOKUP = + initializeLookup(Encoder.class); + private static final MethodHandles.Lookup DECODER_LOOKUP = + initializeLookup(Decoder.class); + private static final VarHandle ENCODER_DT_VH = findDynamicTableVH( + ENCODER_LOOKUP, Encoder.class); + private static final VarHandle DECODER_DT_VH = findDynamicTableVH( + DECODER_LOOKUP, Decoder.class); + + + private static MethodHandles.Lookup initializeLookup(Class clz) { + try { + return MethodHandles.privateLookupIn(clz, MethodHandles.lookup()); + } catch (IllegalAccessException e) { + Assert.fail("Failed to initialize private Lookup instance", e); + return null; + } + } + + private static VarHandle findDynamicTableVH( + final MethodHandles.Lookup lookup, Class recv) { + try { + return lookup.findVarHandle(recv, "dynamicTable", DynamicTable.class); + } catch (Exception e) { + Assert.fail("Failed to acquire dynamic table VarHandle instance", e); + return null; + } + } + + + QueuingStreamPair createEncoderStreams(Consumer receiver, + Consumer errorHandler) { + QueuingStreamPair streamPair = new QueuingStreamPair( + Http3Streams.StreamType.QPACK_ENCODER, + encoderConnection, + receiver, + TestErrorHandler.of(errorHandler), + Utils.getDebugLogger(() -> "test-encoder")); + encoderReceiverFuture.complete(receiver); + encoderStreamPair = streamPair; + return streamPair; + } + + QueuingStreamPair createDecoderStreams(Consumer receiver, + Consumer errorHandler) { + QueuingStreamPair streamPair = new QueuingStreamPair( + Http3Streams.StreamType.QPACK_DECODER, + decoderConnection, + receiver, + TestErrorHandler.of(errorHandler), + Utils.getDebugLogger(() -> "test-decoder")); + decoderReceiverFuture.complete(receiver); + decoderStreamPair = streamPair; + return streamPair; + } + + private class TestQuicConnection extends QuicConnection { + + public TestQuicConnection(CompletableFuture> receiverFuture, + Consumer bytesWriter) { + this.sender = new TestQuicSenderStream(receiverFuture, bytesWriter); + } + + final TestQuicSenderStream sender; + + @Override + public boolean isOpen() { + return true; + } + + @Override + public TerminationCause terminationCause() { + return null; + } + + @Override + public QuicTLSEngine getTLSEngine() { + return null; + } + + @Override + public InetSocketAddress peerAddress() { + return null; + } + + @Override + public SocketAddress localAddress() { + return null; + } + + @Override + public CompletableFuture startHandshake() { + return null; + } + + @Override + public CompletableFuture openNewLocalBidiStream( + Duration duration) { + return null; + } + + @Override + public CompletableFuture openNewLocalUniStream( + Duration duration) { + // This method is called to create two unidirectional streams: + // one for decoder, one for encoder + return MinimalFuture.completedFuture(sender); + } + + @Override + public void addRemoteStreamListener( + Predicate streamConsumer) { + } + + @Override + public boolean removeRemoteStreamListener( + Predicate streamConsumer) { + return false; + } + + @Override + public Stream quicStreams() { + return null; + } + + @Override + public CompletableFuture handshakeReachedPeer() { + return MinimalFuture.completedFuture(null); + } + + @Override + public CompletableFuture requestSendPing() { + return MinimalFuture.completedFuture(-1L); + } + + @Override + public ConnectionTerminator connectionTerminator() { + return null; + } + + @Override + public String dbgTag() { + return null; + } + + @Override + public String logTag() { + return null; + } + } + + + private class TestQuicStreamWriter extends QuicStreamWriter { + final TestQuicSenderStream sender; + volatile boolean gotStreamType; + volatile Http3Streams.StreamType associatedStreamType; + + final CompletableFuture> receiverFuture; + final Consumer bytesWriter; + + TestQuicStreamWriter(SequentialScheduler scheduler, TestQuicSenderStream sender, + CompletableFuture> receiverFuture, + Consumer bytesWriter) { + super(scheduler); + this.sender = sender; + this.gotStreamType = false; + this.receiverFuture = receiverFuture; + this.bytesWriter = bytesWriter; + } + + private void write(ByteBuffer bb) { + if (bytesWriter == null) { + if (!gotStreamType) { + IntegerReader integerReader = new IntegerReader(); + integerReader.configure(8); + try { + integerReader.read(bb); + } catch (QPackException e) { + System.err.println("Can't read stream type byte"); + } + Http3Streams.StreamType type = Http3Streams.StreamType.ofCode((int) integerReader.get()).get(); + System.err.println("Stream opened with type=" + type); + gotStreamType = true; + associatedStreamType = type; + } else { + if (receiverFuture.isDone() && !receiverFuture.isCompletedExceptionally()) { + Consumer receiver = receiverFuture.getNow(null); + if (receiver != null) { + receiver.accept(bb); + } + } + } + } else { + bytesWriter.accept(bb); + } + } + + @Override + public QuicSenderStream.SendingStreamState sendingState() { + return null; + } + + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) + throws IOException { + write(buffer); + } + + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + write(buffer); + } + + @Override + public long credit() { + return Long.MAX_VALUE; + } + + @Override + public void reset(long errorCode) { + } + + @Override + public QuicSenderStream stream() { + return connected() ? sender : null; + } + + @Override + public boolean connected() { + return sender.writer == this; + } + } + + class TestQuicSenderStream implements QuicSenderStream { + private static AtomicLong ids = new AtomicLong(); + private final long id; + TestQuicStreamWriter writer; + Consumer bytesWriter; + final CompletableFuture> receiverFuture; + + TestQuicSenderStream(CompletableFuture> receiverFuture, + Consumer bytesWriter) { + id = ids.getAndIncrement() * 4 + type(); + this.receiverFuture = receiverFuture; + this.bytesWriter = bytesWriter; + } + + @Override + public SendingStreamState sendingState() { + return SendingStreamState.READY; + } + + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + return writer == null ? writer = new TestQuicStreamWriter( + scheduler, this, receiverFuture, bytesWriter) : writer; + } + + @Override + public void disconnectWriter(QuicStreamWriter writer) { + } + + @Override + public void reset(long errorCode) { + } + + @Override + public long dataSent() { + return 0; + } + + @Override + public long streamId() { + return id; + } + + @Override + public StreamMode mode() { + return null; + } + + @Override + public boolean isClientInitiated() { + return true; + } + + @Override + public boolean isServerInitiated() { + return false; + } + + @Override + public boolean isBidirectional() { + return false; + } + + @Override + public boolean isLocalInitiated() { + return true; + } + + @Override + public boolean isRemoteInitiated() { + return false; + } + + @Override + public int type() { + return 0x02; + } + + @Override + public StreamState state() { + return SendingStreamState.READY; + } + + @Override + public long sndErrorCode() { + return -1; + } + + @Override + public boolean stopSendingReceived() { + return false; + } + } + + private static class TestErrorHandler + implements UniStreamPair.StreamErrorHandler { + final Consumer handler; + + private TestErrorHandler(Consumer handler) { + this.handler = handler; + } + + @Override + public void onError(QuicStream stream, UniStreamPair uniStreamPair, + Throwable throwable) { + handler.accept(throwable); + } + + public static TestErrorHandler of(Consumer handler) { + return new TestErrorHandler(handler); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderDecoderTest.java b/test/jdk/java/net/httpclient/qpack/EncoderDecoderTest.java new file mode 100644 index 00000000000..a1fa9745e05 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderDecoderTest.java @@ -0,0 +1,442 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.StaticTable; +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm EncoderDecoderTest + */ +public class EncoderDecoderTest { + private final Random random = new Random(); + + private static final int TEST_STR_MAX_LENGTH = 10; + + + private static void qpackErrorHandler(Throwable error, Http3Error http3Error) { + fail(http3Error + "QPACK error:" + http3Error, error); + } + + @DataProvider(name = "indexProvider") + public Object[][] indexProvider() { + AtomicLong index = new AtomicLong(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(headerField -> List.of(index.getAndIncrement(), headerField)) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider(name = "nameReferenceProvider") + public Object[][] nameReferenceProvider() { + AtomicLong tableIndex = new AtomicLong(); + Map> map = new HashMap<>(); + for (var headerField : StaticTable.HTTP3_HEADER_FIELDS) { + var name = headerField.name(); + var index = tableIndex.getAndIncrement(); + + if (!map.containsKey(name)) + map.put(name, new ArrayList<>()); + map.get(name).add(index); + } + return map.entrySet().stream() + .map(e -> List.of(e.getKey(), randomString(), e.getValue())) + .map(List::toArray).toArray(Object[][]::new); + } + + @DataProvider(name = "literalProvider") + public Object[][] literalProvider() { + var output = new String[100][]; + for (int i = 0; i < 100; i++) { + output[i] = new String[]{randomString(), randomString()}; + } + return output; + } + + private void assertNotFailed(AtomicReference errorRef) { + var error = errorRef.get(); + if (error != null) throw new AssertionError(error); + } + + @Test(dataProvider = "indexProvider") + public void encodeDecodeIndexedOnStaticTable(long index, HeaderField h) throws IOException { + var actual = allocateIndexTestBuffer(index); + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + AtomicReference writerStub = new AtomicReference<>(); + EncoderDecoderConnector connector = new EncoderDecoderConnector(writerStub::set, writerStub::set); + var conn = connector.newEncoderDecoderPair(e -> false, + EncoderDecoderTest::qpackErrorHandler, + EncoderDecoderTest::qpackErrorHandler, + error::set); + + // Create encoder and decoder + var encoder = conn.encoder(); + + // Set encoder maximum dynamic table capacity + conn.encoderTable().setMaxTableCapacity(256); + // Set dynamic table capacity that doesn't exceed the max capacity value + conn.encoderTable().setCapacity(256); + + var decoder = conn.decoder(); + + // Create header frame reader and writer + var callback = new TestingCallBack(index, h.name(), h.value()); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + var headerFrameWriter = encoder.newHeaderFrameWriter(); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, 0, headerFrameWriter); + + // Configures encoder for writing the header name:value pair + encoder.header(context, h.name(), h.value(), false); + + // Write the header + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + buffers.add(actual); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode generated prefix bytes and encoded headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertNotFailed(error); + } + + @Test(dataProvider = "nameReferenceProvider") + public void encodeDecodeLiteralWithNameRefOnStaticTable(String name, String value, List validIndices) throws IOException { + long index = Collections.max(validIndices); + boolean sensitive = random.nextBoolean(); + + var actual = allocateNameRefBuffer(index, value); + List buffers = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + AtomicReference writerStub = new AtomicReference<>(); + + // Create encoder and decoder + EncoderDecoderConnector connector = new EncoderDecoderConnector(writerStub::set, writerStub::set); + var conn = connector.newEncoderDecoderPair(e -> false, + EncoderDecoderTest::qpackErrorHandler, + EncoderDecoderTest::qpackErrorHandler, + error::set); + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingCallBack(validIndices, name, value, sensitive); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, 0, headerFrameWriter); + + // Configures encoder for writing the header name:value pair + encoder.header(context, name, value, sensitive); + + // Write the header + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + buffers.add(actual); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertNotFailed(error); + } + + @Test(dataProvider = "literalProvider") + public void encodeDecodeLiteralWithLiteralNameOnStaticTable(String name, String value) throws IOException { + boolean sensitive = random.nextBoolean(); + List buffers = new ArrayList<>(); + var actual = allocateLiteralBuffer(name, value); + AtomicReference error = new AtomicReference<>(); + AtomicReference writerStub = new AtomicReference<>(); + + // Create encoder and decoder + EncoderDecoderConnector connector = new EncoderDecoderConnector(writerStub::set, writerStub::set); + var conn = connector.newEncoderDecoderPair(e -> false, + EncoderDecoderTest::qpackErrorHandler, + EncoderDecoderTest::qpackErrorHandler, + error::set); + var encoder = conn.encoder(); + var decoder = conn.decoder(); + + // Create header frame reader and writer + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var callback = new TestingCallBack(name, value, sensitive); + var headerFrameReader = decoder.newHeaderFrameReader(callback); + + // create encoding context + Encoder.EncodingContext context = encoder.newEncodingContext( + 0, 0, headerFrameWriter); + + // Configures encoder for writing the header name:value conn + encoder.header(context, name, value, sensitive); + // Write the header + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + buffers.add(actual); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Decode headers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + assertNotFailed(error); + } + + /* Test Methods */ + private void debug(ByteBuffer bb, String msg, boolean verbose) { + if (verbose) + System.out.printf("DEBUG[%s]: pos=%d, limit=%d, remaining=%d%n", + msg, bb.position(), bb.limit(), bb.remaining()); + System.out.printf("DEBUG[%s]: ", msg); + for (byte b : bb.array()) { + System.out.printf("(%s,%d) ", Integer.toBinaryString(b & 0xFF), b & 0xFF); + } + System.out.println(); + } + + private ByteBuffer allocateIndexTestBuffer(long index) { + /* + * Note on Integer Representation used for storing the length of name and value strings. + * Taken from RFC 7541 Section 5.1 + * + * "An integer is represented in two parts: a prefix that fills the current octet and an + * optional list of octets that are used if the integer value does not fit within the + * prefix. The number of bits of the prefix (called N) is a parameter of the integer + * representation. If the integer value is small enough, i.e., strictly less than 2N-1, it + * is encoded within the N-bit prefix. + * + * ... + * + * Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2N-1, is + * encoded using a list of one or more octets. The most significant bit of each octet is + * used as a continuation flag: its value is set to 1 except for the last octet in the list. + * The remaining bits of the octets are used to encode the decreased value." + * + * Use "null" for name, if name isn't being provided (i.e. for a nameRef); otherwise, buffer + * will be too large. + * + */ + int N = 6; // bits available in first byte + int size = 1; + index -= Math.pow(2, N) - 1; // number that you can store in first N bits + while (index >= 0) { + index -= 127; + size++; + } + return ByteBuffer.allocate(size + 2); + } + + private ByteBuffer allocateNameRefBuffer(long index, CharSequence value) { + int N = 4; + return allocateNameRefBuffer(N, index, value); + } + + private ByteBuffer allocateNameRefBuffer(int N, long index, CharSequence value) { + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = 1 + vlen; + + index -= Math.pow(2, N) - 1; + while (index >= 0) { + index -= 127; + size++; + } + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size + 2); + } + + private ByteBuffer allocateLiteralBuffer(CharSequence name, CharSequence value) { + int N = 3; + return allocateLiteralBuffer(N, name, value); + } + + private ByteBuffer allocateLiteralBuffer(int N, CharSequence name, CharSequence value) { + int nlen = Math.min(QuickHuffman.lengthOf(name), name.length()); + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = nlen + vlen; + + nlen -= Math.pow(2, N) - 1; + size++; + while (nlen >= 0) { + nlen -= 127; + size++; + } + + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size + 2); + } + + static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum."""; + + private String randomString() { + int lower = random.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + /** + * The empty string ("") is a valid value String in the static table and the random + * String returned cannot refer to an entry in the table. Therefore, we set the upper + * bound below to a minimum of 1. + */ + return LOREM.substring(lower, 1 + lower + random.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestingCallBack implements DecodingCallback { + final long index; + final boolean huffmanName, huffmanValue; + final boolean sensitive; + final String name, value; + final List validIndices; + + // Indexed + TestingCallBack(long index, String name, String value) { + this(index, null, name, value, false); + } + // Literal w/Literal Name + TestingCallBack(String name, String value, boolean sensitive) { + this(-1L, null, name, value, sensitive); + } + // Literal w/Name Reference + TestingCallBack(List validIndices, String name, String value, boolean sensitive) { + this(-1L, validIndices, name, value, sensitive); + } + TestingCallBack(long index, List validIndices, String name, String value, boolean sensitive) { + this.index = index; + this.validIndices = validIndices; + this.huffmanName = QuickHuffman.isHuffmanBetterFor(name); + this.huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + this.sensitive = sensitive; + this.name = name; + this.value = value; + } + + @Override + public void onDecoded(CharSequence actualName, CharSequence value) { + fail("onDecoded should not be called"); + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + fail("Decoding error: " + http3Error, throwable); + } + + @Override + public long streamId() { + return 0; + } + + @Override + public void onIndexed(long actualIndex, CharSequence actualName, CharSequence actualValue) { + assertEquals(actualIndex, index); + assertEquals(actualName, name); + assertEquals(actualValue, value); + } + + @Override + public void onLiteralWithNameReference(long actualIndex, CharSequence actualName, + CharSequence actualValue, boolean huffmanValue, + boolean actualHideIntermediary) { + assertTrue(validIndices.contains(actualIndex)); + assertEquals(actualName.toString(), name); + assertEquals(actualValue.toString(), value); + assertEquals(huffmanValue, huffmanValue); + assertEquals(actualHideIntermediary, sensitive); + } + + @Override + public void onLiteralWithLiteralName(CharSequence actualName, boolean actualHuffmanName, + CharSequence actualValue, boolean actualHuffmanValue, + boolean actualHideIntermediary) { + assertEquals(actualName.toString(), name); + assertEquals(actualHuffmanName, huffmanName); + assertEquals(actualValue.toString(), value); + assertEquals(actualHuffmanValue, huffmanValue); + assertEquals(actualHideIntermediary, sensitive); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderInstructionsReaderTest.java b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsReaderTest.java new file mode 100644 index 00000000000..5c31762cdb3 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsReaderTest.java @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key randomness + * @library /test/lib + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL EncoderInstructionsReaderTest + */ + +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import java.nio.ByteBuffer; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.writers.StringWriter; +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.RepeatedTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader.Callback; + +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; + +public class EncoderInstructionsReaderTest { + + EncoderInstructionsReader encoderInstructionsReader; + private static final Random RANDOM = RandomFactory.getRandom(); + + @RepeatedTest(5) + public void testCapacity() { + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Capacity (5+) | + // +---+---+---+-------------------+ + + // create logger and callback + QPACK.Logger logger = QPACK.getLogger().subLogger("testCapacity"); + TestCallback callback = new TestCallback(); + + // create a random value to be assigned as capacity + Long expectedCapacity = RANDOM.nextLong(IntegerReader.QPACK_MAX_INTEGER_VALUE); + + // create integerWriter, set expected size for the bytebuffer and write to it + IntegerWriter integerWriter = new IntegerWriter(); + int bufferSize = requiredBufferSize(5, expectedCapacity); + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + int payload = 0b0010_0000; + integerWriter.configure(expectedCapacity, 5, payload); + boolean result = integerWriter.write(byteBuffer); + + // assert that the writer finished and isn't expecting another bytebuffer + assert result; + + byteBuffer.flip(); + + // use EncoderInstructionReader and check it successfully reads the input + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + encoderInstructionsReader.read(byteBuffer, -1); + + long actualCapacity = callback.capacity.get(); + assertEquals(expectedCapacity, actualCapacity, "expected capacity differed from actual result"); + } + + @RepeatedTest(10) + public void testInsertWithNameReference() { + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Name Index (6+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + + QPACK.Logger logger = QPACK.getLogger() + .subLogger("testInsertWithNameReference"); + TestCallback callback = new TestCallback(); + + // Needs both an integer and String writer + IntegerWriter integerWriter = new IntegerWriter(); + StringWriter stringWriter = new StringWriter(); + boolean huffman = RANDOM.nextBoolean(); + boolean staticTable = RANDOM.nextBoolean(); + + int payload; + if (staticTable) { + payload = 0b1100_0000; + } else { + payload = 0b1000_0000; + } + + // get a random member of the dynamic table and create a random string to update it with + long index = RANDOM.nextLong(IntegerReader.QPACK_MAX_INTEGER_VALUE); + String value = randomString(); + + // calculate the size of the byteBuffer + int bufferSize = requiredBufferSize(6, index); + bufferSize += requiredBufferSize(7, value.length()); + + if (huffman) { + bufferSize += QuickHuffman.lengthOf(value); + } else { + bufferSize += value.length(); + } + + integerWriter.configure(index, 6, payload); + stringWriter.configure(value, huffman); + + boolean intWriterFinished = false; + boolean stringWriterFinished = false; + List byteBufferList = new ArrayList<>(); + + // Feed the writers with bytebuffers of random size until they total bufferSize, + // once each bytebuffer is full add it to byteBufferList to be read later + while (!stringWriterFinished) { + int randomSize = RANDOM.nextInt(0, bufferSize + 1); + bufferSize -= randomSize; + ByteBuffer byteBuffer = ByteBuffer.allocate(randomSize); + + if (!intWriterFinished) { + intWriterFinished = integerWriter.write(byteBuffer); + if (!intWriterFinished) { + // writer not finished, add bytebuffer to list of full bytebuffers + // then loop + byteBuffer.flip(); + byteBufferList.add(byteBuffer); + continue; + } + } + + // this stage should only be reached if the intWriter is finished + stringWriterFinished = stringWriter.write(byteBuffer); + byteBuffer.flip(); + byteBufferList.add(byteBuffer); + } + + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + for (var byteBuffer : byteBufferList) { + encoderInstructionsReader.read(byteBuffer, -1); + } + + assertEquals(index, callback.indexInsert.nameIndex); + assertEquals(value, callback.indexInsert.value); + assertEquals(staticTable, callback.indexInsert.staticTable); + } + + @RepeatedTest(10) + public void testInsertWithLiteralName() { + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | H | Name Length (5+) | + // +---+---+---+-------------------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + + QPACK.Logger logger = QPACK.getLogger() + .subLogger("testInsertWithLiteralName"); + TestCallback callback = new TestCallback(); + + StringWriter stringWriter = new StringWriter(); + boolean huffman = RANDOM.nextBoolean(); + + int payload; + if (huffman) { + payload = 0b0110_0000; // static table = true + } else { + payload = 0b0100_0000; // static table = false + } + + //generate name and value then calculate the size required for the bytebuffer + String name = randomString(); + String value = randomString(); + + int bufferSize = requiredBufferSize(5, name.length()); + bufferSize += requiredBufferSize(7, value.length()); + bufferSize += value.length() + name.length(); + + List byteBuffers = new ArrayList<>(); + + // configure the stringWriter to take name + stringWriter.configure(name, 5, payload, huffman); + + boolean firstStringWriterFinished = false; + boolean secondStringWriterFinished = false; + + // Feed the writers with bytebuffers of random size until they total bufferSize, + // once each bytebuffer is full add it to byteBufferList to be read later + while (!secondStringWriterFinished) { + int randomSize = RANDOM.nextInt(0, bufferSize + 1); + bufferSize -= randomSize; + + ByteBuffer byteBuffer = ByteBuffer.allocate(randomSize); + + if (!firstStringWriterFinished) { + firstStringWriterFinished = stringWriter.write(byteBuffer); + + if (!firstStringWriterFinished) { + // writer not finished, add bytebuffer to array of full bytebuffers + // then loop + byteBuffer.flip(); + byteBuffers.add(byteBuffer); + continue; + } else { + // if the name has been written then reset the stringWriter + // so that it can be reused for value + stringWriter.reset(); + stringWriter.configure(value, huffman); + } + } + + // this stage should only be reached if the name has already been written + secondStringWriterFinished = stringWriter.write(byteBuffer); + byteBuffer.flip(); + byteBuffers.add(byteBuffer); + } + + System.err.println(name + " Attempting to insert value: " + value); + + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + + for (var byteBuffer : byteBuffers) { + encoderInstructionsReader.read(byteBuffer, -1); + } + + assertEquals(name, callback.lastInsert.name); + assertEquals(value, callback.lastInsert.value); + } + + @RepeatedTest(5) + public void testDuplicate() { + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | Index (5+) | + // +---+---+---+-------------------+ + // + + QPACK.Logger logger = QPACK.getLogger() + .subLogger("testDuplicate"); + TestCallback callback = new TestCallback(); + + long index = RANDOM.nextLong(0, DT_NAMES.size()); + + IntegerWriter integerWriter = new IntegerWriter(); + int bufferSize = requiredBufferSize(5, index); + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); + int payload = 0b0000_0000; + integerWriter.configure(index, 5, payload); + boolean result = integerWriter.write(byteBuffer); + assert result; + + byteBuffer.flip(); + + encoderInstructionsReader = new EncoderInstructionsReader(callback, logger); + encoderInstructionsReader.read(byteBuffer, -1); + + assertEquals(index, callback.duplicate.get()); + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + static int requiredBufferSize(int N, long value) { + checkPrefix(N); + int size = 1; + int max = (2 << (N - 1)) - 1; + if (value < max) { + return size; + } + size++; + value -= max; + while (value >= 128) { + value /= 128; + size++; + } + return size; + } + + private static final int TEST_STR_MAX_LENGTH = 20; + private static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum.""" + .replaceAll(" ", "") + .replaceAll("\\W", ""); + + private static final List DT_NAMES = generateTableNames(40); + + private static List generateTableNames(int count) { + return IntStream.range(0, count) + .boxed() + .map(i -> randomString()) + .toList(); + } + + private static String randomString() { + int lower = RANDOM.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + return LOREM.substring(lower, 1 + lower + RANDOM.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestCallback implements Callback { + + record LiteralInsert(String name, String value) { + } + + record IndexedInsert(boolean staticTable, Long nameIndex, String value) { + } + + final AtomicLong capacity = new AtomicLong(-1L); + final AtomicLong duplicate = new AtomicLong(-1L); + LiteralInsert lastInsert; + + IndexedInsert indexInsert; + + @Override + public void onCapacityUpdate(long capacity) { + this.capacity.set(capacity); + } + + @Override + public void onInsert(String name, String value) { + lastInsert = new LiteralInsert(name, value); + } + + @Override + public void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString) { + indexInsert = new IndexedInsert(indexInStaticTable, nameIndex, valueString); + } + + @Override + public void onDuplicate(long duplicateValue) { + this.duplicate.set(duplicateValue); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderInstructionsWriterTest.java b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsWriterTest.java new file mode 100644 index 00000000000..fd1fc41d8f7 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderInstructionsWriterTest.java @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader; +import jdk.internal.net.http.qpack.readers.EncoderInstructionsReader.Callback; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.EncoderInstructionsWriter; +import jdk.test.lib.RandomFactory; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.Assert; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static jdk.internal.net.http.qpack.TableEntry.EntryType.NAME; +import static org.junit.jupiter.api.Assertions.*; + +/* + * @test + * @key randomness + * @library /test/lib + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run junit/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL EncoderInstructionsWriterTest + */ + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class EncoderInstructionsWriterTest { + + @RepeatedTest(10) + public void tableCapacityInstructionTest() throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("tableCapacityInstructionTest"); + var dynamicTable = new DynamicTable(logger); + // Generate random capacity value + long capacity = RANDOM.nextLong(IntegerReader.QPACK_MAX_INTEGER_VALUE); + logger.log(System.Logger.Level.TRACE, "Capacity value = " + capacity); + + // Initial dynamic table capacity - required to check for + // writer changing the capacity once the instruction is written. + long initialTableCapacity = capacity == 1234L ? 1235L : 1234L; + + // Create and configure encoder instruction writer for writing the table + // capacity update instruction + var encoderInstructionsWriter = new EncoderInstructionsWriter(); + int calculatedInstructionSize = + encoderInstructionsWriter.configureForTableCapacityUpdate(capacity); + + // Set max capacity to maximum possible value + dynamicTable.setMaxTableCapacity(IntegerReader.QPACK_MAX_INTEGER_VALUE); + + // Create dynamic table with initial capacity + dynamicTable.setCapacity(initialTableCapacity); + + // Perform write operation and then read the capacity update instruction + var callback = new TestEncoderInstructionsCallback(dynamicTable); + + int bytesWritten = writeThenReadInstruction(encoderInstructionsWriter, + callback, -1, dynamicTable, + (dt) -> dt.capacity() == initialTableCapacity, + logger); + + // We expect here to get a callback with the capacity value supplied + // to the instruction writer + assertEquals(capacity, callback.capacityFromCallback.get()); + + // We don't expect dynamic table capacity to be updated by the encoder + // instruction reader + assertNotEquals(capacity, dynamicTable.capacity()); + + // Check if size calculated by the EncoderInstructionsWriter matches + // the number of bytes written to the byte buffers + assertEquals(calculatedInstructionSize, bytesWritten); + } + + @ParameterizedTest + @MethodSource("nameReferenceInsertSource") + public void insertWithNameReferenceInstructionTest(boolean referencingStatic, + long nameIndex, int byteBufferSize) + throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("insertWithNameReferenceInstructionTest"); + var dynamicTable = dynamicTable(logger); + + // generate random value String + String value = randomString(); + TableEntry entry = new TableEntry(referencingStatic, nameIndex, "", value, NAME); + + // Create and configure encoder instruction writer for writing + // the "Insert With Name Reference" instruction + var writer = new EncoderInstructionsWriter(); + int calculatedInstructionSize = writer.configureForEntryInsertion(entry); + + // Perform write operation and then read back the insert entry instruction + var callback = new TestEncoderInstructionsCallback(dynamicTable); + int bytesWritten = writeThenReadInstruction(writer, callback, byteBufferSize, + dynamicTable, (dt) -> true, logger); + + // Check that reader callback values match values supplied to the writer + assertEquals(nameIndex, callback.lastNameInsert.index()); + assertEquals(value, callback.lastNameInsert.value()); + assertEquals(referencingStatic, callback.lastNameInsert.isStaticTable()); + + // Check if size calculated by the EncoderInstructionsWriter matches + // the number of bytes written to the byte buffers + assertEquals(calculatedInstructionSize, bytesWritten); + } + + private static Stream nameReferenceInsertSource() { + Stream staticTableCases = + RANDOM.longs(10, 0, + StaticTable.HTTP3_HEADER_FIELDS.size()) + .boxed() + .map(index -> Arguments.of(true, index, + RANDOM.nextInt(1, 65))); + Stream dynamicTableCases = + RANDOM.longs(10, 0, + DT_NAMES.size()) + .boxed() + .map(index -> Arguments.of(false, index, + RANDOM.nextInt(1, 65))); + return Stream.concat(staticTableCases, dynamicTableCases); + } + + @RepeatedTest(10) + public void insertWithLiteralInstructionTest() throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("insertWithLiteralInstructionTest"); + var dynamicTable = dynamicTable(logger); + + // Generate random strings for name:value entry + String name = randomString(); + String value = randomString(); + var tableEntry = new TableEntry(name, value); + // Create and configure encoder instruction writer for writing the "Insert With Literal Name" + // instruction + var writer = new EncoderInstructionsWriter(); + int calculatedInstructionSize = writer.configureForEntryInsertion(tableEntry); + + var callback = new TestEncoderInstructionsCallback(dynamicTable); + int writtenBytes = writeThenReadInstruction(writer, callback, -1, + dynamicTable, (dt) -> true, logger); + + // Check that reader callback values match values supplied to the writer + assertEquals(name, callback.lastLiteralInsert.name()); + assertEquals(value, callback.lastLiteralInsert.value()); + // Check if size calculated by the EncoderInstructionsWriter matches the number of + // bytes written to the byte buffers + assertEquals(calculatedInstructionSize, writtenBytes); + } + + @RepeatedTest(10) + public void duplicateInstructionTest() throws Exception { + // Get test-case specific logger and a dynamic table instance + QPACK.Logger logger = QPACK.getLogger() + .subLogger("duplicateInstructionTest"); + var dynamicTable = dynamicTable(logger); + // Absolute id to duplicate + // size() - 1 is used to not duplicate the last entry, otherwise the + // DynamicTable.duplicate(relativeIndex) checks below will fail + long idToDuplicate = RANDOM.nextLong(0, DT_NAMES.size() - 1); + // Create and configure encoder instruction writer for writing the "Duplicate" + // instruction + var writer = new EncoderInstructionsWriter(); + // insert count - 1 is the head element index + long relativeIndex = dynamicTable.insertCount() - 1 - idToDuplicate; + int calculatedInstructionSize = writer.configureForEntryDuplication(relativeIndex); + var callback = new TestEncoderInstructionsCallback(dynamicTable); + int writtenBytes = writeThenReadInstruction(writer, callback, -1, + dynamicTable, (dt) -> true, logger); + HeaderField original = dynamicTable.get(idToDuplicate); + HeaderField head = dynamicTable.getRelative(0); + + // Check that reader callback values match values supplied to the writer + assertEquals(relativeIndex, callback.duplicateIdFromCallback.get()); + // Check that DynamicTable.duplicate(relativeIndex) properly recreates + // the referenced entry + assertEquals(head.name(), original.name()); + assertEquals(head.value(), original.value()); + // Check if size calculated by the EncoderInstructionsWriter matches the number + // of bytes written to the byte buffers + assertEquals(calculatedInstructionSize, writtenBytes); + + } + + // Test runner method that writes an instruction with the supplied encoder + // instructions writer pre-configured for writing of a specific instruction. + private static int writeThenReadInstruction( + EncoderInstructionsWriter writer, TestEncoderInstructionsCallback callback, + int bufferSize, DynamicTable dynamicTable, + Function partialWriteCheck, + QPACK.Logger logger) throws Exception { + var buffers = new ArrayList(); + boolean writeDone = false; + int writtenBytes = 0; + // Write instruction to the list of byte buffers + while (!writeDone) { + int allocSize = bufferSize == -1 ? RANDOM.nextInt(1, 65) : bufferSize; + var buffer = ByteBuffer.allocate(allocSize); + writeDone = writer.write(buffer); + writtenBytes += buffer.position(); + if (!writeDone && !partialWriteCheck.apply(dynamicTable)) { + Assert.fail("Wrong dynamic table state after partial write"); + } + buffer.flip(); + buffers.add(buffer); + } + + // Read back the data from byte buffers + var encoderInstructionReader = new EncoderInstructionsReader(callback, logger); + + // Read out an instruction and return the callback instance + for (var bb : buffers) { + encoderInstructionReader.read(bb, -1); + } + return writtenBytes; + } + + private static final Random RANDOM = RandomFactory.getRandom(); + private static final int TEST_STR_MAX_LENGTH = 20; + private static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum.""" + .replaceAll(" ", "") + .replaceAll("\\W", ""); + + private static final List DT_NAMES = generateTableNames(40); + + private static List generateTableNames(int count) { + return IntStream.range(0, count) + .boxed() + .map(i -> randomString()) + .toList(); + } + + private static DynamicTable dynamicTable(QPACK.Logger logger) { + var dt = new DynamicTable(logger.subLogger("dynamicTable")); + dt.setMaxTableCapacity(4096); + dt.setCapacity(4096); + for (var name : DT_NAMES) { + dt.insert(name, randomString()); + } + return dt; + } + + private static String randomString() { + int lower = RANDOM.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + return LOREM.substring(lower, 1 + lower + RANDOM.nextInt(TEST_STR_MAX_LENGTH)); + } + + private static class TestEncoderInstructionsCallback implements Callback { + + final DynamicTable dynamicTable; + public TestEncoderInstructionsCallback(DynamicTable dynamicTable) { + this.dynamicTable = dynamicTable; + } + + record LiteralInsert(String name, String value) { + } + + record IndexedNameInsert(boolean isStaticTable, long index, String value) { + } + + final AtomicLong capacityFromCallback = new AtomicLong(-1L); + final AtomicLong duplicateIdFromCallback = new AtomicLong(-1L); + LiteralInsert lastLiteralInsert; + IndexedNameInsert lastNameInsert; + + @Override + public void onCapacityUpdate(long capacity) { + capacityFromCallback.set(capacity); + } + + @Override + public void onInsert(String name, String value) { + lastLiteralInsert = new LiteralInsert(name, value); + } + + @Override + public void onInsertIndexedName(boolean indexInStaticTable, long nameIndex, String valueString) { + lastNameInsert = new IndexedNameInsert(indexInStaticTable, nameIndex, valueString); + } + + @Override + public void onDuplicate(long l) { + dynamicTable.duplicate(l); + duplicateIdFromCallback.set(l); + } + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EncoderTest.java b/test/jdk/java/net/httpclient/qpack/EncoderTest.java new file mode 100644 index 00000000000..a80a0899420 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EncoderTest.java @@ -0,0 +1,670 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.common.SequentialScheduler; +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.hpack.QuickHuffman; +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.http3.streams.Http3Streams.StreamType; +import jdk.internal.net.http.http3.streams.QueuingStreamPair; +import jdk.internal.net.http.http3.streams.UniStreamPair; +import jdk.internal.net.http.qpack.Encoder; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.writers.StringWriter; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.quic.ConnectionTerminator; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.http.quic.QuicEndpoint; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.http.quic.streams.QuicBidiStream; +import jdk.internal.net.http.quic.streams.QuicReceiverStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream; +import jdk.internal.net.http.quic.streams.QuicSenderStream.SendingStreamState; +import jdk.internal.net.http.quic.streams.QuicStream; +import jdk.internal.net.http.quic.streams.QuicStreamWriter; +import jdk.internal.net.quic.QuicTLSEngine; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.testng.Assert.*; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng/othervm EncoderTest + */ +public class EncoderTest { + private final Random random = new Random(); + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter stringWriter = new StringWriter(); + private static final int TEST_STR_MAX_LENGTH = 10; + + @DataProvider(name = "indexProvider") + public Object[][] indexProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(headerField -> List.of(tableIndex.getAndIncrement(), headerField)) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider + public Object[][] staticNameReferenceProvider() { + AtomicInteger tableIndex = new AtomicInteger(); + Map> map = new HashMap<>(); + for (var headerField : StaticTable.HTTP3_HEADER_FIELDS) { + var name = headerField.name(); + var index = tableIndex.getAndIncrement(); + if (!map.containsKey(name)) + map.put(name, new ArrayList<>()); + map.get(name).add(index); + } + return map.entrySet().stream() + .map(e -> List.of(e.getKey(), randomString(), e.getValue())) + .map(List::toArray).toArray(Object[][]::new); + } + + @DataProvider + public Object[][] literalsProvider() { + var output = new String[100][]; + for (int i = 0; i < 100; i++) { + output[i] = new String[]{ randomString(), randomString() }; + } + return output; + } + + private static class TestErrorHandler implements + UniStreamPair.StreamErrorHandler { + final Consumer handler; + private TestErrorHandler(Consumer handler) { + this.handler = handler; + } + @Override + public void onError(QuicStream stream, UniStreamPair uniStreamPair, Throwable throwable) { + handler.accept(throwable); + } + public static TestErrorHandler of(Consumer handler) { + return new TestErrorHandler(handler); + } + } + + QueuingStreamPair createEncoderStreams(Consumer receiver, + Consumer errorHandler, + TestQuicConnection quicConnection) { + return new QueuingStreamPair(StreamType.QPACK_ENCODER, + quicConnection, + receiver, + TestErrorHandler.of(errorHandler), + Utils.getDebugLogger(() -> "quic-encoder-test") + ); + } + + private void assertNotFailed(AtomicReference errorRef) { + var error = errorRef.get(); + if (error != null) throw new AssertionError(error); + } + + private static void qpackErrorHandler(Throwable error, Http3Error http3Error) { + fail(http3Error + "QPACK error:" + http3Error, error); + } + + @Test(dataProvider = "indexProvider") + public void testFieldLineWriterWithStaticIndex(int index, HeaderField h) { + var actual = allocateIndexBuffer(index); + var expected = writeIndex(index); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> false, + (receiver) -> createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + + encoder.header(context, h.name(), h.value(), false); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + assertEquals(actual, expected, debug(h.name(), h.value(), actual, expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "staticNameReferenceProvider") + public void testInsertWithStaticTableNameReference(String name, String value, List validIndices) { + int index = Collections.max(validIndices); + + var actual = allocateInsertNameRefBuffer(index, value); + var expected = writeInsertNameRef(value, validIndices); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> true, + (receiver) -> createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + configureDynamicTableSize(encoder); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, false); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + TestQuicStreamWriter quicStreamWriter = quicConnection.sender.writer; + + assertTrue(expected.contains(quicStreamWriter.get()), debug(name, value, quicStreamWriter.get(), expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "staticNameReferenceProvider") + public void testFieldLineWithStaticTableNameReference(String name, String value, List validIndices) { + int index = Collections.max(validIndices); + boolean sensitive = random.nextBoolean(); + + var actual = allocateNameRefBuffer(index, value); + var expected = writeNameRef(sensitive, value, validIndices); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> false, (receiver) -> + createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, sensitive); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + assertTrue(expected.contains(actual), debug(name, value, actual, expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "literalsProvider") + public void testInsertWithLiterals(String name, String value) { + var expected = writeInsertLiteral(name, value); + var actual = allocateInsertLiteralBuffer(name, value); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> true, (receiver) -> + createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + configureDynamicTableSize(encoder); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, false); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + TestQuicStreamWriter quicStreamWriter = quicConnection.sender.writer; + assertEquals(quicStreamWriter.get(), expected, debug(name, value, quicStreamWriter.get(), expected)); + assertNotFailed(error); + } + + @Test(dataProvider = "literalsProvider") + public void testFieldLineEncodingWithLiterals(String name, String value) { + boolean sensitive = random.nextBoolean(); + + var expected = writeLiteral(sensitive, name, value); + var actual = allocateLiteralBuffer(name, value); + var quicConnection = new TestQuicConnection(); + AtomicReference error = new AtomicReference<>(); + var encoder = new Encoder(insert -> false, (receiver) -> + createEncoderStreams(receiver, error::set, quicConnection), + EncoderTest::qpackErrorHandler); + + var headerFrameWriter = encoder.newHeaderFrameWriter(); + // create encoding context + Encoder.EncodingContext context = + encoder.newEncodingContext(0, 0, headerFrameWriter); + encoder.header(context, name, value, sensitive); + headerFrameWriter.write(actual); + assertNotEquals(actual.position(), 0); + actual.flip(); + + assertEquals(actual, expected, debug(name, value, actual, expected)); + assertNotFailed(error); + } + + // Test cases which test insertion of entries to the dynamic need to have + // dynamic table with non-zero capacity + private static void configureDynamicTableSize(Encoder encoder) { + // Set encoder maximum dynamic table capacity + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, 256); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + encoder.configure(settings); + // Set dynamic table capacity that doesn't exceed the max capacity value + encoder.setTableCapacity(256); + } + + /* Test Methods */ + private class TestQuicStreamWriter extends QuicStreamWriter { + volatile ByteBuffer b = null; + final TestQuicSenderStream sender; + + TestQuicStreamWriter(SequentialScheduler scheduler, TestQuicSenderStream sender) { + super(scheduler); + this.sender = sender; + } + + private void write(ByteBuffer bb) { + b = bb; + } + public ByteBuffer get() { + if (b == null) { + fail("TestQuicStreamWriter buffer is null"); + } + return b; + } + @Override + public SendingStreamState sendingState() { return null;} + @Override + public void scheduleForWriting(ByteBuffer buffer, boolean last) throws IOException { + write(buffer); + } + @Override + public void queueForWriting(ByteBuffer buffer) throws IOException { + write(buffer); + } + @Override + public long credit() { return Long.MAX_VALUE;} + @Override + public void reset(long errorCode) {} + @Override + public QuicSenderStream stream() { + return connected() ? sender : null; + } + @Override + public boolean connected() { + return sender.writer == this; + } + } + + private class TestQuicConnection extends QuicConnection { + final TestQuicSenderStream sender = new TestQuicSenderStream(); + @Override + public boolean isOpen() {return true;} + @Override + public TerminationCause terminationCause() {return null;} + @Override + public QuicTLSEngine getTLSEngine() {return null;} + @Override + public InetSocketAddress peerAddress() {return null;} + @Override + public SocketAddress localAddress() {return null;} + @Override + public CompletableFuture startHandshake() {return null;} + @Override + public CompletableFuture openNewLocalBidiStream(Duration duration) { + return null; + } + @Override + public CompletableFuture openNewLocalUniStream(Duration duration) { + return MinimalFuture.completedFuture(sender); + } + @Override + public void addRemoteStreamListener(Predicate streamConsumer) { + } + @Override + public boolean removeRemoteStreamListener(Predicate streamConsumer) { + return false; + } + @Override + public Stream quicStreams() { + return null; + } + @Override + public CompletableFuture handshakeReachedPeer() { + return MinimalFuture.completedFuture(null); + } + @Override + public CompletableFuture requestSendPing() { + return MinimalFuture.completedFuture(-1L); + } + + @Override + public ConnectionTerminator connectionTerminator() { + return null; + } + + @Override + public String dbgTag() { return null; } + + @Override + public String logTag() { + return null; + } + } + + class TestQuicSenderStream implements QuicSenderStream { + private static AtomicLong ids = new AtomicLong(); + private final long id; + TestQuicStreamWriter writer; + TestQuicSenderStream() { + id = ids.getAndIncrement() * 4 + type(); + } + @Override + public SendingStreamState sendingState() { return SendingStreamState.READY; } + @Override + public QuicStreamWriter connectWriter(SequentialScheduler scheduler) { + return writer = new TestQuicStreamWriter(scheduler, this); + } + @Override + public void disconnectWriter(QuicStreamWriter writer) { } + @Override + public void reset(long errorCode) { } + @Override + public long dataSent() { return 0; } + @Override + public long streamId() { return id; } + @Override + public StreamMode mode() { return null; } + @Override + public boolean isClientInitiated() { return true; } + @Override + public boolean isServerInitiated() { return false; } + @Override + public boolean isBidirectional() { return false; } + @Override + public boolean isLocalInitiated() { return true; } + @Override + public boolean isRemoteInitiated() { return false; } + @Override + public int type() { return 0x02; } + @Override + public StreamState state() { return SendingStreamState.READY; } + + @Override + public long sndErrorCode() { return -1; } + @Override + public boolean stopSendingReceived() { return false; } + } + + private String debug(String name, String value, ByteBuffer actual, ByteBuffer expected) { + return debug(name, value, actual, List.of(expected)); + } + + private String debug(String name, String value, ByteBuffer actual, List expected) { + var output = new StringBuilder(); + output.append("\n\nBUFFER CONTENTS\n"); + output.append("----------------\n"); + output.append("DEBUG[NAME]: %s\nDEBUG[VALUE]: %s\n".formatted(name, value)); + output.append("DEBUG[ACTUAL]: "); + for (byte b : actual.array()) { + output.append("(%s,%d) ".formatted(Integer.toBinaryString(b & 0xFF), (int)(b & 0xFF))); + } + output.append("\n"); + + output.append("DEBUG[EXPECTED]: "); + for (var bb : expected) { + for (byte b : bb.array()) { + output.append("(%s,%d) ".formatted(Integer.toBinaryString(b & 0xFF), (int) (b & 0xFF))); + } + output.append("\n"); + } + return output.toString(); + } + + private ByteBuffer writeIndex(int index) { + int N = 6; + int payload = 0b1100_0000; // use static table = true; + var bb = ByteBuffer.allocate(2); + + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + bb.flip(); + return bb; + } + + private List writeNameRef(boolean sensitive, String value, List validIndices) { + int N = 4; + int payload = 0b0101_0000; // static table = true + return writeNameRef(N, payload, sensitive, value, validIndices); + } + + private List writeInsertNameRef(String value, List validIndices) { + int N = 6; + int payload = 0b1100_0000; // static table = true + return writeNameRef(N, payload, false, value, validIndices); + } + + private List writeNameRef(int N, int payload, boolean sensitive, String value, List validIndices) { + // Each Header name may have several valid indices associated with it. + List output = new ArrayList<>(); + for (int index : validIndices) { + if (sensitive) + payload |= 0b0010_0000; + var bb = allocateNameRefBuffer(N, index, value); + intWriter.configure(index, N, payload); + intWriter.write(bb); + intWriter.reset(); + + boolean huffman = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffman) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffman); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + output.add(bb); + } + + return output; + } + + private ByteBuffer writeInsertLiteral(String name, String value) { + int N = 5; + int payload = 0b0100_0000; + boolean huffmanName = QuickHuffman.isHuffmanBetterFor(name); + if (huffmanName) + payload |= 0b0010_0000; + return writeLiteral(N, payload, name, value); + } + + private ByteBuffer writeLiteral(boolean sensitive, String name, String value) { + int N = 3; + int payload = 0b0010_0000; // static table = true + if (sensitive) + payload |= 0b0001_0000; + + if (QuickHuffman.isHuffmanBetterFor(name)) + payload |= 0b0000_1000; + return writeLiteral(N, payload, name, value); + } + + private ByteBuffer writeLiteral(int N, int payload, String name, String value) { + var bb = allocateLiteralBuffer(N, name, value); + + boolean huffmanName = QuickHuffman.isHuffmanBetterFor(name); + stringWriter.configure(name, N, payload, huffmanName); + stringWriter.write(bb); + stringWriter.reset(); + + boolean huffmanValue = QuickHuffman.isHuffmanBetterFor(value); + int huffmanMask = 0b0000_0000; + if (huffmanValue) + huffmanMask = 0b1000_0000; + stringWriter.configure(value, 7, huffmanMask, huffmanValue); + stringWriter.write(bb); + stringWriter.reset(); + + bb.flip(); + return bb; + } + + private ByteBuffer allocateIndexBuffer(int index) { + /* + * Note on Integer Representation used for storing the length of name and value strings. + * Taken from RFC 7541 Section 5.1 + * + * "An integer is represented in two parts: a prefix that fills the current octet and an + * optional list of octets that are used if the integer value does not fit within the + * prefix. The number of bits of the prefix (called N) is a parameter of the integer + * representation. If the integer value is small enough, i.e., strictly less than 2N-1, it + * is encoded within the N-bit prefix. + * + * ... + * + * Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2N-1, is + * encoded using a list of one or more octets. The most significant bit of each octet is + * used as a continuation flag: its value is set to 1 except for the last octet in the list. + * The remaining bits of the octets are used to encode the decreased value." + * + * Use "null" for name, if name isn't being provided (i.e. for a nameRef); otherwise, buffer + * will be too large. + * + */ + int N = 6; // bits available in first byte + int size = 1; + index -= Math.pow(2, N) - 1; // number that you can store in first N bits + while (index >= 0) { + index -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateInsertNameRefBuffer(int index, CharSequence value) { + int N = 6; + return allocateNameRefBuffer(N, index, value); + } + + private ByteBuffer allocateNameRefBuffer(int index, CharSequence value) { + int N = 4; + return allocateNameRefBuffer(N, index, value); + } + + private ByteBuffer allocateNameRefBuffer(int N, int index, CharSequence value) { + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = 1 + vlen; + + index -= Math.pow(2, N) - 1; + while (index >= 0) { + index -= 127; + size++; + } + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocateInsertLiteralBuffer(CharSequence name, CharSequence value) { + int N = 5; + return allocateLiteralBuffer(N, name, value); + } + + private ByteBuffer allocateLiteralBuffer(CharSequence name, CharSequence value) { + int N = 3; + return allocateLiteralBuffer(N, name, value); + } + + private ByteBuffer allocateLiteralBuffer(int N, CharSequence name, CharSequence value) { + int nlen = Math.min(QuickHuffman.lengthOf(name), name.length()); + int vlen = Math.min(QuickHuffman.lengthOf(value), value.length()); + int size = nlen + vlen; + + nlen -= Math.pow(2, N) - 1; + size++; + while (nlen >= 0) { + nlen -= 127; + size++; + } + + vlen -= 127; + size++; + while (vlen >= 0) { + vlen -= 127; + size++; + } + return ByteBuffer.allocate(size); + } + + static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur.Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum."""; + + private String randomString() { + int lower = random.nextInt(LOREM.length() - TEST_STR_MAX_LENGTH); + /** + * The empty string ("") is a valid value String in the static table and the random + * String returned cannot refer to a entry in the table. Therefore, we set the upper + * bound below to a minimum of 1. + */ + return LOREM.substring(lower, 1 + lower + random.nextInt(TEST_STR_MAX_LENGTH)); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/EntriesEvictionTest.java b/test/jdk/java/net/httpclient/qpack/EntriesEvictionTest.java new file mode 100644 index 00000000000..c295ec5d01d --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/EntriesEvictionTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary test dynamic table entry eviction scenarios + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA EntriesEvictionTest + */ + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.Encoder.SectionReference; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPACK.Logger; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class EntriesEvictionTest { + + @Test(dataProvider = "evictionScenarios") + public void evictionInsertionTest(TestHeader headerToAdd, + SectionReference sectionReference, + long insertedId, + long largestEvictedId) { + Logger logger = QPACK.getLogger().subLogger("evictionInsertionTest"); + DynamicTable dynamicTable = new DynamicTable(logger); + + dynamicTable.setMaxTableCapacity(TABLE_CAPACITY); + dynamicTable.setCapacity(TABLE_CAPACITY); + + for (TestHeader header : TEST_HEADERS) { + dynamicTable.insert(header.name, header.value); + } + + // Insert last entry + long id = dynamicTable.insert(headerToAdd.name, headerToAdd.value, sectionReference); + + Assert.assertEquals(id, insertedId); + + if (largestEvictedId != -1) { + // Check that evicted entry with the largest absolute index + // is not accessible + Assert.assertThrows(() -> dynamicTable.get(largestEvictedId)); + // Check that an entry after that can be acquired with its + // absolute index + dynamicTable.get(largestEvictedId + 1); + } + + if (insertedId != -1) { + HeaderField insertedField = dynamicTable.get(insertedId); + Assert.assertEquals(insertedField, + new HeaderField(headerToAdd.name(), headerToAdd.value())); + } + } + + @DataProvider + public static Object[][] evictionScenarios() { + + // Header that requires only one entry to be evicted + String oneSizedValue = HEADER_PREXIX + String.format("%03d", HEADERS_COUNT); + TestHeader headerToAddWithOneEviction = TestHeader.newHeader( + oneSizedValue, oneSizedValue); + + // Header that requires two entries to be evicted + // "e" is repeated 16 times to compensate 32 bytes - 16 in header name, + // another 16 in header value + String doubleSizedHeaderValue = (HEADER_PREXIX + "Dbl").repeat(2) + + "e".repeat(16); + TestHeader headerToAddWithTwoEvictions = TestHeader.newHeader( + doubleSizedHeaderValue, doubleSizedHeaderValue); + + // Construct header with size equals to the dynamic table capacity + // / 2 since the string used two times - for headers name and value + String hugeStrPart1 = TEST_HEADERS.stream().map(TestHeader::name) + .collect(Collectors.joining()); + String hugeStrToCompensate32PerElement = "a".repeat(32 * (HEADERS_COUNT - 1) / 2); + String hugeStr = hugeStrPart1 + hugeStrToCompensate32PerElement; + + TestHeader hugeEntryWithAllEviction = TestHeader.newHeader(hugeStr, hugeStr); + + // Header with size 2 bytes bigger than the dynamic table capacity + TestHeader hugeEntryExceedsCapacity = TestHeader.newHeader( + hugeStr + "H", hugeStr + "H"); + + return new Object[][]{ + // Evict one to have space for a new entry + {headerToAddWithOneEviction, SectionReference.noReferences(), + HEADERS_COUNT, 0}, + + // Evict all entries to have space for a new entry + {hugeEntryWithAllEviction, SectionReference.noReferences(), + HEADERS_COUNT, HEADERS_COUNT - 1}, + + // Not enough capacity for a new entry even if all entries are evicted + {hugeEntryExceedsCapacity, SectionReference.noReferences(), + -1, -1}, + + // Entry with size == capacity and there are section references preventing + // eviction of all entries + {hugeEntryWithAllEviction, new SectionReference(0, 1), + -1, -1}, + + // Element with 0 absolute id is not referenced and therefore can be evicted + {headerToAddWithOneEviction, new SectionReference(1, 2), + HEADERS_COUNT, 0}, + + // Elements with 0 and 1 ids are not referenced and should be + // evicted to insert double-sized entry + {headerToAddWithTwoEvictions, new SectionReference(2, 3), + HEADERS_COUNT, 1}, + + // Element with 1 id cannot be evicted since it is + // referenced + {headerToAddWithTwoEvictions, new SectionReference(1, 3), + -1, -1} + }; + } + + record TestHeader(String name, String value, long size) { + public static TestHeader newHeader(String name, String value) { + return new TestHeader(name, value, 32L + name.length() + value.length()); + } + + @Override + public String toString() { + return name + ":" + value + "[" + size + "]"; + } + } + + // Number of headers to insert before running an eviction scenario + private static final int HEADERS_COUNT = 3; + // Test header prefix + private static final String HEADER_PREXIX = "HeaderPrefix"; + // List of headers to insert before running an eviction scenario + private static final List TEST_HEADERS; + // Table capacity required by test scenarios + private static final long TABLE_CAPACITY; + + static { + List testHeaders = new ArrayList<>(); + long capacity = 0; + + // List of headers to prepopulate dynamic table before running + // test cases + for (int i = 0; i < HEADERS_COUNT; i++) { + String headerStr = HEADER_PREXIX + String.format("%03d", i); + var header = TestHeader.newHeader(headerStr, headerStr); + capacity += header.size(); + testHeaders.add(header); + } + TEST_HEADERS = testHeaders; + TABLE_CAPACITY = capacity; + } +} diff --git a/test/jdk/java/net/httpclient/qpack/FieldSectionPrefixTest.java b/test/jdk/java/net/httpclient/qpack/FieldSectionPrefixTest.java new file mode 100644 index 00000000000..e9a7a86e975 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/FieldSectionPrefixTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @modules java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @run testng FieldSectionPrefixTest + */ + + +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.FieldSectionPrefix; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.writers.FieldLineSectionPrefixWriter; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicLong; + +public class FieldSectionPrefixTest { + + private static final long DT_CAPACITY = 220L; + private static final long MAX_ENTRIES = DT_CAPACITY / 32L; + + @Test(dataProvider = "encodingCases") + public void encodingTest(long base, long requiredInsertCount, + byte expectedRic, byte expectedBase) { + var fieldSectionPrefix = new FieldSectionPrefix(requiredInsertCount, base); + FieldLineSectionPrefixWriter writer = new FieldLineSectionPrefixWriter(); + int bytesNeeded = writer.configure(fieldSectionPrefix, MAX_ENTRIES); + var byteBuffer = ByteBuffer.allocate(bytesNeeded); + writer.write(byteBuffer); + byteBuffer.flip(); + Assert.assertEquals(byteBuffer.get(0), expectedRic); + Assert.assertEquals(byteBuffer.get(1), expectedBase); + } + + @DataProvider(name = "encodingCases") + public Object[][] encodingCases() { + var cases = new ArrayList(); + // Simple with 0 values + cases.add(new Object[]{0L, 0L, (byte) 0x0, (byte) 0x0}); + // Based on RFC-9204: "B.2. Dynamic Table example" + cases.add(new Object[]{0L, 2L, (byte) 0x3, (byte) 0x81}); + // Based on RFC-9204: "Duplicate Instruction, Stream Cancellation" + cases.add(new Object[]{4L, 4L, (byte) 0x5, (byte) 0x0}); + return cases.toArray(Object[][]::new); + } + + @Test(dataProvider = "decodingCases") + public void decodingTest(long expectedRIC, long expectedBase, byte... bytes) throws IOException { + var logger = QPACK.getLogger().subLogger("decodingTest"); + var dt = new DynamicTable(logger, false); + dt.setMaxTableCapacity(DT_CAPACITY); + dt.setCapacity(DT_CAPACITY); + var callback = new DecodingCallback() { + @Override + public void onDecoded(CharSequence name, CharSequence value) { + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + throw new RuntimeException("Error during Field Line Section Prefix decoding - " + + http3Error + ": " + throwable.getMessage()); + } + + @Override + public long streamId() { + return 0; + } + }; + AtomicLong blockedStreamsCounter = new AtomicLong(); + // maxBlockStreams = 1 is needed for tests with Required Insert Count > 0 - otherwise test + // fails with "QPACK_DECOMPRESSION_FAILED: too many blocked streams" + HeaderFrameReader reader = new HeaderFrameReader(dt, callback, blockedStreamsCounter, + 1, -1, logger); + var bb = ByteBuffer.wrap(bytes); + reader.read(bb, false); + var fsp = reader.decodedSectionPrefix(); + + System.err.println("Required Insert Count:" + fsp.requiredInsertCount()); + System.err.println("Base:" + fsp.base()); + Assert.assertEquals(fsp.requiredInsertCount(), expectedRIC); + Assert.assertEquals(fsp.base(), expectedBase); + + } + + @DataProvider(name = "decodingCases") + public Object[][] decodingCases() { + var cases = new ArrayList(); + cases.add(new Object[]{0L, 0L, (byte) 0x0, (byte) 0x0}); + cases.add(new Object[]{4L, 4L, (byte) 0x5, (byte) 0x0}); + cases.add(new Object[]{2L, 0L, (byte) 0x3, (byte) 0x81}); + return cases.toArray(Object[][]::new); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/IntegerReaderMaxValuesTest.java b/test/jdk/java/net/httpclient/qpack/IntegerReaderMaxValuesTest.java new file mode 100644 index 00000000000..6f21549f497 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/IntegerReaderMaxValuesTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.IntegerReader; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.stream.IntStream; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=INFO + * IntegerReaderMaxValuesTest + */ +public class IntegerReaderMaxValuesTest { + @DataProvider + public Object[][] nValues() { + return IntStream.range(1, 8) + .boxed() + .map(N -> new Object[]{N}) + .toArray(Object[][]::new); + } + + @Test(dataProvider = "nValues") + public void maxIntegerWriteRead(int N) { + IntegerWriter writer = new IntegerWriter(); + writer.configure(IntegerReader.QPACK_MAX_INTEGER_VALUE, N, 0); + ByteBuffer buffer = ByteBuffer.allocate(1024); + writer.write(buffer); + IntegerReader reader = new IntegerReader(); + reader.configure(N); + buffer.flip(); + reader.read(buffer); + long result = reader.get(); + Assert.assertEquals(result, IntegerReader.QPACK_MAX_INTEGER_VALUE); + } + + @Test(dataProvider = "nValues", expectedExceptions = QPackException.class) + public void overflowInteger(int N) { + // Construct buffer with overflowed integer + ByteBuffer overflowBuffer = ByteBuffer.allocate(11); + + overflowBuffer.put((byte) ((2 << (N - 1)) - 1)); + for (int i = 0; i < 9; i++) { + overflowBuffer.put((byte) 128); + } + overflowBuffer.put((byte) 10); + overflowBuffer.flip(); + // Read the buffer with IntegerReader + IntegerReader reader = new IntegerReader(); + reader.configure(N); + reader.read(overflowBuffer); + } +} diff --git a/test/jdk/java/net/httpclient/qpack/StaticTableFieldsTest.java b/test/jdk/java/net/httpclient/qpack/StaticTableFieldsTest.java new file mode 100644 index 00000000000..662d0c264d8 --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/StaticTableFieldsTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @modules java.net.http/jdk.internal.net.http.qpack + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=NORMAL StaticTableFieldsTest + */ + +import jdk.internal.net.http.qpack.StaticTable; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.testng.Assert.assertEquals; + +public class StaticTableFieldsTest { + + @BeforeTest + public void setUp() { + // Populate expected table as defined by RFC + expectedTable = new ArrayList<>(); + String[] arr = staticTableFields.split("\n"); + for (String s : arr) { + s = s.replaceAll("( )+", " "); + // index + int endOfIndex = s.indexOf(" "); + var i = Integer.parseInt(s.substring(0, endOfIndex)); + // name + int endOfName = s.indexOf(" ", endOfIndex + 1); + var n = s.substring(endOfIndex + 1, endOfName).trim(); + // value + var v = s.substring(endOfName + 1).strip(); + expectedTable.add(new TableLine(i, n, v)); + } + // Populate actual static table currently being used by QPACK + actualTable = new ArrayList<>(); + for (int i = 0; i < StaticTable.HTTP3_HEADER_FIELDS.size(); i++) { + var n = StaticTable.HTTP3_HEADER_FIELDS.get(i).name(); + var v = StaticTable.HTTP3_HEADER_FIELDS.get(i).value(); + actualTable.add(new TableLine(i, n, v)); + } + } + + @Test + public void testStaticTable() { + assertEquals(actualTable.size(), expectedTable.size()); + for (int i = 0; i < expectedTable.size(); i++) { + assertEquals(actualTable.get(i).name(), expectedTable.get(i).name()); + assertEquals(actualTable.get(i).value(), expectedTable.get(i).value()); + } + } + + // Copy-Paste of static table from RFC 9204 for QPACK Appendix A + // https://www.rfc-editor.org/rfc/rfc9204.html#name-static-table-2 + String staticTableFields = """ + 0 :authority \s + 1 :path / + 2 age 0 + 3 content-disposition \s + 4 content-length 0 + 5 cookie \s + 6 date \s + 7 etag \s + 8 if-modified-since \s + 9 if-none-match \s + 10 last-modified \s + 11 link \s + 12 location \s + 13 referer \s + 14 set-cookie \s + 15 :method CONNECT + 16 :method DELETE + 17 :method GET + 18 :method HEAD + 19 :method OPTIONS + 20 :method POST + 21 :method PUT + 22 :scheme http + 23 :scheme https + 24 :status 103 + 25 :status 200 + 26 :status 304 + 27 :status 404 + 28 :status 503 + 29 accept */* + 30 accept application/dns-message + 31 accept-encoding gzip, deflate, br + 32 accept-ranges bytes + 33 access-control-allow-headers cache-control + 34 access-control-allow-headers content-type + 35 access-control-allow-origin * + 36 cache-control max-age=0 + 37 cache-control max-age=2592000 + 38 cache-control max-age=604800 + 39 cache-control no-cache + 40 cache-control no-store + 41 cache-control public, max-age=31536000 + 42 content-encoding br + 43 content-encoding gzip + 44 content-type application/dns-message + 45 content-type application/javascript + 46 content-type application/json + 47 content-type application/x-www-form-urlencoded + 48 content-type image/gif + 49 content-type image/jpeg + 50 content-type image/png + 51 content-type text/css + 52 content-type text/html; charset=utf-8 + 53 content-type text/plain + 54 content-type text/plain;charset=utf-8 + 55 range bytes=0- + 56 strict-transport-security max-age=31536000 + 57 strict-transport-security max-age=31536000; includesubdomains + 58 strict-transport-security max-age=31536000; includesubdomains; preload + 59 vary accept-encoding + 60 vary origin + 61 x-content-type-options nosniff + 62 x-xss-protection 1; mode=block + 63 :status 100 + 64 :status 204 + 65 :status 206 + 66 :status 302 + 67 :status 400 + 68 :status 403 + 69 :status 421 + 70 :status 425 + 71 :status 500 + 72 accept-language \s + 73 access-control-allow-credentials FALSE + 74 access-control-allow-credentials TRUE + 75 access-control-allow-headers * + 76 access-control-allow-methods get + 77 access-control-allow-methods get, post, options + 78 access-control-allow-methods options + 79 access-control-expose-headers content-length + 80 access-control-request-headers content-type + 81 access-control-request-method get + 82 access-control-request-method post + 83 alt-svc clear + 84 authorization \s + 85 content-security-policy script-src 'none'; object-src 'none'; base-uri 'none' + 86 early-data 1 + 87 expect-ct \s + 88 forwarded \s + 89 if-range \s + 90 origin \s + 91 purpose prefetch + 92 server \s + 93 timing-allow-origin * + 94 upgrade-insecure-requests 1 + 95 user-agent \s + 96 x-forwarded-for \s + 97 x-frame-options deny + 98 x-frame-options sameorigin + """; + + private List actualTable, expectedTable; + private record TableLine(int index, String name, String value) { } +} diff --git a/test/jdk/java/net/httpclient/qpack/StringLengthLimitsTest.java b/test/jdk/java/net/httpclient/qpack/StringLengthLimitsTest.java new file mode 100644 index 00000000000..ade50d2843b --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/StringLengthLimitsTest.java @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.QPackException; +import jdk.internal.net.http.qpack.readers.HeaderFrameReader; +import jdk.internal.net.http.qpack.readers.StringReader; +import jdk.internal.net.http.qpack.writers.HeaderFrameWriter; +import jdk.internal.net.http.qpack.writers.IntegerWriter; +import jdk.internal.net.http.qpack.writers.StringWriter; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/* + * @test + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.http.qpack.allowBlockingEncoding=true + * StringLengthLimitsTest + */ +public class StringLengthLimitsTest { + + @DataProvider + Object[][] stringReaderLimitsData() { + return new Object[][]{ + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH, false, false}, + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH - 1, false, true}, + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH / 4, true, false}, + {STRING_READER_STRING_LENGTH, STRING_READER_STRING_LENGTH / 4 - 1, true, true} + }; + } + + @Test(dataProvider = "stringReaderLimitsData") + public void stringReaderLimits(int length, int limit, boolean huffmanBit, + boolean exceptionExpected) throws IOException { + IntegerWriter intWriter = new IntegerWriter(); + intWriter.configure(length, 7, huffmanBit ? STRING_READER_HUFFMAN_PAYLOAD : STRING_READER_PAYLOAD); + + var byteBuffer = ByteBuffer.allocate(2); + if (!intWriter.write(byteBuffer)) { + Assert.fail("Error with test buffer preparations"); + } + byteBuffer.flip(); + + StringReader stringReader = new StringReader(); + StringBuilder unusedOutput = new StringBuilder(); + if (exceptionExpected) { + QPackException exception = Assert.expectThrows(QPackException.class, + () -> stringReader.read(byteBuffer, unusedOutput, limit)); + Throwable cause = exception.getCause(); + Assert.assertNotNull(cause); + Assert.assertTrue(cause instanceof ProtocolException); + System.err.println("Got expected ProtocolException: " + cause); + } else { + boolean done = stringReader.read(byteBuffer, unusedOutput, limit); + Assert.assertFalse(done, "read done"); + } + } + + @DataProvider + Object[][] encoderInstructionLimitsData() { + int maxEntrySize = ENCODER_INSTRUCTIONS_DT_CAPACITY - 32; + return new Object[][]{ + // "Insert with Literal Name" instruction tests + // No Huffman, incomplete instruction, enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize, + false, DO_NOT_GENERATE_PART, false, true}, + // No Huffman, incomplete instruction, not enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize + 1, + false, DO_NOT_GENERATE_PART, false, false}, + // No Huffman, full instruction, enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 2, + false, maxEntrySize / 2, false, true}, + // No Huffman, full instruction, not enough space in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 2, + false, 1 + maxEntrySize / 2, false, false}, + // Huffman (name + value), full instruction, enough space + // in the DT + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 4 / 2, + true, maxEntrySize / 4 / 2, true, true}, + // Huffman (value only), full instruction, not enough space + // in the DT. + // +16 term is added to make sure huffman estimate exceeds the limit + {EncoderInstruction.INSERT_LITERAL_NAME, maxEntrySize / 2, + false, 2 * maxEntrySize + 16, true, false}, + + // "Insert with Name Reference" instruction tests + // Enough space in the DT for the value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize, false, true}, + // Not enough space in the DT for the value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize + 1, false, false}, + // Enough space in the DT for the Huffman encoded value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize * 4, true, true}, + // Not enough space in the DT for the Huffman encoded value part + {EncoderInstruction.INSERT_NAME_REFERENCE, DO_NOT_GENERATE_PART, + false, maxEntrySize * 4 + 4, true, false} + }; + } + + @Test(dataProvider = "encoderInstructionLimitsData") + public void encoderInstructionLimits(EncoderInstruction instruction, + int nameLength, boolean nameHuffman, + int valueLength, boolean valueHuffman, + boolean successExpected) { + // Encoder/decoder pair with instruction that has string and with length > limit + var connector = new EncoderDecoderConnector(); + AtomicReference observedError = new AtomicReference<>(); + QPACK.QPACKErrorHandler errorHandler = (throwable, error) -> { + System.err.println("QPACK error observed: " + error); + observedError.set(throwable); + }; + var streamError = new AtomicReference(); + var pair = connector.newEncoderDecoderPair((_) -> true, errorHandler, errorHandler, streamError::set); + + // Configure dynamic tables + var decoderDT = pair.decoderTable(); + var encoderDT = pair.encoderTable(); + encoderDT.setMaxTableCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + encoderDT.setCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + decoderDT.setMaxTableCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + decoderDT.setCapacity(ENCODER_INSTRUCTIONS_DT_CAPACITY); + + // Generate buffers with encoder instruction bytes + var instructionBuffers = generateInstructionBuffers(instruction, + nameLength, nameHuffman, valueLength, valueHuffman); + for (var buffer : instructionBuffers) { + // Submit encoder instruction with test instructions which + // could be incomplete + pair.encoderStreams().submitData(buffer); + } + Throwable error = observedError.get(); + if (successExpected && error != null) { + Assert.fail("Unexpected error", error); + } else if (error == null && !successExpected) { + Assert.fail("Expected error"); + } + } + + /* + * Generate a list of instruction buffers. + * First buffer contains a name part (index or String), + * Second buffer contains a value part (index or String). + * If instruction type is INSERT_NAME_REFERENCE - + * the nameLength and nameHuffman are ignored. + */ + private static List generateInstructionBuffers( + EncoderInstruction instruction, + int nameLength, boolean nameHuffman, + int valueLength, boolean valueHuffman) { + IntegerWriter intWriter = new IntegerWriter(); + StringWriter stringWriter = new StringWriter(); + List instructionBuffers = new ArrayList<>(); + int valuePartPayload = valueHuffman ? + STRING_READER_HUFFMAN_PAYLOAD : STRING_READER_PAYLOAD; + // Configure writers for an instruction + switch (instruction) { + case INSERT_LITERAL_NAME: + int namePartPayload = nameHuffman ? + INSERT_INSTRUCTION_LITERAL_NAME_HUFFMAN_PAYLOAD : + INSERT_INSTRUCTION_LITERAL_NAME_PAYLOAD; + if (valueLength != DO_NOT_GENERATE_PART) { + // Generate data for the name part + var namePartBB = ByteBuffer.allocate(nameLength + 1); + stringWriter.configure("T".repeat(nameLength), 5, namePartPayload, valueHuffman); + boolean nameDone = stringWriter.write(namePartBB); + assert nameDone; + namePartBB.flip(); + instructionBuffers.add(namePartBB); + // Generate data for the value part + var valuePartBB = generatePartialString(7, valuePartPayload, valueLength); + instructionBuffers.add(valuePartBB); + } else { + // Generate data for the name part only + var namePartBB = generatePartialString(5, namePartPayload, nameLength); + instructionBuffers.add(namePartBB); + } + break; + case INSERT_NAME_REFERENCE: + var nameIndexPart = ByteBuffer.allocate(1); + // Write some static table name id + // Referencing static table entry with id = 16, ie ":method" + // nameLength and nameHuffman are ignored + intWriter.configure(16, 6, INSERT_INSTRUCTION_WITH_NAME_REFERENCE_PAYLOAD); + boolean nameIndexDone = intWriter.write(nameIndexPart); + assert nameIndexDone; + nameIndexPart.flip(); + intWriter.reset(); + // Write value part with specified length and huffman encoding + // Generate data for the value part + var valueLengthPart = + generatePartialString(7, valuePartPayload, valueLength); + // Add both parts to the list of forged instruction buffers + instructionBuffers.add(nameIndexPart); + instructionBuffers.add(valueLengthPart); + break; + } + return instructionBuffers; + } + + @DataProvider + Object[][] fieldLineLimitsData() { + return new Object[][]{ + // Post-Base Index + {-1, -1, ENTRY_NAME, ENTRY_VALUE, true, true}, + // Relative Index + {-1, -1, ENTRY_NAME, ENTRY_VALUE, false, true}, + // Post-Base Name Index + {-1, -1, ENTRY_NAME, "X".repeat(ENTRY_VALUE.length()), true, true}, + // Relative Name Index + {-1, -1, ENTRY_NAME, "X".repeat(ENTRY_VALUE.length()), false, true}, + // Post-Base Index, limit is exceeded + {-1, -1, BIG_ENTRY_NAME, BIG_ENTRY_VALUE, true, false}, + // Relative Index, limit is exceeded + {-1, -1, BIG_ENTRY_NAME, BIG_ENTRY_VALUE, false, false}, + // Post-Base Name Index, limit is exceeded + {-1, -1, BIG_ENTRY_NAME, ENTRY_VALUE, true, false}, + // Relative Name Index, limit is exceeded + {-1, -1, ENTRY_NAME, BIG_ENTRY_VALUE, false, false}, + // Name and Value are literals, limit is not exceeded + {ENTRY_NAME.length(), ENTRY_VALUE.length(), null, null, false, true}, + // Name and Value are literals, limit is exceeded in name part + {ENTRY_NAME.length() + ENTRY_VALUE.length() + 1, 0, null, null, false, false}, + // Name and Value are literals, limit is exceeded in value part + {1, ENTRY_NAME.length() + ENTRY_VALUE.length(), null, null, false, false} + + }; + } + + @Test(dataProvider = "fieldLineLimitsData") + public void fieldLineLimits(int nameLength, int valueLength, + String name, String value, + boolean isPostBase, boolean successExpected) throws IOException { + // QPACK writers for test data generations + IntegerWriter intWriter = new IntegerWriter(); + StringWriter stringWriter = new StringWriter(); + + // QPACK error handlers, stream error capture and decoding callback + var encoderError = new AtomicReference(); + var decoderError = new AtomicReference(); + var streamError = new AtomicReference(); + var decodingCallbackError = new AtomicReference(); + + QPACK.QPACKErrorHandler encoderErrorHandler = (throwable, error) -> { + System.err.println("Encoder error observed: " + error); + encoderError.set(throwable); + }; + + QPACK.QPACKErrorHandler decoderErrorHandler = (throwable, error) -> { + System.err.println("Decoder error observed: " + error); + decoderError.set(throwable); + }; + + var decodingCallback = new FieldLineDecodingCallback(decodingCallbackError); + + // Create encoder/decoder pair + var conn = new EncoderDecoderConnector(); + var pair = conn.newEncoderDecoderPair( + // Disallow entries insertion. + // The dynamic table is pre-populated with needed entries + // before the test execution + _ -> false, + encoderErrorHandler, + decoderErrorHandler, + streamError::set); + var encoder = pair.encoder(); + var decoder = pair.decoder(); + + // Set MAX_HEADER_SIZE limit on a decoder side + // Create settings frame with MAX_FIELD_SECTION_SIZE + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + settingsFrame.setParameter(SettingsFrame.SETTINGS_MAX_FIELD_SECTION_SIZE, MAX_FIELD_SECTION_SIZE_SETTING_VALUE); + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, FIELD_LINES_DT_CAPACITY); + pair.decoder().configure(ConnectionSettings.createFrom(settingsFrame)); + + // Configure tables + configureTablesForFieldLinesTest(pair); + + // Encode the section prefix + long RIC = pair.encoderTable().insertCount(); + long base = isPostBase ? 0 : RIC; + HeaderFrameWriter writer = encoder.newHeaderFrameWriter(); + HeaderFrameReader reader = decoder.newHeaderFrameReader(decodingCallback); + var encodingContext = encoder.newEncodingContext(123, base, writer); + List buffers = new ArrayList<>(); + + if (nameLength == -1 && valueLength == -1) { + // Test configuration for all indexed field line wire formats + // (indexed name and indexed entry) + var headersBuffer = ByteBuffer.allocate(1024); + // knownReceivedCount == InsertCount to allow DT reference encodings, + // since there is one entry in the test dynamic table + encoder.header(encodingContext, name, value, false, RIC); + writer.write(headersBuffer); + headersBuffer.flip(); + buffers.add(headersBuffer); + } else { + if (nameLength > MAX_FIELD_SECTION_SIZE_SETTING_VALUE - DynamicTable.ENTRY_SIZE) { + // if nameLength > limit - only need to generate name part + // We only write partial name part of the "Literal Field Line with Literal + // Name" instruction with String length value + var nameLengthBB = + generatePartialString(3, FIELD_LINE_NAME_VALUE_LITERALS_PAYLOAD, nameLength); + buffers.add(nameLengthBB); + } else if (nameLength + valueLength > + MAX_FIELD_SECTION_SIZE_SETTING_VALUE - DynamicTable.ENTRY_SIZE) { + // if nameLength + valueLength > limit - + // the whole instruction needs to be generated with basic writers + var fieldLineBB = ByteBuffer.allocate(1024); + stringWriter.configure("Z".repeat(nameLength), 3, + FIELD_LINE_NAME_VALUE_LITERALS_PAYLOAD, false); + intWriter.configure(valueLength, 7, 0); + stringWriter.write(fieldLineBB); + intWriter.write(fieldLineBB); + fieldLineBB.flip(); + buffers.add(fieldLineBB); + } else { + // name + value doesn't exceed MAX_FIELD_SECTION_SIZE + var headersBuffer = ByteBuffer.allocate(1024); + // We use 'X' and 'Z' letters to prevent encoder from + // huffman encoding. + encoder.header(encodingContext, + "X".repeat(nameLength), "Z".repeat(valueLength), + false, RIC); + writer.write(headersBuffer); + headersBuffer.flip(); + buffers.add(headersBuffer); + } + } + // Generate field lines section prefix + encoder.generateFieldLineSectionPrefix(encodingContext, buffers); + assert buffers.size() == 2; + + // Decode generated header buffers + decoder.decodeHeader(buffers.get(0), false, reader); + decoder.decodeHeader(buffers.get(1), true, reader); + + // Check if any error is observed and it meets the test expectations + var error = decodingCallbackError.get(); + System.err.println("Decoding callback error: " + error); + if (successExpected && error != null) { + Assert.fail("Unexpected error", error); + } else if (error == null && !successExpected) { + Assert.fail("Error expected"); + } + } + + private static void configureTablesForFieldLinesTest( + EncoderDecoderConnector.EncoderDecoderPair pair) { + // Encoder + pair.encoderTable().setMaxTableCapacity(StringLengthLimitsTest.FIELD_LINES_DT_CAPACITY); + pair.encoderTable().setCapacity(StringLengthLimitsTest.FIELD_LINES_DT_CAPACITY); + + // Decoder max table capacity is set via the settings frame + pair.decoderTable().setCapacity(StringLengthLimitsTest.FIELD_LINES_DT_CAPACITY); + + // Insert test entry to both tables + pair.decoderTable().insert(ENTRY_NAME, ENTRY_VALUE); + pair.encoderTable().insert(ENTRY_NAME, ENTRY_VALUE); + pair.decoderTable().insert(BIG_ENTRY_NAME, BIG_ENTRY_VALUE); + pair.encoderTable().insert(BIG_ENTRY_NAME, BIG_ENTRY_VALUE); + } + + // Encoder instructions under test + public enum EncoderInstruction { + INSERT_LITERAL_NAME, + INSERT_NAME_REFERENCE, + } + + // Decoding callback used by Field Line Representation tests + private static class FieldLineDecodingCallback implements DecodingCallback { + private final AtomicReference decodingError; + + public FieldLineDecodingCallback(AtomicReference decodingCallbackError) { + this.decodingError = decodingCallbackError; + } + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + } + + @Override + public void onComplete() { + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + decodingError.set(throwable); + } + + @Override + public long streamId() { + return 0; + } + } + + // Utility method to generate partial QPack string with length part only + private static ByteBuffer generatePartialString(int N, int payload, int length) { + IntegerWriter intWriter = new IntegerWriter(); + var partialStringBB = ByteBuffer.allocate( + IntegerWriter.requiredBufferSize(N, length)); + intWriter.configure(length, N, payload); + boolean done = intWriter.write(partialStringBB); + assert done; + partialStringBB.flip(); + return partialStringBB; + } + + // Constants for StringReader tests + private static final int STRING_READER_HUFFMAN_PAYLOAD = 0b1000_0000; + private static final int STRING_READER_PAYLOAD = 0b0000_0000; + private static final int STRING_READER_STRING_LENGTH = 32; + + // Constants for Encoder Instructions Reader tests + private static final int ENCODER_INSTRUCTIONS_DT_CAPACITY = 64; + // This constant is used to instruct test data generator not to generate + // value or name (if name is referenced by the DT index) part of the + // decoder instruction + private static final int DO_NOT_GENERATE_PART = -1; + // Encoder instruction payloads used to forge instruction buffers + private static final int INSERT_INSTRUCTION_LITERAL_NAME_HUFFMAN_PAYLOAD = 0b0110_0000; + private static final int INSERT_INSTRUCTION_LITERAL_NAME_PAYLOAD = 0b0100_0000; + private static final int INSERT_INSTRUCTION_WITH_NAME_REFERENCE_PAYLOAD = 0b1100_0000; + private static final int FIELD_LINE_NAME_VALUE_LITERALS_PAYLOAD = 0b0010_0000; + + // Constants for Field Line Representation tests + // Table capacity big enough for insertion of all entries + private static final long FIELD_LINES_DT_CAPACITY = 1024; + private static final String ENTRY_NAME = "FullEntryName"; + private static final String ENTRY_VALUE = "FullEntryValue"; + private static final String BIG_ENTRY_NAME = "FullEntryName_Big"; + private static final String BIG_ENTRY_VALUE = "FullEntryValue_Big"; + private static final long MAX_FIELD_SECTION_SIZE_SETTING_VALUE = + ENTRY_NAME.length() + ENTRY_VALUE.length() + DynamicTable.ENTRY_SIZE; + +} diff --git a/test/jdk/java/net/httpclient/qpack/TablesIndexerTest.java b/test/jdk/java/net/httpclient/qpack/TablesIndexerTest.java new file mode 100644 index 00000000000..4ed489ca6dd --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/TablesIndexerTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @modules java.net.http/jdk.internal.net.http.qpack + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=INFO TablesIndexerTest + */ + +import jdk.internal.net.http.qpack.DynamicTable; +import jdk.internal.net.http.qpack.HeaderField; +import jdk.internal.net.http.qpack.QPACK; +import jdk.internal.net.http.qpack.StaticTable; +import jdk.internal.net.http.qpack.TableEntry; +import jdk.internal.net.http.qpack.TablesIndexer; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static jdk.internal.net.http.qpack.TableEntry.EntryType; + +public class TablesIndexerTest { + + @DataProvider(name = "indicesLookupData") + public Object[][] indicesData() { + List tcs = new ArrayList<>(); + + long index = 0; + for (HeaderField f : StaticTable.HTTP3_HEADER_FIELDS) { + // Full name:value match + tcs.add(new Object[]{ + f.name(), f.value(), f.value(), + Set.of(index), EntryType.NAME_VALUE + }); + // Static and Dynamic tables contain only name match - we expect static + // NAME only entry to be returned + tcs.add(new Object[]{ + f.name(), "NotInStatic", "InDynamic", + acceptableIndicesForName(f.name()), EntryType.NAME}); + index++; + } + return tcs.toArray(Object[][]::new); + } + + @Test(dataProvider = "indicesLookupData") + public void checkIndicesLookup(String name, String value, + String dynamicTableValue, + Set indices, EntryType type) { + // We construct dynamic table with the same name value to check that + // static entry is returned first + DynamicTable dt = new DynamicTable(QPACK.getLogger().subLogger( + "checkStaticIndicesLookup")); + dt.setMaxTableCapacity(256); + dt.setCapacity(256); + dt.insert(name, dynamicTableValue); + + // Construct TablesIndexer + TablesIndexer tablesIndexer = new TablesIndexer(STATIC_TABLE, dt); + + // Use TablesIndexer to locate TableEntry + TableEntry tableEntry = tablesIndexer.entryOf(name, value, + IGNORE_RECEIVED_COUNT_CHECK); + + // TableEntry should be for static table only + Assert.assertTrue(tableEntry.isStaticTable()); + + // If value is not equal to dynamicTableValue, the full name:dynamicTableValue + // should be found in the dynamic table with index 0 + if (!value.equals(dynamicTableValue)) { + TableEntry dtEntry = tablesIndexer.entryOf(name, dynamicTableValue, + IGNORE_RECEIVED_COUNT_CHECK); + Assert.assertFalse(dtEntry.isStaticTable()); + Assert.assertEquals(dtEntry.type(), EntryType.NAME_VALUE); + Assert.assertEquals(dtEntry.index(), 0L); + } + + // Check that found index is contained in a set and returned indices match + Assert.assertTrue(indices.contains(tableEntry.index())); + + // Check that entry type matches + Assert.assertEquals(tableEntry.type(), type); + + var headerField = STATIC_TABLE.get(tableEntry.index()); + // Check that name and/or value matches the one that can be acquired by + // using looked-up index + if (tableEntry.type() == EntryType.NAME) { + Assert.assertEquals(headerField.name(), name); + // If only name entry is found huffmanName should be set to false + Assert.assertFalse(tableEntry.huffmanName()); + } else if (tableEntry.type() == EntryType.NAME_VALUE) { + Assert.assertEquals(headerField.name(), name); + Assert.assertEquals(headerField.value(), value); + // If "name:value" match is found huffmanName and huffmanValue should + // be set to false + Assert.assertFalse(tableEntry.huffmanName()); + Assert.assertFalse(tableEntry.huffmanValue()); + + } else { + Assert.fail("Unexpected TableEntry type returned:" + tableEntry); + } + } + + @Test(dataProvider = "unacknowledgedEntriesLookupData") + public void unacknowledgedEntryLookup(String headerName, String headerValue, + boolean staticEntryExpected, + EntryType expectedType) { + // Construct dynamic table with pre-populated entries + DynamicTable dynamicTable = dynamicTableForUnackedEntriesTest(); + // Construct TablesIndexer + TablesIndexer tablesIndexer = new TablesIndexer(STATIC_TABLE, dynamicTable); + // Search for an entry in the dynamic and the static tables + var entry = tablesIndexer.entryOf(headerName, headerValue, TEST_KNOWN_RECEIVED_COUNT); + // Check that entry references expected table + Assert.assertEquals(entry.isStaticTable(), staticEntryExpected); + // And the type of found entry matches expectations + Assert.assertEquals(entry.type(), expectedType); + } + + @DataProvider + public Object[][] unacknowledgedEntriesLookupData() { + List data = new ArrayList<>(); + data.add(new Object[]{USER_AGENT_ST_NAME, "not-in-dynamic", true, EntryType.NAME}); + data.add(new Object[]{USER_AGENT_ST_NAME, USER_AGENT_DT_VALUE, false, EntryType.NAME_VALUE}); + data.add(new Object[]{TEST_ACKED_ENTRY, "not-in-dynamic", false, EntryType.NAME}); + data.add(new Object[]{TEST_ACKED_ENTRY, TEST_ACKED_ENTRY, false, EntryType.NAME_VALUE}); + data.add(new Object[]{CONTENT_TYPE_ST_NAME, "what/ever", true, EntryType.NAME}); + data.add(new Object[]{TEST_UNACKED_ENTRY, TEST_UNACKED_ENTRY, false, EntryType.NEITHER}); + return data.toArray(Object[][]::new); + } + + private static DynamicTable dynamicTableForUnackedEntriesTest() { + DynamicTable dt = new DynamicTable(QPACK.getLogger() + .subLogger("unacknowledgedEntryLookup")); + dt.setMaxTableCapacity(1024); + dt.setCapacity(1024); + // Acknowledged entry with name available in static and dynamic table + dt.insert(USER_AGENT_ST_NAME, USER_AGENT_DT_VALUE); // 0 + // Acknowledged entry with name available in dynamic table only + dt.insert(TEST_ACKED_ENTRY, TEST_ACKED_ENTRY); // 1 + // Unacknowledged entry with name available in static table + dt.insert(CONTENT_TYPE_ST_NAME, "what/ever"); // 2 + // Unacknowledged entry with name available in dynamic table only + dt.insert(TEST_UNACKED_ENTRY, TEST_UNACKED_ENTRY); // 3 + return dt; + } + + private static final long IGNORE_RECEIVED_COUNT_CHECK = -1L; + private static final long TEST_KNOWN_RECEIVED_COUNT = 2L; + + private static final String USER_AGENT_ST_NAME = "user-agent"; + private static final String USER_AGENT_DT_VALUE = "qpack-test-client"; + private static final String CONTENT_TYPE_ST_NAME = "content-type"; + private static final String TEST_ACKED_ENTRY = "test-acked-entry"; + private static final String TEST_UNACKED_ENTRY = "test-unacked-entry"; + + private final static StaticTable STATIC_TABLE = StaticTable.HTTP3; + + private Set acceptableIndicesForName(String name) { + AtomicLong enumerator = new AtomicLong(); + return StaticTable.HTTP3_HEADER_FIELDS.stream() + .map(f -> new NameEnumeration(enumerator.getAndIncrement(), f.name())) + .filter(ne -> name.equals(ne.name())) + .mapToLong(NameEnumeration::id) + .boxed() + .collect(Collectors.toUnmodifiableSet()); + } + + record NameEnumeration(long id, String name) { + } +} diff --git a/test/jdk/java/net/httpclient/qpack/UnacknowledgedInsertionTest.java b/test/jdk/java/net/httpclient/qpack/UnacknowledgedInsertionTest.java new file mode 100644 index 00000000000..7b621e2d69d --- /dev/null +++ b/test/jdk/java/net/httpclient/qpack/UnacknowledgedInsertionTest.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.http3.ConnectionSettings; +import jdk.internal.net.http.http3.Http3Error; +import jdk.internal.net.http.http3.frames.SettingsFrame; +import jdk.internal.net.http.qpack.DecodingCallback; +import jdk.internal.net.http.qpack.Encoder; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertNotEquals; + +/* + * @test + * @summary check that unacknowledged header is not inserted + * twice to the dynamic table + * @modules java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.hpack + * java.net.http/jdk.internal.net.http.qpack:+open + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * @build EncoderDecoderConnector + * @run testng/othervm -Djdk.internal.httpclient.qpack.log.level=EXTRA UnacknowledgedInsertionTest + */ +public class UnacknowledgedInsertionTest { + + @Test(dataProvider = "duplicateEntryInsertions") + public void unacknowledgedDoubleInsertion(long knownReceiveCount, List expectedHeadersEncodingType) throws Exception { + // When knownReceiveCount is set to -1 the Encoder.knownReceiveCount() + // value is used to encode headers - otherwise the provided value is used + var encoderEh = new UnacknowledgedInsertionTest.TestErrorHandler(); + var decoderEh = new UnacknowledgedInsertionTest.TestErrorHandler(); + var streamError = new AtomicReference(); + EncoderDecoderConnector.EncoderDecoderPair ed = + newPreconfiguredEncoderDecoder(encoderEh, decoderEh, streamError); + + + var encoder = ed.encoder(); + var decoder = ed.decoder(); + // Create a decoding callback to check for completion and to log failures + UnacknowledgedInsertionTest.TestDecodingCallback decodingCallback = + new UnacknowledgedInsertionTest.TestDecodingCallback(); + // Start encoding Headers Frame + var headerFrameWriter = encoder.newHeaderFrameWriter(); + var headerFrameReader = decoder.newHeaderFrameReader(decodingCallback); + + // create encoding context and buffer to hold encoded headers + List buffers = new ArrayList<>(); + + ByteBuffer headersBb = ByteBuffer.allocate(2048); + Encoder.EncodingContext context = encoder.newEncodingContext(0, 0, + headerFrameWriter); + + String name = "name"; + String value = "value"; + + for (int i = 0; i < 3; i++) { + long krcToUse = knownReceiveCount == -1L ? encoder.knownReceivedCount() : knownReceiveCount; + if (i == 1) { + encoder.header(context, name, "nameMatchOnly", false, krcToUse); + } else { + encoder.header(context, name, value, false, krcToUse); + } + headerFrameWriter.write(headersBb); + } + + // Only two entries are expected to be inserted to the dynamic table + Assert.assertEquals(ed.decoderTable().insertCount(), 2); + + // Check that headers byte buffer is not empty + assertNotEquals(headersBb.position(), 0); + headersBb.flip(); + buffers.add(headersBb); + + // Generate field section prefix bytes + encoder.generateFieldLineSectionPrefix(context, buffers); + + // Use decoder to process generated byte buffers + decoder.decodeHeader(buffers.get(0), false, headerFrameReader); + decoder.decodeHeader(buffers.get(1), true, headerFrameReader); + + var actualHeaderEncodingTypes = decodingCallback.decodedHeaders + .stream() + .map(DecodedHeader::encodingType) + .toList(); + Assert.assertEquals(actualHeaderEncodingTypes, expectedHeadersEncodingType); + } + + + @DataProvider(name = "duplicateEntryInsertions") + private Object[][] duplicateEntryInsertionsData() { + return new Object[][]{ + {0, List.of(EncodingType.LITERAL, EncodingType.LITERAL, EncodingType.LITERAL)}, + {-1, List.of(EncodingType.LITERAL, EncodingType.NAME_REF, EncodingType.INDEXED)}, + }; + } + + private static EncoderDecoderConnector.EncoderDecoderPair newPreconfiguredEncoderDecoder( + UnacknowledgedInsertionTest.TestErrorHandler encoderEh, + UnacknowledgedInsertionTest.TestErrorHandler decoderEh, + AtomicReference streamError) { + EncoderDecoderConnector conn = new EncoderDecoderConnector(); + var pair = conn.newEncoderDecoderPair( + e -> true, + encoderEh::qpackErrorHandler, + decoderEh::qpackErrorHandler, + streamError::set); + // Create settings frame with dynamic table capacity and number of blocked streams + SettingsFrame settingsFrame = SettingsFrame.defaultRFCSettings(); + // 4k should be enough for storing dynamic table entries added by 'prepopulateDynamicTable' + settingsFrame.setParameter(SettingsFrame.SETTINGS_QPACK_MAX_TABLE_CAPACITY, DT_CAPACITY); + ConnectionSettings settings = ConnectionSettings.createFrom(settingsFrame); + + // Configure encoder and decoder with constructed ConnectionSettings + pair.encoder().configure(settings); + pair.decoder().configure(settings); + pair.encoderTable().setCapacity(DT_CAPACITY); + pair.decoderTable().setCapacity(DT_CAPACITY); + + return pair; + } + + private static class TestDecodingCallback implements DecodingCallback { + + final List decodedHeaders = new CopyOnWriteArrayList<>(); + final CompletableFuture completed = new CompletableFuture<>(); + final AtomicLong completedTimestamp = new AtomicLong(); + + final AtomicReference lastThrowable = new AtomicReference<>(); + final AtomicReference lastHttp3Error = new AtomicReference<>(); + + @Override + public void onDecoded(CharSequence name, CharSequence value) { + Assert.fail("onDecoded not expected to be called"); + } + + @Override + public void onLiteralWithLiteralName(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman, + boolean hideIntermediary) { + var header = new DecodedHeader(name.toString(), value.toString(), EncodingType.LITERAL); + decodedHeaders.add(header); + System.err.println("Decoding callback 'onLiteralWithLiteralName': " + header); + } + + @Override + public void onLiteralWithNameReference(long index, + CharSequence name, + CharSequence value, + boolean valueHuffman, + boolean hideIntermediary) { + var header = new DecodedHeader(name.toString(), value.toString(), EncodingType.NAME_REF); + decodedHeaders.add(header); + System.err.println("Decoding callback 'onLiteralWithNameReference': " + header); + + } + + @Override + public void onIndexed(long index, CharSequence name, CharSequence value) { + var header = new DecodedHeader(name.toString(), value.toString(), EncodingType.INDEXED); + decodedHeaders.add(header); + System.err.println("Decoding callback 'onIndexed': " + header); + } + + + @Override + public void onComplete() { + System.err.println("Decoding callback 'onComplete'"); + completedTimestamp.set(System.nanoTime()); + completed.complete(null); + } + + @Override + public void onConnectionError(Throwable throwable, Http3Error http3Error) { + System.err.println("Decoding callback 'onError': " + http3Error); + lastThrowable.set(throwable); + lastHttp3Error.set(http3Error); + } + + @Override + public long streamId() { + return 0; + } + } + + private static class TestErrorHandler { + final AtomicReference error = new AtomicReference<>(); + final AtomicReference http3Error = new AtomicReference<>(); + + public void qpackErrorHandler(Throwable error, Http3Error http3Error) { + this.error.set(error); + this.http3Error.set(http3Error); + throw new RuntimeException("http3 error: " + http3Error, error); + } + } + + private record DecodedHeader(String name, String value, EncodingType encodingType) { + } + + enum EncodingType { + LITERAL, + NAME_REF, + INDEXED + } + + private static final long DT_CAPACITY = 4096L; +} diff --git a/test/jdk/java/net/httpclient/quic/AckElicitingTest.java b/test/jdk/java/net/httpclient/quic/AckElicitingTest.java new file mode 100644 index 00000000000..47fdb935598 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/AckElicitingTest.java @@ -0,0 +1,729 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HexFormat; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.AckFrame.AckRange; +import jdk.internal.net.http.quic.frames.ConnectionCloseFrame; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.DataBlockedFrame; +import jdk.internal.net.http.quic.frames.HandshakeDoneFrame; +import jdk.internal.net.http.quic.frames.MaxDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamDataFrame; +import jdk.internal.net.http.quic.frames.MaxStreamsFrame; +import jdk.internal.net.http.quic.frames.NewConnectionIDFrame; +import jdk.internal.net.http.quic.frames.NewTokenFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.PathChallengeFrame; +import jdk.internal.net.http.quic.frames.PathResponseFrame; +import jdk.internal.net.http.quic.frames.PingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.ResetStreamFrame; +import jdk.internal.net.http.quic.frames.RetireConnectionIDFrame; +import jdk.internal.net.http.quic.frames.StopSendingFrame; +import jdk.internal.net.http.quic.frames.StreamDataBlockedFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import jdk.internal.net.http.quic.frames.StreamsBlockedFrame; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.crypto.AEADBadTagException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +import static jdk.internal.net.http.quic.frames.QuicFrame.*; +import static jdk.internal.net.http.quic.frames.ConnectionCloseFrame.CONNECTION_CLOSE_VARIANT; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * @test + * @summary tests the logic to decide whether a packet or + * a frame is ACK-eliciting. + * @library /test/lib + * @run testng AckElicitingTest + * @run testng/othervm -Dseed=-7997973196290088038 AckElicitingTest + */ +public class AckElicitingTest { + + static final Random RANDOM = RandomFactory.getRandom(); + + private static class DummyQuicTLSEngine implements QuicTLSEngine { + @Override + public HandshakeState getHandshakeState() { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean isTLSHandshakeComplete() { + return true; + } + + @Override + public KeySpace getCurrentSendKeySpace() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean keysAvailable(KeySpace keySpace) { + return true; + } + + @Override + public void discardKeys(KeySpace keySpace) { + // no-op + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + throw new AssertionError("should not come here!"); + } + + @Override + public void restartHandshake() throws IOException { + throw new AssertionError("should not come here!"); + } + + @Override + public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) { + throw new AssertionError("should not come here!"); + } + + @Override + public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { } + + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return 0; + } + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) { + return ByteBuffer.allocate(5); + } + + @Override + public int getAuthTagSize() { + return 0; + } + + @Override + public void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException { + // this dummy QUIC TLS engine doesn't do any encryption. + // we just copy over the raw packet payload into the output buffer + output.put(packetPayload); + } + + @Override + public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) { + packet.position(packet.position() + headerLength); + output.put(packet); + } + @Override + public void signRetryPacket(QuicVersion quicVersion, + ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) { + throw new AssertionError("should not come here!"); + } + @Override + public void verifyRetryPacket(QuicVersion quicVersion, + ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException { + throw new AssertionError("should not come here!"); + } + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) { + throw new AssertionError("should not come here!"); + } + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) { + throw new AssertionError("should not come here!"); + } + @Override + public Runnable getDelegatedTask() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryMarkHandshakeDone() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryReceiveHandshakeDone() { + throw new AssertionError("should not come here!"); + } + + @Override + public Set getSupportedQuicVersions() { + return Set.of(QuicVersion.QUIC_V1); + } + + @Override + public void setUseClientMode(boolean mode) { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean getUseClientMode() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLParameters getSSLParameters() { + throw new AssertionError("should not come here!"); + } + + @Override + public void setSSLParameters(SSLParameters sslParameters) { + throw new AssertionError("should not come here!"); + } + + @Override + public String getApplicationProtocol() { + return null; + } + + @Override + public SSLSession getSession() { throw new AssertionError("should not come here!"); } + + @Override + public SSLSession getHandshakeSession() { throw new AssertionError("should not come here!"); } + + @Override + public void versionNegotiated(QuicVersion quicVersion) { + // no-op + } + + @Override + public void setOneRttContext(QuicOneRttContext ctx) { + // no-op + } + } + private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine(); + private static abstract class TestCodingContext implements CodingContext { + TestCodingContext() { } + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new AssertionError("should not come here!"); + } + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new AssertionError("should not come here!"); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicTLSEngine getTLSEngine() { + return TLS_ENGINE; + } + } + + static final int CIDLEN = RANDOM.nextInt(5, QuicConnectionId.MAX_CONNECTION_ID_LENGTH + 1); + + private static final TestCodingContext CONTEXT = new TestCodingContext() { + + @Override + public long largestProcessedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public long largestAckedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public int connectionIdLength() { + return CIDLEN; + } + + @Override + public QuicConnectionId originalServerConnId() { + return null; + } + }; + + /** + * A record to store all the input of a given test case. + * @param type a concrete frame type or packet type + * @param describer a function to describe the {@code obj} instance + * for tracing/diagnosis purposes + * @param ackEliciting the function we want to test. This is either + * {@link QuicFrame#isAckEliciting() + * QuicFrame::isAckEliciting} or + * {@link QuicPacket#isAckEliciting() + * QuicPacket::isAckEliciting} + * @param obj the instance on which to call the {@code ackEliciting} + * function. + * @param expected the expected result of calling + * {@code obj.ackEliciting()} + * @param A concrete subclass of {@link QuicFrame} or {@link QuicPacket} + */ + static record TestCase(Class type, + Function describer, + Predicate ackEliciting, + T obj, + boolean expected) { + + @Override + public String toString() { + // shorter & better toString than the default + return "%s(%s)" + .formatted(type.getSimpleName(), describer.apply(obj)); + } + + private static String describeFrame(QuicFrame frame) { + long type = frame.getTypeField(); + return HexFormat.of().toHexDigits((byte)type); + } + + /** + * Creates an instance of {@code TestCase} for a concrete frame type + * @param type the concrete frame class + * @param frame the concrete instance + * @param expected whether {@link QuicFrame#isAckEliciting()} + * should return true for that instance. + * @param a concrete subclass of {@code QuicFrame} + * @return a new instance of {@code TestCase} + */ + public static TestCase + of(Class type, T frame, boolean expected) { + return new TestCase(type, TestCase::describeFrame, + QuicFrame::isAckEliciting, frame, expected); + } + + /** + * Creates an instance of {@code TestCase} for a concrete frame type + * @param frame the concrete frame instance + * @param expected whether {@link QuicFrame#isAckEliciting()} + * should return true for that instance. + * @param a concrete subclass of {@code QuicFrame} + * @return a new instance of {@code TestCase} + */ + public static TestCase of(T frame, boolean expected) { + return new TestCase((Class)frame.getClass(), + TestCase::describeFrame, + QuicFrame::isAckEliciting, + frame, expected); + } + + /** + * Creates an instance of {@code TestCase} for a concrete packet type + * @param packet the concrete packet instance + * @param expected whether {@link QuicPacket#isAckEliciting()} + * should return true for that instance. + * @param a concrete subclass of {@code QuicPacket} + * @return a new instance of {@code TestCase} + */ + public static TestCase of(T packet, boolean expected) { + return new TestCase((Class)packet.getClass(), + (p) -> p.frames().stream() + .map(Object::getClass) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")), + QuicPacket::isAckEliciting, + packet, expected); + } + } + + // convenient alias to shorten lines in data providers + private static TestCase of(T frame, boolean ackEliciting) { + return TestCase.of(frame, ackEliciting); + } + + // convenient alias to shorten lines in data providers + private static TestCase of(T packet, boolean ackEliciting) { + return TestCase.of(packet, ackEliciting); + } + + /** + * Create a new instance of the given frame type, populated with + * dummy values. + * @param frameClass the frame type + * @param a concrete subclass of {@code QuicFrame} + * @return a new instance of the given concrete class. + */ + T newFrame(Class frameClass) { + var frameType = QuicFrame.frameTypeOf(frameClass); + if (frameType == CONNECTION_CLOSE) { + if (RANDOM.nextBoolean()) { + frameType = CONNECTION_CLOSE_VARIANT; + } + } + long streamId = 4; + long largestAcknowledge = 3; + long ackDelay = 20; + long gap = 0; + long range = largestAcknowledge; + long offset = 0; + boolean fin = false; + int length = 10; + long errorCode = 1; + long finalSize = 10; + int size = 3; + long maxData = 10; + boolean maxStreamsBidi = true; + long maxStreams = 100; + long maxStreamData = 10; + long sequenceNumber = 4; + long retirePriorTo = 3; + String reason = "none"; + long errorFrameType = ACK; + int pathChallengeLen = PathChallengeFrame.LENGTH; + int pathResponseLen = PathResponseFrame.LENGTH; + var frame = switch (frameType) { + case ACK -> new AckFrame(largestAcknowledge, ackDelay, List.of(new AckRange(gap, range))); + case STREAM -> new StreamFrame(streamId, offset, length, fin, ByteBuffer.allocate(length)); + case RESET_STREAM -> new ResetStreamFrame(streamId, errorCode, finalSize); + case PADDING -> new PaddingFrame(size); + case PING -> new PingFrame(); + case STOP_SENDING -> new StopSendingFrame(streamId, errorCode); + case CRYPTO -> new CryptoFrame(offset, length, ByteBuffer.allocate(length)); + case NEW_TOKEN -> new NewTokenFrame(ByteBuffer.allocate(length)); + case DATA_BLOCKED -> new DataBlockedFrame(maxData); + case MAX_DATA -> new MaxDataFrame(maxData); + case MAX_STREAMS -> new MaxStreamsFrame(maxStreamsBidi, maxStreams); + case MAX_STREAM_DATA -> new MaxStreamDataFrame(streamId, maxStreamData); + case STREAM_DATA_BLOCKED -> new StreamDataBlockedFrame(streamId, maxStreamData); + case STREAMS_BLOCKED -> new StreamsBlockedFrame(maxStreamsBidi, maxStreams); + case NEW_CONNECTION_ID -> new NewConnectionIDFrame(sequenceNumber, retirePriorTo, + ByteBuffer.allocate(length), ByteBuffer.allocate(16)); + case RETIRE_CONNECTION_ID -> new RetireConnectionIDFrame(sequenceNumber); + case PATH_CHALLENGE -> new PathChallengeFrame(ByteBuffer.allocate(pathChallengeLen)); + case PATH_RESPONSE -> new PathResponseFrame(ByteBuffer.allocate(pathResponseLen)); + case CONNECTION_CLOSE -> new ConnectionCloseFrame(errorCode, errorFrameType, reason); + case CONNECTION_CLOSE_VARIANT -> new ConnectionCloseFrame(errorCode, reason); + case HANDSHAKE_DONE -> new HandshakeDoneFrame(); + default -> throw new IllegalArgumentException("Unrecognised frame"); + }; + return frameClass.cast(frame); + } + + /** + * Creates a list of {@code TestCase} to test all possible concrete + * subclasses of {@code QuicFrame}. + * + * @return a list of {@code TestCase} to test all possible concrete + * subclasses of {@code QuicFrame} + */ + public List> createFramesTests() { + List> frames = new ArrayList<>(); + frames.add(of(newFrame(AckFrame.class), false)); + frames.add(of(newFrame(ConnectionCloseFrame.class), false)); + frames.add(of(newFrame(PaddingFrame.class), false)); + + for (var frameType : QuicFrame.class.getPermittedSubclasses()) { + if (frameType == AckFrame.class) continue; + if (frameType == ConnectionCloseFrame.class) continue; + if (frameType == PaddingFrame.class) continue; + Class quicFrameClass = (Class)frameType; + frames.add(of(newFrame(quicFrameClass), true)); + } + + return List.copyOf(frames); + } + + /** + * Creates a {@code QuicPacket} containing the given list of frames. + * @param frames a list of frames + * @return a new instance of {@code QuicPacket} + */ + QuicPacket createPacket(List frames) { + PacketType[] values = PacketType.values(); + int index = PacketType.NONE.ordinal(); + while (index == PacketType.NONE.ordinal()) { + index = RANDOM.nextInt(0, values.length); + } + PacketType packetType = values[index]; + QuicPacketEncoder encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1); + byte[] scid = new byte[CIDLEN]; + RANDOM.nextBytes(scid); + byte[] dcid = new byte[CIDLEN]; + RANDOM.nextBytes(dcid); + QuicConnectionId source = new PeerConnectionId(scid); + QuicConnectionId dest = new PeerConnectionId(dcid); + long largestAckedPacket = CONTEXT.largestAckedPN(packetType); + QuicPacket packet = switch (packetType) { + case NONE -> throw new AssertionError("should not come here"); + // TODO: add more packet types + default -> encoder.newInitialPacket(source, dest, null, largestAckedPacket + 1 , + largestAckedPacket, frames, CONTEXT); + }; + return packet; + + } + + /** + * Creates a random instance of {@code QuicPacket} containing a + * pseudo random list of concrete {@link QuicFrame} instances. + * @param ackEliciting whether the returned packet should be + * ack eliciting. + * @return + */ + QuicPacket createPacket(boolean ackEliciting) { + List frames = new ArrayList<>(); + int mincount = ackEliciting ? 1 : 0; + int ackCount = RANDOM.nextInt(mincount, 5); + int nackCount = RANDOM.nextInt(0, 10); + + // TODO: maybe refactor this to make sure the frame + // we use are compatible with the packet type. + List> noAckFrames = List.of(AckFrame.class, + PaddingFrame.class, ConnectionCloseFrame.class); + for (int i=0; i < nackCount ; i++) { + frames.add(newFrame(noAckFrames.get(i % noAckFrames.size()))); + } + if (ackEliciting) { + // TODO: maybe refactor this to make sure the frame + // we use are compatible with the packet type. + Class[] frameClasses = QuicFrame.class.getPermittedSubclasses(); + for (int i=0; i < ackCount; i++) { + Class selected; + do { + int fx = RANDOM.nextInt(0, frameClasses.length); + selected = frameClasses[fx]; + } while (noAckFrames.contains(selected)); + frames.add(newFrame((Class) selected)); + } + } + if (!ackEliciting || RANDOM.nextBoolean()) { + // if !ackEliciting we always shuffle. + // Otherwise, we only shuffle half the time. + Collections.shuffle(frames, RANDOM); + } + return createPacket(mergeConsecutivePaddingFrames(frames)); + } + + private List mergeConsecutivePaddingFrames(List frames) { + var iterator = frames.listIterator(); + QuicFrame previous = null; + + while (iterator.hasNext()) { + var frame = iterator.next(); + if (previous instanceof PaddingFrame prevPad + && frame instanceof PaddingFrame nextPad) { + int previousIndex = iterator.previousIndex(); + QuicFrame merged = new PaddingFrame(prevPad.size() + nextPad.size()); + frames.set(previousIndex, merged); + iterator.remove(); + } else { + previous = frame; + } + } + return frames; + } + + /** + * Creates a list of {@code TestCase} to test random instances of + * {@code QuicPacket} containing random instances of {@link QuicFrame} + * @return a list of {@code TestCase} to test random instances of + * {@code QuicPacket} containing random instances of {@link QuicFrame} + */ + public List> createPacketsTests() { + List> packets = new ArrayList<>(); + packets.add(of(createPacket(List.of(newFrame(AckFrame.class))), false)); + packets.add(of(createPacket(List.of(newFrame(ConnectionCloseFrame.class))), false)); + packets.add(of(createPacket(List.of(newFrame(PaddingFrame.class))), false)); + var frames = new ArrayList<>(List.of( + newFrame(PaddingFrame.class), + newFrame(AckFrame.class), + newFrame(ConnectionCloseFrame.class))); + Collections.shuffle(frames, RANDOM); + packets.add(of(createPacket(List.copyOf(frames)), false)); + + int maxPackets = RANDOM.nextInt(5, 11); + for (int i = 0; i < maxPackets ; i++) { + packets.add(of(createPacket(true), true)); + } + return List.copyOf(packets); + } + + + /** + * A provider of test case to test + * {@link QuicFrame#isAckEliciting()}. + * @return test case to test + * {@link QuicFrame#isAckEliciting()} + */ + @DataProvider(name = "frames") + public Object[][] framesDataProvider() { + return createFramesTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + /** + * A provider of test case to test + * {@link QuicPacket#isAckEliciting()}. + * @return test case to test + * {@link QuicPacket#isAckEliciting()} + */ + @DataProvider(name = "packets") + public Object[][] packetsDataProvider() { + return createPacketsTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + /** + * Verifies the behavior of {@link QuicFrame#isAckEliciting()} + * with the given test case inputs. + * @param test the test inputs + * @param a concrete subclass of QuicFrame + */ + @Test(dataProvider = "frames") + public void testFrames(TestCase test) { + testAckEliciting(test.type(), + test.describer(), + test.ackEliciting(), + test.obj(), + test.expected()); + } + + /** + * Verifies the behavior of {@link QuicPacket#isAckEliciting()} + * with the given test case inputs. + * @param test the test inputs + * @param a concrete subclass of QuickPacket + */ + @Test(dataProvider = "packets") + public void testPackets(TestCase test) { + testAckEliciting(test.type(), + test.describer(), + test.ackEliciting(), + test.obj(), + test.expected()); + } + + + /** + * Asserts that {@code ackEliciting.test(obj) == expected}. + * @param type the concrete type of {@code obj} + * @param describer a function to describe {@code obj} + * @param ackEliciting the function being tested + * @param obj the instance on which to call the function being tested + * @param expected the expected result of {@code ackEliciting.test(obj)} + * @param the concrete class being tested + */ + private void testAckEliciting(Class type, + Function describer, + Predicate ackEliciting, + T obj, + boolean expected) { + System.out.printf("%ntestAckEliciting: %s(%s) - expecting %s%n", + type.getSimpleName(), + describer.apply(obj), + expected); + assertEquals(ackEliciting.test(obj), expected, describer.apply(obj)); + if (obj instanceof QuicFrame frame) { + checkFrame(frame); + } else if (obj instanceof QuicPacket packet) { + checkPacket(packet); + } + } + + // This is not a full-fledged test for frame encoding/decoding. + // Just a smoke test to verify that the ACK-eliciting property + // survives encoding/decoding + private void checkFrame(QuicFrame frame) { + int size = frame.size(); + ByteBuffer buffer = ByteBuffer.allocate(size); + System.out.println("Checking frame: " + frame.getClass()); + try { + frame.encode(buffer); + buffer.flip(); + var decoded = QuicFrame.decode(buffer); + checkFrame(decoded, frame); + } catch (QuicTransportException x) { + throw new AssertionError(frame.getClass().getName(), x); + } + } + + // This is not a full-fledged test for frame equality: + // And we still need a proper decoding/encoding test for frames + private void checkFrame(QuicFrame decoded, QuicFrame expected) { + System.out.printf("Comparing frames: %s with %s%n", + decoded.getClass().getSimpleName(), + expected.getClass().getSimpleName()); + assertEquals(decoded.getClass(), expected.getClass()); + assertEquals(decoded.size(), expected.size()); + assertEquals(decoded.getTypeField(), expected.getTypeField()); + assertEquals(decoded.isAckEliciting(), expected.isAckEliciting()); + } + + // This is not a full-fledged test for packet encoding/decoding. + // Just a smoke test to verify that the ACK-eliciting property + // survives encoding/decoding + private void checkPacket(QuicPacket packet) { + int size = packet.size(); + ByteBuffer buffer = ByteBuffer.allocate(size); + System.out.println("Checking packet: " + packet.getClass()); + try { + var encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1); + var decoder = QuicPacketDecoder.of(QuicVersion.QUIC_V1); + encoder.encode(packet, buffer, CONTEXT); + buffer.flip(); + var decoded = decoder.decode(buffer, CONTEXT); + assertEquals(decoded.size(), packet.size()); + assertEquals(decoded.packetType(), packet.packetType()); + assertEquals(decoded.payloadSize(), packet.payloadSize()); + assertEquals(decoded.isAckEliciting(), packet.isAckEliciting()); + var frames = packet.frames(); + var decodedFrames = decoded.frames(); + assertEquals(decodedFrames.size(), frames.size()); + } catch (Exception x) { + throw new AssertionError(packet.getClass().getName(), x); + } + + } +} diff --git a/test/jdk/java/net/httpclient/quic/AckFrameTest.java b/test/jdk/java/net/httpclient/quic/AckFrameTest.java new file mode 100644 index 00000000000..129394f126c --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/AckFrameTest.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.frames.AckFrame; +import jdk.internal.net.http.quic.frames.AckFrame.AckFrameBuilder; +import jdk.internal.net.http.quic.frames.AckFrame.AckRange; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.test.lib.RandomFactory; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.LongPredicate; +import java.util.stream.LongStream; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertFalse; + +/** + * @test + * @summary tests the logic to build an AckFrame + * @library /test/lib + * @run testng AckFrameTest + */ +public class AckFrameTest { + + static final Random RANDOM = RandomFactory.getRandom(); + + private static abstract class TestCodingContext implements CodingContext { + TestCodingContext() { } + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new AssertionError("should not come here!"); + } + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new AssertionError("should not come here!"); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicTLSEngine getTLSEngine() { + throw new AssertionError("should not come here!"); + } + } + + static final int CIDLEN = RANDOM.nextInt(5, QuicConnectionId.MAX_CONNECTION_ID_LENGTH + 1); + + private static final TestCodingContext CONTEXT = new TestCodingContext() { + + @Override + public long largestProcessedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public long largestAckedPN(PacketNumberSpace packetSpace) { + return 0; + } + + @Override + public int connectionIdLength() { + return CIDLEN; + } + + @Override + public QuicConnectionId originalServerConnId() { + return null; + } + }; + + public static record Acknowledged(long first, long last) { + public boolean contains(long packet) { + return first <= packet && last >= packet; + } + public static List of(long... numbers) { + if (numbers == null || numbers.length == 0) return List.of(); + if (numbers.length%2 != 0) throw new IllegalArgumentException(); + List res = new ArrayList<>(numbers.length/2); + for (int i = 0; i < numbers.length; i += 2) { + res.add(new Acknowledged(numbers[i], numbers[i+1])); + } + return List.copyOf(res); + } + } + public static record Packet(long packetNumber) { + static List ofAcks(List acks) { + return packets(acks); + } + static List of(long... numbers) { + return LongStream.of(numbers).mapToObj(Packet::new).toList(); + } + } + + public static record TestCase(List acks, List packets, boolean shuffled) { + public TestCase(List acks) { + this(acks, Packet.ofAcks(acks), false); + } + public TestCase shuffle() { + List shuffled = new ArrayList<>(); + shuffled.addAll(packets); + Collections.shuffle(shuffled, RANDOM); + return new TestCase(acks, List.copyOf(shuffled), true); + } + } + + List generateTests() { + List tests = new ArrayList<>(); + List simples = List.of( + new TestCase(List.of(new Acknowledged(5,5))), + new TestCase(List.of(new Acknowledged(5,7))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,9))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,7))), + new TestCase(List.of(new Acknowledged(3,3), new Acknowledged(5,7))) + ); + tests.addAll(simples); + List specials = List.of( + new TestCase(Acknowledged.of(5,5,7,7), Packet.of(5,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(5,7,6), true), + new TestCase(Acknowledged.of(6,7), Packet.of(6,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(6,7,5), true), + new TestCase(Acknowledged.of(5,7), Packet.of(5,6,7), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(5, 7, 8), true), + new TestCase(Acknowledged.of(5,5,8,8), Packet.of(8, 5), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(8, 5, 7), true), + new TestCase(Acknowledged.of(3,5,7,9), Packet.of(8,5,7,4,9,3), true), + new TestCase(Acknowledged.of(27,27,31,31), + Packet.of(27, 31), true), + new TestCase(Acknowledged.of(27,27,29,29,31,31), + Packet.of(27, 31, 29), true), + new TestCase(Acknowledged.of(3,5,7,7,9,9,22,22,27,27,29,29,31,31), + Packet.of(4,22,27,31,9,29,7,5,3), true) + ); + tests.addAll(specials); + for (int i=0; i < 5; i++) { + List acks = generateAcks(); + List packets = packets(acks); + TestCase test = new TestCase(acks, List.copyOf(packets), false); + tests.add(test); + for (int j = 0; j < 5; j++) { + tests.add(test.shuffle()); + } + } + return tests; + } + + List generateAcks() { + int count = RANDOM.nextInt(3, 10); + List acks = new ArrayList<>(count); + long prev = -1; + for (int i=0; i packets(List acks) { + List res = new ArrayList<>(); + for (Acknowledged ack : acks) { + for (long i = ack.first() ; i<= ack.last() ; i++) { + var packet = new Packet(i); + assert !res.contains(packet); + res.add(packet); + } + } + return res; + } + + @DataProvider(name = "tests") + public Object[][] tests() { + return generateTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @Test(dataProvider = "tests") + public void testAckFrames(TestCase testCase) { + AckFrameBuilder builder = new AckFrameBuilder(); + List acks = testCase.acks; + List packets = testCase.packets; + long largest = packets.stream() + .mapToLong(Packet::packetNumber) + .max().getAsLong(); + System.out.printf("%ntestAckFrames(%s, %s)%n", acks, testCase.shuffled); + builder.ackDelay(250); + packets.stream().mapToLong(Packet::packetNumber).forEach(builder::addAck); + AckFrame frame = builder.build(); + System.out.printf(" -> %s%n", frame); + checkFrame(frame, testCase, packets, frame); + checkAcknowledging(builder::isAcknowledging, testCase, packets); + + AckFrameBuilder dup = new AckFrameBuilder(frame); + assertEquals(frame, dup.build()); + assertEquals(frame, builder.build()); + checkAcknowledging(dup::isAcknowledging, testCase, packets); + + packets.stream().mapToLong(Packet::packetNumber).forEach(builder::addAck); + checkFrame(builder.build(), testCase, packets, frame); + checkAcknowledging(builder::isAcknowledging, testCase, packets); + + packets.stream().mapToLong(Packet::packetNumber).forEach(dup::addAck); + checkFrame(dup.build(), testCase, packets, frame); + checkAcknowledging(dup::isAcknowledging, testCase, packets); + + AckFrameBuilder dupdup = new AckFrameBuilder(); + dupdup.ackDelay(250); + List dups = new ArrayList<>(packets); + dups.addAll(packets); + dups.addAll(packets); + Collections.shuffle(dups, RANDOM); + dups.stream().mapToLong(Packet::packetNumber).forEach(dupdup::addAck); + checkFrame(dupdup.build(), testCase, dups, frame); + checkAcknowledging(dupdup::isAcknowledging, testCase, packets); + + } + + private void checkFrame(AckFrame frame, TestCase testCase, List packets, AckFrame reference) { + long largest = testCase.packets.stream() + .mapToLong(Packet::packetNumber) + .max().getAsLong(); + assertEquals(frame.largestAcknowledged(), largest); + checkAcknowledging(frame::isAcknowledging, testCase, packets); + for (var ack : testCase.acks) { + checkRangeAcknowledged(frame, ack.first, ack.last); + } + assertEquals(frame, reference); + int size = frame.size(); + ByteBuffer buffer = ByteBuffer.allocate(size + 10); + buffer.position(5); + buffer.limit(size + 5); + try { + frame.encode(buffer); + assertEquals(buffer.position(), buffer.limit()); + buffer.position(5); + buffer.limit(buffer.capacity()); + var decoded = QuicFrame.decode(buffer); + assertEquals(buffer.position(), size + 5); + assertEquals(decoded, frame); + assertEquals(decoded, reference); + } catch (Exception e) { + throw new AssertionError("Can't encode or decode frame: " + frame, e); + } + } + + private void checkRangeAcknowledged(AckFrame frame, long first, long last) { + assertTrue(frame.isRangeAcknowledged(first, last), + "range [%s, %s] should be acked".formatted(first, last)); + if (first > 0) { + if (!frame.isAcknowledging(first - 1)) { + assertFalse(frame.isRangeAcknowledged(first -1, last), + "range [%s, %s] should not be acked".formatted(first -1, last)); + } else { + assertTrue(frame.isRangeAcknowledged(first - 1, last), + "range [%s, %s] should be acked".formatted(first - 1, last)); + if (frame.isAcknowledging(last + 1)) { + assertTrue(frame.isRangeAcknowledged(first -1, last + 1), + "range [%s, %s] should be acked".formatted(first -1, last+1)); + } + } + } + if (!frame.isAcknowledging(last + 1)) { + assertFalse(frame.isRangeAcknowledged(first, last + 1), + "range [%s, %s] should not be acked".formatted(first, last + 1)); + } else { + assertTrue(frame.isRangeAcknowledged(first, last+1), + "range [%s, %s] should be acked".formatted(first, last + 1)); + } + if (last - 1 >= first) { + assertTrue(frame.isRangeAcknowledged(first + 1, last), + "range [%s, %s] should be acked".formatted(first + 1, last)); + assertTrue(frame.isRangeAcknowledged(first, last - 1), + "range [%s, %s] should be acked".formatted(first, last - 1)); + } + if (last - 2 >= first) { + assertTrue(frame.isRangeAcknowledged(first + 1, last - 1), + "range [%s, %s] should be acked".formatted(first + 1, last - 1)); + } + } + + private void checkAcknowledging(LongPredicate isAckPredicate, + TestCase testCase, + List packets) { + long largest = testCase.packets.stream() + .mapToLong(Packet::packetNumber) + .max().getAsLong(); + for (long i = largest + 10; i >= 0; i--) { + long pn = i; + boolean expected = testCase.acks.stream().anyMatch((a) -> a.contains(pn)); + boolean isAcknowledging = isAckPredicate.test(pn); + if (isAcknowledging != expected && testCase.shuffled) { + System.out.printf(" -> %s%n", packets); + } + assertEquals(isAcknowledging, expected, String.valueOf(pn)); + } + for (var p : testCase.packets) { + boolean isAcknowledging = isAckPredicate.test(p.packetNumber); + if (!isAcknowledging && testCase.shuffled) { + System.out.printf(" -> %s%n", packets); + } + assertEquals(isAcknowledging, true, p.toString()); + } + } + + @Test + public void simpleTest() { + AckFrame frame = new AckFrame(1, 0, List.of(new AckRange(0,0))); + System.out.println("simpleTest: " + frame); + assertTrue(frame.isAcknowledging(1), "1 should be acked"); + assertFalse(frame.isAcknowledging(0), "0 should not be acked"); + assertFalse(frame.isAcknowledging(2), "2 should not be acked"); + assertEquals(frame.smallestAcknowledged(), 1); + assertEquals(frame.largestAcknowledged(), 1); + assertEquals(frame.acknowledged().toArray(), new long[] {1L}); + assertTrue(frame.isRangeAcknowledged(1,1), "[1,1] should be acked"); + assertFalse(frame.isRangeAcknowledged(0, 1), "[0,1] should not be acked"); + assertFalse(frame.isRangeAcknowledged(1, 2), "[1,2] should not be acked"); + assertFalse(frame.isRangeAcknowledged(0, 2), "[0,2] should not be acked"); + + frame = new AckFrame(1, 0, List.of(new AckRange(0,1))); + System.out.println("simpleTest: " + frame); + assertTrue(frame.isAcknowledging(1), "1 should be acked"); + assertTrue(frame.isAcknowledging(0), "0 should be acked"); + assertFalse(frame.isAcknowledging(2), "2 should not be acked"); + assertEquals(frame.smallestAcknowledged(), 0); + assertEquals(frame.largestAcknowledged(), 1); + assertEquals(frame.acknowledged().toArray(), new long[] {1L, 0L}); + assertTrue(frame.isRangeAcknowledged(0,0), "[0,0] should be acked"); + assertTrue(frame.isRangeAcknowledged(1,1), "[1,1] should be acked"); + assertTrue(frame.isRangeAcknowledged(0, 1), "[0,1] should be acked"); + assertFalse(frame.isRangeAcknowledged(1, 2), "[1,2] should not be acked"); + assertFalse(frame.isRangeAcknowledged(0, 2), "[0,2] should not be acked"); + + frame = new AckFrame(10, 0, List.of(new AckRange(0,3), new AckRange(2, 3))); + System.out.println("simpleTest: " + frame); + assertTrue(frame.isAcknowledging(10), "10 should be acked"); + assertTrue(frame.isAcknowledging(0), "0 should be acked"); + assertTrue(frame.isRangeAcknowledged(0, 3), "[0,3] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[7,10] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[0,2] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[1,3] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[1,2] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[7,9] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[8,10] should be acked"); + assertTrue(frame.isRangeAcknowledged(7, 10), "[8,9] should be acked"); + assertFalse(frame.isRangeAcknowledged(0, 10), "[0,10] should not be acked"); + assertFalse(frame.isRangeAcknowledged(4, 6), "[4,6] should not be acked"); + assertFalse(frame.isRangeAcknowledged(4, 6), "[3,7] should not be acked"); + assertFalse(frame.isRangeAcknowledged(4, 6), "[2,8] should not be acked"); + } + } diff --git a/test/jdk/java/net/httpclient/quic/BuffersReaderTest.java b/test/jdk/java/net/httpclient/quic/BuffersReaderTest.java new file mode 100644 index 00000000000..8d03f4265b3 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/BuffersReaderTest.java @@ -0,0 +1,520 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; + +import jdk.test.lib.RandomFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.*; + + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.quic + * @run junit/othervm BuffersReaderTest + * @summary Tests various BuffersReader methods + * work as expected. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class BuffersReaderTest { + static final Class IAE = IllegalArgumentException.class; + + static final Random RAND = RandomFactory.getRandom(); + static final int GENERATED = 2; + + // describes a byte buffer at a given global offset + record BB(long globalOffset, int position, int length, int capacity) {} + record Simple(long position, long index, int expected) {} + + record TestCase(List bbs, List simples) {} + + + // describes a BuffersReader configuration composed of 5 bytes buffer + // added with various position, limit, and capacity (limit = position + length) + List specialCases = List.of(new TestCase(List.of( + new BB(0, 10, 10, 30), + new BB(10, 5, 10, 20), + new BB(20, 15, 10, 40), + new BB(30, 0, 10, 20), + new BB(40, 5, 10, 20)), + List.of(new Simple(11, 50, 40)) + )); + + + private List tests() { + int generated = 2; + List allcases = new ArrayList<>(specialCases.size() + GENERATED); + allcases.addAll(specialCases); + for (int i = 0; i < GENERATED; i++) { + allcases.add(new TestCase(generateBBs(), List.of())); + } + return allcases; + } + + private List generateBBs() { + var bbscount = RAND.nextInt(1, 11); + List bbs = new ArrayList<>(bbscount); + long globalOffset = 0; + for (int i = 0; i < bbscount; i++) { + int length = RAND.nextInt(1, 11); + int offset = RAND.nextInt(0,3); + int tail = RAND.nextInt(0,3); + bbs.add(new BB(globalOffset, offset, length, offset + length + tail)); + globalOffset += length; + } + return List.copyOf(bbs); + } + + + @Test + public void testGet() { + test("hello world".getBytes(StandardCharsets.US_ASCII), 2, 10); + } + + @Test + public void testGetPos6() { + test("May the road rise up to meet you".getBytes(StandardCharsets.US_ASCII), 6, 23); + } + + @Test + public void testGetPos0() { + test("May the wind always be at your back".getBytes(StandardCharsets.US_ASCII), 0, 29); + } + + public void test(byte[] values, int position, int limit) { + ByteBuffer bb = ByteBuffer.wrap(values); + bb.position(position); + bb.limit(limit); + + ListBuffersReader br = BuffersReader.list(bb); + assertEquals(br.position(), position); + assertEquals(br.limit(), limit); + for (int i = position; i < limit; i++) { + int j = limit - (i - position) - 1; + System.err.printf("%ntesting(v[i:%s]=%s, v[j:%s]=%s)%n", + i, values[i], j, values[j]); + assertEquals(br.position(), i); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", i, i); + assertEquals((char)br.get(i), (char)values[i]); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", j, j); + assertEquals((char)br.get(j), (char)values[j]); + assertEquals(br.position(), i); + System.err.printf("assertEquals((char)br.get(), (char)values[%s])%n", i); + assertEquals((char)br.get(), (char)values[i]); + assertEquals(br.position(), i+1); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", i, i); + assertEquals((char)br.get(i), (char)values[i]); + System.err.printf("assertEquals((char)br.get(%s), (char)values[%s])%n", j, j); + assertEquals((char)br.get(j), (char)values[j]); + } + assertEquals(br.position(), br.limit()); + br.release(); + assertEquals(br.position(), 0); + assertEquals(br.limit(), 0); + bb.position(0); + bb.limit(bb.capacity()); + int start = 0; + limit = bb.limit(); + br.add(bb); + + final int N = 3; + for (int i = 1 ; i < N; i++) { + ByteBuffer bbb = ByteBuffer.allocate(bb.limit() + 4); + bbb.put((byte)-1); + bbb.put((byte)-2); + bbb.put(bb.slice()); + bbb.put((byte)-3); + bbb.put((byte)-4); + bbb.position(2); + bbb.limit(2 + bb.limit()); + br.add(bbb); + } + + long read = br.read(); + for (int i = start; i < N*limit; i++) { + var vi = values[i%limit]; + var j = N*limit - i - 1; + var vj = values[j%limit]; + System.err.printf("%ndouble testing(v[i:%s]=%s, v[j:%s]=%s) position: %s%n", + i, vi, j, vj, br.position()); + assertEquals(br.get(i), vi); + assertEquals(br.get(j), vj); + assertEquals(br.get(), vi); + assertEquals(br.get(i), vi); + assertEquals(br.get(j), vj); + } + assertEquals(br.position(), N * values.length); + assertEquals(br.read() - read, N * values.length - start); + + if (N > 2) { + System.err.printf("testing getAndRelease()%n"); + br.position(values.length + position); + assertEquals(br.position(), values.length + position); + assertEquals(br.read() - read, values.length + position - start); + var bbl = br.getAndRelease(values.length); + assertEquals(bbl.size(), (position == 0 ? 1 : 2)); + // We expect bbl.getFirst() to be the second byte buffer, which will + // have an offset of 2. The position in that byte buffer + // should therefore be position + 2, since we moved the + // position of the buffers reader to values.length + + // position before calling getAndRelease. + assertEquals(position + 2, bbl.getFirst().position()); + int rstart = (int) bbl.getFirst().position(); + ListBuffersReader br2 = BuffersReader.list(bbl); + System.err.printf("position=%s, bbl[0].position=%s%n", position, rstart); + // br2 initial position should reflect the initial position + // of the first buffer in the bbl list. + assertEquals(br2.position(), rstart); + try { + br2.position(rstart - 1); + throw new AssertionError("Expected IllegalArgumentException not thrown"); + } catch (IllegalArgumentException iae) { + System.err.printf("Got expected exception" + + " trying to move before initial position: %s%n", iae); + } + assertEquals(br2.limit(), values.length + rstart); + for (int i = 0; i < values.length; i++) { + assertEquals(br2.get(), values[(i + position) % values.length]); + } + } + } + + // Encode the given length and then decodes it and compares + // the results, asserting various invariants along the way. + @Test + public void testEncodeDecodeVL() { + testEncodeDecodeVL(4611686018427387903L, 3); + } + + public void testEncodeDecodeVL(long length, int expectedPrefix) { + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(10)) { + System.err.printf("Encode/Decode %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too short, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer shorter = ByteBuffer.allocate(actualSize - 1 + offset); + shorter.position(offset); + ByteBuffer shorterref = ByteBuffer.allocate(actualSize - 1 + offset); + shorterref.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(offset); + shortSlice.limit(offset + actualSize - 1); + ListBuffersReader br = BuffersReader.list(shortSlice); + var actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), offset); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + assertEquals(br.position(), offset); + assertEquals(br.limit(), offset + actualSize - 1); + br.release(); + + // decode with the exact buffer + br = BuffersReader.list(exact); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(exact.position(), offset + actualSize); + assertFalse(exact.hasRemaining()); + assertEquals(br.position(), offset + actualSize); + assertFalse(br.hasRemaining()); + br.release(); + assertEquals(br.read(), actualSize); + assertFalse(br.hasRemaining()); + + + // decode with the longer buffer + long read = br.read(); + assertEquals(br.limit(), 0); + assertEquals(br.position(), 0); + br.add(longer); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.remaining(), 10); + assertEquals(br.position(), offset + actualSize); + assertEquals(br.remaining(), 10); + br.release(); + assertEquals(br.read() - read, actualSize); + assertEquals(br.remaining(), 10); + } + } + + @ParameterizedTest + @MethodSource("tests") + void testAbsolutes(TestCase testCase) { + + List bbs = testCase.bbs(); + // Add byte buffers that match the description in bbs to the BuffersReader. + // The byte buffer bytes that should never be read are set to -1, this way + // if a get returns -1 we know it's peeking outside the expected range. + // bytes at any valid readable position are set to (position - start) % 128 + var reader = BuffersReader.list(); + int val = 0; + for (var bb : bbs) { + var b = ByteBuffer.allocate(bb.capacity); + for (int i=0; i reader.get()); + System.err.printf("Got expected BufferUnderflowException for %s: %s%n", reader.position(), bue); + + if (!testCase.simples.isEmpty()) { + System.err.println("\n*** Simple tests\n"); + } + for (var simple : testCase.simples) { + System.err.printf("get(%s) with position=%s, expect %s%n", + simple.index, simple.position, simple.expected); + long p0 = reader.position(); + reader.position(simple.position); + assertEquals(reader.get(simple.index), simple.expected); + reader.position(p0); + assertEquals(reader.position(), reader.limit()); + } + + System.err.println("\n*** Testing BuffersReader::get(long)\n"); + for (long i=0; i < limit; i++) { + final long pos = i; + if (pos < start) { + var ioobe = assertThrows(IndexOutOfBoundsException.class, () -> reader.get(pos)); + System.err.printf("Got expected IndexOutOfBoundsException for %s: %s%n", pos, ioobe); + } else { + assertEquals(reader.get(pos), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + } + } + System.err.println("\n*** Testing BuffersReader::position(long)\n"); + for (long i=0; i <= limit; i++) { + final long pos = limit-i; + final long rpos = i; + if (pos < start) { + try { + var iae = assertThrows(IAE, () -> reader.position(pos)); + System.err.printf("Got expected IllegalArgumentException for %s: %s%n", pos, iae); + } catch (AssertionError error) { + System.err.printf(error.getMessage() + " for start: %s, index: %s, limit: %s", + start, pos, limit); + throw error; + } + } else { + System.err.printf("> reader.position(%s -> %s)%n", reader.position(), pos); + reader.position(pos); + if (pos < limit) { + try { + assertEquals(reader.get(), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + System.err.printf("> reader.position is now %s%n", reader.position()); + assertEquals(reader.read(), pos - start + 1); + } catch (RuntimeException x) { + System.err.println("get failed at index " + pos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + } + if (rpos >= start && rpos < limit) { + try { + System.err.printf("get(%s) with position=%s, expect %s%n", + rpos, reader.position(), (rpos - start) % 128); + assertEquals(reader.get(rpos), (rpos - start) % 128, + "get failed at index " + rpos + " " + + "(start: " + start + ", limit: " + limit + ")"); + } catch (RuntimeException x) { + System.err.println("get failed at index " + rpos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + assertEquals(reader.read(), reader.position() - start); + if (rpos < start) { + var iae = assertThrows(IAE, () -> reader.position(rpos)); + System.err.printf("Got expected IllegalArgumentException for %s: %s%n", rpos, iae); + } else { + System.err.printf("< reader.position(%s -> %s)%n", reader.position(), rpos); + reader.position(rpos); + if (rpos < limit) { + try { + assertEquals(reader.get(), (rpos - start) % 128, + "get failed at index " + rpos + " " + + "(start: " + start + ", limit: " + limit + ")"); + assertEquals(reader.read(), rpos - start + 1); + System.err.printf("< reader.position is now %s%n", reader.position()); + } catch (RuntimeException x) { + System.err.println("get failed at index " + rpos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + } + if (pos >= start && pos < limit) { + try { + System.err.printf("get(%s) with position=%s, expect %s%n", + pos, reader.position(), (pos - start) % 128); + assertEquals(reader.get(pos), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + } catch (RuntimeException x) { + System.err.println("get failed at index " + pos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + assertEquals(reader.read(), reader.position() - start); + } + + System.err.println("\n*** Testing BuffersReader::position(rand1) and get(rand2)\n"); + List positions = LongStream.range(0, limit+1).mapToObj(Long::valueOf) + .collect(Collectors.toCollection(ArrayList::new)); + Collections.shuffle(positions, RAND); + List indices = LongStream.range(0, limit+1).mapToObj(Long::valueOf) + .collect(Collectors.toCollection(ArrayList::new)); + Collections.shuffle(indices, RAND); + for (int i = 0; i <= limit; i++) { + long pos = positions.get(i); + long index = indices.get(i); + System.err.printf("position(%s) -> get() -> get(%s)%n", pos, index); + if (pos < start) { + try { + var iae = assertThrows(IAE, () -> reader.position(pos)); + System.err.printf("Got expected IllegalArgumentException for %s: %s%n", pos, iae); + } catch (AssertionError error) { + System.err.printf(error.getMessage() + " for start: %s, index: %s, limit: %s", + start, pos, limit); + throw error; + } + } else { + System.err.printf("> reader.position(%s -> %s)%n", reader.position(), pos); + reader.position(pos); + if (pos < limit) { + try { + assertEquals(reader.get(), (pos - start) % 128, + "get failed at index " + pos + " " + + "(start: " + start + ", limit: " + limit + ")"); + System.err.printf("> reader.position is now %s%n", reader.position()); + assertEquals(reader.read(), pos - start + 1); + } catch (RuntimeException x) { + System.err.println("get failed at index " + pos + + " (start: " + start + ", limit: " + limit + ")" + x); + throw x; + } + } + } + if (index < start || index >= limit) { + var ioobe = assertThrows(IndexOutOfBoundsException.class, () -> reader.get(index)); + System.err.printf("Got expected IndexOutOfBoundsException for %s: %s%n", index, ioobe); + } else { + assertEquals(reader.get(index), (index - start) % 128, + "get failed at index " + index + " " + + "(start: " + start + ", limit: " + limit + ")"); + } + } + + } +} diff --git a/test/jdk/java/net/httpclient/quic/BuffersReaderVLTest.java b/test/jdk/java/net/httpclient/quic/BuffersReaderVLTest.java new file mode 100644 index 00000000000..5064bbd5cb3 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/BuffersReaderVLTest.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.ByteBuffer; +import java.util.List; + +import jdk.internal.net.http.quic.BuffersReader; +import jdk.internal.net.http.quic.BuffersReader.ListBuffersReader; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jtreg.SkippedException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.quic + * @run testng/othervm BuffersReaderVLTest + * @summary Tests to check quic/util methods encode/decodeVariableLength methods + * work as expected. + */ +public class BuffersReaderVLTest { + static final Class IAE = IllegalArgumentException.class; + + @DataProvider(name = "decode invariants") + public Object[][] decodeInvariants() { + return new Object[][] + { + { new byte[]{7}, 7, 1 }, // 00 + { new byte[]{65, 11}, 267, 2 }, // 01 + { new byte[]{-65, 11, 22, 33}, 1057691169, 4 }, // 10 + { new byte[]{-1, 11, 22, 33, 44, 55, 66, 77}, 4542748980864827981L, 8 }, // 11 + { new byte[]{-1, -11, -22, -33, -44, -55, -66, -77}, 4608848040752168627L, 8 }, + { new byte[]{}, -1, 0 }, + { new byte[]{-65}, -1, 0 }, + }; + } + @DataProvider(name = "prefix invariants") + public Object[][] prefixInvariants() { + return new Object[][] + { + { Long.MAX_VALUE, 0, IAE }, + { 4611686018427387903L+1, 0, IAE }, + { 4611686018427387903L, 3, null }, + { 4611686018427387903L-1, 3, null }, + { 1073741823+1, 3, null }, + { 1073741823, 2, null }, // (length > (1L << 30)-1) + { 1073741823-1, 2, null }, + { 16383+1, 2, null }, + { 16383, 1, null }, // (length > (1L << 14)-1 + { 16383-1, 1, null }, + { 63+1, 1, null }, + { 63 , 0, null }, // (length > (1L << 6)-1 + { 63-1, 0, null }, + { 100, 1, null }, + { 10, 0, null }, + { 1, 0, null }, + { 0, 0, null }, // (length >= 0) + { -1, 0, IAE }, + { -10, 0, IAE }, + { -100, 0, IAE }, + { Long.MIN_VALUE, 0, IAE }, + { -4611686018427387903L-1, 0, IAE }, + { -4611686018427387903L, 0, IAE }, + { -4611686018427387903L+1, 0, IAE }, + { -1073741823-1, 0, IAE }, + { -1073741823, 0, IAE }, // (length > (1L << 30)-1) + { -1073741823+1, 0, IAE }, + { -16383-1, 0, IAE }, + { -16383, 0, IAE }, // (length > (1L << 14)-1 + { -16383+1, 0, IAE }, + { -63-1, 0, IAE }, + { -63 , 0, IAE }, // (length > (1L << 6)-1 + { -63+1, 0, IAE }, + }; + } + + @Test(dataProvider = "decode invariants") + public void testDecode(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + BuffersReader br = BuffersReader.list(bb); + var actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, expectedPosition); + assertEquals(br.position(), expectedPosition); + br.release(); + assertEquals(br.read(), expectedPosition); + } + + @Test(dataProvider = "decode invariants") + public void testPeek(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + BuffersReader br = BuffersReader.list(bb); + var actualLength = VariableLengthEncoder.peekEncodedValue(br, 0); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, 0); + assertEquals(br.position(), 0); + br.release(); + assertEquals(br.read(), 0); + } + + // Encode the given length and then decodes it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodeDecode(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + } else { + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Decode %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too short, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer shorter = ByteBuffer.allocate(actualSize - 1 + offset); + shorter.position(offset); + ByteBuffer shorterref = ByteBuffer.allocate(actualSize - 1 + offset); + shorterref.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer too short + expectThrows(IAE, () -> VariableLengthEncoder.encode(shorter, length)); + assertEquals(shorter.position(), offset); + assertEquals(shorter.limit(), shorter.capacity()); + + assertEquals(shorter.mismatch(shorterref), -1); + assertEquals(shorterref.mismatch(shorter), -1); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(offset); + shortSlice.limit(offset + actualSize -1); + ListBuffersReader br = BuffersReader.list(shortSlice); + var actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), offset); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + assertEquals(br.position(), offset); + assertEquals(br.limit(), offset + actualSize - 1); + br.release(); + + // decode with the exact buffer + br = BuffersReader.list(exact); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(exact.position(), offset + actualSize); + assertFalse(exact.hasRemaining()); + assertEquals(br.position(), offset + actualSize); + assertFalse(br.hasRemaining()); + br.release(); + assertEquals(br.read(), actualSize); + assertFalse(br.hasRemaining()); + + + // decode with the longer buffer + long read = br.read(); + br.add(longer); + actualLength = VariableLengthEncoder.decode(br); + assertEquals(actualLength, length); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.remaining(), 10); + assertEquals(br.position(), offset + actualSize); + assertEquals(br.remaining(), 10); + br.release(); + assertEquals(br.read() - read, actualSize); + assertEquals(br.remaining(), 10); + } + + } + } + + // Encode the given length and then peeks it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodePeek(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + return; + } + + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Peek %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + exact.position(0); + longer.position(0); + exact.limit(exact.capacity()); + longer.limit(longer.capacity()); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(0); + shortSlice.limit(offset + actualSize - 1); + // need at least one byte to decode the size len... + var expectedSize = shortSlice.limit() <= offset ? -1 : actualSize; + assertEquals(VariableLengthEncoder.peekEncodedValueSize(shortSlice, offset), expectedSize); + var actualLength = VariableLengthEncoder.peekEncodedValue(shortSlice, offset); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), 0); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + + // decode with the exact buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(exact, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(exact, offset); + assertEquals(actualLength, length); + assertEquals(exact.position(), 0); + assertEquals(exact.limit(), exact.capacity()); + + // decode with the longer buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(longer, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(longer, offset); + assertEquals(actualLength, length); + assertEquals(longer.position(), 0); + assertEquals(longer.limit(), longer.capacity()); + } + + } + + + private ByteBuffer getTestBuffer(long length, int capacity) { + return switch (capacity) { + case 0 -> ByteBuffer.allocate(1).put((byte) length); + case 1 -> ByteBuffer.allocate(capacity).put((byte) length); + case 2 -> ByteBuffer.allocate(capacity).putShort((short) length); + case 4 -> ByteBuffer.allocate(capacity).putInt((int) length); + case 8 -> ByteBuffer.allocate(capacity).putLong(length); + default -> throw new SkippedException("bad value used for capacity"); + }; + } +} diff --git a/test/jdk/java/net/httpclient/quic/ConnectionIDSTest.java b/test/jdk/java/net/httpclient/quic/ConnectionIDSTest.java new file mode 100644 index 00000000000..e7fabeeecac --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/ConnectionIDSTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; + +import jdk.internal.net.http.quic.QuicConnectionIdFactory; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +/** + * @test + * @run testng/othervm ConnectionIDSTest + */ +public class ConnectionIDSTest { + + record ConnID(long token, byte[] bytes) { + ConnID { + bytes = bytes.clone(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnID connID = (ConnID) o; + return Arrays.equals(bytes, connID.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + @Override + public String toString() { + return "ConnID{" + + "token=" + token + + ", bytes=" + HexFormat.of().formatHex(bytes) + + '}'; + } + } + + @Test + public void testConnectionIDS() { + List ids = new ArrayList<>(); + + // regular test, for length in [-21, 21] + long previous = 0; + QuicConnectionIdFactory idFactory = QuicConnectionIdFactory.getClient(); + for (int length = -21; length <= 22 ; length++) { + int expectedLength = Math.min(length, 20); + expectedLength = Math.max(9, expectedLength); + long token = idFactory.newToken(); + assertEquals(token, previous +1); + previous = token; + var id = idFactory.newConnectionId(length, token); + var cid = new ConnID(token, id); + System.out.printf("%s: %s/%s%n", length, token, cid); + assertEquals(id.length, expectedLength); + assertEquals(idFactory.getConnectionIdLength(id), expectedLength); + assertEquals(idFactory.getConnectionIdToken(id), token); + ids.add(cid); + } + + // token length test, for token coded on [1, 8] bytes, + // with positive and negative values, for cid length=9 + // Ox7F, -Ox7F, 0x7F7F, -0x7F7F, etc... + previous = 0; + int length = 9; + for (int i=0; i<8; i++) { + long ptoken = (previous << 8) + 0x7F; + long ntoken = - ptoken; + previous = ptoken; + for (long token : List.of(ptoken, ntoken)) { + long expectedToken = token >= 0 ? token : -token -1; + var id = idFactory.newConnectionId(length, token); + var cid = new ConnID(expectedToken, id); + System.out.printf("%s: %s/%s%n", length, token, cid); + assertEquals(id.length, length); + assertEquals(idFactory.getConnectionIdLength(id), length); + assertEquals(idFactory.getConnectionIdToken(id), expectedToken); + ids.add(cid); + } + } + + // test token bounds, for various cid length... + var bounds = List.of(Long.MIN_VALUE, Long.MIN_VALUE + 1L, Long.MIN_VALUE + 255L, -1L, + 0L, 1L, Long.MAX_VALUE -255L, Long.MAX_VALUE - 1L, Long.MAX_VALUE); + // test the bounds twice to try to trigger duplicates with length = 9 + bounds = bounds.stream().mapMulti((n,c) -> {c.accept(n); c.accept(n);}).toList(); + for (length=9; length <= 20; length++) { + for (long token : bounds) { + long expectedToken = token >= 0 ? token : -token - 1; + var id = idFactory.newConnectionId(length, token); + var cid = new ConnID(expectedToken, id); + System.out.printf("%s: %s/%s%n", length, token, cid); + assertEquals(id.length, length); + assertEquals(idFactory.getConnectionIdLength(id), length); + assertEquals(idFactory.getConnectionIdToken(id), expectedToken); + ids.add(cid); + } + } + + // now verify uniqueness + Map tested = new HashMap(); + record duplicates(ConnID first, ConnID second) {} + List duplicates = new ArrayList<>(); + for (var cid : ids) { + if (tested.containsKey(cid)) { + var dup = new duplicates(tested.get(cid), cid); + System.out.printf("duplicate ids: %s%n", dup); + duplicates.add(dup); + } else { + tested.put(cid, cid); + } + } + + // some duplicates can be expected if the connection id is too short + // and the token value is too big; check and remove them + for (var iter = duplicates.iterator(); iter.hasNext(); ) { + var dup = iter.next(); + assertEquals(dup.first.token(), dup.second.token()); + assertEquals(dup.first.bytes().length, dup.second.bytes().length); + assertEquals(dup.first.bytes(), dup.second.bytes()); + long mask = 0x00FFFFFF00000000L; + for (int i=0; i<3; i++) { + mask = mask << 8; + assert (mask & 0xFF00000000000000L) != 0 + : "mask: " + Long.toHexString(mask); + if (dup.first.bytes().length == (9+i)) { + if ((dup.first.token() & mask) != 0L) { + iter.remove(); + System.out.println("duplicates expected due to lack of entropy: " + dup); + } + } + } + } + + // verify no unexpected duplicates + for (var dup : duplicates) { + System.out.println("unexpected duplicate: " + dup); + } + assertEquals(duplicates.size(), 0); + } +} diff --git a/test/jdk/java/net/httpclient/quic/CryptoWriterQueueTest.java b/test/jdk/java/net/httpclient/quic/CryptoWriterQueueTest.java new file mode 100644 index 00000000000..cfc62625605 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/CryptoWriterQueueTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.streams.CryptoWriterQueue; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; + +import static org.testng.Assert.*; + +/** + * @test + * @summary Tests jdk.internal.net.http.quic.streams,CryptoWriterQueue + * @modules java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.quic.frames + * @run testng CryptoWriterQueueTest + */ +public class CryptoWriterQueueTest { + + /** + * {@link CryptoWriterQueue#enqueue(ByteBuffer) enqueues} data from multiple ByteBuffer + * instances and then expects the {@link CryptoWriterQueue#produceFrame(int)} to process + * the enqueued data correctly. + */ + @Test + public void testProduceFrame() throws Exception { + final CryptoWriterQueue writerQueue = new CryptoWriterQueue(); + final ByteBuffer buff1 = createByteBuffer(83); + final ByteBuffer buff2 = createByteBuffer(1429); + final ByteBuffer buff3 = createByteBuffer(4); + // enqueue them + writerQueue.enqueue(buff1); + writerQueue.enqueue(buff2); + writerQueue.enqueue(buff3); + final int expectedRemaining = buff1.remaining() + buff2.remaining() + buff3.remaining(); + assertEquals(writerQueue.remaining(), expectedRemaining, + "Unexpected remaining bytes in CryptoWriterQueue"); + // create frame(s) from the enqueued buffers + final int maxPayloadSize = 1134; + while (writerQueue.remaining() > 0) { + final CryptoFrame frame = writerQueue.produceFrame(maxPayloadSize); + assertNotNull(frame, "Crypto frame is null"); + assertTrue(frame.size() <= maxPayloadSize, "Crypto size " + frame.size() + + " exceeds max payload size of " + maxPayloadSize); + } + } + + private static ByteBuffer createByteBuffer(final int numBytes) { + return ByteBuffer.wrap(new byte[numBytes]); + } + +} diff --git a/test/jdk/java/net/httpclient/quic/KeyUpdateTest.java b/test/jdk/java/net/httpclient/quic/KeyUpdateTest.java new file mode 100644 index 00000000000..631a054b2f9 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/KeyUpdateTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Stack; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicConnection; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import sun.security.ssl.QuicTLSEngineImpl; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/* + * @test + * @summary verifies the QUIC TLS key update process + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @modules java.net.http/jdk.internal.net.http + * java.net.http/jdk.internal.net.http.common + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * + * @modules java.base/jdk.internal.util + * java.base/sun.security.ssl + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.httpclient.test.lib.common.TestUtil + * jdk.test.lib.net.SimpleSSLContext + * @comment the test is run with -Djava.security.properties= to augment + * the master java.security file + * @run testng/othervm -Djava.security.properties=${test.src}/quic-tls-keylimits-java.security + * -Djdk.internal.httpclient.debug=true + * -Djavax.net.debug=all + * KeyUpdateTest + */ +public class KeyUpdateTest { + + private QuicStandaloneServer server; + private SSLContext sslContext; + private ExecutorService executor; + + private static final byte[] HELLO_MSG = "Hello Quic".getBytes(StandardCharsets.UTF_8); + private static final EchoHandler handler = new EchoHandler(HELLO_MSG.length); + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .build(); + // add a handler which deals with incoming connections + server.addHandler(handler); + server.start(); + System.out.println("Server started at " + server.getAddress()); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + if (executor != null) { + executor.close(); + } + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + @Test + public void test() throws Exception { + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + final Stack clientConnKeyPhases = new Stack<>(); + for (int i = 1; i <= 100; i++) { + System.out.println("Iteration: " + i); + // open a bidi stream + final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + // write data on the stream + try (final OutputStream os = bidiStream.outputStream()) { + os.write(HELLO_MSG); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for response + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("client: reading from bidi stream's input stream"); + final byte[] data = is.readAllBytes(); + System.out.println("client: Received response of size " + data.length); + final String response = new String(data, StandardCharsets.UTF_8); + // verify response + System.out.println("client: Response: " + response); + if (!Arrays.equals(response.getBytes(StandardCharsets.UTF_8), HELLO_MSG)) { + throw new AssertionError("Unexpected response: " + response); + } + } finally { + System.err.println("client: Closing bidi stream from test"); + bidiStream.close(); + } + // keep track of the 1-RTT key phase that was used by the client connection + final int invocation = i; + getKeyPhase(conn.underlyingQuicConnection()).ifPresent((keyPhase) -> { + if (clientConnKeyPhases.empty() || clientConnKeyPhases.peek() != keyPhase) { + // new key phase detected, add it + clientConnKeyPhases.push(keyPhase); + System.out.println("Detected client 1-RTT key phase " + keyPhase + + " on connection " + conn + " for invocation " + invocation); + } + }); + } + // verify that the client and server did do a key update + // stacks should contain at least a sequence of 0, 1, 0 + System.out.println("Number of 1-RTT keys used by client connection: " + + clientConnKeyPhases.size() + ", key phase switches: " + clientConnKeyPhases); + System.out.println("Number of 1-RTT keys used by server connection: " + + handler.serverConnKeyPhases.size() + ", key phase switches: " + + handler.serverConnKeyPhases); + assertTrue(clientConnKeyPhases.size() >= 3, "Client connection" + + " didn't do a key update"); + assertTrue(handler.serverConnKeyPhases.size() >= 3, "Server connection" + + " didn't do a key update"); + + assertEquals(0, (int) clientConnKeyPhases.getFirst(), "Client connection used" + + " unexpected first key phase"); + assertEquals(0, (int) handler.serverConnKeyPhases.getFirst(), "Server connection used" + + " unexpected first key phase"); + + assertEquals(1, (int) clientConnKeyPhases.get(1), "Client connection used" + + " unexpected second key phase"); + assertEquals(1, (int) handler.serverConnKeyPhases.get(1), "Server connection used" + + " unexpected second key phase"); + + assertEquals(0, (int) clientConnKeyPhases.get(2), "Client connection used" + + " unexpected third key phase"); + assertEquals(0, (int) handler.serverConnKeyPhases.get(2), "Server connection used" + + " unexpected third key phase"); + } + } + + /** + * Reads data from incoming client initiated bidirectional stream of a Quic connection + * and writes back a response which is same as the read data + */ + private static final class EchoHandler implements QuicServerHandler { + + private final int numBytesToRead; + private final AtomicInteger numInvocations = new AtomicInteger(); + private final Stack serverConnKeyPhases = new Stack<>(); + + private EchoHandler(final int numBytesToRead) { + this.numBytesToRead = numBytesToRead; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + final int invocation = numInvocations.incrementAndGet(); + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + // keep track of the 1-RTT key phase that was used by the server connection + getKeyPhase(conn).ifPresent((keyPhase) -> { + if (this.serverConnKeyPhases.empty() + || this.serverConnKeyPhases.peek() != keyPhase) { + // new key phase detected, add it + this.serverConnKeyPhases.push(keyPhase); + System.out.println("Detected server 1-RTT key phase " + keyPhase + + " on connection " + conn + " for invocation " + invocation); + } + }); + final byte[] data; + // read the request content + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("Handler reading data from bidi stream's inputstream " + is); + data = is.readAllBytes(); + System.out.println("Handler read " + data.length + " bytes of data"); + } + if (data.length != numBytesToRead) { + throw new IOException("Expected to read " + numBytesToRead + + " bytes but read only " + data.length + " bytes"); + } + // write response + try (final OutputStream os = bidiStream.outputStream()) { + System.out.println("Handler writing data to bidi stream's outputstream " + os); + os.write(data); + } + System.out.println("Handler invocation complete"); + } + } + + private static Optional getKeyPhase(final QuicConnection conn) throws IOException { + if (!(conn.getTLSEngine() instanceof QuicTLSEngineImpl qtls)) { + return Optional.empty(); + } + final int keyPhase; + try { + keyPhase = qtls.getOneRttKeyPhase(); + } catch (QuicKeyUnavailableException e) { + throw new IOException("failed to get key phase, reason: " + e.getMessage()); + } + if (keyPhase != 0 && keyPhase != 1) { + throw new IOException("Unexpected 1-RTT key phase on connection: " + conn); + } + return Optional.of(keyPhase); + } +} diff --git a/test/jdk/java/net/httpclient/quic/OrderedFlowTest.java b/test/jdk/java/net/httpclient/quic/OrderedFlowTest.java new file mode 100644 index 00000000000..cb95620c00e --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/OrderedFlowTest.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.ToLongFunction; +import java.util.stream.Collectors; + +import jdk.internal.net.http.quic.OrderedFlow; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.frames.StreamFrame; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; + +/** + * @test + * @summary tests the reordering logic implemented by OrderedFlow + * and its two concrete subclasses + * @library /test/lib + * @run testng OrderedFlowTest + * @run testng/othervm -Dseed=-2680947227866359853 OrderedFlowTest + * @run testng/othervm -Dseed=-273117134353023275 OrderedFlowTest + * @run testng/othervm -Dseed=3649132517916066643 OrderedFlowTest + * @run testng/othervm -Dseed=4568737726943220431 OrderedFlowTest + */ +public class OrderedFlowTest { + + static final int WITH_DUPS = 1; + static final int WITH_OVERLAPS = 2; + + record TestData(Class frameType, + Supplier> flowSupplier, + Function payloadAccessor, + Comparator framesComparator, + List frames, + String expectedResult, + boolean duplicates, + boolean shuffled) { + + boolean hasEmptyFrames() { + return frames.stream().map(payloadAccessor) + .mapToInt(String::length) + .anyMatch((i) -> i == 0); + } + + @Override + public String toString() { + return frameType.getSimpleName() + + "(frames=" + frames.size() + + ", duplicates=" + duplicates + + ", shuffled=" + shuffled + + ", hasEmptyFrames=" + hasEmptyFrames() + + ")"; + } + } + + static final Random RANDOM = jdk.test.lib.RandomFactory.getRandom(); + static final String LOREM = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer + id elementum sem. In rhoncus nisi a ante convallis, at iaculis augue + elementum. Ut eget imperdiet justo, sed sodales est. In nec laoreet + lorem. Integer et arcu nibh. Quisque quis felis consectetur, luctus + libero eu, facilisis risus. Aliquam at viverra diam. Sed nec lacus + eget dui hendrerit porttitor et nec ligula. Suspendisse rutrum, + augue non ultricies vestibulum, metus orci faucibus est, et tempus + ante diam a quam. Proin venenatis justo eleifend vestibulum tincidunt. + + Nullam nec elementum sem. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. Integer euismod, + purus ut sollicitudin semper, quam turpis condimentum arcu, sit amet + suscipit sapien elit ac nisi. Orci varius natoque penatibus et magnis + dis parturient montes, nascetur ridiculus mus. Duis vel tortor non purus + scelerisque iaculis at efficitur dolor. Nam dapibus tellus non aliquet + suscipit. Nulla facilisis mi eget ex blandit sodales. Pellentesque enim + sem, aliquet non luctus id, feugiat in eros. Aliquam molestie felis + lorem, eget tristique nisi mollis lobortis. + + Suspendisse aliquam vitae purus nec mollis. Quisque et urna nec nunc + porttitor blandit quis a magna. Maecenas porta est velit, in volutpat + felis suscipit eget. Vivamus porta semper ipsum, et sodales nibh molestie + eget. Vivamus tincidunt quam id ante efficitur tincidunt. Suspendisse + potenti. Integer posuere felis ut semper feugiat. Vivamus id dui quam. + + Pellentesque accumsan quam non est pretium faucibus. Donec vel euismod + magna, ac scelerisque mauris. Nullam vitae varius diam, hendrerit semper + velit. Vestibulum et nisl felis. Orci varius natoque penatibus et magnis + dis parturient montes, nascetur ridiculus mus. Cras elementum auctor lacus, + vel tempor erat lobortis sed. Suspendisse sed felis ut mi condimentum + eleifend. Proin et arcu cursus, fermentum arcu non, tristique nulla. + Suspendisse tristique volutpat elit, et blandit metus aliquet id. Nunc non + dapibus dui. Nam sagittis justo magna. Nulla pharetra ex nec sem porta + consequat. + + Nam sit amet luctus ante, nec eleifend nunc. Phasellus lobortis lorem a + auctor ornare. Sed venenatis fermentum arcu, ut tincidunt turpis auctor + at. Praesent felis mi, tincidunt a sem et, luctus condimentum libero. + Phasellus egestas ac lectus vitae tincidunt. Etiam eu lobortis felis. + Nulla semper est ac nisl placerat, vitae sollicitudin diam lobortis. + Cras pellentesque semper purus at rutrum. Suspendisse a pellentesque + orci, ac tincidunt libero. Integer ex augue, ultrices sit amet aliquam + eget, laoreet eget elit. + """; + + interface FramesFactory { + public T create(int offset, String payload, boolean fin); + public int length(T frame); + public long offset(T frame); + public String getPayload(T frame); + public OrderedFlow flow(); + public Class frameType(); + public Comparator comparator(); + } + + static class StreamFrameFactory implements FramesFactory { + final long streamId = RANDOM.nextInt(0, Integer.MAX_VALUE); + + @Override + public StreamFrame create(int offset, String payload, boolean fin) { + byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); + int length = RANDOM.nextBoolean() ? bytes.length : -1; + return new StreamFrame(streamId, offset, length, + fin, ByteBuffer.wrap(bytes)); + } + + @Override + public int length(StreamFrame frame) { + return frame.dataLength(); + } + + @Override + public long offset(StreamFrame frame) { + return frame.offset(); + } + + @Override + public String getPayload(StreamFrame frame) { + int length = frame.dataLength(); + byte[] bytes = new byte[length]; + frame.payload().get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public OrderedFlow flow() { + return new OrderedFlow.StreamDataFlow(); + } + + @Override + public Class frameType() { + return StreamFrame.class; + } + + @Override + public Comparator comparator() { + return StreamFrame::compareOffsets; + } + } + + static class CryptoFrameFactory implements FramesFactory { + final long streamId = RANDOM.nextInt(0, Integer.MAX_VALUE); + + @Override + public CryptoFrame create(int offset, String payload, boolean fin) { + byte[] bytes = payload.getBytes(StandardCharsets.UTF_8); + int length = bytes.length; + return new CryptoFrame(offset, length, ByteBuffer.wrap(bytes)); + } + + @Override + public int length(CryptoFrame frame) { + return frame.length(); + } + + @Override + public long offset(CryptoFrame frame) { + return frame.offset(); + } + + @Override + public String getPayload(CryptoFrame frame) { + int length = frame.length(); + byte[] bytes = new byte[length]; + frame.payload().get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public OrderedFlow flow() { + return new OrderedFlow.CryptoDataFlow(); + } + + @Override + public Class frameType() { + return CryptoFrame.class; + } + + @Override + public Comparator comparator() { + return CryptoFrame::compareOffsets; + } + } + + static TestData generateData(FramesFactory factory, int options) { + int length = LOREM.length(); + int chunks = length/20; + int offset = 0; + int remaining = length; + List frames = new ArrayList<>(); + T first = null; + T second = null; + boolean duplicates = (options & WITH_DUPS) == WITH_DUPS; + boolean overlaps = (options & WITH_OVERLAPS) == WITH_OVERLAPS; + while (remaining > 0) { + int len = remaining < 20 + ? remaining + : RANDOM.nextInt(Math.min(19, remaining - 1), Math.min(chunks, remaining)); + remaining -= len; + String data = LOREM.substring(offset, offset + len); + T frame; + if (overlaps && len > 4) { + int start = RANDOM.nextInt(0, len/4); + int end = RANDOM.nextInt(3*len/4, len); + for (int i=start; i < end; i+=2) { + frame = factory.create(offset+i, data.substring(i, i+1), + i == len-1 && remaining == 0); + frames.add(frame); + } + } + frame = factory.create(offset, data, remaining == 0); + frames.add(frame); + if (first == null) first = frame; + else if (second == null) second = frame; + if (duplicates && RANDOM.nextInt(1, 5) > 3) { + frames.add(factory.create(offset, data, remaining == 0)); + } else if (overlaps && RANDOM.nextInt(1, 5) > 3 && second != null && len > 1) { + // next frame will overlap with this one. + offset -= len / 2; remaining += len / 2; + } + offset += len; + } + if (duplicates) frames.add(first); + if (overlaps && frames.size() > 1) { + if (factory.length(first) > 0) { + frames.remove(second); + String firstpayload = factory.getPayload(first); + String secondpayload = factory.getPayload(second); + String newpayload = firstpayload.charAt(firstpayload.length() - 1) + + secondpayload; + long newoffset = factory.offset(second) - 1; + assert newoffset >= 0; + frames.add(1, factory.create((int) newoffset, newpayload, frames.size() == 2)); + } + } + return new TestData<>(factory.frameType(), factory::flow, + factory::getPayload, factory.comparator(), + List.copyOf(frames), LOREM, + duplicates, false); + } + + // Returns a new data set where all frames have been shuffled randomly. + // This should help flush bugs with buffering of frames that come out of order. + static TestData shuffle(TestData data) { + List shuffled = new ArrayList<>(data.frames()); + Collections.shuffle(shuffled, RANDOM); + return new TestData<>(data.frameType(),data.flowSupplier(), data.payloadAccessor(), + data.framesComparator(), List.copyOf(shuffled), data.expectedResult(), + data.duplicates(), true); + } + + // Returns a new data set where all frames have been sorted in reverse + // order: largest offset first. This is the worst case scenario for + // buffering. This should help checking that the amount of data buffered + // never exceeds the length of the stream, as duplicates and overlaps should + // not be buffered. + static TestData reversed(TestData data) { + List sorted = new ArrayList<>(data.frames()); + Collections.sort(sorted, data.framesComparator().reversed()); + return new TestData<>(data.frameType(),data.flowSupplier(), data.payloadAccessor(), + data.framesComparator(), List.copyOf(sorted), data.expectedResult(), + data.duplicates(), true); + } + + static List> generateData(FramesFactory factory) { + List> result = new ArrayList<>(); + TestData data = generateData(factory, 0); + TestData withdups = generateData(factory, WITH_DUPS); + TestData withoverlaps = generateData(factory, WITH_OVERLAPS); + TestData withall = generateData(factory, WITH_DUPS | WITH_OVERLAPS); + result.add(data); + result.add(withdups); + result.add(withoverlaps); + result.add(withall); + result.add(reversed(data)); + result.add(reversed(withdups)); + result.add(reversed(withoverlaps)); + result.add(reversed(withall)); + for (int i=0; i<5; i++) { + result.add(shuffle(data)); + result.add(shuffle(withdups)); + result.add(shuffle(withoverlaps)); + result.add(shuffle(withall)); + } + return List.copyOf(result); + } + + @DataProvider(name="CryptoFrame") + Object[][] generateCryptoFrames() { + return generateData(new CryptoFrameFactory()) + .stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + @DataProvider(name="StreamFrame") + Object[][] generateStreanFrames() { + return generateData(new StreamFrameFactory()) + .stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + private void testOrderedFlow(TestData testData, ToLongFunction offset) { + System.out.println("\n ---------------- " + + testData.frameType().getName() + + " ---------------- \n"); + System.out.println("testOrderedFlow: " + testData); + String offsets = testData.frames().stream().mapToLong(offset) + .mapToObj(Long::toString).collect(Collectors.joining(", ")); + System.out.println("offsets: " + offsets); + + // we should not have empty frames, but maybe we do? + // if we do - should we make allowance for that? + var hasEmptyFrames = testData.hasEmptyFrames(); + assertFalse(hasEmptyFrames, "generated data has empty frames"); + + var flow = testData.flowSupplier().get(); + var size = LOREM.length(); + StringBuilder result = new StringBuilder(size); + long maxBuffered = 0; + for (var f : testData.frames()) { + T received = flow.receive(f); + var buffered = flow.buffered(); + maxBuffered = Math.max(buffered, maxBuffered); + assertTrue(buffered < size, + "buffered data %s exceeds or equals payload size %s".formatted(buffered, size)); + while (received != null) { + var payload = testData.payloadAccessor.apply(received); + assertNotEquals(payload, "", "empty frames not expected: " + received); + result.append(payload); + received = flow.poll(); + } + } + assertEquals(result.toString(), testData.expectedResult); + } + + @Test(dataProvider = "CryptoFrame") + public void testCryptoFlow(TestData testData) { + testOrderedFlow(testData, CryptoFrame::offset); + } + + @Test(dataProvider = "StreamFrame") + public void testStreamFlow(TestData testData) { + testOrderedFlow(testData, StreamFrame::offset); + } + +} diff --git a/test/jdk/java/net/httpclient/quic/PacketEncodingTest.java b/test/jdk/java/net/httpclient/quic/PacketEncodingTest.java new file mode 100644 index 00000000000..495e8d41403 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/PacketEncodingTest.java @@ -0,0 +1,1440 @@ +/* + * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.common.Utils; +import jdk.internal.net.http.quic.CodingContext; +import jdk.internal.net.http.quic.PeerConnectionId; +import jdk.internal.net.http.quic.QuicConnectionIdFactory; +import jdk.internal.net.http.quic.packets.LongHeader; +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicOneRttContext; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.internal.net.http.quic.frames.CryptoFrame; +import jdk.internal.net.http.quic.frames.PaddingFrame; +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.http.quic.packets.HandshakePacket; +import jdk.internal.net.http.quic.packets.InitialPacket; +import jdk.internal.net.http.quic.packets.LongHeaderPacket; +import jdk.internal.net.http.quic.packets.OneRttPacket; +import jdk.internal.net.http.quic.packets.QuicPacket; +import jdk.internal.net.http.quic.packets.QuicPacket.HeadersType; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace; +import jdk.internal.net.http.quic.packets.QuicPacket.PacketType; +import jdk.internal.net.http.quic.packets.QuicPacketDecoder; +import jdk.internal.net.http.quic.packets.QuicPacketEncoder; +import jdk.internal.net.http.quic.packets.QuicPacketNumbers; +import jdk.internal.net.http.quic.packets.RetryPacket; +import jdk.internal.net.http.quic.packets.ShortHeaderPacket; +import jdk.internal.net.http.quic.packets.VersionNegotiationPacket; +import jdk.internal.net.http.quic.packets.ZeroRttPacket; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportParametersConsumer; +import jdk.internal.net.http.quic.VariableLengthEncoder; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import javax.crypto.AEADBadTagException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.IntFunction; +import java.util.stream.Collectors; + +import static jdk.internal.net.http.quic.packets.QuicPacketNumbers.computePacketNumberLength; +import static org.testng.Assert.*; + +/** + * @test + * @library /test/lib + * @summary test packet encoding and decoding in unencrypted form and without + * any network involvement. + * @run testng/othervm -Dseed=2646683818688275736 PacketEncodingTest + * @run testng/othervm -Dseed=-3723256402256409075 PacketEncodingTest + * @run testng/othervm -Dseed=-3689060484817342283 PacketEncodingTest + * @run testng/othervm -Dseed=2425718686525936108 PacketEncodingTest + * @run testng/othervm -Dseed=-2996954753243104355 PacketEncodingTest + * @run testng/othervm -Dseed=8750823652999067800 PacketEncodingTest + * @run testng/othervm -Dseed=2906555779406889127 PacketEncodingTest + * @run testng/othervm -Dseed=902801756808168822 PacketEncodingTest + * @run testng/othervm -Dseed=5643545543196691308 PacketEncodingTest + * @run testng/othervm -Dseed=2646683818688275736 PacketEncodingTest + * @run testng/othervm -Djdk.internal.httpclient.debug=true PacketEncodingTest + */ +public class PacketEncodingTest { + + @DataProvider + public Object[][] longHeaderPacketProvider() { + final QuicVersion[] quicVersions = QuicVersion.values(); + final List params = new ArrayList<>(); + for (final QuicVersion version : quicVersions) { + final var p = new Object[][] { + // quic-version, srcIdLen, dstIdLen, pn, largestAck + new Object[] {version, 20, 20, 0L, -1L}, + new Object[] {version, 10, 20, 1L, 0L}, + new Object[] {version, 10, 20, 255L, 0L}, + new Object[] {version, 12, 15, 0xFFFFL, 0L}, + new Object[] {version, 9, 8, 0x7FFFFFFFL, 255L}, + new Object[] {version, 13, 11, 0x8FFFFFFFL, 0x10000000L}, + new Object[] {version, 19, 6, 0xFFFFFFFFL, 0xFFFFFFFEL}, + new Object[] {version, 6, 17, 0xFFFFFFFFFFL, 0xFFFFFFFF00L}, + new Object[] {version, 15, 14, 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L}, + new Object[] {version, 7, 9, 0xa82f9b32L, 0xa82f30eaL}, + new Object[] {version, 18, 16, 0xace8feL, 0xabe8b3L}, + new Object[] {version, 16, 19, 0xac5c02L, 0xabe8b3L} + }; + params.addAll(Arrays.asList(p)); + } + return params.toArray(Object[][]::new); + } + + @DataProvider + public Object[][] shortHeaderPacketProvider() { + final QuicVersion[] quicVersions = QuicVersion.values(); + final List params = new ArrayList<>(); + for (final QuicVersion version : quicVersions) { + final var p = new Object[][] { + new Object[] {version, 20, 0L, -1L}, + new Object[] {version, 17, 1L, 0L}, + new Object[] {version, 10, 255L, 0L}, + new Object[] {version, 12, 0xFFFFL, 0L}, + new Object[] {version, 9, 0x7FFFFFFFL, 255L}, + new Object[] {version, 13, 0x8FFFFFFFL, 0x10000000L}, + new Object[] {version, 19, 0xFFFFFFFFL, 0xFFFFFFFEL}, + new Object[] {version, 6, 0xFFFFFFFFFFL, 0xFFFFFFFF00L}, + new Object[] {version, 15, 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L}, + new Object[] {version, 7, 0xa82f9b32L, 0xa82f30eaL}, + new Object[] {version, 18, 0xace8feL, 0xabe8b3L}, + new Object[] {version, 16, 0xac5c02L, 0xabe8b3L}, + }; + params.addAll(Arrays.asList(p)); + } + return params.toArray(Object[][]::new); + } + + @DataProvider + public Object[][] versionAndRetryProvider() { + final QuicVersion[] quicVersions = QuicVersion.values(); + final List params = new ArrayList<>(); + for (final QuicVersion version : quicVersions) { + final var p = new Object[][] { + // quic-version, srcIdLen, dstIdLen, pn, largestAck + new Object[] {version, 20, 20}, + new Object[] {version, 10, 20}, + new Object[] {version, 12, 15}, + new Object[] {version, 9, 8}, + new Object[] {version, 13, 11}, + new Object[] {version, 19, 6}, + new Object[] {version, 6, 17}, + new Object[] {version, 15, 14}, + new Object[] {version, 7, 9}, + new Object[] {version, 18, 16}, + new Object[] {version, 16, 19}, + }; + params.addAll(Arrays.asList(p)); + } + return params.toArray(Object[][]::new); + } + + private static final AtomicLong IDS = new AtomicLong(); + private static final Random RANDOM = jdk.test.lib.RandomFactory.getRandom(); + private static final int MAX_DATAGRAM_IPV6 = 65527; + + byte[] randomIdBytes(int connectionLength) { + byte[] bytes = new byte[connectionLength]; + RANDOM.nextBytes(bytes); + return bytes; + } + + private static class DummyQuicTLSEngine implements QuicTLSEngine { + @Override + public HandshakeState getHandshakeState() { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean isTLSHandshakeComplete() { + return true; + } + + @Override + public KeySpace getCurrentSendKeySpace() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean keysAvailable(KeySpace keySpace) { + return true; + } + + @Override + public void discardKeys(KeySpace keySpace) { + // no-op + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + throw new AssertionError("should not come here!"); + } + + @Override + public void restartHandshake() throws IOException { + throw new AssertionError("should not come here!"); + } + + @Override + public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) { + throw new AssertionError("should not come here!"); + } + @Override + public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { } + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return 0; + } + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) { + return ByteBuffer.allocate(5); + } + + @Override + public int getAuthTagSize() { + return 0; + } + + @Override + public void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException { + // this dummy QUIC TLS engine doesn't do any encryption. + // we just copy over the raw packet payload into the output buffer + output.put(packetPayload); + } + + @Override + public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) { + packet.position(packet.position() + headerLength); + output.put(packet); + } + + @Override + public void signRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) { + output.put(ByteBuffer.allocate(16)); + } + @Override + public void verifyRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException { + } + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) { + throw new AssertionError("should not come here!"); + } + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) { + throw new AssertionError("should not come here!"); + } + @Override + public Runnable getDelegatedTask() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryMarkHandshakeDone() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryReceiveHandshakeDone() { + throw new AssertionError("should not come here!"); + } + + @Override + public Set getSupportedQuicVersions() { + return Set.of(QuicVersion.QUIC_V1); + } + + @Override + public void setUseClientMode(boolean mode) { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean getUseClientMode() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLParameters getSSLParameters() { + throw new AssertionError("should not come here!"); + } + + @Override + public void setSSLParameters(SSLParameters sslParameters) { + throw new AssertionError("should not come here!"); + } + + @Override + public String getApplicationProtocol() { + return null; + } + + @Override + public SSLSession getSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLSession getHandshakeSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public void versionNegotiated(QuicVersion quicVersion) { + // no-op + } + + @Override + public void setOneRttContext(QuicOneRttContext ctx) { + // no-op + } + } + + private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine(); + private static abstract class TestCodingContext implements CodingContext { + TestCodingContext() { } + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) { + throw new AssertionError("should not come here!"); + } + @Override + public QuicPacket parsePacket(ByteBuffer src) throws IOException { + throw new AssertionError("should not come here!"); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicConnectionId originalServerConnId() { + throw new AssertionError("should not come here!"); + } + + @Override + public QuicTLSEngine getTLSEngine() { + return TLS_ENGINE; + } + + @Override + public int minShortPacketPayloadSize(int destConnectionIdLength) { + return 100 - (destConnectionIdLength - connectionIdLength()); + } + } + + private void checkLongHeaderPacket(LongHeaderPacket packet, + PacketType packetType, + int versionNumber, + PacketNumberSpace packetNumberSpace, + long packetNumber, + QuicConnectionId srcConnectionId, + QuicConnectionId destConnectionId, + List payload, + int padding) { + List expected; + if (padding == 0) { + expected = payload; + } else if (payload.get(0) instanceof PaddingFrame pf) { + expected = new ArrayList<>(payload); + expected.set(0, new PaddingFrame(padding + pf.size())); + } else { + expected = new ArrayList<>(payload.size()+1); + expected.add(new PaddingFrame(padding)); + expected.addAll(payload); + } + checkLongHeaderPacket(packet, packetType, versionNumber, packetNumberSpace, packetNumber, + srcConnectionId, destConnectionId, expected); + + } + + private void checkLongHeaderPacket(LongHeaderPacket packet, + PacketType packetType, + int versionNumber, + PacketNumberSpace packetNumberSpace, + long packetNumber, + QuicConnectionId srcConnectionId, + QuicConnectionId destConnectionId, + List payload) { + // Check created packet + assertEquals(packet.headersType(), HeadersType.LONG); + assertEquals(packet.packetType(), packetType); + boolean hasLength = switch (packetType) { + case VERSIONS, RETRY -> false; + default -> true; + }; + assertEquals(packet.hasLength(), hasLength); + assertEquals(packet.numberSpace(), packetNumberSpace); + if (payload == null) { + assertTrue(packet.frames().isEmpty()); + } else { + assertEquals(getBuffers(packet.frames()), getBuffers(payload)); + } + assertEquals(packet.version(), versionNumber); + assertEquals(packet.packetNumber(), packetNumber); + assertEquals(packet.sourceId(), srcConnectionId); + assertEquals(packet.destinationId(), destConnectionId); + + } + + private static ByteBuffer encodeFrame(QuicFrame frame) { + ByteBuffer result = ByteBuffer.allocate(frame.size()); + frame.encode(result); + return result; + } + private static List getBuffers(List payload) { + return payload.stream().map(PacketEncodingTest::encodeFrame).toList(); + } + private static List getBuffers(List payload, int minSize) { + int payloadSize = payload.stream().mapToInt(QuicFrame::size).sum(); + if (payloadSize < minSize) { + payload = new ArrayList<>(payload); + payload.add(0, new PaddingFrame(minSize - payloadSize)); + } + return payload.stream().map(PacketEncodingTest::encodeFrame).toList(); + } + + private static String toHex(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, buffer.remaining()); + return HexFormat.of().formatHex(bytes); + } + + private static String toHex(List byteBuffers) { + return "0x" + byteBuffers.stream() + .map(PacketEncodingTest::toHex) + .collect(Collectors.joining(":")); + } + + private void checkLongHeaderPacketAt(ByteBuffer datagram, int offset, + PacketType packetType, int versionNumber, + QuicConnectionId srcConnectionId, + QuicConnectionId destConnectionId) { + assertEquals(QuicPacketDecoder.peekHeaderType(datagram, offset), HeadersType.LONG); + assertEquals(QuicPacketDecoder.of(datagram, offset).peekPacketType(datagram, offset), packetType); + LongHeader header = QuicPacketDecoder.peekLongHeader(datagram, offset); + assertNotNull(header, "Could not parse packet header"); + assertEquals(header.version(), versionNumber); + assertTrue(header.destinationId() + .matches(destConnectionId.asReadOnlyBuffer()), "Destination ID doesn't match"); + assertTrue(header.sourceId() + .matches(srcConnectionId.asReadOnlyBuffer()), "Source ID doesn't match"); + } + + private List frames(byte[] payload) throws IOException { + return frames(payload, false); + } + + private List frames(byte[] payload, boolean insert) throws IOException { + int payloadSize = payload.length; + ByteBuffer buf = ByteBuffer.wrap(payload); + List frames = new ArrayList<>(); + int remaining = payloadSize; + while (remaining > 7) { + int size = RANDOM.nextInt(1, remaining - 6); + byte[] data = new byte[size]; + RANDOM.nextBytes(data); + QuicFrame frame = new CryptoFrame(0, size, ByteBuffer.wrap(data)); + int encoded = frame.size(); + assertTrue(encoded > 0, String.valueOf(encoded)); + assertTrue(encoded <= remaining, String.valueOf(encoded)); + if (insert) { + frames.add(0, frame); + buf.position(remaining - encoded); + } else { + frames.add(frame); + } + frame.encode(buf); + remaining -= encoded; + } + if (remaining > 0) { + var padding = new PaddingFrame(remaining); + if (insert) { + frames.add(0, padding); + buf.position(0); + } else { + frames.add(padding); + } + padding.encode(buf); + } + if (insert) { + assertEquals(buf.position(), remaining); + assertEquals(buf.remaining(), payloadSize - remaining); + } else { + assertEquals(buf.remaining(), 0); + } + return List.copyOf(frames); + } + + private ByteBuffer toByteBuffer(QuicPacketEncoder encoder, QuicPacket outgoingQuicPacket, CodingContext context) + throws Exception { + int size = outgoingQuicPacket.size(); + ByteBuffer buffer = ByteBuffer.allocate(size); + encoder.encode(outgoingQuicPacket, buffer, context); + assertEquals(buffer.position(), size, " for " + outgoingQuicPacket); + buffer.flip(); + return buffer; + } + + private void checkShortHeaderPacket(ShortHeaderPacket packet, + PacketType packetType, + PacketNumberSpace packetNumberSpace, + long packetNumber, + QuicConnectionId destConnectionId, + List payload, + int minSize) { + // Check created packet + assertEquals(packet.headersType(), HeadersType.SHORT); + assertEquals(packet.packetType(), packetType); + assertEquals(packet.hasLength(), false); + assertEquals(packet.numberSpace(), packetNumberSpace); + assertEquals(getBuffers(packet.frames()), getBuffers(payload, minSize)); + assertEquals(packet.packetNumber(), packetNumber); + assertEquals(packet.destinationId(), destConnectionId); + } + + private void checkShortHeaderPacketAt(ByteBuffer datagram, int offset, + PacketType packetType, + QuicConnectionId destConnectionId, + CodingContext context) { + assertEquals(QuicPacketDecoder.peekHeaderType(datagram, offset), HeadersType.SHORT); + assertEquals(QuicPacketDecoder.of(QuicVersion.QUIC_V1).peekPacketType(datagram, offset), packetType); + assertEquals(QuicPacketDecoder.peekVersion(datagram, offset), 0); + int pos = datagram.position(); + if (pos != offset) datagram.position(offset); + try { + assertEquals(QuicPacketDecoder.peekShortConnectionId(datagram, destConnectionId.length()) + .mismatch(destConnectionId.asReadOnlyBuffer()), -1); + } finally { + if (pos != offset) datagram.position(pos); + } + } + + @Test(dataProvider = "longHeaderPacketProvider") + public void testInitialPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestInitialPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + final QuicConnectionId destConnectionId = new PeerConnectionId(destid); + assertEquals(destid.length, destConnectionId.length(), "dcid length"); + destIdLength = destid.length; + + byte[] srcid = randomIdBytes(srcIdLength); + final QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + assertEquals(srcid.length, srcConnectionId.length(), "scid length"); + + int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + // ensure that bound - tokenLength - 1 > 0 + assert bound > 4; + int tokenLength = RANDOM.nextInt(bound - 4); + + byte[] token = tokenLength == 0 ? null : new byte[tokenLength]; + if (token != null) RANDOM.nextBytes(token); + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - tokenLength - 1) + 1, 4 - packetNumberLength); + System.out.printf("testInitialPacket.encode(scid:%s, dcid:%s, token:%d, payload:%d)%n", + srcIdLength, destIdLength, tokenLength, payloadSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.INITIAL ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.INITIAL ? largestAcked : -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + int minsize = encoder.computeMaxInitialPayloadSize(context, + computePacketNumberLength(packetNumber, + context.largestAckedPN(PacketNumberSpace.INITIAL)), + tokenLength, srcIdLength, + destIdLength, 1200); + int padding = (payloadSize < minsize) ? minsize - payloadSize : 0; + System.out.println("testInitialPacket: available=%s, payload=%s, padding=%s" + .formatted(minsize, payloadSize, padding)); + + + byte[] payload = new byte[payloadSize]; + List frames = frames(payload, padding != 0); + assertEquals(frames.stream().mapToInt(QuicFrame::size) + .reduce(0, Math::addExact), payloadSize); + + + // Create an initial packet + var packet = encoder.newInitialPacket(srcConnectionId, + destConnectionId, + token, + packetNumber, + largestAcked, + frames, + context); + + if (padding > 0) { + var frames2 = new ArrayList<>(frames); + frames2.add(0, new PaddingFrame(padding)); + var packet2 = encoder.newInitialPacket(srcConnectionId, + destConnectionId, + token, + packetNumber, + largestAcked, + frames2, + context); + assertEquals(padding, padding + (1200 - packet2.size())); + } + + // Check created packet + assertTrue(packet instanceof InitialPacket); + var initialPacket = (InitialPacket) packet; + System.out.printf("%s: pn:%s, tklen:%s, payloadSize:%s, padding:%s, packet::size:%s, " + + "\n\tinputFrames: %s, " + + "\n\tencodedFrames:%s%n", + PacketType.INITIAL, packetNumber, tokenLength, payload.length, padding, + packet.size(), frames, packet.frames()); + checkLongHeaderPacket(initialPacket, PacketType.INITIAL, quicVersion.versionNumber(), + PacketNumberSpace.INITIAL, packetNumber, + srcConnectionId, destConnectionId, frames, padding); + assertEquals(initialPacket.tokenLength(), tokenLength); + assertEquals(initialPacket.token(), token); + assertEquals(initialPacket.hasLength(), true); + assertEquals(initialPacket.length(), packetNumberLength + payloadSize + padding); + + // Check that peeking at the encoded packet returns correct information + + // Decode the two packets in the datagram + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.INITIAL, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // coalesce two packets in a single datagram and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int second = offset + encoded.limit(); + System.out.printf("testInitialPacket.encode(offset:%d, second:%d)%n", + offset, second); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n", + offset, second, datagram.position(), datagram.limit()); + System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.INITIAL, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n", + second, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, second, PacketType.INITIAL, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip both packets + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), second); + decoder.skipPacket(datagram, second); + assertEquals(datagram.remaining(), offset); + + datagram.position(offset); + int size = second - offset; + for (int i=0; i<2; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof InitialPacket, "decoded: " + decodedPacket); + InitialPacket initialDecoded = InitialPacket.class.cast(decodedPacket); + checkLongHeaderPacket(initialDecoded, PacketType.INITIAL, quicVersion.versionNumber(), + PacketNumberSpace.INITIAL, packetNumber, + srcConnectionId, destConnectionId, frames, padding); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(initialDecoded.tokenLength(), tokenLength); + assertEquals(initialDecoded.token(), token); + assertEquals(initialDecoded.length(), initialPacket.length()); + assertEquals(initialDecoded.length(), packetNumberLength + payloadSize + padding); + } + assertEquals(datagram.position(), second + second - offset); + } + + @Test(dataProvider = "longHeaderPacketProvider") + public void testHandshakePacket(QuicVersion quicVersion, int srcIdLength, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestHandshakePacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength); + byte[] payload = new byte[payloadSize]; + var frames = frames(payload); + System.out.printf("testHandshakePacket.encode(payload:%d)%n", payloadSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.HANDSHAKE ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.HANDSHAKE ? largestAcked : -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + // Create an initial packet + var packet = encoder.newHandshakePacket(srcConnectionId, + destConnectionId, + packetNumber, + largestAcked, + frames, + context); + + // Check created packet + assertTrue(packet instanceof HandshakePacket); + var handshakePacket = (HandshakePacket) packet; + checkLongHeaderPacket(handshakePacket, PacketType.HANDSHAKE, quicVersion.versionNumber(), + PacketNumberSpace.HANDSHAKE, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(handshakePacket.hasLength(), true); + assertEquals(handshakePacket.length(), packetNumberLength + payloadSize); + + // Decode the two packets in the datagram + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.HANDSHAKE, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // coalesce two packets in a single datagram and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int second = offset + encoded.limit(); + System.out.printf("testHandshakePacket.encode(offset:%d, second:%d)%n", + offset, second); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n", + offset, second, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.HANDSHAKE, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n", + second, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, second, PacketType.HANDSHAKE, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip both packets + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), second); + decoder.skipPacket(datagram, second); + assertEquals(datagram.remaining(), offset); + + datagram.position(offset); + int size = second - offset; + for (int i=0; i<2; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof HandshakePacket, "decoded: " + decodedPacket); + HandshakePacket handshakeDecoded = HandshakePacket.class.cast(decodedPacket); + checkLongHeaderPacket(handshakeDecoded, PacketType.HANDSHAKE, quicVersion.versionNumber(), + PacketNumberSpace.HANDSHAKE, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(handshakeDecoded.length(), handshakePacket.length()); + assertEquals(handshakeDecoded.length(), packetNumberLength + payloadSize); + } + assertEquals(datagram.position(), second + second - offset); + } + + @Test(dataProvider = "longHeaderPacketProvider") + public void testZeroRTTPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestZeroRTTPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + int bound = MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength); + byte[] payload = new byte[payloadSize]; + var frames = frames(payload); + System.out.printf("testZeroRTTPacket.encode(payload:%d)%n", payloadSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + // Create an initial packet + var packet = encoder.newZeroRttPacket(srcConnectionId, + destConnectionId, + packetNumber, + largestAcked, + frames, + context); + + // Check created packet + assertTrue(packet instanceof ZeroRttPacket); + var zeroRttPacket = (ZeroRttPacket) packet; + checkLongHeaderPacket(zeroRttPacket, PacketType.ZERORTT, quicVersion.versionNumber(), + PacketNumberSpace.APPLICATION, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(zeroRttPacket.hasLength(), true); + assertEquals(zeroRttPacket.length(), packetNumberLength + payloadSize); + + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.ZERORTT, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // coalesce two packets in a single datagram and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int second = offset + encoded.limit(); + System.out.printf("testZeroRTTPacket.encode(offset:%d, second:%d)%n", + offset, second); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() * 2 + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, second:%d, position:%d, limit:%d)%n", + offset, second, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading first datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.ZERORTT, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + System.out.printf("reading second datagram(offset:%d, position:%d, limit:%d)%n", + second, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, second, PacketType.ZERORTT, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip both packets + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), second); + decoder.skipPacket(datagram, second); + assertEquals(datagram.remaining(), offset); + + // Decode the two packets in the datagram + datagram.position(offset); + int size = second - offset; + for (int i=0; i<2; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof ZeroRttPacket, "decoded: " + decodedPacket); + ZeroRttPacket zeroRttDecoded = ZeroRttPacket.class.cast(decodedPacket); + checkLongHeaderPacket(zeroRttDecoded, PacketType.ZERORTT, quicVersion.versionNumber(), + PacketNumberSpace.APPLICATION, packetNumber, + srcConnectionId, destConnectionId, frames); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(zeroRttDecoded.length(), zeroRttPacket.length()); + assertEquals(zeroRttDecoded.length(), packetNumberLength + payloadSize); + } + assertEquals(datagram.position(), second + second - offset); + } + + @Test(dataProvider = "versionAndRetryProvider") + public void testVersionNegotiationPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength) + throws Exception { + System.out.printf("%ntestVersionNegotiationPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, -1, -1); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + + final List versionList = new ArrayList<>(); + for (final QuicVersion qv : QuicVersion.values()) { + versionList.add(qv.versionNumber()); + } + System.out.printf("testVersionNegotiationPacket.encode(versions:%d)%n", versionList.size()); + + // Create an initial packet + var packet = QuicPacketEncoder.newVersionNegotiationPacket(srcConnectionId, + destConnectionId, + versionList.stream().mapToInt(Integer::intValue).toArray()); + + // Check created packet + assertTrue(packet instanceof VersionNegotiationPacket); + var versionPacket = (VersionNegotiationPacket) packet; + checkLongHeaderPacket(versionPacket, PacketType.VERSIONS, 0, + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(versionPacket.hasLength(), false); + assertEquals(versionPacket.supportedVersions(), + versionList.stream().mapToInt(Integer::intValue).toArray()); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + }; + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.VERSIONS, 0, + srcConnectionId, destConnectionId); + + // version negotiation packets can't be coalesced + int offset = RANDOM.nextInt(256); + int end = offset + encoded.limit(); + System.out.printf("testVersionNegotiationPacket.encode(offset:%d, end:%d)%n", + offset, end); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.VERSIONS, 0, + srcConnectionId, destConnectionId); + + // check that skip packet can skip packet + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), end); + assertEquals(datagram.remaining(), 0); + + // Decode the two packets in the datagram + datagram.position(offset); + int size = end - offset; + for (int i=0; i<1; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof VersionNegotiationPacket, "decoded: " + decodedPacket); + VersionNegotiationPacket decodedVersion = VersionNegotiationPacket.class.cast(decodedPacket); + checkLongHeaderPacket(decodedVersion, PacketType.VERSIONS, 0, + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(decodedVersion.supportedVersions(), + versionList.stream().mapToInt(Integer::intValue).toArray()); + } + assertEquals(datagram.position(), end); + } + + @Test(dataProvider = "versionAndRetryProvider") + public void testRetryPacket(QuicVersion quicVersion, int srcIdLength, int destIdLength) + throws Exception { + System.out.printf("%ntestRetryPacket(qv:%s, scid:%d, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, srcIdLength, destIdLength, -1, -1); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + byte[] srcid = randomIdBytes(srcIdLength); + QuicConnectionId srcConnectionId = new PeerConnectionId(srcid); + byte[] origId = randomIdBytes(destIdLength); + QuicConnectionId origConnectionId = new PeerConnectionId(origId); + int bound = (MAX_DATAGRAM_IPV6 - srcIdLength - destid.length - 7); + + int retryTokenLength = RANDOM.nextInt(bound - 16) + 1; + byte[] retryToken = new byte[retryTokenLength]; + RANDOM.nextBytes(retryToken); + System.out.printf("testRetryPacket.encode(token:%d)%n", retryTokenLength); + int expectedSize = 7 + 16 + destid.length + srcIdLength + retryTokenLength; + + // Create an initial packet + var packet = encoder.newRetryPacket(srcConnectionId, + destConnectionId, + retryToken); + + // Check created packet + assertTrue(packet instanceof RetryPacket); + var retryPacket = (RetryPacket) packet; + checkLongHeaderPacket(retryPacket, PacketType.RETRY, quicVersion.versionNumber(), + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(retryPacket.hasLength(), false); + assertEquals(retryPacket.retryToken(), retryToken); + assertEquals(retryPacket.size(), expectedSize); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return -1; + } + @Override public int connectionIdLength() { + return srcIdLength; + } + @Override public QuicConnectionId originalServerConnId() { return origConnectionId; } + }; + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkLongHeaderPacketAt(encoded, 0, PacketType.RETRY, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // version negotiation packets can't be coalesced + int offset = RANDOM.nextInt(256); + int end = offset + encoded.limit(); + System.out.printf("testRetryPacket.encode(offset:%d, end:%d)%n", + offset, end); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + + // check header, type and version of both packets + System.out.printf("datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + // set position to first packet to check connection ids + System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkLongHeaderPacketAt(datagram, offset, PacketType.RETRY, quicVersion.versionNumber(), + srcConnectionId, destConnectionId); + + // check that skip packet can skip packet + datagram.position(0); + datagram.limit(datagram.capacity()); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), end); + assertEquals(datagram.remaining(), 0); + + // Decode the two packets in the datagram + datagram.position(offset); + int size = end - offset; + for (int i=0; i<1; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof RetryPacket, "decoded: " + decodedPacket); + RetryPacket decodedRetry = RetryPacket.class.cast(decodedPacket); + checkLongHeaderPacket(decodedRetry, PacketType.RETRY, quicVersion.versionNumber(), + PacketNumberSpace.NONE, -1, + srcConnectionId, destConnectionId, null); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + assertEquals(decodedPacket.size(), expectedSize); + assertEquals(decodedRetry.retryToken(), retryToken); + } + assertEquals(datagram.position(), end); + } + + @Test(dataProvider = "shortHeaderPacketProvider") + public void testOneRTTPacket(QuicVersion quicVersion, int destIdLength, + long packetNumber, long largestAcked) throws Exception { + System.out.printf("%ntestOneRTTPacket(qv:%s, dcid:%d, pn:%d, ack:%d)%n", + quicVersion, destIdLength, packetNumber, largestAcked); + QuicPacketEncoder encoder = QuicPacketEncoder.of(quicVersion); + QuicPacketDecoder decoder = QuicPacketDecoder.of(quicVersion); + byte[] destid = QuicConnectionIdFactory.getClient() + .newConnectionId(destIdLength, IDS.incrementAndGet()); + assert destid.length <= 20; + QuicConnectionId destConnectionId = new PeerConnectionId(destid); + int bound = MAX_DATAGRAM_IPV6 - destid.length - 7 + - QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked) + - VariableLengthEncoder.getEncodedSize(MAX_DATAGRAM_IPV6); + + int packetNumberLength = + QuicPacketNumbers.computePacketNumberLength(packetNumber, largestAcked); + int payloadSize = Math.max(RANDOM.nextInt(bound - 1) + 1, 4 - packetNumberLength); + byte[] payload = new byte[payloadSize]; + var frames = frames(payload); + + CodingContext context = new TestCodingContext() { + @Override public long largestProcessedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + @Override public long largestAckedPN(PacketNumberSpace packetSpace) { + return packetSpace == PacketNumberSpace.APPLICATION ? largestAcked : -1; + } + // since we're going to decode the short packet, we need to return + // the same length that was used as destination cid in the packet + @Override public int connectionIdLength() { + return destid.length; + } + }; + + int paddedPayLoadSize = Math.max(payloadSize + packetNumberLength, context.minShortPacketPayloadSize(destid.length)); + System.out.printf("testOneRTTPacket.encode(payload:%d, padded:%d, destid.length: %d)%n", + payloadSize, paddedPayLoadSize, destid.length); + int expectedSize = 1 + destid.length + paddedPayLoadSize; + // Create an 1-RTT packet + OneRttPacket packet = encoder.newOneRttPacket(destConnectionId, + packetNumber, + largestAcked, + frames, + context); + + int minPayloadSize = context.minShortPacketPayloadSize(destConnectionId.length()) - packetNumberLength; + checkShortHeaderPacket(packet, PacketType.ONERTT, + PacketNumberSpace.APPLICATION, packetNumber, + destConnectionId, frames, minPayloadSize); + assertEquals(packet.hasLength(), false); + assertEquals(packet.size(), expectedSize); + + // Check that peeking at the encoded packet returns correct information + ByteBuffer encoded = toByteBuffer(encoder, packet, context); + checkShortHeaderPacketAt(encoded, 0, PacketType.ONERTT, + destConnectionId, context); + + // write packet at an offset in the datagram to simulate + // short packet coalesced after long packet and check + // the peek methods again + int offset = RANDOM.nextInt(256); + int end = offset + encoded.limit(); + System.out.printf("testOneRTTPacket.encode(offset:%d, end:%d)%n", + offset, end); + ByteBuffer datagram = ByteBuffer.allocate(encoded.limit() + offset * 2); + datagram.position(offset); + datagram.put(encoded); + encoded.flip(); + datagram.flip(); + assert datagram.limit() == offset + encoded.remaining(); + + // set position to first packet to check connection ids + System.out.printf("reading datagram(offset:%d, position:%d, limit:%d)%n", + offset, datagram.position(), datagram.limit()); + checkShortHeaderPacketAt(datagram, offset, PacketType.ONERTT, + destConnectionId, context); + + // check that skip packet can skip packet at offset + datagram.position(0); + datagram.limit(end); + decoder.skipPacket(datagram, offset); + assertEquals(datagram.position(), offset + expectedSize); + assertEquals(datagram.position(), datagram.limit()); + assertEquals(datagram.position(), datagram.capacity() - offset); + + + // Decode the packet in the datagram + datagram.position(offset); + int size = expectedSize; + for (int i=0; i<1; i++) { + int pos = datagram.position(); + System.out.printf("Decoding packet: %d at %d%n", (i+1), pos); + var decodedPacket = decoder.decode(datagram, context); + assertEquals(datagram.position(), pos + size); + assertTrue(decodedPacket instanceof OneRttPacket, "decoded: " + decodedPacket); + OneRttPacket oneRttDecoded = OneRttPacket.class.cast(decodedPacket); + List expectedFrames = frames; + if (frames.size() > 0 && frames.get(0) instanceof PaddingFrame) { + // The first frame should be a crypto frame, except if payloadSize + // was less than 7. + int frameSizes = frames.stream().mapToInt(QuicFrame::size).sum(); + assert frameSizes == payloadSize; + assert frameSizes <= 7; + // decoder will coalesce padding frames. So instead of finding + // two padding frames in the decoded packet we will find just one. + // To make the check pass, we should expect a bigger padding frame. + if (minPayloadSize > frameSizes) { + // replace the first frame with a bigger padding frame + expectedFrames = new ArrayList<>(frames); + var first = frames.get(0); + // replace the first frame with a bigger padding frame that + // coalesce the first padding payload frame with the padding that + // should have been added by the encoder. + // We will then be able to check that the decoded packet contains + // that single bigger padding frame. + expectedFrames.set(0, new PaddingFrame(minPayloadSize - frameSizes + first.size())); + } + } + checkShortHeaderPacket(oneRttDecoded, PacketType.ONERTT, + PacketNumberSpace.APPLICATION, packetNumber, + destConnectionId, expectedFrames, minPayloadSize); + assertEquals(decodedPacket.size(), packet.size()); + assertEquals(decodedPacket.size(), size); + } + assertEquals(datagram.position(), offset + size); + assertEquals(datagram.remaining(), 0); + assertEquals(datagram.limit(), end); + + } + + @Test + public void testNoMismatch() { + List match1 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20}) + ); + List match2 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + List match3 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + assertEquals(Utils.mismatch(match1, match1), -1); + assertEquals(Utils.mismatch(match2, match2), -1); + assertEquals(Utils.mismatch(match3, match3), -1); + assertEquals(Utils.mismatch(match1, match2), -1); + assertEquals(Utils.mismatch(match2, match1), -1); + assertEquals(Utils.mismatch(match1, match3), -1); + assertEquals(Utils.mismatch(match3, match1), -1); + assertEquals(Utils.mismatch(match2, match3), -1); + assertEquals(Utils.mismatch(match3, match2), -1); + } + + @Test + public void testMismatch() { + // match1, match2, match3 match with each others + List match1 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20}) + ); + List match2 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + List match3 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + // nomatch0, nomatch10, nomatch19 differ from the previous + // list at some index in [0..20[ + // nomatch0 mismatches at index 0 + List nomatch0 = List.of( + ByteBuffer.wrap(new byte[] {21, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20}) + ); + // nomatch10 mismatches at index 10 + List nomatch10 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 31}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}) + ); + // nomatch19 mismatches at index 19 + List nomatch19 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 40}) + ); + // morematch1 has one more byte at the end + List morematch1 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4}), + ByteBuffer.wrap(new byte[] {5, 6}), + ByteBuffer.wrap(new byte[] {7, 8}), + ByteBuffer.wrap(new byte[] {9}), + ByteBuffer.wrap(new byte[] {10, 11, 12}), + ByteBuffer.wrap(new byte[] {13, 14, 15, 16}), + ByteBuffer.wrap(new byte[] {17, 18, 19, 20, 41}) + ); + // morematch2 and morematch3 have the same 3 additional + // bytes at the end + List morematch2 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3}), + ByteBuffer.wrap(new byte[] {4, 5}), + ByteBuffer.wrap(new byte[] {6}), + ByteBuffer.wrap(new byte[] {7}), + ByteBuffer.wrap(new byte[] {8, 9}), + ByteBuffer.wrap(new byte[] {10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20}), + ByteBuffer.wrap(new byte[] {41, 42, 43}) + ); + List morematch3 = List.of( + ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}), + ByteBuffer.wrap(new byte[] {9, 10, 11}), + ByteBuffer.wrap(new byte[] {12, 13, 14}), + ByteBuffer.wrap(new byte[] {15}), + ByteBuffer.wrap(new byte[] {16, 17}), + ByteBuffer.wrap(new byte[] {18, 19, 20, 41, 42, 43}) + ); + + assertEquals(Utils.mismatch(nomatch0, nomatch0), -1L); + assertEquals(Utils.mismatch(nomatch10, nomatch10), -1L); + assertEquals(Utils.mismatch(nomatch19, nomatch19), -1L); + assertEquals(Utils.mismatch(morematch1, morematch1), -1L); + assertEquals(Utils.mismatch(morematch2, morematch2), -1L); + assertEquals(Utils.mismatch(morematch3, morematch3), -1L); + assertEquals(Utils.mismatch(morematch2, morematch3), -1L); + assertEquals(Utils.mismatch(morematch3, morematch2), -1L); + + for (var match : List.of(match1, match2, match3)) { + assertEquals(Utils.mismatch(match, nomatch0), 0L); + assertEquals(Utils.mismatch(match, nomatch10), 10L); + assertEquals(Utils.mismatch(match, nomatch19), 19L); + assertEquals(Utils.mismatch(nomatch0, match), 0L); + assertEquals(Utils.mismatch(nomatch10, match), 10L); + assertEquals(Utils.mismatch(nomatch19, match), 19L); + for (var morematch : List.of(morematch1, morematch2, morematch3)) { + assertEquals(Utils.mismatch(match, morematch), 20L); + assertEquals(Utils.mismatch(morematch, match), 20L); + } + + } + } + +} diff --git a/test/jdk/java/net/httpclient/quic/PacketLossTest.java b/test/jdk/java/net/httpclient/quic/PacketLossTest.java new file mode 100644 index 00000000000..1298e0977de --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/PacketLossTest.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.DatagramDeliveryPolicy; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/* + * @test + * @summary Verifies QUIC client interaction against servers which exhibit packet loss + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.httpclient.test.lib.common.TestUtil + * jdk.test.lib.net.SimpleSSLContext + * @run junit/othervm/timeout=240 -Djdk.internal.httpclient.debug=true + * -Djdk.httpclient.quic.minPtoBackoffTime=60 + * -Djdk.httpclient.quic.maxPtoBackoffTime=10 + * -Djdk.httpclient.quic.maxPtoBackoff=9 + * -Djdk.httpclient.HttpClient.log=quic,errors PacketLossTest + */ +public class PacketLossTest { + + private static SSLContext sslContext; + private static ExecutorService executor; + + private static final byte[] HELLO_MSG = "Hello Quic".getBytes(StandardCharsets.UTF_8); + + @BeforeAll + public static void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + } + + @AfterAll + public static void afterAll() throws Exception { + if (executor != null) { + executor.close(); + } + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + sealed interface DropPolicy { + final record DropRandomly() implements DropPolicy {} + final record DropEveryNth(int n) implements DropPolicy {} + final record DropNone() implements DropPolicy {} + final record DropAll() implements DropPolicy {} + default DatagramDeliveryPolicy policy() { + if (this instanceof DropRandomly) { + return DatagramDeliveryPolicy.dropRandomly(); + } else if (this instanceof DropEveryNth en) { + return DatagramDeliveryPolicy.dropEveryNth(en.n()); + } else if (this instanceof DropNone) { + return DatagramDeliveryPolicy.alwaysDeliver(); + } else if (this instanceof DropAll) { + return DatagramDeliveryPolicy.neverDeliver(); + } + throw new IllegalStateException("Unknown policy: " + this); + } + } + + record DropServer(String name, QuicStandaloneServer server) implements AutoCloseable { + @Override + public void close() throws IOException { + server.close(); + } + public InetSocketAddress getAddress() { + return server.getAddress(); + } + } + + DropServer of(QuicServer.Builder builder, DropPolicy incomimg, DropPolicy outgoing) + throws IOException { + QuicStandaloneServer server = builder + .incomingDeliveryPolicy(incomimg.policy()) + .outgoingDeliveryPolicy(outgoing.policy()) + .build(); + String name = "DropServer(%s, in: %s, out: %s)".formatted(server.name(), incomimg, outgoing); + return new DropServer(name, server); + } + + DropPolicy dropEveryNth(int n) { + return new DropPolicy.DropEveryNth(n); + } + DropPolicy dropRandomly() { + return new DropPolicy.DropRandomly(); + } + + // returns a List of unstarted Quic servers configured with different incoming/outgoing + // datagram delivery policies + private List unstartedServers() throws Exception { + final QuicServer.Builder builder = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext); + final List servers = new ArrayList<>(); + servers.add(of(builder, dropEveryNth(3), dropEveryNth(7))); + servers.add(of(builder, dropRandomly(), dropRandomly())); + servers.add(of(builder, dropEveryNth(5), dropRandomly())); + return servers; + } + + private static void startServer(final QuicStandaloneServer server) throws IOException { + // add a handler which deals with incoming connections + server.addHandler(new EchoHandler(HELLO_MSG.length)); + server.start(); + System.out.println("Server " + server.name() + " started at " + server.getAddress()); + } + + /** + * Uses {@link QuicClient} to pass data and expect back the data to/from Quic servers which + * might drop incoming/outgoing packets. + */ + @Test + public void testDataTransfer() throws Exception { + for (final DropServer server : unstartedServers()) { + startServer(server.server()); + try (server) { + System.out.printf("%n%n===== %s =====%n%n", server.name()); + System.err.printf("%n%n===== %s =====%n%n", server.name()); + final int numTimes = 20; + try (final QuicClient client = createClient()) { + final InetSocketAddress serverAddr = server.getAddress(); + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, serverAddr); + for (int i = 1; i <= numTimes; i++) { + System.out.println("iteration " + i + " against server: " + server.name() + + ", server addr: " + serverAddr); + // open a bidi stream + final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + // write data on the stream + try (final OutputStream os = bidiStream.outputStream()) { + os.write(HELLO_MSG); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for response + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("client: reading from bidi stream's input stream"); + final byte[] data = is.readAllBytes(); + System.out.println("client: Received response of size " + data.length); + final String response = new String(data, StandardCharsets.UTF_8); + // verify response + System.out.println("client: Response: " + response); + if (!Arrays.equals(response.getBytes(StandardCharsets.UTF_8), HELLO_MSG)) { + throw new AssertionError("Unexpected response: " + response); + } + } finally { + System.err.println("client: Closing bidi stream from test"); + bidiStream.close(); + } + } + } + } + } + } + + /** + * Reads data from incoming client initiated bidirectional stream of a Quic connection + * and writes back a response which is same as the read data + */ + private static final class EchoHandler implements QuicServerHandler { + + private final int numBytesToRead; + + private EchoHandler(final int numBytesToRead) { + this.numBytesToRead = numBytesToRead; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + final byte[] data; + // read the request content + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("Handler reading data from bidi stream's inputstream " + is); + data = is.readAllBytes(); + System.out.println("Handler read " + data.length + " bytes of data"); + } + if (data.length != numBytesToRead) { + throw new IOException("Expected to read " + numBytesToRead + + " bytes but read only " + data.length + " bytes"); + } + // write response + try (final OutputStream os = bidiStream.outputStream()) { + System.out.println("Handler writing data to bidi stream's outputstream " + os); + os.write(data); + } + System.out.println("Handler invocation complete"); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/PacketNumbersTest.java b/test/jdk/java/net/httpclient/quic/PacketNumbersTest.java new file mode 100644 index 00000000000..3a4237d0847 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/PacketNumbersTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import jdk.internal.net.http.quic.packets.QuicPacketNumbers; +import org.testng.SkipException; +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.expectThrows; + +/** + * @test + * @run testng PacketNumbersTest + */ +public class PacketNumbersTest { + + record EncodeResult(int expected, boolean assertion, Class failure) { + public static EncodeResult illegal() { + return new EncodeResult(-1, true, IllegalArgumentException.class); + } + public static EncodeResult asserting() { + return new EncodeResult(-1, true, AssertionError.class); + } + public static EncodeResult success(int result) { + return new EncodeResult(result, false, null); + } + public static EncodeResult fail(Class failure) { + return new EncodeResult(-1, false, failure); + } + public boolean fail() { + return failure() != null; + } + + @Override + public String toString() { + return fail() ? failure.getSimpleName() + : String.valueOf(expected); + } + } + record TestCase(String desc, long fullPN, long largestAck, EncodeResult result) { + static AtomicInteger count = new AtomicInteger(); + TestCase { + desc = count.incrementAndGet() + " - expecting " + desc; + } + public byte[] encode() { + return QuicPacketNumbers.encodePacketNumber(fullPN(), largestAck()); + } + + public long decode() { + byte[] encoded = encode(); + var largestProcessed = largestAck(); + return QuicPacketNumbers.decodePacketNumber(largestProcessed, + ByteBuffer.wrap(encoded), encoded.length); + } + + @Override + public String toString() { + return "%s: (%d, %d) -> %s".formatted(desc, fullPN, largestAck, result); + } + } + + @DataProvider + public Object[][] encode() { + return List.of( + // these first three test cases are extracted from RFC 9000, appendix A.2 and A.3 + new TestCase("success", 0xa82f9b32L, 0xa82f30eaL, EncodeResult.success(0x9b32)), + new TestCase("success", 0xace8feL, 0xabe8b3L, EncodeResult.success(0xace8fe & 0xFFFFFF)), + new TestCase("success", 0xac5c02L, 0xabe8b3L, EncodeResult.success(0xac5c02 & 0xFFFF)), + // additional test cases - these have been obtained empirically to test at the limits + new TestCase("success", 0x7FFFFFFFFFFFL, 0x7FFFFFFFFF00L, EncodeResult.success(0x0000FFFF)), + new TestCase("success", 0xFFFFFFFFFFL, 0xFFFFFFFF00L, EncodeResult.success(0x0000FFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xFFFFFFFEL, EncodeResult.success(0x000000FF)), + new TestCase("success", 0xFFFFFFFFL, 0xFFFFFF00L, EncodeResult.success(0x0000FFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xFFFF0000L, EncodeResult.success(0x00FFFFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xFF000000L, EncodeResult.success(0xFFFFFFFF)), + new TestCase("success", 0xFFFFFFFFL, 0xF0000000L, EncodeResult.success(0xFFFFFFFF)), + new TestCase("success", 0xFFFFFFFFL, 0x80000000L, EncodeResult.success(0xFFFFFFFF)), + new TestCase("illegal(5)",0xFFFFFFFFL, 0x7FFFFFFFL, EncodeResult.illegal()), + new TestCase("success", 0x8FFFFFFFL, 0x10000000L, EncodeResult.success(0x8FFFFFFF)), + new TestCase("illegal(5)",0x8FFFFFFFL, 0x0FFFFFFFL, EncodeResult.illegal()), + new TestCase("illegal(5)",0x8FFFFFFFL, 256L, EncodeResult.illegal()), + new TestCase("success", 0x7FFFFFFFL, 255L, EncodeResult.success(0x7FFFFFFF)), + new TestCase("success", 0x7FFFFFFFL, 0L, EncodeResult.success(0x7FFFFFFF)), + new TestCase("illegal(5)",0x7FFFFFFFL, -1L, EncodeResult.illegal()), + new TestCase("success", 0x6FFFFFFFL, 0L, EncodeResult.success(0x6FFFFFFF)), + new TestCase("success", 0xFFFFFFL, 0L, EncodeResult.success(0xFFFFFF)), + new TestCase("success", 0xFFFFL, 0L, EncodeResult.success(0xFFFF)), + new TestCase("success", 255L, 0L, EncodeResult.success(255)), + new TestCase("success", 1L, 0L, EncodeResult.success(1)), + new TestCase("success", 0x6FFFFFFFL, -1L, EncodeResult.success(0x6FFFFFFF)), + new TestCase("success", 0xFFFFFFL, -1L, EncodeResult.success(0xFFFFFF)), + new TestCase("success", 0xFFFFL, -1L, EncodeResult.success(0xFFFF)), + new TestCase("success", 255L, -1L, EncodeResult.success(255)), + new TestCase("success", 1L, -1L, EncodeResult.success(1)), + new TestCase("success", 0L, -1L, EncodeResult.success(0)), + new TestCase("assert", 0L, 1L, EncodeResult.asserting()), + new TestCase("assert", 0L, 0L, EncodeResult.asserting()), + new TestCase("assert", 1L, 1L, EncodeResult.asserting()) + ).stream().map(Stream::of) + .map(Stream::toArray) + .toArray(Object[][]::new); + } + + @Test(dataProvider = "encode") + public void testEncodePacketNumber(TestCase test) { + System.out.println(test); + if (test.result().assertion()) { + if (!QuicPacketNumbers.class.desiredAssertionStatus()) { + throw new SkipException("needs assertion enabled (-esa)"); + } + Throwable t = expectThrows(test.result().failure(), test::encode); + System.out.println("Got expected assertion: " + t); + return; + } + if (test.result().fail()) { + Throwable t = expectThrows(test.result().failure(), test::encode); + System.out.println("Got expected exception: " + t); + return; + + } + byte[] res = test.encode(); + int truncated = 0; + for (int i=0; i initial; + case HANDSHAKE -> handshake; + case APPLICATION -> app; + case NONE -> throw new AssertionError("invalid number space: " + packetNumberSpace); + }; + if (result == null) { + throw new AssertionError("invalid number space: " + packetNumberSpace); + } + return result; + } + } + + private static class DummyQuicTLSEngine implements QuicTLSEngine { + @Override + public HandshakeState getHandshakeState() { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean isTLSHandshakeComplete() { + return true; + } + + @Override + public KeySpace getCurrentSendKeySpace() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean keysAvailable(KeySpace keySpace) { + return true; + } + + @Override + public void discardKeys(KeySpace keySpace) { + // no-op + } + + @Override + public void setLocalQuicTransportParameters(ByteBuffer params) { + throw new AssertionError("should not come here!"); + } + + @Override + public void restartHandshake() throws IOException { + throw new AssertionError("should not come here!"); + } + + @Override + public void setRemoteQuicTransportParametersConsumer(QuicTransportParametersConsumer consumer) { + throw new AssertionError("should not come here!"); + } + @Override + public void deriveInitialKeys(QuicVersion version, ByteBuffer connectionId) { } + @Override + public int getHeaderProtectionSampleSize(KeySpace keySpace) { + return 0; + } + @Override + public ByteBuffer computeHeaderProtectionMask(KeySpace keySpace, boolean incoming, ByteBuffer sample) { + return ByteBuffer.allocate(5); + } + + @Override + public int getAuthTagSize() { + return 0; + } + + @Override + public void encryptPacket(KeySpace keySpace, long packetNumber, + IntFunction headerGenerator, + ByteBuffer packetPayload, ByteBuffer output) + throws QuicKeyUnavailableException, QuicTransportException { + // this dummy QUIC TLS engine doesn't do any encryption. + // we just copy over the raw packet payload into the output buffer + output.put(packetPayload); + } + + @Override + public void decryptPacket(KeySpace keySpace, long packetNumber, int keyPhase, + ByteBuffer packet, int headerLength, ByteBuffer output) { + packet.position(packet.position() + headerLength); + output.put(packet); + } + + @Override + public void signRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet, ByteBuffer output) { + throw new AssertionError("should not come here!"); + } + @Override + public void verifyRetryPacket(QuicVersion version, + ByteBuffer originalConnectionId, ByteBuffer packet) throws AEADBadTagException { + throw new AssertionError("should not come here!"); + } + @Override + public ByteBuffer getHandshakeBytes(KeySpace keySpace) { + throw new AssertionError("should not come here!"); + } + @Override + public void consumeHandshakeBytes(KeySpace keySpace, ByteBuffer payload) { + throw new AssertionError("should not come here!"); + } + @Override + public Runnable getDelegatedTask() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryMarkHandshakeDone() { + throw new AssertionError("should not come here!"); + } + @Override + public boolean tryReceiveHandshakeDone() { + throw new AssertionError("should not come here!"); + } + + @Override + public Set getSupportedQuicVersions() { + return Set.of(QuicVersion.QUIC_V1); + } + + @Override + public void setUseClientMode(boolean mode) { + throw new AssertionError("should not come here!"); + } + + @Override + public boolean getUseClientMode() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLParameters getSSLParameters() { + throw new AssertionError("should not come here!"); + } + + @Override + public void setSSLParameters(SSLParameters sslParameters) { + throw new AssertionError("should not come here!"); + } + + @Override + public String getApplicationProtocol() { + return null; + } + + @Override + public SSLSession getSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public SSLSession getHandshakeSession() { + throw new AssertionError("should not come here!"); + } + + @Override + public void versionNegotiated(QuicVersion quicVersion) { + // no-op + } + + @Override + public void setOneRttContext(QuicOneRttContext ctx) { + // no-op + } + } + + private static final QuicTLSEngine TLS_ENGINE = new DummyQuicTLSEngine(); + private static class TestCodingContext implements CodingContext { + final QuicPacketEncoder encoder; + final QuicPacketDecoder decoder; + final PacketSpaces spaces; + TestCodingContext(PacketSpaces spaces) { + this.spaces = spaces; + this.encoder = QuicPacketEncoder.of(QuicVersion.QUIC_V1); + this.decoder = QuicPacketDecoder.of(QuicVersion.QUIC_V1); + } + + @Override + public long largestProcessedPN(PacketNumberSpace packetNumberSpace) { + return spaces.get(packetNumberSpace).getLargestProcessedPN(); + } + + @Override + public long largestAckedPN(PacketNumberSpace packetNumberSpace) { + return spaces.get(packetNumberSpace).getLargestPeerAckedPN(); + } + + @Override + public int connectionIdLength() { + return CIDLEN; + } + + @Override + public int writePacket(QuicPacket packet, ByteBuffer buffer) + throws QuicKeyUnavailableException, QuicTransportException { + int pos = buffer.position(); + encoder.encode(packet, buffer, this); + return buffer.position() - pos; + } + @Override + public QuicPacket parsePacket(ByteBuffer src) + throws IOException, QuicKeyUnavailableException, QuicTransportException { + return decoder.decode(src, this); + } + @Override + public boolean verifyToken(QuicConnectionId destinationID, byte[] token) { + return true; + } + @Override + public QuicConnectionId originalServerConnId() { + return null; + } + + @Override + public QuicTLSEngine getTLSEngine() { + return TLS_ENGINE; + } + } + + /** + * An acknowledgement range, where acknowledged packets are [first..last]. + * For instance [9,9] acknowledges only packet 9. + * @param first the first packet acknowledged, inclusive + * @param last the last packet acknowledged, inclusive + */ + public static record Acknowledged(long first, long last) { + public Acknowledged { + assert first >= 0 && first <= last; + } + public boolean contains(long packet) { + return first <= packet && last >= packet; + } + public static List of(long... numbers) { + if (numbers == null || numbers.length == 0) return List.of(); + if (numbers.length % 2 != 0) throw new IllegalArgumentException(); + List res = new ArrayList<>(numbers.length/2); + for (int i = 0; i < numbers.length; i += 2) { + res.add(new Acknowledged(numbers[i], numbers[i+1])); + } + return List.copyOf(res); + } + } + + /** + * A packet to be emitted, followed by a pouse of {@code delay} in milliseconds. + * @param packetNumber the packet number of the packet to send + * @param delay a delay before the next packet should be emitted + */ + public static record Packet(long packetNumber, long delay) { + Packet(long packetNumber) { + this(packetNumber, RANDOM.nextLong(1, 255)); + } + static List ofAcks(List acks) { + return packets(acks); + } + static List of(long... numbers) { + return LongStream.of(numbers).mapToObj(Packet::new).toList(); + } + static final Comparator COMPARE_NUMBERS = Comparator.comparingLong(Packet::packetNumber); + } + + /** + * A test case. Composed of a list of acknowledgements, a list of packets, + * and a list of AckFrames. The list of packets is built from the list of + * acknowledgement - that is - every packet emitted should eventually be + * acknowledged. The list of AckFrame is built from the list of Packets, + * by randomly selecting a few consecutive packets in the packet list + * to acknowledge. The list of AckFrame is sorted by increasing + * largestAcknowledged. The list of Packets can be shuffled, which + * result on having AckFrames with gaps. + * @param acks A list of acknowledgement ranges + * @param packets A list of packets generated from the acknowledgement ranges. + * The list can be shuffled. + * @param ackframes A list of AckFrames, derived from the possibly shuffled + * list of packets. The list of AckFrame is sorted by increasing + * largestAcknowledged (since a packet can't be acknowledged + * before it's been emitted). + * @param shuffled whether the list of packets is shuffled. + */ + public static record TestCase(List acks, + List packets, + List ackframes, + boolean shuffled) { + public TestCase(List acks, List packets) { + this(acks, packets, ackFrames(packets),false); + } + public TestCase(List acks, List packets, boolean shuffled) { + this(acks, packets, ackFrames(packets), shuffled); + } + public TestCase(List acks) { + this(acks, Packet.ofAcks(acks)); + } + public TestCase shuffle() { + List shuffled = new ArrayList<>(packets); + Collections.shuffle(shuffled, RANDOM); + return new TestCase(acks, List.copyOf(shuffled), true); + } + } + + /** + * Construct a list of AckFrames from the possibly shuffled list + * of Packets. + * @param packets a list of packets + * @return a sorted list of AckFrames + */ + private static List ackFrames(List packets) { + List result = new ArrayList<>(); + int remaining = packets.size(); + int i = 0; + while (remaining > 0) { + int ackCount = Math.min(RANDOM.nextInt(1, 5), remaining); + AckFrameBuilder builder = new AckFrameBuilder(); + for (int j=0; j < ackCount; j++) { + builder.addAck(packets.get(i + j).packetNumber); + } + result.add(builder.build()); + i += ackCount; + remaining -= ackCount; + } + result.sort(Comparator.comparingLong(AckFrame::largestAcknowledged)); + return List.copyOf(result); + } + + /** + * Generates test cases - by concatenating a list of simple test case, + * a list of special testcases, and a list of random testcases. + * @return A list of TestCases to test. + */ + List generateTests() { + List tests = new ArrayList<>(); + List simples = List.of( + new TestCase(List.of(new Acknowledged(5,5))), + new TestCase(List.of(new Acknowledged(5,7))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,9))), + new TestCase(List.of(new Acknowledged(3, 5), new Acknowledged(7,7))), + new TestCase(List.of(new Acknowledged(3,3), new Acknowledged(5,7))) + ); + tests.addAll(simples); + List specials = List.of( + new TestCase(Acknowledged.of(5,5,7,7), Packet.of(5,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(5,7,6), true), + new TestCase(Acknowledged.of(6,7), Packet.of(6,7), false), + new TestCase(Acknowledged.of(5,7), Packet.of(6,7,5), true), + new TestCase(Acknowledged.of(5,7), Packet.of(5,6,7), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(5, 7, 8), true), + new TestCase(Acknowledged.of(5,5,8,8), Packet.of(8, 5), true), + new TestCase(Acknowledged.of(5,5,7,8), Packet.of(8, 5, 7), true), + new TestCase(Acknowledged.of(3,5,7,9), Packet.of(8,5,7,4,9,3), true), + new TestCase(Acknowledged.of(27,27,31,31), + Packet.of(27, 31), true), + new TestCase(Acknowledged.of(27,27,29,29,31,31), + Packet.of(27, 31, 29), true), + new TestCase(Acknowledged.of(3,5,7,7,9,9,22,22,27,27,29,29,31,31), + Packet.of(4,22,27,31,9,29,7,5,3), true) + ); + tests.addAll(specials); + for (int i=0; i < 5; i++) { + List acks = generateAcks(); + List packets = packets(acks); + TestCase test = new TestCase(acks, List.copyOf(packets), false); + tests.add(test); + for (int j = 0; j < 5; j++) { + tests.add(test.shuffle()); + } + } + return tests; + } + + /** + * Generate a random list of increasing acknowledgement ranges. + * A packet should only be present once. + * @return a random list of increasing acknowledgement ranges. + */ + List generateAcks() { + int count = RANDOM.nextInt(3, 10); + List acks = new ArrayList<>(count); + long prev = -1; + for (int i=0; i packets(List acks) { + List res = new ArrayList<>(); + for (Acknowledged ack : acks) { + for (long i = ack.first() ; i<= ack.last() ; i++) { + var packet = new Packet(i); + assert !res.contains(packet); + res.add(packet); + } + } + return res; + } + + @DataProvider(name = "tests") + public Object[][] tests() { + return generateTests().stream() + .map(List::of) + .map(List::toArray) + .toArray(Object[][]::new); + } + + // TODO: + // 1. TestCase should have an ordered list of packets. + // 2. packets will be emitted in order - that is - + // PacketSpaceManager::packetSent will be called for each packet + // in order. + // 3. acknowledgements of packet should arrive in random order. + // a selection of packets should be acknowledged in bunch... + // However, a packet shouldn't be acknowledged before it is emitted. + // 4. some packets should not be acknowledged in time, causing + // them to be retransmitted. + // 5. all of retransmitted packets should eventually be acknowledged, + // but which packet number (among the list of numbers under which + // a packet is retransmitted should be random). + // 6. packets that are acknowledged should no longer be retransmitted + // 7. code an AsynchronousTestDriver that uses the EXECUTOR + // this is more difficult as it makes it more difficult to guess + // at when exactly a packet will be retransmitted... + + /** + * A synchronous test driver to drive a TestCase. + * The method {@link #run()} drives the test. + */ + static class SynchronousTestDriver implements PacketEmitter { + final TestCase test; + final long timeline; + final QuicTimerQueue timerQueue; + final PriorityBlockingQueue packetQueue; + final PriorityBlockingQueue framesQueue; + final PacketSpaceManager manager; + final PacketNumberSpace space; + final Executor executor = this::execute; + final Logger debug = TestLoggerUtil.getErrOutLogger(this::toString); + final TestCodingContext codingContext; + final ConcurrentLinkedQueue emittedAckPackets; + final QuicRttEstimator rttEstimator; + final QuicCongestionController congestionController; + final QuicConnectionId localId; + final QuicConnectionId peerId; + final TimeSource timeSource = new TimeSource(); + final long maxPacketNumber; + final AckFrameBuilder allAcks = new AckFrameBuilder(); + + SynchronousTestDriver(TestCase test) { + this.space = PacketNumberSpace.INITIAL; + this.test = test; + + localId = newId(); + peerId = newId(); + timeline = test.packets().stream() + .mapToLong(Packet::delay) + .reduce(0, Math::addExact); + timerQueue = new QuicTimerQueue(this::notifyQueue, debug); + packetQueue = new PriorityBlockingQueue<>(test.packets.size(), Packet.COMPARE_NUMBERS); + packetQueue.addAll(test.packets); + framesQueue = new PriorityBlockingQueue<>(test.ackframes.size(), + Comparator.comparingLong(AckFrame::largestAcknowledged)); + framesQueue.addAll(test.ackframes); + emittedAckPackets = new ConcurrentLinkedQueue<>(); + rttEstimator = new QuicRttEstimator() { + @Override + public synchronized Duration getLossThreshold() { + return Duration.ofMillis(250); + } + + @Override + public synchronized Duration getBasePtoDuration() { + return Duration.ofMillis(250); + } + }; + congestionController = new QuicCongestionController() { + @Override + public boolean canSendPacket() { + return true; + } + @Override + public void updateMaxDatagramSize(int newSize) { } + @Override + public void packetSent(int packetBytes) { } + @Override + public void packetAcked(int packetBytes, Deadline sentTime) { } + @Override + public void packetLost(Collection lostPackets, Deadline sentTime, boolean persistent) { } + @Override + public void packetDiscarded(Collection discardedPackets) { } + }; + manager = new PacketSpaceManager(space, this, timeSource, + rttEstimator, congestionController, new DummyQuicTLSEngine(), + this::toString); + maxPacketNumber = test.packets().stream().mapToLong(Packet::packetNumber) + .max().getAsLong(); + manager.getNextPN().set(maxPacketNumber + 1); + codingContext = new TestCodingContext(new PacketSpaces(manager, null, null)); + } + + static class TimeSource implements TimeLine { + final Deadline first = jdk.internal.net.http.common.TimeSource.now(); + volatile Deadline current = first; + public synchronized Deadline advance(long duration, TemporalUnit unit) { + return current = current.plus(duration, unit); + } + public Deadline advanceMillis(long millis) { + return advance(millis, ChronoUnit.MILLIS); + } + @Override + public Deadline instant() { + return current; + } + } + + void notifyQueue() { + timerQueue.processEventsAndReturnNextDeadline(now(), executor); + } + + @Override + public QuicTimerQueue timer() { return timerQueue;} + + @Override + public void retransmit(PacketSpace packetSpaceManager, QuicPacket packet, int attempts) { + if (!(packet instanceof InitialPacket initial)) + throw new AssertionError("unexpected packet type: " + packet); + long newPacketNumber = packetSpaceManager.allocateNextPN(); + debug.log("Retransmitting packet %d as %d (%d attempts)", + packet.packetNumber(), newPacketNumber, attempts); + assert attempts >= 0; + QuicPacket newPacket = codingContext.encoder + .newInitialPacket(initial.sourceId(), initial.destinationId(), + initial.token(), newPacketNumber, + packetSpaceManager.getLargestPeerAckedPN(), + initial.frames(), codingContext); + long number = initial.packetNumber(); + Deadline now = now(); + manager.packetSent(newPacket, number, newPacket.packetNumber()); + retransmissions.add(new Retransmission(initial.packetNumber(), now, + AckFrame.largestAcknowledgedInPacket(newPacket))); + expectedRetransmissions.stream() + .filter(r -> r.isFor(number)) + .forEach(r -> { + assertTrue(r.isDue(now) || + packetSpaceManager.getLargestPeerAckedPN() - 3 > number, + "retransmitted packet %d is not yet due".formatted(number)); + successfulExpectations.add(r); + }); + boolean removed = expectedRetransmissions.removeIf(r -> r.isFor(number)); + if (number <= maxPacketNumber) { + assertTrue(removed, "retransmission of packet %d was not expected" + .formatted(number)); + } + } + + @Override + public long emitAckPacket(PacketSpace packetSpaceManager, + AckFrame ackFrame, boolean sendPing) { + long newPacketNumber = packetSpaceManager.allocateNextPN(); + debug.log("Emitting ack packet %d for %s (sendPing: %s)", + newPacketNumber, ackFrame, sendPing); + List frames; + if (ackFrame != null) { + frames = sendPing + ? List.of(new PingFrame(), ackFrame) + : List.of(ackFrame); + } else { + assert sendPing; + frames = List.of(new PingFrame()); + } + QuicPacket newPacket = codingContext.encoder + .newInitialPacket(localId, peerId, + null, newPacketNumber, + packetSpaceManager.getLargestPeerAckedPN(), + frames, codingContext); + packetSpaceManager.packetSent(newPacket, -1, newPacketNumber); + emittedAckPackets.offer(newPacket); + return newPacket.packetNumber(); + } + + @Override + public void acknowledged(QuicPacket packet) { + // TODO: nothing to do? + } + + @Override + public boolean sendData(PacketNumberSpace packetNumberSpace) { + return false; + } + + @Override + public Executor executor() { + return this::execute; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void checkAbort(PacketNumberSpace packetNumberSpace) { } + + final CopyOnWriteArrayList expectedRetransmissions = new CopyOnWriteArrayList<>(); + final CopyOnWriteArrayList retransmissions = new CopyOnWriteArrayList<>(); + final CopyOnWriteArrayList successfulExpectations = new CopyOnWriteArrayList<>(); + + static record Retransmission(long packetNumber, Deadline atOrAfter, long largestAckSent) { + boolean isFor(long number) { + return number == packetNumber; + } + boolean isDue(Deadline now) { + return !atOrAfter.isAfter(now); + } + } + + /** + * Drives the test by pretending to emit each packet in order, + * then pretending to receive ack frames (as soon as possible + * given the largest packet number emitted). + * The timeline is advanced by chunks as instructed by + * the test. + * This method checks that the retransmission logic works as + * expected. + * @throws Exception + */ + // TODO: in the end we need to check that everything that was + // expected to happen happened. What is missing is to + // check the generation of ACK packets... Also a + // retransmitted packet may need to itself retransmitted + // again and we have no test for that. + public void run() throws Exception { + long timeline = 0; + long serverPacketNumbers = 0; + long maxAck; + Packet packet; + debug.log("Packets: %s", test.packets.stream().mapToLong(Packet::packetNumber) + .mapToObj(String::valueOf) + .collect(Collectors.joining(", ", "[", "]"))); + debug.log("Frames: %s", test.ackframes.stream().mapToLong(AckFrame::largestAcknowledged) + .mapToObj(String::valueOf) + .collect(Collectors.joining(", ", "[", "]"))); + long maxRetransmissionDelay = 250; + long maxAckDelay = manager.getMaxAckDelay(); + + Deadline start = now(); + Deadline nextSendAckDeadline = Deadline.MAX; + long firstAckPaket = -1, lastAckPacket = -1; + long largestAckAcked = -1; + boolean previousAckEliciting = false; + // simulate sending each packet, ordered by their packet number + while ((packet = packetQueue.poll()) != null) { + long offset = packet.packetNumber; + AckFrame nextAck = framesQueue.peek(); + maxAck = nextAck == null ? Long.MAX_VALUE : nextAck.largestAcknowledged(); + long largestReceivedAckedPN = manager.getLargestPeerAckedPN(); + debug.log("timeline: at %dms", timeline); + debug.log("sending packet: %d, largest ACK received: %d", + packet.packetNumber, largestReceivedAckedPN); + + // randomly decide whether we should attempt to include an ack frame + // with the next packet we send out... + boolean sendAck = RANDOM.nextBoolean(); + AckFrame ackFrameToSend = sendAck ? manager.getNextAckFrame(false) : null; + long largestAckSent = -1; + if (ackFrameToSend != null) { + previousAckEliciting = false; + debug.log("including ACK frame: " + ackFrameToSend); + nextSendAckDeadline = Deadline.MAX; + // assertFalse used on purpose here to make + // sure the stack trace can't be confused with one that + // originate in another similar lambda that use assertTrue below. + LongStream.range(firstAckPaket, lastAckPacket + 1).sequential() + .forEach(p -> assertFalse(!ackFrameToSend.isAcknowledging(p), + "frame %s should acknowledge %d" + .formatted(ackFrameToSend, p))); + largestAckSent = ackFrameToSend.largestAcknowledged(); + debug.log("largestAckSent is: " + largestAckAcked); + } + + // add a crypto frame and build the packet + CryptoFrame crypto = new CryptoFrame(offset, 1, + ByteBuffer.wrap(new byte[] {nextByte(offset)})); + List frames = ackFrameToSend == null ? + List.of(crypto) : List.of(crypto, ackFrameToSend); + QuicPacket newPacket = codingContext.encoder + .newInitialPacket(localId, peerId, + null, + packet.packetNumber, + largestReceivedAckedPN, + frames, codingContext); + // pretend that we sent a packet + manager.packetSent(newPacket, -1, packet.packetNumber); + + // compute next deadline + var nextDeadline = timerQueue.nextDeadline(); + var nextScheduledDeadline = manager.nextScheduledDeadline(); + var nextComputedDeadline = manager.computeNextDeadline(); + var now = now(); + debugDeadline("nextDeadline", start, now, nextDeadline); + debugDeadline("nextScheduledDeadline", start, now, nextScheduledDeadline); + debugDeadline("nextComputedDeadline", start, now, nextComputedDeadline); + assertFalse(nextDeadline.isAfter(now.plusMillis(maxRetransmissionDelay * rttEstimator.getPtoBackoff())), + "nextDeadline should not be after %dms from now!" + .formatted(maxRetransmissionDelay)); + expectedRetransmissions.add(new Retransmission(packet.packetNumber, + now.plus(maxRetransmissionDelay, ChronoUnit.MILLIS), largestAckSent)); + + List pending = manager.pendingAcknowledgements((s) ->s.boxed().toList()); + debug.log("pending ack: %s", pending); + assertContains(Assertion.TRUE, pending, packet.packetNumber, + "pending ack"); + + pending = manager.pendingRetransmission((s) ->s.boxed().toList()); + debug.log("pending retransmission: %s", pending); + assertContains(Assertion.FALSE, pending, packet.packetNumber, + "pending retransmission"); + + pending = manager.triggeredForRetransmission((s) ->s.boxed().toList()); + debug.log("triggered for retransmission: %s", pending); + assertContains(Assertion.FALSE, pending, packet.packetNumber, + "triggered for retransmission"); + + if (!nextDeadline.isAfter(now) || !nextSendAckDeadline.isAfter(now)) { + var nextI = timerQueue + .processEventsAndReturnNextDeadline(now, executor); + // this might have triggered sending an ACK packet, unless we + // already sent the ack with the initial packet just above. + debugDeadline("new deadline after events", start, now, nextI); + } + + // check generated ack packets, if any should have been generated... + if (!nextSendAckDeadline.isAfter(now)) { + debug.log("checking emitted ack packets: emitted %d", emittedAckPackets.size()); + var ackPacket = emittedAckPackets.poll(); + assertNotNull(ackPacket); + List ackFrames = ackPacket.frames() + .stream().filter(AckFrame.class::isInstance) + .map(AckFrame.class::cast) + .toList(); + assertEquals(frames.size(), 1, + "unexpected ack frames: " + frames); + AckFrame ackFrame = ackFrames.get(0); + LongStream.range(firstAckPaket, lastAckPacket + 1) + .forEach(p -> assertTrue(ackFrame.isAcknowledging(p), + "frame %s should acknowledge %d" + .formatted(ackFrame, p))); + assertNull(emittedAckPackets.peek(), + "emitted ackPacket queue not empty: " + emittedAckPackets); + debug.log("Got expected ackFrame for emitted ack packet: %s", ackFrame); + previousAckEliciting = false; + nextSendAckDeadline = Deadline.MAX; + } + + // advance the timeline by the instructed delay... + Deadline next = timeSource.advanceMillis(packet.delay); + timeline = timeline + packet.delay; + debug.log("advance deadline by %dms at %dms", packet.delay, timeline); + // note: beyond this point now > now(); packets between now and now() + // may not be retransmitted yet + + // Do not pretend to receive acknowledgement for + // packets that we haven't sent yet. + if (packet.packetNumber >= maxAck) { + // pretend to be receiving the next ack frame... + nextAck = framesQueue.poll(); + debug.log("Receiving acks for " + nextAck); + long spn = serverPacketNumbers++; + boolean isAckEliciting = RANDOM.nextBoolean(); + manager.packetReceived(PacketType.INITIAL, spn, isAckEliciting); + + // calculate if and when we should send out the ack frame for + // the ACK packet we just received + if (firstAckPaket == -1) firstAckPaket = spn; + lastAckPacket = spn; + debug.log("next sent ack should acknowledge [%d..%d]", + firstAckPaket, lastAckPacket); + if (isAckEliciting) { + debug.log("prevEliciting: %s", + previousAckEliciting); + if (previousAckEliciting) { + nextSendAckDeadline = min(now, nextSendAckDeadline); + } else { + nextSendAckDeadline = min(nextSendAckDeadline, next.plusMillis(maxAckDelay)); + } + debugDeadline("next ack deadline", start, next, nextSendAckDeadline); + } + previousAckEliciting |= isAckEliciting; + + // process the ack frame we just received + assertNotNull(nextAck); + manager.processAckFrame(nextAck); + firstAckPaket = Math.max(firstAckPaket, manager.getMinPNThreshold() + 1); + + // Here we can compute which packets will not be acknowledged yet, + // and which packets will be retransmitted. + long largestAckAckedBefore = largestAckAcked; + for (long number : acknowledgePackets(nextAck)) { + allAcks.addAck(number); + pending = manager.pendingAcknowledgements((s) -> s.boxed().toList()); + assertContains(Assertion.FALSE, pending, number, + "pending ack"); + + pending = manager.pendingRetransmission((s) -> s.boxed().toList()); + assertContains(Assertion.FALSE, pending, number, + "pending retransmission"); + + pending = manager.triggeredForRetransmission((s) -> s.boxed().toList()); + assertContains(Assertion.FALSE, pending, number, + "triggered for retransmission"); + // TODO check if we only retransmitted the expected packets + // ...need to replicate the logic + + /*expectedRetransmissions.stream() + .filter(r -> r.isFor(number)) + .forEach(r -> assertFalse(r.isDue(now), + "due packet %d was not retransmitted".formatted(number))); + for (Retransmission r : expectedRetransmissions) { + if (r.isFor(number)) { + largestAckAcked = Math.max(largestAckAcked, r.largestAckSent); + } + }*/ + for (Retransmission r : successfulExpectations) { + if (r.isFor(number)) { + largestAckAcked = Math.max(largestAckAcked, r.largestAckSent); + } + } + if (largestAckAcked != largestAckAckedBefore) { + debug.log("largestAckAcked is now %d", largestAckAcked); + largestAckAckedBefore = largestAckAcked; + } + if (largestAckAcked > -1) { + boolean changed = false; + if (firstAckPaket <= largestAckAcked) { + changed = true; + if (lastAckPacket > largestAckAcked) { + firstAckPaket = largestAckAcked + 1; + } else firstAckPaket = -1; + } + if (lastAckPacket <= largestAckAcked) { + changed = true; + lastAckPacket = -1; + } + if (changed) { + debug.log("next sent ack should now be [%d..%d]", + firstAckPaket, lastAckPacket); + } + } + boolean removed = expectedRetransmissions.removeIf(r -> r.isFor(number)); + successfulExpectations.stream() + .filter(r -> r.isFor(number)) + .forEach( r -> assertTrue(r.isDue(now()) || + manager.getLargestPeerAckedPN() - 3 > r.packetNumber, + "packet %d was retransmitted too early (deadline was %d)" + .formatted(number, start + .until(r.atOrAfter, ChronoUnit.MILLIS)))); + retransmissions.stream().filter(r -> r.isFor(number)).forEach( r -> { + assertTrue(r.isDue(now()), + "packet %d was retransmitted too early at %d" + .formatted(number, start + .until(r.atOrAfter, ChronoUnit.MILLIS))); + assertFalse(removed, "packet %d was in both lists" + .formatted(number)); + }); + } + } + } + } + + Deadline min(Deadline one, Deadline two) { + return one.isAfter(two) ? two : one; + } + + /** + * Should be called after {@link #run()}. + */ + void check() { + assertFalse(now().isBefore(timeSource.first.plusMillis(timeline))); + assertTrue(expectedRetransmissions.isEmpty()); + assertEquals(retransmissions.stream() + .map(Retransmission::packetNumber) + .filter(pn -> pn <= maxPacketNumber).toList(), + successfulExpectations.stream().map(Retransmission::packetNumber) + .filter(pn -> pn <= maxPacketNumber).toList()); + for (Retransmission r : retransmissions) { + if (r.packetNumber > maxPacketNumber || + manager.getLargestPeerAckedPN() - 3 > r.packetNumber) continue; + List succesful = successfulExpectations.stream() + .filter(s -> s.isFor(r.packetNumber)) + .toList(); + assertEquals(succesful.size(), 1); + succesful.forEach(s -> assertFalse(s.atOrAfter.isAfter(r.atOrAfter))); + } + + List acknowledged = new ArrayList<>(acknowledgePackets(allAcks.build())); + Collections.sort(acknowledged); + assertEquals(acknowledged, test.packets.stream() + .map(Packet::packetNumber).sorted().toList()); + } + + // TODO: add a LongStream acknowledged() to AckFrame - write a spliterator + // for that. + List acknowledgePackets(AckFrame frame) { + List list = new ArrayList<>(); + long largest = frame.largestAcknowledged(); + long smallest = largest + 2; + for (AckRange range : frame.ackRanges()) { + largest = smallest - range.gap() -2; + smallest = largest - range.range(); + for (long i = largest; i >= smallest; i--) { + assert frame.isAcknowledging(i) + : "%s is not acknowledging %d".formatted(frame, i); + list.add(i); + } + } + return list; + } + + interface Assertion { + void check(boolean result, String message); + default String negation() { + return (this == FALSE) ? "doesn't " : ""; + } + Assertion TRUE = Assert::assertTrue; + Assertion FALSE = Assert::assertFalse; + } + static void assertContains(Assertion assertion, List list, long number, String desc) { + assertion.check(list.contains(number), + "%s: %s %scontains %d".formatted(desc, list, assertion.negation(), number)); + } + + void debugDeadline(String desc, Deadline start, Deadline now, Deadline deadline) { + long nextMs = deadline.equals(Deadline.MAX) ? 0 : + now.until(deadline, ChronoUnit.MILLIS); + long at = deadline.equals(Deadline.MAX) ? 0 : + start.until(deadline, ChronoUnit.MILLIS); + String when = deadline.equals(Deadline.MAX) ? "never" + : nextMs >= 0 ? ("at %d in %dms".formatted(at, nextMs)) + : ("at %d due by %dms".formatted(at, (-nextMs))); + debug.log("%s: %s", desc, when); + } + + byte nextByte(long offset) { + long start = 'a'; + int len = 'z' - 'a' + 1; + long res = start + offset % len; + assert res >= 'a' && res <= 'z'; + return (byte) res; + } + + private void execute(Runnable runnable) { + runnable.run(); + } + + Deadline now() { + return timeSource.instant(); + } + + static QuicConnectionId newId() { + byte[] idbites = new byte[CIDLEN]; + RANDOM.nextBytes(idbites); + return new PeerConnectionId(idbites); + } + } + + @Test(dataProvider = "tests") + public void testPacketSpaceManager(TestCase testCase) throws Exception { + System.out.printf("%n ------- testPacketSpaceManager ------- %n"); + SynchronousTestDriver driver = new SynchronousTestDriver(testCase); + driver.run(); + driver.check(); + } + + } diff --git a/test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java b/test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java new file mode 100644 index 00000000000..f0d4b316e46 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.quic.frames.QuicFrame; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.HexFormat; + +import static org.testng.Assert.*; + + +/* + * @test + * @library /test/lib + * @summary Tests to check QUIC frame decoding errors are handled correctly + * @run testng/othervm QuicFramesDecoderTest + */ +public class QuicFramesDecoderTest { + + + // correct frames. Single byte frames (padding, ping, handshake_done) omitted. + // ACK without ECN, acked = 0, 2; delay = 2 + private static final byte[] ACK_BASE = HexFormat.of().parseHex("02020201000000"); + private static final byte[] ACK_BASE_B = HexFormat.of().parseHex("0202020100004000"); + + // ACK with ECN, acked = 0, 2; delay = 2, ECN=(3,4,5) + private static final byte[] ACK_ECN = HexFormat.of().parseHex("03020201000000030405"); + private static final byte[] ACK_ECN_B = HexFormat.of().parseHex("0302020100000003044005"); + + // RESET_STREAM, stream 3, error 2, final size 1 + private static final byte[] RESET_STREAM = HexFormat.of().parseHex("04030201"); + private static final byte[] RESET_STREAM_B = HexFormat.of().parseHex("0403024001"); + + // STOP_SENDING, stream 4, error 3 + private static final byte[] STOP_SENDING = HexFormat.of().parseHex("050403"); + private static final byte[] STOP_SENDING_B = HexFormat.of().parseHex("05044003"); + + // CRYPTO, offset 5, length 4, data + private static final byte[] CRYPTO = HexFormat.of().parseHex("06050403020100"); + + // NEW_TOKEN, length 6, data + private static final byte[] NEW_TOKEN = HexFormat.of().parseHex("0706050403020100"); + + // STREAM-o-l-f, stream 7, no data + private static final byte[] STREAM = HexFormat.of().parseHex("0807"); + + // STREAM-o-l+f, stream 8, no data + private static final byte[] STREAM_F = HexFormat.of().parseHex("0908"); + + // STREAM-o+l-f, stream 9, length 8 + private static final byte[] STREAM_L = HexFormat.of().parseHex("0a09080706050403020100"); + + // STREAM-o+l+f, stream 10, length 9 + private static final byte[] STREAM_LF = HexFormat.of().parseHex("0b0a09080706050403020100"); + + // STREAM+o-l-f, stream 11, offset 0, no data + private static final byte[] STREAM_O = HexFormat.of().parseHex("0c000a"); + + // STREAM+o-l+f, stream 12, offset 0, no data + private static final byte[] STREAM_OF = HexFormat.of().parseHex("0d000b"); + + // STREAM+o+l-f, stream 13, offset 0, length 3 + private static final byte[] STREAM_OL = HexFormat.of().parseHex("0e000c03020100"); + + // STREAM+o+l+f, stream 14, offset 0, length 3 + private static final byte[] STREAM_OLF = HexFormat.of().parseHex("0f000d03020100"); + + // MAX_DATA, max=15 + private static final byte[] MAX_DATA = HexFormat.of().parseHex("100f"); + private static final byte[] MAX_DATA_B = HexFormat.of().parseHex("10400f"); + + // MAX_STREAM_DATA, stream = 16 max=15 + private static final byte[] MAX_STREAM_DATA = HexFormat.of().parseHex("11100f"); + private static final byte[] MAX_STREAM_DATA_B = HexFormat.of().parseHex("1110400f"); + + // MAX_STREAMS, bidi, streams = 2^60 + private static final byte[] MAX_STREAMS_B = HexFormat.of().parseHex("12d000000000000000"); + + // MAX_STREAMS, uni, streams = 2^60 + private static final byte[] MAX_STREAMS_U = HexFormat.of().parseHex("13d000000000000000"); + + // DATA_BLOCKED, max=19 + private static final byte[] DATA_BLOCKED = HexFormat.of().parseHex("1413"); + private static final byte[] DATA_BLOCKED_B = HexFormat.of().parseHex("144013"); + + // STREAM_DATA_BLOCKED, stream = 20 max=19 + private static final byte[] STREAM_DATA_BLOCKED = HexFormat.of().parseHex("151413"); + private static final byte[] STREAM_DATA_BLOCKED_B = HexFormat.of().parseHex("15144013"); + + // STREAMS_BLOCKED, bidi, streams = 2^60 + private static final byte[] STREAMS_BLOCKED_B = HexFormat.of().parseHex("16d000000000000000"); + + // STREAMS_BLOCKED, uni, streams = 2^60 + private static final byte[] STREAMS_BLOCKED_U = HexFormat.of().parseHex("17d000000000000000"); + + // NEW_CONNECTION_ID, seq=23, retire=22, len = 5 + private static final byte[] NEW_CONNECTION_ID = HexFormat.of().parseHex("181716"+ + "051413121110"+"0f0e0d0c0b0a09080706050403020100"); + + // RETIRE_CONNECTION_ID, seq=24 + private static final byte[] RETIRE_CONNECTION_ID = HexFormat.of().parseHex("1918"); + private static final byte[] RETIRE_CONNECTION_ID_B = HexFormat.of().parseHex("194018"); + + // PATH_CHALLENGE + private static final byte[] PATH_CHALLENGE = HexFormat.of().parseHex("1a0706050403020100"); + + // PATH_RESPONSE + private static final byte[] PATH_RESPONSE = HexFormat.of().parseHex("1b0706050403020100"); + + // CONNECTION_CLOSE, quic, error 27, frame type 26, reason='\0' + private static final byte[] CONNECTION_CLOSE_Q = HexFormat.of().parseHex("1c1b1a0100"); + // CONNECTION_CLOSE, quic, error 27, frame type 26, reason= + // efbfbf (U+FFFF) - "not a valid unicode character + // edb080 (U+DC00) - low surrogate, prohibited in UTF8 (RFC3629), must be preceded by high surrogate otherwise + // eda080 (U+D800) - high surrogate, prohibited in UTF8 (RFC3629), must be followed by low surrogate otherwise + // 80 - not a first byte of UTF8 sequence + // c0d0e0f0ff - not a valid UTF8 sequence + private static final byte[] CONNECTION_CLOSE_Q_BAD_REASON = HexFormat.of().parseHex("1c1b1a0fefbfbfedb080eda08080c0d0e0f0ff"); + + // CONNECTION_CLOSE, app, error 28, reason='\0' + private static final byte[] CONNECTION_CLOSE_A = HexFormat.of().parseHex("1d1c0100"); + // CONNECTION_CLOSE, app, error 28, reason= same as CONNECTION_CLOSE_Q_BAD_REASON + private static final byte[] CONNECTION_CLOSE_A_BAD_REASON = HexFormat.of().parseHex("1d1c0fefbfbfedb080eda08080c0d0e0f0ff"); + // end of correct frames + + // malformed frames other than truncated + // ACK acknowledging negative packet + // ACK without ECN, acked = -1, 1; delay = 2 + private static final byte[] ACK_NEG_BASE = HexFormat.of().parseHex("02010201000000"); + + // ACK with ECN, acked = -1, 1; delay = 2, ECN=(3,4,5) + private static final byte[] ACK_NEG_ECN = HexFormat.of().parseHex("03010201000000030405"); + + // ACK without ECN, acked = -1, 0; delay = 2 + private static final byte[] ACK_NEG_BASE_2 = HexFormat.of().parseHex("0200020001"); + + // CRYPTO out of range: offset MAX_VL_INT, len=1 + private static final byte[] CRYPTO_OOR = HexFormat.of().parseHex("06ffffffffffffffff0100"); + + // NEW_TOKEN empty + private static final byte[] NEW_TOKEN_EMPTY = HexFormat.of().parseHex("0700"); + + // MAX_STREAMS out of range + // MAX_STREAMS, bidi, streams = 2^60+1 + private static final byte[] MAX_STREAMS_B_OOR = HexFormat.of().parseHex("12d000000000000001"); + // MAX_STREAMS, uni, streams = 2^60+1 + private static final byte[] MAX_STREAMS_U_OOR = HexFormat.of().parseHex("13d000000000000001"); + + // STREAMS_BLOCKED out of range + // STREAMS_BLOCKED, bidi, streams = 2^60+1 + private static final byte[] STREAMS_BLOCKED_B_OOR = HexFormat.of().parseHex("16d000000000000001"); + + // STREAMS_BLOCKED, uni, streams = 2^60+1 + private static final byte[] STREAMS_BLOCKED_U_OOR = HexFormat.of().parseHex("17d000000000000001"); + + // NEW_CONNECTION_ID, seq=23, retire=22, len = 0 + private static final byte[] NEW_CONNECTION_ID_ZERO = HexFormat.of().parseHex("181716"+ + "00"+"0f0e0d0c0b0a09080706050403020100"); + + @DataProvider + public static Object[][] goodFrames() { + return new Object[][]{ + new Object[]{"ack without ecn", ACK_BASE, false}, + new Object[]{"ack without ecn", ACK_BASE_B, true}, + new Object[]{"ack with ecn", ACK_ECN, false}, + new Object[]{"ack with ecn", ACK_ECN_B, true}, + new Object[]{"RESET_STREAM", RESET_STREAM, false}, + new Object[]{"RESET_STREAM", RESET_STREAM_B, true}, + new Object[]{"STOP_SENDING", STOP_SENDING, false}, + new Object[]{"STOP_SENDING", STOP_SENDING_B, true}, + new Object[]{"CRYPTO", CRYPTO, false}, + new Object[]{"NEW_TOKEN", NEW_TOKEN, false}, + new Object[]{"STREAM-o-l-f", STREAM, false}, + new Object[]{"STREAM-o-l+f", STREAM_F, false}, + new Object[]{"STREAM-o+l-f", STREAM_L, false}, + new Object[]{"STREAM-o+l+f", STREAM_LF, false}, + new Object[]{"STREAM+o-l-f", STREAM_O, false}, + new Object[]{"STREAM+o-l+f", STREAM_OF, false}, + new Object[]{"STREAM+o+l-f", STREAM_OL, false}, + new Object[]{"STREAM+o+l+f", STREAM_OLF, false}, + new Object[]{"MAX_DATA", MAX_DATA, false}, + new Object[]{"MAX_DATA", MAX_DATA_B, true}, + new Object[]{"MAX_STREAM_DATA", MAX_STREAM_DATA, false}, + new Object[]{"MAX_STREAM_DATA", MAX_STREAM_DATA_B, true}, + new Object[]{"MAX_STREAMS bidi", MAX_STREAMS_B, false}, + new Object[]{"MAX_STREAMS uni", MAX_STREAMS_U, false}, + new Object[]{"DATA_BLOCKED", DATA_BLOCKED, false}, + new Object[]{"DATA_BLOCKED", DATA_BLOCKED_B, true}, + new Object[]{"STREAM_DATA_BLOCKED", STREAM_DATA_BLOCKED, false}, + new Object[]{"STREAM_DATA_BLOCKED", STREAM_DATA_BLOCKED_B, true}, + new Object[]{"STREAMS_BLOCKED bidi", STREAMS_BLOCKED_B, false}, + new Object[]{"STREAMS_BLOCKED uni", STREAMS_BLOCKED_U, false}, + new Object[]{"NEW_CONNECTION_ID", NEW_CONNECTION_ID, false}, + new Object[]{"RETIRE_CONNECTION_ID", RETIRE_CONNECTION_ID, false}, + new Object[]{"RETIRE_CONNECTION_ID", RETIRE_CONNECTION_ID_B, true}, + new Object[]{"PATH_CHALLENGE", PATH_CHALLENGE, false}, + new Object[]{"PATH_RESPONSE", PATH_RESPONSE, false}, + new Object[]{"CONNECTION_CLOSE QUIC", CONNECTION_CLOSE_Q, false}, + new Object[]{"CONNECTION_CLOSE QUIC non-utf8 reason", CONNECTION_CLOSE_Q_BAD_REASON, false}, + new Object[]{"CONNECTION_CLOSE app", CONNECTION_CLOSE_A, false}, + new Object[]{"CONNECTION_CLOSE app non-utf8 reason", CONNECTION_CLOSE_A_BAD_REASON, false}, + }; + } + + @DataProvider + public static Object[][] badFrames() { + return new Object[][]{ + new Object[]{"ack without ecn, negative pn", ACK_NEG_BASE}, + new Object[]{"ack without ecn, negative pn, v2", ACK_NEG_BASE_2}, + new Object[]{"ack with ecn, negative pn", ACK_NEG_ECN}, + new Object[]{"CRYPTO out of range", CRYPTO_OOR}, + new Object[]{"NEW_TOKEN empty", NEW_TOKEN_EMPTY}, + new Object[]{"MAX_STREAMS bidi out of range", MAX_STREAMS_B_OOR}, + new Object[]{"MAX_STREAMS uni out of range", MAX_STREAMS_U_OOR}, + new Object[]{"STREAMS_BLOCKED bidi out of range", STREAMS_BLOCKED_B_OOR}, + new Object[]{"STREAMS_BLOCKED uni out of range", STREAMS_BLOCKED_U_OOR}, + new Object[]{"NEW_CONNECTION_ID zero length", NEW_CONNECTION_ID_ZERO}, + }; + } + + @Test(dataProvider = "goodFrames") + public void testReencode(String desc, byte[] frame, boolean bloated) throws Exception { + // check if the goodFrames provider indeed contains good frames + ByteBuffer buf = ByteBuffer.wrap(frame); + var qf = QuicFrame.decode(buf); + assertFalse(buf.hasRemaining(), buf.remaining() + " bytes left in buffer after parsing"); + // some frames deliberately use suboptimal encoding, skip them + if (bloated) return; + assertEquals(qf.size(), frame.length, "Frame size mismatch"); + buf.clear(); + ByteBuffer encoded = ByteBuffer.allocate(frame.length); + qf.encode(encoded); + assertFalse(encoded.hasRemaining(), "Actual frame length mismatch"); + encoded.flip(); + assertEquals(buf, encoded, "Encoded buffer is different from the original one"); + } + + @Test(dataProvider = "goodFrames") + public void testToString(String desc, byte[] frame, boolean bloated) throws Exception { + // check if the goodFrames provider indeed contains good frames + ByteBuffer buf = ByteBuffer.wrap(frame); + var qf = QuicFrame.decode(buf); + assertFalse(buf.hasRemaining(), buf.remaining() + " bytes left in buffer after parsing"); + System.out.println(qf); // should not throw + } + + @Test(dataProvider = "goodFrames") + public void testTruncatedFrame(String desc, byte[] frame, boolean bloated) throws Exception { + // check if parsing a truncated frame throws the right error + ByteBuffer buf = ByteBuffer.wrap(frame); + for (int i = 1; i < buf.capacity(); i++) { + buf.position(0); + buf.limit(i); + try { + var qf = QuicFrame.decode(buf); + fail("Expected the decoder to throw on length " + i + ", got: " + qf); + } catch (QuicTransportException e) { + assertEquals(e.getErrorCode(), QuicTransportErrors.FRAME_ENCODING_ERROR.code()); + } + } + } + + @Test(dataProvider = "badFrames") + public void testBadFrame(String desc, byte[] frame) throws Exception { + // check if parsing a bad frame throws the right error + ByteBuffer buf = ByteBuffer.wrap(frame); + try { + var qf = QuicFrame.decode(buf); + fail("Expected the decoder to throw, got: "+qf); + } catch (QuicTransportException e) { + assertEquals(e.getErrorCode(), QuicTransportErrors.FRAME_ENCODING_ERROR.code()); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/QuicRequestResponseTest.java b/test/jdk/java/net/httpclient/quic/QuicRequestResponseTest.java new file mode 100644 index 00000000000..2c5844dc4c4 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/QuicRequestResponseTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/* + * @test + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.httpclient.test.lib.common.TestUtil + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.internal.httpclient.debug=true QuicRequestResponseTest + */ +public class QuicRequestResponseTest { + + private QuicStandaloneServer server; + private SSLContext sslContext; + private ExecutorService executor; + + private static final byte[] HELLO_MSG = "Hello Quic".getBytes(StandardCharsets.UTF_8); + + @BeforeClass + public void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .build(); + // add a handler which deals with incoming connections + server.addHandler(new EchoHandler(HELLO_MSG.length)); + server.start(); + System.out.println("Server started at " + server.getAddress()); + } + + @AfterClass + public void afterClass() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + if (executor != null) executor.close(); + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + @Test + public void test() throws Exception { + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, server.getAddress()); + // open a bidi stream + final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + // write data on the stream + try (final OutputStream os = bidiStream.outputStream()) { + os.write(HELLO_MSG); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for response + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("client: reading from bidi stream's input stream"); + final byte[] data = is.readAllBytes(); + System.out.println("client: Received response of size " + data.length); + final String response = new String(data, StandardCharsets.UTF_8); + // verify response + System.out.println("client: Response: " + response); + if (!Arrays.equals(response.getBytes(StandardCharsets.UTF_8), HELLO_MSG)) { + throw new AssertionError("Unexpected response: " + response); + } + } finally { + System.err.println("client: Closing bidi stream from test"); + bidiStream.close(); + } + } + } + + /** + * Reads data from incoming client initiated bidirectional stream of a Quic connection + * and writes back a response which is same as the read data + */ + private static final class EchoHandler implements QuicServerHandler { + + private final int numBytesToRead; + + private EchoHandler(final int numBytesToRead) { + this.numBytesToRead = numBytesToRead; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + final byte[] data; + // read the request content + try (final InputStream is = bidiStream.inputStream()) { + System.out.println("Handler reading data from bidi stream's inputstream " + is); + data = is.readAllBytes(); + System.out.println("Handler read " + data.length + " bytes of data"); + } + if (data.length != numBytesToRead) { + throw new IOException("Expected to read " + numBytesToRead + + " bytes but read only " + data.length + " bytes"); + } + // write response + try (final OutputStream os = bidiStream.outputStream()) { + System.out.println("Handler writing data to bidi stream's outputstream " + os); + os.write(data); + } + System.out.println("Handler invocation complete"); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/StatelessResetReceiptTest.java b/test/jdk/java/net/httpclient/quic/StatelessResetReceiptTest.java new file mode 100644 index 00000000000..8eb7a3663d8 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/StatelessResetReceiptTest.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.common.MinimalFuture; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.http.quic.QuicConnectionId; +import jdk.internal.net.http.quic.QuicConnectionImpl; +import jdk.internal.net.http.quic.TerminationCause; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static jdk.internal.net.http.quic.TerminationCause.forTransportError; +import static jdk.internal.net.quic.QuicTransportErrors.NO_VIABLE_PATH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary verify that when a QUIC (client) connection receives a stateless reset + * from the peer, then the connection is properly terminated + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestUtil + * @run junit/othervm -Djdk.internal.httpclient.debug=true StatelessResetReceiptTest + */ +public class StatelessResetReceiptTest { + + private static QuicStandaloneServer server; + private static SSLContext sslContext; + private static ExecutorService executor; + + @BeforeAll + static void beforeAll() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1}) + .sslContext(sslContext) + .build(); + server.start(); + System.out.println("Server started at " + server.getAddress()); + } + + @AfterAll + static void afterAll() throws Exception { + if (server != null) { + System.out.println("Stopping server " + server.getAddress()); + server.close(); + } + if (executor != null) { + executor.close(); + } + } + + private QuicClient createClient() { + var versions = List.of(QuicVersion.QUIC_V1); + var context = new QuicTLSContext(sslContext); + var params = new SSLParameters(); + return new QuicClient.Builder() + .availableVersions(versions) + .tlsContext(context) + .sslParameters(params) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + /** + * Initiates a connection between client and server. When the connection is still + * active, this test initiates a stateless reset from the server connection against + * the client connection. The test then expects that the client connection, which was active + * until then, is terminated due to this stateless reset. + */ + @Test + public void testActiveConnection() throws Exception { + final CompletableFuture serverConnCF = new MinimalFuture<>(); + final NotifyingHandler handler = new NotifyingHandler(serverConnCF); + server.addHandler(handler); + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + // write data on the stream + try (final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + final OutputStream os = bidiStream.outputStream()) { + os.write("foobar".getBytes(StandardCharsets.UTF_8)); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for the handler on the server's connection to be invoked + System.out.println("waiting for the request to be handled by the server connection"); + final QuicServerConnection serverConn = serverConnCF.get(); + System.out.println("request handled by the server connection " + serverConn); + // verify the connection is still open + assertTrue(conn.underlyingQuicConnection().isOpen(), "QUIC connection is not open"); + sendStatelessResetFrom(serverConn); + // now expect the (active) client connection to be terminated + assertStatelessResetTermination(conn); + } + } + + /** + * Initiates a connection between client and server. The client connection is then + * closed and it thus moves to the closing state. The test then initiates a stateless reset + * from the server connection against this closing client connection. The test then verifies + * that the client connection, which was in closing state, has been completely removed from the + * endpoint upon receiving this stateless reset. + */ + @Test + public void testClosingConnection() throws Exception { + final CompletableFuture serverConnCF = new MinimalFuture<>(); + final NotifyingHandler handler = new NotifyingHandler(serverConnCF); + server.addHandler(handler); + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + // write data on the stream + try (final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + final OutputStream os = bidiStream.outputStream()) { + os.write("foobar".getBytes(StandardCharsets.UTF_8)); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for the handler on the server's connection to be invoked + System.out.println("waiting for the request to be handled by the server connection"); + final QuicServerConnection serverConn = serverConnCF.get(); + System.out.println("request handled by the server connection " + serverConn); + // now close the client/local connection so that it transitions to closing state + System.out.println("closing client connection " + conn); + conn.close(); + // verify connection is no longer open + assertFalse(conn.underlyingQuicConnection().isOpen(), "QUIC connection is still open"); + // now send a stateless reset from the server connection + sendStatelessResetFrom(serverConn); + // wait for the stateless reset to be processed + final Instant waitEnd = Instant.now().plus(Duration.ofSeconds(2)); + while (Instant.now().isBefore(waitEnd)) { + if (conn.endpoint().connectionCount() != 0) { + // wait for a while + Thread.sleep(10); + } + } + // now expect the endpoint to have removed the client connection. + // this isn't a fool proof verification because the connection could have been + // moved from the closing state to draining and then removed, without having processed + // the stateless reset, but we don't have any other credible way of verifying this + assertEquals(0, conn.endpoint().connectionCount(), "unexpected number of connections" + + " known to QUIC endpoint"); + } + } + + /** + * Initiates a connection between client and server. The server connection is then + * closed and the client connection thus moves to the draining state. The test then initiates + * a stateless reset from the server connection against this draining client connection. The + * test then verifies that the client connection, which was in draining state, has been + * completely removed from the endpoint upon receiving this stateless reset. + */ + @Test + public void testDrainingConnection() throws Exception { + final CompletableFuture serverConnCF = new MinimalFuture<>(); + final NotifyingHandler handler = new NotifyingHandler(serverConnCF); + server.addHandler(handler); + try (final QuicClient client = createClient()) { + // create a QUIC connection to the server + final ClientConnection conn = ClientConnection.establishConnection(client, + server.getAddress()); + // write data on the stream + try (final ConnectedBidiStream bidiStream = conn.initiateNewBidiStream(); + final OutputStream os = bidiStream.outputStream()) { + os.write("foobar".getBytes(StandardCharsets.UTF_8)); + System.out.println("client: Client wrote message to bidi stream's output stream"); + } + // wait for the handler on the server's connection to be invoked + System.out.println("waiting for the request to be handled by the server connection"); + final QuicServerConnection serverConn = serverConnCF.get(); + System.out.println("request handled by the server connection " + serverConn); + // now close the server connection so that the client conn transitions to draining state + System.out.println("closing server connection " + serverConn); + // intentionally use a "unique" error to confidently verify the termination cause + final TerminationCause tc = forTransportError(NO_VIABLE_PATH) + .loggedAs("intentionally closed by server to initiate draining state" + + " on client connection"); + serverConn.connectionTerminator().terminate(tc); + // wait for client conn to terminate + final TerminationCause clientTC = ((QuicConnectionImpl) conn.underlyingQuicConnection()) + .futureTerminationCause().get(); + // verify connection closed for the right reason + assertEquals(NO_VIABLE_PATH.code(), clientTC.getCloseCode(), + "unexpected termination cause"); + // now send a stateless reset from the server connection + sendStatelessResetFrom(serverConn); + // wait for the stateless reset to be processed + final Instant waitEnd = Instant.now().plus(Duration.ofSeconds(2)); + while (Instant.now().isBefore(waitEnd)) { + if (conn.endpoint().connectionCount() != 0) { + // wait for a while + Thread.sleep(10); + } + } + // now expect the endpoint to have removed the client connection. + // this isn't a fool proof verification because the connection could have been + // removed after moving out from the draining state, without having processed the + // stateless reset, but we don't have any other credible way of verifying this + assertEquals(0, conn.endpoint().connectionCount(), "unexpected number of connections" + + " known to QUIC endpoint"); + } + } + + private static void sendStatelessResetFrom(final QuicServerConnection serverConn) + throws IOException { + final QuicConnectionId localConnId = serverConn.localConnectionId(); + final ByteBuffer resetDatagram = serverConn.endpoint().idFactory().statelessReset( + localConnId.asReadOnlyBuffer(), 43); + final InetSocketAddress targetAddr = serverConn.peerAddress(); + ((DatagramChannel) serverConn.channel()).send(resetDatagram, targetAddr); + System.out.println("sent stateless reset from server conn " + serverConn + " to " + + targetAddr); + } + + private static void assertStatelessResetTermination(final ClientConnection conn) + throws Exception { + final CompletableFuture cf = + ((QuicConnectionImpl) conn.underlyingQuicConnection()).futureTerminationCause(); + final TerminationCause tc = cf.get(); + System.out.println("got termination cause " + tc.getCloseCause() + " - " + tc.getLogMsg()); + final IOException closeCause = tc.getCloseCause(); + assertNotNull(closeCause, "close cause IOException is null"); + final String expectedMsg = "stateless reset from peer"; + if (closeCause.getMessage() != null && closeCause.getMessage().contains(expectedMsg)) { + // got expected IOException + return; + } + // unexpected IOException. throw it back + throw closeCause; + } + + private static final class NotifyingHandler implements QuicServerHandler { + + private final CompletableFuture serverConnCF; + + private NotifyingHandler(final CompletableFuture serverConnCF) { + this.serverConnCF = serverConnCF; + } + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) { + System.out.println("Handling incoming bidi stream " + bidiStream + + " on connection " + conn); + this.serverConnCF.complete(conn); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/VariableLengthTest.java b/test/jdk/java/net/httpclient/quic/VariableLengthTest.java new file mode 100644 index 00000000000..aec3e999812 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/VariableLengthTest.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +import jdk.internal.net.http.quic.VariableLengthEncoder; +import jtreg.SkippedException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +/* + * @test + * @library /test/lib + * @modules java.net.http/jdk.internal.net.http.quic + * @run testng/othervm VariableLengthTest + * @summary Tests to check quic/util methods encode/decodeVariableLength methods + * work as expected. + */ +public class VariableLengthTest { + static final Class IAE = IllegalArgumentException.class; + + @DataProvider(name = "decode invariants") + public Object[][] decodeInvariants() { + return new Object[][] + { + { new byte[]{7}, 7, 1 }, // 00 + { new byte[]{65, 11}, 267, 2 }, // 01 + { new byte[]{-65, 11, 22, 33}, 1057691169, 4 }, // 10 + { new byte[]{-1, 11, 22, 33, 44, 55, 66, 77}, 4542748980864827981L, 8 }, // 11 + { new byte[]{-1, -11, -22, -33, -44, -55, -66, -77}, 4608848040752168627L, 8 }, + { new byte[]{}, -1, 0 }, + { new byte[]{-65}, -1, 0 }, + }; + } + @DataProvider(name = "encode invariants") + public Object[][] encodeInvariants() { + return new Object[][] + { + { 7, 1, null }, // 00 + { 267, 2, null }, // 01 + { 1057691169, 4, null }, // 10 + { 4542748980864827981L, 8, null }, // 11 + { Long.MAX_VALUE, 0, IAE }, + { -1, 0, IAE }, + }; + } + @DataProvider(name = "prefix invariants") + public Object[][] prefixInvariants() { + return new Object[][] + { + { Long.MAX_VALUE, 0, IAE }, + { 4611686018427387903L+1, 0, IAE }, + { 4611686018427387903L, 3, null }, + { 4611686018427387903L-1, 3, null }, + { 1073741823+1, 3, null }, + { 1073741823, 2, null }, // (length > (1L << 30)-1) + { 1073741823-1, 2, null }, + { 16383+1, 2, null }, + { 16383, 1, null }, // (length > (1L << 14)-1 + { 16383-1, 1, null }, + { 63+1, 1, null }, + { 63 , 0, null }, // (length > (1L << 6)-1 + { 63-1, 0, null }, + { 100, 1, null }, + { 10, 0, null }, + { 1, 0, null }, + { 0, 0, null }, // (length >= 0) + { -1, 0, IAE }, + { -10, 0, IAE }, + { -100, 0, IAE }, + { Long.MIN_VALUE, 0, IAE }, + { -4611686018427387903L-1, 0, IAE }, + { -4611686018427387903L, 0, IAE }, + { -4611686018427387903L+1, 0, IAE }, + { -1073741823-1, 0, IAE }, + { -1073741823, 0, IAE }, // (length > (1L << 30)-1) + { -1073741823+1, 0, IAE }, + { -16383-1, 0, IAE }, + { -16383, 0, IAE }, // (length > (1L << 14)-1 + { -16383+1, 0, IAE }, + { -63-1, 0, IAE }, + { -63 , 0, IAE }, // (length > (1L << 6)-1 + { -63+1, 0, IAE }, + }; + } + + @Test(dataProvider = "decode invariants") + public void testDecode(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + var actualLength = VariableLengthEncoder.decode(bb); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, expectedPosition); + } + + @Test(dataProvider = "decode invariants") + public void testPeek(byte[] values, long expectedLength, int expectedPosition) { + ByteBuffer bb = ByteBuffer.wrap(values); + var actualLength = VariableLengthEncoder.peekEncodedValue(bb, 0); + assertEquals(actualLength, expectedLength); + + var actualPosition = bb.position(); + assertEquals(actualPosition, 0); + } + + @Test(dataProvider = "encode invariants") + public void testEncode(long length, int capacity, Class exception) throws IOException { + var actualBuffer = ByteBuffer.allocate(capacity); + var expectedBuffer = getTestBuffer(length, capacity); + + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.encode(actualBuffer, length)); + // if method fails ensure that position hasn't changed + var actualPosition = actualBuffer.position(); + assertEquals(actualPosition, capacity); + } else { + VariableLengthEncoder.encode(actualBuffer, length); + var actualPosition = actualBuffer.position(); + assertEquals(actualPosition, capacity); + + // check length prefix + int firstByte = actualBuffer.get(0) & 0xFF; + int lengthPrefix = firstByte & 0xC0; + lengthPrefix >>= 6; + int expectedValue = (int)(Math.log(capacity) / Math.log(2)); + assertEquals(lengthPrefix, expectedValue); + + // check length encoded in buffer correctly + int b = firstByte & 0x3F; + actualBuffer.put(0, (byte) b); + assertEquals(actualBuffer.compareTo(expectedBuffer), 0); + } + } + + @Test(dataProvider = "prefix invariants") + public void testLengthPrefix(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getVariableLengthPrefix(length)); + } else { + var actualValue = VariableLengthEncoder.getVariableLengthPrefix(length); + assertEquals(actualValue, expectedPrefix); + } + } + + // Encode the given length and then decodes it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodeDecode(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + } else { + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Decode %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too short, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer shorter = ByteBuffer.allocate(actualSize - 1 + offset); + shorter.position(offset); + ByteBuffer shorterref = ByteBuffer.allocate(actualSize - 1 + offset); + shorterref.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer too short + expectThrows(IAE, () -> VariableLengthEncoder.encode(shorter, length)); + assertEquals(shorter.position(), offset); + assertEquals(shorter.limit(), shorter.capacity()); + + assertEquals(shorter.mismatch(shorterref), -1); + assertEquals(shorterref.mismatch(shorter), -1); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(offset); + shortSlice.limit(offset + actualSize -1); + var actualLength = VariableLengthEncoder.decode(shortSlice); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), offset); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + + // decode with the exact buffer + actualLength = VariableLengthEncoder.decode(exact); + assertEquals(actualLength, length); + assertEquals(exact.position(), offset + actualSize); + assertFalse(exact.hasRemaining()); + + // decode with the longer buffer + actualLength = VariableLengthEncoder.decode(longer); + assertEquals(actualLength, length); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.remaining(), 10); + } + + } + } + + // Encode the given length and then peeks it and compares + // the results, asserting various invariants along the way. + @Test(dataProvider = "prefix invariants") + public void testEncodePeek(long length, int expectedPrefix, Class exception) { + if (exception != null) { + assertThrows(exception, () -> VariableLengthEncoder.getEncodedSize(length)); + assertThrows(exception, () -> VariableLengthEncoder.encode(ByteBuffer.allocate(16), length)); + return; + } + + var actualSize = VariableLengthEncoder.getEncodedSize(length); + assertEquals(actualSize, 1 << expectedPrefix); + assertTrue(actualSize > 0, "length is negative or zero: " + actualSize); + assertTrue(actualSize < 9, "length is too big: " + actualSize); + + // Use different offsets for the position at which to encode/decode + for (int offset : List.of(0, 10)) { + System.out.printf("Encode/Peek %s on %s bytes with offset %s%n", + length, actualSize, offset); + + // allocate buffers: one exact, one too long + ByteBuffer exact = ByteBuffer.allocate(actualSize + offset); + exact.position(offset); + ByteBuffer longer = ByteBuffer.allocate(actualSize + 10 + offset); + longer.position(offset); + + // attempt to encode with a buffer that has the exact size + var exactres = VariableLengthEncoder.encode(exact, length); + assertEquals(exactres, actualSize); + assertEquals(exact.position(), actualSize + offset); + assertFalse(exact.hasRemaining()); + + // attempt to encode with a buffer that has more bytes + var longres = VariableLengthEncoder.encode(longer, length); + assertEquals(longres, actualSize); + assertEquals(longer.position(), offset + actualSize); + assertEquals(longer.limit(), longer.capacity()); + assertEquals(longer.remaining(), 10); + + // compare encodings + + // first reset buffer positions for reading. + exact.position(offset); + longer.position(offset); + assertEquals(longer.mismatch(exact), actualSize); + assertEquals(exact.mismatch(longer), actualSize); + exact.position(0); + longer.position(0); + exact.limit(exact.capacity()); + longer.limit(longer.capacity()); + + // decode with a buffer that is missing the last + // byte... + var shortSlice = exact.duplicate(); + shortSlice.position(0); + shortSlice.limit(offset + actualSize - 1); + // need at least one byte to decode the size len... + var expectedSize = shortSlice.limit() <= offset ? -1 : actualSize; + assertEquals(VariableLengthEncoder.peekEncodedValueSize(shortSlice, offset), expectedSize); + var actualLength = VariableLengthEncoder.peekEncodedValue(shortSlice, offset); + assertEquals(actualLength, -1L); + assertEquals(shortSlice.position(), 0); + assertEquals(shortSlice.limit(), offset + actualSize - 1); + + // decode with the exact buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(exact, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(exact, offset); + assertEquals(actualLength, length); + assertEquals(exact.position(), 0); + assertEquals(exact.limit(), exact.capacity()); + + // decode with the longer buffer + assertEquals(VariableLengthEncoder.peekEncodedValueSize(longer, offset), actualSize); + actualLength = VariableLengthEncoder.peekEncodedValue(longer, offset); + assertEquals(actualLength, length); + assertEquals(longer.position(), 0); + assertEquals(longer.limit(), longer.capacity()); + } + + } + + + private ByteBuffer getTestBuffer(long length, int capacity) { + return switch (capacity) { + case 0 -> ByteBuffer.allocate(1).put((byte) length); + case 1 -> ByteBuffer.allocate(capacity).put((byte) length); + case 2 -> ByteBuffer.allocate(capacity).putShort((short) length); + case 4 -> ByteBuffer.allocate(capacity).putInt((int) length); + case 8 -> ByteBuffer.allocate(capacity).putLong(length); + default -> throw new SkippedException("bad value used for capacity"); + }; + } +} diff --git a/test/jdk/java/net/httpclient/quic/VersionNegotiationTest.java b/test/jdk/java/net/httpclient/quic/VersionNegotiationTest.java new file mode 100644 index 00000000000..d0de5ce9a60 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/VersionNegotiationTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; + +import jdk.httpclient.test.lib.common.TestUtil; +import jdk.httpclient.test.lib.quic.ClientConnection; +import jdk.httpclient.test.lib.quic.ConnectedBidiStream; +import jdk.httpclient.test.lib.quic.QuicServer; +import jdk.httpclient.test.lib.quic.QuicServerConnection; +import jdk.httpclient.test.lib.quic.QuicServerHandler; +import jdk.httpclient.test.lib.quic.QuicStandaloneServer; +import jdk.internal.net.http.quic.QuicClient; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +/* + * @test + * @summary Test the version negotiation semantics of Quic + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build jdk.httpclient.test.lib.quic.QuicStandaloneServer + * jdk.httpclient.test.lib.common.TestUtil + * jdk.httpclient.test.lib.quic.ClientConnection + * jdk.test.lib.net.SimpleSSLContext + * @run testng/othervm -Djdk.internal.httpclient.debug=true VersionNegotiationTest + */ +public class VersionNegotiationTest { + + private static SSLContext sslContext; + private static ExecutorService executor; + + @BeforeClass + public static void beforeClass() throws Exception { + sslContext = new SimpleSSLContext().get(); + if (sslContext == null) { + throw new AssertionError("Unexpected null sslContext"); + } + executor = Executors.newCachedThreadPool(); + } + + @AfterClass + public static void afterClass() throws Exception { + if (executor != null) executor.shutdown(); + } + + private QuicClient createClient() { + return new QuicClient.Builder() + .availableVersions(List.of(QuicVersion.QUIC_V1)) + .tlsContext(new QuicTLSContext(sslContext)) + .sslParameters(new SSLParameters()) + .executor(executor) + .bindAddress(TestUtil.chooseClientBindAddress().orElse(null)) + .build(); + } + + /** + * Uses a Quic client which is enabled for a specific version and a Quic server + * which is enabled for a different version. Verifies that the connection attempt fails + * as noted in RFC-9000, section 6.2 + */ + @Test + public void testUnsupportedClientVersion() throws Exception { + try (final var client = createClient()) { + final QuicVersion serverVersion = QuicVersion.QUIC_V2; + try (final QuicServer server = createAndStartServer(serverVersion)) { + System.out.println("Attempting to connect " + client.getAvailableVersions() + + " client to a " + server.getAvailableVersions() + " server"); + final IOException thrown = expectThrows(IOException.class, + () -> ClientConnection.establishConnection(client, server.getAddress())); + // a version negotiation failure (since it happens during a QUIC connection + // handshake) gets thrown as a SSLHandshakeException + if (!(thrown.getCause() instanceof SSLHandshakeException sslhe)) { + throw thrown; + } + System.out.println("Received (potentially expected) exception: " + sslhe); + // additional check to make sure it was thrown for the right reason + assertEquals(sslhe.getMessage(), "QUIC connection establishment failed"); + // underlying cause of SSLHandshakeException should be version negotiation failure + final Throwable underlyingCause = sslhe.getCause(); + assertNotNull(underlyingCause, "missing cause in SSLHandshakeException"); + assertNotNull(underlyingCause.getMessage(), "missing message in " + underlyingCause); + assertTrue(underlyingCause.getMessage().contains("No support for any of the" + + " QUIC versions being negotiated")); + } + } + } + + /** + * Creates a server which supports only the specified Quic version + */ + private static QuicServer createAndStartServer(final QuicVersion version) throws IOException { + final QuicStandaloneServer server = QuicStandaloneServer.newBuilder() + .availableVersions(new QuicVersion[]{version}) + .sslContext(sslContext) + .build(); + server.addHandler(new ExceptionThrowingHandler()); + server.start(); + System.out.println("Quic server with version " + version + " started at " + server.getAddress()); + return server; + } + + private static final class ExceptionThrowingHandler implements QuicServerHandler { + + @Override + public void handleBidiStream(final QuicServerConnection conn, + final ConnectedBidiStream bidiStream) throws IOException { + throw new AssertionError("Handler shouldn't have been called for " + + bidiStream + " on connection " + conn); + } + } +} diff --git a/test/jdk/java/net/httpclient/quic/quic-tls-keylimits-java.security b/test/jdk/java/net/httpclient/quic/quic-tls-keylimits-java.security new file mode 100644 index 00000000000..0d66b3a9e2f --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/quic-tls-keylimits-java.security @@ -0,0 +1,4 @@ +# meant to override the jdk.quic.tls.keyLimits security property +# in the KeyUpdateTest +jdk.quic.tls.keyLimits=AES/GCM/NoPadding 10, \ + ChaCha20-Poly1305 -1 diff --git a/test/jdk/java/net/httpclient/quic/tls/PacketEncryptionTest.java b/test/jdk/java/net/httpclient/quic/tls/PacketEncryptionTest.java new file mode 100644 index 00000000000..ed1cbc08de0 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/PacketEncryptionTest.java @@ -0,0 +1,457 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import org.testng.annotations.Test; +import sun.security.ssl.QuicTLSEngineImpl; +import sun.security.ssl.QuicTLSEngineImplAccessor; + +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLContext; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.function.IntFunction; + +import static jdk.internal.net.quic.QuicVersion.QUIC_V1; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build java.base/sun.security.ssl.QuicTLSEngineImplAccessor + * @summary known-answer test for packet encryption and decryption + * @run testng/othervm PacketEncryptionTest + */ +public class PacketEncryptionTest { + + // RFC 9001, appendix A + private static final String INITIAL_DCID = "8394c8f03e515708"; + // section A.2 + // header includes 4-byte packet number 2 + private static final String INITIAL_C_HEADER = "c300000001088394c8f03e5157080000449e00000002"; + private static final int INITIAL_C_PAYLOAD_OFFSET = INITIAL_C_HEADER.length() / 2; + private static final int INITIAL_C_PN_OFFSET = INITIAL_C_PAYLOAD_OFFSET - 4; + private static final int INITIAL_C_PN = 2; + // payload is zero-padded to 1162 bytes, not shown here + private static final String INITIAL_C_PAYLOAD = + "060040f1010000ed0303ebf8fa56f129"+"39b9584a3896472ec40bb863cfd3e868" + + "04fe3a47f06a2b69484c000004130113"+"02010000c000000010000e00000b6578" + + "616d706c652e636f6dff01000100000a"+"00080006001d00170018001000070005" + + "04616c706e0005000501000000000033"+"00260024001d00209370b2c9caa47fba" + + "baf4559fedba753de171fa71f50f1ce1"+"5d43e994ec74d748002b000302030400" + + "0d0010000e0403050306030203080408"+"050806002d00020101001c0002400100" + + "3900320408ffffffffffffffff050480"+"00ffff07048000ffff08011001048000" + + "75300901100f088394c8f03e51570806"+"048000ffff"; + private static final int INITIAL_C_PAYLOAD_LENGTH = 1162; + private static final String ENCRYPTED_C_PAYLOAD = + "c000000001088394c8f03e5157080000"+"449e7b9aec34d1b1c98dd7689fb8ec11" + + "d242b123dc9bd8bab936b47d92ec356c"+"0bab7df5976d27cd449f63300099f399" + + "1c260ec4c60d17b31f8429157bb35a12"+"82a643a8d2262cad67500cadb8e7378c" + + "8eb7539ec4d4905fed1bee1fc8aafba1"+"7c750e2c7ace01e6005f80fcb7df6212" + + "30c83711b39343fa028cea7f7fb5ff89"+"eac2308249a02252155e2347b63d58c5" + + "457afd84d05dfffdb20392844ae81215"+"4682e9cf012f9021a6f0be17ddd0c208" + + "4dce25ff9b06cde535d0f920a2db1bf3"+"62c23e596d11a4f5a6cf3948838a3aec" + + "4e15daf8500a6ef69ec4e3feb6b1d98e"+"610ac8b7ec3faf6ad760b7bad1db4ba3" + + "485e8a94dc250ae3fdb41ed15fb6a8e5"+"eba0fc3dd60bc8e30c5c4287e53805db" + + "059ae0648db2f64264ed5e39be2e20d8"+"2df566da8dd5998ccabdae053060ae6c" + + "7b4378e846d29f37ed7b4ea9ec5d82e7"+"961b7f25a9323851f681d582363aa5f8" + + "9937f5a67258bf63ad6f1a0b1d96dbd4"+"faddfcefc5266ba6611722395c906556" + + "be52afe3f565636ad1b17d508b73d874"+"3eeb524be22b3dcbc2c7468d54119c74" + + "68449a13d8e3b95811a198f3491de3e7"+"fe942b330407abf82a4ed7c1b311663a" + + "c69890f4157015853d91e923037c227a"+"33cdd5ec281ca3f79c44546b9d90ca00" + + "f064c99e3dd97911d39fe9c5d0b23a22"+"9a234cb36186c4819e8b9c5927726632" + + "291d6a418211cc2962e20fe47feb3edf"+"330f2c603a9d48c0fcb5699dbfe58964" + + "25c5bac4aee82e57a85aaf4e2513e4f0"+"5796b07ba2ee47d80506f8d2c25e50fd" + + "14de71e6c418559302f939b0e1abd576"+"f279c4b2e0feb85c1f28ff18f58891ff" + + "ef132eef2fa09346aee33c28eb130ff2"+"8f5b766953334113211996d20011a198" + + "e3fc433f9f2541010ae17c1bf202580f"+"6047472fb36857fe843b19f5984009dd" + + "c324044e847a4f4a0ab34f719595de37"+"252d6235365e9b84392b061085349d73" + + "203a4a13e96f5432ec0fd4a1ee65accd"+"d5e3904df54c1da510b0ff20dcc0c77f" + + "cb2c0e0eb605cb0504db87632cf3d8b4"+"dae6e705769d1de354270123cb11450e" + + "fc60ac47683d7b8d0f811365565fd98c"+"4c8eb936bcab8d069fc33bd801b03ade" + + "a2e1fbc5aa463d08ca19896d2bf59a07"+"1b851e6c239052172f296bfb5e724047" + + "90a2181014f3b94a4e97d117b4381303"+"68cc39dbb2d198065ae3986547926cd2" + + "162f40a29f0c3c8745c0f50fba3852e5"+"66d44575c29d39a03f0cda721984b6f4" + + "40591f355e12d439ff150aab7613499d"+"bd49adabc8676eef023b15b65bfc5ca0" + + "6948109f23f350db82123535eb8a7433"+"bdabcb909271a6ecbcb58b936a88cd4e" + + "8f2e6ff5800175f113253d8fa9ca8885"+"c2f552e657dc603f252e1a8e308f76f0" + + "be79e2fb8f5d5fbbe2e30ecadd220723"+"c8c0aea8078cdfcb3868263ff8f09400" + + "54da48781893a7e49ad5aff4af300cd8"+"04a6b6279ab3ff3afb64491c85194aab" + + "760d58a606654f9f4400e8b38591356f"+"bf6425aca26dc85244259ff2b19c41b9" + + "f96f3ca9ec1dde434da7d2d392b905dd"+"f3d1f9af93d1af5950bd493f5aa731b4" + + "056df31bd267b6b90a079831aaf579be"+"0a39013137aac6d404f518cfd4684064" + + "7e78bfe706ca4cf5e9c5453e9f7cfd2b"+"8b4c8d169a44e55c88d4a9a7f9474241" + + "e221af44860018ab0856972e194cd934"; + // section A.3 + // header includes 2-byte packet number 1 + private static final String INITIAL_S_HEADER = "c1000000010008f067a5502a4262b50040750001"; + private static final int INITIAL_S_PAYLOAD_OFFSET = INITIAL_S_HEADER.length() / 2; + private static final int INITIAL_S_PN_OFFSET = INITIAL_S_PAYLOAD_OFFSET - 2; + private static final int INITIAL_S_PN = 1; + // complete packet, no padding + private static final String INITIAL_S_PAYLOAD = + "02000000000600405a020000560303ee"+"fce7f7b37ba1d1632e96677825ddf739" + + "88cfc79825df566dc5430b9a045a1200"+"130100002e00330024001d00209d3c94" + + "0d89690b84d08a60993c144eca684d10"+"81287c834d5311bcf32bb9da1a002b00" + + "020304"; + private static final int INITIAL_S_PAYLOAD_LENGTH = INITIAL_S_PAYLOAD.length() / 2; + private static final String ENCRYPTED_S_PAYLOAD = + "cf000000010008f067a5502a4262b500"+"4075c0d95a482cd0991cd25b0aac406a" + + "5816b6394100f37a1c69797554780bb3"+"8cc5a99f5ede4cf73c3ec2493a1839b3" + + "dbcba3f6ea46c5b7684df3548e7ddeb9"+"c3bf9c73cc3f3bded74b562bfb19fb84" + + "022f8ef4cdd93795d77d06edbb7aaf2f"+"58891850abbdca3d20398c276456cbc4" + + "2158407dd074ee"; + + // section A.4 + private static final String SIGNED_RETRY = + "ff000000010008f067a5502a4262b574"+"6f6b656e04a265ba2eff4d829058fb3f" + + "0f2496ba"; + + // section A.5 + public static final String ONERTT_SECRET = "9ac312a7f877468ebe69422748ad00a1" + + "5443f18203a07d6060f688f30f21632b"; + private static final String ONERTT_HEADER = "4200bff4"; + private static final int ONERTT_PAYLOAD_OFFSET = ONERTT_HEADER.length() / 2; + private static final int ONERTT_PN_OFFSET = 1; + private static final int ONERTT_PN = 654360564; + // payload is zero-padded to 1162 bytes, not shown here + private static final String ONERTT_PAYLOAD = + "01"; + private static final int ONERTT_PAYLOAD_LENGTH = + ONERTT_PAYLOAD.length() / 2; + private static final String ENCRYPTED_ONERTT_PAYLOAD = + "4cfe4189655e5cd55c41f69080575d7999c25a5bfb"; + + private static final class FixedHeaderContent implements IntFunction { + private final ByteBuffer header; + private FixedHeaderContent(ByteBuffer header) { + this.header = header; + } + + @Override + public ByteBuffer apply(final int keyphase) { + // ignore keyphase + return this.header; + } + } + + @Test + public void testEncryptClientInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V1, dcid); + + final int packetLen = INITIAL_C_PAYLOAD_OFFSET + INITIAL_C_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_C_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_C_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_C_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_C_PAYLOAD_OFFSET, INITIAL_C_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_C_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, clientEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_C_PAYLOAD); + } + + @Test + public void testDecryptClientInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV1Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V1, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, + src, INITIAL_C_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_C_HEADER + INITIAL_C_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptClientInitialPacketBadTag() throws Exception { + QuicTLSEngine serverEngine = getQuicV1Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V1, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, + src, INITIAL_C_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + @Test + public void testEncryptServerInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV1Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V1, dcid); + + final int packetLen = INITIAL_S_PAYLOAD_OFFSET + INITIAL_S_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_S_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_S_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_S_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_S_PAYLOAD_OFFSET, INITIAL_S_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_S_PAYLOAD_OFFSET); + serverEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, serverEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_S_PAYLOAD); + } + + @Test + public void testDecryptServerInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V1, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, + src, INITIAL_S_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_S_HEADER + INITIAL_S_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptServerInitialPacketTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V1, dcid); + + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + // must not throw + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + } + + @Test + public void testSignRetry() throws NoSuchAlgorithmException, ShortBufferException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.allocate(SIGNED_RETRY.length() / 2); + packet.put(HexFormat.of().parseHex(SIGNED_RETRY), 0, SIGNED_RETRY.length() / 2 - 16); + + ByteBuffer src = packet.asReadOnlyBuffer(); + src.limit(src.position()); + src.position(0); + + clientEngine.signRetryPacket(QUIC_V1, dcid, src, packet); + + assertEquals(HexFormat.of().formatHex(packet.array()), SIGNED_RETRY); + } + + @Test + public void testVerifyRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + clientEngine.verifyRetryPacket(QUIC_V1, dcid, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testVerifyBadRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV1Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + clientEngine.verifyRetryPacket(QUIC_V1, dcid, packet); + fail("Verification should have failed"); + } + + @Test + public void testEncryptChaCha() throws Exception { + QuicTLSEngineImpl clientEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), true); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, clientEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", true); + + final int packetLen = ONERTT_PAYLOAD_OFFSET + ONERTT_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(ONERTT_HEADER)); + packet.put(HexFormat.of().parseHex(ONERTT_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, ONERTT_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(ONERTT_PAYLOAD_OFFSET, ONERTT_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(ONERTT_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN , new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, clientEngine, 0x1f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_ONERTT_PAYLOAD); + } + + @Test + public void testDecryptChaCha() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, (byte) 0, + src, ONERTT_PAYLOAD_OFFSET, packet); + + String expectedContents = ONERTT_HEADER + ONERTT_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptChaChaTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + final int keyPhase = 0; + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyPhase, src, ONERTT_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + // must not throw + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyPhase, src, ONERTT_PAYLOAD_OFFSET, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptChaChaBadTag() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV1Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V1, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, (byte) 0, + src, ONERTT_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + + private void protect(QuicTLSEngine.KeySpace space, ByteBuffer buffer, + int packetNumberStart, int packetNumberLength, QuicTLSEngine tlsEngine, + int headersMask) throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(space, false, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void unprotect(QuicTLSEngine.KeySpace keySpace, ByteBuffer buffer, + int packetNumberStart, int packetNumberLength, + QuicTLSEngine tlsEngine, int headersMask) throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(keySpace, true, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void maskPacketNumber(ByteBuffer buffer, int packetNumberStart, int packetNumberLength, ByteBuffer mask) { + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(packetNumberStart + i, (byte)(buffer.get(packetNumberStart + i) ^ mask.get())); + } + } + + // returns a QuicTLSEngine with only Quic version 1 enabled + private QuicTLSEngine getQuicV1Engine(SSLContext context, boolean mode) { + final QuicTLSContext quicTLSContext = new QuicTLSContext(context); + final QuicTLSEngine engine = quicTLSContext.createEngine(); + engine.setUseClientMode(mode); + return engine; + } +} diff --git a/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineBadParametersTest.java b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineBadParametersTest.java new file mode 100644 index 00000000000..e6a99caebad --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineBadParametersTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportErrors; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build jdk.test.lib.net.SimpleSSLContext + * @summary Verify that QuicTransportExceptions thrown by transport parameter consumer + * are propagated to the QuicTLSEngine user + * @run junit/othervm QuicTLSEngineBadParametersTest + */ +public class QuicTLSEngineBadParametersTest { + + @Test + void testServerConsumerExceptionPropagated() throws IOException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + QuicTransportException ex = + new QuicTransportException("", null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + serverEngine.setRemoteQuicTransportParametersConsumer(p -> { throw ex; }); + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + try { + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertSame(ex, e, "Incorrect exception caught"); + } + } + + @Test + void testClientConsumerExceptionPropagated() throws IOException, QuicTransportException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + QuicTransportException ex = + new QuicTransportException("", null, 0, QuicTransportErrors.TRANSPORT_PARAMETER_ERROR); + clientEngine.setRemoteQuicTransportParametersConsumer(p -> { throw ex; }); + + // client hello + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + assertFalse(cTOs.hasRemaining()); + // server hello + ByteBuffer sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, sTOc); + assertFalse(sTOc.hasRemaining()); + // encrypted extensions + sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE); + try { + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE, sTOc); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertSame(ex, e, "Incorrect exception caught"); + } + + } + + private static QuicTLSEngine createServerEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine(); + engine.setUseClientMode(false); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + + private static QuicTLSEngine createClientEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine("localhost", 1234); + engine.setUseClientMode(true); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + +} diff --git a/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineFailedALPNTest.java b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineFailedALPNTest.java new file mode 100644 index 00000000000..fa7df3bc5c3 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineFailedALPNTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build jdk.test.lib.net.SimpleSSLContext + * @summary Verify that a missing ALPN extension results in no_application_protocol alert + * @run junit/othervm QuicTLSEngineFailedALPNTest + */ +public class QuicTLSEngineFailedALPNTest { + + @Test + void testServerRequiresALPN() throws IOException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + try { + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x0178, e.getErrorCode(), "Unexpected error code"); + } + } + + @Test + void testClientRequiresALPN() throws IOException, QuicTransportException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + SSLParameters params = clientEngine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + clientEngine.setSSLParameters(params); + // client hello + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + assertFalse(cTOs.hasRemaining()); + // server hello + ByteBuffer sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, sTOc); + assertFalse(sTOc.hasRemaining()); + // encrypted extensions + sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE); + try { + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE, sTOc); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x0178, e.getErrorCode(), "Unexpected error code"); + } + + } + + private static QuicTLSEngine createServerEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine(); + engine.setUseClientMode(false); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(params-> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + + private static QuicTLSEngine createClientEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine("localhost", 1234); + engine.setUseClientMode(true); + engine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + engine.setRemoteQuicTransportParametersConsumer(params-> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + +} diff --git a/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineMissingParametersTest.java b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineMissingParametersTest.java new file mode 100644 index 00000000000..da9f06785a2 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/QuicTLSEngineMissingParametersTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import jdk.internal.net.quic.QuicVersion; +import jdk.test.lib.net.SimpleSSLContext; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build jdk.test.lib.net.SimpleSSLContext + * @summary Verify that a missing transport parameters extension results in missing_extension alert + * @run junit/othervm QuicTLSEngineMissingParametersTest + */ +public class QuicTLSEngineMissingParametersTest { + + @Test + void testServerRequiresTransportParameters() throws IOException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + try { + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x016d, e.getErrorCode(), "Unexpected error code"); + } + } + + @Test + void testClientRequiresTransportParameters() throws IOException, QuicTransportException { + SSLContext ctx = SimpleSSLContext.getContext("TLSv1.3"); + QuicTLSContext qctx = new QuicTLSContext(ctx); + QuicTLSEngine clientEngine = createClientEngine(qctx); + QuicTLSEngine serverEngine = createServerEngine(qctx); + clientEngine.setLocalQuicTransportParameters(ByteBuffer.allocate(0)); + // client hello + ByteBuffer cTOs = clientEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + serverEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, cTOs); + assertFalse(cTOs.hasRemaining()); + // server hello + ByteBuffer sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL); + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.INITIAL, sTOc); + assertFalse(sTOc.hasRemaining()); + // encrypted extensions + sTOc = serverEngine.getHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE); + try { + clientEngine.consumeHandshakeBytes(QuicTLSEngine.KeySpace.HANDSHAKE, sTOc); + fail("Expected exception not thrown"); + } catch (QuicTransportException e) { + assertEquals(0x016d, e.getErrorCode(), "Unexpected error code"); + } + + } + + private static QuicTLSEngine createServerEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine(); + engine.setUseClientMode(false); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + + private static QuicTLSEngine createClientEngine(QuicTLSContext qctx) { + QuicTLSEngine engine = qctx.createEngine("localhost", 1234); + engine.setUseClientMode(true); + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(new String[] { "test" }); + engine.setSSLParameters(params); + engine.setRemoteQuicTransportParametersConsumer(p -> { }); + engine.versionNegotiated(QuicVersion.QUIC_V1); + return engine; + } + +} diff --git a/test/jdk/java/net/httpclient/quic/tls/Quicv2PacketEncryptionTest.java b/test/jdk/java/net/httpclient/quic/tls/Quicv2PacketEncryptionTest.java new file mode 100644 index 00000000000..a506495ed5e --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/Quicv2PacketEncryptionTest.java @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.quic.QuicKeyUnavailableException; +import jdk.internal.net.quic.QuicTLSContext; +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicTransportException; +import org.testng.annotations.Test; +import sun.security.ssl.QuicTLSEngineImpl; +import sun.security.ssl.QuicTLSEngineImplAccessor; + +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.SSLContext; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.function.IntFunction; + +import static jdk.internal.net.quic.QuicVersion.QUIC_V2; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +/** + * @test + * @library /test/lib + * @modules java.base/sun.security.ssl + * java.base/jdk.internal.net.quic + * @build java.base/sun.security.ssl.QuicTLSEngineImplAccessor + * @summary known-answer test for packet encryption and decryption with Quic v2 + * @run testng/othervm Quicv2PacketEncryptionTest + */ +public class Quicv2PacketEncryptionTest { + + // RFC 9369, appendix A + private static final String INITIAL_DCID = "8394c8f03e515708"; + // section A.2 + // header includes 4-byte packet number 2 + private static final String INITIAL_C_HEADER = "d36b3343cf088394c8f03e5157080000449e00000002"; + private static final int INITIAL_C_PAYLOAD_OFFSET = INITIAL_C_HEADER.length() / 2; + private static final int INITIAL_C_PN_OFFSET = INITIAL_C_PAYLOAD_OFFSET - 4; + private static final int INITIAL_C_PN = 2; + // payload is zero-padded to 1162 bytes, not shown here + private static final String INITIAL_C_PAYLOAD = + "060040f1010000ed0303ebf8fa56f129"+"39b9584a3896472ec40bb863cfd3e868" + + "04fe3a47f06a2b69484c000004130113"+"02010000c000000010000e00000b6578" + + "616d706c652e636f6dff01000100000a"+"00080006001d00170018001000070005" + + "04616c706e0005000501000000000033"+"00260024001d00209370b2c9caa47fba" + + "baf4559fedba753de171fa71f50f1ce1"+"5d43e994ec74d748002b000302030400" + + "0d0010000e0403050306030203080408"+"050806002d00020101001c0002400100" + + "3900320408ffffffffffffffff050480"+"00ffff07048000ffff08011001048000" + + "75300901100f088394c8f03e51570806"+"048000ffff"; + private static final int INITIAL_C_PAYLOAD_LENGTH = 1162; + private static final String ENCRYPTED_C_PAYLOAD = + "d76b3343cf088394c8f03e5157080000"+"449ea0c95e82ffe67b6abcdb4298b485" + + "dd04de806071bf03dceebfa162e75d6c"+"96058bdbfb127cdfcbf903388e99ad04" + + "9f9a3dd4425ae4d0992cfff18ecf0fdb"+"5a842d09747052f17ac2053d21f57c5d" + + "250f2c4f0e0202b70785b7946e992e58"+"a59ac52dea6774d4f03b55545243cf1a" + + "12834e3f249a78d395e0d18f4d766004"+"f1a2674802a747eaa901c3f10cda5500" + + "cb9122faa9f1df66c392079a1b40f0de"+"1c6054196a11cbea40afb6ef5253cd68" + + "18f6625efce3b6def6ba7e4b37a40f77"+"32e093daa7d52190935b8da58976ff33" + + "12ae50b187c1433c0f028edcc4c2838b"+"6a9bfc226ca4b4530e7a4ccee1bfa2a3" + + "d396ae5a3fb512384b2fdd851f784a65"+"e03f2c4fbe11a53c7777c023462239dd" + + "6f7521a3f6c7d5dd3ec9b3f233773d4b"+"46d23cc375eb198c63301c21801f6520" + + "bcfb7966fc49b393f0061d974a2706df"+"8c4a9449f11d7f3d2dcbb90c6b877045" + + "636e7c0c0fe4eb0f697545460c806910"+"d2c355f1d253bc9d2452aaa549e27a1f" + + "ac7cf4ed77f322e8fa894b6a83810a34"+"b361901751a6f5eb65a0326e07de7c12" + + "16ccce2d0193f958bb3850a833f7ae43"+"2b65bc5a53975c155aa4bcb4f7b2c4e5" + + "4df16efaf6ddea94e2c50b4cd1dfe060"+"17e0e9d02900cffe1935e0491d77ffb4" + + "fdf85290fdd893d577b1131a610ef6a5"+"c32b2ee0293617a37cbb08b847741c3b" + + "8017c25ca9052ca1079d8b78aebd4787"+"6d330a30f6a8c6d61dd1ab5589329de7" + + "14d19d61370f8149748c72f132f0fc99"+"f34d766c6938597040d8f9e2bb522ff9" + + "9c63a344d6a2ae8aa8e51b7b90a4a806"+"105fcbca31506c446151adfeceb51b91" + + "abfe43960977c87471cf9ad4074d30e1"+"0d6a7f03c63bd5d4317f68ff325ba3bd" + + "80bf4dc8b52a0ba031758022eb025cdd"+"770b44d6d6cf0670f4e990b22347a7db" + + "848265e3e5eb72dfe8299ad7481a4083"+"22cac55786e52f633b2fb6b614eaed18" + + "d703dd84045a274ae8bfa73379661388"+"d6991fe39b0d93debb41700b41f90a15" + + "c4d526250235ddcd6776fc77bc97e7a4"+"17ebcb31600d01e57f32162a8560cacc" + + "7e27a096d37a1a86952ec71bd89a3e9a"+"30a2a26162984d7740f81193e8238e61" + + "f6b5b984d4d3dfa033c1bb7e4f0037fe"+"bf406d91c0dccf32acf423cfa1e70710" + + "10d3f270121b493ce85054ef58bada42"+"310138fe081adb04e2bd901f2f13458b" + + "3d6758158197107c14ebb193230cd115"+"7380aa79cae1374a7c1e5bbcb80ee23e" + + "06ebfde206bfb0fcbc0edc4ebec30966"+"1bdd908d532eb0c6adc38b7ca7331dce" + + "8dfce39ab71e7c32d318d136b6100671"+"a1ae6a6600e3899f31f0eed19e3417d1" + + "34b90c9058f8632c798d4490da498730"+"7cba922d61c39805d072b589bd52fdf1" + + "e86215c2d54e6670e07383a27bbffb5a"+"ddf47d66aa85a0c6f9f32e59d85a44dd" + + "5d3b22dc2be80919b490437ae4f36a0a"+"e55edf1d0b5cb4e9a3ecabee93dfc6e3" + + "8d209d0fa6536d27a5d6fbb17641cde2"+"7525d61093f1b28072d111b2b4ae5f89" + + "d5974ee12e5cf7d5da4d6a31123041f3"+"3e61407e76cffcdcfd7e19ba58cf4b53" + + "6f4c4938ae79324dc402894b44faf8af"+"bab35282ab659d13c93f70412e85cb19" + + "9a37ddec600545473cfb5a05e08d0b20"+"9973b2172b4d21fb69745a262ccde96b" + + "a18b2faa745b6fe189cf772a9f84cbfc"; + // section A.3 + // header includes 2-byte packet number 1 + private static final String INITIAL_S_HEADER = "d16b3343cf0008f067a5502a4262b50040750001"; + private static final int INITIAL_S_PAYLOAD_OFFSET = INITIAL_S_HEADER.length() / 2; + private static final int INITIAL_S_PN_OFFSET = INITIAL_S_PAYLOAD_OFFSET - 2; + private static final int INITIAL_S_PN = 1; + // complete packet, no padding + private static final String INITIAL_S_PAYLOAD = + "02000000000600405a020000560303ee"+"fce7f7b37ba1d1632e96677825ddf739" + + "88cfc79825df566dc5430b9a045a1200"+"130100002e00330024001d00209d3c94" + + "0d89690b84d08a60993c144eca684d10"+"81287c834d5311bcf32bb9da1a002b00" + + "020304"; + private static final int INITIAL_S_PAYLOAD_LENGTH = INITIAL_S_PAYLOAD.length() / 2; + private static final String ENCRYPTED_S_PAYLOAD = + "dc6b3343cf0008f067a5502a4262b500"+"4075d92faaf16f05d8a4398c47089698" + + "baeea26b91eb761d9b89237bbf872630"+"17915358230035f7fd3945d88965cf17" + + "f9af6e16886c61bfc703106fbaf3cb4c"+"fa52382dd16a393e42757507698075b2" + + "c984c707f0a0812d8cd5a6881eaf21ce"+"da98f4bd23f6fe1a3e2c43edd9ce7ca8" + + "4bed8521e2e140"; + + // section A.4 + private static final String SIGNED_RETRY = + "cf6b3343cf0008f067a5502a4262b574"+"6f6b656ec8646ce8bfe33952d9555436" + + "65dcc7b6"; + + // section A.5 + public static final String ONERTT_SECRET = "9ac312a7f877468ebe69422748ad00a1" + + "5443f18203a07d6060f688f30f21632b"; + private static final String ONERTT_HEADER = "4200bff4"; + private static final int ONERTT_PAYLOAD_OFFSET = ONERTT_HEADER.length() / 2; + private static final int ONERTT_PN_OFFSET = 1; + private static final int ONERTT_PN = 654360564; + // payload is zero-padded to 1162 bytes, not shown here + private static final String ONERTT_PAYLOAD = + "01"; + private static final int ONERTT_PAYLOAD_LENGTH = + ONERTT_PAYLOAD.length() / 2; + private static final String ENCRYPTED_ONERTT_PAYLOAD = + "5558b1c60ae7b6b932bc27d786f4bc2bb20f2162ba"; + + private static final class FixedHeaderContent implements IntFunction { + private final ByteBuffer header; + private FixedHeaderContent(ByteBuffer header) { + this.header = header; + } + + @Override + public ByteBuffer apply(final int keyphase) { + // ignore keyphase + return this.header; + } + } + + @Test + public void testEncryptClientInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V2, dcid); + final int packetLen = INITIAL_C_PAYLOAD_OFFSET + INITIAL_C_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_C_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_C_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_C_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_C_PAYLOAD_OFFSET, INITIAL_C_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_C_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, clientEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_C_PAYLOAD); + } + + @Test + public void testDecryptClientInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV2Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V2, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, src, INITIAL_C_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_C_HEADER + INITIAL_C_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptClientInitialPacketBadTag() throws Exception { + QuicTLSEngine serverEngine = getQuicV2Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V2, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_C_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_C_PN_OFFSET, INITIAL_C_PAYLOAD_OFFSET - INITIAL_C_PN_OFFSET, serverEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_C_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_C_PN, -1, src, INITIAL_C_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + @Test + public void testEncryptServerInitialPacket() throws Exception { + QuicTLSEngine serverEngine = getQuicV2Engine(SSLContext.getDefault(), false); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + serverEngine.deriveInitialKeys(QUIC_V2, dcid); + + final int packetLen = INITIAL_S_PAYLOAD_OFFSET + INITIAL_S_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(INITIAL_S_HEADER)); + packet.put(HexFormat.of().parseHex(INITIAL_S_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, INITIAL_S_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(INITIAL_S_PAYLOAD_OFFSET, INITIAL_S_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(INITIAL_S_PAYLOAD_OFFSET); + serverEngine.encryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, serverEngine, 0x0f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_S_PAYLOAD); + } + + @Test + public void testDecryptServerInitialPacket() throws Exception { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V2, dcid); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + + String expectedContents = INITIAL_S_HEADER + INITIAL_S_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptServerInitialPacketTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + clientEngine.deriveInitialKeys(QUIC_V2, dcid); + + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_S_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.INITIAL, packet, INITIAL_S_PN_OFFSET, INITIAL_S_PAYLOAD_OFFSET - INITIAL_S_PN_OFFSET, clientEngine, 0x0f); + src = packet.asReadOnlyBuffer(); + packet.position(INITIAL_S_PAYLOAD_OFFSET); + // must not throw + clientEngine.decryptPacket(QuicTLSEngine.KeySpace.INITIAL, INITIAL_S_PN, -1, src, INITIAL_S_PAYLOAD_OFFSET, packet); + } + + @Test + public void testSignRetry() throws NoSuchAlgorithmException, ShortBufferException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.allocate(SIGNED_RETRY.length() / 2); + packet.put(HexFormat.of().parseHex(SIGNED_RETRY), 0, SIGNED_RETRY.length() / 2 - 16); + + ByteBuffer src = packet.asReadOnlyBuffer(); + src.limit(src.position()); + src.position(0); + + clientEngine.signRetryPacket(QUIC_V2, dcid, src, packet); + + assertEquals(HexFormat.of().formatHex(packet.array()), SIGNED_RETRY); + } + + @Test + public void testVerifyRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + clientEngine.verifyRetryPacket(QUIC_V2, dcid, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testVerifyBadRetry() throws NoSuchAlgorithmException, AEADBadTagException, QuicTransportException { + QuicTLSEngine clientEngine = getQuicV2Engine(SSLContext.getDefault(), true); + ByteBuffer dcid = ByteBuffer.wrap(HexFormat.of().parseHex(INITIAL_DCID)); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(SIGNED_RETRY)); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + clientEngine.verifyRetryPacket(QUIC_V2, dcid, packet); + fail("Verification should have failed"); + } + + @Test + public void testEncryptChaCha() throws Exception { + QuicTLSEngineImpl clientEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), true); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, clientEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", true); + + final int packetLen = ONERTT_PAYLOAD_OFFSET + ONERTT_PAYLOAD_LENGTH + 16; + final ByteBuffer packet = ByteBuffer.allocate(packetLen); + packet.put(HexFormat.of().parseHex(ONERTT_HEADER)); + packet.put(HexFormat.of().parseHex(ONERTT_PAYLOAD)); + + final ByteBuffer header = packet.slice(0, ONERTT_PAYLOAD_OFFSET).asReadOnlyBuffer(); + final ByteBuffer payload = packet.slice(ONERTT_PAYLOAD_OFFSET, ONERTT_PAYLOAD_LENGTH).asReadOnlyBuffer(); + + packet.position(ONERTT_PAYLOAD_OFFSET); + clientEngine.encryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN , new FixedHeaderContent(header), payload, packet); + protect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, clientEngine, 0x1f); + + assertEquals(HexFormat.of().formatHex(packet.array()), ENCRYPTED_ONERTT_PAYLOAD); + } + + @Test + public void testDecryptChaCha() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, 0, src, ONERTT_PAYLOAD_OFFSET, packet); + + String expectedContents = ONERTT_HEADER + ONERTT_PAYLOAD; + + assertEquals(HexFormat.of().formatHex(packet.array()).substring(0, expectedContents.length()), expectedContents); + } + + @Test + public void testDecryptChaChaTwice() throws Exception { + // verify that decrypting the same packet twice does not throw + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + // attempt 1 + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + final int keyphase = 0; + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyphase, src, ONERTT_PAYLOAD_OFFSET, packet); + + // attempt 2 + packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + // must not throw + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + // must not throw + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, keyphase, src, ONERTT_PAYLOAD_OFFSET, packet); + } + + @Test(expectedExceptions = AEADBadTagException.class) + public void testDecryptChaChaBadTag() throws Exception { + QuicTLSEngineImpl serverEngine = (QuicTLSEngineImpl) getQuicV2Engine(SSLContext.getDefault(), false); + // mark the TLS handshake as FINISHED + QuicTLSEngineImplAccessor.completeHandshake(serverEngine); + SecretKey key = new SecretKeySpec(HexFormat.of().parseHex(ONERTT_SECRET), 0, 32, "ChaCha20-Poly1305"); + QuicTLSEngineImplAccessor.testDeriveOneRTTKeys(QUIC_V2, serverEngine, key, key, "TLS_CHACHA20_POLY1305_SHA256", false); + + ByteBuffer packet = ByteBuffer.wrap(HexFormat.of().parseHex(ENCRYPTED_ONERTT_PAYLOAD)); + unprotect(QuicTLSEngine.KeySpace.ONE_RTT, packet, ONERTT_PN_OFFSET, ONERTT_PAYLOAD_OFFSET - ONERTT_PN_OFFSET, serverEngine, 0x1f); + ByteBuffer src = packet.asReadOnlyBuffer(); + packet.position(ONERTT_PAYLOAD_OFFSET); + + // change one byte of AEAD tag + packet.put(packet.limit() - 1, (byte)0); + + serverEngine.decryptPacket(QuicTLSEngine.KeySpace.ONE_RTT, ONERTT_PN, 0, src, ONERTT_PAYLOAD_OFFSET, packet); + fail("Decryption should have failed"); + } + + + private void protect(QuicTLSEngine.KeySpace space, ByteBuffer buffer, int packetNumberStart, + int packetNumberLength, QuicTLSEngine tlsEngine, int headersMask) + throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(space, false, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void unprotect(QuicTLSEngine.KeySpace keySpace, ByteBuffer buffer, int packetNumberStart, + int packetNumberLength, QuicTLSEngine tlsEngine, int headersMask) + throws QuicKeyUnavailableException, QuicTransportException { + ByteBuffer sample = buffer.slice(packetNumberStart + 4, 16); + ByteBuffer encryptedSample = tlsEngine.computeHeaderProtectionMask(keySpace, true, sample); + byte headers = buffer.get(0); + headers ^= encryptedSample.get() & headersMask; + buffer.put(0, headers); + maskPacketNumber(buffer, packetNumberStart, packetNumberLength, encryptedSample); + } + + private void maskPacketNumber(ByteBuffer buffer, int packetNumberStart, int packetNumberLength, ByteBuffer mask) { + for (int i = 0; i < packetNumberLength; i++) { + buffer.put(packetNumberStart + i, (byte)(buffer.get(packetNumberStart + i) ^ mask.get())); + } + } + + // returns a QuicTLSEngine with only Quic version 2 enabled + private QuicTLSEngine getQuicV2Engine(SSLContext context, boolean mode) { + final QuicTLSContext quicTLSContext = new QuicTLSContext(context); + final QuicTLSEngine engine = quicTLSContext.createEngine(); + engine.setUseClientMode(mode); + return engine; + } +} diff --git a/test/jdk/java/net/httpclient/quic/tls/java.base/sun/security/ssl/QuicTLSEngineImplAccessor.java b/test/jdk/java/net/httpclient/quic/tls/java.base/sun/security/ssl/QuicTLSEngineImplAccessor.java new file mode 100644 index 00000000000..e9a57310e0b --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/tls/java.base/sun/security/ssl/QuicTLSEngineImplAccessor.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.security.ssl; + +import javax.crypto.SecretKey; +import java.io.IOException; +import java.lang.reflect.Field; +import java.security.GeneralSecurityException; + +import jdk.internal.net.quic.QuicTLSEngine; +import jdk.internal.net.quic.QuicVersion; + +public final class QuicTLSEngineImplAccessor { + // visible for testing + public static void testDeriveOneRTTKeys(QuicVersion version, + QuicTLSEngineImpl engine, + SecretKey client_application_traffic_secret_0, + SecretKey server_application_traffic_secret_0, + String negotiatedCipherSuite, + boolean clientMode) + throws IOException, GeneralSecurityException + { + engine.deriveOneRTTKeys(version, client_application_traffic_secret_0, + server_application_traffic_secret_0, + CipherSuite.valueOf(negotiatedCipherSuite), + clientMode); + } + + // visible for testing + public static void completeHandshake(final QuicTLSEngineImpl engine) { + try { + final Field f = QuicTLSEngineImpl.class.getDeclaredField("handshakeState"); + f.setAccessible(true); + f.set(engine, QuicTLSEngine.HandshakeState.HANDSHAKE_CONFIRMED); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/test/jdk/java/net/httpclient/ssltest/CertificateTest.java b/test/jdk/java/net/httpclient/ssltest/CertificateTest.java index 96ba5c05ed3..1d0502c9ce3 100644 --- a/test/jdk/java/net/httpclient/ssltest/CertificateTest.java +++ b/test/jdk/java/net/httpclient/ssltest/CertificateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -39,8 +39,9 @@ import jdk.test.lib.security.SSLContextBuilder; /* * @test - * @library /test/lib - * @build Server CertificateTest + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build Server CertificateTest jdk.httpclient.test.lib.common.TestServerConfigurator + * @modules java.net.http/jdk.internal.net.http.common * @run main/othervm CertificateTest GOOD_CERT expectSuccess * @run main/othervm CertificateTest BAD_CERT expectFailure * @run main/othervm diff --git a/test/jdk/java/net/httpclient/ssltest/Server.java b/test/jdk/java/net/httpclient/ssltest/Server.java index fc954016991..d2d98268e14 100644 --- a/test/jdk/java/net/httpclient/ssltest/Server.java +++ b/test/jdk/java/net/httpclient/ssltest/Server.java @@ -22,6 +22,7 @@ */ import com.sun.net.httpserver.*; +import jdk.httpclient.test.lib.common.TestServerConfigurator; import java.io.*; import java.net.InetAddress; @@ -44,9 +45,9 @@ public class Server { // response with a short text string. public Server(SSLContext ctx) throws Exception { initLogger(); - Configurator cfg = new Configurator(ctx); InetSocketAddress addr = new InetSocketAddress( InetAddress.getLoopbackAddress(), 0); + Configurator cfg = new Configurator(addr.getAddress(), ctx); server = HttpsServer.create(addr, 10); server.setHttpsConfigurator(cfg); server.createContext("/", new MyHandler()); @@ -94,15 +95,20 @@ public class Server { } class Configurator extends HttpsConfigurator { - public Configurator(SSLContext ctx) throws Exception { + private final InetAddress serverAddr; + + public Configurator(InetAddress addr, SSLContext ctx) throws Exception { super(ctx); + this.serverAddr = addr; } + @Override public void configure(HttpsParameters params) { SSLParameters p = getSSLContext().getDefaultSSLParameters(); for (String cipher : p.getCipherSuites()) System.out.println("Cipher: " + cipher); System.err.println("Params = " + p); + TestServerConfigurator.addSNIMatcher(this.serverAddr, p); params.setSSLParameters(p); } } diff --git a/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.java b/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.java index 322a3e3789b..2e364461793 100644 --- a/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.java +++ b/test/jdk/java/net/httpclient/ssltest/TlsVersionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,14 +32,19 @@ import jdk.test.lib.net.URIBuilder; import jdk.test.lib.security.KeyEntry; import jdk.test.lib.security.KeyStoreUtils; import jdk.test.lib.security.SSLContextBuilder; + +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static java.net.http.HttpClient.Version.HTTP_2; +import static java.net.http.HttpClient.Version.HTTP_3; import static java.net.http.HttpResponse.BodyHandlers.ofString; import static java.net.http.HttpClient.Builder.NO_PROXY; /* * @test * @bug 8239594 8239595 - * @library /test/lib - * @build Server TlsVersionTest + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build Server TlsVersionTest jdk.httpclient.test.lib.common.TestServerConfigurator + * @modules java.net.http/jdk.internal.net.http.common * @run main/othervm * -Djdk.internal.httpclient.disableHostnameVerification * TlsVersionTest false @@ -121,11 +126,12 @@ public class TlsVersionTest { System.out.println("Making request to " + serverURI.getPath()); SSLContext ctx = getClientSSLContext(cert); HttpClient client = HttpClient.newBuilder() + .version(HTTP_2) .proxy(NO_PROXY) .sslContext(ctx) .build(); - for (var version : List.of(HttpClient.Version.HTTP_2, HttpClient.Version.HTTP_1_1)) { + for (var version : List.of(HTTP_3, HTTP_2, HTTP_1_1)) { HttpRequest request = HttpRequest.newBuilder(serverURI) .version(version) .GET() diff --git a/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java b/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java index 5065563c9ec..6d242a596e7 100644 --- a/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java +++ b/test/jdk/java/net/httpclient/websocket/HandshakeUrlEncodingTest.java @@ -205,4 +205,3 @@ public class HandshakeUrlEncodingTest { } } } - diff --git a/test/jdk/java/net/httpclient/websocket/ReaderDriver.java b/test/jdk/java/net/httpclient/websocket/ReaderDriver.java index 61829a5a010..0b874792b4f 100644 --- a/test/jdk/java/net/httpclient/websocket/ReaderDriver.java +++ b/test/jdk/java/net/httpclient/websocket/ReaderDriver.java @@ -25,6 +25,6 @@ * @test * @bug 8159053 * @modules java.net.http/jdk.internal.net.http.websocket:open - * @run testng/othervm --add-reads java.net.http=ALL-UNNAMED java.net.http/jdk.internal.net.http.websocket.ReaderTest + * @run testng/othervm/timeout=240 --add-reads java.net.http=ALL-UNNAMED java.net.http/jdk.internal.net.http.websocket.ReaderTest */ public final class ReaderDriver { } diff --git a/test/jdk/java/net/httpclient/whitebox/AltSvcFrameTest.java b/test/jdk/java/net/httpclient/whitebox/AltSvcFrameTest.java new file mode 100644 index 00000000000..949c950fd76 --- /dev/null +++ b/test/jdk/java/net/httpclient/whitebox/AltSvcFrameTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Optional; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; + +import jdk.httpclient.test.lib.http2.BodyOutputStream; +import jdk.httpclient.test.lib.http2.Http2Handler; +import jdk.httpclient.test.lib.http2.Http2TestExchange; +import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import jdk.httpclient.test.lib.http2.Http2TestServerConnection; +import jdk.internal.net.http.AltServicesRegistry; +import jdk.internal.net.http.HttpClientAccess; +import jdk.internal.net.http.common.HttpHeadersBuilder; +import jdk.internal.net.http.frame.AltSvcFrame; +import jdk.test.lib.net.SimpleSSLContext; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; +import static java.net.http.HttpResponse.BodyHandlers.ofString; +import static jdk.internal.net.http.AltServicesRegistry.AltService; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/* + * @test + * @summary This test verifies alt-svc registry updation for frames + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build java.net.http/jdk.internal.net.http.HttpClientAccess + * jdk.httpclient.test.lib.http2.Http2TestServer + * @modules java.net.http/jdk.internal.net.http + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.util + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.logging + * java.base/sun.net.www.http + * java.base/sun.net.www + * java.base/sun.net + * @run testng/othervm + * -Dtest.requiresHost=true + * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.internal.httpclient.disableHostnameVerification + * -Djdk.internal.httpclient.debug=true + * AltSvcFrameTest + */ + + +public class AltSvcFrameTest { + + private static final String IGNORED_HOST = "www.should-be-ignored.com"; + private static final String ALT_SVC_TO_IGNORE = "h3=\"" + IGNORED_HOST + ":443\""; + + private static final String ACCEPTED_HOST = "www.example.com"; + private static final String ALT_SVC_TO_ACCEPT = "h3=\"" + ACCEPTED_HOST + ":443\""; + + private static final String STREAM_0_ACCEPTED_HOST = "jdk.java.net"; + private static final String STREAM_0_ALT_SVC_TO_ACCEPT = "h3=\"" + STREAM_0_ACCEPTED_HOST + ":443\""; + + private static final String FOO_BAR_ORIGIN = "https://www.foo-bar.hello-world:443"; + + static Http2TestServer https2Server; + static String https2URI; + static HttpClient client; + static SSLContext server; + + @BeforeTest + public void setUp() throws Exception { + server = SimpleSSLContext.getContext("TLS"); + getRegistry(); + https2Server = new Http2TestServer("localhost", true, server); + https2Server.addHandler(new AltSvcFrameTestHandler(), "/"); + https2Server.setExchangeSupplier(AltSvcFrameTest.CFTHttp2TestExchange::new); + https2Server.start(); + https2URI = "https://" + https2Server.serverAuthority() + "/"; + + + } + + static AltServicesRegistry getRegistry() { + client = HttpClient.newBuilder() + .sslContext(server) + .version(HttpClient.Version.HTTP_2) + .build(); + return HttpClientAccess.getRegistry(client); + } + + /* + * Verify handling of alt-svc frame on a stream other than stream 0 + */ + @Test + public void testNonStream0AltSvcFrame() throws URISyntaxException, IOException, InterruptedException { + AltServicesRegistry registry = getRegistry(); + HttpRequest request = HttpRequest.newBuilder(new URI(https2URI)) + .GET() + .build(); + HttpResponse response = client.send(request, ofString()); + assertEquals(response.statusCode(), 200, "unexpected response code"); + final List services = registry.lookup(URI.create(https2URI), "h3").toList(); + System.out.println("Alt services in registry for " + https2URI + " = " + services); + final boolean hasExpectedAltSvc = services.stream().anyMatch( + alt -> alt.alpn().equals("h3") + && alt.host().contains(ACCEPTED_HOST)); + assertTrue(hasExpectedAltSvc, "missing entry in alt service registry for origin: " + https2URI); + } + + /* + * Verify handling of alt-svc frame on stream 0 of the connection + */ + @Test + public void testStream0AltSvcFrame() throws URISyntaxException, IOException, InterruptedException { + AltServicesRegistry registry = getRegistry(); + HttpRequest request = HttpRequest.newBuilder(new URI(https2URI + "?altsvc-on-stream-0")) + .GET() + .build(); + HttpResponse response = client.send(request, ofString()); + assertEquals(response.statusCode(), 200, "unexpected response code"); + final List services = registry.lookup( + URI.create(FOO_BAR_ORIGIN), "h3").toList(); + System.out.println("Alt services in registry for " + FOO_BAR_ORIGIN + " = " + services); + final boolean containsIgnoredHost = services.stream().anyMatch( + alt -> alt.alpn().equals("h3") + && alt.host().contains(IGNORED_HOST)); + assertFalse(containsIgnoredHost, "unexpected alt service in the registry for origin: " + + FOO_BAR_ORIGIN); + + final List svcs = registry.lookup(URI.create(https2URI), "h3").toList(); + System.out.println("Alt services in registry for " + https2URI + " = " + svcs); + final boolean hasExpectedAltSvc = svcs.stream().anyMatch( + alt -> alt.alpn().equals("h3") + && alt.host().contains(STREAM_0_ACCEPTED_HOST)); + assertTrue(hasExpectedAltSvc, "missing entry in alt service registry for origin: " + https2URI); + } + + static class AltSvcFrameTestHandler implements Http2Handler { + + @Override + public void handle(Http2TestExchange t) throws IOException { + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + byte[] bytes = is.readAllBytes(); + t.sendResponseHeaders(200, bytes.length); + os.write(bytes); + } + } + } + + // A custom Http2TestExchangeImpl that overrides sendResponseHeaders to + // allow headers to be sent with AltSvcFrame. + static class CFTHttp2TestExchange extends Http2TestExchangeImpl { + + CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders, + HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, + SSLSession sslSession, BodyOutputStream os, + Http2TestServerConnection conn, boolean pushAllowed) { + super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, + os, conn, pushAllowed); + + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + final String reqQuery = getRequestURI().getQuery(); + if (reqQuery != null && reqQuery.contains("altsvc-on-stream-0")) { + final InetSocketAddress addr = this.getLocalAddress(); + final String connectionOrigin = "https://" + addr.getAddress().getHostAddress() + + ":" + addr.getPort(); + // send one alt-svc on stream 0 with the same Origin as the connection's Origin + enqueueAltSvcFrame(new AltSvcFrame(0, 0, + // the Origin for which the alt-svc is being advertised + Optional.of(connectionOrigin), + STREAM_0_ALT_SVC_TO_ACCEPT)); + + // send one alt-svc on stream 0 with a different Origin than the connection's Origin + enqueueAltSvcFrame(new AltSvcFrame(0, 0, + // the Origin for which the alt-svc is being advertised + Optional.of(FOO_BAR_ORIGIN), + ALT_SVC_TO_IGNORE)); + } else { + // send alt-svc on non-zero stream id. + // for non-zero stream id, as per spec, the origin is inferred from the stream's origin + // by the HTTP client + enqueueAltSvcFrame(new AltSvcFrame(streamid, 0, Optional.empty(), ALT_SVC_TO_ACCEPT)); + } + super.sendResponseHeaders(rCode, responseLength); + System.out.println("Sent response headers " + rCode); + } + + private void enqueueAltSvcFrame(final AltSvcFrame frame) throws IOException { + System.out.println("enqueueing Alt-Svc frame: " + frame); + conn.addToOutputQ(frame); + } + } +} diff --git a/test/jdk/java/net/httpclient/whitebox/AltSvcRegistryTest.java b/test/jdk/java/net/httpclient/whitebox/AltSvcRegistryTest.java new file mode 100644 index 00000000000..47e69338715 --- /dev/null +++ b/test/jdk/java/net/httpclient/whitebox/AltSvcRegistryTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.net.http.HttpClientAccess; +import jdk.internal.net.http.AltServicesRegistry; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.httpclient.test.lib.common.HttpServerAdapters; +import jdk.httpclient.test.lib.http2.Http2TestServer; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + + +import static jdk.internal.net.http.AltServicesRegistry.AltService; +import static java.net.http.HttpResponse.BodyHandlers.ofString; + +/* + * @test + * @summary This test verifies alt-svc registry updates + * @library /test/lib /test/jdk/java/net/httpclient/lib + * @build java.net.http/jdk.internal.net.http.HttpClientAccess + * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.httpclient.test.lib.http2.Http2TestServer + * @modules java.net.http/jdk.internal.net.http + * java.net.http/jdk.internal.net.http.common + * java.net.http/jdk.internal.net.http.frame + * java.net.http/jdk.internal.net.http.hpack + * java.base/jdk.internal.net.quic + * java.net.http/jdk.internal.net.http.quic + * java.net.http/jdk.internal.net.http.quic.packets + * java.net.http/jdk.internal.net.http.quic.frames + * java.net.http/jdk.internal.net.http.quic.streams + * java.net.http/jdk.internal.net.http.http3.streams + * java.net.http/jdk.internal.net.http.http3.frames + * java.net.http/jdk.internal.net.http.http3 + * java.net.http/jdk.internal.net.http.qpack + * java.net.http/jdk.internal.net.http.qpack.readers + * java.net.http/jdk.internal.net.http.qpack.writers + * java.logging + * java.base/sun.net.www.http + * java.base/sun.net.www + * java.base/sun.net + * java.base/jdk.internal.util + * @run testng/othervm + * -Dtest.requiresHost=true + * -Djdk.httpclient.HttpClient.log=headers + * -Djdk.internal.httpclient.disableHostnameVerification + * -Djdk.internal.httpclient.debug=true + * AltSvcRegistryTest + */ + + +public class AltSvcRegistryTest implements HttpServerAdapters { + + static HttpTestServer https2Server; + static String https2URI; + static HttpClient client; + static SSLContext server; + + @BeforeTest + public void setUp() throws Exception { + server = SimpleSSLContext.getContext("TLS"); + getRegistry(); + final ExecutorService executor = Executors.newCachedThreadPool(); + https2Server = HttpServerAdapters.HttpTestServer.of( + new Http2TestServer("localhost", true, 0, executor, 50, null, server, true)); + https2Server.addHandler(new AltSvcRegistryTestHandler("https", https2Server), "/"); + https2Server.start(); + https2URI = "https://" + https2Server.serverAuthority() + "/"; + + + } + + static AltServicesRegistry getRegistry() { + client = HttpClient.newBuilder() + .sslContext(server) + .version(HttpClient.Version.HTTP_2) + .build(); + return HttpClientAccess.getRegistry(client); + } + @Test + public void testAltSvcRegistry() throws URISyntaxException, IOException, InterruptedException { + AltServicesRegistry registry = getRegistry(); + HttpRequest request = HttpRequest.newBuilder(new URI(https2URI)) + .GET() + .build(); + HttpResponse response = client.send(request, ofString()); + assert response.statusCode() == 200; + List h3service = registry.lookup(URI.create(https2URI), "h3") + .toList(); + System.out.println("h3 services: " + h3service); + assert h3service.stream().anyMatch( alt -> alt.alpn().equals("h3") + && alt.host().equals("www.example.com") + && alt.port() == 443 + && alt.isPersist()); + assert h3service.stream().anyMatch( alt -> alt.alpn().equals("h3") + && alt.host().equals(request.uri().getHost()) + && alt.port() == 4567 + && !alt.isPersist()); + + List h34service = registry.lookup(URI.create(https2URI), "h3-34") + .toList(); + System.out.println("h3-34 services: " + h34service); + assert h34service.stream().noneMatch( alt -> alt.alpn().equals("h3-34")); + } + + static class AltSvcRegistryTestHandler implements HttpTestHandler { + final String scheme; + final HttpTestServer server; + + AltSvcRegistryTestHandler(String scheme, HttpTestServer server) { + this.scheme = scheme; + this.server = server; + } + + @Override + public void handle(HttpTestExchange t) throws IOException { + try (InputStream is = t.getRequestBody(); + OutputStream os = t.getResponseBody()) { + var altsvc = """ + h3-34=":5678", h3="www.example.com:443"; persist=1, h3=":4567"; persist=0""" ; + t.getResponseHeaders().addHeader("alt-svc", altsvc.trim()); + byte[] bytes = is.readAllBytes(); + t.sendResponseHeaders(200, 10); + os.write(bytes); + } + } + } +} diff --git a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/HttpClientAccess.java b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/HttpClientAccess.java new file mode 100644 index 00000000000..93cf04dbcc4 --- /dev/null +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/HttpClientAccess.java @@ -0,0 +1,10 @@ +package jdk.internal.net.http; + +import java.net.http.HttpClient; + +public class HttpClientAccess { + + public static AltServicesRegistry getRegistry(HttpClient client) { + return ((HttpClientFacade)client).impl.registry(); + } +} diff --git a/test/jdk/jdk/internal/net/http/quic/packets/QuicPacketNumbersTest.java b/test/jdk/jdk/internal/net/http/quic/packets/QuicPacketNumbersTest.java new file mode 100644 index 00000000000..3ec16bbc51c --- /dev/null +++ b/test/jdk/jdk/internal/net/http/quic/packets/QuicPacketNumbersTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.nio.ByteBuffer; +import jdk.internal.net.http.quic.packets.*; +import java.util.Arrays; + +/** + * @test + * @summary Unit encoding/decoding tests for the QUICPacketNumbers. + * @modules java.net.http/jdk.internal.net.http.quic.packets + */ +public class QuicPacketNumbersTest { + + public static void main(String[] args) throws Exception { + + // Test encoding logic. + // Test values from the upcoming QUIC RFC, Appendix A.2. + checkEncodePacketNumber(0xac5c02L, 0xabe8b3L, + new byte[]{(byte) 0x5c, (byte) 0x02}); + checkEncodePacketNumber(0xace8feL, 0xabe8bcL, + new byte[]{(byte) 0xac, (byte) 0xe8, (byte) 0xfe} + ); + + // Various checks for "None" encodings. + checkEncodePacketNumber(0x00L, -1L, + new byte[]{(byte) 0x00}); + checkEncodePacketNumber(0x05L, -1L, + new byte[]{(byte) 0x05}); + checkEncodePacketNumber(0x7eL, -1L, + new byte[]{(byte) 0x7E}); + checkEncodePacketNumber(0x7fL, -1L, + new byte[]{(byte) 0x00, (byte) 0x7f}); + checkEncodePacketNumber(0x80L, -1L, + new byte[]{(byte) 0x00, (byte) 0x80}); + checkEncodePacketNumber(0xffL, -1L, + new byte[]{(byte) 0x00, (byte) 0xff}); + checkEncodePacketNumber(0x100L, -1L, + new byte[]{(byte) 0x01, (byte) 0x00}); + + // Various checks for a packet 0 that has been ack'd. + checkEncodePacketNumber(0x7fL, 0L, + new byte[]{(byte) 0x7f}); + checkEncodePacketNumber(0x80L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80}); + checkEncodePacketNumber(0xffL, 0L, + new byte[]{(byte) 0x00, (byte) 0xff}); + checkEncodePacketNumber(0x100L, 0L, + new byte[]{(byte) 0x01, (byte) 0x00}); + checkEncodePacketNumber(0x7FFFL, 0L, + new byte[]{(byte) 0x7f, (byte) 0xFF}); + checkEncodePacketNumber(0x8000L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80, (byte) 0x00}); + checkEncodePacketNumber(0x7FFFFFL, 0L, + new byte[]{(byte) 0x7f, (byte) 0xff, (byte) 0xFF}); + checkEncodePacketNumber(0x800000L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80, (byte) 0x00, (byte) 0x00}); + checkEncodePacketNumber(0x7FFFFFFFL, 0L, + new byte[]{(byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xFF}); + + // Check some similar truncations. + checkEncodePacketNumber(0x0101L, 0x82L, + new byte[]{(byte) 0x01}); + checkEncodePacketNumber(0x0101L, 0x81L, + new byte[]{(byte) 0x01, (byte) 0x01}); + checkEncodePacketNumber(0x10001L, 0xFF82, + new byte[]{(byte) 0x01}); + checkEncodePacketNumber(0x10001L, 0xFF81, + new byte[]{(byte) 0x00, (byte) 0x01}); + checkEncodePacketNumber(0x1000001L, 0xFFFF82, + new byte[]{(byte) 0x01}); + checkEncodePacketNumber(0x1000001L, 0xFFFF81, + new byte[]{(byte) 0x00, (byte) 0x01}); + + // Check that > 4 bytes are not generated. + try { + checkEncodePacketNumber(0x80000000L, 0L, + new byte[]{(byte) 0x00, (byte) 0x80, (byte) 0x00, + (byte) 0x00, (byte) 0x00}); + throw new Exception("Shouldn't encode"); + } catch (RuntimeException e) { + System.out.println("Caught the right exception!"); + } + + // Test decoding logic. + + // Test values from the upcoming QUIC RFC, Appendix A.3. + checkDecodePacketNumber(0xa82f30eaL, + ByteBuffer.wrap(new byte[]{(byte) 0x9b, (byte) 0x32}), + 2, 0xa82f9b32L); + + // TBD: More test values + } + + public static void checkEncodePacketNumber( + long fullPN, long largestAcked, byte[] bytes) throws Exception { + + byte[] answer = QuicPacketNumbers.encodePacketNumber( + fullPN, largestAcked); + + if (!Arrays.equals(answer, bytes)) { + throw new Exception("Encoding Problem"); + } + } + + public static void checkDecodePacketNumber( + long largestPN, ByteBuffer buf, int headerNumBytes, + long answer) throws Exception { + + long result = QuicPacketNumbers.decodePacketNumber( + largestPN, buf, headerNumBytes); + + if (result != answer) { + throw new Exception("Decoding Problem"); + } + } +} From 0ba4141cb12414c08be88b37ea2a163aacbfa7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20H=C3=A4ssig?= Date: Mon, 22 Sep 2025 11:24:30 +0000 Subject: [PATCH 151/556] 8366878: Improve flags of compiler/loopopts/superword/TestAlignVectorFuzzer.java Co-authored-by: Emanuel Peter Reviewed-by: epeter, mchevalier --- .../superword/TestAlignVectorFuzzer.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java index d75db965ea3..019dad55b65 100644 --- a/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java +++ b/test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVectorFuzzer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,7 +30,19 @@ * @key randomness * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* + * compiler.loopopts.superword.TestAlignVectorFuzzer + */ + +/* + * @test id=CompileOnly + * @bug 8253191 + * @summary Fuzzing loops with different (random) init, limit, stride, scale etc. Do not force alignment. + * @modules java.base/jdk.internal.misc + * @library /test/lib + * @key randomness + * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions + * -XX:LoopUnrollLimit=250 + * -XX:CompileCommand=compileonly,compiler.loopopts.superword.TestAlignVectorFuzzer::* * compiler.loopopts.superword.TestAlignVectorFuzzer */ @@ -44,7 +56,6 @@ * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:+AlignVector -XX:+VerifyAlignVector * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* * compiler.loopopts.superword.TestAlignVectorFuzzer */ @@ -58,7 +69,6 @@ * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:+AlignVector -XX:+VerifyAlignVector * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* * -XX:ObjectAlignmentInBytes=16 * compiler.loopopts.superword.TestAlignVectorFuzzer */ @@ -73,7 +83,6 @@ * @run main/bootclasspath/othervm -XX:+IgnoreUnrecognizedVMOptions * -XX:+AlignVector -XX:+VerifyAlignVector * -XX:LoopUnrollLimit=250 - * -XX:CompileCommand=printcompilation,compiler.loopopts.superword.TestAlignVectorFuzzer::* * -XX:-TieredCompilation -Xbatch * compiler.loopopts.superword.TestAlignVectorFuzzer */ From f779ad64ac3184a90e2d3ddf2cba5321d050d325 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Mon, 22 Sep 2025 12:56:31 +0000 Subject: [PATCH 152/556] 8368104: Parallel: Refactor PSThreadRootsTaskClosure Reviewed-by: stefank, fandreuzzi --- src/hotspot/share/gc/parallel/psScavenge.cpp | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psScavenge.cpp b/src/hotspot/share/gc/parallel/psScavenge.cpp index 676d9e74dbf..0af2ab1fd68 100644 --- a/src/hotspot/share/gc/parallel/psScavenge.cpp +++ b/src/hotspot/share/gc/parallel/psScavenge.cpp @@ -216,20 +216,19 @@ public: }; class PSThreadRootsTaskClosure : public ThreadClosure { - uint _worker_id; + PSPromotionManager* _pm; public: - PSThreadRootsTaskClosure(uint worker_id) : _worker_id(worker_id) { } + PSThreadRootsTaskClosure(PSPromotionManager* pm) : _pm(pm) {} virtual void do_thread(Thread* thread) { assert(ParallelScavengeHeap::heap()->is_stw_gc_active(), "called outside gc"); - PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(_worker_id); - PSScavengeRootsClosure roots_closure(pm); + PSScavengeRootsClosure roots_closure(_pm); // No need to visit nmethods, because they are handled by ScavengableNMethods. thread->oops_do(&roots_closure, nullptr); // Do the real work - pm->drain_stacks(false); + _pm->drain_stacks(false); } }; @@ -263,12 +262,12 @@ public: virtual void work(uint worker_id) { assert(worker_id < _active_workers, "Sanity"); ResourceMark rm; + PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(worker_id); if (!_is_old_gen_empty) { // There are only old-to-young pointers if there are objects // in the old gen. { - PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(worker_id); PSCardTable* card_table = ParallelScavengeHeap::heap()->card_table(); // The top of the old gen changes during scavenge when objects are promoted. @@ -288,14 +287,14 @@ public: scavenge_roots_work(static_cast(root_type), worker_id); } - PSThreadRootsTaskClosure closure(worker_id); - Threads::possibly_parallel_threads_do(_active_workers > 1 /* is_par */, &closure); + PSThreadRootsTaskClosure thread_closure(pm); + Threads::possibly_parallel_threads_do(_active_workers > 1 /* is_par */, &thread_closure); // Scavenge OopStorages { - PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(worker_id); - PSScavengeRootsClosure closure(pm); - _oop_storage_strong_par_state.oops_do(&closure); + PSScavengeRootsClosure root_closure(pm); + _oop_storage_strong_par_state.oops_do(&root_closure); + // Do the real work pm->drain_stacks(false); } From 2131584add9ab46c3380bbf35170307e4878ce51 Mon Sep 17 00:00:00 2001 From: Casper Norrbin Date: Mon, 22 Sep 2025 12:57:01 +0000 Subject: [PATCH 153/556] 8367536: Change RBTree to use C++17 features Reviewed-by: kbarrett, ayang --- src/hotspot/share/utilities/rbTree.hpp | 88 ++++++++----------- src/hotspot/share/utilities/rbTree.inline.hpp | 10 +-- 2 files changed, 41 insertions(+), 57 deletions(-) diff --git a/src/hotspot/share/utilities/rbTree.hpp b/src/hotspot/share/utilities/rbTree.hpp index 2c4edcc2070..940d98fbe12 100644 --- a/src/hotspot/share/utilities/rbTree.hpp +++ b/src/hotspot/share/utilities/rbTree.hpp @@ -196,52 +196,43 @@ protected: DEBUG_ONLY(mutable bool _expected_visited); private: - template - struct has_cmp_type : std::false_type {}; - template - struct has_cmp_type(CMP::cmp), void())> : std::true_type {}; + static constexpr bool HasKeyComparator = + std::is_invocable_r_v; + + static constexpr bool HasNodeComparator = + std::is_invocable_r_v; + + // Due to a bug in older GCC versions with static templated constexpr data members (see GCC PR 71954), + // we have to express this trait through a struct instead of a constexpr variable directly. + template + struct HasNodeVerifierImpl : std::false_type {}; template - static constexpr bool HasKeyComparator = has_cmp_type::value; + struct HasNodeVerifierImpl> + : std::bool_constant> {}; - template - static constexpr bool HasNodeComparator = has_cmp_type::value; + static constexpr bool HasNodeVerifier = HasNodeVerifierImpl::value; - template - struct has_less_than_type : std::false_type {}; - template - struct has_less_than_type(CMP::less), void())> : std::true_type {}; - - template - static constexpr bool HasNodeVerifier = has_less_than_type::value; - - template && !HasNodeComparator)> RBTreeOrdering cmp(const K& a, const NodeType* b) const { - return COMPARATOR::cmp(a, b->key()); + if constexpr (HasNodeComparator) { + return COMPARATOR::cmp(a, b); + } else if constexpr (HasKeyComparator) { + return COMPARATOR::cmp(a, b->key()); + } } - template )> - RBTreeOrdering cmp(const K& a, const NodeType* b) const { - return COMPARATOR::cmp(a, b); - } - - template )> bool less_than(const NodeType* a, const NodeType* b) const { - return true; + if constexpr (HasNodeVerifier) { + return COMPARATOR::less_than(a, b); + } else { + return true; + } } - template )> - bool less_than(const NodeType* a, const NodeType* b) const { - return COMPARATOR::less_than(a, b); - } - - // Cannot assert if no key comparator exist. - template )> - void assert_key_leq(K a, K b) const {} - - template )> void assert_key_leq(K a, K b) const { - assert(COMPARATOR::cmp(a, b) != RBTreeOrdering::GT, "key a must be less or equal to key b"); + if constexpr (HasKeyComparator) { // Cannot assert if no key comparator exist. + assert(COMPARATOR::cmp(a, b) != RBTreeOrdering::GT, "key a must be less or equal to key b"); + } } // True if node is black (nil nodes count as black) @@ -283,8 +274,7 @@ public: AbstractRBTree() : _num_nodes(0), _root(nullptr) DEBUG_ONLY(COMMA _expected_visited(false)) { static_assert(std::is_trivially_destructible::value, "key type must be trivially destructable"); - static_assert(HasKeyComparator || HasNodeComparator, - "comparator must be of correct type"); + static_assert(HasKeyComparator || HasNodeComparator, "comparator must be of correct type"); } size_t size() const { return _num_nodes; } @@ -439,25 +429,19 @@ public: // Verifies that the tree is correct and holds rb-properties // If not using a key comparator (when using IntrusiveRBTree for example), - // A second `cmp` must exist in COMPARATOR (see top of file). + // a function `less_than` must exist in COMPARATOR (see top of file). // Accepts an optional callable `bool extra_verifier(const Node* n)`. // This should return true if the node is valid. // If provided, each node is also verified through this callable. - template )> + template void verify_self(const USER_VERIFIER& extra_verifier = USER_VERIFIER()) const { - verify_self([](const NodeType* a, const NodeType* b){ return COMPARATOR::less_than(a, b);}, extra_verifier); - } - - template && !HasNodeVerifier)> - void verify_self(const USER_VERIFIER& extra_verifier = USER_VERIFIER()) const { - verify_self([](const NodeType* a, const NodeType* b){ return COMPARATOR::cmp(a->key(), b->key()) == RBTreeOrdering::LT; }, extra_verifier); - } - - template && !HasKeyComparator && !HasNodeVerifier)> - void verify_self(const USER_VERIFIER& extra_verifier = USER_VERIFIER()) const { - verify_self([](const NodeType*, const NodeType*){ return true;}, extra_verifier); + if constexpr (HasNodeVerifier) { + verify_self([](const NodeType* a, const NodeType* b){ return COMPARATOR::less_than(a, b);}, extra_verifier); + } else if constexpr (HasKeyComparator) { + verify_self([](const NodeType* a, const NodeType* b){ return COMPARATOR::cmp(a->key(), b->key()) == RBTreeOrdering::LT; }, extra_verifier); + } else { + verify_self([](const NodeType*, const NodeType*){ return true;}, extra_verifier); + } } // Accepts an optional printing callable `void node_printer(outputStream* st, const Node* n, int depth)`. diff --git a/src/hotspot/share/utilities/rbTree.inline.hpp b/src/hotspot/share/utilities/rbTree.inline.hpp index b7de0a7ac4c..6e01f92d12a 100644 --- a/src/hotspot/share/utilities/rbTree.inline.hpp +++ b/src/hotspot/share/utilities/rbTree.inline.hpp @@ -695,21 +695,21 @@ inline void AbstractRBTree::verify_self(NODE_VERIFIER v } template ::value), - ENABLE_IF(std::is_signed::value)> + ENABLE_IF(std::is_integral_v), + ENABLE_IF(std::is_signed_v)> void print_T(outputStream* st, T x) { st->print(INT64_FORMAT, (int64_t)x); } template ::value), - ENABLE_IF(std::is_unsigned::value)> + ENABLE_IF(std::is_integral_v), + ENABLE_IF(std::is_unsigned_v)> void print_T(outputStream* st, T x) { st->print(UINT64_FORMAT, (uint64_t)x); } template ::value)> + ENABLE_IF(std::is_pointer_v)> void print_T(outputStream* st, T x) { st->print(PTR_FORMAT, p2i(x)); } From ca182912a305e1e226d97d9613c7baf8a3d22780 Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Mon, 22 Sep 2025 13:35:35 +0000 Subject: [PATCH 154/556] 8368094: Fix problem list errors Reviewed-by: dholmes, dcubed, syan --- test/hotspot/jtreg/ProblemList-zgc.txt | 16 ++-------------- test/hotspot/jtreg/ProblemList.txt | 4 ++-- .../jtreg/serviceability/sa/ClhsdbScanOops.java | 4 ++-- test/jdk/ProblemList-Virtual.txt | 4 ---- test/jdk/ProblemList.txt | 3 +-- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/test/hotspot/jtreg/ProblemList-zgc.txt b/test/hotspot/jtreg/ProblemList-zgc.txt index 9571c717641..911e3adc9ca 100644 --- a/test/hotspot/jtreg/ProblemList-zgc.txt +++ b/test/hotspot/jtreg/ProblemList-zgc.txt @@ -40,7 +40,6 @@ serviceability/sa/ClhsdbClasses.java 8307393 generic- serviceability/sa/ClhsdbDumpclass.java 8307393 generic-all serviceability/sa/ClhsdbDumpheap.java 8307393 generic-all serviceability/sa/ClhsdbField.java 8307393 generic-all -serviceability/sa/ClhsdbFindPC.java#apa 8307393 generic-all serviceability/sa/ClhsdbFindPC.java#no-xcomp-core 8307393 generic-all serviceability/sa/ClhsdbFindPC.java#no-xcomp-process 8307393 generic-all serviceability/sa/ClhsdbFindPC.java#xcomp-core 8307393 generic-all @@ -54,9 +53,7 @@ serviceability/sa/ClhsdbJstack.java#id0 8307393 generic- serviceability/sa/ClhsdbJstack.java#id1 8307393 generic-all serviceability/sa/ClhsdbJstackWithConcurrentLock.java 8307393 generic-all serviceability/sa/ClhsdbJstackXcompStress.java 8307393 generic-all -serviceability/sa/ClhsdbLauncher.java 8307393 generic-all serviceability/sa/ClhsdbLongConstant.java 8307393 generic-all -serviceability/sa/ClhsdbPmap.java 8307393 generic-all serviceability/sa/ClhsdbPmap.java#core 8307393 generic-all serviceability/sa/ClhsdbPmap.java#process 8307393 generic-all serviceability/sa/ClhsdbPrintAll.java 8307393 generic-all @@ -64,7 +61,8 @@ serviceability/sa/ClhsdbPrintAs.java 8307393 generic- serviceability/sa/ClhsdbPrintStatics.java 8307393 generic-all serviceability/sa/ClhsdbPstack.java#core 8307393 generic-all serviceability/sa/ClhsdbPstack.java#process 8307393 generic-all -serviceability/sa/ClhsdbScanOops.java 8307393 generic-all +serviceability/sa/ClhsdbScanOops.java#parallel 8307393 generic-all +serviceability/sa/ClhsdbScanOops.java#serial 8307393 generic-all serviceability/sa/ClhsdbSource.java 8307393 generic-all serviceability/sa/ClhsdbSymbol.java 8307393 generic-all serviceability/sa/ClhsdbThread.java 8307393 generic-all @@ -73,14 +71,6 @@ serviceability/sa/ClhsdbVmStructsDump.java 8307393 generic- serviceability/sa/ClhsdbWhere.java 8307393 generic-all serviceability/sa/DeadlockDetectionTest.java 8307393 generic-all serviceability/sa/JhsdbThreadInfoTest.java 8307393 generic-all -serviceability/sa/LingeredAppSysProps.java 8307393 generic-all -serviceability/sa/LingeredAppWithDefaultMethods.java 8307393 generic-all -serviceability/sa/LingeredAppWithEnum.java 8307393 generic-all -serviceability/sa/LingeredAppWithInterface.java 8307393 generic-all -serviceability/sa/LingeredAppWithInvokeDynamic.java 8307393 generic-all -serviceability/sa/LingeredAppWithLock.java 8307393 generic-all -serviceability/sa/LingeredAppWithNativeMethod.java 8307393 generic-all -serviceability/sa/LingeredAppWithRecComputation.java 8307393 generic-all serviceability/sa/TestClassDump.java 8307393 generic-all serviceability/sa/TestClhsdbJstackLock.java 8307393 generic-all serviceability/sa/TestCpoolForInvokeDynamic.java 8307393 generic-all @@ -104,13 +94,11 @@ serviceability/sa/TestRevPtrsForInvokeDynamic.java 8307393 generic- serviceability/sa/TestSysProps.java 8307393 generic-all serviceability/sa/TestType.java 8307393 generic-all serviceability/sa/UniqueVtableTest.java 8307393 generic-all -serviceability/sa/jmap-hprof/JMapHProfLargeHeapProc.java 8307393 generic-all serviceability/sa/jmap-hprof/JMapHProfLargeHeapTest.java 8307393 generic-all serviceability/sa/sadebugd/ClhsdbAttachToDebugServer.java 8307393 generic-all serviceability/sa/sadebugd/ClhsdbTestConnectArgument.java 8307393 generic-all serviceability/sa/ClhsdbTestAllocationMerge.java 8307393 generic-all serviceability/sa/sadebugd/DebugdConnectTest.java 8307393 generic-all -serviceability/sa/sadebugd/DebugdUtils.java 8307393 generic-all serviceability/sa/sadebugd/DisableRegistryTest.java 8307393 generic-all serviceability/sa/sadebugd/PmapOnDebugdTest.java 8307393 generic-all serviceability/sa/sadebugd/RunCommandOnServerTest.java 8307393 generic-all diff --git a/test/hotspot/jtreg/ProblemList.txt b/test/hotspot/jtreg/ProblemList.txt index 37986c67dd8..d061236c957 100644 --- a/test/hotspot/jtreg/ProblemList.txt +++ b/test/hotspot/jtreg/ProblemList.txt @@ -121,7 +121,8 @@ containers/docker/TestJcmdWithSideCar.java 8341518 linux-x64 # :hotspot_serviceability -serviceability/sa/sadebugd/DebugdConnectTest.java 8239062,8270326 macosx-x64,macosx-aarch64 +# 8239062 and 8270326 only affects macosx-x64,macosx-aarch64 +serviceability/sa/sadebugd/DebugdConnectTest.java 8239062,8270326,8344261 generic-all serviceability/sa/TestRevPtrsForInvokeDynamic.java 8241235 generic-all serviceability/jvmti/vthread/GetThreadStateMountedTest/GetThreadStateMountedTest.java 8318090,8318729 generic-all @@ -143,7 +144,6 @@ serviceability/jvmti/stress/StackTrace/NotSuspended/GetStackTraceNotSuspendedStr serviceability/sa/JhsdbThreadInfoTest.java 8344261 generic-all serviceability/sa/TestJhsdbJstackLock.java 8344261 generic-all -serviceability/sa/sadebugd/DebugdConnectTest.java 8344261 generic-all serviceability/attach/RemovingUnixDomainSocketTest.java 8344261 generic-all ############################################################################# diff --git a/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java b/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java index cb90067f2f7..b6919a566a9 100644 --- a/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java +++ b/test/hotspot/jtreg/serviceability/sa/ClhsdbScanOops.java @@ -22,7 +22,7 @@ */ /** - * @test + * @test id=parallel * @bug 8192985 * @summary Test the clhsdb 'scanoops' command * @requires vm.gc.Parallel @@ -33,7 +33,7 @@ */ /** - * @test + * @test id=serial * @bug 8192985 * @summary Test the clhsdb 'scanoops' command * @requires vm.gc.Serial diff --git a/test/jdk/ProblemList-Virtual.txt b/test/jdk/ProblemList-Virtual.txt index d7692b578cd..d578f296ff9 100644 --- a/test/jdk/ProblemList-Virtual.txt +++ b/test/jdk/ProblemList-Virtual.txt @@ -55,7 +55,6 @@ java/lang/StackWalker/StackWalkTest.java 0000000 generic-all java/lang/StackWalker/CallerFromMain.java 0000000 generic-all java/lang/Thread/MainThreadTest.java 0000000 generic-all java/lang/Thread/UncaughtExceptionsTest.java 0000000 generic-all -java/lang/Thread/virtual/GetStackTraceWhenRunnable.java 0000000 generic-all java/lang/invoke/condy/CondyNestedResolutionTest.java 0000000 generic-all java/lang/ref/OOMEInReferenceHandler.java 0000000 generic-all java/util/concurrent/locks/Lock/OOMEInAQS.java 0000000 generic-all @@ -63,9 +62,6 @@ jdk/internal/vm/Continuation/Scoped.java 0000000 generic-all # The problems with permissions jdk/jfr/startupargs/TestDumpOnExit.java 0000000 generic-all -java/lang/SecurityManager/modules/CustomSecurityManagerTest.java 0000000 generic-all -java/util/PluggableLocale/PermissionTest.java 0000000 generic-all -java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all javax/management/ImplementationVersion/ImplVersionTest.java 0000000 generic-all javax/management/remote/mandatory/version/ImplVersionTest.java 0000000 generic-all diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 8a84bbf60d5..8b99564b967 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -542,7 +542,7 @@ java/lang/instrument/RetransformBigClass.sh 8065756 generic- # jdk_io java/io/pathNames/GeneralWin32.java 8180264 windows-all -java/io/IO/IO.java 8337935 linux-ppc64le +java/lang/IO/IO.java 8337935 linux-ppc64le ############################################################################ @@ -552,7 +552,6 @@ com/sun/management/OperatingSystemMXBean/GetProcessCpuLoad.java 8030957 aix-all com/sun/management/OperatingSystemMXBean/GetSystemCpuLoad.java 8030957 aix-all java/lang/management/MemoryMXBean/Pending.java 8158837 generic-all -java/lang/management/MemoryMXBean/PendingAllGC.sh 8158837 generic-all java/lang/management/ThreadMXBean/ThreadMXBeanStateTest.java 8247426 generic-all sun/management/jdp/JdpDefaultsTest.java 8308807,8241865 aix-ppc64,macosx-all From 8d5c0056420731cbbd83f2d23837bbb5cdc9e4cc Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Mon, 22 Sep 2025 13:47:45 +0000 Subject: [PATCH 155/556] 8342382: Implement JEP 522: G1 GC: Improve Throughput by Reducing Synchronization Co-authored-by: Amit Kumar Co-authored-by: Martin Doerr Co-authored-by: Carlo Refice Co-authored-by: Fei Yang Reviewed-by: iwalulya, rcastanedalo, aph, ayang --- .../gc/g1/g1BarrierSetAssembler_aarch64.cpp | 239 ++---- .../gc/g1/g1BarrierSetAssembler_aarch64.hpp | 17 +- src/hotspot/cpu/aarch64/gc/g1/g1_aarch64.ad | 8 +- .../arm/gc/g1/g1BarrierSetAssembler_arm.cpp | 239 +----- .../arm/gc/g1/g1BarrierSetAssembler_arm.hpp | 17 +- src/hotspot/cpu/arm/gc/g1/g1_arm.ad | 8 +- .../ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp | 270 ++---- .../ppc/gc/g1/g1BarrierSetAssembler_ppc.hpp | 21 +- src/hotspot/cpu/ppc/gc/g1/g1_ppc.ad | 8 +- .../gc/g1/g1BarrierSetAssembler_riscv.cpp | 263 ++---- .../gc/g1/g1BarrierSetAssembler_riscv.hpp | 18 +- src/hotspot/cpu/riscv/gc/g1/g1_riscv.ad | 8 +- .../s390/gc/g1/g1BarrierSetAssembler_s390.cpp | 351 ++------ .../s390/gc/g1/g1BarrierSetAssembler_s390.hpp | 18 +- src/hotspot/cpu/s390/gc/g1/g1_s390.ad | 8 +- .../x86/gc/g1/g1BarrierSetAssembler_x86.cpp | 265 ++---- .../x86/gc/g1/g1BarrierSetAssembler_x86.hpp | 31 +- src/hotspot/cpu/x86/gc/g1/g1_x86_64.ad | 38 +- src/hotspot/share/code/aotCodeCache.cpp | 1 - src/hotspot/share/gc/g1/c1/g1BarrierSetC1.cpp | 130 ++- src/hotspot/share/gc/g1/c1/g1BarrierSetC1.hpp | 33 +- src/hotspot/share/gc/g1/c2/g1BarrierSetC2.cpp | 69 +- src/hotspot/share/gc/g1/c2/g1BarrierSetC2.hpp | 25 +- src/hotspot/share/gc/g1/g1Allocator.cpp | 3 - src/hotspot/share/gc/g1/g1Analytics.cpp | 40 +- src/hotspot/share/gc/g1/g1Analytics.hpp | 19 +- src/hotspot/share/gc/g1/g1Arguments.cpp | 9 +- src/hotspot/share/gc/g1/g1BarrierSet.cpp | 91 +- src/hotspot/share/gc/g1/g1BarrierSet.hpp | 60 +- .../share/gc/g1/g1BarrierSet.inline.hpp | 7 +- .../share/gc/g1/g1BarrierSetRuntime.cpp | 14 +- .../share/gc/g1/g1BarrierSetRuntime.hpp | 3 +- src/hotspot/share/gc/g1/g1CardTable.cpp | 40 +- src/hotspot/share/gc/g1/g1CardTable.hpp | 59 +- .../share/gc/g1/g1CardTable.inline.hpp | 52 +- .../share/gc/g1/g1CardTableClaimTable.cpp | 97 +++ .../share/gc/g1/g1CardTableClaimTable.hpp | 137 ++++ .../gc/g1/g1CardTableClaimTable.inline.hpp | 128 +++ src/hotspot/share/gc/g1/g1CollectedHeap.cpp | 125 ++- src/hotspot/share/gc/g1/g1CollectedHeap.hpp | 46 +- .../share/gc/g1/g1CollectedHeap.inline.hpp | 24 - src/hotspot/share/gc/g1/g1CollectionSet.cpp | 9 +- src/hotspot/share/gc/g1/g1ConcurrentMark.cpp | 7 +- src/hotspot/share/gc/g1/g1ConcurrentMark.hpp | 2 + .../gc/g1/g1ConcurrentMarkRemarkTasks.cpp | 10 +- .../gc/g1/g1ConcurrentRebuildAndScrub.cpp | 2 +- .../share/gc/g1/g1ConcurrentRefine.cpp | 674 +++++++++------ .../share/gc/g1/g1ConcurrentRefine.hpp | 247 ++++-- .../share/gc/g1/g1ConcurrentRefineStats.cpp | 50 +- .../share/gc/g1/g1ConcurrentRefineStats.hpp | 71 +- .../gc/g1/g1ConcurrentRefineSweepTask.cpp | 191 +++++ .../g1ConcurrentRefineSweepTask.hpp} | 31 +- .../share/gc/g1/g1ConcurrentRefineThread.cpp | 270 +++--- .../share/gc/g1/g1ConcurrentRefineThread.hpp | 42 +- .../gc/g1/g1ConcurrentRefineThreadsNeeded.cpp | 52 +- src/hotspot/share/gc/g1/g1DirtyCardQueue.cpp | 599 -------------- src/hotspot/share/gc/g1/g1DirtyCardQueue.hpp | 302 ------- src/hotspot/share/gc/g1/g1FromCardCache.cpp | 4 +- .../share/gc/g1/g1FullGCCompactTask.cpp | 4 + .../gc/g1/g1FullGCPrepareTask.inline.hpp | 4 + .../share/gc/g1/g1FullGCResetMetadataTask.cpp | 2 +- src/hotspot/share/gc/g1/g1GCPhaseTimes.cpp | 34 +- src/hotspot/share/gc/g1/g1GCPhaseTimes.hpp | 43 +- src/hotspot/share/gc/g1/g1HeapRegion.cpp | 38 +- src/hotspot/share/gc/g1/g1HeapRegion.hpp | 6 +- .../share/gc/g1/g1HeapRegionManager.cpp | 25 +- .../share/gc/g1/g1HeapRegionManager.hpp | 6 +- src/hotspot/share/gc/g1/g1HeapVerifier.cpp | 106 ++- src/hotspot/share/gc/g1/g1HeapVerifier.hpp | 15 +- src/hotspot/share/gc/g1/g1OopClosures.hpp | 36 +- .../share/gc/g1/g1OopClosures.inline.hpp | 31 +- .../share/gc/g1/g1ParScanThreadState.cpp | 56 +- .../share/gc/g1/g1ParScanThreadState.hpp | 50 +- .../gc/g1/g1ParScanThreadState.inline.hpp | 34 +- src/hotspot/share/gc/g1/g1Policy.cpp | 407 +++++---- src/hotspot/share/gc/g1/g1Policy.hpp | 67 +- .../share/gc/g1/g1RedirtyCardsQueue.cpp | 148 ---- .../share/gc/g1/g1RedirtyCardsQueue.hpp | 98 --- src/hotspot/share/gc/g1/g1RemSet.cpp | 776 ++++++------------ src/hotspot/share/gc/g1/g1RemSet.hpp | 29 +- src/hotspot/share/gc/g1/g1RemSetSummary.cpp | 74 +- src/hotspot/share/gc/g1/g1RemSetSummary.hpp | 11 +- .../share/gc/g1/g1ReviseYoungLengthTask.cpp | 96 +++ .../share/gc/g1/g1ReviseYoungLengthTask.hpp | 63 ++ src/hotspot/share/gc/g1/g1ThreadLocalData.hpp | 32 +- src/hotspot/share/gc/g1/g1YoungCollector.cpp | 10 +- src/hotspot/share/gc/g1/g1YoungCollector.hpp | 1 - .../gc/g1/g1YoungGCPostEvacuateTasks.cpp | 113 +-- .../gc/g1/g1YoungGCPostEvacuateTasks.hpp | 8 +- .../share/gc/g1/g1YoungGCPreEvacuateTasks.cpp | 99 +-- .../share/gc/g1/g1YoungGCPreEvacuateTasks.hpp | 13 +- src/hotspot/share/gc/g1/g1_globals.hpp | 9 +- .../share/gc/g1/jvmFlagConstraintsG1.cpp | 6 - .../share/gc/g1/jvmFlagConstraintsG1.hpp | 1 - src/hotspot/share/gc/g1/vmStructs_g1.hpp | 4 +- .../share/gc/shared/bufferNodeList.cpp | 38 - src/hotspot/share/gc/shared/cardTable.cpp | 8 +- src/hotspot/share/gc/shared/cardTable.hpp | 8 +- .../share/gc/shared/workerDataArray.hpp | 4 +- src/hotspot/share/jvmci/jvmciRuntime.cpp | 4 - src/hotspot/share/jvmci/vmStructs_jvmci.cpp | 6 +- src/hotspot/share/oops/oop.cpp | 11 +- src/hotspot/share/runtime/arguments.cpp | 1 + src/hotspot/share/runtime/cpuTimeCounters.cpp | 3 + src/hotspot/share/runtime/cpuTimeCounters.hpp | 1 + src/hotspot/share/runtime/mutexLocker.cpp | 10 +- src/hotspot/share/runtime/mutexLocker.hpp | 2 +- src/hotspot/share/runtime/vmOperation.hpp | 3 +- .../gcbarriers/TestG1BarrierGeneration.java | 4 +- .../jtreg/gc/g1/TestGCLogMessages.java | 25 +- .../TestOptionsWithRanges.java | 1 - .../ir_framework/tests/TestIRMatching.java | 2 +- .../vmTestbase/gc/ArrayJuggle/Juggle2.java | 7 +- .../gc/collection/TestG1ParallelPhases.java | 11 +- 114 files changed, 3625 insertions(+), 4681 deletions(-) create mode 100644 src/hotspot/share/gc/g1/g1CardTableClaimTable.cpp create mode 100644 src/hotspot/share/gc/g1/g1CardTableClaimTable.hpp create mode 100644 src/hotspot/share/gc/g1/g1CardTableClaimTable.inline.hpp create mode 100644 src/hotspot/share/gc/g1/g1ConcurrentRefineSweepTask.cpp rename src/hotspot/share/gc/{shared/bufferNodeList.hpp => g1/g1ConcurrentRefineSweepTask.hpp} (57%) delete mode 100644 src/hotspot/share/gc/g1/g1DirtyCardQueue.cpp delete mode 100644 src/hotspot/share/gc/g1/g1DirtyCardQueue.hpp delete mode 100644 src/hotspot/share/gc/g1/g1RedirtyCardsQueue.cpp delete mode 100644 src/hotspot/share/gc/g1/g1RedirtyCardsQueue.hpp create mode 100644 src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.cpp create mode 100644 src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.hpp delete mode 100644 src/hotspot/share/gc/shared/bufferNodeList.cpp diff --git a/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp index 42f3c4a015a..9950feb7470 100644 --- a/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp @@ -86,15 +86,48 @@ void G1BarrierSetAssembler::gen_write_ref_array_pre_barrier(MacroAssembler* masm } } -void G1BarrierSetAssembler::gen_write_ref_array_post_barrier(MacroAssembler* masm, DecoratorSet decorators, - Register start, Register count, Register scratch, RegSet saved_regs) { - __ push(saved_regs, sp); - assert_different_registers(start, count, scratch); - assert_different_registers(c_rarg0, count); - __ mov(c_rarg0, start); - __ mov(c_rarg1, count); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_array_post_entry), 2); - __ pop(saved_regs, sp); +void G1BarrierSetAssembler::gen_write_ref_array_post_barrier(MacroAssembler* masm, + DecoratorSet decorators, + Register start, + Register count, + Register scratch, + RegSet saved_regs) { + + Label done; + Label loop; + Label next; + + __ cbz(count, done); + + // Calculate the number of card marks to set. Since the object might start and + // end within a card, we need to calculate this via the card table indexes of + // the actual start and last addresses covered by the object. + // Temporarily use the count register for the last element address. + __ lea(count, Address(start, count, Address::lsl(LogBytesPerHeapOop))); // end = start + count << LogBytesPerHeapOop + __ sub(count, count, BytesPerHeapOop); // Use last element address for end. + + __ lsr(start, start, CardTable::card_shift()); + __ lsr(count, count, CardTable::card_shift()); + __ sub(count, count, start); // Number of bytes to mark - 1. + + // Add card table base offset to start. + __ ldr(scratch, Address(rthread, in_bytes(G1ThreadLocalData::card_table_base_offset()))); + __ add(start, start, scratch); + + __ bind(loop); + if (UseCondCardMark) { + __ ldrb(scratch, Address(start, count)); + // Instead of loading clean_card_val and comparing, we exploit the fact that + // the LSB of non-clean cards is always 0, and the LSB of clean cards 1. + __ tbz(scratch, 0, next); + } + static_assert(G1CardTable::dirty_card_val() == 0, "must be to use zr"); + __ strb(zr, Address(start, count)); + __ bind(next); + __ subs(count, count, 1); + __ br(Assembler::GE, loop); + + __ bind(done); } static void generate_queue_test_and_insertion(MacroAssembler* masm, ByteSize index_offset, ByteSize buffer_offset, Label& runtime, @@ -202,10 +235,14 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, static void generate_post_barrier_fast_path(MacroAssembler* masm, const Register store_addr, const Register new_val, + const Register thread, const Register tmp1, const Register tmp2, Label& done, bool new_val_may_be_null) { + assert(thread == rthread, "must be"); + assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg, rscratch1); + // Does store cross heap regions? __ eor(tmp1, store_addr, new_val); // tmp1 := store address ^ new value __ lsr(tmp1, tmp1, G1HeapRegion::LogOfHRGrainBytes); // tmp1 := ((store address ^ new value) >> LogOfHRGrainBytes) @@ -214,33 +251,19 @@ static void generate_post_barrier_fast_path(MacroAssembler* masm, if (new_val_may_be_null) { __ cbz(new_val, done); } - // Storing region crossing non-null, is card young? + // Storing region crossing non-null. __ lsr(tmp1, store_addr, CardTable::card_shift()); // tmp1 := card address relative to card table base - __ load_byte_map_base(tmp2); // tmp2 := card table base address - __ add(tmp1, tmp1, tmp2); // tmp1 := card address - __ ldrb(tmp2, Address(tmp1)); // tmp2 := card - __ cmpw(tmp2, (int)G1CardTable::g1_young_card_val()); // tmp2 := card == young_card_val? -} -static void generate_post_barrier_slow_path(MacroAssembler* masm, - const Register thread, - const Register tmp1, - const Register tmp2, - Label& done, - Label& runtime) { - __ membar(Assembler::StoreLoad); // StoreLoad membar - __ ldrb(tmp2, Address(tmp1)); // tmp2 := card - __ cbzw(tmp2, done); - // Storing a region crossing, non-null oop, card is clean. - // Dirty card and log. - STATIC_ASSERT(CardTable::dirty_card_val() == 0); - __ strb(zr, Address(tmp1)); // *(card address) := dirty_card_val - generate_queue_test_and_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - runtime, - thread, tmp1, tmp2, rscratch1); - __ b(done); + Address card_table_addr(thread, in_bytes(G1ThreadLocalData::card_table_base_offset())); + __ ldr(tmp2, card_table_addr); // tmp2 := card table base address + if (UseCondCardMark) { + __ ldrb(rscratch1, Address(tmp1, tmp2)); // rscratch1 := card + // Instead of loading clean_card_val and comparing, we exploit the fact that + // the LSB of non-clean cards is always 0, and the LSB of clean cards 1. + __ tbz(rscratch1, 0, done); + } + static_assert(G1CardTable::dirty_card_val() == 0, "must be to use zr"); + __ strb(zr, Address(tmp1, tmp2)); // *(card address) := dirty_card_val } void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, @@ -249,27 +272,8 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Register thread, Register tmp1, Register tmp2) { - assert(thread == rthread, "must be"); - assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, - rscratch1); - assert(store_addr != noreg && new_val != noreg && tmp1 != noreg - && tmp2 != noreg, "expecting a register"); - Label done; - Label runtime; - - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, tmp2, done, true /* new_val_may_be_null */); - // If card is young, jump to done - __ br(Assembler::EQ, done); - generate_post_barrier_slow_path(masm, thread, tmp1, tmp2, done, runtime); - - __ bind(runtime); - // save the live input values - RegSet saved = RegSet::of(store_addr); - __ push(saved, sp); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), tmp1, thread); - __ pop(saved, sp); - + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, false /* new_val_may_be_null */); __ bind(done); } @@ -329,38 +333,10 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register thread, Register tmp1, Register tmp2, - G1PostBarrierStubC2* stub) { - assert(thread == rthread, "must be"); - assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, - rscratch1); - assert(store_addr != noreg && new_val != noreg && tmp1 != noreg - && tmp2 != noreg, "expecting a register"); - - stub->initialize_registers(thread, tmp1, tmp2); - - bool new_val_may_be_null = (stub->barrier_data() & G1C2BarrierPostNotNull) == 0; - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, tmp2, *stub->continuation(), new_val_may_be_null); - // If card is not young, jump to stub (slow path) - __ br(Assembler::NE, *stub->entry()); - - __ bind(*stub->continuation()); -} - -void G1BarrierSetAssembler::generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const { - Assembler::InlineSkippedInstructionsCounter skip_counter(masm); - Label runtime; - Register thread = stub->thread(); - Register tmp1 = stub->tmp1(); // tmp1 holds the card address. - Register tmp2 = stub->tmp2(); - assert(stub->tmp3() == noreg, "not needed in this platform"); - - __ bind(*stub->entry()); - generate_post_barrier_slow_path(masm, thread, tmp1, tmp2, *stub->continuation(), runtime); - - __ bind(runtime); - generate_c2_barrier_runtime_call(masm, stub, tmp1, CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry)); - __ b(*stub->continuation()); + bool new_val_may_be_null) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + __ bind(done); } #endif // COMPILER2 @@ -456,20 +432,19 @@ void G1BarrierSetAssembler::gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrier __ b(*stub->continuation()); } -void G1BarrierSetAssembler::gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub) { - G1BarrierSetC1* bs = (G1BarrierSetC1*)BarrierSet::barrier_set()->barrier_set_c1(); - __ bind(*stub->entry()); - assert(stub->addr()->is_register(), "Precondition."); - assert(stub->new_val()->is_register(), "Precondition."); - Register new_val_reg = stub->new_val()->as_register(); - __ cbz(new_val_reg, *stub->continuation()); - ce->store_parameter(stub->addr()->as_pointer_register(), 0); - __ far_call(RuntimeAddress(bs->post_barrier_c1_runtime_code_blob()->code_begin())); - __ b(*stub->continuation()); -} - #undef __ +void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + masm->bind(done); +} + #define __ sasm-> void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm) { @@ -521,74 +496,6 @@ void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* __ epilogue(); } -void G1BarrierSetAssembler::generate_c1_post_barrier_runtime_stub(StubAssembler* sasm) { - __ prologue("g1_post_barrier", false); - - // arg0: store_address - Address store_addr(rfp, 2*BytesPerWord); - - BarrierSet* bs = BarrierSet::barrier_set(); - CardTableBarrierSet* ctbs = barrier_set_cast(bs); - CardTable* ct = ctbs->card_table(); - - Label done; - Label runtime; - - // At this point we know new_value is non-null and the new_value crosses regions. - // Must check to see if card is already dirty - - const Register thread = rthread; - - Address queue_index(thread, in_bytes(G1ThreadLocalData::dirty_card_queue_index_offset())); - Address buffer(thread, in_bytes(G1ThreadLocalData::dirty_card_queue_buffer_offset())); - - const Register card_offset = rscratch2; - // LR is free here, so we can use it to hold the byte_map_base. - const Register byte_map_base = lr; - - assert_different_registers(card_offset, byte_map_base, rscratch1); - - __ load_parameter(0, card_offset); - __ lsr(card_offset, card_offset, CardTable::card_shift()); - __ load_byte_map_base(byte_map_base); - __ ldrb(rscratch1, Address(byte_map_base, card_offset)); - __ cmpw(rscratch1, (int)G1CardTable::g1_young_card_val()); - __ br(Assembler::EQ, done); - - assert((int)CardTable::dirty_card_val() == 0, "must be 0"); - - __ membar(Assembler::StoreLoad); - __ ldrb(rscratch1, Address(byte_map_base, card_offset)); - __ cbzw(rscratch1, done); - - // storing region crossing non-null, card is clean. - // dirty card and log. - __ strb(zr, Address(byte_map_base, card_offset)); - - // Convert card offset into an address in card_addr - Register card_addr = card_offset; - __ add(card_addr, byte_map_base, card_addr); - - __ ldr(rscratch1, queue_index); - __ cbz(rscratch1, runtime); - __ sub(rscratch1, rscratch1, wordSize); - __ str(rscratch1, queue_index); - - // Reuse LR to hold buffer_addr - const Register buffer_addr = lr; - - __ ldr(buffer_addr, buffer); - __ str(card_addr, Address(buffer_addr, rscratch1)); - __ b(done); - - __ bind(runtime); - __ push_call_clobbered_registers(); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), card_addr, thread); - __ pop_call_clobbered_registers(); - __ bind(done); - __ epilogue(); -} - #undef __ #endif // COMPILER1 diff --git a/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.hpp b/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.hpp index 04ac2096096..72040cd7ad2 100644 --- a/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,9 +32,7 @@ class LIR_Assembler; class StubAssembler; class G1PreBarrierStub; -class G1PostBarrierStub; class G1PreBarrierStubC2; -class G1PostBarrierStubC2; class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { protected: @@ -65,10 +63,15 @@ protected: public: #ifdef COMPILER1 void gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrierStub* stub); - void gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub); void generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm); - void generate_c1_post_barrier_runtime_stub(StubAssembler* sasm); + + void g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2); #endif #ifdef COMPILER2 @@ -87,9 +90,7 @@ public: Register thread, Register tmp1, Register tmp2, - G1PostBarrierStubC2* c2_stub); - void generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const; + bool new_val_may_be_null); #endif void load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, diff --git a/src/hotspot/cpu/aarch64/gc/g1/g1_aarch64.ad b/src/hotspot/cpu/aarch64/gc/g1/g1_aarch64.ad index 081a67d6880..18fc27a4af4 100644 --- a/src/hotspot/cpu/aarch64/gc/g1/g1_aarch64.ad +++ b/src/hotspot/cpu/aarch64/gc/g1/g1_aarch64.ad @@ -1,5 +1,5 @@ // -// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. // DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. // // This code is free software; you can redistribute it and/or modify it @@ -62,13 +62,13 @@ static void write_barrier_post(MacroAssembler* masm, Register new_val, Register tmp1, Register tmp2) { - if (!G1PostBarrierStubC2::needs_barrier(node)) { + if (!G1BarrierStubC2::needs_post_barrier(node)) { return; } Assembler::InlineSkippedInstructionsCounter skip_counter(masm); G1BarrierSetAssembler* g1_asm = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); - G1PostBarrierStubC2* const stub = G1PostBarrierStubC2::create(node); - g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, rthread, tmp1, tmp2, stub); + bool new_val_may_be_null = G1BarrierStubC2::post_new_val_may_be_null(node); + g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, rthread, tmp1, tmp2, new_val_may_be_null); } %} diff --git a/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp b/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp index 049477cda76..71f8931eb5f 100644 --- a/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp +++ b/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp @@ -201,12 +201,15 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, static void generate_post_barrier_fast_path(MacroAssembler* masm, const Register store_addr, const Register new_val, + const Register thread, const Register tmp1, const Register tmp2, Label& done, bool new_val_may_be_null) { - // Does store cross heap regions? + assert(thread == Rthread, "must be"); + assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg); + // Does store cross heap regions? __ eor(tmp1, store_addr, new_val); __ movs(tmp1, AsmOperand(tmp1, lsr, G1HeapRegion::LogOfHRGrainBytes)); __ b(done, eq); @@ -215,76 +218,34 @@ static void generate_post_barrier_fast_path(MacroAssembler* masm, if (new_val_may_be_null) { __ cbz(new_val, done); } - // storing region crossing non-null, is card already dirty? - const Register card_addr = tmp1; - CardTableBarrierSet* ct = barrier_set_cast(BarrierSet::barrier_set()); - __ mov_address(tmp2, (address)ct->card_table()->byte_map_base()); - __ add(card_addr, tmp2, AsmOperand(store_addr, lsr, CardTable::card_shift())); + // storing region crossing non-null, is card already non-clean? + Address card_table_addr(thread, in_bytes(G1ThreadLocalData::card_table_base_offset())); + __ ldr(tmp2, card_table_addr); + __ add(tmp1, tmp2, AsmOperand(store_addr, lsr, CardTable::card_shift())); - __ ldrb(tmp2, Address(card_addr)); - __ cmp(tmp2, (int)G1CardTable::g1_young_card_val()); + if (UseCondCardMark) { + __ ldrb(tmp2, Address(tmp1)); + // Instead of loading clean_card_val and comparing, we exploit the fact that + // the LSB of non-clean cards is always 0, and the LSB of clean cards 1. + __ tbz(tmp2, 0, done); + } + + static_assert(G1CardTable::dirty_card_val() == 0, "must be to use zero_register()"); + __ zero_register(tmp2); + __ strb(tmp2, Address(tmp1)); // *(card address) := dirty_card_val } -static void generate_post_barrier_slow_path(MacroAssembler* masm, - const Register thread, - const Register tmp1, - const Register tmp2, - const Register tmp3, - Label& done, - Label& runtime) { - __ membar(MacroAssembler::Membar_mask_bits(MacroAssembler::StoreLoad), tmp2); - assert(CardTable::dirty_card_val() == 0, "adjust this code"); - // card_addr is loaded by generate_post_barrier_fast_path - const Register card_addr = tmp1; - __ ldrb(tmp2, Address(card_addr)); - __ cbz(tmp2, done); - - // storing a region crossing, non-null oop, card is clean. - // dirty card and log. - - __ strb(__ zero_register(tmp2), Address(card_addr)); - generate_queue_test_and_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - runtime, - thread, card_addr, tmp2, tmp3); - __ b(done); -} - - // G1 post-barrier. // Blows all volatile registers R0-R3, LR). void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, - Register store_addr, - Register new_val, - Register tmp1, - Register tmp2, - Register tmp3) { + Register store_addr, + Register new_val, + Register tmp1, + Register tmp2, + Register tmp3) { Label done; - Label runtime; - - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, tmp2, done, true /* new_val_may_be_null */); - // If card is young, jump to done - // card_addr and card are loaded by generate_post_barrier_fast_path - const Register card = tmp2; - const Register card_addr = tmp1; - __ b(done, eq); - generate_post_barrier_slow_path(masm, Rthread, card_addr, tmp2, tmp3, done, runtime); - - __ bind(runtime); - - RegisterSet set = RegisterSet(store_addr) | RegisterSet(R0, R3) | RegisterSet(R12); - __ push(set); - - if (card_addr != R0) { - __ mov(R0, card_addr); - } - __ mov(R1, Rthread); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), R0, R1); - - __ pop(set); - + generate_post_barrier_fast_path(masm, store_addr, new_val, Rthread, tmp1, tmp2, done, true /* new_val_may_be_null */); __ bind(done); } @@ -344,35 +305,10 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register tmp1, Register tmp2, Register tmp3, - G1PostBarrierStubC2* stub) { - assert(thread == Rthread, "must be"); - assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg); - - stub->initialize_registers(thread, tmp1, tmp2, tmp3); - - bool new_val_may_be_null = (stub->barrier_data() & G1C2BarrierPostNotNull) == 0; - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, tmp2, *stub->continuation(), new_val_may_be_null); - // If card is not young, jump to stub (slow path) - __ b(*stub->entry(), ne); - - __ bind(*stub->continuation()); -} - -void G1BarrierSetAssembler::generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const { - Assembler::InlineSkippedInstructionsCounter skip_counter(masm); - Label runtime; - Register thread = stub->thread(); - Register tmp1 = stub->tmp1(); // tmp1 holds the card address. - Register tmp2 = stub->tmp2(); - Register tmp3 = stub->tmp3(); - - __ bind(*stub->entry()); - generate_post_barrier_slow_path(masm, thread, tmp1, tmp2, tmp3, *stub->continuation(), runtime); - - __ bind(runtime); - generate_c2_barrier_runtime_call(masm, stub, tmp1, CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), tmp2); - __ b(*stub->continuation()); + bool new_val_may_be_null) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + __ bind(done); } #endif // COMPILER2 @@ -463,20 +399,19 @@ void G1BarrierSetAssembler::gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrier __ b(*stub->continuation()); } -void G1BarrierSetAssembler::gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub) { - G1BarrierSetC1* bs = (G1BarrierSetC1*)BarrierSet::barrier_set()->barrier_set_c1(); - __ bind(*stub->entry()); - assert(stub->addr()->is_register(), "Precondition."); - assert(stub->new_val()->is_register(), "Precondition."); - Register new_val_reg = stub->new_val()->as_register(); - __ cbz(new_val_reg, *stub->continuation()); - ce->verify_reserved_argument_area_size(1); - __ str(stub->addr()->as_pointer_register(), Address(SP)); - __ call(bs->post_barrier_c1_runtime_code_blob()->code_begin(), relocInfo::runtime_call_type); - __ b(*stub->continuation()); +#undef __ + +void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + masm->bind(done); } -#undef __ #define __ sasm-> void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm) { @@ -536,102 +471,6 @@ void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* __ b(done); } -void G1BarrierSetAssembler::generate_c1_post_barrier_runtime_stub(StubAssembler* sasm) { - // Input: - // - store_addr, pushed on the stack - - __ set_info("g1_post_barrier_slow_id", false); - - Label done; - Label recheck; - Label runtime; - - Address queue_index(Rthread, in_bytes(G1ThreadLocalData::dirty_card_queue_index_offset())); - Address buffer(Rthread, in_bytes(G1ThreadLocalData::dirty_card_queue_buffer_offset())); - - AddressLiteral cardtable(ci_card_table_address_as
        (), relocInfo::none); - - // save at least the registers that need saving if the runtime is called - const RegisterSet saved_regs = RegisterSet(R0,R3) | RegisterSet(R12) | RegisterSet(LR); - const int nb_saved_regs = 6; - assert(nb_saved_regs == saved_regs.size(), "fix nb_saved_regs"); - __ push(saved_regs); - - const Register r_card_addr_0 = R0; // must be R0 for the slow case - const Register r_obj_0 = R0; - const Register r_card_base_1 = R1; - const Register r_tmp2 = R2; - const Register r_index_2 = R2; - const Register r_buffer_3 = R3; - const Register tmp1 = Rtemp; - - __ ldr(r_obj_0, Address(SP, nb_saved_regs*wordSize)); - // Note: there is a comment in x86 code about not using - // ExternalAddress / lea, due to relocation not working - // properly for that address. Should be OK for arm, where we - // explicitly specify that 'cardtable' has a relocInfo::none - // type. - __ lea(r_card_base_1, cardtable); - __ add(r_card_addr_0, r_card_base_1, AsmOperand(r_obj_0, lsr, CardTable::card_shift())); - - // first quick check without barrier - __ ldrb(r_tmp2, Address(r_card_addr_0)); - - __ cmp(r_tmp2, (int)G1CardTable::g1_young_card_val()); - __ b(recheck, ne); - - __ bind(done); - - __ pop(saved_regs); - - __ ret(); - - __ bind(recheck); - - __ membar(MacroAssembler::Membar_mask_bits(MacroAssembler::StoreLoad), tmp1); - - // reload card state after the barrier that ensures the stored oop was visible - __ ldrb(r_tmp2, Address(r_card_addr_0)); - - assert(CardTable::dirty_card_val() == 0, "adjust this code"); - __ cbz(r_tmp2, done); - - // storing region crossing non-null, card is clean. - // dirty card and log. - - assert(0 == (int)CardTable::dirty_card_val(), "adjust this code"); - if ((ci_card_table_address_as() & 0xff) == 0) { - // Card table is aligned so the lowest byte of the table address base is zero. - __ strb(r_card_base_1, Address(r_card_addr_0)); - } else { - __ strb(__ zero_register(r_tmp2), Address(r_card_addr_0)); - } - - __ ldr(r_index_2, queue_index); - __ ldr(r_buffer_3, buffer); - - __ subs(r_index_2, r_index_2, wordSize); - __ b(runtime, lt); // go to runtime if now negative - - __ str(r_index_2, queue_index); - - __ str(r_card_addr_0, Address(r_buffer_3, r_index_2)); - - __ b(done); - - __ bind(runtime); - - __ save_live_registers(); - - assert(r_card_addr_0 == c_rarg0, "card_addr should be in R0"); - __ mov(c_rarg1, Rthread); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), c_rarg0, c_rarg1); - - __ restore_live_registers_without_return(); - - __ b(done); -} - #undef __ #endif // COMPILER1 diff --git a/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.hpp b/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.hpp index 4e49e655e3e..9e0eff4601b 100644 --- a/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.hpp +++ b/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,9 +32,7 @@ class LIR_Assembler; class StubAssembler; class G1PreBarrierStub; -class G1PostBarrierStub; class G1PreBarrierStubC2; -class G1PostBarrierStubC2; class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { protected: @@ -66,10 +64,15 @@ public: #ifdef COMPILER1 public: void gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrierStub* stub); - void gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub); void generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm); - void generate_c1_post_barrier_runtime_stub(StubAssembler* sasm); + + void g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2); #endif #ifdef COMPILER2 @@ -89,9 +92,7 @@ public: Register tmp1, Register tmp2, Register tmp3, - G1PostBarrierStubC2* c2_stub); - void generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const; + bool new_val_may_be_null); #endif }; diff --git a/src/hotspot/cpu/arm/gc/g1/g1_arm.ad b/src/hotspot/cpu/arm/gc/g1/g1_arm.ad index 8a0a9e1aa53..e905ba9ff67 100644 --- a/src/hotspot/cpu/arm/gc/g1/g1_arm.ad +++ b/src/hotspot/cpu/arm/gc/g1/g1_arm.ad @@ -1,5 +1,5 @@ // -// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. // DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. // // This code is free software; you can redistribute it and/or modify it @@ -63,13 +63,13 @@ static void write_barrier_post(MacroAssembler* masm, Register tmp1, Register tmp2, Register tmp3) { - if (!G1PostBarrierStubC2::needs_barrier(node)) { + if (!G1BarrierStubC2::needs_post_barrier(node)) { return; } Assembler::InlineSkippedInstructionsCounter skip_counter(masm); G1BarrierSetAssembler* g1_asm = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); - G1PostBarrierStubC2* const stub = G1PostBarrierStubC2::create(node); - g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, Rthread, tmp1, tmp2, tmp3, stub); + bool new_val_may_be_null = G1BarrierStubC2::post_new_val_may_be_null(node); + g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, Rthread, tmp1, tmp2, tmp3, new_val_may_be_null); } %} diff --git a/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp b/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp index 4fb13422f59..262bb1eae89 100644 --- a/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp +++ b/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp @@ -28,7 +28,6 @@ #include "gc/g1/g1BarrierSetAssembler.hpp" #include "gc/g1/g1BarrierSetRuntime.hpp" #include "gc/g1/g1CardTable.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1HeapRegion.hpp" #include "gc/g1/g1SATBMarkQueueSet.hpp" #include "gc/g1/g1ThreadLocalData.hpp" @@ -230,78 +229,52 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, Decorator __ bind(filtered); } -static void generate_region_crossing_test(MacroAssembler* masm, const Register store_addr, const Register new_val) { - __ xorr(R0, store_addr, new_val); // tmp1 := store address ^ new value - __ srdi_(R0, R0, G1HeapRegion::LogOfHRGrainBytes); // tmp1 := ((store address ^ new value) >> LogOfHRGrainBytes) -} +static void generate_post_barrier_fast_path(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register thread, + const Register tmp1, + const Register tmp2, + Label& done, + bool new_val_may_be_null) { + assert_different_registers(store_addr, new_val, tmp1, R0); + assert_different_registers(store_addr, tmp1, tmp2, R0); -static Address generate_card_young_test(MacroAssembler* masm, const Register store_addr, const Register tmp1, const Register tmp2) { - CardTableBarrierSet* ct = barrier_set_cast(BarrierSet::barrier_set()); - __ load_const_optimized(tmp1, (address)(ct->card_table()->byte_map_base()), tmp2); - __ srdi(tmp2, store_addr, CardTable::card_shift()); // tmp1 := card address relative to card table base - __ lbzx(R0, tmp1, tmp2); // tmp1 := card address - __ cmpwi(CR0, R0, (int)G1CardTable::g1_young_card_val()); - return Address(tmp1, tmp2); // return card address -} + __ xorr(R0, store_addr, new_val); // R0 := store address ^ new value + __ srdi_(R0, R0, G1HeapRegion::LogOfHRGrainBytes); // R0 := ((store address ^ new value) >> LogOfHRGrainBytes) + __ beq(CR0, done); -static void generate_card_dirty_test(MacroAssembler* masm, Address card_addr) { - __ membar(Assembler::StoreLoad); // Must reload after StoreLoad membar due to concurrent refinement - __ lbzx(R0, card_addr.base(), card_addr.index()); // tmp2 := card - __ cmpwi(CR0, R0, (int)G1CardTable::dirty_card_val()); // tmp2 := card == dirty_card_val? + // Crosses regions, storing null? + if (!new_val_may_be_null) { +#ifdef ASSERT + __ cmpdi(CR0, new_val, 0); + __ asm_assert_ne("null oop not allowed (G1 post)"); // Checked by caller. +#endif + } else { + __ cmpdi(CR0, new_val, 0); + __ beq(CR0, done); + } + + __ ld(tmp1, G1ThreadLocalData::card_table_base_offset(), thread); + __ srdi(tmp2, store_addr, CardTable::card_shift()); // tmp2 := card address relative to card table base + if (UseCondCardMark) { + __ lbzx(R0, tmp1, tmp2); + __ cmpwi(CR0, R0, (int)G1CardTable::clean_card_val()); + __ bne(CR0, done); + } + + __ li(R0, G1CardTable::dirty_card_val()); + __ stbx(R0, tmp1, tmp2); } void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, DecoratorSet decorators, Register store_addr, Register new_val, - Register tmp1, Register tmp2, Register tmp3, - MacroAssembler::PreservationLevel preservation_level) { + Register tmp1, Register tmp2) { bool not_null = (decorators & IS_NOT_NULL) != 0; - Label runtime, filtered; - assert_different_registers(store_addr, new_val, tmp1, tmp2); - - CardTableBarrierSet* ct = barrier_set_cast(BarrierSet::barrier_set()); - - generate_region_crossing_test(masm, store_addr, new_val); - __ beq(CR0, filtered); - - // Crosses regions, storing null? - if (not_null) { -#ifdef ASSERT - __ cmpdi(CR0, new_val, 0); - __ asm_assert_ne("null oop not allowed (G1 post)"); // Checked by caller. -#endif - } else { - __ cmpdi(CR0, new_val, 0); - __ beq(CR0, filtered); - } - - Address card_addr = generate_card_young_test(masm, store_addr, tmp1, tmp2); - __ beq(CR0, filtered); - - generate_card_dirty_test(masm, card_addr); - __ beq(CR0, filtered); - - __ li(R0, (int)G1CardTable::dirty_card_val()); - __ stbx(R0, card_addr.base(), card_addr.index()); // *(card address) := dirty_card_val - - Register Rcard_addr = tmp3; - __ add(Rcard_addr, card_addr.base(), card_addr.index()); // This is the address which needs to get enqueued. - - generate_queue_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - runtime, Rcard_addr, tmp1); - __ b(filtered); - - __ bind(runtime); - - assert(preservation_level == MacroAssembler::PRESERVATION_NONE, - "g1_write_barrier_post doesn't support preservation levels higher than PRESERVATION_NONE"); - - // Save the live input values. - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), Rcard_addr, R16_thread); - - __ bind(filtered); + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, R16_thread, tmp1, tmp2, done, !not_null); + __ bind(done); } void G1BarrierSetAssembler::oop_store_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, @@ -333,8 +306,7 @@ void G1BarrierSetAssembler::oop_store_at(MacroAssembler* masm, DecoratorSet deco } g1_write_barrier_post(masm, decorators, base, val, - tmp1, tmp2, tmp3, - preservation_level); + tmp1, tmp2); } } @@ -457,70 +429,29 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register new_val, Register tmp1, Register tmp2, - G1PostBarrierStubC2* stub, + bool new_val_may_be_null, bool decode_new_val) { assert_different_registers(store_addr, new_val, tmp1, R0); assert_different_registers(store_addr, tmp1, tmp2, R0); - stub->initialize_registers(R16_thread, tmp1, tmp2); + Label done; - bool null_check_required = (stub->barrier_data() & G1C2BarrierPostNotNull) == 0; Register new_val_decoded = new_val; if (decode_new_val) { assert(UseCompressedOops, "or should not be here"); - if (null_check_required && CompressedOops::base() != nullptr) { + if (new_val_may_be_null && CompressedOops::base() != nullptr) { // We prefer doing the null check after the region crossing check. // Only compressed oop modes with base != null require a null check here. __ cmpwi(CR0, new_val, 0); - __ beq(CR0, *stub->continuation()); - null_check_required = false; + __ beq(CR0, done); + new_val_may_be_null = false; } new_val_decoded = __ decode_heap_oop_not_null(tmp2, new_val); } - generate_region_crossing_test(masm, store_addr, new_val_decoded); - __ beq(CR0, *stub->continuation()); - - // crosses regions, storing null? - if (null_check_required) { - __ cmpdi(CR0, new_val_decoded, 0); - __ beq(CR0, *stub->continuation()); - } - - Address card_addr = generate_card_young_test(masm, store_addr, tmp1, tmp2); - assert(card_addr.base() == tmp1 && card_addr.index() == tmp2, "needed by post barrier stub"); - __ bc_far_optimized(Assembler::bcondCRbiIs0, __ bi0(CR0, Assembler::equal), *stub->entry()); - - __ bind(*stub->continuation()); -} - -void G1BarrierSetAssembler::generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const { - Assembler::InlineSkippedInstructionsCounter skip_counter(masm); - Label runtime; - Address card_addr(stub->tmp1(), stub->tmp2()); // See above. - - __ bind(*stub->entry()); - - generate_card_dirty_test(masm, card_addr); - __ bc_far_optimized(Assembler::bcondCRbiIs1, __ bi0(CR0, Assembler::equal), *stub->continuation()); - - __ li(R0, (int)G1CardTable::dirty_card_val()); - __ stbx(R0, card_addr.base(), card_addr.index()); // *(card address) := dirty_card_val - - Register Rcard_addr = stub->tmp1(); - __ add(Rcard_addr, card_addr.base(), card_addr.index()); // This is the address which needs to get enqueued. - - generate_queue_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - runtime, Rcard_addr, stub->tmp2()); - __ b(*stub->continuation()); - - __ bind(runtime); - generate_c2_barrier_runtime_call(masm, stub, Rcard_addr, CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry)); - __ b(*stub->continuation()); + generate_post_barrier_fast_path(masm, store_addr, new_val_decoded, R16_thread, tmp1, tmp2, done, new_val_may_be_null); + __ bind(done); } #endif // COMPILER2 @@ -558,28 +489,19 @@ void G1BarrierSetAssembler::gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrier __ b(*stub->continuation()); } -void G1BarrierSetAssembler::gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub) { - G1BarrierSetC1* bs = (G1BarrierSetC1*)BarrierSet::barrier_set()->barrier_set_c1(); - __ bind(*stub->entry()); +#undef __ - assert(stub->addr()->is_register(), "Precondition."); - assert(stub->new_val()->is_register(), "Precondition."); - Register addr_reg = stub->addr()->as_pointer_register(); - Register new_val_reg = stub->new_val()->as_register(); - - __ cmpdi(CR0, new_val_reg, 0); - __ bc_far_optimized(Assembler::bcondCRbiIs1, __ bi0(CR0, Assembler::equal), *stub->continuation()); - - address c_code = bs->post_barrier_c1_runtime_code_blob()->code_begin(); - //__ load_const_optimized(R0, c_code); - __ add_const_optimized(R0, R29_TOC, MacroAssembler::offset_to_global_toc(c_code)); - __ mtctr(R0); - __ mr(R0, addr_reg); // Pass addr in R0. - __ bctrl(); - __ b(*stub->continuation()); +void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + masm->bind(done); } -#undef __ #define __ sasm-> void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm) { @@ -642,86 +564,6 @@ void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* __ b(restart); } -void G1BarrierSetAssembler::generate_c1_post_barrier_runtime_stub(StubAssembler* sasm) { - G1BarrierSet* bs = barrier_set_cast(BarrierSet::barrier_set()); - - __ set_info("g1_post_barrier_slow_id", false); - - // Using stack slots: spill addr, spill tmp2 - const int stack_slots = 2; - Register tmp = R0; - Register addr = R14; - Register tmp2 = R15; - CardTable::CardValue* byte_map_base = bs->card_table()->byte_map_base(); - - Label restart, refill, ret; - - // Spill - __ std(addr, -8, R1_SP); - __ std(tmp2, -16, R1_SP); - - __ srdi(addr, R0, CardTable::card_shift()); // Addr is passed in R0. - __ load_const_optimized(/*cardtable*/ tmp2, byte_map_base, tmp); - __ add(addr, tmp2, addr); - __ lbz(tmp, 0, addr); // tmp := [addr + cardtable] - - // Return if young card. - __ cmpwi(CR0, tmp, G1CardTable::g1_young_card_val()); - __ beq(CR0, ret); - - // Return if sequential consistent value is already dirty. - __ membar(Assembler::StoreLoad); - __ lbz(tmp, 0, addr); // tmp := [addr + cardtable] - - __ cmpwi(CR0, tmp, G1CardTable::dirty_card_val()); - __ beq(CR0, ret); - - // Not dirty. - - // First, dirty it. - __ li(tmp, G1CardTable::dirty_card_val()); - __ stb(tmp, 0, addr); - - int dirty_card_q_index_byte_offset = in_bytes(G1ThreadLocalData::dirty_card_queue_index_offset()); - int dirty_card_q_buf_byte_offset = in_bytes(G1ThreadLocalData::dirty_card_queue_buffer_offset()); - - __ bind(restart); - - // Get the index into the update buffer. G1DirtyCardQueue::_index is - // a size_t so ld_ptr is appropriate here. - __ ld(tmp2, dirty_card_q_index_byte_offset, R16_thread); - - // index == 0? - __ cmpdi(CR0, tmp2, 0); - __ beq(CR0, refill); - - __ ld(tmp, dirty_card_q_buf_byte_offset, R16_thread); - __ addi(tmp2, tmp2, -oopSize); - - __ std(tmp2, dirty_card_q_index_byte_offset, R16_thread); - __ add(tmp2, tmp, tmp2); - __ std(addr, 0, tmp2); // [_buf + index] := - - // Restore temp registers and return-from-leaf. - __ bind(ret); - __ ld(tmp2, -16, R1_SP); - __ ld(addr, -8, R1_SP); - __ blr(); - - __ bind(refill); - const int nbytes_save = (MacroAssembler::num_volatile_regs + stack_slots) * BytesPerWord; - __ save_volatile_gprs(R1_SP, -nbytes_save); // except R0 - __ mflr(R0); - __ std(R0, _abi0(lr), R1_SP); - __ push_frame_reg_args(nbytes_save, R0); // dummy frame for C call - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1DirtyCardQueueSet::handle_zero_index_for_thread), R16_thread); - __ pop_frame(); - __ ld(R0, _abi0(lr), R1_SP); - __ mtlr(R0); - __ restore_volatile_gprs(R1_SP, -nbytes_save); // except R0 - __ b(restart); -} - #undef __ #endif // COMPILER1 diff --git a/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.hpp b/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.hpp index 33cb89dacc6..e059cc661af 100644 --- a/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.hpp +++ b/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018, 2021 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -37,9 +37,7 @@ class LIR_Assembler; class StubAssembler; class G1PreBarrierStub; -class G1PostBarrierStub; class G1PreBarrierStubC2; -class G1PostBarrierStubC2; class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { protected: @@ -56,8 +54,7 @@ protected: MacroAssembler::PreservationLevel preservation_level); void g1_write_barrier_post(MacroAssembler* masm, DecoratorSet decorators, Register store_addr, Register new_val, - Register tmp1, Register tmp2, Register tmp3, - MacroAssembler::PreservationLevel preservation_level); + Register tmp1, Register tmp2); virtual void oop_store_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, Register base, RegisterOrConstant ind_or_offs, Register val, @@ -79,17 +76,21 @@ public: Register new_val, Register tmp1, Register tmp2, - G1PostBarrierStubC2* c2_stub, + bool new_val_may_be_null, bool decode_new_val); - void generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const; #endif #ifdef COMPILER1 void gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrierStub* stub); - void gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub); void generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm); - void generate_c1_post_barrier_runtime_stub(StubAssembler* sasm); + + void g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2); + #endif virtual void load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, diff --git a/src/hotspot/cpu/ppc/gc/g1/g1_ppc.ad b/src/hotspot/cpu/ppc/gc/g1/g1_ppc.ad index 4f24efe872b..0a4a9442855 100644 --- a/src/hotspot/cpu/ppc/gc/g1/g1_ppc.ad +++ b/src/hotspot/cpu/ppc/gc/g1/g1_ppc.ad @@ -1,5 +1,5 @@ // -// Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. // Copyright (c) 2025 SAP SE. All rights reserved. // DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. // @@ -64,13 +64,13 @@ static void post_write_barrier(MacroAssembler* masm, Register tmp1, Register tmp2, bool decode_new_val = false) { - if (!G1PostBarrierStubC2::needs_barrier(node)) { + if (!G1BarrierStubC2::needs_post_barrier(node)) { return; } Assembler::InlineSkippedInstructionsCounter skip_counter(masm); G1BarrierSetAssembler* g1_asm = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); - G1PostBarrierStubC2* const stub = G1PostBarrierStubC2::create(node); - g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, tmp1, tmp2, stub, decode_new_val); + bool new_val_may_be_null = G1BarrierStubC2::post_new_val_may_be_null(node); + g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, tmp1, tmp2, new_val_may_be_null, decode_new_val); } %} diff --git a/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp b/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp index ef5dcdd8074..9c3bd93f8a6 100644 --- a/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp +++ b/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp @@ -87,15 +87,54 @@ void G1BarrierSetAssembler::gen_write_ref_array_pre_barrier(MacroAssembler* masm } } -void G1BarrierSetAssembler::gen_write_ref_array_post_barrier(MacroAssembler* masm, DecoratorSet decorators, - Register start, Register count, Register tmp, RegSet saved_regs) { - __ push_reg(saved_regs, sp); +void G1BarrierSetAssembler::gen_write_ref_array_post_barrier(MacroAssembler* masm, + DecoratorSet decorators, + Register start, + Register count, + Register tmp, + RegSet saved_regs) { assert_different_registers(start, count, tmp); - assert_different_registers(c_rarg0, count); - __ mv(c_rarg0, start); - __ mv(c_rarg1, count); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_array_post_entry), 2); - __ pop_reg(saved_regs, sp); + + Label loop, next, done; + + // Zero count? Nothing to do. + __ beqz(count, done); + + // Calculate the number of card marks to set. Since the object might start and + // end within a card, we need to calculate this via the card table indexes of + // the actual start and last addresses covered by the object. + // Temporarily use the count register for the last element address. + __ shadd(count, count, start, tmp, LogBytesPerHeapOop); // end = start + count << LogBytesPerHeapOop + __ subi(count, count, BytesPerHeapOop); // Use last element address for end. + + __ srli(start, start, CardTable::card_shift()); + __ srli(count, count, CardTable::card_shift()); + __ sub(count, count, start); // Number of bytes to mark - 1. + + // Add card table base offset to start. + Address card_table_address(xthread, G1ThreadLocalData::card_table_base_offset()); + __ ld(tmp, card_table_address); + __ add(start, start, tmp); + + __ bind(loop); + if (UseCondCardMark) { + __ add(tmp, start, count); + __ lbu(tmp, Address(tmp, 0)); + static_assert((uint)G1CardTable::clean_card_val() == 0xff, "must be"); + __ subi(tmp, tmp, G1CardTable::clean_card_val()); // Convert to clean_card_value() to a comparison + // against zero to avoid use of an extra temp. + __ bnez(tmp, next); + } + + __ add(tmp, start, count); + static_assert(G1CardTable::dirty_card_val() == 0, "must be to use zr"); + __ sb(zr, Address(tmp, 0)); + + __ bind(next); + __ subi(count, count, 1); + __ bgez(count, loop); + + __ bind(done); } static void generate_queue_test_and_insertion(MacroAssembler* masm, ByteSize index_offset, ByteSize buffer_offset, Label& runtime, @@ -192,44 +231,37 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, static void generate_post_barrier_fast_path(MacroAssembler* masm, const Register store_addr, const Register new_val, - const Register tmp1, - const Register tmp2, - Label& done, - bool new_val_may_be_null) { - // Does store cross heap regions? - __ xorr(tmp1, store_addr, new_val); // tmp1 := store address ^ new value - __ srli(tmp1, tmp1, G1HeapRegion::LogOfHRGrainBytes); // tmp1 := ((store address ^ new value) >> LogOfHRGrainBytes) - __ beqz(tmp1, done); - // Crosses regions, storing null? - if (new_val_may_be_null) { - __ beqz(new_val, done); - } - // Storing region crossing non-null, is card young? - __ srli(tmp1, store_addr, CardTable::card_shift()); // tmp1 := card address relative to card table base - __ load_byte_map_base(tmp2); // tmp2 := card table base address - __ add(tmp1, tmp1, tmp2); // tmp1 := card address - __ lbu(tmp2, Address(tmp1)); // tmp2 := card -} - -static void generate_post_barrier_slow_path(MacroAssembler* masm, const Register thread, const Register tmp1, const Register tmp2, Label& done, - Label& runtime) { - __ membar(MacroAssembler::StoreLoad); // StoreLoad membar - __ lbu(tmp2, Address(tmp1)); // tmp2 := card - __ beqz(tmp2, done, true); - // Storing a region crossing, non-null oop, card is clean. - // Dirty card and log. - STATIC_ASSERT(CardTable::dirty_card_val() == 0); - __ sb(zr, Address(tmp1)); // *(card address) := dirty_card_val - generate_queue_test_and_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - runtime, - thread, tmp1, tmp2, t0); - __ j(done); + bool new_val_may_be_null) { + assert(thread == xthread, "must be"); + assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg); + // Does store cross heap regions? + __ xorr(tmp1, store_addr, new_val); // tmp1 := store address ^ new value + __ srli(tmp1, tmp1, G1HeapRegion::LogOfHRGrainBytes); // tmp1 := ((store address ^ new value) >> LogOfHRGrainBytes) + __ beqz(tmp1, done); + + // Crosses regions, storing null? + if (new_val_may_be_null) { + __ beqz(new_val, done); + } + // Storing region crossing non-null, is card clean? + __ srli(tmp1, store_addr, CardTable::card_shift()); // tmp1 := card address relative to card table base + + Address card_table_address(xthread, G1ThreadLocalData::card_table_base_offset()); + __ ld(tmp2, card_table_address); // tmp2 := card table base address + __ add(tmp1, tmp1, tmp2); // tmp1 := card address + if (UseCondCardMark) { + static_assert((uint)G1CardTable::clean_card_val() == 0xff, "must be"); + __ lbu(tmp2, Address(tmp1, 0)); // tmp2 := card + __ subi(tmp2, tmp2, G1CardTable::clean_card_val()); // Convert to clean_card_value() to a comparison + // against zero to avoid use of an extra temp. + __ bnez(tmp2, done); + } + static_assert((uint)G1CardTable::dirty_card_val() == 0, "must be to use zr"); + __ sb(zr, Address(tmp1, 0)); } void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, @@ -238,27 +270,8 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Register thread, Register tmp1, Register tmp2) { - assert(thread == xthread, "must be"); - assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, t0); - assert(store_addr != noreg && new_val != noreg && tmp1 != noreg && tmp2 != noreg, - "expecting a register"); - Label done; - Label runtime; - - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, tmp2, done, true /* new_val_may_be_null */); - // If card is young, jump to done (tmp2 holds the card value) - __ mv(t0, (int)G1CardTable::g1_young_card_val()); - __ beq(tmp2, t0, done); // card == young_card_val? - generate_post_barrier_slow_path(masm, thread, tmp1, tmp2, done, runtime); - - __ bind(runtime); - // save the live input values - RegSet saved = RegSet::of(store_addr); - __ push_reg(saved, sp); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), tmp1, thread); - __ pop_reg(saved, sp); - + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); __ bind(done); } @@ -318,37 +331,10 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register thread, Register tmp1, Register tmp2, - G1PostBarrierStubC2* stub) { - assert(thread == xthread, "must be"); - assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, t0); - assert(store_addr != noreg && new_val != noreg && tmp1 != noreg && tmp2 != noreg, - "expecting a register"); - - stub->initialize_registers(thread, tmp1, tmp2); - - bool new_val_may_be_null = (stub->barrier_data() & G1C2BarrierPostNotNull) == 0; - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, tmp2, *stub->continuation(), new_val_may_be_null); - // If card is not young, jump to stub (slow path) (tmp2 holds the card value) - __ mv(t0, (int)G1CardTable::g1_young_card_val()); - __ bne(tmp2, t0, *stub->entry(), true); - - __ bind(*stub->continuation()); -} - -void G1BarrierSetAssembler::generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const { - Assembler::InlineSkippedInstructionsCounter skip_counter(masm); - Label runtime; - Register thread = stub->thread(); - Register tmp1 = stub->tmp1(); // tmp1 holds the card address. - Register tmp2 = stub->tmp2(); - - __ bind(*stub->entry()); - generate_post_barrier_slow_path(masm, thread, tmp1, tmp2, *stub->continuation(), runtime); - - __ bind(runtime); - generate_c2_barrier_runtime_call(masm, stub, tmp1, CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry)); - __ j(*stub->continuation()); + bool new_val_may_be_null) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + __ bind(done); } #endif // COMPILER2 @@ -443,20 +429,19 @@ void G1BarrierSetAssembler::gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrier __ j(*stub->continuation()); } -void G1BarrierSetAssembler::gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub) { - G1BarrierSetC1* bs = (G1BarrierSetC1*)BarrierSet::barrier_set()->barrier_set_c1(); - __ bind(*stub->entry()); - assert(stub->addr()->is_register(), "Precondition"); - assert(stub->new_val()->is_register(), "Precondition"); - Register new_val_reg = stub->new_val()->as_register(); - __ beqz(new_val_reg, *stub->continuation(), /* is_far */ true); - ce->store_parameter(stub->addr()->as_pointer_register(), 0); - __ far_call(RuntimeAddress(bs->post_barrier_c1_runtime_code_blob()->code_begin())); - __ j(*stub->continuation()); -} - #undef __ +void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + masm->bind(done); +} + #define __ sasm-> void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm) { @@ -507,74 +492,6 @@ void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* __ epilogue(); } -void G1BarrierSetAssembler::generate_c1_post_barrier_runtime_stub(StubAssembler* sasm) { - __ prologue("g1_post_barrier", false); - - // arg0 : store_address - Address store_addr(fp, 2 * BytesPerWord); // 2 BytesPerWord from fp - - BarrierSet* bs = BarrierSet::barrier_set(); - CardTableBarrierSet* ctbs = barrier_set_cast(bs); - - Label done; - Label runtime; - - // At this point we know new_value is non-null and the new_value crosses regions. - // Must check to see if card is already dirty - const Register thread = xthread; - - Address queue_index(thread, in_bytes(G1ThreadLocalData::dirty_card_queue_index_offset())); - Address buffer(thread, in_bytes(G1ThreadLocalData::dirty_card_queue_buffer_offset())); - - const Register card_offset = t1; - // RA is free here, so we can use it to hold the byte_map_base. - const Register byte_map_base = ra; - - assert_different_registers(card_offset, byte_map_base, t0); - - __ load_parameter(0, card_offset); - __ srli(card_offset, card_offset, CardTable::card_shift()); - __ load_byte_map_base(byte_map_base); - - // Convert card offset into an address in card_addr - Register card_addr = card_offset; - __ add(card_addr, byte_map_base, card_addr); - - __ lbu(t0, Address(card_addr, 0)); - __ sub(t0, t0, (int)G1CardTable::g1_young_card_val()); - __ beqz(t0, done); - - assert((int)CardTable::dirty_card_val() == 0, "must be 0"); - - __ membar(MacroAssembler::StoreLoad); - __ lbu(t0, Address(card_addr, 0)); - __ beqz(t0, done); - - // storing region crossing non-null, card is clean. - // dirty card and log. - __ sb(zr, Address(card_addr, 0)); - - __ ld(t0, queue_index); - __ beqz(t0, runtime); - __ subi(t0, t0, wordSize); - __ sd(t0, queue_index); - - // Reuse RA to hold buffer_addr - const Register buffer_addr = ra; - - __ ld(buffer_addr, buffer); - __ add(t0, buffer_addr, t0); - __ sd(card_addr, Address(t0, 0)); - __ j(done); - - __ bind(runtime); - __ push_call_clobbered_registers(); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), card_addr, thread); - __ pop_call_clobbered_registers(); - __ bind(done); - __ epilogue(); -} - #undef __ #endif // COMPILER1 diff --git a/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.hpp b/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.hpp index 26310231362..654ba934242 100644 --- a/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.hpp +++ b/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, 2024, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -35,9 +35,7 @@ class LIR_Assembler; #endif class StubAssembler; class G1PreBarrierStub; -class G1PostBarrierStub; class G1PreBarrierStubC2; -class G1PostBarrierStubC2; class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { protected: @@ -68,10 +66,16 @@ protected: public: #ifdef COMPILER1 void gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrierStub* stub); - void gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub); void generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm); - void generate_c1_post_barrier_runtime_stub(StubAssembler* sasm); + + void g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2); + #endif #ifdef COMPILER2 @@ -90,9 +94,7 @@ public: Register thread, Register tmp1, Register tmp2, - G1PostBarrierStubC2* c2_stub); - void generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const; + bool new_val_may_be_null); #endif void load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, diff --git a/src/hotspot/cpu/riscv/gc/g1/g1_riscv.ad b/src/hotspot/cpu/riscv/gc/g1/g1_riscv.ad index 7a525323021..8461a36e68c 100644 --- a/src/hotspot/cpu/riscv/gc/g1/g1_riscv.ad +++ b/src/hotspot/cpu/riscv/gc/g1/g1_riscv.ad @@ -1,5 +1,5 @@ // -// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. // Copyright (c) 2024, Huawei Technologies Co., Ltd. All rights reserved. // DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. // @@ -63,13 +63,13 @@ static void write_barrier_post(MacroAssembler* masm, Register new_val, Register tmp1, Register tmp2) { - if (!G1PostBarrierStubC2::needs_barrier(node)) { + if (!G1BarrierStubC2::needs_post_barrier(node)) { return; } Assembler::InlineSkippedInstructionsCounter skip_counter(masm); G1BarrierSetAssembler* g1_asm = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); - G1PostBarrierStubC2* const stub = G1PostBarrierStubC2::create(node); - g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, xthread, tmp1, tmp2, stub); + bool new_val_may_be_null = G1BarrierStubC2::post_new_val_may_be_null(node); + g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, xthread, tmp1, tmp2, new_val_may_be_null); } %} diff --git a/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp b/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp index dea3317270e..3e176309c27 100644 --- a/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp +++ b/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp @@ -28,7 +28,6 @@ #include "gc/g1/g1BarrierSetAssembler.hpp" #include "gc/g1/g1BarrierSetRuntime.hpp" #include "gc/g1/g1CardTable.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1HeapRegion.hpp" #include "gc/g1/g1SATBMarkQueueSet.hpp" #include "gc/g1/g1ThreadLocalData.hpp" @@ -205,104 +204,71 @@ void G1BarrierSetAssembler::generate_c2_pre_barrier_stub(MacroAssembler* masm, BLOCK_COMMENT("} generate_c2_pre_barrier_stub"); } +static void generate_post_barrier_fast_path(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register thread, + const Register tmp1, + const Register tmp2, + Label& done, + bool new_val_may_be_null) { + + __ block_comment("generate_post_barrier_fast_path {"); + + assert(thread == Z_thread, "must be"); + assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg); + + // Does store cross heap regions? + if (VM_Version::has_DistinctOpnds()) { + __ z_xgrk(tmp1, store_addr, new_val); // tmp1 := store address ^ new value + } else { + __ z_lgr(tmp1, store_addr); + __ z_xgr(tmp1, new_val); + } + __ z_srag(tmp1, tmp1, G1HeapRegion::LogOfHRGrainBytes); // tmp1 := ((store address ^ new value) >> LogOfHRGrainBytes) + __ branch_optimized(Assembler::bcondEqual, done); + + // Crosses regions, storing null? + if (new_val_may_be_null) { + __ z_ltgr(new_val, new_val); + __ z_bre(done); + } else { +#ifdef ASSERT + __ z_ltgr(new_val, new_val); + __ asm_assert(Assembler::bcondNotZero, "null oop not allowed (G1 post)", 0x322); // Checked by caller. +#endif + } + + __ z_srag(tmp1, store_addr, CardTable::card_shift()); + + Address card_table_addr(thread, in_bytes(G1ThreadLocalData::card_table_base_offset())); + __ z_alg(tmp1, card_table_addr); // tmp1 := card address + + if(UseCondCardMark) { + __ z_cli(0, tmp1, G1CardTable::clean_card_val()); + __ branch_optimized(Assembler::bcondNotEqual, done); + } + + static_assert(G1CardTable::dirty_card_val() == 0, "must be to use z_mvi"); + __ z_mvi(0, tmp1, G1CardTable::dirty_card_val()); // *(card address) := dirty_card_val + + __ block_comment("} generate_post_barrier_fast_path"); +} + void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register store_addr, Register new_val, Register thread, Register tmp1, Register tmp2, - G1PostBarrierStubC2* stub) { + bool new_val_may_be_null) { BLOCK_COMMENT("g1_write_barrier_post_c2 {"); - - assert(thread == Z_thread, "must be"); - assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, Z_R1_scratch); - - assert(store_addr != noreg && new_val != noreg && tmp1 != noreg && tmp2 != noreg, "expecting a register"); - - stub->initialize_registers(thread, tmp1, tmp2); - - BLOCK_COMMENT("generate_region_crossing_test {"); - if (VM_Version::has_DistinctOpnds()) { - __ z_xgrk(tmp1, store_addr, new_val); - } else { - __ z_lgr(tmp1, store_addr); - __ z_xgr(tmp1, new_val); - } - __ z_srag(tmp1, tmp1, G1HeapRegion::LogOfHRGrainBytes); - __ branch_optimized(Assembler::bcondEqual, *stub->continuation()); - BLOCK_COMMENT("} generate_region_crossing_test"); - - // crosses regions, storing null? - if ((stub->barrier_data() & G1C2BarrierPostNotNull) == 0) { - __ z_ltgr(new_val, new_val); - __ branch_optimized(Assembler::bcondEqual, *stub->continuation()); - } - - BLOCK_COMMENT("generate_card_young_test {"); - CardTableBarrierSet* ct = barrier_set_cast(BarrierSet::barrier_set()); - // calculate address of card - __ load_const_optimized(tmp2, (address)ct->card_table()->byte_map_base()); // Card table base. - __ z_srlg(tmp1, store_addr, CardTable::card_shift()); // Index into card table. - __ z_algr(tmp1, tmp2); // Explicit calculation needed for cli. - - // Filter young. - __ z_cli(0, tmp1, G1CardTable::g1_young_card_val()); - - BLOCK_COMMENT("} generate_card_young_test"); - - // From here on, tmp1 holds the card address. - __ branch_optimized(Assembler::bcondNotEqual, *stub->entry()); - - __ bind(*stub->continuation()); - + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + __ bind(done); BLOCK_COMMENT("} g1_write_barrier_post_c2"); } -void G1BarrierSetAssembler::generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const { - - BLOCK_COMMENT("generate_c2_post_barrier_stub {"); - - Assembler::InlineSkippedInstructionsCounter skip_counter(masm); - Label runtime; - - Register thread = stub->thread(); - Register tmp1 = stub->tmp1(); // tmp1 holds the card address. - Register tmp2 = stub->tmp2(); - Register Rcard_addr = tmp1; - - __ bind(*stub->entry()); - - BLOCK_COMMENT("generate_card_clean_test {"); - __ z_sync(); // Required to support concurrent cleaning. - __ z_cli(0, Rcard_addr, 0); // Reload after membar. - __ branch_optimized(Assembler::bcondEqual, *stub->continuation()); - BLOCK_COMMENT("} generate_card_clean_test"); - - BLOCK_COMMENT("generate_dirty_card {"); - // Storing a region crossing, non-null oop, card is clean. - // Dirty card and log. - STATIC_ASSERT(CardTable::dirty_card_val() == 0); - __ z_mvi(0, Rcard_addr, CardTable::dirty_card_val()); - BLOCK_COMMENT("} generate_dirty_card"); - - generate_queue_test_and_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - runtime, - Z_thread, tmp1, tmp2); - - __ branch_optimized(Assembler::bcondAlways, *stub->continuation()); - - __ bind(runtime); - - generate_c2_barrier_runtime_call(masm, stub, tmp1, CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry)); - - __ branch_optimized(Assembler::bcondAlways, *stub->continuation()); - - BLOCK_COMMENT("} generate_c2_post_barrier_stub"); -} - #endif //COMPILER2 void G1BarrierSetAssembler::load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, @@ -451,99 +417,9 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Decorato Register Rtmp1, Register Rtmp2, Register Rtmp3) { bool not_null = (decorators & IS_NOT_NULL) != 0; - assert_different_registers(Rstore_addr, Rnew_val, Rtmp1, Rtmp2); // Most probably, Rnew_val == Rtmp3. - - Label callRuntime, filtered; - - CardTableBarrierSet* ct = barrier_set_cast(BarrierSet::barrier_set()); - - BLOCK_COMMENT("g1_write_barrier_post {"); - - // Does store cross heap regions? - // It does if the two addresses specify different grain addresses. - if (VM_Version::has_DistinctOpnds()) { - __ z_xgrk(Rtmp1, Rstore_addr, Rnew_val); - } else { - __ z_lgr(Rtmp1, Rstore_addr); - __ z_xgr(Rtmp1, Rnew_val); - } - __ z_srag(Rtmp1, Rtmp1, G1HeapRegion::LogOfHRGrainBytes); - __ z_bre(filtered); - - // Crosses regions, storing null? - if (not_null) { -#ifdef ASSERT - __ z_ltgr(Rnew_val, Rnew_val); - __ asm_assert(Assembler::bcondNotZero, "null oop not allowed (G1 post)", 0x322); // Checked by caller. -#endif - } else { - __ z_ltgr(Rnew_val, Rnew_val); - __ z_bre(filtered); - } - - Rnew_val = noreg; // end of lifetime - - // Storing region crossing non-null, is card already dirty? - assert_different_registers(Rtmp1, Rtmp2, Rtmp3); - // Make sure not to use Z_R0 for any of these registers. - Register Rcard_addr = (Rtmp1 != Z_R0_scratch) ? Rtmp1 : Rtmp3; - Register Rbase = (Rtmp2 != Z_R0_scratch) ? Rtmp2 : Rtmp3; - - // calculate address of card - __ load_const_optimized(Rbase, (address)ct->card_table()->byte_map_base()); // Card table base. - __ z_srlg(Rcard_addr, Rstore_addr, CardTable::card_shift()); // Index into card table. - __ z_algr(Rcard_addr, Rbase); // Explicit calculation needed for cli. - Rbase = noreg; // end of lifetime - - // Filter young. - __ z_cli(0, Rcard_addr, G1CardTable::g1_young_card_val()); - __ z_bre(filtered); - - // Check the card value. If dirty, we're done. - // This also avoids false sharing of the (already dirty) card. - __ z_sync(); // Required to support concurrent cleaning. - __ z_cli(0, Rcard_addr, G1CardTable::dirty_card_val()); // Reload after membar. - __ z_bre(filtered); - - // Storing a region crossing, non-null oop, card is clean. - // Dirty card and log. - __ z_mvi(0, Rcard_addr, G1CardTable::dirty_card_val()); - - Register Rcard_addr_x = Rcard_addr; - Register Rqueue_index = (Rtmp2 != Z_R0_scratch) ? Rtmp2 : Rtmp1; - if (Rcard_addr == Rqueue_index) { - Rcard_addr_x = Z_R0_scratch; // Register shortage. We have to use Z_R0. - } - __ lgr_if_needed(Rcard_addr_x, Rcard_addr); - - generate_queue_test_and_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - callRuntime, - Z_thread, Rcard_addr_x, Rqueue_index); - __ z_bru(filtered); - - __ bind(callRuntime); - - // TODO: do we need a frame? Introduced to be on the safe side. - bool needs_frame = true; - __ lgr_if_needed(Rcard_addr, Rcard_addr_x); // copy back asap. push_frame will destroy Z_R0_scratch! - - // VM call need frame to access(write) O register. - if (needs_frame) { - __ save_return_pc(); - __ push_frame_abi160(0); // Will use Z_R0 as tmp on old CPUs. - } - - // Save the live input values. - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), Rcard_addr, Z_thread); - - if (needs_frame) { - __ pop_frame(); - __ restore_return_pc(); - } - - __ bind(filtered); + Label done; + generate_post_barrier_fast_path(masm, Rstore_addr, Rnew_val, Z_thread, Rtmp1, Rtmp2, done, !not_null); + __ bind(done); BLOCK_COMMENT("} g1_write_barrier_post"); } @@ -615,22 +491,19 @@ void G1BarrierSetAssembler::gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrier __ branch_optimized(Assembler::bcondAlways, *stub->continuation()); } -void G1BarrierSetAssembler::gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub) { - G1BarrierSetC1* bs = (G1BarrierSetC1*)BarrierSet::barrier_set()->barrier_set_c1(); - __ bind(*stub->entry()); - ce->check_reserved_argument_area(16); // RT stub needs 2 spill slots. - assert(stub->addr()->is_register(), "Precondition."); - assert(stub->new_val()->is_register(), "Precondition."); - Register new_val_reg = stub->new_val()->as_register(); - __ z_ltgr(new_val_reg, new_val_reg); - __ branch_optimized(Assembler::bcondZero, *stub->continuation()); - __ z_lgr(Z_R1_scratch, stub->addr()->as_pointer_register()); - ce->emit_call_c(bs->post_barrier_c1_runtime_code_blob()->code_begin()); - __ branch_optimized(Assembler::bcondAlways, *stub->continuation()); -} - #undef __ +void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + masm->bind(done); +} + #define __ sasm-> static OopMap* save_volatile_registers(StubAssembler* sasm, Register return_pc = Z_R14) { @@ -705,92 +578,6 @@ void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* __ z_bru(restart); } -void G1BarrierSetAssembler::generate_c1_post_barrier_runtime_stub(StubAssembler* sasm) { - // Z_R1_scratch: oop address, address of updated memory slot - - BarrierSet* bs = BarrierSet::barrier_set(); - __ set_info("g1_post_barrier_slow_id", false); - - Register addr_oop = Z_R1_scratch; - Register addr_card = Z_R1_scratch; - Register r1 = Z_R6; // Must be saved/restored. - Register r2 = Z_R7; // Must be saved/restored. - Register cardtable = r1; // Must be non-volatile, because it is used to save addr_card. - CardTableBarrierSet* ctbs = barrier_set_cast(bs); - CardTable* ct = ctbs->card_table(); - CardTable::CardValue* byte_map_base = ct->byte_map_base(); - - // Save registers used below (see assertion in G1PreBarrierStub::emit_code()). - __ z_stg(r1, 0*BytesPerWord + FrameMap::first_available_sp_in_frame, Z_SP); - - Label not_already_dirty, restart, refill, young_card; - - // Calculate address of card corresponding to the updated oop slot. - AddressLiteral rs(byte_map_base); - __ z_srlg(addr_card, addr_oop, CardTable::card_shift()); - addr_oop = noreg; // dead now - __ load_const_optimized(cardtable, rs); // cardtable := - __ z_agr(addr_card, cardtable); // addr_card := addr_oop>>card_shift + cardtable - - __ z_cli(0, addr_card, (int)G1CardTable::g1_young_card_val()); - __ z_bre(young_card); - - __ z_sync(); // Required to support concurrent cleaning. - - __ z_cli(0, addr_card, (int)CardTable::dirty_card_val()); - __ z_brne(not_already_dirty); - - __ bind(young_card); - // We didn't take the branch, so we're already dirty: restore - // used registers and return. - __ z_lg(r1, 0*BytesPerWord + FrameMap::first_available_sp_in_frame, Z_SP); - __ z_br(Z_R14); - - // Not dirty. - __ bind(not_already_dirty); - - // First, dirty it: [addr_card] := 0 - __ z_mvi(0, addr_card, CardTable::dirty_card_val()); - - Register idx = cardtable; // Must be non-volatile, because it is used to save addr_card. - Register buf = r2; - cardtable = noreg; // now dead - - // Save registers used below (see assertion in G1PreBarrierStub::emit_code()). - __ z_stg(r2, 1*BytesPerWord + FrameMap::first_available_sp_in_frame, Z_SP); - - ByteSize dirty_card_q_index_byte_offset = G1ThreadLocalData::dirty_card_queue_index_offset(); - ByteSize dirty_card_q_buf_byte_offset = G1ThreadLocalData::dirty_card_queue_buffer_offset(); - - __ bind(restart); - - // Get the index into the update buffer. G1DirtyCardQueue::_index is - // a size_t so z_ltg is appropriate here. - __ z_ltg(idx, Address(Z_thread, dirty_card_q_index_byte_offset)); - - // index == 0? - __ z_brz(refill); - - __ z_lg(buf, Address(Z_thread, dirty_card_q_buf_byte_offset)); - __ add2reg(idx, -oopSize); - - __ z_stg(addr_card, 0, idx, buf); // [_buf + index] := - __ z_stg(idx, Address(Z_thread, dirty_card_q_index_byte_offset)); - // Restore killed registers and return. - __ z_lg(r1, 0*BytesPerWord + FrameMap::first_available_sp_in_frame, Z_SP); - __ z_lg(r2, 1*BytesPerWord + FrameMap::first_available_sp_in_frame, Z_SP); - __ z_br(Z_R14); - - __ bind(refill); - save_volatile_registers(sasm); - __ z_lgr(idx, addr_card); // Save addr_card, tmp3 must be non-volatile. - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1DirtyCardQueueSet::handle_zero_index_for_thread), - Z_thread); - __ z_lgr(addr_card, idx); - restore_volatile_registers(sasm); // Restore addr_card. - __ z_bru(restart); -} - #undef __ #endif // COMPILER1 diff --git a/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.hpp b/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.hpp index 0f0bdd8b83c..fdec751c43b 100644 --- a/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.hpp +++ b/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018, 2024 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -33,9 +33,7 @@ class LIR_Assembler; class StubAssembler; class G1PreBarrierStub; -class G1PostBarrierStub; class G1PreBarrierStubC2; -class G1PostBarrierStubC2; class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { protected: @@ -60,10 +58,16 @@ class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { public: #ifdef COMPILER1 void gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrierStub* stub); - void gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub); void generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm); - void generate_c1_post_barrier_runtime_stub(StubAssembler* sasm); + + void g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2); + #endif // COMPILER1 #ifdef COMPILER2 @@ -81,9 +85,7 @@ class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { Register thread, Register tmp1, Register tmp2, - G1PostBarrierStubC2* c2_stub); - void generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const; + bool new_val_may_be_null); #endif // COMPILER2 virtual void load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, diff --git a/src/hotspot/cpu/s390/gc/g1/g1_s390.ad b/src/hotspot/cpu/s390/gc/g1/g1_s390.ad index 31f60c4aeff..7aed374fdae 100644 --- a/src/hotspot/cpu/s390/gc/g1/g1_s390.ad +++ b/src/hotspot/cpu/s390/gc/g1/g1_s390.ad @@ -1,5 +1,5 @@ // -// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. // Copyright 2024 IBM Corporation. All rights reserved. // DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. // @@ -62,13 +62,13 @@ static void write_barrier_post(MacroAssembler* masm, Register new_val, Register tmp1, Register tmp2) { - if (!G1PostBarrierStubC2::needs_barrier(node)) { + if (!G1BarrierStubC2::needs_post_barrier(node)) { return; } Assembler::InlineSkippedInstructionsCounter skip_counter(masm); G1BarrierSetAssembler* g1_asm = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); - G1PostBarrierStubC2* const stub = G1PostBarrierStubC2::create(node); - g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, Z_thread, tmp1, tmp2, stub); + bool new_val_may_be_null = G1BarrierStubC2::post_new_val_may_be_null(node); + g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, Z_thread, tmp1, tmp2, new_val_may_be_null); } %} // source diff --git a/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp b/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp index c1920b52837..31f27e140e0 100644 --- a/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp @@ -89,19 +89,53 @@ void G1BarrierSetAssembler::gen_write_ref_array_pre_barrier(MacroAssembler* masm void G1BarrierSetAssembler::gen_write_ref_array_post_barrier(MacroAssembler* masm, DecoratorSet decorators, Register addr, Register count, Register tmp) { - __ push_call_clobbered_registers(false /* save_fpu */); - if (c_rarg0 == count) { // On win64 c_rarg0 == rcx - assert_different_registers(c_rarg1, addr); - __ mov(c_rarg1, count); - __ mov(c_rarg0, addr); - } else { - assert_different_registers(c_rarg0, count); - __ mov(c_rarg0, addr); - __ mov(c_rarg1, count); - } - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_array_post_entry), 2); - __ pop_call_clobbered_registers(false /* save_fpu */); + Label done; + __ testptr(count, count); + __ jcc(Assembler::zero, done); + + // Calculate end address in "count". + Address::ScaleFactor scale = UseCompressedOops ? Address::times_4 : Address::times_8; + __ leaq(count, Address(addr, count, scale)); + + // Calculate start card address in "addr". + __ shrptr(addr, CardTable::card_shift()); + + Register thread = r15_thread; + + __ movptr(tmp, Address(thread, in_bytes(G1ThreadLocalData::card_table_base_offset()))); + __ addptr(addr, tmp); + + // Calculate address of card of last word in the array. + __ subptr(count, 1); + __ shrptr(count, CardTable::card_shift()); + __ addptr(count, tmp); + + Label loop; + // Iterate from start card to end card (inclusive). + __ bind(loop); + + Label is_clean_card; + if (UseCondCardMark) { + __ cmpb(Address(addr, 0), G1CardTable::clean_card_val()); + __ jcc(Assembler::equal, is_clean_card); + } else { + __ movb(Address(addr, 0), G1CardTable::dirty_card_val()); + } + + Label next_card; + __ bind(next_card); + __ addptr(addr, sizeof(CardTable::CardValue)); + __ cmpptr(addr, count); + __ jcc(Assembler::belowEqual, loop); + __ jmp(done); + + __ bind(is_clean_card); + // Card was clean. Dirty card and go to next.. + __ movb(Address(addr, 0), G1CardTable::dirty_card_val()); + __ jmp(next_card); + + __ bind(done); } void G1BarrierSetAssembler::load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, @@ -182,7 +216,6 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, // If expand_call is true then we expand the call_VM_leaf macro // directly to skip generating the check by // InterpreterMacroAssembler::call_VM_leaf_base that checks _last_sp. - const Register thread = r15_thread; Label done; @@ -238,73 +271,46 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, static void generate_post_barrier_fast_path(MacroAssembler* masm, const Register store_addr, const Register new_val, - const Register tmp, - const Register tmp2, + const Register tmp1, Label& done, bool new_val_may_be_null) { - CardTableBarrierSet* ct = barrier_set_cast(BarrierSet::barrier_set()); + + assert_different_registers(store_addr, new_val, tmp1, noreg); + + Register thread = r15_thread; + // Does store cross heap regions? - __ movptr(tmp, store_addr); // tmp := store address - __ xorptr(tmp, new_val); // tmp := store address ^ new value - __ shrptr(tmp, G1HeapRegion::LogOfHRGrainBytes); // ((store address ^ new value) >> LogOfHRGrainBytes) == 0? + __ movptr(tmp1, store_addr); // tmp1 := store address + __ xorptr(tmp1, new_val); // tmp1 := store address ^ new value + __ shrptr(tmp1, G1HeapRegion::LogOfHRGrainBytes); // ((store address ^ new value) >> LogOfHRGrainBytes) == 0? __ jcc(Assembler::equal, done); + // Crosses regions, storing null? if (new_val_may_be_null) { - __ cmpptr(new_val, NULL_WORD); // new value == null? + __ cmpptr(new_val, NULL_WORD); // new value == null? __ jcc(Assembler::equal, done); } - // Storing region crossing non-null, is card young? - __ movptr(tmp, store_addr); // tmp := store address - __ shrptr(tmp, CardTable::card_shift()); // tmp := card address relative to card table base - // Do not use ExternalAddress to load 'byte_map_base', since 'byte_map_base' is NOT - // a valid address and therefore is not properly handled by the relocation code. - __ movptr(tmp2, (intptr_t)ct->card_table()->byte_map_base()); // tmp2 := card table base address - __ addptr(tmp, tmp2); // tmp := card address - __ cmpb(Address(tmp, 0), G1CardTable::g1_young_card_val()); // *(card address) == young_card_val? -} -static void generate_post_barrier_slow_path(MacroAssembler* masm, - const Register thread, - const Register tmp, - const Register tmp2, - Label& done, - Label& runtime) { - __ membar(Assembler::Membar_mask_bits(Assembler::StoreLoad)); // StoreLoad membar - __ cmpb(Address(tmp, 0), G1CardTable::dirty_card_val()); // *(card address) == dirty_card_val? - __ jcc(Assembler::equal, done); + __ movptr(tmp1, store_addr); // tmp1 := store address + __ shrptr(tmp1, CardTable::card_shift()); // tmp1 := card address relative to card table base + + Address card_table_addr(thread, in_bytes(G1ThreadLocalData::card_table_base_offset())); + __ addptr(tmp1, card_table_addr); // tmp1 := card address + if (UseCondCardMark) { + __ cmpb(Address(tmp1, 0), G1CardTable::clean_card_val()); // *(card address) == clean_card_val? + __ jcc(Assembler::notEqual, done); + } // Storing a region crossing, non-null oop, card is clean. - // Dirty card and log. - __ movb(Address(tmp, 0), G1CardTable::dirty_card_val()); // *(card address) := dirty_card_val - generate_queue_insertion(masm, - G1ThreadLocalData::dirty_card_queue_index_offset(), - G1ThreadLocalData::dirty_card_queue_buffer_offset(), - runtime, - thread, tmp, tmp2); - __ jmp(done); + // Dirty card. + __ movb(Address(tmp1, 0), G1CardTable::dirty_card_val()); // *(card address) := dirty_card_val } void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Register store_addr, Register new_val, - Register tmp, - Register tmp2) { - const Register thread = r15_thread; - + Register tmp) { Label done; - Label runtime; - - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp, tmp2, done, true /* new_val_may_be_null */); - // If card is young, jump to done - __ jcc(Assembler::equal, done); - generate_post_barrier_slow_path(masm, thread, tmp, tmp2, done, runtime); - - __ bind(runtime); - // save the live input values - RegSet saved = RegSet::of(store_addr); - __ push_set(saved); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), tmp, thread); - __ pop_set(saved); - + generate_post_barrier_fast_path(masm, store_addr, new_val, tmp, done, true /* new_val_may_be_null */); __ bind(done); } @@ -367,34 +373,10 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register store_addr, Register new_val, Register tmp, - Register tmp2, - G1PostBarrierStubC2* stub) { - const Register thread = r15_thread; - stub->initialize_registers(thread, tmp, tmp2); - - bool new_val_may_be_null = (stub->barrier_data() & G1C2BarrierPostNotNull) == 0; - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp, tmp2, *stub->continuation(), new_val_may_be_null); - // If card is not young, jump to stub (slow path) - __ jcc(Assembler::notEqual, *stub->entry()); - - __ bind(*stub->continuation()); -} - -void G1BarrierSetAssembler::generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const { - Assembler::InlineSkippedInstructionsCounter skip_counter(masm); - Label runtime; - Register thread = stub->thread(); - Register tmp = stub->tmp1(); // tmp holds the card address. - Register tmp2 = stub->tmp2(); - assert(stub->tmp3() == noreg, "not needed in this platform"); - - __ bind(*stub->entry()); - generate_post_barrier_slow_path(masm, thread, tmp, tmp2, *stub->continuation(), runtime); - - __ bind(runtime); - generate_c2_barrier_runtime_call(masm, stub, tmp, CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry)); - __ jmp(*stub->continuation()); + bool new_val_may_be_null) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, tmp, done, new_val_may_be_null); + __ bind(done); } #endif // COMPILER2 @@ -441,8 +423,7 @@ void G1BarrierSetAssembler::oop_store_at(MacroAssembler* masm, DecoratorSet deco g1_write_barrier_post(masm /*masm*/, tmp1 /* store_adr */, new_val /* new_val */, - tmp3 /* tmp */, - tmp2 /* tmp2 */); + tmp3 /* tmp */); } } } @@ -476,21 +457,19 @@ void G1BarrierSetAssembler::gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrier } -void G1BarrierSetAssembler::gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub) { - G1BarrierSetC1* bs = (G1BarrierSetC1*)BarrierSet::barrier_set()->barrier_set_c1(); - __ bind(*stub->entry()); - assert(stub->addr()->is_register(), "Precondition."); - assert(stub->new_val()->is_register(), "Precondition."); - Register new_val_reg = stub->new_val()->as_register(); - __ cmpptr(new_val_reg, NULL_WORD); - __ jcc(Assembler::equal, *stub->continuation()); - ce->store_parameter(stub->addr()->as_pointer_register(), 0); - __ call(RuntimeAddress(bs->post_barrier_c1_runtime_code_blob()->code_begin())); - __ jmp(*stub->continuation()); -} - #undef __ +void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2 /* unused on x86 */) { + Label done; + generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, done, true /* new_val_may_be_null */); + masm->bind(done); +} + #define __ sasm-> void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm) { @@ -555,78 +534,6 @@ void G1BarrierSetAssembler::generate_c1_pre_barrier_runtime_stub(StubAssembler* __ epilogue(); } -void G1BarrierSetAssembler::generate_c1_post_barrier_runtime_stub(StubAssembler* sasm) { - __ prologue("g1_post_barrier", false); - - CardTableBarrierSet* ct = - barrier_set_cast(BarrierSet::barrier_set()); - - Label done; - Label enqueued; - Label runtime; - - // At this point we know new_value is non-null and the new_value crosses regions. - // Must check to see if card is already dirty - - const Register thread = r15_thread; - - Address queue_index(thread, in_bytes(G1ThreadLocalData::dirty_card_queue_index_offset())); - Address buffer(thread, in_bytes(G1ThreadLocalData::dirty_card_queue_buffer_offset())); - - __ push_ppx(rax); - __ push_ppx(rcx); - - const Register cardtable = rax; - const Register card_addr = rcx; - - __ load_parameter(0, card_addr); - __ shrptr(card_addr, CardTable::card_shift()); - // Do not use ExternalAddress to load 'byte_map_base', since 'byte_map_base' is NOT - // a valid address and therefore is not properly handled by the relocation code. - __ movptr(cardtable, (intptr_t)ct->card_table()->byte_map_base()); - __ addptr(card_addr, cardtable); - - __ cmpb(Address(card_addr, 0), G1CardTable::g1_young_card_val()); - __ jcc(Assembler::equal, done); - - __ membar(Assembler::Membar_mask_bits(Assembler::StoreLoad)); - __ cmpb(Address(card_addr, 0), CardTable::dirty_card_val()); - __ jcc(Assembler::equal, done); - - // storing region crossing non-null, card is clean. - // dirty card and log. - - __ movb(Address(card_addr, 0), CardTable::dirty_card_val()); - - const Register tmp = rdx; - __ push_ppx(rdx); - - __ movptr(tmp, queue_index); - __ testptr(tmp, tmp); - __ jcc(Assembler::zero, runtime); - __ subptr(tmp, wordSize); - __ movptr(queue_index, tmp); - __ addptr(tmp, buffer); - __ movptr(Address(tmp, 0), card_addr); - __ jmp(enqueued); - - __ bind(runtime); - __ push_call_clobbered_registers(); - - __ call_VM_leaf(CAST_FROM_FN_PTR(address, G1BarrierSetRuntime::write_ref_field_post_entry), card_addr, thread); - - __ pop_call_clobbered_registers(); - - __ bind(enqueued); - __ pop_ppx(rdx); - - __ bind(done); - __ pop_ppx(rcx); - __ pop_ppx(rax); - - __ epilogue(); -} - #undef __ #endif // COMPILER1 diff --git a/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.hpp b/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.hpp index 774e87b916c..4b2de41de69 100644 --- a/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.hpp +++ b/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,10 +31,8 @@ class LIR_Assembler; class StubAssembler; class G1PreBarrierStub; -class G1PostBarrierStub; class G1BarrierStubC2; class G1PreBarrierStubC2; -class G1PostBarrierStubC2; class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { protected: @@ -51,22 +49,28 @@ class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { void g1_write_barrier_post(MacroAssembler* masm, Register store_addr, Register new_val, - Register tmp, - Register tmp2); + Register tmp); virtual void oop_store_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, Address dst, Register val, Register tmp1, Register tmp2, Register tmp3); public: - void gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrierStub* stub); - void gen_post_barrier_stub(LIR_Assembler* ce, G1PostBarrierStub* stub); - - void generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm); - void generate_c1_post_barrier_runtime_stub(StubAssembler* sasm); - virtual void load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, Register dst, Address src, Register tmp1); +#ifdef COMPILER1 + void gen_pre_barrier_stub(LIR_Assembler* ce, G1PreBarrierStub* stub); + + void generate_c1_pre_barrier_runtime_stub(StubAssembler* sasm); + + void g1_write_barrier_post_c1(MacroAssembler* masm, + Register store_addr, + Register new_val, + Register thread, + Register tmp1, + Register tmp2); +#endif + #ifdef COMPILER2 void g1_write_barrier_pre_c2(MacroAssembler* masm, Register obj, @@ -79,10 +83,7 @@ class G1BarrierSetAssembler: public ModRefBarrierSetAssembler { Register store_addr, Register new_val, Register tmp, - Register tmp2, - G1PostBarrierStubC2* c2_stub); - void generate_c2_post_barrier_stub(MacroAssembler* masm, - G1PostBarrierStubC2* stub) const; + bool new_val_may_be_null); #endif // COMPILER2 }; diff --git a/src/hotspot/cpu/x86/gc/g1/g1_x86_64.ad b/src/hotspot/cpu/x86/gc/g1/g1_x86_64.ad index 819cd97696c..94607cd6796 100644 --- a/src/hotspot/cpu/x86/gc/g1/g1_x86_64.ad +++ b/src/hotspot/cpu/x86/gc/g1/g1_x86_64.ad @@ -1,5 +1,5 @@ // -// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. // DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. // // This code is free software; you can redistribute it and/or modify it @@ -59,15 +59,14 @@ static void write_barrier_post(MacroAssembler* masm, const MachNode* node, Register store_addr, Register new_val, - Register tmp1, - Register tmp2) { - if (!G1PostBarrierStubC2::needs_barrier(node)) { + Register tmp1) { + if (!G1BarrierStubC2::needs_post_barrier(node)) { return; } Assembler::InlineSkippedInstructionsCounter skip_counter(masm); G1BarrierSetAssembler* g1_asm = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); - G1PostBarrierStubC2* const stub = G1PostBarrierStubC2::create(node); - g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, tmp1, tmp2, stub); + bool new_val_may_be_null = G1BarrierStubC2::post_new_val_may_be_null(node); + g1_asm->g1_write_barrier_post_c2(masm, store_addr, new_val, tmp1, new_val_may_be_null); } %} @@ -95,8 +94,7 @@ instruct g1StoreP(memory mem, any_RegP src, rRegP tmp1, rRegP tmp2, rRegP tmp3, write_barrier_post(masm, this, $tmp1$$Register /* store_addr */, $src$$Register /* new_val */, - $tmp3$$Register /* tmp1 */, - $tmp2$$Register /* tmp2 */); + $tmp3$$Register /* tmp1 */); %} ins_pipe(ialu_mem_reg); %} @@ -127,8 +125,7 @@ instruct g1StoreN(memory mem, rRegN src, rRegP tmp1, rRegP tmp2, rRegP tmp3, rFl write_barrier_post(masm, this, $tmp1$$Register /* store_addr */, $tmp2$$Register /* new_val */, - $tmp3$$Register /* tmp1 */, - $tmp2$$Register /* tmp2 */); + $tmp3$$Register /* tmp1 */); %} ins_pipe(ialu_mem_reg); %} @@ -158,8 +155,7 @@ instruct g1EncodePAndStoreN(memory mem, any_RegP src, rRegP tmp1, rRegP tmp2, rR write_barrier_post(masm, this, $tmp1$$Register /* store_addr */, $src$$Register /* new_val */, - $tmp3$$Register /* tmp1 */, - $tmp2$$Register /* tmp2 */); + $tmp3$$Register /* tmp1 */); %} ins_pipe(ialu_mem_reg); %} @@ -187,8 +183,7 @@ instruct g1CompareAndExchangeP(indirect mem, rRegP newval, rRegP tmp1, rRegP tmp write_barrier_post(masm, this, $mem$$Register /* store_addr */, $tmp1$$Register /* new_val */, - $tmp2$$Register /* tmp1 */, - $tmp3$$Register /* tmp2 */); + $tmp2$$Register /* tmp1 */); %} ins_pipe(pipe_cmpxchg); %} @@ -214,8 +209,7 @@ instruct g1CompareAndExchangeN(indirect mem, rRegN newval, rRegP tmp1, rRegP tmp write_barrier_post(masm, this, $mem$$Register /* store_addr */, $tmp1$$Register /* new_val */, - $tmp2$$Register /* tmp1 */, - $tmp3$$Register /* tmp2 */); + $tmp2$$Register /* tmp1 */); %} ins_pipe(pipe_cmpxchg); %} @@ -246,8 +240,7 @@ instruct g1CompareAndSwapP(rRegI res, indirect mem, rRegP newval, rRegP tmp1, rR write_barrier_post(masm, this, $mem$$Register /* store_addr */, $tmp1$$Register /* new_val */, - $tmp2$$Register /* tmp1 */, - $tmp3$$Register /* tmp2 */); + $tmp2$$Register /* tmp1 */); %} ins_pipe(pipe_cmpxchg); %} @@ -279,8 +272,7 @@ instruct g1CompareAndSwapN(rRegI res, indirect mem, rRegN newval, rRegP tmp1, rR write_barrier_post(masm, this, $mem$$Register /* store_addr */, $tmp1$$Register /* new_val */, - $tmp2$$Register /* tmp1 */, - $tmp3$$Register /* tmp2 */); + $tmp2$$Register /* tmp1 */); %} ins_pipe(pipe_cmpxchg); %} @@ -303,8 +295,7 @@ instruct g1GetAndSetP(indirect mem, rRegP newval, rRegP tmp1, rRegP tmp2, rRegP write_barrier_post(masm, this, $mem$$Register /* store_addr */, $tmp1$$Register /* new_val */, - $tmp2$$Register /* tmp1 */, - $tmp3$$Register /* tmp2 */); + $tmp2$$Register /* tmp1 */); %} ins_pipe(pipe_cmpxchg); %} @@ -328,8 +319,7 @@ instruct g1GetAndSetN(indirect mem, rRegN newval, rRegP tmp1, rRegP tmp2, rRegP write_barrier_post(masm, this, $mem$$Register /* store_addr */, $tmp1$$Register /* new_val */, - $tmp2$$Register /* tmp1 */, - $tmp3$$Register /* tmp2 */); + $tmp2$$Register /* tmp1 */); %} ins_pipe(pipe_cmpxchg); %} diff --git a/src/hotspot/share/code/aotCodeCache.cpp b/src/hotspot/share/code/aotCodeCache.cpp index a24bae03137..04776f4c16c 100644 --- a/src/hotspot/share/code/aotCodeCache.cpp +++ b/src/hotspot/share/code/aotCodeCache.cpp @@ -1365,7 +1365,6 @@ void AOTCodeAddressTable::init_extrs() { #endif // COMPILER2 #if INCLUDE_G1GC - SET_ADDRESS(_extrs, G1BarrierSetRuntime::write_ref_field_post_entry); SET_ADDRESS(_extrs, G1BarrierSetRuntime::write_ref_field_pre_entry); #endif #if INCLUDE_SHENANDOAHGC diff --git a/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.cpp b/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.cpp index 425be474602..51c8a53b54a 100644 --- a/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.cpp +++ b/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.cpp @@ -23,12 +23,15 @@ */ #include "c1/c1_CodeStubs.hpp" +#include "c1/c1_LIRAssembler.hpp" #include "c1/c1_LIRGenerator.hpp" +#include "c1/c1_MacroAssembler.hpp" #include "gc/g1/c1/g1BarrierSetC1.hpp" #include "gc/g1/g1BarrierSet.hpp" #include "gc/g1/g1BarrierSetAssembler.hpp" #include "gc/g1/g1HeapRegion.hpp" #include "gc/g1/g1ThreadLocalData.hpp" +#include "utilities/formatBuffer.hpp" #include "utilities/macros.hpp" #ifdef ASSERT @@ -42,11 +45,6 @@ void G1PreBarrierStub::emit_code(LIR_Assembler* ce) { bs->gen_pre_barrier_stub(ce, this); } -void G1PostBarrierStub::emit_code(LIR_Assembler* ce) { - G1BarrierSetAssembler* bs = (G1BarrierSetAssembler*)BarrierSet::barrier_set()->barrier_set_assembler(); - bs->gen_post_barrier_stub(ce, this); -} - void G1BarrierSetC1::pre_barrier(LIRAccess& access, LIR_Opr addr_opr, LIR_Opr pre_val, CodeEmitInfo* info) { LIRGenerator* gen = access.gen(); @@ -114,6 +112,87 @@ void G1BarrierSetC1::pre_barrier(LIRAccess& access, LIR_Opr addr_opr, __ branch_destination(slow->continuation()); } +class LIR_OpG1PostBarrier : public LIR_Op { + friend class LIR_OpVisitState; + +private: + LIR_Opr _addr; + LIR_Opr _new_val; + LIR_Opr _thread; + LIR_Opr _tmp1; + LIR_Opr _tmp2; + +public: + LIR_OpG1PostBarrier(LIR_Opr addr, + LIR_Opr new_val, + LIR_Opr thread, + LIR_Opr tmp1, + LIR_Opr tmp2) + : LIR_Op(lir_none, lir_none, nullptr), + _addr(addr), + _new_val(new_val), + _thread(thread), + _tmp1(tmp1), + _tmp2(tmp2) + {} + + virtual void visit(LIR_OpVisitState* state) { + state->do_input(_addr); + state->do_input(_new_val); + state->do_input(_thread); + + // Use temps to enforce different registers. + state->do_temp(_addr); + state->do_temp(_new_val); + state->do_temp(_thread); + state->do_temp(_tmp1); + state->do_temp(_tmp2); + + if (_info != nullptr) { + state->do_info(_info); + } + } + + virtual void emit_code(LIR_Assembler* ce) { + if (_info != nullptr) { + ce->add_debug_info_for_null_check_here(_info); + } + + Register addr = _addr->as_pointer_register(); + Register new_val = _new_val->as_pointer_register(); + Register thread = _thread->as_pointer_register(); + Register tmp1 = _tmp1->as_pointer_register(); + Register tmp2 = _tmp2->as_pointer_register(); + + // This may happen for a store of x.a = x - we do not need a post barrier for those + // as the cross-region test will always exit early anyway. + // The post barrier implementations can assume that addr and new_val are different + // then. + if (addr == new_val) { + ce->masm()->block_comment(err_msg("same addr/new_val due to self-referential store with imprecise card mark %s", addr->name())); + return; + } + + G1BarrierSetAssembler* bs_asm = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); + bs_asm->g1_write_barrier_post_c1(ce->masm(), addr, new_val, thread, tmp1, tmp2); + } + + virtual void print_instr(outputStream* out) const { + _addr->print(out); out->print(" "); + _new_val->print(out); out->print(" "); + _thread->print(out); out->print(" "); + _tmp1->print(out); out->print(" "); + _tmp2->print(out); out->print(" "); + out->cr(); + } + +#ifndef PRODUCT + virtual const char* name() const { + return "lir_g1_post_barrier"; + } +#endif // PRODUCT +}; + void G1BarrierSetC1::post_barrier(LIRAccess& access, LIR_Opr addr, LIR_Opr new_val) { LIRGenerator* gen = access.gen(); DecoratorSet decorators = access.decorators(); @@ -150,29 +229,11 @@ void G1BarrierSetC1::post_barrier(LIRAccess& access, LIR_Opr addr, LIR_Opr new_v } assert(addr->is_register(), "must be a register at this point"); - LIR_Opr xor_res = gen->new_pointer_register(); - LIR_Opr xor_shift_res = gen->new_pointer_register(); - if (two_operand_lir_form) { - __ move(addr, xor_res); - __ logical_xor(xor_res, new_val, xor_res); - __ move(xor_res, xor_shift_res); - __ unsigned_shift_right(xor_shift_res, - LIR_OprFact::intConst(checked_cast(G1HeapRegion::LogOfHRGrainBytes)), - xor_shift_res, - LIR_Opr::illegalOpr()); - } else { - __ logical_xor(addr, new_val, xor_res); - __ unsigned_shift_right(xor_res, - LIR_OprFact::intConst(checked_cast(G1HeapRegion::LogOfHRGrainBytes)), - xor_shift_res, - LIR_Opr::illegalOpr()); - } - - __ cmp(lir_cond_notEqual, xor_shift_res, LIR_OprFact::intptrConst(NULL_WORD)); - - CodeStub* slow = new G1PostBarrierStub(addr, new_val); - __ branch(lir_cond_notEqual, slow); - __ branch_destination(slow->continuation()); + __ append(new LIR_OpG1PostBarrier(addr, + new_val, + gen->getThreadPointer() /* thread */, + gen->new_pointer_register() /* tmp1 */, + gen->new_pointer_register() /* tmp2 */)); } void G1BarrierSetC1::load_at_resolved(LIRAccess& access, LIR_Opr result) { @@ -207,20 +268,9 @@ class C1G1PreBarrierCodeGenClosure : public StubAssemblerCodeGenClosure { } }; -class C1G1PostBarrierCodeGenClosure : public StubAssemblerCodeGenClosure { - virtual OopMapSet* generate_code(StubAssembler* sasm) { - G1BarrierSetAssembler* bs = (G1BarrierSetAssembler*)BarrierSet::barrier_set()->barrier_set_assembler(); - bs->generate_c1_post_barrier_runtime_stub(sasm); - return nullptr; - } -}; - bool G1BarrierSetC1::generate_c1_runtime_stubs(BufferBlob* buffer_blob) { C1G1PreBarrierCodeGenClosure pre_code_gen_cl; - C1G1PostBarrierCodeGenClosure post_code_gen_cl; _pre_barrier_c1_runtime_code_blob = Runtime1::generate_blob(buffer_blob, StubId::NO_STUBID, "g1_pre_barrier_slow", false, &pre_code_gen_cl); - _post_barrier_c1_runtime_code_blob = Runtime1::generate_blob(buffer_blob, StubId::NO_STUBID, "g1_post_barrier_slow", - false, &post_code_gen_cl); - return _pre_barrier_c1_runtime_code_blob != nullptr && _post_barrier_c1_runtime_code_blob != nullptr; + return _pre_barrier_c1_runtime_code_blob != nullptr; } diff --git a/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.hpp b/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.hpp index 4baaf8ac58c..89f5676a2d2 100644 --- a/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.hpp +++ b/src/hotspot/share/gc/g1/c1/g1BarrierSetC1.hpp @@ -91,40 +91,11 @@ class G1PreBarrierStub: public CodeStub { #endif // PRODUCT }; -class G1PostBarrierStub: public CodeStub { - friend class G1BarrierSetC1; - private: - LIR_Opr _addr; - LIR_Opr _new_val; - - public: - // addr (the address of the object head) and new_val must be registers. - G1PostBarrierStub(LIR_Opr addr, LIR_Opr new_val): _addr(addr), _new_val(new_val) { - FrameMap* f = Compilation::current()->frame_map(); - f->update_reserved_argument_area_size(2 * BytesPerWord); - } - - LIR_Opr addr() const { return _addr; } - LIR_Opr new_val() const { return _new_val; } - - virtual void emit_code(LIR_Assembler* e); - virtual void visit(LIR_OpVisitState* visitor) { - // don't pass in the code emit info since it's processed in the fast path - visitor->do_slow_case(); - visitor->do_input(_addr); - visitor->do_input(_new_val); - } -#ifndef PRODUCT - virtual void print_name(outputStream* out) const { out->print("G1PostBarrierStub"); } -#endif // PRODUCT -}; - class CodeBlob; class G1BarrierSetC1 : public ModRefBarrierSetC1 { protected: CodeBlob* _pre_barrier_c1_runtime_code_blob; - CodeBlob* _post_barrier_c1_runtime_code_blob; virtual void pre_barrier(LIRAccess& access, LIR_Opr addr_opr, LIR_Opr pre_val, CodeEmitInfo* info); @@ -134,11 +105,9 @@ class G1BarrierSetC1 : public ModRefBarrierSetC1 { public: G1BarrierSetC1() - : _pre_barrier_c1_runtime_code_blob(nullptr), - _post_barrier_c1_runtime_code_blob(nullptr) {} + : _pre_barrier_c1_runtime_code_blob(nullptr) {} CodeBlob* pre_barrier_c1_runtime_code_blob() { return _pre_barrier_c1_runtime_code_blob; } - CodeBlob* post_barrier_c1_runtime_code_blob() { return _post_barrier_c1_runtime_code_blob; } virtual bool generate_c1_runtime_stubs(BufferBlob* buffer_blob); }; diff --git a/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.cpp b/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.cpp index bca2255479b..61402301eb1 100644 --- a/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.cpp +++ b/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.cpp @@ -298,7 +298,13 @@ uint G1BarrierSetC2::estimated_barrier_size(const Node* node) const { nodes += 6; } if ((barrier_data & G1C2BarrierPost) != 0) { - nodes += 60; + // Approximate the number of nodes needed; an if costs 4 nodes (Cmp, Bool, + // If, If projection), any other (Assembly) instruction is approximated with + // a cost of 1. + nodes += 4 // base cost for the card write containing getting base offset, address calculation and the card write; + + 6 // same region check: Uncompress (new_val) oop, xor, shr, (cmp), jmp + + 4 // new_val is null check + + (UseCondCardMark ? 4 : 0); // card not clean check. } return nodes; } @@ -386,8 +392,9 @@ public: } bool needs_liveness_data(const MachNode* mach) const { - return G1PreBarrierStubC2::needs_barrier(mach) || - G1PostBarrierStubC2::needs_barrier(mach); + // Liveness data is only required to compute registers that must be preserved + // across the runtime call in the pre-barrier stub. + return G1BarrierStubC2::needs_pre_barrier(mach); } bool needs_livein_data() const { @@ -401,10 +408,22 @@ static G1BarrierSetC2State* barrier_set_state() { G1BarrierStubC2::G1BarrierStubC2(const MachNode* node) : BarrierStubC2(node) {} +bool G1BarrierStubC2::needs_pre_barrier(const MachNode* node) { + return (node->barrier_data() & G1C2BarrierPre) != 0; +} + +bool G1BarrierStubC2::needs_post_barrier(const MachNode* node) { + return (node->barrier_data() & G1C2BarrierPost) != 0; +} + +bool G1BarrierStubC2::post_new_val_may_be_null(const MachNode* node) { + return (node->barrier_data() & G1C2BarrierPostNotNull) == 0; +} + G1PreBarrierStubC2::G1PreBarrierStubC2(const MachNode* node) : G1BarrierStubC2(node) {} bool G1PreBarrierStubC2::needs_barrier(const MachNode* node) { - return (node->barrier_data() & G1C2BarrierPre) != 0; + return needs_pre_barrier(node); } G1PreBarrierStubC2* G1PreBarrierStubC2::create(const MachNode* node) { @@ -448,48 +467,6 @@ void G1PreBarrierStubC2::emit_code(MacroAssembler& masm) { bs->generate_c2_pre_barrier_stub(&masm, this); } -G1PostBarrierStubC2::G1PostBarrierStubC2(const MachNode* node) : G1BarrierStubC2(node) {} - -bool G1PostBarrierStubC2::needs_barrier(const MachNode* node) { - return (node->barrier_data() & G1C2BarrierPost) != 0; -} - -G1PostBarrierStubC2* G1PostBarrierStubC2::create(const MachNode* node) { - G1PostBarrierStubC2* const stub = new (Compile::current()->comp_arena()) G1PostBarrierStubC2(node); - if (!Compile::current()->output()->in_scratch_emit_size()) { - barrier_set_state()->stubs()->append(stub); - } - return stub; -} - -void G1PostBarrierStubC2::initialize_registers(Register thread, Register tmp1, Register tmp2, Register tmp3) { - _thread = thread; - _tmp1 = tmp1; - _tmp2 = tmp2; - _tmp3 = tmp3; -} - -Register G1PostBarrierStubC2::thread() const { - return _thread; -} - -Register G1PostBarrierStubC2::tmp1() const { - return _tmp1; -} - -Register G1PostBarrierStubC2::tmp2() const { - return _tmp2; -} - -Register G1PostBarrierStubC2::tmp3() const { - return _tmp3; -} - -void G1PostBarrierStubC2::emit_code(MacroAssembler& masm) { - G1BarrierSetAssembler* bs = static_cast(BarrierSet::barrier_set()->barrier_set_assembler()); - bs->generate_c2_post_barrier_stub(&masm, this); -} - void* G1BarrierSetC2::create_barrier_state(Arena* comp_arena) const { return new (comp_arena) G1BarrierSetC2State(comp_arena); } diff --git a/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.hpp b/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.hpp index 5f85714d889..601d0f1138e 100644 --- a/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.hpp +++ b/src/hotspot/share/gc/g1/c2/g1BarrierSetC2.hpp @@ -37,6 +37,10 @@ const int G1C2BarrierPostNotNull = 4; class G1BarrierStubC2 : public BarrierStubC2 { public: + static bool needs_pre_barrier(const MachNode* node); + static bool needs_post_barrier(const MachNode* node); + static bool post_new_val_may_be_null(const MachNode* node); + G1BarrierStubC2(const MachNode* node); virtual void emit_code(MacroAssembler& masm) = 0; }; @@ -64,27 +68,6 @@ public: virtual void emit_code(MacroAssembler& masm); }; -class G1PostBarrierStubC2 : public G1BarrierStubC2 { -private: - Register _thread; - Register _tmp1; - Register _tmp2; - Register _tmp3; - -protected: - G1PostBarrierStubC2(const MachNode* node); - -public: - static bool needs_barrier(const MachNode* node); - static G1PostBarrierStubC2* create(const MachNode* node); - void initialize_registers(Register thread, Register tmp1 = noreg, Register tmp2 = noreg, Register tmp3 = noreg); - Register thread() const; - Register tmp1() const; - Register tmp2() const; - Register tmp3() const; - virtual void emit_code(MacroAssembler& masm); -}; - class G1BarrierSetC2: public CardTableBarrierSetC2 { private: void analyze_dominating_barriers() const; diff --git a/src/hotspot/share/gc/g1/g1Allocator.cpp b/src/hotspot/share/gc/g1/g1Allocator.cpp index 7f2916ae895..713bafd4782 100644 --- a/src/hotspot/share/gc/g1/g1Allocator.cpp +++ b/src/hotspot/share/gc/g1/g1Allocator.cpp @@ -262,9 +262,6 @@ HeapWord* G1Allocator::survivor_attempt_allocation(uint node_index, } } } - if (result != nullptr) { - _g1h->dirty_young_block(result, *actual_word_size); - } return result; } diff --git a/src/hotspot/share/gc/g1/g1Analytics.cpp b/src/hotspot/share/gc/g1/g1Analytics.cpp index 8fe0b25ceb7..6e7f46ca1d1 100644 --- a/src/hotspot/share/gc/g1/g1Analytics.cpp +++ b/src/hotspot/share/gc/g1/g1Analytics.cpp @@ -37,12 +37,10 @@ // They were chosen by running GCOld and SPECjbb on debris with different // numbers of GC threads and choosing them based on the results -static double cost_per_logged_card_ms_defaults[] = { - 0.01, 0.005, 0.005, 0.003, 0.003, 0.002, 0.002, 0.0015 -}; +static double cost_per_pending_card_ms_default = 0.01; // all the same -static double young_card_scan_to_merge_ratio_defaults[] = { +static double young_card_merge_to_scan_ratio_defaults[] = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }; @@ -78,8 +76,7 @@ G1Analytics::G1Analytics(const G1Predictions* predictor) : _concurrent_gc_cpu_time_ms(), _concurrent_refine_rate_ms_seq(TruncatedSeqLength), _dirtied_cards_rate_ms_seq(TruncatedSeqLength), - _dirtied_cards_in_thread_buffers_seq(TruncatedSeqLength), - _card_scan_to_merge_ratio_seq(TruncatedSeqLength), + _card_merge_to_scan_ratio_seq(TruncatedSeqLength), _cost_per_card_scan_ms_seq(TruncatedSeqLength), _cost_per_card_merge_ms_seq(TruncatedSeqLength), _cost_per_code_root_ms_seq(TruncatedSeqLength), @@ -87,6 +84,7 @@ G1Analytics::G1Analytics(const G1Predictions* predictor) : _pending_cards_seq(TruncatedSeqLength), _card_rs_length_seq(TruncatedSeqLength), _code_root_rs_length_seq(TruncatedSeqLength), + _merge_refinement_table_ms_seq(TruncatedSeqLength), _constant_other_time_ms_seq(TruncatedSeqLength), _young_other_cost_per_region_ms_seq(TruncatedSeqLength), _non_young_other_cost_per_region_ms_seq(TruncatedSeqLength), @@ -100,17 +98,17 @@ G1Analytics::G1Analytics(const G1Predictions* predictor) : uint index = MIN2(ParallelGCThreads - 1, 7u); - // Start with inverse of maximum STW cost. - _concurrent_refine_rate_ms_seq.add(1/cost_per_logged_card_ms_defaults[0]); - // Some applications have very low rates for logging cards. + _concurrent_refine_rate_ms_seq.add(1 / cost_per_pending_card_ms_default); + // Some applications have very low rates for dirtying cards. _dirtied_cards_rate_ms_seq.add(0.0); - _card_scan_to_merge_ratio_seq.set_initial(young_card_scan_to_merge_ratio_defaults[index]); + _card_merge_to_scan_ratio_seq.set_initial(young_card_merge_to_scan_ratio_defaults[index]); _cost_per_card_scan_ms_seq.set_initial(young_only_cost_per_card_scan_ms_defaults[index]); _card_rs_length_seq.set_initial(0); _code_root_rs_length_seq.set_initial(0); _cost_per_byte_copied_ms_seq.set_initial(cost_per_byte_ms_defaults[index]); + _merge_refinement_table_ms_seq.add(0); _constant_other_time_ms_seq.add(constant_other_time_ms_defaults[index]); _young_other_cost_per_region_ms_seq.add(young_other_cost_per_region_ms_defaults[index]); _non_young_other_cost_per_region_ms_seq.add(non_young_other_cost_per_region_ms_defaults[index]); @@ -196,10 +194,6 @@ void G1Analytics::report_dirtied_cards_rate_ms(double cards_per_ms) { _dirtied_cards_rate_ms_seq.add(cards_per_ms); } -void G1Analytics::report_dirtied_cards_in_thread_buffers(size_t cards) { - _dirtied_cards_in_thread_buffers_seq.add(double(cards)); -} - void G1Analytics::report_cost_per_card_scan_ms(double cost_per_card_ms, bool for_young_only_phase) { _cost_per_card_scan_ms_seq.add(cost_per_card_ms, for_young_only_phase); } @@ -212,8 +206,8 @@ void G1Analytics::report_cost_per_code_root_scan_ms(double cost_per_code_root_ms _cost_per_code_root_ms_seq.add(cost_per_code_root_ms, for_young_only_phase); } -void G1Analytics::report_card_scan_to_merge_ratio(double merge_to_scan_ratio, bool for_young_only_phase) { - _card_scan_to_merge_ratio_seq.add(merge_to_scan_ratio, for_young_only_phase); +void G1Analytics::report_card_merge_to_scan_ratio(double merge_to_scan_ratio, bool for_young_only_phase) { + _card_merge_to_scan_ratio_seq.add(merge_to_scan_ratio, for_young_only_phase); } void G1Analytics::report_cost_per_byte_ms(double cost_per_byte_ms, bool for_young_only_phase) { @@ -228,6 +222,10 @@ void G1Analytics::report_non_young_other_cost_per_region_ms(double other_cost_pe _non_young_other_cost_per_region_ms_seq.add(other_cost_per_region_ms); } +void G1Analytics::report_merge_refinement_table_time_ms(double merge_refinement_table_time_ms) { + _merge_refinement_table_ms_seq.add(merge_refinement_table_time_ms); +} + void G1Analytics::report_constant_other_time_ms(double constant_other_time_ms) { _constant_other_time_ms_seq.add(constant_other_time_ms); } @@ -260,12 +258,8 @@ double G1Analytics::predict_dirtied_cards_rate_ms() const { return predict_zero_bounded(&_dirtied_cards_rate_ms_seq); } -size_t G1Analytics::predict_dirtied_cards_in_thread_buffers() const { - return predict_size(&_dirtied_cards_in_thread_buffers_seq); -} - size_t G1Analytics::predict_scan_card_num(size_t card_rs_length, bool for_young_only_phase) const { - return card_rs_length * predict_in_unit_interval(&_card_scan_to_merge_ratio_seq, for_young_only_phase); + return card_rs_length * predict_in_unit_interval(&_card_merge_to_scan_ratio_seq, for_young_only_phase); } double G1Analytics::predict_card_merge_time_ms(size_t card_num, bool for_young_only_phase) const { @@ -284,6 +278,10 @@ double G1Analytics::predict_object_copy_time_ms(size_t bytes_to_copy, bool for_y return bytes_to_copy * predict_zero_bounded(&_cost_per_byte_copied_ms_seq, for_young_only_phase); } +double G1Analytics::predict_merge_refinement_table_time_ms() const { + return predict_zero_bounded(&_merge_refinement_table_ms_seq); +} + double G1Analytics::predict_constant_other_time_ms() const { return predict_zero_bounded(&_constant_other_time_ms_seq); } diff --git a/src/hotspot/share/gc/g1/g1Analytics.hpp b/src/hotspot/share/gc/g1/g1Analytics.hpp index e5e2dd74101..1f609815632 100644 --- a/src/hotspot/share/gc/g1/g1Analytics.hpp +++ b/src/hotspot/share/gc/g1/g1Analytics.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -56,14 +56,13 @@ class G1Analytics: public CHeapObj { TruncatedSeq _concurrent_refine_rate_ms_seq; TruncatedSeq _dirtied_cards_rate_ms_seq; - TruncatedSeq _dirtied_cards_in_thread_buffers_seq; - // The ratio between the number of scanned cards and actually merged cards, for - // young-only and mixed gcs. - G1PhaseDependentSeq _card_scan_to_merge_ratio_seq; + // The ratio between the number of merged cards to actually scanned cards for + // card based remembered sets, for young-only and mixed gcs. + G1PhaseDependentSeq _card_merge_to_scan_ratio_seq; // The cost to scan a card during young-only and mixed gcs in ms. G1PhaseDependentSeq _cost_per_card_scan_ms_seq; - // The cost to merge a card during young-only and mixed gcs in ms. + // The cost to merge a card from the remembered sets for non-young regions in ms. G1PhaseDependentSeq _cost_per_card_merge_ms_seq; // The cost to scan entries in the code root remembered set in ms. G1PhaseDependentSeq _cost_per_code_root_ms_seq; @@ -74,6 +73,8 @@ class G1Analytics: public CHeapObj { G1PhaseDependentSeq _card_rs_length_seq; G1PhaseDependentSeq _code_root_rs_length_seq; + // Prediction for merging the refinement table to the card table during GC. + TruncatedSeq _merge_refinement_table_ms_seq; TruncatedSeq _constant_other_time_ms_seq; TruncatedSeq _young_other_cost_per_region_ms_seq; TruncatedSeq _non_young_other_cost_per_region_ms_seq; @@ -149,14 +150,14 @@ public: void report_alloc_rate_ms(double alloc_rate); void report_concurrent_refine_rate_ms(double cards_per_ms); void report_dirtied_cards_rate_ms(double cards_per_ms); - void report_dirtied_cards_in_thread_buffers(size_t num_cards); void report_cost_per_card_scan_ms(double cost_per_remset_card_ms, bool for_young_only_phase); void report_cost_per_card_merge_ms(double cost_per_card_ms, bool for_young_only_phase); void report_cost_per_code_root_scan_ms(double cost_per_code_root_ms, bool for_young_only_phase); - void report_card_scan_to_merge_ratio(double cards_per_entry_ratio, bool for_young_only_phase); + void report_card_merge_to_scan_ratio(double merge_to_scan_ratio, bool for_young_only_phase); void report_cost_per_byte_ms(double cost_per_byte_ms, bool for_young_only_phase); void report_young_other_cost_per_region_ms(double other_cost_per_region_ms); void report_non_young_other_cost_per_region_ms(double other_cost_per_region_ms); + void report_merge_refinement_table_time_ms(double pending_card_merge_time_ms); void report_constant_other_time_ms(double constant_other_time_ms); void report_pending_cards(double pending_cards, bool for_young_only_phase); void report_card_rs_length(double card_rs_length, bool for_young_only_phase); @@ -167,7 +168,6 @@ public: double predict_concurrent_refine_rate_ms() const; double predict_dirtied_cards_rate_ms() const; - size_t predict_dirtied_cards_in_thread_buffers() const; // Predict how many of the given remembered set of length card_rs_length will add to // the number of total cards scanned. @@ -180,6 +180,7 @@ public: double predict_object_copy_time_ms(size_t bytes_to_copy, bool for_young_only_phase) const; + double predict_merge_refinement_table_time_ms() const; double predict_constant_other_time_ms() const; double predict_young_other_time_ms(size_t young_num) const; diff --git a/src/hotspot/share/gc/g1/g1Arguments.cpp b/src/hotspot/share/gc/g1/g1Arguments.cpp index ee91c327337..5cbafd2ae94 100644 --- a/src/hotspot/share/gc/g1/g1Arguments.cpp +++ b/src/hotspot/share/gc/g1/g1Arguments.cpp @@ -68,6 +68,12 @@ void G1Arguments::initialize_alignments() { if (FLAG_IS_DEFAULT(G1EagerReclaimRemSetThreshold)) { FLAG_SET_ERGO(G1EagerReclaimRemSetThreshold, G1RemSetArrayOfCardsEntries); } + // G1 prefers to use conditional card marking to avoid overwriting cards that + // have already been found to contain a to-collection set reference. This reduces + // refinement effort. + if (FLAG_IS_DEFAULT(UseCondCardMark)) { + FLAG_SET_ERGO(UseCondCardMark, true); + } } size_t G1Arguments::conservative_max_heap_alignment() { @@ -241,9 +247,8 @@ void G1Arguments::initialize() { // Verify that the maximum parallelism isn't too high to eventually overflow // the refcount in G1CardSetContainer. - uint max_parallel_refinement_threads = G1ConcRefinementThreads + G1DirtyCardQueueSet::num_par_ids(); uint const divisor = 3; // Safe divisor; we increment by 2 for each claim, but there is a small initial value. - if (max_parallel_refinement_threads > UINT_MAX / divisor) { + if (G1ConcRefinementThreads > UINT_MAX / divisor) { vm_exit_during_initialization("Too large parallelism for remembered sets."); } diff --git a/src/hotspot/share/gc/g1/g1BarrierSet.cpp b/src/hotspot/share/gc/g1/g1BarrierSet.cpp index c56434340cd..ab7d6febf4c 100644 --- a/src/hotspot/share/gc/g1/g1BarrierSet.cpp +++ b/src/hotspot/share/gc/g1/g1BarrierSet.cpp @@ -32,12 +32,14 @@ #include "gc/g1/g1ThreadLocalData.hpp" #include "gc/shared/satbMarkQueue.hpp" #include "logging/log.hpp" +#include "memory/iterator.hpp" #include "oops/access.inline.hpp" #include "oops/compressedOops.inline.hpp" #include "oops/oop.inline.hpp" #include "runtime/interfaceSupport.inline.hpp" #include "runtime/javaThread.hpp" #include "runtime/orderAccess.hpp" +#include "runtime/threads.hpp" #include "utilities/macros.hpp" #ifdef COMPILER1 #include "gc/g1/c1/g1BarrierSetC1.hpp" @@ -49,18 +51,38 @@ class G1BarrierSetC1; class G1BarrierSetC2; -G1BarrierSet::G1BarrierSet(G1CardTable* card_table) : +G1BarrierSet::G1BarrierSet(G1CardTable* card_table, + G1CardTable* refinement_table) : CardTableBarrierSet(make_barrier_set_assembler(), make_barrier_set_c1(), make_barrier_set_c2(), card_table, BarrierSet::FakeRtti(BarrierSet::G1BarrierSet)), _satb_mark_queue_buffer_allocator("SATB Buffer Allocator", G1SATBBufferSize), - _dirty_card_queue_buffer_allocator("DC Buffer Allocator", G1UpdateBufferSize), _satb_mark_queue_set(&_satb_mark_queue_buffer_allocator), - _dirty_card_queue_set(&_dirty_card_queue_buffer_allocator) + _refinement_table(refinement_table) {} +G1BarrierSet::~G1BarrierSet() { + delete _refinement_table; +} + +void G1BarrierSet::swap_global_card_table() { + G1CardTable* temp = static_cast(_card_table); + _card_table = _refinement_table; + _refinement_table = temp; +} + +void G1BarrierSet::update_card_table_base(Thread* thread) { +#ifdef ASSERT + { + ResourceMark rm; + assert(thread->is_Java_thread(), "may only update card table base of JavaThreads, not %s", thread->name()); + } +#endif + G1ThreadLocalData::set_byte_map_base(thread, _card_table->byte_map_base()); +} + template void G1BarrierSet::write_ref_array_pre_work(T* dst, size_t count) { G1SATBMarkQueueSet& queue_set = G1BarrierSet::satb_mark_queue_set(); @@ -89,28 +111,14 @@ void G1BarrierSet::write_ref_array_pre(narrowOop* dst, size_t count, bool dest_u } } -void G1BarrierSet::write_ref_field_post_slow(volatile CardValue* byte) { - // In the slow path, we know a card is not young - assert(*byte != G1CardTable::g1_young_card_val(), "slow path invoked without filtering"); - OrderAccess::storeload(); - if (*byte != G1CardTable::dirty_card_val()) { - *byte = G1CardTable::dirty_card_val(); - Thread* thr = Thread::current(); - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(thr); - G1BarrierSet::dirty_card_queue_set().enqueue(queue, byte); - } -} - void G1BarrierSet::write_region(JavaThread* thread, MemRegion mr) { if (mr.is_empty()) { return; } - volatile CardValue* byte = _card_table->byte_for(mr.start()); - CardValue* last_byte = _card_table->byte_for(mr.last()); - // skip young gen cards - if (*byte == G1CardTable::g1_young_card_val()) { - // MemRegion should not span multiple regions for the young gen. + // Skip writes to young gen. + if (G1CollectedHeap::heap()->heap_region_containing(mr.start())->is_young()) { + // MemRegion should not span multiple regions for arrays in young gen. DEBUG_ONLY(G1HeapRegion* containing_hr = G1CollectedHeap::heap()->heap_region_containing(mr.start());) assert(containing_hr->is_young(), "it should be young"); assert(containing_hr->is_in(mr.start()), "it should contain start"); @@ -118,16 +126,25 @@ void G1BarrierSet::write_region(JavaThread* thread, MemRegion mr) { return; } - OrderAccess::storeload(); - // Enqueue if necessary. - G1DirtyCardQueueSet& qset = G1BarrierSet::dirty_card_queue_set(); - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(thread); + // We need to make sure that we get the start/end byte information for the area + // to mark from the same card table to avoid getting confused in the mark loop + // further below - we might execute while the global card table is being switched. + // + // It does not matter which card table we write to: at worst we may write to the + // new card table (after the switching), which means that we will catch the + // marks next time. + // If we write to the old card table (after the switching, then the refinement + // table) the oncoming handshake will do the memory synchronization. + CardTable* card_table = AtomicAccess::load(&_card_table); + + volatile CardValue* byte = card_table->byte_for(mr.start()); + CardValue* last_byte = card_table->byte_for(mr.last()); + + // Dirty cards only if necessary. for (; byte <= last_byte; byte++) { CardValue bv = *byte; - assert(bv != G1CardTable::g1_young_card_val(), "Invalid card"); - if (bv != G1CardTable::dirty_card_val()) { + if (bv == G1CardTable::clean_card_val()) { *byte = G1CardTable::dirty_card_val(); - qset.enqueue(queue, byte); } } } @@ -148,14 +165,15 @@ void G1BarrierSet::on_thread_attach(Thread* thread) { assert(!satbq.is_active(), "SATB queue should not be active"); assert(satbq.buffer() == nullptr, "SATB queue should not have a buffer"); assert(satbq.index() == 0, "SATB queue index should be zero"); - G1DirtyCardQueue& dirtyq = G1ThreadLocalData::dirty_card_queue(thread); - assert(dirtyq.buffer() == nullptr, "Dirty Card queue should not have a buffer"); - assert(dirtyq.index() == 0, "Dirty Card queue index should be zero"); - // If we are creating the thread during a marking cycle, we should // set the active field of the SATB queue to true. That involves // copying the global is_active value to this thread's queue. satbq.set_active(_satb_mark_queue_set.is_active()); + + if (thread->is_Java_thread()) { + assert(Threads_lock->is_locked(), "must be, synchronization with refinement."); + update_card_table_base(thread); + } } void G1BarrierSet::on_thread_detach(Thread* thread) { @@ -165,14 +183,13 @@ void G1BarrierSet::on_thread_detach(Thread* thread) { SATBMarkQueue& queue = G1ThreadLocalData::satb_mark_queue(thread); G1BarrierSet::satb_mark_queue_set().flush_queue(queue); } - { - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(thread); - G1DirtyCardQueueSet& qset = G1BarrierSet::dirty_card_queue_set(); - qset.flush_queue(queue); - qset.record_detached_refinement_stats(queue.refinement_stats()); - } { G1RegionPinCache& cache = G1ThreadLocalData::pin_count_cache(thread); cache.flush(); } } + +void G1BarrierSet::print_on(outputStream* st) const { + _card_table->print_on(st, "Card"); + _refinement_table->print_on(st, "Refinement"); +} diff --git a/src/hotspot/share/gc/g1/g1BarrierSet.hpp b/src/hotspot/share/gc/g1/g1BarrierSet.hpp index 2b1074fcd7a..40e87c373b7 100644 --- a/src/hotspot/share/gc/g1/g1BarrierSet.hpp +++ b/src/hotspot/share/gc/g1/g1BarrierSet.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,32 +25,65 @@ #ifndef SHARE_GC_G1_G1BARRIERSET_HPP #define SHARE_GC_G1_G1BARRIERSET_HPP -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1SATBMarkQueueSet.hpp" #include "gc/shared/bufferNode.hpp" #include "gc/shared/cardTable.hpp" #include "gc/shared/cardTableBarrierSet.hpp" class G1CardTable; +class Thread; -// This barrier is specialized to use a logging barrier to support -// snapshot-at-the-beginning marking. - +// This barrier set is specialized to manage two card tables: +// * one the mutator is currently working on ("card table") +// * one the refinement threads or GC during pause are working on ("refinement table") +// +// The card table acts like a regular card table where the mutator dirties cards +// containing potentially interesting references. +// +// When the amount of dirty cards on the card table exceeds a threshold, G1 swaps +// the card tables and has the refinement threads reduce them by "refining" +// them. +// I.e. refinement looks at all dirty cards on the refinement table, and updates +// the remembered sets accordingly, clearing the cards on the refinement table. +// +// Meanwhile the mutator continues dirtying the now empty card table. +// +// This separation of data the mutator and refinement threads are working on +// removes the need for any fine-grained (per mutator write) synchronization between +// them, keeping the write barrier simple. +// +// The refinement threads mark cards in the current collection set specially on the +// card table - this is fine wrt synchronization with the mutator, because at +// most the mutator will overwrite it again if there is a race, as G1 will scan the +// entire card either way during the GC pause. +// +// During garbage collection, if the refinement table is known to be non-empty, G1 +// merges it back (and cleaning it) to the card table which is scanned for dirty +// cards. +// class G1BarrierSet: public CardTableBarrierSet { friend class VMStructs; private: BufferNode::Allocator _satb_mark_queue_buffer_allocator; - BufferNode::Allocator _dirty_card_queue_buffer_allocator; G1SATBMarkQueueSet _satb_mark_queue_set; - G1DirtyCardQueueSet _dirty_card_queue_set; + + G1CardTable* _refinement_table; + + public: + G1BarrierSet(G1CardTable* card_table, G1CardTable* refinement_table); + virtual ~G1BarrierSet(); static G1BarrierSet* g1_barrier_set() { return barrier_set_cast(BarrierSet::barrier_set()); } - public: - G1BarrierSet(G1CardTable* table); - ~G1BarrierSet() { } + G1CardTable* refinement_table() const { return _refinement_table; } + + // Swap the global card table references, without synchronization. + void swap_global_card_table(); + + // Update the given thread's card table (byte map) base to the current card table's. + void update_card_table_base(Thread* thread); virtual bool card_mark_must_follow_store() const { return true; @@ -74,9 +107,8 @@ class G1BarrierSet: public CardTableBarrierSet { inline void write_region(MemRegion mr); void write_region(JavaThread* thread, MemRegion mr); - template + template void write_ref_field_post(T* field); - void write_ref_field_post_slow(volatile CardValue* byte); virtual void on_thread_create(Thread* thread); virtual void on_thread_destroy(Thread* thread); @@ -87,9 +119,7 @@ class G1BarrierSet: public CardTableBarrierSet { return g1_barrier_set()->_satb_mark_queue_set; } - static G1DirtyCardQueueSet& dirty_card_queue_set() { - return g1_barrier_set()->_dirty_card_queue_set; - } + virtual void print_on(outputStream* st) const; // Callbacks for runtime accesses. template diff --git a/src/hotspot/share/gc/g1/g1BarrierSet.inline.hpp b/src/hotspot/share/gc/g1/g1BarrierSet.inline.hpp index 9678da190af..0888fc58937 100644 --- a/src/hotspot/share/gc/g1/g1BarrierSet.inline.hpp +++ b/src/hotspot/share/gc/g1/g1BarrierSet.inline.hpp @@ -75,9 +75,8 @@ inline void G1BarrierSet::write_region(MemRegion mr) { template inline void G1BarrierSet::write_ref_field_post(T* field) { volatile CardValue* byte = _card_table->byte_for(field); - if (*byte != G1CardTable::g1_young_card_val()) { - // Take a slow path for cards in old - write_ref_field_post_slow(byte); + if (*byte == G1CardTable::clean_card_val()) { + *byte = G1CardTable::dirty_card_val(); } } @@ -127,7 +126,7 @@ inline void G1BarrierSet::AccessBarrier:: oop_store_not_in_heap(T* addr, oop new_value) { // Apply SATB barriers for all non-heap references, to allow // concurrent scanning of such references. - G1BarrierSet *bs = barrier_set_cast(BarrierSet::barrier_set()); + G1BarrierSet *bs = g1_barrier_set(); bs->write_ref_field_pre(addr); Raw::oop_store(addr, new_value); } diff --git a/src/hotspot/share/gc/g1/g1BarrierSetRuntime.cpp b/src/hotspot/share/gc/g1/g1BarrierSetRuntime.cpp index 205829bba1a..24ade277afe 100644 --- a/src/hotspot/share/gc/g1/g1BarrierSetRuntime.cpp +++ b/src/hotspot/share/gc/g1/g1BarrierSetRuntime.cpp @@ -29,17 +29,17 @@ #include "utilities/macros.hpp" void G1BarrierSetRuntime::write_ref_array_pre_oop_entry(oop* dst, size_t length) { - G1BarrierSet *bs = barrier_set_cast(BarrierSet::barrier_set()); + G1BarrierSet *bs = G1BarrierSet::g1_barrier_set(); bs->write_ref_array_pre(dst, length, false); } void G1BarrierSetRuntime::write_ref_array_pre_narrow_oop_entry(narrowOop* dst, size_t length) { - G1BarrierSet *bs = barrier_set_cast(BarrierSet::barrier_set()); + G1BarrierSet *bs = G1BarrierSet::g1_barrier_set(); bs->write_ref_array_pre(dst, length, false); } void G1BarrierSetRuntime::write_ref_array_post_entry(HeapWord* dst, size_t length) { - G1BarrierSet *bs = barrier_set_cast(BarrierSet::barrier_set()); + G1BarrierSet *bs = G1BarrierSet::g1_barrier_set(); bs->G1BarrierSet::write_ref_array(dst, length); } @@ -53,14 +53,6 @@ JRT_LEAF(void, G1BarrierSetRuntime::write_ref_field_pre_entry(oopDesc* orig, Jav G1BarrierSet::satb_mark_queue_set().enqueue_known_active(queue, orig); JRT_END -// G1 post write barrier slowpath -JRT_LEAF(void, G1BarrierSetRuntime::write_ref_field_post_entry(volatile G1CardTable::CardValue* card_addr, - JavaThread* thread)) - assert(thread == JavaThread::current(), "pre-condition"); - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(thread); - G1BarrierSet::dirty_card_queue_set().enqueue(queue, card_addr); -JRT_END - JRT_LEAF(void, G1BarrierSetRuntime::clone(oopDesc* src, oopDesc* dst, size_t size)) HeapAccess<>::clone(src, dst, size); JRT_END diff --git a/src/hotspot/share/gc/g1/g1BarrierSetRuntime.hpp b/src/hotspot/share/gc/g1/g1BarrierSetRuntime.hpp index 27287a0624b..ba7bc4d90f4 100644 --- a/src/hotspot/share/gc/g1/g1BarrierSetRuntime.hpp +++ b/src/hotspot/share/gc/g1/g1BarrierSetRuntime.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -47,7 +47,6 @@ public: // C2 slow-path runtime calls. static void write_ref_field_pre_entry(oopDesc* orig, JavaThread *thread); - static void write_ref_field_post_entry(volatile CardValue* card_addr, JavaThread* thread); static address clone_addr(); }; diff --git a/src/hotspot/share/gc/g1/g1CardTable.cpp b/src/hotspot/share/gc/g1/g1CardTable.cpp index 303b8cda91f..6df178d49c5 100644 --- a/src/hotspot/share/gc/g1/g1CardTable.cpp +++ b/src/hotspot/share/gc/g1/g1CardTable.cpp @@ -28,18 +28,37 @@ #include "logging/log.hpp" #include "runtime/os.hpp" -void G1CardTable::g1_mark_as_young(const MemRegion& mr) { - CardValue *const first = byte_for(mr.start()); - CardValue *const last = byte_after(mr.last()); +void G1CardTable::verify_region(MemRegion mr, CardValue val, bool val_equals) { + if (mr.is_empty()) { + return; + } + CardValue* start = byte_for(mr.start()); + CardValue* end = byte_for(mr.last()); - memset_with_concurrent_readers(first, g1_young_gen, pointer_delta(last, first, sizeof(CardValue))); -} + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + G1HeapRegion* r = g1h->heap_region_containing(mr.start()); -#ifndef PRODUCT -void G1CardTable::verify_g1_young_region(MemRegion mr) { - verify_region(mr, g1_young_gen, true); + assert(r == g1h->heap_region_containing(mr.last()), "MemRegion crosses region"); + + bool failures = false; + for (CardValue* curr = start; curr <= end; ++curr) { + CardValue curr_val = *curr; + bool failed = (val_equals) ? (curr_val != val) : (curr_val == val); + if (failed) { + if (!failures) { + log_error(gc, verify)("== CT verification failed: [" PTR_FORMAT "," PTR_FORMAT "] r: %d (%s) %sexpecting value: %d", + p2i(start), p2i(end), r->hrm_index(), r->get_short_type_str(), + (val_equals) ? "" : "not ", val); + failures = true; + } + log_error(gc, verify)("== card " PTR_FORMAT " [" PTR_FORMAT "," PTR_FORMAT "], val: %d", + p2i(curr), p2i(addr_for(curr)), + p2i((HeapWord*) (((size_t) addr_for(curr)) + _card_size)), + (int) curr_val); + } + } + guarantee(!failures, "there should not have been any failures"); } -#endif void G1CardTableChangedListener::on_commit(uint start_idx, size_t num_regions, bool zero_filled) { // Default value for a clean card on the card table is -1. So we cannot take advantage of the zero_filled parameter. @@ -74,6 +93,5 @@ void G1CardTable::initialize(G1RegionToSpaceMapper* mapper) { } bool G1CardTable::is_in_young(const void* p) const { - volatile CardValue* card = byte_for(p); - return *card == G1CardTable::g1_young_card_val(); + return G1CollectedHeap::heap()->heap_region_containing(p)->is_young(); } diff --git a/src/hotspot/share/gc/g1/g1CardTable.hpp b/src/hotspot/share/gc/g1/g1CardTable.hpp index 16133029a11..060e5459778 100644 --- a/src/hotspot/share/gc/g1/g1CardTable.hpp +++ b/src/hotspot/share/gc/g1/g1CardTable.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -52,8 +52,6 @@ class G1CardTable : public CardTable { public: enum G1CardValues { - g1_young_gen = CT_MR_BS_last_reserved << 1, - // During evacuation we use the card table to consolidate the cards we need to // scan for roots onto the card table from the various sources. Further it is // used to record already completely scanned cards to avoid re-scanning them @@ -63,18 +61,43 @@ public: // The merge at the start of each evacuation round simply sets cards to dirty // that are clean; scanned cards are set to 0x1. // - // This means that the LSB determines what to do with the card during evacuation - // given the following possible values: + // This means that the LSB determines whether the card is clean or non-clean + // (LSB is 1 -> clean, LSB is 0 -> non-clean) given the following possible values: // - // 11111111 - clean, do not scan - // 00000001 - already scanned, do not scan + // xxxxxxx1 - clean, already scanned, do not scan again (during GC only). + // 00000100 - dirty, needs to be scanned, dirty from remembered set (during GC only) + // 00000010 - dirty, needs to be scanned, contains reference to collection set. // 00000000 - dirty, needs to be scanned. // - g1_card_already_scanned = 0x1 + // g1_to_cset_card and g1_from_remset_card are both used for optimization and + // needed for more accurate prediction of card generation rate. + // + // g1_to_cset_card allows to separate dirty card generation rate by the mutator + // (which just dirties cards) from cards that will be scanned during next garbage + // collection anyway. + // Further it allows the optimization to not refine them, assuming that their + // references to young gen does not change, and not add this card to any other + // remembered set. + // This color is sticky during mutator time: refinement threads encountering + // this card on the refinement table will just copy it over to the regular card + // table without re-refining this card. This saves on refinement effort spent + // on that card because most of the time already found interesting references + // stay interesting. + // + // g1_from_remset_card allows separation of cards generated by the mutator from + // cards in the remembered set, again to make mutator dirty card generation + // prediction more accurate. + // + // More accurate prediction allow better (less wasteful) refinement control. + g1_dirty_card = dirty_card, + g1_card_already_scanned = 0x1, + g1_to_cset_card = 0x2, + g1_from_remset_card = 0x4 }; static const size_t WordAllClean = SIZE_MAX; static const size_t WordAllDirty = 0; + static const size_t WordAllFromRemset = (SIZE_MAX / 255) * g1_from_remset_card; STATIC_ASSERT(BitsPerByte == 8); static const size_t WordAlreadyScanned = (SIZE_MAX / 255) * g1_card_already_scanned; @@ -83,27 +106,27 @@ public: _listener.set_card_table(this); } - static CardValue g1_young_card_val() { return g1_young_gen; } static CardValue g1_scanned_card_val() { return g1_card_already_scanned; } - void verify_g1_young_region(MemRegion mr) PRODUCT_RETURN; - void g1_mark_as_young(const MemRegion& mr); + void verify_region(MemRegion mr, CardValue val, bool val_equals) override; size_t index_for_cardvalue(CardValue const* p) const { return pointer_delta(p, _byte_map, sizeof(CardValue)); } - // Mark the given card as Dirty if it is Clean. Returns whether the card was + // Mark the given card as From Remset if it is Clean. Returns whether the card was // Clean before this operation. This result may be inaccurate as it does not // perform the dirtying atomically. - inline bool mark_clean_as_dirty(CardValue* card); + inline bool mark_clean_as_from_remset(CardValue* card); - // Change Clean cards in a (large) area on the card table as Dirty, preserving - // already scanned cards. Assumes that most cards in that area are Clean. - inline void mark_range_dirty(size_t start_card_index, size_t num_cards); + // Change Clean cards in a (large) area on the card table as From_Remset, preserving + // cards already marked otherwise. Assumes that most cards in that area are Clean. + // Not atomic. + inline size_t mark_clean_range_as_from_remset(size_t start_card_index, size_t num_cards); - // Change the given range of dirty cards to "which". All of these cards must be Dirty. - inline void change_dirty_cards_to(CardValue* start_card, CardValue* end_card, CardValue which); + // Change the given range of dirty cards to "which". All of these cards must be non-clean. + // Returns the number of pending cards found. + inline size_t change_dirty_cards_to(CardValue* start_card, CardValue* end_card, CardValue which); inline uint region_idx_for(CardValue* p); diff --git a/src/hotspot/share/gc/g1/g1CardTable.inline.hpp b/src/hotspot/share/gc/g1/g1CardTable.inline.hpp index 03bce7d50d7..370dc22ded0 100644 --- a/src/hotspot/share/gc/g1/g1CardTable.inline.hpp +++ b/src/hotspot/share/gc/g1/g1CardTable.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,25 +28,39 @@ #include "gc/g1/g1CardTable.hpp" #include "gc/g1/g1HeapRegion.hpp" +#include "utilities/population_count.hpp" inline uint G1CardTable::region_idx_for(CardValue* p) { size_t const card_idx = pointer_delta(p, _byte_map, sizeof(CardValue)); return (uint)(card_idx >> G1HeapRegion::LogCardsPerRegion); } -inline bool G1CardTable::mark_clean_as_dirty(CardValue* card) { +inline bool G1CardTable::mark_clean_as_from_remset(CardValue* card) { CardValue value = *card; if (value == clean_card_val()) { - *card = dirty_card_val(); + *card = g1_from_remset_card; return true; } return false; } -inline void G1CardTable::mark_range_dirty(size_t start_card_index, size_t num_cards) { +// Returns bits from a where mask is 0, and bits from b where mask is 1. +// +// Example: +// a = 0xAAAAAAAA +// b = 0xBBBBBBBB +// mask = 0xFF00FF00 +// result = 0xBBAABBAA +inline size_t blend(size_t a, size_t b, size_t mask) { + return (a & ~mask) | (b & mask); +} + +inline size_t G1CardTable::mark_clean_range_as_from_remset(size_t start_card_index, size_t num_cards) { assert(is_aligned(start_card_index, sizeof(size_t)), "Start card index must be aligned."); assert(is_aligned(num_cards, sizeof(size_t)), "Number of cards to change must be evenly divisible."); + size_t result = 0; + size_t const num_chunks = num_cards / sizeof(size_t); size_t* cur_word = (size_t*)&_byte_map[start_card_index]; @@ -54,31 +68,33 @@ inline void G1CardTable::mark_range_dirty(size_t start_card_index, size_t num_ca while (cur_word < end_word_map) { size_t value = *cur_word; if (value == WordAllClean) { - *cur_word = WordAllDirty; - } else if (value == WordAllDirty) { - // do nothing. + *cur_word = WordAllFromRemset; + result += sizeof(size_t); + } else if ((value & WordAlreadyScanned) == 0) { + // Do nothing if there is no "Clean" card in it. } else { - // There is a mix of cards in there. Tread slowly. - CardValue* cur = (CardValue*)cur_word; - for (size_t i = 0; i < sizeof(size_t); i++) { - CardValue value = *cur; - if (value == clean_card_val()) { - *cur = dirty_card_val(); - } - cur++; - } + // There is a mix of cards in there. Tread "slowly". + size_t clean_card_mask = (value & WordAlreadyScanned) * 0xff; // All "Clean" cards have 0xff, all other places 0x00 now. + result += population_count(clean_card_mask) / BitsPerByte; + *cur_word = blend(value, WordAllFromRemset, clean_card_mask); } cur_word++; } + return result; } -inline void G1CardTable::change_dirty_cards_to(CardValue* start_card, CardValue* end_card, CardValue which) { +inline size_t G1CardTable::change_dirty_cards_to(CardValue* start_card, CardValue* end_card, CardValue which) { + size_t result = 0; for (CardValue* i_card = start_card; i_card < end_card; ++i_card) { CardValue value = *i_card; - assert(value == dirty_card_val(), + assert((value & g1_card_already_scanned) == 0, "Must have been dirty %d start " PTR_FORMAT " " PTR_FORMAT, value, p2i(start_card), p2i(end_card)); + if (value == g1_dirty_card) { + result++; + } *i_card = which; } + return result; } #endif /* SHARE_GC_G1_G1CARDTABLE_INLINE_HPP */ diff --git a/src/hotspot/share/gc/g1/g1CardTableClaimTable.cpp b/src/hotspot/share/gc/g1/g1CardTableClaimTable.cpp new file mode 100644 index 00000000000..e0cadbdd907 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1CardTableClaimTable.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "gc/g1/g1CardTableClaimTable.inline.hpp" +#include "gc/g1/g1CollectedHeap.inline.hpp" +#include "gc/g1/g1HeapRegion.inline.hpp" +#include "gc/shared/workerThread.hpp" +#include "memory/allocation.hpp" +#include "utilities/checkedCast.hpp" +#include "utilities/powerOfTwo.hpp" + +G1CardTableClaimTable::G1CardTableClaimTable(uint chunks_per_region) : + _max_reserved_regions(0), + _card_claims(nullptr), + _cards_per_chunk(checked_cast(G1HeapRegion::CardsPerRegion / chunks_per_region)) +{ + guarantee(chunks_per_region > 0, "%u chunks per region", chunks_per_region); +} + +G1CardTableClaimTable::~G1CardTableClaimTable() { + FREE_C_HEAP_ARRAY(uint, _card_claims); +} + +void G1CardTableClaimTable::initialize(uint max_reserved_regions) { + assert(_card_claims == nullptr, "Must not be initialized twice"); + _card_claims = NEW_C_HEAP_ARRAY(uint, max_reserved_regions, mtGC); + _max_reserved_regions = max_reserved_regions; + reset_all_to_unclaimed(); +} + +void G1CardTableClaimTable::reset_all_to_unclaimed() { + for (uint i = 0; i < _max_reserved_regions; i++) { + _card_claims[i] = 0; + } +} + +void G1CardTableClaimTable::reset_all_to_claimed() { + for (uint i = 0; i < _max_reserved_regions; i++) { + _card_claims[i] = (uint)G1HeapRegion::CardsPerRegion; + } +} + +void G1CardTableClaimTable::heap_region_iterate_from_worker_offset(G1HeapRegionClosure* cl, uint worker_id, uint max_workers) { + // Every worker will actually look at all regions, skipping over regions that + // are completed. + const size_t n_regions = _max_reserved_regions; + const uint start_index = (uint)(worker_id * n_regions / max_workers); + + for (uint count = 0; count < n_regions; count++) { + const uint index = (start_index + count) % n_regions; + assert(index < n_regions, "sanity"); + // Skip over fully processed regions + if (!has_unclaimed_cards(index)) { + continue; + } + G1HeapRegion* r = G1CollectedHeap::heap()->region_at(index); + bool res = cl->do_heap_region(r); + if (res) { + return; + } + } +} + +G1CardTableChunkClaimer::G1CardTableChunkClaimer(G1CardTableClaimTable* scan_state, uint region_idx) : + _claim_values(scan_state), + _region_idx(region_idx), + _cur_claim(0) { + guarantee(size() <= G1HeapRegion::CardsPerRegion, "Should not claim more space than possible."); +} + +G1ChunkScanner::G1ChunkScanner(CardValue* const start_card, CardValue* const end_card) : + _start_card(start_card), + _end_card(end_card) { + assert(is_word_aligned(start_card), "precondition"); + assert(is_word_aligned(end_card), "precondition"); +} diff --git a/src/hotspot/share/gc/g1/g1CardTableClaimTable.hpp b/src/hotspot/share/gc/g1/g1CardTableClaimTable.hpp new file mode 100644 index 00000000000..4f524b83f97 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1CardTableClaimTable.hpp @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#ifndef SHARE_GC_G1_G1CARDTABLECLAIMTABLE_HPP +#define SHARE_GC_G1_G1CARDTABLECLAIMTABLE_HPP + +#include "gc/g1/g1CardTable.hpp" +#include "memory/allocation.hpp" + +class G1HeapRegionClosure; + +// Helper class representing claim values for the cards in the card table corresponding +// to a region. +// I.e. for every region this class stores an atomic counter that represents the +// number of cards from 0 to the number of cards per region already claimed for +// this region. +// If the claimed value is >= the number of cards of a region, the region can be +// considered fully claimed. +// +// Claiming works on full region (all cards in region) or a range of contiguous cards +// (chunk). Chunk size is given at construction time. +class G1CardTableClaimTable : public CHeapObj { + uint _max_reserved_regions; + + // Card table iteration claim values for every heap region, from 0 (completely unclaimed) + // to (>=) G1HeapRegion::CardsPerRegion (completely claimed). + uint volatile* _card_claims; + + uint _cards_per_chunk; // For conversion between card index and chunk index. + + // Claim increment number of cards, returning the previous claim value. + inline uint claim_cards(uint region, uint increment); + +public: + G1CardTableClaimTable(uint chunks_per_region); + ~G1CardTableClaimTable(); + + // Allocates the data structure and initializes the claims to unclaimed. + void initialize(uint max_reserved_regions); + + void reset_all_to_unclaimed(); + void reset_all_to_claimed(); + + inline bool has_unclaimed_cards(uint region); + inline void reset_to_unclaimed(uint region); + + // Claims all cards in that region, returning the previous claim value. + inline uint claim_all_cards(uint region); + + // Claim a single chunk in that region, returning the previous claim value. + inline uint claim_chunk(uint region); + inline uint cards_per_chunk() const; + + size_t max_reserved_regions() { return _max_reserved_regions; } + + void heap_region_iterate_from_worker_offset(G1HeapRegionClosure* cl, uint worker_id, uint max_workers); +}; + +// Helper class to claim dirty chunks within the card table for a given region. +class G1CardTableChunkClaimer { + G1CardTableClaimTable* _claim_values; + + uint _region_idx; + uint _cur_claim; + +public: + G1CardTableChunkClaimer(G1CardTableClaimTable* claim_table, uint region_idx); + + inline bool has_next(); + + inline uint value() const; + inline uint size() const; +}; + +// Helper class to locate consecutive dirty cards inside a range of cards. +class G1ChunkScanner { + using Word = size_t; + using CardValue = G1CardTable::CardValue; + + CardValue* const _start_card; + CardValue* const _end_card; + + static const size_t ExpandedToScanMask = G1CardTable::WordAlreadyScanned; + static const size_t ToScanMask = G1CardTable::g1_card_already_scanned; + + inline bool is_card_dirty(const CardValue* const card) const; + + inline bool is_word_aligned(const void* const addr) const; + + inline CardValue* find_first_dirty_card(CardValue* i_card) const; + inline CardValue* find_first_non_dirty_card(CardValue* i_card) const; + +public: + G1ChunkScanner(CardValue* const start_card, CardValue* const end_card); + + template + void on_dirty_cards(Func&& f) { + for (CardValue* cur_card = _start_card; cur_card < _end_card; /* empty */) { + CardValue* dirty_l = find_first_dirty_card(cur_card); + CardValue* dirty_r = find_first_non_dirty_card(dirty_l); + + assert(dirty_l <= dirty_r, "inv"); + + if (dirty_l == dirty_r) { + assert(dirty_r == _end_card, "finished the entire chunk"); + return; + } + + f(dirty_l, dirty_r); + + cur_card = dirty_r + 1; + } + } +}; + +#endif // SHARE_GC_G1_G1CARDTABLECLAIMTABLE_HPP diff --git a/src/hotspot/share/gc/g1/g1CardTableClaimTable.inline.hpp b/src/hotspot/share/gc/g1/g1CardTableClaimTable.inline.hpp new file mode 100644 index 00000000000..d682f0d17ae --- /dev/null +++ b/src/hotspot/share/gc/g1/g1CardTableClaimTable.inline.hpp @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#ifndef SHARE_GC_G1_G1CARDTABLECLAIMTABLE_INLINE_HPP +#define SHARE_GC_G1_G1CARDTABLECLAIMTABLE_INLINE_HPP + +#include "gc/g1/g1CardTableClaimTable.hpp" + +#include "gc/g1/g1CollectedHeap.inline.hpp" +#include "gc/g1/g1HeapRegion.inline.hpp" +#include "runtime/atomicAccess.hpp" + +bool G1CardTableClaimTable::has_unclaimed_cards(uint region) { + assert(region < _max_reserved_regions, "Tried to access invalid region %u", region); + return AtomicAccess::load(&_card_claims[region]) < G1HeapRegion::CardsPerRegion; +} + +void G1CardTableClaimTable::reset_to_unclaimed(uint region) { + assert(region < _max_reserved_regions, "Tried to access invalid region %u", region); + AtomicAccess::store(&_card_claims[region], 0u); +} + +uint G1CardTableClaimTable::claim_cards(uint region, uint increment) { + assert(region < _max_reserved_regions, "Tried to access invalid region %u", region); + return AtomicAccess::fetch_then_add(&_card_claims[region], increment, memory_order_relaxed); +} + +uint G1CardTableClaimTable::claim_chunk(uint region) { + assert(region < _max_reserved_regions, "Tried to access invalid region %u", region); + return AtomicAccess::fetch_then_add(&_card_claims[region], cards_per_chunk(), memory_order_relaxed); +} + +uint G1CardTableClaimTable::claim_all_cards(uint region) { + return claim_cards(region, (uint)G1HeapRegion::CardsPerRegion); +} + +uint G1CardTableClaimTable::cards_per_chunk() const { return _cards_per_chunk; } + +bool G1CardTableChunkClaimer::has_next() { + _cur_claim = _claim_values->claim_chunk(_region_idx); + return (_cur_claim < G1HeapRegion::CardsPerRegion); +} + +uint G1CardTableChunkClaimer::value() const { return _cur_claim; } +uint G1CardTableChunkClaimer::size() const { return _claim_values->cards_per_chunk(); } + +bool G1ChunkScanner::is_card_dirty(const CardValue* const card) const { + return (*card & ToScanMask) == 0; +} + +bool G1ChunkScanner::is_word_aligned(const void* const addr) const { + return ((uintptr_t)addr) % sizeof(Word) == 0; +} + +G1CardTable::CardValue* G1ChunkScanner::find_first_dirty_card(CardValue* i_card) const { + while (!is_word_aligned(i_card)) { + if (is_card_dirty(i_card)) { + return i_card; + } + i_card++; + } + + for (/* empty */; i_card < _end_card; i_card += sizeof(Word)) { + Word word_value = *reinterpret_cast(i_card); + bool has_dirty_cards_in_word = (~word_value & ExpandedToScanMask) != 0; + + if (has_dirty_cards_in_word) { + for (uint i = 0; i < sizeof(Word); ++i) { + if (is_card_dirty(i_card)) { + return i_card; + } + i_card++; + } + ShouldNotReachHere(); + } + } + + return _end_card; +} + +G1CardTable::CardValue* G1ChunkScanner::find_first_non_dirty_card(CardValue* i_card) const { + while (!is_word_aligned(i_card)) { + if (!is_card_dirty(i_card)) { + return i_card; + } + i_card++; + } + + for (/* empty */; i_card < _end_card; i_card += sizeof(Word)) { + Word word_value = *reinterpret_cast(i_card); + bool all_cards_dirty = (word_value & ExpandedToScanMask) == 0; + + if (!all_cards_dirty) { + for (uint i = 0; i < sizeof(Word); ++i) { + if (!is_card_dirty(i_card)) { + return i_card; + } + i_card++; + } + ShouldNotReachHere(); + } + } + + return _end_card; +} + +#endif // SHARE_GC_G1_G1CARDTABLECLAIMTABLE_INLINE_HPP diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index 4a257265931..ed21c9aa370 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -38,7 +38,6 @@ #include "gc/g1/g1ConcurrentMarkThread.inline.hpp" #include "gc/g1/g1ConcurrentRefine.hpp" #include "gc/g1/g1ConcurrentRefineThread.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1EvacStats.inline.hpp" #include "gc/g1/g1FullCollector.hpp" #include "gc/g1/g1GCCounters.hpp" @@ -60,10 +59,10 @@ #include "gc/g1/g1ParScanThreadState.inline.hpp" #include "gc/g1/g1PeriodicGCTask.hpp" #include "gc/g1/g1Policy.hpp" -#include "gc/g1/g1RedirtyCardsQueue.hpp" #include "gc/g1/g1RegionPinCache.inline.hpp" #include "gc/g1/g1RegionToSpaceMapper.hpp" #include "gc/g1/g1RemSet.hpp" +#include "gc/g1/g1ReviseYoungLengthTask.hpp" #include "gc/g1/g1RootClosures.hpp" #include "gc/g1/g1RootProcessor.hpp" #include "gc/g1/g1SATBMarkQueueSet.hpp" @@ -111,6 +110,7 @@ #include "runtime/init.hpp" #include "runtime/java.hpp" #include "runtime/orderAccess.hpp" +#include "runtime/threads.hpp" #include "runtime/threadSMR.hpp" #include "runtime/vmThread.hpp" #include "utilities/align.hpp" @@ -146,7 +146,7 @@ void G1CollectedHeap::run_batch_task(G1BatchedTask* cl) { workers()->run_task(cl, num_workers); } -uint G1CollectedHeap::get_chunks_per_region() { +uint G1CollectedHeap::get_chunks_per_region_for_scan() { uint log_region_size = G1HeapRegion::LogOfHRGrainBytes; // Limit the expected input values to current known possible values of the // (log) region size. Adjust as necessary after testing if changing the permissible @@ -156,6 +156,18 @@ uint G1CollectedHeap::get_chunks_per_region() { return 1u << (log_region_size / 2 - 4); } +uint G1CollectedHeap::get_chunks_per_region_for_merge() { + uint log_region_size = G1HeapRegion::LogOfHRGrainBytes; + // Limit the expected input values to current known possible values of the + // (log) region size. Adjust as necessary after testing if changing the permissible + // values for region size. + assert(log_region_size >= 20 && log_region_size <= 29, + "expected value in [20,29], but got %u", log_region_size); + + uint half_log_region_size = (log_region_size + 1) / 2; + return 1 << (half_log_region_size - 9); +} + G1HeapRegion* G1CollectedHeap::new_heap_region(uint hrs_index, MemRegion mr) { return new G1HeapRegion(hrs_index, bot(), mr, &_card_set_config); @@ -614,7 +626,6 @@ inline HeapWord* G1CollectedHeap::attempt_allocation(size_t min_word_size, assert_heap_not_locked(); if (result != nullptr) { assert(*actual_word_size != 0, "Actual size must have been set here"); - dirty_young_block(result, *actual_word_size); } else { *actual_word_size = 0; } @@ -809,11 +820,27 @@ void G1CollectedHeap::prepare_for_mutator_after_full_collection(size_t allocatio } void G1CollectedHeap::abort_refinement() { - // Discard all remembered set updates and reset refinement statistics. - G1BarrierSet::dirty_card_queue_set().abandon_logs_and_stats(); - assert(G1BarrierSet::dirty_card_queue_set().num_cards() == 0, - "DCQS should be empty"); - concurrent_refine()->get_and_reset_refinement_stats(); + G1ConcurrentRefineSweepState& sweep_state = concurrent_refine()->sweep_state(); + if (sweep_state.is_in_progress()) { + + if (!sweep_state.are_java_threads_synched()) { + // Synchronize Java threads with global card table that has already been swapped. + class SwapThreadCardTableClosure : public ThreadClosure { + public: + + virtual void do_thread(Thread* t) { + G1BarrierSet* bs = G1BarrierSet::g1_barrier_set(); + bs->update_card_table_base(t); + } + } cl; + Threads::java_threads_do(&cl); + } + + // Record any available refinement statistics. + policy()->record_refinement_stats(sweep_state.stats()); + sweep_state.complete_work(false /* concurrent */, false /* print_log */); + } + sweep_state.reset_stats(); } void G1CollectedHeap::verify_after_full_collection() { @@ -825,6 +852,7 @@ void G1CollectedHeap::verify_after_full_collection() { } _hrm.verify_optional(); _verifier->verify_region_sets_optional(); + _verifier->verify_card_tables_clean(true /* both_card_tables */); _verifier->verify_after_gc(); _verifier->verify_bitmap_clear(false /* above_tams_only */); @@ -1168,8 +1196,13 @@ G1CollectedHeap::G1CollectedHeap() : _service_thread(nullptr), _periodic_gc_task(nullptr), _free_arena_memory_task(nullptr), + _revise_young_length_task(nullptr), _workers(nullptr), - _card_table(nullptr), + _refinement_epoch(0), + _last_synchronized_start(0), + _last_refinement_epoch_start(0), + _yield_duration_in_refinement_epoch(0), + _last_safepoint_refinement_epoch(0), _collection_pause_end(Ticks::now()), _old_set("Old Region Set", new OldRegionSetChecker()), _humongous_set("Humongous Region Set", new HumongousRegionSetChecker()), @@ -1289,7 +1322,7 @@ G1RegionToSpaceMapper* G1CollectedHeap::create_aux_memory_mapper(const char* des jint G1CollectedHeap::initialize_concurrent_refinement() { jint ecode = JNI_OK; - _cr = G1ConcurrentRefine::create(policy(), &ecode); + _cr = G1ConcurrentRefine::create(this, &ecode); return ecode; } @@ -1345,18 +1378,12 @@ jint G1CollectedHeap::initialize() { initialize_reserved_region(heap_rs); // Create the barrier set for the entire reserved region. - G1CardTable* ct = new G1CardTable(_reserved); - G1BarrierSet* bs = new G1BarrierSet(ct); + G1CardTable* card_table = new G1CardTable(_reserved); + G1CardTable* refinement_table = new G1CardTable(_reserved); + + G1BarrierSet* bs = new G1BarrierSet(card_table, refinement_table); bs->initialize(); assert(bs->is_a(BarrierSet::G1BarrierSet), "sanity"); - BarrierSet::set_barrier_set(bs); - _card_table = ct; - - { - G1SATBMarkQueueSet& satbqs = bs->satb_mark_queue_set(); - satbqs.set_process_completed_buffers_threshold(G1SATBProcessCompletedThreshold); - satbqs.set_buffer_enqueue_threshold_percentage(G1SATBBufferEnqueueingThresholdPercent); - } // Create space mappers. size_t page_size = heap_rs.page_size(); @@ -1391,12 +1418,26 @@ jint G1CollectedHeap::initialize() { G1CardTable::compute_size(heap_rs.size() / HeapWordSize), G1CardTable::heap_map_factor()); + G1RegionToSpaceMapper* refinement_cards_storage = + create_aux_memory_mapper("Refinement Card Table", + G1CardTable::compute_size(heap_rs.size() / HeapWordSize), + G1CardTable::heap_map_factor()); + size_t bitmap_size = G1CMBitMap::compute_size(heap_rs.size()); G1RegionToSpaceMapper* bitmap_storage = create_aux_memory_mapper("Mark Bitmap", bitmap_size, G1CMBitMap::heap_map_factor()); - _hrm.initialize(heap_storage, bitmap_storage, bot_storage, cardtable_storage); - _card_table->initialize(cardtable_storage); + _hrm.initialize(heap_storage, bitmap_storage, bot_storage, cardtable_storage, refinement_cards_storage); + card_table->initialize(cardtable_storage); + refinement_table->initialize(refinement_cards_storage); + + BarrierSet::set_barrier_set(bs); + + { + G1SATBMarkQueueSet& satbqs = bs->satb_mark_queue_set(); + satbqs.set_process_completed_buffers_threshold(G1SATBProcessCompletedThreshold); + satbqs.set_buffer_enqueue_threshold_percentage(G1SATBBufferEnqueueingThresholdPercent); + } // 6843694 - ensure that the maximum region index can fit // in the remembered set structures. @@ -1408,7 +1449,7 @@ jint G1CollectedHeap::initialize() { guarantee((uintptr_t)(heap_rs.base()) >= G1CardTable::card_size(), "Java heap must not start within the first card."); G1FromCardCache::initialize(max_num_regions()); // Also create a G1 rem set. - _rem_set = new G1RemSet(this, _card_table); + _rem_set = new G1RemSet(this); _rem_set->initialize(max_num_regions()); size_t max_cards_per_region = ((size_t)1 << (sizeof(CardIdx_t)*BitsPerByte-1)) - 1; @@ -1467,6 +1508,11 @@ jint G1CollectedHeap::initialize() { _free_arena_memory_task = new G1MonotonicArenaFreeMemoryTask("Card Set Free Memory Task"); _service_thread->register_task(_free_arena_memory_task); + if (policy()->use_adaptive_young_list_length()) { + _revise_young_length_task = new G1ReviseYoungLengthTask("Revise Young Length List Task"); + _service_thread->register_task(_revise_young_length_task); + } + // Here we allocate the dummy G1HeapRegion that is required by the // G1AllocRegion class. G1HeapRegion* dummy_region = _hrm.get_dummy_region(); @@ -1495,6 +1541,7 @@ jint G1CollectedHeap::initialize() { CPUTimeCounters::create_counter(CPUTimeGroups::CPUTimeType::gc_parallel_workers); CPUTimeCounters::create_counter(CPUTimeGroups::CPUTimeType::gc_conc_mark); CPUTimeCounters::create_counter(CPUTimeGroups::CPUTimeType::gc_conc_refine); + CPUTimeCounters::create_counter(CPUTimeGroups::CPUTimeType::gc_conc_refine_control); CPUTimeCounters::create_counter(CPUTimeGroups::CPUTimeType::gc_service); G1InitLogger::print(); @@ -1519,12 +1566,35 @@ void G1CollectedHeap::stop() { void G1CollectedHeap::safepoint_synchronize_begin() { SuspendibleThreadSet::synchronize(); + + _last_synchronized_start = os::elapsed_counter(); } void G1CollectedHeap::safepoint_synchronize_end() { + jlong now = os::elapsed_counter(); + jlong synchronize_duration = now - _last_synchronized_start; + + if (_last_safepoint_refinement_epoch == _refinement_epoch) { + _yield_duration_in_refinement_epoch += synchronize_duration; + } else { + _last_refinement_epoch_start = now; + _last_safepoint_refinement_epoch = _refinement_epoch; + _yield_duration_in_refinement_epoch = 0; + } + SuspendibleThreadSet::desynchronize(); } +void G1CollectedHeap::set_last_refinement_epoch_start(jlong epoch_start, jlong last_yield_duration) { + _last_refinement_epoch_start = epoch_start; + guarantee(_yield_duration_in_refinement_epoch >= last_yield_duration, "should be"); + _yield_duration_in_refinement_epoch -= last_yield_duration; +} + +jlong G1CollectedHeap::yield_duration_in_refinement_epoch() { + return _yield_duration_in_refinement_epoch; +} + void G1CollectedHeap::post_initialize() { CollectedHeap::post_initialize(); ref_processing_init(); @@ -2336,6 +2406,7 @@ void G1CollectedHeap::gc_epilogue(bool full) { &_collection_set_candidates_card_set_stats); update_perf_counter_cpu_time(); + _refinement_epoch++; } uint G1CollectedHeap::uncommit_regions(uint region_limit) { @@ -2468,7 +2539,6 @@ void G1CollectedHeap::verify_before_young_collection(G1HeapVerifier::G1VerifyTyp Ticks start = Ticks::now(); _verifier->prepare_for_verify(); _verifier->verify_region_sets_optional(); - _verifier->verify_dirty_young_regions(); _verifier->verify_before_gc(); verify_numa_regions("GC Start"); phase_times()->record_verify_before_time_ms((Ticks::now() - start).seconds() * MILLIUNITS); @@ -2734,6 +2804,11 @@ void G1CollectedHeap::free_region(G1HeapRegion* hr, G1FreeRegionList* free_list) if (free_list != nullptr) { free_list->add_ordered(hr); } + if (VerifyDuringGC) { + // Card and refinement table must be clear for freed regions. + card_table()->verify_region(MemRegion(hr->bottom(), hr->end()), G1CardTable::clean_card_val(), true); + refinement_table()->verify_region(MemRegion(hr->bottom(), hr->end()), G1CardTable::clean_card_val(), true); + } } void G1CollectedHeap::retain_region(G1HeapRegion* hr) { diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp index 8d26bcb1c0b..43839cc48d5 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp @@ -75,6 +75,7 @@ class G1GCPhaseTimes; class G1HeapSizingPolicy; class G1NewTracer; class G1RemSet; +class G1ReviseYoungLengthTask; class G1ServiceTask; class G1ServiceThread; class GCMemoryManager; @@ -171,9 +172,23 @@ private: G1ServiceThread* _service_thread; G1ServiceTask* _periodic_gc_task; G1MonotonicArenaFreeMemoryTask* _free_arena_memory_task; + G1ReviseYoungLengthTask* _revise_young_length_task; WorkerThreads* _workers; - G1CardTable* _card_table; + + // The current epoch for refinement, i.e. the number of times the card tables + // have been swapped by a garbage collection. + // Used for detecting whether concurrent refinement has been interrupted by a + // garbage collection. + size_t _refinement_epoch; + + // The following members are for tracking safepoint durations between garbage + // collections. + jlong _last_synchronized_start; + + jlong _last_refinement_epoch_start; + jlong _yield_duration_in_refinement_epoch; // Time spent in safepoints since beginning of last refinement epoch. + size_t _last_safepoint_refinement_epoch; // Refinement epoch before last safepoint. Ticks _collection_pause_end; @@ -541,12 +556,17 @@ public: void run_batch_task(G1BatchedTask* cl); // Return "optimal" number of chunks per region we want to use for claiming areas - // within a region to claim. + // within a region to claim during card table scanning. // The returned value is a trade-off between granularity of work distribution and // memory usage and maintenance costs of that table. // Testing showed that 64 for 1M/2M region, 128 for 4M/8M regions, 256 for 16/32M regions, // and so on seems to be such a good trade-off. - static uint get_chunks_per_region(); + static uint get_chunks_per_region_for_scan(); + // Return "optimal" number of chunks per region we want to use for claiming areas + // within a region to claim during card table merging. + // This is much smaller than for scanning as the merge work is much smaller. + // Currently 1 for 1M regions, 2 for 2/4M regions, 4 for 8/16M regions and so on. + static uint get_chunks_per_region_for_merge(); G1Allocator* allocator() { return _allocator; @@ -687,11 +707,6 @@ public: // Add the given region to the retained regions collection set candidates. void retain_region(G1HeapRegion* hr); - // It dirties the cards that cover the block so that the post - // write barrier never queues anything when updating objects on this - // block. It is assumed (and in fact we assert) that the block - // belongs to a young region. - inline void dirty_young_block(HeapWord* start, size_t word_size); // Frees a humongous region by collapsing it into individual regions // and calling free_region() for each of them. The freed regions @@ -905,6 +920,10 @@ public: void safepoint_synchronize_begin() override; void safepoint_synchronize_end() override; + jlong last_refinement_epoch_start() const { return _last_refinement_epoch_start; } + void set_last_refinement_epoch_start(jlong epoch_start, jlong last_yield_duration); + jlong yield_duration_in_refinement_epoch(); + // Does operations required after initialization has been done. void post_initialize() override; @@ -1069,7 +1088,16 @@ public: } G1CardTable* card_table() const { - return _card_table; + return static_cast(G1BarrierSet::g1_barrier_set()->card_table()); + } + + G1CardTable* refinement_table() const { + return G1BarrierSet::g1_barrier_set()->refinement_table(); + } + + G1CardTable::CardValue* card_table_base() const { + assert(card_table() != nullptr, "must be"); + return card_table()->byte_map_base(); } // Iteration functions. diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.inline.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.inline.hpp index 3370ff9938f..fdc8585dbc0 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.inline.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.inline.hpp @@ -149,30 +149,6 @@ inline void G1CollectedHeap::old_set_remove(G1HeapRegion* hr) { _old_set.remove(hr); } -// It dirties the cards that cover the block so that the post -// write barrier never queues anything when updating objects on this -// block. It is assumed (and in fact we assert) that the block -// belongs to a young region. -inline void -G1CollectedHeap::dirty_young_block(HeapWord* start, size_t word_size) { - assert_heap_not_locked(); - - // Assign the containing region to containing_hr so that we don't - // have to keep calling heap_region_containing() in the - // asserts below. - DEBUG_ONLY(G1HeapRegion* containing_hr = heap_region_containing(start);) - assert(word_size > 0, "pre-condition"); - assert(containing_hr->is_in(start), "it should contain start"); - assert(containing_hr->is_young(), "it should be young"); - assert(!containing_hr->is_humongous(), "it should not be humongous"); - - HeapWord* end = start + word_size; - assert(containing_hr->is_in(end - 1), "it should also contain end - 1"); - - MemRegion mr(start, end); - card_table()->g1_mark_as_young(mr); -} - inline G1ScannerTasksQueueSet* G1CollectedHeap::task_queues() const { return _task_queues; } diff --git a/src/hotspot/share/gc/g1/g1CollectionSet.cpp b/src/hotspot/share/gc/g1/g1CollectionSet.cpp index d501ee5b47b..abfb620d626 100644 --- a/src/hotspot/share/gc/g1/g1CollectionSet.cpp +++ b/src/hotspot/share/gc/g1/g1CollectionSet.cpp @@ -308,7 +308,8 @@ double G1CollectionSet::finalize_young_part(double target_pause_time_ms, G1Survi guarantee(target_pause_time_ms > 0.0, "target_pause_time_ms = %1.6lf should be positive", target_pause_time_ms); - size_t pending_cards = _policy->pending_cards_at_gc_start(); + bool in_young_only_phase = _policy->collector_state()->in_young_only_phase(); + size_t pending_cards = _policy->analytics()->predict_pending_cards(in_young_only_phase); log_trace(gc, ergo, cset)("Start choosing CSet. Pending cards: %zu target pause time: %1.2fms", pending_cards, target_pause_time_ms); @@ -323,10 +324,8 @@ double G1CollectionSet::finalize_young_part(double target_pause_time_ms, G1Survi verify_young_cset_indices(); - size_t num_young_cards = _g1h->young_regions_cardset()->occupied(); - _policy->record_card_rs_length(num_young_cards); - - double predicted_base_time_ms = _policy->predict_base_time_ms(pending_cards, num_young_cards); + size_t card_rs_length = _policy->analytics()->predict_card_rs_length(in_young_only_phase); + double predicted_base_time_ms = _policy->predict_base_time_ms(pending_cards, card_rs_length); // Base time already includes the whole remembered set related time, so do not add that here // again. double predicted_eden_time = _policy->predict_young_region_other_time_ms(eden_region_length) + diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp index e52d380e26b..97386cb9720 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp @@ -27,6 +27,7 @@ #include "gc/g1/g1BarrierSet.hpp" #include "gc/g1/g1BatchedTask.hpp" #include "gc/g1/g1CardSetMemory.hpp" +#include "gc/g1/g1CardTableClaimTable.inline.hpp" #include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1CollectionSetChooser.hpp" #include "gc/g1/g1CollectorState.hpp" @@ -34,7 +35,7 @@ #include "gc/g1/g1ConcurrentMarkRemarkTasks.hpp" #include "gc/g1/g1ConcurrentMarkThread.inline.hpp" #include "gc/g1/g1ConcurrentRebuildAndScrub.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" +#include "gc/g1/g1ConcurrentRefine.hpp" #include "gc/g1/g1HeapRegion.inline.hpp" #include "gc/g1/g1HeapRegionManager.hpp" #include "gc/g1/g1HeapRegionPrinter.hpp" @@ -483,7 +484,7 @@ G1ConcurrentMark::G1ConcurrentMark(G1CollectedHeap* g1h, // _finger set in set_non_marking_state - _worker_id_offset(G1DirtyCardQueueSet::num_par_ids() + G1ConcRefinementThreads), + _worker_id_offset(G1ConcRefinementThreads), // The refinement control thread does not refine cards, so it's just the worker threads. _max_num_tasks(MAX2(ConcGCThreads, ParallelGCThreads)), // _num_active_tasks set in set_non_marking_state() // _tasks set inside the constructor @@ -1141,7 +1142,7 @@ void G1ConcurrentMark::mark_from_roots() { // worker threads may currently exist and more may not be // available. active_workers = _concurrent_workers->set_active_workers(active_workers); - log_info(gc, task)("Using %u workers of %u for marking", active_workers, _concurrent_workers->max_workers()); + log_info(gc, task)("Concurrent Mark Using %u of %u Workers", active_workers, _concurrent_workers->max_workers()); _num_concurrent_workers = active_workers; diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMark.hpp b/src/hotspot/share/gc/g1/g1ConcurrentMark.hpp index 4977da4729d..752082ce629 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMark.hpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMark.hpp @@ -580,6 +580,8 @@ public: // TARS for the given region during remembered set rebuilding. inline HeapWord* top_at_rebuild_start(G1HeapRegion* r) const; + uint worker_id_offset() const { return _worker_id_offset; } + // Clear statistics gathered during the concurrent cycle for the given region after // it has been reclaimed. void clear_statistics(G1HeapRegion* r); diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp index 02afc443d68..fdef4214622 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMarkRemarkTasks.cpp @@ -25,6 +25,7 @@ #include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1ConcurrentMark.inline.hpp" #include "gc/g1/g1ConcurrentMarkRemarkTasks.hpp" +#include "gc/g1/g1ConcurrentRefine.hpp" #include "gc/g1/g1HeapRegion.inline.hpp" #include "gc/g1/g1HeapRegionPrinter.hpp" #include "gc/g1/g1RemSetTrackingPolicy.hpp" @@ -54,15 +55,16 @@ struct G1UpdateRegionLivenessAndSelectForRebuildTask::G1OnRegionClosure : public _num_humongous_regions_removed(0), _local_cleanup_list(local_cleanup_list) {} - void reclaim_empty_region(G1HeapRegion* hr) { + void reclaim_empty_region_common(G1HeapRegion* hr) { assert(!hr->has_pinned_objects(), "precondition"); assert(hr->used() > 0, "precondition"); _freed_bytes += hr->used(); hr->set_containing_set(nullptr); - hr->clear_cardtable(); + hr->clear_both_card_tables(); _cm->clear_statistics(hr); G1HeapRegionPrinter::mark_reclaim(hr); + _g1h->concurrent_refine()->notify_region_reclaimed(hr); } void reclaim_empty_humongous_region(G1HeapRegion* hr) { @@ -71,8 +73,8 @@ struct G1UpdateRegionLivenessAndSelectForRebuildTask::G1OnRegionClosure : public auto on_humongous_region = [&] (G1HeapRegion* hr) { assert(hr->is_humongous(), "precondition"); - reclaim_empty_region(hr); _num_humongous_regions_removed++; + reclaim_empty_region_common(hr); _g1h->free_humongous_region(hr, _local_cleanup_list); }; @@ -82,8 +84,8 @@ struct G1UpdateRegionLivenessAndSelectForRebuildTask::G1OnRegionClosure : public void reclaim_empty_old_region(G1HeapRegion* hr) { assert(hr->is_old(), "precondition"); - reclaim_empty_region(hr); _num_old_regions_removed++; + reclaim_empty_region_common(hr); _g1h->free_region(hr, _local_cleanup_list); } diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRebuildAndScrub.cpp b/src/hotspot/share/gc/g1/g1ConcurrentRebuildAndScrub.cpp index 0633e18411d..cd560a41333 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRebuildAndScrub.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRebuildAndScrub.cpp @@ -245,7 +245,7 @@ class G1RebuildRSAndScrubTask : public WorkerTask { G1RebuildRSAndScrubRegionClosure(G1ConcurrentMark* cm, bool should_rebuild_remset, uint worker_id) : _cm(cm), _bitmap(_cm->mark_bitmap()), - _rebuild_closure(G1CollectedHeap::heap(), worker_id), + _rebuild_closure(G1CollectedHeap::heap(), worker_id + cm->worker_id_offset()), _should_rebuild_remset(should_rebuild_remset), _processed_words(0) { } diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefine.cpp b/src/hotspot/share/gc/g1/g1ConcurrentRefine.cpp index 84776b7a4b1..ed6a9ad4292 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefine.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefine.cpp @@ -22,15 +22,20 @@ * */ +#include "gc/g1/g1Analytics.hpp" #include "gc/g1/g1BarrierSet.hpp" +#include "gc/g1/g1CardTableClaimTable.inline.hpp" +#include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1CollectionSet.hpp" #include "gc/g1/g1ConcurrentRefine.hpp" +#include "gc/g1/g1ConcurrentRefineSweepTask.hpp" #include "gc/g1/g1ConcurrentRefineThread.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1HeapRegion.inline.hpp" #include "gc/g1/g1HeapRegionRemSet.inline.hpp" #include "gc/g1/g1Policy.hpp" #include "gc/shared/gc_globals.hpp" +#include "gc/shared/gcTraceTime.inline.hpp" +#include "gc/shared/workerThread.hpp" #include "logging/log.hpp" #include "memory/allocation.inline.hpp" #include "memory/iterator.hpp" @@ -38,17 +43,15 @@ #include "runtime/mutexLocker.hpp" #include "utilities/debug.hpp" #include "utilities/globalDefinitions.hpp" +#include "utilities/ticks.hpp" #include -G1ConcurrentRefineThread* G1ConcurrentRefineThreadControl::create_refinement_thread(uint worker_id, bool initializing) { +G1ConcurrentRefineThread* G1ConcurrentRefineThreadControl::create_refinement_thread() { G1ConcurrentRefineThread* result = nullptr; - if (initializing || !InjectGCWorkerCreationFailure) { - result = G1ConcurrentRefineThread::create(_cr, worker_id); - } + result = G1ConcurrentRefineThread::create(_cr); if (result == nullptr || result->osthread() == nullptr) { - log_warning(gc)("Failed to create refinement thread %u, no more %s", - worker_id, + log_warning(gc)("Failed to create refinement control thread, no more %s", result == nullptr ? "memory" : "OS threads"); if (result != nullptr) { delete result; @@ -60,106 +63,392 @@ G1ConcurrentRefineThread* G1ConcurrentRefineThreadControl::create_refinement_thr G1ConcurrentRefineThreadControl::G1ConcurrentRefineThreadControl(uint max_num_threads) : _cr(nullptr), - _threads(max_num_threads) + _control_thread(nullptr), + _workers(nullptr), + _max_num_threads(max_num_threads) {} G1ConcurrentRefineThreadControl::~G1ConcurrentRefineThreadControl() { - while (_threads.is_nonempty()) { - delete _threads.pop(); - } -} - -bool G1ConcurrentRefineThreadControl::ensure_threads_created(uint worker_id, bool initializing) { - assert(worker_id < max_num_threads(), "precondition"); - - while ((uint)_threads.length() <= worker_id) { - G1ConcurrentRefineThread* rt = create_refinement_thread(_threads.length(), initializing); - if (rt == nullptr) { - return false; - } - _threads.push(rt); - } - - return true; + delete _control_thread; + delete _workers; } jint G1ConcurrentRefineThreadControl::initialize(G1ConcurrentRefine* cr) { assert(cr != nullptr, "G1ConcurrentRefine must not be null"); _cr = cr; - if (max_num_threads() > 0) { - _threads.push(create_refinement_thread(0, true)); - if (_threads.at(0) == nullptr) { - vm_shutdown_during_initialization("Could not allocate primary refinement thread"); + if (is_refinement_enabled()) { + _control_thread = create_refinement_thread(); + if (_control_thread == nullptr) { + vm_shutdown_during_initialization("Could not allocate refinement control thread"); return JNI_ENOMEM; } - - if (!UseDynamicNumberOfGCThreads) { - if (!ensure_threads_created(max_num_threads() - 1, true)) { - vm_shutdown_during_initialization("Could not allocate refinement threads"); - return JNI_ENOMEM; - } - } + _workers = new WorkerThreads("G1 Refinement Workers", max_num_threads()); + _workers->initialize_workers(); } - return JNI_OK; } #ifdef ASSERT -void G1ConcurrentRefineThreadControl::assert_current_thread_is_primary_refinement_thread() const { - assert(Thread::current() == _threads.at(0), "Not primary thread"); +void G1ConcurrentRefineThreadControl::assert_current_thread_is_control_refinement_thread() const { + assert(Thread::current() == _control_thread, "Not refinement control thread"); } #endif // ASSERT -bool G1ConcurrentRefineThreadControl::activate(uint worker_id) { - if (ensure_threads_created(worker_id, false)) { - _threads.at(worker_id)->activate(); - return true; - } +void G1ConcurrentRefineThreadControl::activate() { + _control_thread->activate(); +} - return false; +void G1ConcurrentRefineThreadControl::run_task(WorkerTask* task, uint num_workers) { + assert(num_workers >= 1, "must be"); + + WithActiveWorkers w(_workers, num_workers); + _workers->run_task(task); +} + +void G1ConcurrentRefineThreadControl::control_thread_do(ThreadClosure* tc) { + if (is_refinement_enabled()) { + tc->do_thread(_control_thread); + } } void G1ConcurrentRefineThreadControl::worker_threads_do(ThreadClosure* tc) { - for (G1ConcurrentRefineThread* t : _threads) { - tc->do_thread(t); + if (is_refinement_enabled()) { + _workers->threads_do(tc); } } void G1ConcurrentRefineThreadControl::stop() { - for (G1ConcurrentRefineThread* t : _threads) { - t->stop(); + if (is_refinement_enabled()) { + _control_thread->stop(); } } +G1ConcurrentRefineSweepState::G1ConcurrentRefineSweepState(uint max_reserved_regions) : + _state(State::Idle), + _sweep_table(new G1CardTableClaimTable(G1CollectedHeap::get_chunks_per_region_for_merge())), + _stats() +{ + _sweep_table->initialize(max_reserved_regions); +} + +G1ConcurrentRefineSweepState::~G1ConcurrentRefineSweepState() { + delete _sweep_table; +} + +void G1ConcurrentRefineSweepState::set_state_start_time() { + _state_start[static_cast(_state)] = Ticks::now(); +} + +Tickspan G1ConcurrentRefineSweepState::get_duration(State start, State end) { + return _state_start[static_cast(end)] - _state_start[static_cast(start)]; +} + +void G1ConcurrentRefineSweepState::reset_stats() { + stats()->reset(); +} + +void G1ConcurrentRefineSweepState::add_yield_during_sweep_duration(jlong duration) { + stats()->inc_yield_during_sweep_duration(duration); +} + +bool G1ConcurrentRefineSweepState::advance_state(State next_state) { + bool result = is_in_progress(); + if (result) { + _state = next_state; + } else { + _state = State::Idle; + } + return result; +} + +void G1ConcurrentRefineSweepState::assert_state(State expected) { + assert(_state == expected, "must be %s but is %s", state_name(expected), state_name(_state)); +} + +void G1ConcurrentRefineSweepState::start_work() { + assert_state(State::Idle); + + set_state_start_time(); + + _stats.reset(); + + _state = State::SwapGlobalCT; +} + +bool G1ConcurrentRefineSweepState::swap_global_card_table() { + assert_state(State::SwapGlobalCT); + + GCTraceTime(Info, gc, refine) tm("Concurrent Refine Global Card Table Swap"); + set_state_start_time(); + + { + // We can't have any new threads being in the process of created while we + // swap the card table because we read the current card table state during + // initialization. + // A safepoint may occur during that time, so leave the STS temporarily. + SuspendibleThreadSetLeaver sts_leave; + + MutexLocker mu(Threads_lock); + // A GC that advanced the epoch might have happened, which already switched + // The global card table. Do nothing. + if (is_in_progress()) { + G1BarrierSet::g1_barrier_set()->swap_global_card_table(); + } + } + + return advance_state(State::SwapJavaThreadsCT); +} + +bool G1ConcurrentRefineSweepState::swap_java_threads_ct() { + assert_state(State::SwapJavaThreadsCT); + + GCTraceTime(Info, gc, refine) tm("Concurrent Refine Java Thread CT swap"); + + set_state_start_time(); + + { + // Need to leave the STS to avoid potential deadlock in the handshake. + SuspendibleThreadSetLeaver sts; + + class G1SwapThreadCardTableClosure : public HandshakeClosure { + public: + G1SwapThreadCardTableClosure() : HandshakeClosure("G1 Java Thread CT swap") { } + + virtual void do_thread(Thread* thread) { + G1BarrierSet* bs = G1BarrierSet::g1_barrier_set(); + bs->update_card_table_base(thread); + } + } cl; + Handshake::execute(&cl); + } + + return advance_state(State::SynchronizeGCThreads); + } + +bool G1ConcurrentRefineSweepState::swap_gc_threads_ct() { + assert_state(State::SynchronizeGCThreads); + + GCTraceTime(Info, gc, refine) tm("Concurrent Refine GC Thread CT swap"); + + set_state_start_time(); + + { + class RendezvousGCThreads: public VM_Operation { + public: + VMOp_Type type() const { return VMOp_G1RendezvousGCThreads; } + + virtual bool evaluate_at_safepoint() const { + // We only care about synchronizing the GC threads. + // Leave the Java threads running. + return false; + } + + virtual bool skip_thread_oop_barriers() const { + fatal("Concurrent VMOps should not call this"); + return true; + } + + void doit() { + // Light weight "handshake" of the GC threads for memory synchronization; + // both changes to the Java heap need to be synchronized as well as the + // previous global card table reference change, so that no GC thread + // accesses the wrong card table. + // For example in the rebuild remset process the marking threads write + // marks into the card table, and that card table reference must be the + // correct one. + SuspendibleThreadSet::synchronize(); + SuspendibleThreadSet::desynchronize(); + }; + } op; + + SuspendibleThreadSetLeaver sts_leave; + VMThread::execute(&op); + } + + return advance_state(State::SnapshotHeap); +} + +void G1ConcurrentRefineSweepState::snapshot_heap(bool concurrent) { + if (concurrent) { + GCTraceTime(Info, gc, refine) tm("Concurrent Refine Snapshot Heap"); + + assert_state(State::SnapshotHeap); + + set_state_start_time(); + + snapshot_heap_inner(); + + advance_state(State::SweepRT); + } else { + assert_state(State::Idle); + assert_at_safepoint(); + + snapshot_heap_inner(); + } +} + +void G1ConcurrentRefineSweepState::sweep_refinement_table_start() { + assert_state(State::SweepRT); + + set_state_start_time(); +} + +bool G1ConcurrentRefineSweepState::sweep_refinement_table_step() { + assert_state(State::SweepRT); + + GCTraceTime(Info, gc, refine) tm("Concurrent Refine Table Step"); + + G1ConcurrentRefine* cr = G1CollectedHeap::heap()->concurrent_refine(); + + G1ConcurrentRefineSweepTask task(_sweep_table, &_stats, cr->num_threads_wanted()); + cr->run_with_refinement_workers(&task); + + if (task.sweep_completed()) { + advance_state(State::CompleteRefineWork); + return true; + } else { + return false; + } +} + +bool G1ConcurrentRefineSweepState::complete_work(bool concurrent, bool print_log) { + if (concurrent) { + assert_state(State::CompleteRefineWork); + } else { + // May have been forced to complete at any other time. + assert(is_in_progress() && _state != State::CompleteRefineWork, "must be but is %s", state_name(_state)); + } + + set_state_start_time(); + + if (print_log) { + G1ConcurrentRefineStats* s = &_stats; + + log_debug(gc, refine)("Refinement took %.2fms (pre-sweep %.2fms card refine %.2f) " + "(scanned %zu clean %zu (%.2f%%) not_clean %zu (%.2f%%) not_parsable %zu " + "refers_to_cset %zu (%.2f%%) still_refers_to_cset %zu (%.2f%%) no_cross_region %zu pending %zu)", + get_duration(State::Idle, _state).seconds() * 1000.0, + get_duration(State::Idle, State::SweepRT).seconds() * 1000.0, + TimeHelper::counter_to_millis(s->refine_duration()), + s->cards_scanned(), + s->cards_clean(), + percent_of(s->cards_clean(), s->cards_scanned()), + s->cards_not_clean(), + percent_of(s->cards_not_clean(), s->cards_scanned()), + s->cards_not_parsable(), + s->cards_refer_to_cset(), + percent_of(s->cards_refer_to_cset(), s->cards_not_clean()), + s->cards_already_refer_to_cset(), + percent_of(s->cards_already_refer_to_cset(), s->cards_not_clean()), + s->cards_no_cross_region(), + s->cards_pending() + ); + } + + bool has_sweep_rt_work = _state == State::SweepRT; + + advance_state(State::Idle); + return has_sweep_rt_work; +} + +void G1ConcurrentRefineSweepState::snapshot_heap_inner() { + // G1CollectedHeap::heap_region_iterate() below will only visit currently committed + // regions. Initialize all entries in the state table here and later in this method + // selectively enable regions that we are interested. This way regions committed + // later will be automatically excluded from iteration. + // Their refinement table must be completely empty anyway. + _sweep_table->reset_all_to_claimed(); + + class SnapshotRegionsClosure : public G1HeapRegionClosure { + G1CardTableClaimTable* _sweep_table; + + public: + SnapshotRegionsClosure(G1CardTableClaimTable* sweep_table) : G1HeapRegionClosure(), _sweep_table(sweep_table) { } + + bool do_heap_region(G1HeapRegion* r) override { + if (!r->is_free()) { + // Need to scan all parts of non-free regions, so reset the claim. + // No need for synchronization: we are only interested in regions + // that were allocated before the handshake; the handshake makes such + // regions' metadata visible to all threads, and we do not care about + // humongous regions that were allocated afterwards. + _sweep_table->reset_to_unclaimed(r->hrm_index()); + } + return false; + } + } cl(_sweep_table); + G1CollectedHeap::heap()->heap_region_iterate(&cl); +} + +bool G1ConcurrentRefineSweepState::is_in_progress() const { + return _state != State::Idle; +} + +bool G1ConcurrentRefineSweepState::are_java_threads_synched() const { + return _state > State::SwapJavaThreadsCT || !is_in_progress(); +} + uint64_t G1ConcurrentRefine::adjust_threads_period_ms() const { // Instead of a fixed value, this could be a command line option. But then // we might also want to allow configuration of adjust_threads_wait_ms(). - return 50; + + // Use a prime number close to 50ms, different to other components that derive + // their wait time from the try_get_available_bytes_estimate() call to minimize + // interference. + return 53; } static size_t minimum_pending_cards_target() { - // One buffer per thread. - return ParallelGCThreads * G1UpdateBufferSize; + return ParallelGCThreads * G1PerThreadPendingCardThreshold; } -G1ConcurrentRefine::G1ConcurrentRefine(G1Policy* policy) : - _policy(policy), - _threads_wanted(0), +G1ConcurrentRefine::G1ConcurrentRefine(G1CollectedHeap* g1h) : + _policy(g1h->policy()), + _num_threads_wanted(0), _pending_cards_target(PendingCardsTargetUninitialized), _last_adjust(), _needs_adjust(false), - _threads_needed(policy, adjust_threads_period_ms()), + _heap_was_locked(false), + _threads_needed(g1h->policy(), adjust_threads_period_ms()), _thread_control(G1ConcRefinementThreads), - _dcqs(G1BarrierSet::dirty_card_queue_set()) -{} + _sweep_state(g1h->max_num_regions()) +{ } jint G1ConcurrentRefine::initialize() { return _thread_control.initialize(this); } -G1ConcurrentRefine* G1ConcurrentRefine::create(G1Policy* policy, jint* ecode) { - G1ConcurrentRefine* cr = new G1ConcurrentRefine(policy); +G1ConcurrentRefineSweepState& G1ConcurrentRefine::sweep_state_for_merge() { + bool has_sweep_claims = sweep_state().complete_work(false /* concurrent */); + if (has_sweep_claims) { + log_debug(gc, refine)("Continue existing work"); + } else { + // Refinement has been interrupted without having a snapshot. There may + // be a mix of already swapped and not-swapped card tables assigned to threads, + // so they might have already dirtied the swapped card tables. + // Conservatively scan all (non-free, non-committed) region's card tables, + // creating the snapshot right now. + log_debug(gc, refine)("Create work from scratch"); + + sweep_state().snapshot_heap(false /* concurrent */); + } + return sweep_state(); +} + +void G1ConcurrentRefine::run_with_refinement_workers(WorkerTask* task) { + _thread_control.run_task(task, num_threads_wanted()); +} + +void G1ConcurrentRefine::notify_region_reclaimed(G1HeapRegion* r) { + assert_at_safepoint(); + if (_sweep_state.is_in_progress()) { + _sweep_state.sweep_table()->claim_all_cards(r->hrm_index()); + } +} + +G1ConcurrentRefine* G1ConcurrentRefine::create(G1CollectedHeap* g1h, jint* ecode) { + G1ConcurrentRefine* cr = new G1ConcurrentRefine(g1h); *ecode = cr->initialize(); if (*ecode != 0) { delete cr; @@ -176,25 +465,31 @@ G1ConcurrentRefine::~G1ConcurrentRefine() { } void G1ConcurrentRefine::threads_do(ThreadClosure *tc) { + worker_threads_do(tc); + control_thread_do(tc); +} + +void G1ConcurrentRefine::worker_threads_do(ThreadClosure *tc) { _thread_control.worker_threads_do(tc); } -void G1ConcurrentRefine::update_pending_cards_target(double logged_cards_time_ms, - size_t processed_logged_cards, - size_t predicted_thread_buffer_cards, +void G1ConcurrentRefine::control_thread_do(ThreadClosure *tc) { + _thread_control.control_thread_do(tc); +} + +void G1ConcurrentRefine::update_pending_cards_target(double pending_cards_time_ms, + size_t processed_pending_cards, double goal_ms) { size_t minimum = minimum_pending_cards_target(); - if ((processed_logged_cards < minimum) || (logged_cards_time_ms == 0.0)) { - log_debug(gc, ergo, refine)("Unchanged pending cards target: %zu", - _pending_cards_target); + if ((processed_pending_cards < minimum) || (pending_cards_time_ms == 0.0)) { + log_debug(gc, ergo, refine)("Unchanged pending cards target: %zu (processed %zu minimum %zu time %1.2f)", + _pending_cards_target, processed_pending_cards, minimum, pending_cards_time_ms); return; } // Base the pending cards budget on the measured rate. - double rate = processed_logged_cards / logged_cards_time_ms; - size_t budget = static_cast(goal_ms * rate); - // Deduct predicted cards in thread buffers to get target. - size_t new_target = budget - MIN2(budget, predicted_thread_buffer_cards); + double rate = processed_pending_cards / pending_cards_time_ms; + size_t new_target = static_cast(goal_ms * rate); // Add some hysteresis with previous values. if (is_pending_cards_target_initialized()) { new_target = (new_target + _pending_cards_target) / 2; @@ -205,46 +500,36 @@ void G1ConcurrentRefine::update_pending_cards_target(double logged_cards_time_ms log_debug(gc, ergo, refine)("New pending cards target: %zu", new_target); } -void G1ConcurrentRefine::adjust_after_gc(double logged_cards_time_ms, - size_t processed_logged_cards, - size_t predicted_thread_buffer_cards, +void G1ConcurrentRefine::adjust_after_gc(double pending_cards_time_ms, + size_t processed_pending_cards, double goal_ms) { - if (!G1UseConcRefinement) return; + if (!G1UseConcRefinement) { + return; + } - update_pending_cards_target(logged_cards_time_ms, - processed_logged_cards, - predicted_thread_buffer_cards, + update_pending_cards_target(pending_cards_time_ms, + processed_pending_cards, goal_ms); - if (_thread_control.max_num_threads() == 0) { - // If no refinement threads then the mutator threshold is the target. - _dcqs.set_mutator_refinement_threshold(_pending_cards_target); - } else { - // Provisionally make the mutator threshold unlimited, to be updated by - // the next periodic adjustment. Because card state may have changed - // drastically, record that adjustment is needed and kick the primary - // thread, in case it is waiting. - _dcqs.set_mutator_refinement_threshold(SIZE_MAX); + if (_thread_control.is_refinement_enabled()) { _needs_adjust = true; if (is_pending_cards_target_initialized()) { - _thread_control.activate(0); + _thread_control.activate(); } } } -// Wake up the primary thread less frequently when the time available until -// the next GC is longer. But don't increase the wait time too rapidly. -// This reduces the number of primary thread wakeups that just immediately -// go back to waiting, while still being responsive to behavior changes. -static uint64_t compute_adjust_wait_time_ms(double available_ms) { - return static_cast(sqrt(available_ms) * 4.0); -} - uint64_t G1ConcurrentRefine::adjust_threads_wait_ms() const { - assert_current_thread_is_primary_refinement_thread(); + assert_current_thread_is_control_refinement_thread(); if (is_pending_cards_target_initialized()) { - double available_ms = _threads_needed.predicted_time_until_next_gc_ms(); - uint64_t wait_time_ms = compute_adjust_wait_time_ms(available_ms); - return MAX2(wait_time_ms, adjust_threads_period_ms()); + // Retry asap when the cause for not getting a prediction was that we temporarily + // did not get the heap lock. Otherwise we might wait for too long until we get + // back here. + if (_heap_was_locked) { + return 1; + } + double available_time_ms = _threads_needed.predicted_time_until_next_gc_ms(); + + return _policy->adjust_wait_time_ms(available_time_ms, adjust_threads_period_ms()); } else { // If target not yet initialized then wait forever (until explicitly // activated). This happens during startup, when we don't bother with @@ -253,185 +538,74 @@ uint64_t G1ConcurrentRefine::adjust_threads_wait_ms() const { } } -class G1ConcurrentRefine::RemSetSamplingClosure : public G1HeapRegionClosure { - size_t _sampled_code_root_rs_length; +bool G1ConcurrentRefine::adjust_num_threads_periodically() { + assert_current_thread_is_control_refinement_thread(); -public: - RemSetSamplingClosure() : - _sampled_code_root_rs_length(0) {} - - bool do_heap_region(G1HeapRegion* r) override { - G1HeapRegionRemSet* rem_set = r->rem_set(); - _sampled_code_root_rs_length += rem_set->code_roots_list_length(); - return false; - } - - size_t sampled_code_root_rs_length() const { return _sampled_code_root_rs_length; } -}; - -// Adjust the target length (in regions) of the young gen, based on the -// current length of the remembered sets. -// -// At the end of the GC G1 determines the length of the young gen based on -// how much time the next GC can take, and when the next GC may occur -// according to the MMU. -// -// The assumption is that a significant part of the GC is spent on scanning -// the remembered sets (and many other components), so this thread constantly -// reevaluates the prediction for the remembered set scanning costs, and potentially -// resizes the young gen. This may do a premature GC or even increase the young -// gen size to keep pause time length goal. -void G1ConcurrentRefine::adjust_young_list_target_length() { - if (_policy->use_adaptive_young_list_length()) { - G1CollectedHeap* g1h = G1CollectedHeap::heap(); - G1CollectionSet* cset = g1h->collection_set(); - RemSetSamplingClosure cl; - cset->iterate(&cl); - - size_t card_rs_length = g1h->young_regions_cardset()->occupied(); - - size_t sampled_code_root_rs_length = cl.sampled_code_root_rs_length(); - _policy->revise_young_list_target_length(card_rs_length, sampled_code_root_rs_length); - } -} - -bool G1ConcurrentRefine::adjust_threads_periodically() { - assert_current_thread_is_primary_refinement_thread(); - - // Check whether it's time to do a periodic adjustment. + _heap_was_locked = false; + // Check whether it's time to do a periodic adjustment if there is no explicit + // request pending. We might have spuriously woken up. if (!_needs_adjust) { Tickspan since_adjust = Ticks::now() - _last_adjust; - if (since_adjust.milliseconds() >= adjust_threads_period_ms()) { - _needs_adjust = true; + if (since_adjust.milliseconds() < adjust_threads_period_ms()) { + _num_threads_wanted = 0; + return false; } } - // If needed, try to adjust threads wanted. - if (_needs_adjust) { - // Getting used young bytes requires holding Heap_lock. But we can't use - // normal lock and block until available. Blocking on the lock could - // deadlock with a GC VMOp that is holding the lock and requesting a - // safepoint. Instead try to lock, and if fail then skip adjustment for - // this iteration of the thread, do some refinement work, and retry the - // adjustment later. - if (Heap_lock->try_lock()) { - size_t used_bytes = _policy->estimate_used_young_bytes_locked(); - Heap_lock->unlock(); - adjust_young_list_target_length(); - size_t young_bytes = _policy->young_list_target_length() * G1HeapRegion::GrainBytes; - size_t available_bytes = young_bytes - MIN2(young_bytes, used_bytes); - adjust_threads_wanted(available_bytes); - _needs_adjust = false; - _last_adjust = Ticks::now(); - return true; - } + // Reset pending request. + _needs_adjust = false; + size_t available_bytes = 0; + if (_policy->try_get_available_bytes_estimate(available_bytes)) { + adjust_threads_wanted(available_bytes); + _last_adjust = Ticks::now(); + } else { + _heap_was_locked = true; + // Defer adjustment to next time. + _needs_adjust = true; } - return false; -} - -bool G1ConcurrentRefine::is_in_last_adjustment_period() const { - return _threads_needed.predicted_time_until_next_gc_ms() <= adjust_threads_period_ms(); + return (_num_threads_wanted > 0) && !heap_was_locked(); } void G1ConcurrentRefine::adjust_threads_wanted(size_t available_bytes) { - assert_current_thread_is_primary_refinement_thread(); - size_t num_cards = _dcqs.num_cards(); - size_t mutator_threshold = SIZE_MAX; - uint old_wanted = AtomicAccess::load(&_threads_wanted); + assert_current_thread_is_control_refinement_thread(); - _threads_needed.update(old_wanted, + G1Policy* policy = G1CollectedHeap::heap()->policy(); + const G1Analytics* analytics = policy->analytics(); + + size_t num_cards = policy->current_pending_cards(); + + _threads_needed.update(_num_threads_wanted, available_bytes, num_cards, _pending_cards_target); uint new_wanted = _threads_needed.threads_needed(); if (new_wanted > _thread_control.max_num_threads()) { - // If running all the threads can't reach goal, turn on refinement by - // mutator threads. Using target as the threshold may be stronger - // than required, but will do the most to get us under goal, and we'll - // reevaluate with the next adjustment. - mutator_threshold = _pending_cards_target; + // Bound the wanted threads by maximum available. new_wanted = _thread_control.max_num_threads(); - } else if (is_in_last_adjustment_period()) { - // If very little time remains until GC, enable mutator refinement. If - // the target has been reached, this keeps the number of pending cards on - // target even if refinement threads deactivate in the meantime. And if - // the target hasn't been reached, this prevents things from getting - // worse. - mutator_threshold = _pending_cards_target; } - AtomicAccess::store(&_threads_wanted, new_wanted); - _dcqs.set_mutator_refinement_threshold(mutator_threshold); - log_debug(gc, refine)("Concurrent refinement: wanted %u, cards: %zu, " - "predicted: %zu, time: %1.2fms", + + _num_threads_wanted = new_wanted; + + log_debug(gc, refine)("Concurrent refinement: wanted %u, pending cards: %zu (pending-from-gc %zu), " + "predicted: %zu, goal %zu, time-until-next-gc: %1.2fms pred-refine-rate %1.2fc/ms log-rate %1.2fc/ms", new_wanted, num_cards, + G1CollectedHeap::heap()->policy()->pending_cards_from_gc(), _threads_needed.predicted_cards_at_next_gc(), - _threads_needed.predicted_time_until_next_gc_ms()); - // Activate newly wanted threads. The current thread is the primary - // refinement thread, so is already active. - for (uint i = MAX2(old_wanted, 1u); i < new_wanted; ++i) { - if (!_thread_control.activate(i)) { - // Failed to allocate and activate thread. Stop trying to activate, and - // instead use mutator threads to make up the gap. - AtomicAccess::store(&_threads_wanted, i); - _dcqs.set_mutator_refinement_threshold(_pending_cards_target); - break; - } - } -} - -void G1ConcurrentRefine::reduce_threads_wanted() { - assert_current_thread_is_primary_refinement_thread(); - if (!_needs_adjust) { // Defer if adjustment request is active. - uint wanted = AtomicAccess::load(&_threads_wanted); - if (wanted > 0) { - AtomicAccess::store(&_threads_wanted, --wanted); - } - // If very little time remains until GC, enable mutator refinement. If - // the target has been reached, this keeps the number of pending cards on - // target even as refinement threads deactivate in the meantime. - if (is_in_last_adjustment_period()) { - _dcqs.set_mutator_refinement_threshold(_pending_cards_target); - } - } -} - -bool G1ConcurrentRefine::is_thread_wanted(uint worker_id) const { - return worker_id < AtomicAccess::load(&_threads_wanted); + _pending_cards_target, + _threads_needed.predicted_time_until_next_gc_ms(), + analytics->predict_concurrent_refine_rate_ms(), + analytics->predict_dirtied_cards_rate_ms() + ); } bool G1ConcurrentRefine::is_thread_adjustment_needed() const { - assert_current_thread_is_primary_refinement_thread(); + assert_current_thread_is_control_refinement_thread(); return _needs_adjust; } void G1ConcurrentRefine::record_thread_adjustment_needed() { - assert_current_thread_is_primary_refinement_thread(); + assert_current_thread_is_control_refinement_thread(); _needs_adjust = true; } - -G1ConcurrentRefineStats G1ConcurrentRefine::get_and_reset_refinement_stats() { - struct CollectStats : public ThreadClosure { - G1ConcurrentRefineStats _total_stats; - virtual void do_thread(Thread* t) { - G1ConcurrentRefineThread* crt = static_cast(t); - G1ConcurrentRefineStats& stats = *crt->refinement_stats(); - _total_stats += stats; - stats.reset(); - } - } collector; - threads_do(&collector); - return collector._total_stats; -} - -uint G1ConcurrentRefine::worker_id_offset() { - return G1DirtyCardQueueSet::num_par_ids(); -} - -bool G1ConcurrentRefine::try_refinement_step(uint worker_id, - size_t stop_at, - G1ConcurrentRefineStats* stats) { - uint adjusted_id = worker_id + worker_id_offset(); - return _dcqs.refine_completed_buffer_concurrently(adjusted_id, stop_at, stats); -} diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefine.hpp b/src/hotspot/share/gc/g1/g1ConcurrentRefine.hpp index dd0b62a22ea..5e96ed738fd 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefine.hpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefine.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,23 +34,28 @@ #include "utilities/macros.hpp" // Forward decl +class G1CardTableClaimTable; +class G1CollectedHeap; class G1ConcurrentRefine; class G1ConcurrentRefineThread; -class G1DirtyCardQueueSet; +class G1HeapRegion; class G1Policy; class ThreadClosure; +class WorkerTask; +class WorkerThreads; // Helper class for refinement thread management. Used to start, stop and // iterate over them. class G1ConcurrentRefineThreadControl { G1ConcurrentRefine* _cr; - GrowableArrayCHeap _threads; + G1ConcurrentRefineThread* _control_thread; + + WorkerThreads* _workers; + uint _max_num_threads; // Create the refinement thread for the given worker id. // If initializing is true, ignore InjectGCWorkerCreationFailure. - G1ConcurrentRefineThread* create_refinement_thread(uint worker_id, bool initializing); - - bool ensure_threads_created(uint worker_id, bool initializing); + G1ConcurrentRefineThread* create_refinement_thread(); NONCOPYABLE(G1ConcurrentRefineThreadControl); @@ -60,21 +65,119 @@ public: jint initialize(G1ConcurrentRefine* cr); - void assert_current_thread_is_primary_refinement_thread() const NOT_DEBUG_RETURN; + void assert_current_thread_is_control_refinement_thread() const NOT_DEBUG_RETURN; - uint max_num_threads() const { return _threads.capacity(); } + uint max_num_threads() const { return _max_num_threads; } + bool is_refinement_enabled() const { return _max_num_threads > 0; } - // Activate the indicated thread. If the thread has not yet been allocated, - // allocate and then activate. If allocation is needed and fails, return - // false. Otherwise return true. - // precondition: worker_id < max_num_threads(). - // precondition: current thread is not the designated worker. - bool activate(uint worker_id); + // Activate the control thread. + void activate(); + void run_task(WorkerTask* task, uint num_workers); + + void control_thread_do(ThreadClosure* tc); void worker_threads_do(ThreadClosure* tc); void stop(); }; +// Tracks the current state of re-examining the dirty cards from idle to completion +// (and reset back to idle). +// +// The process steps are as follows: +// +// 1) Swap global card table pointers +// +// 2) Swap Java Thread's card table pointers +// +// 3) Synchronize GC Threads +// Ensures memory visibility +// +// After this point mutator threads should not mark the refinement table. +// +// 4) Snapshot the heap +// Determines which regions need to be swept. +// +// 5) Sweep Refinement table +// Examines non-Clean cards on the refinement table. +// +// 6) Completion Work +// Calculates statistics about the process to be used in various parts of +// the garbage collection. +// +// All but step 4 are interruptible by safepoints. In case of a garbage collection, +// the garbage collection will interrupt this process, and go to Idle state. +// +class G1ConcurrentRefineSweepState { + + enum class State : uint { + Idle, // Refinement is doing nothing. + SwapGlobalCT, // Swap global card table. + SwapJavaThreadsCT, // Swap java thread's card tables. + SynchronizeGCThreads, // Synchronize GC thread's memory view. + SnapshotHeap, // Take a snapshot of the region's top() values. + SweepRT, // Sweep the refinement table for pending (dirty) cards. + CompleteRefineWork, // Cleanup of refinement work, reset to idle. + Last + } _state; + + static const char* state_name(State state) { + static const char* _state_names[] = { + "Idle", + "Swap Global Card Table", + "Swap JavaThread Card Table", + "Synchronize GC Threads", + "Snapshot Heap", + "Sweep Refinement Table", + "Complete Sweep Work" + }; + + return _state_names[static_cast(state)]; + } + + // Current heap snapshot. + G1CardTableClaimTable* _sweep_table; + + // Start times for all states. + Ticks _state_start[static_cast(State::Last)]; + + void set_state_start_time(); + Tickspan get_duration(State start, State end); + + G1ConcurrentRefineStats _stats; + + // Advances the state to next_state if not interrupted by a changed epoch. Returns + // to Idle otherwise. + bool advance_state(State next_state); + + void assert_state(State expected); + + void snapshot_heap_inner(); + +public: + G1ConcurrentRefineSweepState(uint max_reserved_regions); + ~G1ConcurrentRefineSweepState(); + + void start_work(); + + bool swap_global_card_table(); + bool swap_java_threads_ct(); + bool swap_gc_threads_ct(); + void snapshot_heap(bool concurrent = true); + void sweep_refinement_table_start(); + bool sweep_refinement_table_step(); + + bool complete_work(bool concurrent, bool print_log = true); + + G1CardTableClaimTable* sweep_table() { return _sweep_table; } + G1ConcurrentRefineStats* stats() { return &_stats; } + void reset_stats(); + + void add_yield_during_sweep_duration(jlong duration); + + bool is_in_progress() const; + bool are_java_threads_synched() const; +}; + // Controls concurrent refinement. // // Mutator threads produce dirty cards, which need to be examined for updates @@ -84,49 +187,43 @@ public: // pending dirty cards at the start of a GC can be processed within that time // budget. // -// Concurrent refinement is performed by a combination of dedicated threads -// and by mutator threads as they produce dirty cards. If configured to not -// have any dedicated threads (-XX:G1ConcRefinementThreads=0) then all -// concurrent refinement work is performed by mutator threads. When there are -// dedicated threads, they generally do most of the concurrent refinement -// work, to minimize throughput impact of refinement work on mutator threads. +// Concurrent refinement is performed by a set of dedicated threads. If configured +// to not have any dedicated threads (-XX:G1ConcRefinementThreads=0) then no +// refinement work is performed at all. // // This class determines the target number of dirty cards pending for the next // GC. It also owns the dedicated refinement threads and controls their // activation in order to achieve that target. // -// There are two kinds of dedicated refinement threads, a single primary -// thread and some number of secondary threads. When active, all refinement -// threads take buffers of dirty cards from the dirty card queue and process -// them. Between buffers they query this owning object to find out whether -// they should continue running, deactivating themselves if not. +// There are two kinds of dedicated refinement threads, a single control +// thread and some number of refinement worker threads. +// The control thread determines whether there is need to do work, and then starts +// an appropriate number of refinement worker threads to get back to the target +// number of pending dirty cards. +// +// The control wakes up periodically whether there is need to do refinement +// work, starting the refinement process as necessary. // -// The primary thread drives the control system that determines how many -// refinement threads should be active. If inactive, it wakes up periodically -// to recalculate the number of active threads needed, and activates -// additional threads as necessary. While active it also periodically -// recalculates the number wanted and activates more threads if needed. It -// also reduces the number of wanted threads when the target has been reached, -// triggering deactivations. class G1ConcurrentRefine : public CHeapObj { G1Policy* _policy; - volatile uint _threads_wanted; + volatile uint _num_threads_wanted; size_t _pending_cards_target; Ticks _last_adjust; Ticks _last_deactivate; bool _needs_adjust; + bool _heap_was_locked; // The heap has been locked the last time we tried to adjust the number of refinement threads. + G1ConcurrentRefineThreadsNeeded _threads_needed; G1ConcurrentRefineThreadControl _thread_control; - G1DirtyCardQueueSet& _dcqs; - G1ConcurrentRefine(G1Policy* policy); + G1ConcurrentRefineSweepState _sweep_state; - static uint worker_id_offset(); + G1ConcurrentRefine(G1CollectedHeap* g1h); jint initialize(); - void assert_current_thread_is_primary_refinement_thread() const { - _thread_control.assert_current_thread_is_primary_refinement_thread(); + void assert_current_thread_is_control_refinement_thread() const { + _thread_control.assert_current_thread_is_control_refinement_thread(); } // For the first few collection cycles we don't have a target (and so don't @@ -138,16 +235,11 @@ class G1ConcurrentRefine : public CHeapObj { return _pending_cards_target != PendingCardsTargetUninitialized; } - void update_pending_cards_target(double logged_cards_scan_time_ms, - size_t processed_logged_cards, - size_t predicted_thread_buffer_cards, + void update_pending_cards_target(double pending_cards_scan_time_ms, + size_t processed_pending_cards, double goal_ms); uint64_t adjust_threads_period_ms() const; - bool is_in_last_adjustment_period() const; - - class RemSetSamplingClosure; // Helper class for adjusting young length. - void adjust_young_list_target_length(); void adjust_threads_wanted(size_t available_bytes); @@ -156,67 +248,66 @@ class G1ConcurrentRefine : public CHeapObj { public: ~G1ConcurrentRefine(); + G1ConcurrentRefineSweepState& sweep_state() { return _sweep_state; } + + G1ConcurrentRefineSweepState& sweep_state_for_merge(); + + void run_with_refinement_workers(WorkerTask* task); + + void notify_region_reclaimed(G1HeapRegion* r); + // Returns a G1ConcurrentRefine instance if succeeded to create/initialize the // G1ConcurrentRefine instance. Otherwise, returns null with error code. - static G1ConcurrentRefine* create(G1Policy* policy, jint* ecode); + static G1ConcurrentRefine* create(G1CollectedHeap* g1h, jint* ecode); // Stop all the refinement threads. void stop(); // Called at the end of a GC to prepare for refinement during the next // concurrent phase. Updates the target for the number of pending dirty - // cards. Updates the mutator refinement threshold. Ensures the primary - // refinement thread (if it exists) is active, so it will adjust the number + // cards. Updates the mutator refinement threshold. Ensures the refinement + // control thread (if it exists) is active, so it will adjust the number // of running threads. - void adjust_after_gc(double logged_cards_scan_time_ms, - size_t processed_logged_cards, - size_t predicted_thread_buffer_cards, + void adjust_after_gc(double pending_cards_scan_time_ms, + size_t processed_pending_cards, double goal_ms); // Target number of pending dirty cards at the start of the next GC. size_t pending_cards_target() const { return _pending_cards_target; } - // May recalculate the number of refinement threads that should be active in - // order to meet the pending cards target. Returns true if adjustment was - // performed, and clears any pending request. Returns false if the - // adjustment period has not expired, or because a timed or requested - // adjustment could not be performed immediately and so was deferred. - // precondition: current thread is the primary refinement thread. - bool adjust_threads_periodically(); + // Recalculates the number of refinement threads that should be active in + // order to meet the pending cards target. + // Returns true if it could recalculate the number of threads and + // refinement threads should be started. + // Returns false if the adjustment period has not expired, or because a timed + // or requested adjustment could not be performed immediately and so was deferred. + bool adjust_num_threads_periodically(); - // The amount of time (in ms) the primary refinement thread should sleep + // The amount of time (in ms) the refinement control thread should sleep // when it is inactive. It requests adjustment whenever it is reactivated. - // precondition: current thread is the primary refinement thread. + // precondition: current thread is the refinement control thread. uint64_t adjust_threads_wait_ms() const; // Record a request for thread adjustment as soon as possible. - // precondition: current thread is the primary refinement thread. + // precondition: current thread is the refinement control thread. void record_thread_adjustment_needed(); // Test whether there is a pending request for thread adjustment. - // precondition: current thread is the primary refinement thread. + // precondition: current thread is the refinement control thread. bool is_thread_adjustment_needed() const; - // Reduce the number of active threads wanted. - // precondition: current thread is the primary refinement thread. - void reduce_threads_wanted(); + // Indicate that last refinement adjustment had been deferred due to not + // obtaining the heap lock. + bool heap_was_locked() const { return _heap_was_locked; } - // Test whether the thread designated by worker_id should be active. - bool is_thread_wanted(uint worker_id) const; - - // Return total of concurrent refinement stats for the - // ConcurrentRefineThreads. Also reset the stats for the threads. - G1ConcurrentRefineStats get_and_reset_refinement_stats(); - - // Perform a single refinement step; called by the refinement - // threads. Returns true if there was refinement work available. - // Updates stats. - bool try_refinement_step(uint worker_id, - size_t stop_at, - G1ConcurrentRefineStats* stats); + uint num_threads_wanted() const { return _num_threads_wanted; } + uint max_num_threads() const { return _thread_control.max_num_threads(); } // Iterate over all concurrent refinement threads applying the given closure. void threads_do(ThreadClosure *tc); + // Iterate over specific refinement threads applying the given closure. + void worker_threads_do(ThreadClosure *tc); + void control_thread_do(ThreadClosure *tc); }; #endif // SHARE_GC_G1_G1CONCURRENTREFINE_HPP diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.cpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.cpp index 7f0bcc5b50f..83a09c55a3f 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.cpp @@ -23,41 +23,33 @@ */ #include "gc/g1/g1ConcurrentRefineStats.hpp" +#include "runtime/atomicAccess.hpp" +#include "runtime/timer.hpp" G1ConcurrentRefineStats::G1ConcurrentRefineStats() : - _refinement_time(), - _refined_cards(0), - _precleaned_cards(0), - _dirtied_cards(0) + _sweep_duration(0), + _yield_during_sweep_duration(0), + _cards_scanned(0), + _cards_clean(0), + _cards_not_parsable(0), + _cards_already_refer_to_cset(0), + _cards_refer_to_cset(0), + _cards_no_cross_region(0), + _refine_duration(0) {} -double G1ConcurrentRefineStats::refinement_rate_ms() const { - // Report 0 when no time recorded because no refinement performed. - double secs = refinement_time().seconds(); - return (secs > 0) ? (refined_cards() / (secs * MILLIUNITS)) : 0.0; -} +void G1ConcurrentRefineStats::add_atomic(G1ConcurrentRefineStats* other) { + AtomicAccess::add(&_sweep_duration, other->_sweep_duration, memory_order_relaxed); + AtomicAccess::add(&_yield_during_sweep_duration, other->_yield_during_sweep_duration, memory_order_relaxed); -G1ConcurrentRefineStats& -G1ConcurrentRefineStats::operator+=(const G1ConcurrentRefineStats& other) { - _refinement_time += other._refinement_time; - _refined_cards += other._refined_cards; - _precleaned_cards += other._precleaned_cards; - _dirtied_cards += other._dirtied_cards; - return *this; -} + AtomicAccess::add(&_cards_scanned, other->_cards_scanned, memory_order_relaxed); + AtomicAccess::add(&_cards_clean, other->_cards_clean, memory_order_relaxed); + AtomicAccess::add(&_cards_not_parsable, other->_cards_not_parsable, memory_order_relaxed); + AtomicAccess::add(&_cards_already_refer_to_cset, other->_cards_already_refer_to_cset, memory_order_relaxed); + AtomicAccess::add(&_cards_refer_to_cset, other->_cards_refer_to_cset, memory_order_relaxed); + AtomicAccess::add(&_cards_no_cross_region, other->_cards_no_cross_region, memory_order_relaxed); -template -static T clipped_sub(T x, T y) { - return (x < y) ? T() : (x - y); -} - -G1ConcurrentRefineStats& -G1ConcurrentRefineStats::operator-=(const G1ConcurrentRefineStats& other) { - _refinement_time = clipped_sub(_refinement_time, other._refinement_time); - _refined_cards = clipped_sub(_refined_cards, other._refined_cards); - _precleaned_cards = clipped_sub(_precleaned_cards, other._precleaned_cards); - _dirtied_cards = clipped_sub(_dirtied_cards, other._dirtied_cards); - return *this; + AtomicAccess::add(&_refine_duration, other->_refine_duration, memory_order_relaxed); } void G1ConcurrentRefineStats::reset() { diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.hpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.hpp index ae576778a07..ce22f4317df 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.hpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineStats.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,47 +33,56 @@ // Used for collecting per-thread statistics and for summaries over a // collection of threads. class G1ConcurrentRefineStats : public CHeapObj { - Tickspan _refinement_time; - size_t _refined_cards; - size_t _precleaned_cards; - size_t _dirtied_cards; + jlong _sweep_duration; // Time spent sweeping the table finding non-clean cards + // and refining them. + jlong _yield_during_sweep_duration; // Time spent yielding during the sweep (not doing the sweep). + + size_t _cards_scanned; // Total number of cards scanned. + size_t _cards_clean; // Number of cards found clean. + size_t _cards_not_parsable; // Number of cards we could not parse and left unrefined. + size_t _cards_already_refer_to_cset;// Number of cards marked found to be already young. + size_t _cards_refer_to_cset; // Number of dirty cards that were recently found to contain a to-cset reference. + size_t _cards_no_cross_region; // Number of dirty cards that were dirtied, but then cleaned again by the mutator. + + jlong _refine_duration; // Time spent during actual refinement. public: G1ConcurrentRefineStats(); - // Time spent performing concurrent refinement. - Tickspan refinement_time() const { return _refinement_time; } + // Time spent performing sweeping the refinement table (includes actual refinement, + // but not yield time). + jlong sweep_duration() const { return _sweep_duration - _yield_during_sweep_duration; } + jlong yield_during_sweep_duration() const { return _yield_during_sweep_duration; } + jlong refine_duration() const { return _refine_duration; } // Number of refined cards. - size_t refined_cards() const { return _refined_cards; } + size_t refined_cards() const { return cards_not_clean(); } - // Refinement rate, in cards per ms. - double refinement_rate_ms() const; + size_t cards_scanned() const { return _cards_scanned; } + size_t cards_clean() const { return _cards_clean; } + size_t cards_not_clean() const { return _cards_scanned - _cards_clean; } + size_t cards_not_parsable() const { return _cards_not_parsable; } + size_t cards_already_refer_to_cset() const { return _cards_already_refer_to_cset; } + size_t cards_refer_to_cset() const { return _cards_refer_to_cset; } + size_t cards_no_cross_region() const { return _cards_no_cross_region; } + // Number of cards that were marked dirty and in need of refinement. This includes cards recently + // found to refer to the collection set as they originally were dirty. + size_t cards_pending() const { return cards_not_clean() - _cards_already_refer_to_cset; } - // Number of cards for which refinement was skipped because some other - // thread had already refined them. - size_t precleaned_cards() const { return _precleaned_cards; } + size_t cards_to_cset() const { return _cards_already_refer_to_cset + _cards_refer_to_cset; } - // Number of cards marked dirty and in need of refinement. - size_t dirtied_cards() const { return _dirtied_cards; } + void inc_sweep_time(jlong t) { _sweep_duration += t; } + void inc_yield_during_sweep_duration(jlong t) { _yield_during_sweep_duration += t; } + void inc_refine_duration(jlong t) { _refine_duration += t; } - void inc_refinement_time(Tickspan t) { _refinement_time += t; } - void inc_refined_cards(size_t cards) { _refined_cards += cards; } - void inc_precleaned_cards(size_t cards) { _precleaned_cards += cards; } - void inc_dirtied_cards(size_t cards) { _dirtied_cards += cards; } + void inc_cards_scanned(size_t increment) { _cards_scanned += increment; } + void inc_cards_clean(size_t increment) { _cards_clean += increment; } + void inc_cards_not_parsable() { _cards_not_parsable++; } + void inc_cards_already_refer_to_cset() { _cards_already_refer_to_cset++; } + void inc_cards_refer_to_cset() { _cards_refer_to_cset++; } + void inc_cards_no_cross_region() { _cards_no_cross_region++; } - G1ConcurrentRefineStats& operator+=(const G1ConcurrentRefineStats& other); - G1ConcurrentRefineStats& operator-=(const G1ConcurrentRefineStats& other); - - friend G1ConcurrentRefineStats operator+(G1ConcurrentRefineStats x, - const G1ConcurrentRefineStats& y) { - return x += y; - } - - friend G1ConcurrentRefineStats operator-(G1ConcurrentRefineStats x, - const G1ConcurrentRefineStats& y) { - return x -= y; - } + void add_atomic(G1ConcurrentRefineStats* other); void reset(); }; diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefineSweepTask.cpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineSweepTask.cpp new file mode 100644 index 00000000000..ca5bc9ebe5f --- /dev/null +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineSweepTask.cpp @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "gc/g1/g1CardTableClaimTable.inline.hpp" +#include "gc/g1/g1CollectedHeap.inline.hpp" +#include "gc/g1/g1ConcurrentRefineSweepTask.hpp" + +class G1RefineRegionClosure : public G1HeapRegionClosure { + using CardValue = G1CardTable::CardValue; + + G1RemSet* _rem_set; + G1CardTableClaimTable* _scan_state; + + uint _worker_id; + + size_t _num_collections_at_start; + + bool has_work(G1HeapRegion* r) { + return _scan_state->has_unclaimed_cards(r->hrm_index()); + } + + void verify_card_pair_refers_to_same_card(CardValue* source_card, CardValue* dest_card) { +#ifdef ASSERT + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + G1HeapRegion* refinement_r = g1h->heap_region_containing(g1h->refinement_table()->addr_for(source_card)); + G1HeapRegion* card_r = g1h->heap_region_containing(g1h->card_table()->addr_for(dest_card)); + size_t refinement_i = g1h->refinement_table()->index_for_cardvalue(source_card); + size_t card_i = g1h->card_table()->index_for_cardvalue(dest_card); + + assert(refinement_r == card_r, "not same region source %u (%zu) dest %u (%zu) ", refinement_r->hrm_index(), refinement_i, card_r->hrm_index(), card_i); + assert(refinement_i == card_i, "indexes are not same %zu %zu", refinement_i, card_i); +#endif + } + + void do_dirty_card(CardValue* source_card, CardValue* dest_card) { + verify_card_pair_refers_to_same_card(source_card, dest_card); + + G1RemSet::RefineResult res = _rem_set->refine_card_concurrently(source_card, _worker_id); + // Gather statistics based on the result. + switch (res) { + case G1RemSet::HasRefToCSet: { + *dest_card = G1CardTable::g1_to_cset_card; + _refine_stats.inc_cards_refer_to_cset(); + break; + } + case G1RemSet::AlreadyToCSet: { + *dest_card = G1CardTable::g1_to_cset_card; + _refine_stats.inc_cards_already_refer_to_cset(); + break; + } + case G1RemSet::NoCrossRegion: { + _refine_stats.inc_cards_no_cross_region(); + break; + } + case G1RemSet::CouldNotParse: { + // Could not refine - redirty with the original value. + *dest_card = *source_card; + _refine_stats.inc_cards_not_parsable(); + break; + } + case G1RemSet::HasRefToOld : break; // Nothing special to do. + } + // Clean card on source card table. + *source_card = G1CardTable::clean_card_val(); + } + + void do_claimed_block(CardValue* dirty_l, CardValue* dirty_r, CardValue* dest_card) { + for (CardValue* source = dirty_l; source < dirty_r; ++source, ++dest_card) { + do_dirty_card(source, dest_card); + } + } + +public: + bool _completed; + G1ConcurrentRefineStats _refine_stats; + + G1RefineRegionClosure(uint worker_id, G1CardTableClaimTable* scan_state) : + G1HeapRegionClosure(), + _rem_set(G1CollectedHeap::heap()->rem_set()), + _scan_state(scan_state), + _worker_id(worker_id), + _completed(true), + _refine_stats() { } + + bool do_heap_region(G1HeapRegion* r) override { + + if (!has_work(r)) { + return false; + } + + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + + if (r->is_young()) { + if (_scan_state->claim_all_cards(r->hrm_index()) == 0) { + // Clear the pre-dirtying information. + r->clear_refinement_table(); + } + return false; + } + + G1CardTable* card_table = g1h->card_table(); + G1CardTable* refinement_table = g1h->refinement_table(); + + G1CardTableChunkClaimer claim(_scan_state, r->hrm_index()); + + size_t const region_card_base_idx = (size_t)r->hrm_index() << G1HeapRegion::LogCardsPerRegion; + + while (claim.has_next()) { + size_t const start_idx = region_card_base_idx + claim.value(); + CardValue* const start_card = refinement_table->byte_for_index(start_idx); + CardValue* const end_card = start_card + claim.size(); + + CardValue* dest_card = card_table->byte_for_index(start_idx); + + G1ChunkScanner scanner{start_card, end_card}; + + size_t num_dirty_cards = 0; + scanner.on_dirty_cards([&] (CardValue* dirty_l, CardValue* dirty_r) { + jlong refine_start = os::elapsed_counter(); + + do_claimed_block(dirty_l, dirty_r, dest_card + pointer_delta(dirty_l, start_card, sizeof(CardValue))); + num_dirty_cards += pointer_delta(dirty_r, dirty_l, sizeof(CardValue)); + + _refine_stats.inc_refine_duration(os::elapsed_counter() - refine_start); + }); + + if (VerifyDuringGC) { + for (CardValue* i = start_card; i < end_card; ++i) { + guarantee(*i == G1CardTable::clean_card_val(), "must be"); + } + } + + _refine_stats.inc_cards_scanned(claim.size()); + _refine_stats.inc_cards_clean(claim.size() - num_dirty_cards); + + if (SuspendibleThreadSet::should_yield()) { + _completed = false; + break; + } + } + + return !_completed; + } +}; + +G1ConcurrentRefineSweepTask::G1ConcurrentRefineSweepTask(G1CardTableClaimTable* scan_state, + G1ConcurrentRefineStats* stats, + uint max_workers) : + WorkerTask("G1 Refine Task"), + _scan_state(scan_state), + _stats(stats), + _max_workers(max_workers), + _sweep_completed(true) +{ } + +void G1ConcurrentRefineSweepTask::work(uint worker_id) { + jlong start = os::elapsed_counter(); + + G1RefineRegionClosure sweep_cl(worker_id, _scan_state); + _scan_state->heap_region_iterate_from_worker_offset(&sweep_cl, worker_id, _max_workers); + + if (!sweep_cl._completed) { + _sweep_completed = false; + } + + sweep_cl._refine_stats.inc_sweep_time(os::elapsed_counter() - start); + _stats->add_atomic(&sweep_cl._refine_stats); +} + +bool G1ConcurrentRefineSweepTask::sweep_completed() const { return _sweep_completed; } \ No newline at end of file diff --git a/src/hotspot/share/gc/shared/bufferNodeList.hpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineSweepTask.hpp similarity index 57% rename from src/hotspot/share/gc/shared/bufferNodeList.hpp rename to src/hotspot/share/gc/g1/g1ConcurrentRefineSweepTask.hpp index 55905ec071a..bf24c5ae850 100644 --- a/src/hotspot/share/gc/shared/bufferNodeList.hpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineSweepTask.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,20 +22,27 @@ * */ -#ifndef SHARE_GC_SHARED_BUFFERNODELIST_HPP -#define SHARE_GC_SHARED_BUFFERNODELIST_HPP +#ifndef SHARE_GC_G1_G1CONCURRENTREFINESWEEPTASK_HPP +#define SHARE_GC_G1_G1CONCURRENTREFINESWEEPTASK_HPP -#include "utilities/globalDefinitions.hpp" +#include "gc/g1/g1ConcurrentRefineStats.hpp" +#include "gc/shared/workerThread.hpp" -class BufferNode; +class G1CardTableClaimTable; -struct BufferNodeList { - BufferNode* _head; // First node in list or null if empty. - BufferNode* _tail; // Last node in list or null if empty. - size_t _entry_count; // Sum of entries in nodes in list. +class G1ConcurrentRefineSweepTask : public WorkerTask { + G1CardTableClaimTable* _scan_state; + G1ConcurrentRefineStats* _stats; + uint _max_workers; + bool _sweep_completed; - BufferNodeList(); - BufferNodeList(BufferNode* head, BufferNode* tail, size_t entry_count); +public: + + G1ConcurrentRefineSweepTask(G1CardTableClaimTable* scan_state, G1ConcurrentRefineStats* stats, uint max_workers); + + void work(uint worker_id) override; + + bool sweep_completed() const; }; -#endif // SHARE_GC_SHARED_BUFFERNODELIST_HPP +#endif /* SHARE_GC_G1_G1CONCURRENTREFINESWEEPTASK_HPP */ diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.cpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.cpp index 2fa19d46093..eccfe466d48 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.cpp @@ -23,10 +23,13 @@ */ #include "gc/g1/g1BarrierSet.hpp" +#include "gc/g1/g1CardTableClaimTable.inline.hpp" +#include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1ConcurrentRefine.hpp" #include "gc/g1/g1ConcurrentRefineStats.hpp" +#include "gc/g1/g1ConcurrentRefineSweepTask.hpp" #include "gc/g1/g1ConcurrentRefineThread.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" +#include "gc/shared/gcTraceTime.inline.hpp" #include "gc/shared/suspendibleThreadSet.hpp" #include "logging/log.hpp" #include "runtime/cpuTimeCounters.hpp" @@ -38,60 +41,61 @@ #include "utilities/globalDefinitions.hpp" #include "utilities/ticks.hpp" -G1ConcurrentRefineThread::G1ConcurrentRefineThread(G1ConcurrentRefine* cr, uint worker_id) : +G1ConcurrentRefineThread::G1ConcurrentRefineThread(G1ConcurrentRefine* cr) : ConcurrentGCThread(), - _notifier(Mutex::nosafepoint, FormatBuffer<>("G1 Refine#%d", worker_id), true), + _notifier(Mutex::nosafepoint, "G1 Refine Control", true), _requested_active(false), - _refinement_stats(), - _worker_id(worker_id), _cr(cr) { - // set name - set_name("G1 Refine#%d", worker_id); + set_name("G1 Refine Control"); } void G1ConcurrentRefineThread::run_service() { - while (wait_for_completed_buffers()) { + while (wait_for_work()) { SuspendibleThreadSetJoiner sts_join; - G1ConcurrentRefineStats active_stats_start = _refinement_stats; report_active("Activated"); while (!should_terminate()) { if (sts_join.should_yield()) { - report_inactive("Paused", _refinement_stats - active_stats_start); + report_inactive("Paused"); sts_join.yield(); // Reset after yield rather than accumulating across yields, else a // very long running thread could overflow. - active_stats_start = _refinement_stats; report_active("Resumed"); - } else if (maybe_deactivate()) { - break; + } + // Look if we want to do refinement. If we don't then don't do any refinement + // this. This thread may have just woken up but no threads are currently + // needed, which is common. In this case we want to just go back to + // waiting, with a minimum of fuss; in particular, don't do any "premature" + // refinement. However, adjustment may be pending but temporarily + // blocked. In that case we wait for adjustment to succeed. + Ticks adjust_start = Ticks::now(); + if (cr()->adjust_num_threads_periodically()) { + GCTraceTime(Info, gc, refine) tm("Concurrent Refine Cycle"); + do_refinement(); } else { - do_refinement_step(); + log_debug(gc, refine)("Concurrent Refine Adjust Only (#threads wanted: %u adjustment_needed: %s wait_for_heap_lock: %s) %.2fms", + cr()->num_threads_wanted(), + BOOL_TO_STR(cr()->is_thread_adjustment_needed()), + BOOL_TO_STR(cr()->heap_was_locked()), + (Ticks::now() - adjust_start).seconds() * MILLIUNITS); + + deactivate(); + break; } } - report_inactive("Deactivated", _refinement_stats - active_stats_start); + report_inactive("Deactivated"); update_perf_counter_cpu_time(); } - log_debug(gc, refine)("Stopping %d", _worker_id); + log_debug(gc, refine)("Stopping %s", name()); } void G1ConcurrentRefineThread::report_active(const char* reason) const { - log_trace(gc, refine)("%s worker %u, current: %zu", - reason, - _worker_id, - G1BarrierSet::dirty_card_queue_set().num_cards()); + log_trace(gc, refine)("%s active (%s)", name(), reason); } -void G1ConcurrentRefineThread::report_inactive(const char* reason, - const G1ConcurrentRefineStats& stats) const { - log_trace(gc, refine) - ("%s worker %u, cards: %zu, refined %zu, rate %1.2fc/ms", - reason, - _worker_id, - G1BarrierSet::dirty_card_queue_set().num_cards(), - stats.refined_cards(), - stats.refinement_rate_ms()); +void G1ConcurrentRefineThread::report_inactive(const char* reason) const { + log_trace(gc, refine)("%s inactive (%s)", name(), reason); } void G1ConcurrentRefineThread::activate() { @@ -103,21 +107,12 @@ void G1ConcurrentRefineThread::activate() { } } -bool G1ConcurrentRefineThread::maybe_deactivate() { +bool G1ConcurrentRefineThread::deactivate() { assert(this == Thread::current(), "precondition"); - if (cr()->is_thread_wanted(_worker_id)) { - return false; - } else { - MutexLocker ml(&_notifier, Mutex::_no_safepoint_check_flag); - bool requested = _requested_active; - _requested_active = false; - return !requested; // Deactivate only if not recently requested active. - } -} - -bool G1ConcurrentRefineThread::try_refinement_step(size_t stop_at) { - assert(this == Thread::current(), "precondition"); - return _cr->try_refinement_step(_worker_id, stop_at, &_refinement_stats); + MutexLocker ml(&_notifier, Mutex::_no_safepoint_check_flag); + bool requested = _requested_active; + _requested_active = false; + return !requested; // Deactivate only if not recently requested active. } void G1ConcurrentRefineThread::stop_service() { @@ -128,23 +123,9 @@ jlong G1ConcurrentRefineThread::cpu_time() { return os::thread_cpu_time(this); } -// The (single) primary thread drives the controller for the refinement threads. -class G1PrimaryConcurrentRefineThread final : public G1ConcurrentRefineThread { - bool wait_for_completed_buffers() override; - bool maybe_deactivate() override; - void do_refinement_step() override; - // Updates jstat cpu usage for all refinement threads. - void update_perf_counter_cpu_time() override; - -public: - G1PrimaryConcurrentRefineThread(G1ConcurrentRefine* cr) : - G1ConcurrentRefineThread(cr, 0) - {} -}; - -// When inactive, the primary thread periodically wakes up and requests -// adjustment of the number of active refinement threads. -bool G1PrimaryConcurrentRefineThread::wait_for_completed_buffers() { +// When inactive, the control thread periodically wakes up to check if there is +// refinement work pending. +bool G1ConcurrentRefineThread::wait_for_work() { assert(this == Thread::current(), "precondition"); MonitorLocker ml(notifier(), Mutex::_no_safepoint_check_flag); if (!requested_active() && !should_terminate()) { @@ -157,78 +138,115 @@ bool G1PrimaryConcurrentRefineThread::wait_for_completed_buffers() { return !should_terminate(); } -bool G1PrimaryConcurrentRefineThread::maybe_deactivate() { - // Don't deactivate while needing to adjust the number of active threads. - return !cr()->is_thread_adjustment_needed() && - G1ConcurrentRefineThread::maybe_deactivate(); +void G1ConcurrentRefineThread::do_refinement() { + G1ConcurrentRefineSweepState& state = _cr->sweep_state(); + + state.start_work(); + + // Swap card tables. + + // 1. Global card table + if (!state.swap_global_card_table()) { + log_debug(gc, refine)("GC pause after Global Card Table Swap"); + return; + } + + // 2. Java threads + if (!state.swap_java_threads_ct()) { + log_debug(gc, refine)("GC pause after Java Thread CT swap"); + return; + } + + // 3. GC threads + if (!state.swap_gc_threads_ct()) { + log_debug(gc, refine)("GC pause after GC Thread CT swap"); + return; + } + + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + jlong epoch_yield_duration = g1h->yield_duration_in_refinement_epoch(); + jlong next_epoch_start = os::elapsed_counter(); + + jlong total_yield_during_sweep_duration = 0; + + // 4. Snapshot heap. + state.snapshot_heap(); + + // 5. Sweep refinement table until done + bool interrupted_by_gc = false; + + log_info(gc, task)("Concurrent Refine Sweep Using %u of %u Workers", _cr->num_threads_wanted(), _cr->max_num_threads()); + + state.sweep_refinement_table_start(); + while (true) { + bool completed = state.sweep_refinement_table_step(); + + if (completed) { + break; + } + + if (SuspendibleThreadSet::should_yield()) { + jlong yield_during_sweep_start = os::elapsed_counter(); + SuspendibleThreadSet::yield(); + + // The yielding may have completed the task, check. + if (!state.is_in_progress()) { + log_debug(gc, refine)("GC completed sweeping, aborting concurrent operation"); + interrupted_by_gc = true; + break; + } else { + jlong yield_during_sweep_duration = os::elapsed_counter() - yield_during_sweep_start; + log_debug(gc, refine)("Yielded from card table sweeping for %.2fms, no GC inbetween, continue", + TimeHelper::counter_to_millis(yield_during_sweep_duration)); + total_yield_during_sweep_duration += yield_during_sweep_duration; + } + } + } + + if (!interrupted_by_gc) { + GCTraceTime(Info, gc, refine) tm("Concurrent Refine Complete Work"); + + state.add_yield_during_sweep_duration(total_yield_during_sweep_duration); + + state.complete_work(true); + + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + G1Policy* policy = g1h->policy(); + G1ConcurrentRefineStats* stats = state.stats(); + policy->record_refinement_stats(stats); + + { + // The young gen revising mechanism reads the predictor and the values set + // here. Avoid inconsistencies by locking. + MutexLocker x(G1ReviseYoungLength_lock, Mutex::_no_safepoint_check_flag); + policy->record_dirtying_stats(TimeHelper::counter_to_millis(G1CollectedHeap::heap()->last_refinement_epoch_start()), + TimeHelper::counter_to_millis(next_epoch_start), + stats->cards_pending(), + TimeHelper::counter_to_millis(epoch_yield_duration), + 0 /* pending_cards_from_gc */, + stats->cards_to_cset()); + G1CollectedHeap::heap()->set_last_refinement_epoch_start(next_epoch_start, epoch_yield_duration); + } + stats->reset(); + } } -void G1PrimaryConcurrentRefineThread::do_refinement_step() { - // Try adjustment first. If it succeeds then don't do any refinement this - // round. This thread may have just woken up but no threads are currently - // needed, which is common. In this case we want to just go back to - // waiting, with a minimum of fuss; in particular, don't do any "premature" - // refinement. However, adjustment may be pending but temporarily - // blocked. In that case we *do* try refinement, rather than possibly - // uselessly spinning while waiting for adjustment to succeed. - if (!cr()->adjust_threads_periodically()) { - // No adjustment, so try refinement, with the target as a cuttoff. - if (!try_refinement_step(cr()->pending_cards_target())) { - // Refinement was cut off, so proceed with fewer threads. - cr()->reduce_threads_wanted(); +void G1ConcurrentRefineThread::update_perf_counter_cpu_time() { + // The control thread is responsible for updating the CPU time for all workers. + if (UsePerfData) { + { + ThreadTotalCPUTimeClosure tttc(CPUTimeGroups::CPUTimeType::gc_conc_refine); + cr()->worker_threads_do(&tttc); + } + { + ThreadTotalCPUTimeClosure tttc(CPUTimeGroups::CPUTimeType::gc_conc_refine_control); + cr()->control_thread_do(&tttc); } } } -void G1PrimaryConcurrentRefineThread::update_perf_counter_cpu_time() { - if (UsePerfData) { - ThreadTotalCPUTimeClosure tttc(CPUTimeGroups::CPUTimeType::gc_conc_refine); - cr()->threads_do(&tttc); - } -} - -class G1SecondaryConcurrentRefineThread final : public G1ConcurrentRefineThread { - bool wait_for_completed_buffers() override; - void do_refinement_step() override; - void update_perf_counter_cpu_time() override { /* Nothing to do. The primary thread does all the work. */ } - -public: - G1SecondaryConcurrentRefineThread(G1ConcurrentRefine* cr, uint worker_id) : - G1ConcurrentRefineThread(cr, worker_id) - { - assert(worker_id > 0, "precondition"); - } -}; - -bool G1SecondaryConcurrentRefineThread::wait_for_completed_buffers() { - assert(this == Thread::current(), "precondition"); - MonitorLocker ml(notifier(), Mutex::_no_safepoint_check_flag); - while (!requested_active() && !should_terminate()) { - ml.wait(); - } - return !should_terminate(); -} - -void G1SecondaryConcurrentRefineThread::do_refinement_step() { - assert(this == Thread::current(), "precondition"); - // Secondary threads ignore the target and just drive the number of pending - // dirty cards down. The primary thread is responsible for noticing the - // target has been reached and reducing the number of wanted threads. This - // makes the control of wanted threads all under the primary, while avoiding - // useless spinning by secondary threads until the primary thread notices. - // (Useless spinning is still possible if there are no pending cards, but - // that should rarely happen.) - try_refinement_step(0); -} - -G1ConcurrentRefineThread* -G1ConcurrentRefineThread::create(G1ConcurrentRefine* cr, uint worker_id) { - G1ConcurrentRefineThread* crt; - if (worker_id == 0) { - crt = new (std::nothrow) G1PrimaryConcurrentRefineThread(cr); - } else { - crt = new (std::nothrow) G1SecondaryConcurrentRefineThread(cr, worker_id); - } +G1ConcurrentRefineThread* G1ConcurrentRefineThread::create(G1ConcurrentRefine* cr) { + G1ConcurrentRefineThread* crt = new (std::nothrow) G1ConcurrentRefineThread(cr); if (crt != nullptr) { crt->create_and_start(); } diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp index b1e34e4b78d..8e635247cd3 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp @@ -33,8 +33,8 @@ // Forward Decl. class G1ConcurrentRefine; -// One or more G1 Concurrent Refinement Threads may be active if concurrent -// refinement is in progress. +// Concurrent refinement control thread watching card mark accrual on the card table +// and starting refinement work. class G1ConcurrentRefineThread: public ConcurrentGCThread { friend class VMStructs; friend class G1CollectedHeap; @@ -42,43 +42,34 @@ class G1ConcurrentRefineThread: public ConcurrentGCThread { Monitor _notifier; bool _requested_active; - G1ConcurrentRefineStats _refinement_stats; - uint _worker_id; G1ConcurrentRefine* _cr; NONCOPYABLE(G1ConcurrentRefineThread); -protected: - G1ConcurrentRefineThread(G1ConcurrentRefine* cr, uint worker_id); + G1ConcurrentRefineThread(G1ConcurrentRefine* cr); Monitor* notifier() { return &_notifier; } bool requested_active() const { return _requested_active; } // Returns !should_terminate(). // precondition: this is the current thread. - virtual bool wait_for_completed_buffers() = 0; + bool wait_for_work(); // Deactivate if appropriate. Returns true if deactivated. // precondition: this is the current thread. - virtual bool maybe_deactivate(); + bool deactivate(); - // Attempt to do some refinement work. - // precondition: this is the current thread. - virtual void do_refinement_step() = 0; + // Swap card table and do a complete re-examination/refinement pass over the + // refinement table. + void do_refinement(); // Update concurrent refine threads cpu time stats. - virtual void update_perf_counter_cpu_time() = 0; - - // Helper for do_refinement_step implementations. Try to perform some - // refinement work, limited by stop_at. Returns true if any refinement work - // was performed, false if no work available per stop_at. - // precondition: this is the current thread. - bool try_refinement_step(size_t stop_at); + void update_perf_counter_cpu_time(); void report_active(const char* reason) const; - void report_inactive(const char* reason, const G1ConcurrentRefineStats& stats) const; + void report_inactive(const char* reason) const; G1ConcurrentRefine* cr() const { return _cr; } @@ -86,23 +77,12 @@ protected: void stop_service() override; public: - static G1ConcurrentRefineThread* create(G1ConcurrentRefine* cr, uint worker_id); - virtual ~G1ConcurrentRefineThread() = default; - - uint worker_id() const { return _worker_id; } + static G1ConcurrentRefineThread* create(G1ConcurrentRefine* cr); // Activate this thread. // precondition: this is not the current thread. void activate(); - G1ConcurrentRefineStats* refinement_stats() { - return &_refinement_stats; - } - - const G1ConcurrentRefineStats* refinement_stats() const { - return &_refinement_stats; - } - // Total cpu time spent in this thread so far. jlong cpu_time(); }; diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefineThreadsNeeded.cpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineThreadsNeeded.cpp index d34229bd359..3ab26bd72af 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefineThreadsNeeded.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineThreadsNeeded.cpp @@ -45,48 +45,22 @@ G1ConcurrentRefineThreadsNeeded::G1ConcurrentRefineThreadsNeeded(G1Policy* polic // // 1. Minimize the number of refinement threads running at once. // -// 2. Minimize the number of activations and deactivations for the -// refinement threads that run. -// -// 3. Delay performing refinement work. Having more dirty cards waiting to +// 2. Delay performing refinement work. Having more dirty cards waiting to // be refined can be beneficial, as further writes to the same card don't // create more work. void G1ConcurrentRefineThreadsNeeded::update(uint active_threads, size_t available_bytes, size_t num_cards, size_t target_num_cards) { + _predicted_time_until_next_gc_ms = _policy->predict_time_to_next_gc_ms(available_bytes); + + // Estimate number of cards that need to be processed before next GC. const G1Analytics* analytics = _policy->analytics(); - // Estimate time until next GC, based on remaining bytes available for - // allocation and the allocation rate. - double alloc_region_rate = analytics->predict_alloc_rate_ms(); - double alloc_bytes_rate = alloc_region_rate * G1HeapRegion::GrainBytes; - if (alloc_bytes_rate == 0.0) { - // A zero rate indicates we don't yet have data to use for predictions. - // Since we don't have any idea how long until the next GC, use a time of - // zero. - _predicted_time_until_next_gc_ms = 0.0; - } else { - // If the heap size is large and the allocation rate is small, we can get - // a predicted time until next GC that is so large it can cause problems - // (such as overflow) in other calculations. Limit the prediction to one - // hour, which is still large in this context. - const double one_hour_ms = 60.0 * 60.0 * MILLIUNITS; - double raw_time_ms = available_bytes / alloc_bytes_rate; - _predicted_time_until_next_gc_ms = MIN2(raw_time_ms, one_hour_ms); - } + double incoming_rate = analytics->predict_dirtied_cards_rate_ms(); + double raw_cards = incoming_rate * _predicted_time_until_next_gc_ms; + size_t incoming_cards = static_cast(raw_cards); - // Estimate number of cards that need to be processed before next GC. There - // are no incoming cards when time is short, because in that case the - // controller activates refinement by mutator threads to stay on target even - // if threads deactivate in the meantime. This also covers the case of not - // having a real prediction of time until GC. - size_t incoming_cards = 0; - if (_predicted_time_until_next_gc_ms > _update_period_ms) { - double incoming_rate = analytics->predict_dirtied_cards_rate_ms(); - double raw_cards = incoming_rate * _predicted_time_until_next_gc_ms; - incoming_cards = static_cast(raw_cards); - } size_t total_cards = num_cards + incoming_cards; _predicted_cards_at_next_gc = total_cards; @@ -100,9 +74,8 @@ void G1ConcurrentRefineThreadsNeeded::update(uint active_threads, // The calculation of the number of threads needed isn't very stable when // time is short, and can lead to starting up lots of threads for not much // profit. If we're in the last update period, don't change the number of - // threads running, other than to treat the current thread as running. That - // might not be sufficient, but hopefully we were already reasonably close. - // We won't accumulate more because mutator refinement will be activated. + // threads needed. That might not be sufficient, but hopefully we were + // already reasonably close. if (_predicted_time_until_next_gc_ms <= _update_period_ms) { _threads_needed = MAX2(active_threads, 1u); return; @@ -133,11 +106,12 @@ void G1ConcurrentRefineThreadsNeeded::update(uint active_threads, // close to the next GC we want to drive toward the target, so round up // then. The rest of the time we round to nearest, trying to remain near // the middle of the range. + double rthreads = nthreads; if (_predicted_time_until_next_gc_ms <= _update_period_ms * 5.0) { - nthreads = ::ceil(nthreads); + rthreads = ::ceil(nthreads); } else { - nthreads = ::round(nthreads); + rthreads = ::round(nthreads); } - _threads_needed = static_cast(MIN2(nthreads, UINT_MAX)); + _threads_needed = static_cast(MIN2(rthreads, UINT_MAX)); } diff --git a/src/hotspot/share/gc/g1/g1DirtyCardQueue.cpp b/src/hotspot/share/gc/g1/g1DirtyCardQueue.cpp deleted file mode 100644 index ec9d68af3bb..00000000000 --- a/src/hotspot/share/gc/g1/g1DirtyCardQueue.cpp +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#include "gc/g1/g1BarrierSet.inline.hpp" -#include "gc/g1/g1CardTableEntryClosure.hpp" -#include "gc/g1/g1CollectedHeap.inline.hpp" -#include "gc/g1/g1ConcurrentRefineStats.hpp" -#include "gc/g1/g1ConcurrentRefineThread.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" -#include "gc/g1/g1FreeIdSet.hpp" -#include "gc/g1/g1HeapRegionRemSet.inline.hpp" -#include "gc/g1/g1RedirtyCardsQueue.hpp" -#include "gc/g1/g1RemSet.hpp" -#include "gc/g1/g1ThreadLocalData.hpp" -#include "gc/shared/bufferNode.hpp" -#include "gc/shared/bufferNodeList.hpp" -#include "gc/shared/suspendibleThreadSet.hpp" -#include "memory/iterator.hpp" -#include "runtime/atomicAccess.hpp" -#include "runtime/javaThread.hpp" -#include "runtime/mutex.hpp" -#include "runtime/mutexLocker.hpp" -#include "runtime/os.hpp" -#include "runtime/safepoint.hpp" -#include "runtime/threads.hpp" -#include "runtime/threadSMR.hpp" -#include "utilities/globalCounter.inline.hpp" -#include "utilities/macros.hpp" -#include "utilities/nonblockingQueue.inline.hpp" -#include "utilities/pair.hpp" -#include "utilities/quickSort.hpp" -#include "utilities/ticks.hpp" - -G1DirtyCardQueue::G1DirtyCardQueue(G1DirtyCardQueueSet* qset) : - PtrQueue(qset), - _refinement_stats(new G1ConcurrentRefineStats()) -{ } - -G1DirtyCardQueue::~G1DirtyCardQueue() { - delete _refinement_stats; -} - -// Assumed to be zero by concurrent threads. -static uint par_ids_start() { return 0; } - -G1DirtyCardQueueSet::G1DirtyCardQueueSet(BufferNode::Allocator* allocator) : - PtrQueueSet(allocator), - _num_cards(0), - _mutator_refinement_threshold(SIZE_MAX), - _completed(), - _paused(), - _free_ids(par_ids_start(), num_par_ids()), - _detached_refinement_stats() -{} - -G1DirtyCardQueueSet::~G1DirtyCardQueueSet() { - abandon_completed_buffers(); -} - -// Determines how many mutator threads can process the buffers in parallel. -uint G1DirtyCardQueueSet::num_par_ids() { - return (uint)os::initial_active_processor_count(); -} - -void G1DirtyCardQueueSet::flush_queue(G1DirtyCardQueue& queue) { - if (queue.buffer() != nullptr) { - G1ConcurrentRefineStats* stats = queue.refinement_stats(); - stats->inc_dirtied_cards(queue.size()); - } - PtrQueueSet::flush_queue(queue); -} - -void G1DirtyCardQueueSet::enqueue(G1DirtyCardQueue& queue, - volatile CardValue* card_ptr) { - CardValue* value = const_cast(card_ptr); - if (!try_enqueue(queue, value)) { - handle_zero_index(queue); - retry_enqueue(queue, value); - } -} - -void G1DirtyCardQueueSet::handle_zero_index(G1DirtyCardQueue& queue) { - assert(queue.index() == 0, "precondition"); - BufferNode* old_node = exchange_buffer_with_new(queue); - if (old_node != nullptr) { - assert(old_node->index() == 0, "invariant"); - G1ConcurrentRefineStats* stats = queue.refinement_stats(); - stats->inc_dirtied_cards(old_node->capacity()); - handle_completed_buffer(old_node, stats); - } -} - -void G1DirtyCardQueueSet::handle_zero_index_for_thread(Thread* t) { - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(t); - G1BarrierSet::dirty_card_queue_set().handle_zero_index(queue); -} - -size_t G1DirtyCardQueueSet::num_cards() const { - return AtomicAccess::load(&_num_cards); -} - -void G1DirtyCardQueueSet::enqueue_completed_buffer(BufferNode* cbn) { - assert(cbn != nullptr, "precondition"); - // Increment _num_cards before adding to queue, so queue removal doesn't - // need to deal with _num_cards possibly going negative. - AtomicAccess::add(&_num_cards, cbn->size()); - // Perform push in CS. The old tail may be popped while the push is - // observing it (attaching it to the new buffer). We need to ensure it - // can't be reused until the push completes, to avoid ABA problems. - GlobalCounter::CriticalSection cs(Thread::current()); - _completed.push(*cbn); -} - -// Thread-safe attempt to remove and return the first buffer from -// the _completed queue, using the NonblockingQueue::try_pop() underneath. -// It has a limitation that it may return null when there are objects -// in the queue if there is a concurrent push/append operation. -BufferNode* G1DirtyCardQueueSet::dequeue_completed_buffer() { - Thread* current_thread = Thread::current(); - BufferNode* result = nullptr; - while (true) { - // Use GlobalCounter critical section to avoid ABA problem. - // The release of a buffer to its allocator's free list uses - // GlobalCounter::write_synchronize() to coordinate with this - // dequeuing operation. - // We use a CS per iteration, rather than over the whole loop, - // because we're not guaranteed to make progress. Lingering in - // one CS could defer releasing buffer to the free list for reuse, - // leading to excessive allocations. - GlobalCounter::CriticalSection cs(current_thread); - if (_completed.try_pop(&result)) return result; - } -} - -BufferNode* G1DirtyCardQueueSet::get_completed_buffer() { - BufferNode* result = dequeue_completed_buffer(); - if (result == nullptr) { // Unlikely if no paused buffers. - enqueue_previous_paused_buffers(); - result = dequeue_completed_buffer(); - if (result == nullptr) return nullptr; - } - AtomicAccess::sub(&_num_cards, result->size()); - return result; -} - -#ifdef ASSERT -void G1DirtyCardQueueSet::verify_num_cards() const { - size_t actual = 0; - for (BufferNode* cur = _completed.first(); - !_completed.is_end(cur); - cur = cur->next()) { - actual += cur->size(); - } - assert(actual == AtomicAccess::load(&_num_cards), - "Num entries in completed buffers should be %zu but are %zu", - AtomicAccess::load(&_num_cards), actual); -} -#endif // ASSERT - -G1DirtyCardQueueSet::PausedBuffers::PausedList::PausedList() : - _head(nullptr), _tail(nullptr), - _safepoint_id(SafepointSynchronize::safepoint_id()) -{} - -#ifdef ASSERT -G1DirtyCardQueueSet::PausedBuffers::PausedList::~PausedList() { - assert(AtomicAccess::load(&_head) == nullptr, "precondition"); - assert(_tail == nullptr, "precondition"); -} -#endif // ASSERT - -bool G1DirtyCardQueueSet::PausedBuffers::PausedList::is_next() const { - assert_not_at_safepoint(); - return _safepoint_id == SafepointSynchronize::safepoint_id(); -} - -void G1DirtyCardQueueSet::PausedBuffers::PausedList::add(BufferNode* node) { - assert_not_at_safepoint(); - assert(is_next(), "precondition"); - BufferNode* old_head = AtomicAccess::xchg(&_head, node); - if (old_head == nullptr) { - assert(_tail == nullptr, "invariant"); - _tail = node; - } else { - node->set_next(old_head); - } -} - -G1DirtyCardQueueSet::HeadTail G1DirtyCardQueueSet::PausedBuffers::PausedList::take() { - BufferNode* head = AtomicAccess::load(&_head); - BufferNode* tail = _tail; - AtomicAccess::store(&_head, (BufferNode*)nullptr); - _tail = nullptr; - return HeadTail(head, tail); -} - -G1DirtyCardQueueSet::PausedBuffers::PausedBuffers() : _plist(nullptr) {} - -#ifdef ASSERT -G1DirtyCardQueueSet::PausedBuffers::~PausedBuffers() { - assert(AtomicAccess::load(&_plist) == nullptr, "invariant"); -} -#endif // ASSERT - -void G1DirtyCardQueueSet::PausedBuffers::add(BufferNode* node) { - assert_not_at_safepoint(); - PausedList* plist = AtomicAccess::load_acquire(&_plist); - if (plist == nullptr) { - // Try to install a new next list. - plist = new PausedList(); - PausedList* old_plist = AtomicAccess::cmpxchg(&_plist, (PausedList*)nullptr, plist); - if (old_plist != nullptr) { - // Some other thread installed a new next list. Use it instead. - delete plist; - plist = old_plist; - } - } - assert(plist->is_next(), "invariant"); - plist->add(node); -} - -G1DirtyCardQueueSet::HeadTail G1DirtyCardQueueSet::PausedBuffers::take_previous() { - assert_not_at_safepoint(); - PausedList* previous; - { - // Deal with plist in a critical section, to prevent it from being - // deleted out from under us by a concurrent take_previous(). - GlobalCounter::CriticalSection cs(Thread::current()); - previous = AtomicAccess::load_acquire(&_plist); - if ((previous == nullptr) || // Nothing to take. - previous->is_next() || // Not from a previous safepoint. - // Some other thread stole it. - (AtomicAccess::cmpxchg(&_plist, previous, (PausedList*)nullptr) != previous)) { - return HeadTail(); - } - } - // We now own previous. - HeadTail result = previous->take(); - // There might be other threads examining previous (in concurrent - // take_previous()). Synchronize to wait until any such threads are - // done with such examination before deleting. - GlobalCounter::write_synchronize(); - delete previous; - return result; -} - -G1DirtyCardQueueSet::HeadTail G1DirtyCardQueueSet::PausedBuffers::take_all() { - assert_at_safepoint(); - HeadTail result; - PausedList* plist = AtomicAccess::load(&_plist); - if (plist != nullptr) { - AtomicAccess::store(&_plist, (PausedList*)nullptr); - result = plist->take(); - delete plist; - } - return result; -} - -void G1DirtyCardQueueSet::record_paused_buffer(BufferNode* node) { - assert_not_at_safepoint(); - assert(node->next() == nullptr, "precondition"); - // Ensure there aren't any paused buffers from a previous safepoint. - enqueue_previous_paused_buffers(); - // Cards for paused buffers are included in count, to contribute to - // notification checking after the coming safepoint if it doesn't GC. - // Note that this means the queue's _num_cards differs from the number - // of cards in the queued buffers when there are paused buffers. - AtomicAccess::add(&_num_cards, node->size()); - _paused.add(node); -} - -void G1DirtyCardQueueSet::enqueue_paused_buffers_aux(const HeadTail& paused) { - if (paused._head != nullptr) { - assert(paused._tail != nullptr, "invariant"); - // Cards from paused buffers are already recorded in the queue count. - _completed.append(*paused._head, *paused._tail); - } -} - -void G1DirtyCardQueueSet::enqueue_previous_paused_buffers() { - assert_not_at_safepoint(); - enqueue_paused_buffers_aux(_paused.take_previous()); -} - -void G1DirtyCardQueueSet::enqueue_all_paused_buffers() { - assert_at_safepoint(); - enqueue_paused_buffers_aux(_paused.take_all()); -} - -void G1DirtyCardQueueSet::abandon_completed_buffers() { - BufferNodeList list = take_all_completed_buffers(); - BufferNode* buffers_to_delete = list._head; - while (buffers_to_delete != nullptr) { - BufferNode* bn = buffers_to_delete; - buffers_to_delete = bn->next(); - bn->set_next(nullptr); - deallocate_buffer(bn); - } -} - -// Merge lists of buffers. The source queue set is emptied as a -// result. The queue sets must share the same allocator. -void G1DirtyCardQueueSet::merge_bufferlists(G1RedirtyCardsQueueSet* src) { - assert(allocator() == src->allocator(), "precondition"); - const BufferNodeList from = src->take_all_completed_buffers(); - if (from._head != nullptr) { - AtomicAccess::add(&_num_cards, from._entry_count); - _completed.append(*from._head, *from._tail); - } -} - -BufferNodeList G1DirtyCardQueueSet::take_all_completed_buffers() { - enqueue_all_paused_buffers(); - verify_num_cards(); - Pair pair = _completed.take_all(); - size_t num_cards = AtomicAccess::load(&_num_cards); - AtomicAccess::store(&_num_cards, size_t(0)); - return BufferNodeList(pair.first, pair.second, num_cards); -} - -class G1RefineBufferedCards : public StackObj { - BufferNode* const _node; - CardTable::CardValue** const _node_buffer; - const size_t _node_buffer_capacity; - const uint _worker_id; - G1ConcurrentRefineStats* _stats; - G1RemSet* const _g1rs; - - static inline ptrdiff_t compare_cards(const CardTable::CardValue* p1, - const CardTable::CardValue* p2) { - return p2 - p1; - } - - // Sorts the cards from start_index to _node_buffer_capacity in *decreasing* - // address order. Tests showed that this order is preferable to not sorting - // or increasing address order. - void sort_cards(size_t start_index) { - QuickSort::sort(&_node_buffer[start_index], - _node_buffer_capacity - start_index, - compare_cards); - } - - // Returns the index to the first clean card in the buffer. - size_t clean_cards() { - const size_t start = _node->index(); - assert(start <= _node_buffer_capacity, "invariant"); - - // Two-fingered compaction algorithm similar to the filtering mechanism in - // SATBMarkQueue. The main difference is that clean_card_before_refine() - // could change the buffer element in-place. - // We don't check for SuspendibleThreadSet::should_yield(), because - // cleaning and redirtying the cards is fast. - CardTable::CardValue** src = &_node_buffer[start]; - CardTable::CardValue** dst = &_node_buffer[_node_buffer_capacity]; - assert(src <= dst, "invariant"); - for ( ; src < dst; ++src) { - // Search low to high for a card to keep. - if (_g1rs->clean_card_before_refine(src)) { - // Found keeper. Search high to low for a card to discard. - while (src < --dst) { - if (!_g1rs->clean_card_before_refine(dst)) { - *dst = *src; // Replace discard with keeper. - break; - } - } - // If discard search failed (src == dst), the outer loop will also end. - } - } - - // dst points to the first retained clean card, or the end of the buffer - // if all the cards were discarded. - const size_t first_clean = dst - _node_buffer; - assert(first_clean >= start && first_clean <= _node_buffer_capacity, "invariant"); - // Discarded cards are considered as refined. - _stats->inc_refined_cards(first_clean - start); - _stats->inc_precleaned_cards(first_clean - start); - return first_clean; - } - - bool refine_cleaned_cards(size_t start_index) { - bool result = true; - size_t i = start_index; - for ( ; i < _node_buffer_capacity; ++i) { - if (SuspendibleThreadSet::should_yield()) { - redirty_unrefined_cards(i); - result = false; - break; - } - _g1rs->refine_card_concurrently(_node_buffer[i], _worker_id); - } - _node->set_index(i); - _stats->inc_refined_cards(i - start_index); - return result; - } - - void redirty_unrefined_cards(size_t start) { - for ( ; start < _node_buffer_capacity; ++start) { - *_node_buffer[start] = G1CardTable::dirty_card_val(); - } - } - -public: - G1RefineBufferedCards(BufferNode* node, - uint worker_id, - G1ConcurrentRefineStats* stats) : - _node(node), - _node_buffer(reinterpret_cast(BufferNode::make_buffer_from_node(node))), - _node_buffer_capacity(node->capacity()), - _worker_id(worker_id), - _stats(stats), - _g1rs(G1CollectedHeap::heap()->rem_set()) {} - - bool refine() { - size_t first_clean_index = clean_cards(); - if (first_clean_index == _node_buffer_capacity) { - _node->set_index(first_clean_index); - return true; - } - // This fence serves two purposes. First, the cards must be cleaned - // before processing the contents. Second, we can't proceed with - // processing a region until after the read of the region's top in - // collect_and_clean_cards(), for synchronization with possibly concurrent - // humongous object allocation (see comment at the StoreStore fence before - // setting the regions' tops in humongous allocation path). - // It's okay that reading region's top and reading region's type were racy - // wrto each other. We need both set, in any order, to proceed. - OrderAccess::fence(); - sort_cards(first_clean_index); - return refine_cleaned_cards(first_clean_index); - } -}; - -bool G1DirtyCardQueueSet::refine_buffer(BufferNode* node, - uint worker_id, - G1ConcurrentRefineStats* stats) { - Ticks start_time = Ticks::now(); - G1RefineBufferedCards buffered_cards(node, worker_id, stats); - bool result = buffered_cards.refine(); - stats->inc_refinement_time(Ticks::now() - start_time); - return result; -} - -void G1DirtyCardQueueSet::handle_refined_buffer(BufferNode* node, - bool fully_processed) { - if (fully_processed) { - assert(node->is_empty(), "Buffer not fully consumed: index: %zu, size: %zu", - node->index(), node->capacity()); - deallocate_buffer(node); - } else { - assert(!node->is_empty(), "Buffer fully consumed."); - // Buffer incompletely processed because there is a pending safepoint. - // Record partially processed buffer, to be finished later. - record_paused_buffer(node); - } -} - -void G1DirtyCardQueueSet::handle_completed_buffer(BufferNode* new_node, - G1ConcurrentRefineStats* stats) { - enqueue_completed_buffer(new_node); - - // No need for mutator refinement if number of cards is below limit. - if (AtomicAccess::load(&_num_cards) <= AtomicAccess::load(&_mutator_refinement_threshold)) { - return; - } - - // Don't try to process a buffer that will just get immediately paused. - // When going into a safepoint it's just a waste of effort. - // When coming out of a safepoint, Java threads may be running before the - // yield request (for non-Java threads) has been cleared. - if (SuspendibleThreadSet::should_yield()) { - return; - } - - // Only Java threads perform mutator refinement. - if (!Thread::current()->is_Java_thread()) { - return; - } - - BufferNode* node = get_completed_buffer(); - if (node == nullptr) return; // Didn't get a buffer to process. - - // Refine cards in buffer. - - uint worker_id = _free_ids.claim_par_id(); // temporarily claim an id - bool fully_processed = refine_buffer(node, worker_id, stats); - _free_ids.release_par_id(worker_id); // release the id - - // Deal with buffer after releasing id, to let another thread use id. - handle_refined_buffer(node, fully_processed); -} - -bool G1DirtyCardQueueSet::refine_completed_buffer_concurrently(uint worker_id, - size_t stop_at, - G1ConcurrentRefineStats* stats) { - // Not enough cards to trigger processing. - if (AtomicAccess::load(&_num_cards) <= stop_at) return false; - - BufferNode* node = get_completed_buffer(); - if (node == nullptr) return false; // Didn't get a buffer to process. - - bool fully_processed = refine_buffer(node, worker_id, stats); - handle_refined_buffer(node, fully_processed); - return true; -} - -void G1DirtyCardQueueSet::abandon_logs_and_stats() { - assert_at_safepoint(); - - // Disable mutator refinement until concurrent refinement decides otherwise. - set_mutator_refinement_threshold(SIZE_MAX); - - // Iterate over all the threads, resetting per-thread queues and stats. - struct AbandonThreadLogClosure : public ThreadClosure { - G1DirtyCardQueueSet& _qset; - AbandonThreadLogClosure(G1DirtyCardQueueSet& qset) : _qset(qset) {} - virtual void do_thread(Thread* t) { - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(t); - _qset.reset_queue(queue); - queue.refinement_stats()->reset(); - } - } closure(*this); - Threads::threads_do(&closure); - - enqueue_all_paused_buffers(); - abandon_completed_buffers(); - - // Reset stats from detached threads. - MutexLocker ml(G1DetachedRefinementStats_lock, Mutex::_no_safepoint_check_flag); - _detached_refinement_stats.reset(); -} - -void G1DirtyCardQueueSet::update_refinement_stats(G1ConcurrentRefineStats& stats) { - assert_at_safepoint(); - - _concatenated_refinement_stats = stats; - - enqueue_all_paused_buffers(); - verify_num_cards(); - - // Collect and reset stats from detached threads. - MutexLocker ml(G1DetachedRefinementStats_lock, Mutex::_no_safepoint_check_flag); - _concatenated_refinement_stats += _detached_refinement_stats; - _detached_refinement_stats.reset(); -} - -G1ConcurrentRefineStats G1DirtyCardQueueSet::concatenate_log_and_stats(Thread* thread) { - assert_at_safepoint(); - - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(thread); - // Flush the buffer if non-empty. Flush before accumulating and - // resetting stats, since flushing may modify the stats. - if (!queue.is_empty()) { - flush_queue(queue); - } - - G1ConcurrentRefineStats result = *queue.refinement_stats(); - queue.refinement_stats()->reset(); - return result; -} - -G1ConcurrentRefineStats G1DirtyCardQueueSet::concatenated_refinement_stats() const { - assert_at_safepoint(); - return _concatenated_refinement_stats; -} - -void G1DirtyCardQueueSet::record_detached_refinement_stats(G1ConcurrentRefineStats* stats) { - MutexLocker ml(G1DetachedRefinementStats_lock, Mutex::_no_safepoint_check_flag); - _detached_refinement_stats += *stats; - stats->reset(); -} - -size_t G1DirtyCardQueueSet::mutator_refinement_threshold() const { - return AtomicAccess::load(&_mutator_refinement_threshold); -} - -void G1DirtyCardQueueSet::set_mutator_refinement_threshold(size_t value) { - AtomicAccess::store(&_mutator_refinement_threshold, value); -} diff --git a/src/hotspot/share/gc/g1/g1DirtyCardQueue.hpp b/src/hotspot/share/gc/g1/g1DirtyCardQueue.hpp deleted file mode 100644 index 6beb536df87..00000000000 --- a/src/hotspot/share/gc/g1/g1DirtyCardQueue.hpp +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef SHARE_GC_G1_G1DIRTYCARDQUEUE_HPP -#define SHARE_GC_G1_G1DIRTYCARDQUEUE_HPP - -#include "gc/g1/g1CardTable.hpp" -#include "gc/g1/g1ConcurrentRefineStats.hpp" -#include "gc/g1/g1FreeIdSet.hpp" -#include "gc/shared/bufferNode.hpp" -#include "gc/shared/bufferNodeList.hpp" -#include "gc/shared/ptrQueue.hpp" -#include "memory/allocation.hpp" -#include "memory/padded.hpp" -#include "utilities/nonblockingQueue.hpp" - -class G1PrimaryConcurrentRefineThread; -class G1DirtyCardQueueSet; -class G1RedirtyCardsQueueSet; -class Thread; - -// A ptrQueue whose elements are "oops", pointers to object heads. -class G1DirtyCardQueue: public PtrQueue { - G1ConcurrentRefineStats* _refinement_stats; - -public: - G1DirtyCardQueue(G1DirtyCardQueueSet* qset); - - // Flush before destroying; queue may be used to capture pending work while - // doing something else, with auto-flush on completion. - ~G1DirtyCardQueue(); - - G1ConcurrentRefineStats* refinement_stats() const { - return _refinement_stats; - } - - // Compiler support. - static ByteSize byte_offset_of_index() { - return PtrQueue::byte_offset_of_index(); - } - using PtrQueue::byte_width_of_index; - - static ByteSize byte_offset_of_buf() { - return PtrQueue::byte_offset_of_buf(); - } - using PtrQueue::byte_width_of_buf; - -}; - -class G1DirtyCardQueueSet: public PtrQueueSet { - // Head and tail of a list of BufferNodes, linked through their next() - // fields. Similar to BufferNodeList, but without the _entry_count. - struct HeadTail { - BufferNode* _head; - BufferNode* _tail; - HeadTail() : _head(nullptr), _tail(nullptr) {} - HeadTail(BufferNode* head, BufferNode* tail) : _head(head), _tail(tail) {} - }; - - // Concurrent refinement may stop processing in the middle of a buffer if - // there is a pending safepoint, to avoid long delays to safepoint. A - // partially processed buffer needs to be recorded for processing by the - // safepoint if it's a GC safepoint; otherwise it needs to be recorded for - // further concurrent refinement work after the safepoint. But if the - // buffer was obtained from the completed buffer queue then it can't simply - // be added back to the queue, as that would introduce a new source of ABA - // for the queue. - // - // The PausedBuffer object is used to record such buffers for the upcoming - // safepoint, and provides access to the buffers recorded for previous - // safepoints. Before obtaining a buffer from the completed buffers queue, - // we first transfer any buffers from previous safepoints to the queue. - // This is ABA-safe because threads cannot be in the midst of a queue pop - // across a safepoint. - // - // The paused buffers are conceptually an extension of the completed buffers - // queue, and operations which need to deal with all of the queued buffers - // (such as concatenating or abandoning logs) also need to deal with any - // paused buffers. In general, if a safepoint performs a GC then the paused - // buffers will be processed as part of it, and there won't be any paused - // buffers after a GC safepoint. - class PausedBuffers { - class PausedList : public CHeapObj { - BufferNode* volatile _head; - BufferNode* _tail; - size_t _safepoint_id; - - NONCOPYABLE(PausedList); - - public: - PausedList(); - DEBUG_ONLY(~PausedList();) - - // Return true if this list was created to hold buffers for the - // next safepoint. - // precondition: not at safepoint. - bool is_next() const; - - // Thread-safe add the buffer to the list. - // precondition: not at safepoint. - // precondition: is_next(). - void add(BufferNode* node); - - // Take all the buffers from the list. Not thread-safe. - HeadTail take(); - }; - - // The most recently created list, which might be for either the next or - // a previous safepoint, or might be null if the next list hasn't been - // created yet. We only need one list because of the requirement that - // threads calling add() must first ensure there are no paused buffers - // from a previous safepoint. There might be many list instances existing - // at the same time though; there can be many threads competing to create - // and install the next list, and meanwhile there can be a thread dealing - // with the previous list. - PausedList* volatile _plist; - DEFINE_PAD_MINUS_SIZE(1, DEFAULT_PADDING_SIZE, sizeof(PausedList*)); - - NONCOPYABLE(PausedBuffers); - - public: - PausedBuffers(); - DEBUG_ONLY(~PausedBuffers();) - - // Thread-safe add the buffer to paused list for next safepoint. - // precondition: not at safepoint. - // precondition: does not have paused buffers from a previous safepoint. - void add(BufferNode* node); - - // Thread-safe take all paused buffers for previous safepoints. - // precondition: not at safepoint. - HeadTail take_previous(); - - // Take all the paused buffers. - // precondition: at safepoint. - HeadTail take_all(); - }; - - DEFINE_PAD_MINUS_SIZE(0, DEFAULT_PADDING_SIZE, 0); - // Upper bound on the number of cards in the completed and paused buffers. - volatile size_t _num_cards; - DEFINE_PAD_MINUS_SIZE(1, DEFAULT_PADDING_SIZE, sizeof(size_t)); - // If the queue contains more cards than configured here, the - // mutator must start doing some of the concurrent refinement work. - volatile size_t _mutator_refinement_threshold; - DEFINE_PAD_MINUS_SIZE(2, DEFAULT_PADDING_SIZE, sizeof(size_t)); - // Buffers ready for refinement. - // NonblockingQueue has inner padding of one cache line. - NonblockingQueue _completed; - // Add a trailer padding after NonblockingQueue. - DEFINE_PAD_MINUS_SIZE(3, DEFAULT_PADDING_SIZE, sizeof(BufferNode*)); - // Buffers for which refinement is temporarily paused. - // PausedBuffers has inner padding, including trailer. - PausedBuffers _paused; - - G1FreeIdSet _free_ids; - - G1ConcurrentRefineStats _concatenated_refinement_stats; - G1ConcurrentRefineStats _detached_refinement_stats; - - // Verify _num_cards == sum of cards in the completed queue. - void verify_num_cards() const NOT_DEBUG_RETURN; - - // Thread-safe add a buffer to paused list for next safepoint. - // precondition: not at safepoint. - void record_paused_buffer(BufferNode* node); - void enqueue_paused_buffers_aux(const HeadTail& paused); - // Thread-safe transfer paused buffers for previous safepoints to the queue. - // precondition: not at safepoint. - void enqueue_previous_paused_buffers(); - // Transfer all paused buffers to the queue. - // precondition: at safepoint. - void enqueue_all_paused_buffers(); - - void abandon_completed_buffers(); - - // Refine the cards in "node" from its index to buffer_capacity. - // Stops processing if SuspendibleThreadSet::should_yield() is true. - // Returns true if the entire buffer was processed, false if there - // is a pending yield request. The node's index is updated to exclude - // the processed elements, e.g. up to the element before processing - // stopped, or one past the last element if the entire buffer was - // processed. Updates stats. - bool refine_buffer(BufferNode* node, - uint worker_id, - G1ConcurrentRefineStats* stats); - - // Deal with buffer after a call to refine_buffer. If fully processed, - // deallocate the buffer. Otherwise, record it as paused. - void handle_refined_buffer(BufferNode* node, bool fully_processed); - - // Thread-safe attempt to remove and return the first buffer from - // the _completed queue. - // Returns null if the queue is empty, or if a concurrent push/append - // interferes. It uses GlobalCounter critical section to avoid ABA problem. - BufferNode* dequeue_completed_buffer(); - // Remove and return a completed buffer from the list, or return null - // if none available. - BufferNode* get_completed_buffer(); - - // Called when queue is full or has no buffer. - void handle_zero_index(G1DirtyCardQueue& queue); - - // Enqueue the buffer, and optionally perform refinement by the mutator. - // Mutator refinement is only done by Java threads, and only if there - // are more than mutator_refinement_threshold cards in the completed buffers. - // Updates stats. - // - // Mutator refinement, if performed, stops processing a buffer if - // SuspendibleThreadSet::should_yield(), recording the incompletely - // processed buffer for later processing of the remainder. - void handle_completed_buffer(BufferNode* node, G1ConcurrentRefineStats* stats); - -public: - G1DirtyCardQueueSet(BufferNode::Allocator* allocator); - ~G1DirtyCardQueueSet(); - - // The number of parallel ids that can be claimed to allow collector or - // mutator threads to do card-processing work. - static uint num_par_ids(); - - static void handle_zero_index_for_thread(Thread* t); - - virtual void enqueue_completed_buffer(BufferNode* node); - - // Upper bound on the number of cards currently in this queue set. - // Read without synchronization. The value may be high because there - // is a concurrent modification of the set of buffers. - size_t num_cards() const; - - void merge_bufferlists(G1RedirtyCardsQueueSet* src); - - BufferNodeList take_all_completed_buffers(); - - void flush_queue(G1DirtyCardQueue& queue); - - using CardValue = G1CardTable::CardValue; - void enqueue(G1DirtyCardQueue& queue, volatile CardValue* card_ptr); - - // If there are more than stop_at cards in the completed buffers, pop - // a buffer, refine its contents, and return true. Otherwise return - // false. Updates stats. - // - // Stops processing a buffer if SuspendibleThreadSet::should_yield(), - // recording the incompletely processed buffer for later processing of - // the remainder. - bool refine_completed_buffer_concurrently(uint worker_id, - size_t stop_at, - G1ConcurrentRefineStats* stats); - - // If a full collection is happening, reset per-thread refinement stats and - // partial logs, and release completed logs. The full collection will make - // them all irrelevant. - // precondition: at safepoint. - void abandon_logs_and_stats(); - - // Update global refinement statistics with the ones given and the ones from - // detached threads. - // precondition: at safepoint. - void update_refinement_stats(G1ConcurrentRefineStats& stats); - // Add the given thread's partial logs to the global list and return and reset - // its refinement stats. - // precondition: at safepoint. - G1ConcurrentRefineStats concatenate_log_and_stats(Thread* thread); - - // Return the total of mutator refinement stats for all threads. - // precondition: at safepoint. - // precondition: only call after concatenate_logs_and_stats. - G1ConcurrentRefineStats concatenated_refinement_stats() const; - - // Accumulate refinement stats from threads that are detaching. - void record_detached_refinement_stats(G1ConcurrentRefineStats* stats); - - // Number of cards above which mutator threads should do refinement. - size_t mutator_refinement_threshold() const; - - // Set number of cards above which mutator threads should do refinement. - void set_mutator_refinement_threshold(size_t value); -}; - -#endif // SHARE_GC_G1_G1DIRTYCARDQUEUE_HPP diff --git a/src/hotspot/share/gc/g1/g1FromCardCache.cpp b/src/hotspot/share/gc/g1/g1FromCardCache.cpp index 4a29bcbc6dc..8f5c84da0e3 100644 --- a/src/hotspot/share/gc/g1/g1FromCardCache.cpp +++ b/src/hotspot/share/gc/g1/g1FromCardCache.cpp @@ -22,8 +22,6 @@ * */ -#include "gc/g1/g1ConcurrentRefine.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1FromCardCache.hpp" #include "gc/shared/gc_globals.hpp" #include "memory/padded.inline.hpp" @@ -80,7 +78,7 @@ void G1FromCardCache::print(outputStream* out) { #endif uint G1FromCardCache::num_par_rem_sets() { - return G1DirtyCardQueueSet::num_par_ids() + G1ConcRefinementThreads + MAX2(ConcGCThreads, ParallelGCThreads); + return G1ConcRefinementThreads + ConcGCThreads; } void G1FromCardCache::clear(uint region_idx) { diff --git a/src/hotspot/share/gc/g1/g1FullGCCompactTask.cpp b/src/hotspot/share/gc/g1/g1FullGCCompactTask.cpp index cc71cf86172..5dbf70f36b3 100644 --- a/src/hotspot/share/gc/g1/g1FullGCCompactTask.cpp +++ b/src/hotspot/share/gc/g1/g1FullGCCompactTask.cpp @@ -147,6 +147,10 @@ void G1FullGCCompactTask::free_non_overlapping_regions(uint src_start_idx, uint for (uint i = non_overlapping_start; i <= src_end_idx; ++i) { G1HeapRegion* hr = _g1h->region_at(i); + if (VerifyDuringGC) { + // Satisfy some asserts in free_..._region + hr->clear_both_card_tables(); + } _g1h->free_humongous_region(hr, nullptr); } } diff --git a/src/hotspot/share/gc/g1/g1FullGCPrepareTask.inline.hpp b/src/hotspot/share/gc/g1/g1FullGCPrepareTask.inline.hpp index f9868bba678..64d85660ca7 100644 --- a/src/hotspot/share/gc/g1/g1FullGCPrepareTask.inline.hpp +++ b/src/hotspot/share/gc/g1/g1FullGCPrepareTask.inline.hpp @@ -35,6 +35,10 @@ #include "gc/shared/fullGCForwarding.inline.hpp" void G1DetermineCompactionQueueClosure::free_empty_humongous_region(G1HeapRegion* hr) { + if (VerifyDuringGC) { + // Satisfy some asserts in free_..._region. + hr->clear_both_card_tables(); + } _g1h->free_humongous_region(hr, nullptr); _collector->set_free(hr->hrm_index()); add_to_compaction_queue(hr); diff --git a/src/hotspot/share/gc/g1/g1FullGCResetMetadataTask.cpp b/src/hotspot/share/gc/g1/g1FullGCResetMetadataTask.cpp index ae9a78a9cdf..02397392a6e 100644 --- a/src/hotspot/share/gc/g1/g1FullGCResetMetadataTask.cpp +++ b/src/hotspot/share/gc/g1/g1FullGCResetMetadataTask.cpp @@ -32,7 +32,7 @@ G1FullGCResetMetadataTask::G1ResetMetadataClosure::G1ResetMetadataClosure(G1Full void G1FullGCResetMetadataTask::G1ResetMetadataClosure::reset_region_metadata(G1HeapRegion* hr) { hr->rem_set()->clear(); - hr->clear_cardtable(); + hr->clear_both_card_tables(); } bool G1FullGCResetMetadataTask::G1ResetMetadataClosure::do_heap_region(G1HeapRegion* hr) { diff --git a/src/hotspot/share/gc/g1/g1GCPhaseTimes.cpp b/src/hotspot/share/gc/g1/g1GCPhaseTimes.cpp index 15fb65c5700..b211b1e32fb 100644 --- a/src/hotspot/share/gc/g1/g1GCPhaseTimes.cpp +++ b/src/hotspot/share/gc/g1/g1GCPhaseTimes.cpp @@ -50,8 +50,7 @@ G1GCPhaseTimes::G1GCPhaseTimes(STWGCTimer* gc_timer, uint max_gc_threads) : { assert(max_gc_threads > 0, "Must have some GC threads"); - _gc_par_phases[RetireTLABsAndFlushLogs] = new WorkerDataArray("RetireTLABsAndFlushLogs", "JT Retire TLABs And Flush Logs (ms):", max_gc_threads); - _gc_par_phases[NonJavaThreadFlushLogs] = new WorkerDataArray("NonJavaThreadFlushLogs", "Non-JT Flush Logs (ms):", max_gc_threads); + _gc_par_phases[RetireTLABs] = new WorkerDataArray("RetireTLABs", "JavaThread Retire TLABs (ms):", max_gc_threads); _gc_par_phases[GCWorkerStart] = new WorkerDataArray("GCWorkerStart", "GC Worker Start (ms):", max_gc_threads); _gc_par_phases[ExtRootScan] = new WorkerDataArray("ExtRootScan", "Ext Root Scanning (ms):", max_gc_threads); @@ -83,7 +82,7 @@ G1GCPhaseTimes::G1GCPhaseTimes(STWGCTimer* gc_timer, uint max_gc_threads) : _gc_par_phases[OptMergeRS]->create_thread_work_items(GCMergeRSWorkItemsStrings[i], i); } - _gc_par_phases[MergeLB] = new WorkerDataArray("MergeLB", "Log Buffers (ms):", max_gc_threads); + _gc_par_phases[SweepRT] = new WorkerDataArray("SweepRT", "Sweep (ms):", max_gc_threads); _gc_par_phases[ScanHR] = new WorkerDataArray("ScanHR", "Scan Heap Roots (ms):", max_gc_threads); _gc_par_phases[OptScanHR] = new WorkerDataArray("OptScanHR", "Optional Scan Heap Roots (ms):", max_gc_threads); _gc_par_phases[CodeRoots] = new WorkerDataArray("CodeRoots", "Code Root Scan (ms):", max_gc_threads); @@ -98,7 +97,7 @@ G1GCPhaseTimes::G1GCPhaseTimes(STWGCTimer* gc_timer, uint max_gc_threads) : _gc_par_phases[MergePSS] = new WorkerDataArray("MergePSS", "Merge Per-Thread State (ms):", max_gc_threads); _gc_par_phases[RestoreEvacuationFailedRegions] = new WorkerDataArray("RestoreEvacuationFailedRegions", "Restore Evacuation Failed Regions (ms):", max_gc_threads); _gc_par_phases[RemoveSelfForwards] = new WorkerDataArray("RemoveSelfForwards", "Remove Self Forwards (ms):", max_gc_threads); - _gc_par_phases[ClearCardTable] = new WorkerDataArray("ClearLoggedCards", "Clear Logged Cards (ms):", max_gc_threads); + _gc_par_phases[ClearCardTable] = new WorkerDataArray("ClearPendingCards", "Clear Pending Cards (ms):", max_gc_threads); _gc_par_phases[RecalculateUsed] = new WorkerDataArray("RecalculateUsed", "Recalculate Used Memory (ms):", max_gc_threads); #if COMPILER2_OR_JVMCI _gc_par_phases[UpdateDerivedPointers] = new WorkerDataArray("UpdateDerivedPointers", "Update Derived Pointers (ms):", max_gc_threads); @@ -107,11 +106,15 @@ G1GCPhaseTimes::G1GCPhaseTimes(STWGCTimer* gc_timer, uint max_gc_threads) : _gc_par_phases[ResetPartialArrayStateManager] = new WorkerDataArray("ResetPartialArrayStateManager", "Reset Partial Array State Manager (ms):", max_gc_threads); _gc_par_phases[ProcessEvacuationFailedRegions] = new WorkerDataArray("ProcessEvacuationFailedRegions", "Process Evacuation Failed Regions (ms):", max_gc_threads); + _gc_par_phases[ScanHR]->create_thread_work_items("Pending Cards:", ScanHRPendingCards); + _gc_par_phases[ScanHR]->create_thread_work_items("Scanned Empty:", ScanHRScannedEmptyCards); _gc_par_phases[ScanHR]->create_thread_work_items("Scanned Cards:", ScanHRScannedCards); _gc_par_phases[ScanHR]->create_thread_work_items("Scanned Blocks:", ScanHRScannedBlocks); _gc_par_phases[ScanHR]->create_thread_work_items("Claimed Chunks:", ScanHRClaimedChunks); _gc_par_phases[ScanHR]->create_thread_work_items("Found Roots:", ScanHRFoundRoots); + _gc_par_phases[OptScanHR]->create_thread_work_items("Pending Cards:", ScanHRPendingCards); + _gc_par_phases[OptScanHR]->create_thread_work_items("Scanned Empty:", ScanHRScannedEmptyCards); _gc_par_phases[OptScanHR]->create_thread_work_items("Scanned Cards:", ScanHRScannedCards); _gc_par_phases[OptScanHR]->create_thread_work_items("Scanned Blocks:", ScanHRScannedBlocks); _gc_par_phases[OptScanHR]->create_thread_work_items("Claimed Chunks:", ScanHRClaimedChunks); @@ -119,9 +122,6 @@ G1GCPhaseTimes::G1GCPhaseTimes(STWGCTimer* gc_timer, uint max_gc_threads) : _gc_par_phases[OptScanHR]->create_thread_work_items("Scanned Refs:", ScanHRScannedOptRefs); _gc_par_phases[OptScanHR]->create_thread_work_items("Used Memory:", ScanHRUsedMemory); - _gc_par_phases[MergeLB]->create_thread_work_items("Dirty Cards:", MergeLBDirtyCards); - _gc_par_phases[MergeLB]->create_thread_work_items("Skipped Cards:", MergeLBSkippedCards); - _gc_par_phases[CodeRoots]->create_thread_work_items("Scanned Nmethods:", CodeRootsScannedNMethods); _gc_par_phases[OptCodeRoots]->create_thread_work_items("Scanned Nmethods:", CodeRootsScannedNMethods); @@ -129,7 +129,10 @@ G1GCPhaseTimes::G1GCPhaseTimes(STWGCTimer* gc_timer, uint max_gc_threads) : _gc_par_phases[MergePSS]->create_thread_work_items("Copied Bytes:", MergePSSCopiedBytes); _gc_par_phases[MergePSS]->create_thread_work_items("LAB Waste:", MergePSSLABWasteBytes); _gc_par_phases[MergePSS]->create_thread_work_items("LAB Undo Waste:", MergePSSLABUndoWasteBytes); - _gc_par_phases[MergePSS]->create_thread_work_items("Evac Fail Extra Cards:", MergePSSEvacFailExtra); + _gc_par_phases[MergePSS]->create_thread_work_items("Pending Cards:", MergePSSPendingCards); + _gc_par_phases[MergePSS]->create_thread_work_items("To-Young-Gen Cards:", MergePSSToYoungGenCards); + _gc_par_phases[MergePSS]->create_thread_work_items("Evac-Fail Cards:", MergePSSEvacFail); + _gc_par_phases[MergePSS]->create_thread_work_items("Marked Cards:", MergePSSMarked); _gc_par_phases[RestoreEvacuationFailedRegions]->create_thread_work_items("Evacuation Failed Regions:", RestoreEvacFailureRegionsEvacFailedNum); _gc_par_phases[RestoreEvacuationFailedRegions]->create_thread_work_items("Pinned Regions:", RestoreEvacFailureRegionsPinnedNum); @@ -150,9 +153,6 @@ G1GCPhaseTimes::G1GCPhaseTimes(STWGCTimer* gc_timer, uint max_gc_threads) : _gc_par_phases[OptTermination]->create_thread_work_items("Optional Termination Attempts:"); - _gc_par_phases[RedirtyCards] = new WorkerDataArray("RedirtyCards", "Redirty Logged Cards (ms):", max_gc_threads); - _gc_par_phases[RedirtyCards]->create_thread_work_items("Redirtied Cards:"); - _gc_par_phases[ResizeThreadLABs] = new WorkerDataArray("ResizeTLABs", "Resize TLABs (ms):", max_gc_threads); _gc_par_phases[FreeCollectionSet] = new WorkerDataArray("FreeCSet", "Free Collection Set (ms):", max_gc_threads); @@ -171,9 +171,9 @@ void G1GCPhaseTimes::reset() { _cur_optional_evac_time_ms = 0.0; _cur_collection_nmethod_list_cleanup_time_ms = 0.0; _cur_merge_heap_roots_time_ms = 0.0; + _cur_merge_refinement_table_time_ms = 0.0; _cur_optional_merge_heap_roots_time_ms = 0.0; _cur_prepare_merge_heap_roots_time_ms = 0.0; - _cur_distribute_log_buffers_time_ms = 0.0; _cur_optional_prepare_merge_heap_roots_time_ms = 0.0; _cur_pre_evacuate_prepare_time_ms = 0.0; _cur_post_evacuate_cleanup_1_time_ms = 0.0; @@ -249,7 +249,7 @@ void G1GCPhaseTimes::record_gc_pause_end() { ASSERT_PHASE_UNINITIALIZED(MergeER); ASSERT_PHASE_UNINITIALIZED(MergeRS); ASSERT_PHASE_UNINITIALIZED(OptMergeRS); - ASSERT_PHASE_UNINITIALIZED(MergeLB); + ASSERT_PHASE_UNINITIALIZED(SweepRT); ASSERT_PHASE_UNINITIALIZED(ScanHR); ASSERT_PHASE_UNINITIALIZED(CodeRoots); ASSERT_PHASE_UNINITIALIZED(OptCodeRoots); @@ -425,8 +425,7 @@ double G1GCPhaseTimes::print_pre_evacuate_collection_set() const { } debug_time("Pre Evacuate Prepare", _cur_pre_evacuate_prepare_time_ms); - debug_phase(_gc_par_phases[RetireTLABsAndFlushLogs], 1); - debug_phase(_gc_par_phases[NonJavaThreadFlushLogs], 1); + debug_phase(_gc_par_phases[RetireTLABs], 1); debug_time("Choose Collection Set", (_recorded_young_cset_choice_time_ms + _recorded_non_young_cset_choice_time_ms)); debug_time("Region Register", _cur_region_register_time); @@ -458,8 +457,8 @@ double G1GCPhaseTimes::print_evacuate_initial_collection_set() const { debug_time("Prepare Merge Heap Roots", _cur_prepare_merge_heap_roots_time_ms); debug_phase_merge_remset(); - debug_time("Distribute Log Buffers", _cur_distribute_log_buffers_time_ms); - debug_phase(_gc_par_phases[MergeLB]); + debug_time("Merge Refinement Table", _cur_merge_refinement_table_time_ms); + debug_phase(_gc_par_phases[SweepRT], 1); info_time("Evacuate Collection Set", _cur_collection_initial_evac_time_ms); @@ -521,7 +520,6 @@ double G1GCPhaseTimes::print_post_evacuate_collection_set(bool evacuation_failed if (G1CollectedHeap::heap()->should_sample_collection_set_candidates()) { debug_phase(_gc_par_phases[SampleCollectionSetCandidates], 1); } - debug_phase(_gc_par_phases[RedirtyCards], 1); if (UseTLAB && ResizeTLAB) { debug_phase(_gc_par_phases[ResizeThreadLABs], 1); } diff --git a/src/hotspot/share/gc/g1/g1GCPhaseTimes.hpp b/src/hotspot/share/gc/g1/g1GCPhaseTimes.hpp index 045160a6162..8223148b791 100644 --- a/src/hotspot/share/gc/g1/g1GCPhaseTimes.hpp +++ b/src/hotspot/share/gc/g1/g1GCPhaseTimes.hpp @@ -46,8 +46,7 @@ class G1GCPhaseTimes : public CHeapObj { public: enum GCParPhases { - RetireTLABsAndFlushLogs, - NonJavaThreadFlushLogs, + RetireTLABs, GCWorkerStart, ExtRootScan, ThreadRoots, @@ -59,7 +58,7 @@ class G1GCPhaseTimes : public CHeapObj { MergeER = StrongOopStorageSetRoots + EnumRange().size(), MergeRS, OptMergeRS, - MergeLB, + SweepRT, ScanHR, OptScanHR, CodeRoots, @@ -71,7 +70,6 @@ class G1GCPhaseTimes : public CHeapObj { Other, GCWorkerTotal, GCWorkerEnd, - RedirtyCards, FreeCollectionSet, YoungFreeCSet, NonYoungFreeCSet, @@ -111,16 +109,19 @@ class G1GCPhaseTimes : public CHeapObj { MergeRSHowlArrayOfCards, MergeRSHowlBitmap, MergeRSHowlFull, - MergeRSCards, + MergeRSFromRemSetCards, + MergeRSTotalCards, MergeRSContainersSentinel }; static constexpr const char* GCMergeRSWorkItemsStrings[MergeRSContainersSentinel] = { "Merged Inline:", "Merged ArrayOfCards:", "Merged Howl:", "Merged Full:", "Merged Howl Inline:", "Merged Howl ArrayOfCards:", "Merged Howl BitMap:", "Merged Howl Full:", - "Merged Cards:" }; + "Merged From RS Cards:", "Total Cards:" }; enum GCScanHRWorkItems { + ScanHRPendingCards, + ScanHRScannedEmptyCards, ScanHRScannedCards, ScanHRScannedBlocks, ScanHRClaimedChunks, @@ -129,11 +130,6 @@ class G1GCPhaseTimes : public CHeapObj { ScanHRUsedMemory }; - enum GCMergeLBWorkItems { - MergeLBDirtyCards, - MergeLBSkippedCards - }; - enum GCCodeRootsWorkItems { CodeRootsScannedNMethods }; @@ -143,7 +139,10 @@ class G1GCPhaseTimes : public CHeapObj { MergePSSLABSize, MergePSSLABWasteBytes, MergePSSLABUndoWasteBytes, - MergePSSEvacFailExtra + MergePSSPendingCards, // To be scanned cards generated by GC (from cross-references and evacuation failure). + MergePSSToYoungGenCards, // To-young-gen cards generated by GC. + MergePSSEvacFail, // Evacuation failure generated dirty cards by GC. + MergePSSMarked, // Total newly marked cards. }; enum RestoreEvacFailureRegionsWorkItems { @@ -176,9 +175,9 @@ class G1GCPhaseTimes : public CHeapObj { double _cur_collection_nmethod_list_cleanup_time_ms; double _cur_merge_heap_roots_time_ms; + // Merge refinement table time. Note that this time is included in _cur_merge_heap_roots_time_ms. + double _cur_merge_refinement_table_time_ms; double _cur_optional_merge_heap_roots_time_ms; - // Included in above merge and optional-merge time. - double _cur_distribute_log_buffers_time_ms; double _cur_prepare_merge_heap_roots_time_ms; double _cur_optional_prepare_merge_heap_roots_time_ms; @@ -302,6 +301,10 @@ class G1GCPhaseTimes : public CHeapObj { _cur_merge_heap_roots_time_ms += ms; } + void record_merge_refinement_table_time(double ms) { + _cur_merge_refinement_table_time_ms = ms; + } + void record_or_add_optional_merge_heap_roots_time(double ms) { _cur_optional_merge_heap_roots_time_ms += ms; } @@ -310,10 +313,6 @@ class G1GCPhaseTimes : public CHeapObj { _cur_prepare_merge_heap_roots_time_ms += ms; } - void record_distribute_log_buffers_time_ms(double ms) { - _cur_distribute_log_buffers_time_ms += ms; - } - void record_or_add_optional_prepare_merge_heap_roots_time(double ms) { _cur_optional_prepare_merge_heap_roots_time_ms += ms; } @@ -382,10 +381,6 @@ class G1GCPhaseTimes : public CHeapObj { _recorded_prepare_heap_roots_time_ms = recorded_prepare_heap_roots_time_ms; } - double cur_distribute_log_buffers_time_ms() { - return _cur_distribute_log_buffers_time_ms; - } - double cur_collection_par_time_ms() { return _cur_collection_initial_evac_time_ms + _cur_optional_evac_time_ms + @@ -396,6 +391,10 @@ class G1GCPhaseTimes : public CHeapObj { _cur_collection_nmethod_list_cleanup_time_ms; } + double cur_merge_refinement_table_time() const { + return _cur_merge_refinement_table_time_ms; + } + double cur_resize_heap_time_ms() { return _cur_resize_heap_time_ms; } diff --git a/src/hotspot/share/gc/g1/g1HeapRegion.cpp b/src/hotspot/share/gc/g1/g1HeapRegion.cpp index 09bdfefccb7..ca4359dcc24 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.cpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.cpp @@ -39,6 +39,7 @@ #include "logging/log.hpp" #include "logging/logStream.hpp" #include "memory/iterator.inline.hpp" +#include "memory/memRegion.hpp" #include "memory/resourceArea.hpp" #include "oops/access.inline.hpp" #include "oops/compressedOops.inline.hpp" @@ -137,11 +138,21 @@ void G1HeapRegion::hr_clear(bool clear_space) { if (clear_space) clear(SpaceDecorator::Mangle); } -void G1HeapRegion::clear_cardtable() { +void G1HeapRegion::clear_card_table() { G1CardTable* ct = G1CollectedHeap::heap()->card_table(); ct->clear_MemRegion(MemRegion(bottom(), end())); } +void G1HeapRegion::clear_refinement_table() { + G1CardTable* ct = G1CollectedHeap::heap()->refinement_table(); + ct->clear_MemRegion(MemRegion(bottom(), end())); +} + +void G1HeapRegion::clear_both_card_tables() { + clear_card_table(); + clear_refinement_table(); +} + void G1HeapRegion::set_free() { if (!is_free()) { report_region_type_change(G1HeapRegionTraceType::Free); @@ -591,8 +602,12 @@ class G1VerifyLiveAndRemSetClosure : public BasicOopIterateClosure { G1HeapRegion* _from; G1HeapRegion* _to; - CardValue _cv_obj; - CardValue _cv_field; + + CardValue _cv_obj_ct; // In card table. + CardValue _cv_field_ct; + + CardValue _cv_obj_rt; // In refinement table. + CardValue _cv_field_rt; RemSetChecker(G1VerifyFailureCounter* failures, oop containing_obj, T* p, oop obj) : Checker(failures, containing_obj, p, obj) { @@ -600,19 +615,23 @@ class G1VerifyLiveAndRemSetClosure : public BasicOopIterateClosure { _to = this->_g1h->heap_region_containing(obj); CardTable* ct = this->_g1h->card_table(); - _cv_obj = *ct->byte_for_const(this->_containing_obj); - _cv_field = *ct->byte_for_const(p); + _cv_obj_ct = *ct->byte_for_const(this->_containing_obj); + _cv_field_ct = *ct->byte_for_const(p); + + ct = this->_g1h->refinement_table(); + _cv_obj_rt = *ct->byte_for_const(this->_containing_obj); + _cv_field_rt = *ct->byte_for_const(p); } bool failed() const { if (_from != _to && !_from->is_young() && _to->rem_set()->is_complete() && _from->rem_set()->cset_group() != _to->rem_set()->cset_group()) { - const CardValue dirty = G1CardTable::dirty_card_val(); + const CardValue clean = G1CardTable::clean_card_val(); return !(_to->rem_set()->contains_reference(this->_p) || (this->_containing_obj->is_objArray() ? - _cv_field == dirty : - _cv_obj == dirty || _cv_field == dirty)); + (_cv_field_ct != clean || _cv_field_rt != clean) : + (_cv_obj_ct != clean || _cv_field_ct != clean || _cv_obj_rt != clean || _cv_field_rt != clean))); } return false; } @@ -630,7 +649,8 @@ class G1VerifyLiveAndRemSetClosure : public BasicOopIterateClosure { log.error("Missing rem set entry:"); this->print_containing_obj(&ls, _from); this->print_referenced_obj(&ls, _to, ""); - log.error("Obj head CV = %d, field CV = %d.", _cv_obj, _cv_field); + log.error("CT obj head CV = %d, field CV = %d.", _cv_obj_ct, _cv_field_ct); + log.error("RT Obj head CV = %d, field CV = %d.", _cv_obj_rt, _cv_field_rt); log.error("----------"); } }; diff --git a/src/hotspot/share/gc/g1/g1HeapRegion.hpp b/src/hotspot/share/gc/g1/g1HeapRegion.hpp index 71584ffb24d..17ec3055b52 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegion.hpp +++ b/src/hotspot/share/gc/g1/g1HeapRegion.hpp @@ -42,7 +42,6 @@ class G1CollectedHeap; class G1CMBitMap; class G1CSetCandidateGroup; class G1Predictions; -class G1HeapRegion; class G1HeapRegionRemSet; class G1HeapRegionSetBase; class nmethod; @@ -478,7 +477,10 @@ public: // Callers must ensure this is not called by multiple threads at the same time. void hr_clear(bool clear_space); // Clear the card table corresponding to this region. - void clear_cardtable(); + void clear_card_table(); + void clear_refinement_table(); + + void clear_both_card_tables(); // Notify the region that an evacuation failure occurred for an object within this // region. diff --git a/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp b/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp index d4286a1caeb..795b6543bae 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp +++ b/src/hotspot/share/gc/g1/g1HeapRegionManager.cpp @@ -63,7 +63,8 @@ public: G1HeapRegionManager::G1HeapRegionManager() : _bot_mapper(nullptr), - _cardtable_mapper(nullptr), + _card_table_mapper(nullptr), + _refinement_table_mapper(nullptr), _committed_map(), _next_highest_used_hrm_index(0), _regions(), _heap_mapper(nullptr), @@ -74,7 +75,8 @@ G1HeapRegionManager::G1HeapRegionManager() : void G1HeapRegionManager::initialize(G1RegionToSpaceMapper* heap_storage, G1RegionToSpaceMapper* bitmap, G1RegionToSpaceMapper* bot, - G1RegionToSpaceMapper* cardtable) { + G1RegionToSpaceMapper* card_table, + G1RegionToSpaceMapper* refinement_table) { _next_highest_used_hrm_index = 0; _heap_mapper = heap_storage; @@ -82,7 +84,8 @@ void G1HeapRegionManager::initialize(G1RegionToSpaceMapper* heap_storage, _bitmap_mapper = bitmap; _bot_mapper = bot; - _cardtable_mapper = cardtable; + _card_table_mapper = card_table; + _refinement_table_mapper = refinement_table; _regions.initialize(heap_storage->reserved(), G1HeapRegion::GrainBytes); @@ -186,7 +189,8 @@ void G1HeapRegionManager::commit_regions(uint index, size_t num_regions, WorkerT _bitmap_mapper->commit_regions(index, num_regions, pretouch_workers); _bot_mapper->commit_regions(index, num_regions, pretouch_workers); - _cardtable_mapper->commit_regions(index, num_regions, pretouch_workers); + _card_table_mapper->commit_regions(index, num_regions, pretouch_workers); + _refinement_table_mapper->commit_regions(index, num_regions, pretouch_workers); } void G1HeapRegionManager::uncommit_regions(uint start, uint num_regions) { @@ -209,7 +213,8 @@ void G1HeapRegionManager::uncommit_regions(uint start, uint num_regions) { _bitmap_mapper->uncommit_regions(start, num_regions); _bot_mapper->uncommit_regions(start, num_regions); - _cardtable_mapper->uncommit_regions(start, num_regions); + _card_table_mapper->uncommit_regions(start, num_regions); + _refinement_table_mapper->uncommit_regions(start, num_regions); _committed_map.uncommit(start, end); } @@ -261,19 +266,23 @@ void G1HeapRegionManager::clear_auxiliary_data_structures(uint start, uint num_r // Signal G1BlockOffsetTable to clear the given regions. _bot_mapper->signal_mapping_changed(start, num_regions); // Signal G1CardTable to clear the given regions. - _cardtable_mapper->signal_mapping_changed(start, num_regions); + _card_table_mapper->signal_mapping_changed(start, num_regions); + // Signal refinement table to clear the given regions. + _refinement_table_mapper->signal_mapping_changed(start, num_regions); } MemoryUsage G1HeapRegionManager::get_auxiliary_data_memory_usage() const { size_t used_sz = _bitmap_mapper->committed_size() + _bot_mapper->committed_size() + - _cardtable_mapper->committed_size(); + _card_table_mapper->committed_size() + + _refinement_table_mapper->committed_size(); size_t committed_sz = _bitmap_mapper->reserved_size() + _bot_mapper->reserved_size() + - _cardtable_mapper->reserved_size(); + _card_table_mapper->reserved_size() + + _refinement_table_mapper->reserved_size(); return MemoryUsage(0, used_sz, committed_sz, committed_sz); } diff --git a/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp b/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp index 19ae9887e94..b4ce3b0a8be 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp +++ b/src/hotspot/share/gc/g1/g1HeapRegionManager.hpp @@ -74,7 +74,8 @@ class G1HeapRegionManager: public CHeapObj { friend class G1HeapRegionClaimer; G1RegionToSpaceMapper* _bot_mapper; - G1RegionToSpaceMapper* _cardtable_mapper; + G1RegionToSpaceMapper* _card_table_mapper; + G1RegionToSpaceMapper* _refinement_table_mapper; // Keeps track of the currently committed regions in the heap. The committed regions // can either be active (ready for use) or inactive (ready for uncommit). @@ -161,7 +162,8 @@ public: void initialize(G1RegionToSpaceMapper* heap_storage, G1RegionToSpaceMapper* bitmap, G1RegionToSpaceMapper* bot, - G1RegionToSpaceMapper* cardtable); + G1RegionToSpaceMapper* card_table, + G1RegionToSpaceMapper* refinement_table); // Return the "dummy" region used for G1AllocRegion. This is currently a hardwired // new G1HeapRegion that owns G1HeapRegion at index 0. Since at the moment we commit diff --git a/src/hotspot/share/gc/g1/g1HeapVerifier.cpp b/src/hotspot/share/gc/g1/g1HeapVerifier.cpp index c5af7e34dd9..21b3545f7e0 100644 --- a/src/hotspot/share/gc/g1/g1HeapVerifier.cpp +++ b/src/hotspot/share/gc/g1/g1HeapVerifier.cpp @@ -42,6 +42,7 @@ #include "oops/compressedOops.inline.hpp" #include "oops/oop.inline.hpp" #include "runtime/handles.inline.hpp" +#include "runtime/threads.hpp" int G1HeapVerifier::_enabled_verification_types = G1HeapVerifier::G1VerifyAll; @@ -528,6 +529,7 @@ void G1HeapVerifier::verify_before_gc() { void G1HeapVerifier::verify_after_gc() { verify(VerifyOption::G1UseConcMarking, "After GC"); + verify_card_tables_in_sync(); } void G1HeapVerifier::verify_bitmap_clear(bool from_tams) { @@ -556,17 +558,17 @@ void G1HeapVerifier::verify_bitmap_clear(bool from_tams) { G1CollectedHeap::heap()->heap_region_iterate(&cl); } -#ifndef PRODUCT class G1VerifyCardTableCleanup: public G1HeapRegionClosure { G1HeapVerifier* _verifier; public: G1VerifyCardTableCleanup(G1HeapVerifier* verifier) : _verifier(verifier) { } virtual bool do_heap_region(G1HeapRegion* r) { + _verifier->verify_ct_clean_region(r); if (r->is_survivor()) { - _verifier->verify_dirty_region(r); + _verifier->verify_rt_clean_region(r); } else { - _verifier->verify_not_dirty_region(r); + _verifier->verify_rt_clean_from_top(r); } return false; } @@ -579,14 +581,35 @@ void G1HeapVerifier::verify_card_table_cleanup() { } } -void G1HeapVerifier::verify_not_dirty_region(G1HeapRegion* hr) { - // All of the region should be clean. - G1CardTable* ct = _g1h->card_table(); - MemRegion mr(hr->bottom(), hr->end()); - ct->verify_not_dirty_region(mr); +class G1VerifyCardTablesClean: public G1HeapRegionClosure { + G1HeapVerifier* _verifier; + bool _both_card_tables; + +public: + G1VerifyCardTablesClean(G1HeapVerifier* verifier, bool both_card_tables = true) + : _verifier(verifier), _both_card_tables(both_card_tables) { } + + virtual bool do_heap_region(G1HeapRegion* r) { + _verifier->verify_rt_clean_region(r); // Must be all Clean from bottom -> end. + if (_both_card_tables) { + _verifier->verify_ct_clean_region(r); + } + return false; + } +}; + +void G1HeapVerifier::verify_card_tables_clean(bool both_card_tables) { + G1VerifyCardTablesClean cl(this, both_card_tables); + _g1h->heap_region_iterate(&cl); } -void G1HeapVerifier::verify_dirty_region(G1HeapRegion* hr) { +void G1HeapVerifier::verify_rt_clean_from_top(G1HeapRegion* hr) { + G1CardTable* ct = _g1h->refinement_table(); + MemRegion mr(align_up(hr->top(), G1CardTable::card_size()), hr->end()); + ct->verify_region(mr, G1CardTable::clean_card_val(), true); +} + +void G1HeapVerifier::verify_rt_dirty_to_dummy_top(G1HeapRegion* hr) { // We cannot guarantee that [bottom(),end()] is dirty. Threads // dirty allocated blocks as they allocate them. The thread that // retires each region and replaces it with a new one will do a @@ -594,29 +617,56 @@ void G1HeapVerifier::verify_dirty_region(G1HeapRegion* hr) { // not dirty that area (one less thing to have to do while holding // a lock). So we can only verify that [bottom(),pre_dummy_top()] // is dirty. - G1CardTable* ct = _g1h->card_table(); + G1CardTable* ct = _g1h->refinement_table(); MemRegion mr(hr->bottom(), hr->pre_dummy_top()); - if (hr->is_young()) { - ct->verify_g1_young_region(mr); - } else { - ct->verify_dirty_region(mr); - } + ct->verify_dirty_region(mr); } -class G1VerifyDirtyYoungListClosure : public G1HeapRegionClosure { -private: - G1HeapVerifier* _verifier; -public: - G1VerifyDirtyYoungListClosure(G1HeapVerifier* verifier) : G1HeapRegionClosure(), _verifier(verifier) { } - virtual bool do_heap_region(G1HeapRegion* r) { - _verifier->verify_dirty_region(r); - return false; - } -}; +void G1HeapVerifier::verify_ct_clean_region(G1HeapRegion* hr) { + G1CardTable* ct = _g1h->card_table(); + MemRegion mr(hr->bottom(), hr->end()); + ct->verify_region(mr, G1CardTable::clean_card_val(), true); +} -void G1HeapVerifier::verify_dirty_young_regions() { - G1VerifyDirtyYoungListClosure cl(this); - _g1h->collection_set()->iterate(&cl); +void G1HeapVerifier::verify_rt_clean_region(G1HeapRegion* hr) { + G1CardTable* ct = _g1h->refinement_table(); + MemRegion mr(hr->bottom(), hr->end()); + ct->verify_region(mr, G1CardTable::clean_card_val(), true); +} + +#ifndef PRODUCT + +void G1HeapVerifier::verify_card_tables_in_sync() { + + // Non-Java thread card tables must be null. + class AssertCardTableBaseNull : public ThreadClosure { + public: + + void do_thread(Thread* thread) { + ResourceMark rm; + assert(G1ThreadLocalData::get_byte_map_base(thread) == nullptr, "thread " PTR_FORMAT " (%s) has non-null card table base", + p2i(thread), thread->name()); + } + } check_null_cl; + + Threads::non_java_threads_do(&check_null_cl); + + // Java thread card tables must be the same as the global card table. + class AssertSameCardTableClosure : public ThreadClosure { + public: + + void do_thread(Thread* thread) { + G1CardTable::CardValue* global_ct_base = G1CollectedHeap::heap()->card_table_base(); + G1CardTable::CardValue* cur_ct_base = G1ThreadLocalData::get_byte_map_base(thread); + + ResourceMark rm; + assert(cur_ct_base == global_ct_base, + "thread " PTR_FORMAT " (%s) has wrong card table base, should be " PTR_FORMAT " is " PTR_FORMAT, + p2i(thread), thread->name(), p2i(global_ct_base), p2i(cur_ct_base)); + } + } check_same_cl; + + Threads::java_threads_do(&check_same_cl); } class G1CheckRegionAttrTableClosure : public G1HeapRegionClosure { diff --git a/src/hotspot/share/gc/g1/g1HeapVerifier.hpp b/src/hotspot/share/gc/g1/g1HeapVerifier.hpp index d4ab4c60214..6a26c77ec0d 100644 --- a/src/hotspot/share/gc/g1/g1HeapVerifier.hpp +++ b/src/hotspot/share/gc/g1/g1HeapVerifier.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -78,11 +78,16 @@ public: // Do sanity check on the contents of the in-cset fast test table. bool check_region_attr_table() PRODUCT_RETURN_( return true; ); - void verify_card_table_cleanup() PRODUCT_RETURN; + void verify_card_table_cleanup(); + void verify_card_tables_clean(bool both_card_tables); - void verify_not_dirty_region(G1HeapRegion* hr) PRODUCT_RETURN; - void verify_dirty_region(G1HeapRegion* hr) PRODUCT_RETURN; - void verify_dirty_young_regions() PRODUCT_RETURN; + void verify_ct_clean_region(G1HeapRegion* hr); + void verify_rt_dirty_to_dummy_top(G1HeapRegion* hr); + void verify_rt_clean_from_top(G1HeapRegion* hr); + void verify_rt_clean_region(G1HeapRegion* hr); + + // Verify that the global card table and the thread's card tables are in sync. + void verify_card_tables_in_sync() PRODUCT_RETURN; }; #endif // SHARE_GC_G1_G1HEAPVERIFIER_HPP diff --git a/src/hotspot/share/gc/g1/g1OopClosures.hpp b/src/hotspot/share/gc/g1/g1OopClosures.hpp index 3bff668bcec..a61c9d17f70 100644 --- a/src/hotspot/share/gc/g1/g1OopClosures.hpp +++ b/src/hotspot/share/gc/g1/g1OopClosures.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -86,19 +86,19 @@ public: // This closure is applied to the fields of the objects that have just been copied during evacuation. class G1ScanEvacuatedObjClosure : public G1ScanClosureBase { - friend class G1SkipCardEnqueueSetter; + friend class G1SkipCardMarkSetter; - enum SkipCardEnqueueTristate { + enum SkipCardMarkTristate { False = 0, True, Uninitialized }; - SkipCardEnqueueTristate _skip_card_enqueue; + SkipCardMarkTristate _skip_card_mark; public: G1ScanEvacuatedObjClosure(G1CollectedHeap* g1h, G1ParScanThreadState* par_scan_state) : - G1ScanClosureBase(g1h, par_scan_state), _skip_card_enqueue(Uninitialized) { } + G1ScanClosureBase(g1h, par_scan_state), _skip_card_mark(Uninitialized) { } template void do_oop_work(T* p); virtual void do_oop(oop* p) { do_oop_work(p); } @@ -109,22 +109,22 @@ public: } #ifdef ASSERT - bool skip_card_enqueue_set() const { return _skip_card_enqueue != Uninitialized; } + bool skip_card_mark_set() const { return _skip_card_mark != Uninitialized; } #endif }; -// RAII object to properly set the _skip_card_enqueue field in G1ScanEvacuatedObjClosure. -class G1SkipCardEnqueueSetter : public StackObj { +// RAII object to properly set the _skip_card_mark field in G1ScanEvacuatedObjClosure. +class G1SkipCardMarkSetter : public StackObj { G1ScanEvacuatedObjClosure* _closure; public: - G1SkipCardEnqueueSetter(G1ScanEvacuatedObjClosure* closure, bool skip_card_enqueue) : _closure(closure) { - assert(_closure->_skip_card_enqueue == G1ScanEvacuatedObjClosure::Uninitialized, "Must not be set"); - _closure->_skip_card_enqueue = skip_card_enqueue ? G1ScanEvacuatedObjClosure::True : G1ScanEvacuatedObjClosure::False; + G1SkipCardMarkSetter(G1ScanEvacuatedObjClosure* closure, bool skip_card_mark) : _closure(closure) { + assert(_closure->_skip_card_mark == G1ScanEvacuatedObjClosure::Uninitialized, "Must not be set"); + _closure->_skip_card_mark = skip_card_mark ? G1ScanEvacuatedObjClosure::True : G1ScanEvacuatedObjClosure::False; } - ~G1SkipCardEnqueueSetter() { - DEBUG_ONLY(_closure->_skip_card_enqueue = G1ScanEvacuatedObjClosure::Uninitialized;) + ~G1SkipCardMarkSetter() { + DEBUG_ONLY(_closure->_skip_card_mark = G1ScanEvacuatedObjClosure::Uninitialized;) } }; @@ -206,13 +206,20 @@ public: class G1ConcurrentRefineOopClosure: public BasicOopIterateClosure { G1CollectedHeap* _g1h; uint _worker_id; + bool _has_ref_to_cset; + bool _has_ref_to_old; public: G1ConcurrentRefineOopClosure(G1CollectedHeap* g1h, uint worker_id) : _g1h(g1h), - _worker_id(worker_id) { + _worker_id(worker_id), + _has_ref_to_cset(false), + _has_ref_to_old(false) { } + bool has_ref_to_cset() const { return _has_ref_to_cset; } + bool has_ref_to_old() const { return _has_ref_to_old; } + virtual ReferenceIterationMode reference_iteration_mode() { return DO_FIELDS; } template void do_oop_work(T* p); @@ -223,6 +230,7 @@ public: class G1RebuildRemSetClosure : public BasicOopIterateClosure { G1CollectedHeap* _g1h; uint _worker_id; + public: G1RebuildRemSetClosure(G1CollectedHeap* g1h, uint worker_id) : _g1h(g1h), _worker_id(worker_id) { } diff --git a/src/hotspot/share/gc/g1/g1OopClosures.inline.hpp b/src/hotspot/share/gc/g1/g1OopClosures.inline.hpp index c0c67fda949..87e3a1cc7c4 100644 --- a/src/hotspot/share/gc/g1/g1OopClosures.inline.hpp +++ b/src/hotspot/share/gc/g1/g1OopClosures.inline.hpp @@ -90,11 +90,11 @@ inline void G1ScanEvacuatedObjClosure::do_oop_work(T* p) { prefetch_and_push(p, obj); } else if (!G1HeapRegion::is_in_same_region(p, obj)) { handle_non_cset_obj_common(region_attr, p, obj); - assert(_skip_card_enqueue != Uninitialized, "Scan location has not been initialized."); - if (_skip_card_enqueue == True) { + assert(_skip_card_mark != Uninitialized, "Scan location has not been initialized."); + if (_skip_card_mark == True) { return; } - _par_scan_state->enqueue_card_if_tracked(region_attr, p, obj); + _par_scan_state->mark_card_if_tracked(region_attr, p, obj); } } @@ -127,6 +127,11 @@ inline static void check_obj_during_refinement(T* p, oop const obj) { template inline void G1ConcurrentRefineOopClosure::do_oop_work(T* p) { + // Early out if we already found a to-young reference. + if (_has_ref_to_cset) { + return; + } + T o = RawAccess::oop_load(p); if (CompressedOops::is_null(o)) { return; @@ -146,7 +151,12 @@ inline void G1ConcurrentRefineOopClosure::do_oop_work(T* p) { return; } - G1HeapRegionRemSet* to_rem_set = _g1h->heap_region_containing(obj)->rem_set(); + G1HeapRegion* to_region = _g1h->heap_region_containing(obj); + if (to_region->is_young()) { + _has_ref_to_cset = true; + return; + } + G1HeapRegionRemSet* to_rem_set = to_region->rem_set(); assert(to_rem_set != nullptr, "Need per-region 'into' remsets."); if (to_rem_set->is_tracked()) { @@ -154,6 +164,7 @@ inline void G1ConcurrentRefineOopClosure::do_oop_work(T* p) { if (from->rem_set()->cset_group() != to_rem_set->cset_group()) { to_rem_set->add_reference(p, _worker_id); + _has_ref_to_old = true; } } } @@ -180,7 +191,7 @@ inline void G1ScanCardClosure::do_oop_work(T* p) { _heap_roots_found++; } else if (!G1HeapRegion::is_in_same_region(p, obj)) { handle_non_cset_obj_common(region_attr, p, obj); - _par_scan_state->enqueue_card_if_tracked(region_attr, p, obj); + _par_scan_state->mark_card_if_tracked(region_attr, p, obj); } } @@ -272,10 +283,14 @@ template void G1RebuildRemSetClosure::do_oop_work(T* p) { G1HeapRegion* to = _g1h->heap_region_containing(obj); G1HeapRegionRemSet* rem_set = to->rem_set(); if (rem_set->is_tracked()) { - G1HeapRegion* from = _g1h->heap_region_containing(p); + if (to->is_young()) { + G1BarrierSet::g1_barrier_set()->write_ref_field_post(p); + } else { + G1HeapRegion* from = _g1h->heap_region_containing(p); - if (from->rem_set()->cset_group() != rem_set->cset_group()) { - rem_set->add_reference(p, _worker_id); + if (from->rem_set()->cset_group() != rem_set->cset_group()) { + rem_set->add_reference(p, _worker_id); + } } } } diff --git a/src/hotspot/share/gc/g1/g1ParScanThreadState.cpp b/src/hotspot/share/gc/g1/g1ParScanThreadState.cpp index 42c3a872e6b..80e5fd44fcd 100644 --- a/src/hotspot/share/gc/g1/g1ParScanThreadState.cpp +++ b/src/hotspot/share/gc/g1/g1ParScanThreadState.cpp @@ -57,22 +57,21 @@ #define MAYBE_INLINE_EVACUATION NOT_DEBUG(inline) DEBUG_ONLY(NOINLINE) G1ParScanThreadState::G1ParScanThreadState(G1CollectedHeap* g1h, - G1RedirtyCardsQueueSet* rdcqs, uint worker_id, uint num_workers, G1CollectionSet* collection_set, G1EvacFailureRegions* evac_failure_regions) : _g1h(g1h), _task_queue(g1h->task_queue(worker_id)), - _rdc_local_qset(rdcqs), - _ct(g1h->card_table()), + _ct(g1h->refinement_table()), _closures(nullptr), _plab_allocator(nullptr), _age_table(false), _tenuring_threshold(g1h->policy()->tenuring_threshold()), _scanner(g1h, this), _worker_id(worker_id), - _last_enqueued_card(SIZE_MAX), + _num_cards_marked_dirty(0), + _num_cards_marked_to_cset(0), _stack_trim_upper_threshold(GCDrainStackTargetSize * 2 + 1), _stack_trim_lower_threshold(GCDrainStackTargetSize), _trim_ticks(), @@ -88,7 +87,7 @@ G1ParScanThreadState::G1ParScanThreadState(G1CollectedHeap* g1h, ALLOCATION_FAILURE_INJECTOR_ONLY(_allocation_failure_inject_counter(0) COMMA) _evacuation_failed_info(), _evac_failure_regions(evac_failure_regions), - _evac_failure_enqueued_cards(0) + _num_cards_from_evac_failure(0) { // We allocate number of young gen regions in the collection set plus one // entries, since entry 0 keeps track of surviving bytes for non-young regions. @@ -112,8 +111,7 @@ G1ParScanThreadState::G1ParScanThreadState(G1CollectedHeap* g1h, initialize_numa_stats(); } -size_t G1ParScanThreadState::flush_stats(size_t* surviving_young_words, uint num_workers, BufferNodeList* rdc_buffers) { - *rdc_buffers = _rdc_local_qset.flush(); +size_t G1ParScanThreadState::flush_stats(size_t* surviving_young_words, uint num_workers) { flush_numa_stats(); // Update allocation statistics. _plab_allocator->flush_and_retire_stats(num_workers); @@ -147,8 +145,16 @@ size_t G1ParScanThreadState::lab_undo_waste_words() const { return _plab_allocator->undo_waste(); } -size_t G1ParScanThreadState::evac_failure_enqueued_cards() const { - return _evac_failure_enqueued_cards; +size_t G1ParScanThreadState::num_cards_pending() const { + return _num_cards_marked_dirty + _num_cards_from_evac_failure; +} + +size_t G1ParScanThreadState::num_cards_marked() const { + return num_cards_pending() + _num_cards_marked_to_cset; +} + +size_t G1ParScanThreadState::num_cards_from_evac_failure() const { + return _num_cards_from_evac_failure; } #ifdef ASSERT @@ -230,7 +236,7 @@ void G1ParScanThreadState::do_partial_array(PartialArrayState* state, bool stole PartialArraySplitter::Claim claim = _partial_array_splitter.claim(state, _task_queue, stolen); G1HeapRegionAttr dest_attr = _g1h->region_attr(to_array); - G1SkipCardEnqueueSetter x(&_scanner, dest_attr.is_new_survivor()); + G1SkipCardMarkSetter x(&_scanner, dest_attr.is_new_survivor()); // Process claimed task. to_array->oop_iterate_range(&_scanner, checked_cast(claim._start), @@ -250,7 +256,7 @@ void G1ParScanThreadState::start_partial_objarray(oop from_obj, // The source array is unused when processing states. _partial_array_splitter.start(_task_queue, nullptr, to_array, array_length); - assert(_scanner.skip_card_enqueue_set(), "must be"); + assert(_scanner.skip_card_mark_set(), "must be"); // Process the initial chunk. No need to process the type in the // klass, as it will already be handled by processing the built-in // module. @@ -451,7 +457,7 @@ void G1ParScanThreadState::do_iterate_object(oop const obj, _string_dedup_requests.add(old); } - assert(_scanner.skip_card_enqueue_set(), "must be"); + assert(_scanner.skip_card_mark_set(), "must be"); obj->oop_iterate_backwards(&_scanner, klass); } @@ -546,7 +552,7 @@ oop G1ParScanThreadState::do_copy_to_survivor_space(G1HeapRegionAttr const regio // Instead, we use dest_attr.is_young() because the two values are always // equal: successfully allocated young regions must be survivor regions. assert(dest_attr.is_young() == _g1h->heap_region_containing(obj)->is_survivor(), "must be"); - G1SkipCardEnqueueSetter x(&_scanner, dest_attr.is_young()); + G1SkipCardMarkSetter x(&_scanner, dest_attr.is_young()); do_iterate_object(obj, old, klass, region_attr, dest_attr, age); } @@ -569,7 +575,7 @@ G1ParScanThreadState* G1ParScanThreadStateSet::state_for_worker(uint worker_id) assert(worker_id < _num_workers, "out of bounds access"); if (_states[worker_id] == nullptr) { _states[worker_id] = - new G1ParScanThreadState(_g1h, rdcqs(), + new G1ParScanThreadState(_g1h, worker_id, _num_workers, _collection_set, @@ -595,22 +601,24 @@ void G1ParScanThreadStateSet::flush_stats() { // because it resets the PLAB allocator where we get this info from. size_t lab_waste_bytes = pss->lab_waste_words() * HeapWordSize; size_t lab_undo_waste_bytes = pss->lab_undo_waste_words() * HeapWordSize; - size_t copied_bytes = pss->flush_stats(_surviving_young_words_total, _num_workers, &_rdc_buffers[worker_id]) * HeapWordSize; - size_t evac_fail_enqueued_cards = pss->evac_failure_enqueued_cards(); + size_t copied_bytes = pss->flush_stats(_surviving_young_words_total, _num_workers) * HeapWordSize; + size_t pending_cards = pss->num_cards_pending(); + size_t to_young_gen_cards = pss->num_cards_marked() - pss->num_cards_pending(); + size_t evac_failure_cards = pss->num_cards_from_evac_failure(); + size_t marked_cards = pss->num_cards_marked(); p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, copied_bytes, G1GCPhaseTimes::MergePSSCopiedBytes); p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, lab_waste_bytes, G1GCPhaseTimes::MergePSSLABWasteBytes); p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, lab_undo_waste_bytes, G1GCPhaseTimes::MergePSSLABUndoWasteBytes); - p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, evac_fail_enqueued_cards, G1GCPhaseTimes::MergePSSEvacFailExtra); + p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, pending_cards, G1GCPhaseTimes::MergePSSPendingCards); + p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, to_young_gen_cards, G1GCPhaseTimes::MergePSSToYoungGenCards); + p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, evac_failure_cards, G1GCPhaseTimes::MergePSSEvacFail); + p->record_or_add_thread_work_item(G1GCPhaseTimes::MergePSS, worker_id, marked_cards, G1GCPhaseTimes::MergePSSMarked); delete pss; _states[worker_id] = nullptr; } - G1DirtyCardQueueSet& dcq = G1BarrierSet::dirty_card_queue_set(); - dcq.merge_bufferlists(rdcqs()); - rdcqs()->verify_empty(); - _flushed = true; } @@ -652,7 +660,7 @@ oop G1ParScanThreadState::handle_evacuation_failure_par(oop old, markWord m, Kla // existing closure to scan evacuated objects; since we are iterating from a // collection set region (i.e. never a Survivor region), we always need to // gather cards for this case. - G1SkipCardEnqueueSetter x(&_scanner, false /* skip_card_enqueue */); + G1SkipCardMarkSetter x(&_scanner, false /* skip_card_mark */); do_iterate_object(old, old, klass, attr, attr, m.age()); } @@ -709,9 +717,7 @@ G1ParScanThreadStateSet::G1ParScanThreadStateSet(G1CollectedHeap* g1h, G1EvacFailureRegions* evac_failure_regions) : _g1h(g1h), _collection_set(collection_set), - _rdcqs(G1BarrierSet::dirty_card_queue_set().allocator()), _states(NEW_C_HEAP_ARRAY(G1ParScanThreadState*, num_workers, mtGC)), - _rdc_buffers(NEW_C_HEAP_ARRAY(BufferNodeList, num_workers, mtGC)), _surviving_young_words_total(NEW_C_HEAP_ARRAY(size_t, collection_set->young_region_length() + 1, mtGC)), _num_workers(num_workers), _flushed(false), @@ -719,7 +725,6 @@ G1ParScanThreadStateSet::G1ParScanThreadStateSet(G1CollectedHeap* g1h, { for (uint i = 0; i < num_workers; ++i) { _states[i] = nullptr; - _rdc_buffers[i] = BufferNodeList(); } memset(_surviving_young_words_total, 0, (collection_set->young_region_length() + 1) * sizeof(size_t)); } @@ -728,7 +733,6 @@ G1ParScanThreadStateSet::~G1ParScanThreadStateSet() { assert(_flushed, "thread local state from the per thread states should have been flushed"); FREE_C_HEAP_ARRAY(G1ParScanThreadState*, _states); FREE_C_HEAP_ARRAY(size_t, _surviving_young_words_total); - FREE_C_HEAP_ARRAY(BufferNodeList, _rdc_buffers); } #if TASKQUEUE_STATS diff --git a/src/hotspot/share/gc/g1/g1ParScanThreadState.hpp b/src/hotspot/share/gc/g1/g1ParScanThreadState.hpp index 4d569622238..3fb080d40be 100644 --- a/src/hotspot/share/gc/g1/g1ParScanThreadState.hpp +++ b/src/hotspot/share/gc/g1/g1ParScanThreadState.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ #include "gc/g1/g1CollectedHeap.hpp" #include "gc/g1/g1OopClosures.hpp" -#include "gc/g1/g1RedirtyCardsQueue.hpp" #include "gc/g1/g1YoungGCAllocationFailureInjector.hpp" #include "gc/shared/ageTable.hpp" #include "gc/shared/copyFailedInfo.hpp" @@ -52,7 +51,6 @@ class outputStream; class G1ParScanThreadState : public CHeapObj { G1CollectedHeap* _g1h; G1ScannerTasksQueue* _task_queue; - G1RedirtyCardsLocalQueueSet _rdc_local_qset; G1CardTable* _ct; G1EvacuationRootClosures* _closures; @@ -65,9 +63,8 @@ class G1ParScanThreadState : public CHeapObj { uint _worker_id; - // Remember the last enqueued card to avoid enqueuing the same card over and over; - // since we only ever scan a card once, this is sufficient. - size_t _last_enqueued_card; + size_t _num_cards_marked_dirty; + size_t _num_cards_marked_to_cset; // Upper and lower threshold to start and end work queue draining. uint const _stack_trim_upper_threshold; @@ -104,22 +101,19 @@ class G1ParScanThreadState : public CHeapObj { EvacuationFailedInfo _evacuation_failed_info; G1EvacFailureRegions* _evac_failure_regions; - // Number of additional cards into evacuation failed regions enqueued into - // the local DCQS. This is an approximation, as cards that would be added later - // outside of evacuation failure will not be subtracted again. - size_t _evac_failure_enqueued_cards; + // Number of additional cards into evacuation failed regions. + size_t _num_cards_from_evac_failure; - // Enqueue the card if not already in the set; this is a best-effort attempt on + // Mark the card if not already in the set; this is a best-effort attempt on // detecting duplicates. - template bool enqueue_if_new(T* p); - // Enqueue the card of p into the (evacuation failed) region. - template void enqueue_card_into_evac_fail_region(T* p, oop obj); + template bool mark_if_new(T* p, bool into_survivor); + // Mark the card of p into the (evacuation failed) region. + template void mark_card_into_evac_fail_region(T* p, oop obj); bool inject_allocation_failure(uint region_idx) ALLOCATION_FAILURE_INJECTOR_RETURN_( return false; ); public: G1ParScanThreadState(G1CollectedHeap* g1h, - G1RedirtyCardsQueueSet* rdcqs, uint worker_id, uint num_workers, G1CollectionSet* collection_set, @@ -139,16 +133,16 @@ public: void push_on_queue(ScannerTask task); - // Apply the post barrier to the given reference field. Enqueues the card of p + // Apply the post barrier to the given reference field. Marks the card of p // if the barrier does not filter out the reference for some reason (e.g. // p and q are in the same region, p is in survivor, p is in collection set) // To be called during GC if nothing particular about p and obj are known. template void write_ref_field_post(T* p, oop obj); - // Enqueue the card if the reference's target region's remembered set is tracked. + // Mark the card if the reference's target region's remembered set is tracked. // Assumes that a significant amount of pre-filtering (like done by // write_ref_field_post() above) has already been performed. - template void enqueue_card_if_tracked(G1HeapRegionAttr region_attr, T* p, oop o); + template void mark_card_if_tracked(G1HeapRegionAttr region_attr, T* p, oop o); G1EvacuationRootClosures* closures() { return _closures; } uint worker_id() { return _worker_id; } @@ -156,11 +150,22 @@ public: size_t lab_waste_words() const; size_t lab_undo_waste_words() const; - size_t evac_failure_enqueued_cards() const; + // Newly marked cards during this garbage collection, to be refined concurrently + // later. Contains both marks generated by new cross-region references as well + // as cards generated from regions into evacuation failed regions. + // Does not contain cards into the next collection set (e.g. survivors) - they will not + // be refined concurrently. Calculation is done on a best-effort basis. + size_t num_cards_pending() const; + // Number of cards newly generated by references into evacuation failed regions. + // Calculation is done on a best-effort basis. + size_t num_cards_from_evac_failure() const; + // Sum of cards marked by evacuation. Contains both pending cards as well as cards + // into the next collection set (e.g. survivors). + size_t num_cards_marked() const; // Pass locally gathered statistics to global state. Returns the total number of // HeapWords copied. - size_t flush_stats(size_t* surviving_young_words, uint num_workers, BufferNodeList* buffer_log); + size_t flush_stats(size_t* surviving_young_words, uint num_workers); #if TASKQUEUE_STATS PartialArrayTaskStats* partial_array_task_stats(); @@ -249,9 +254,7 @@ public: class G1ParScanThreadStateSet : public StackObj { G1CollectedHeap* _g1h; G1CollectionSet* _collection_set; - G1RedirtyCardsQueueSet _rdcqs; G1ParScanThreadState** _states; - BufferNodeList* _rdc_buffers; size_t* _surviving_young_words_total; uint _num_workers; bool _flushed; @@ -264,9 +267,6 @@ class G1ParScanThreadStateSet : public StackObj { G1EvacFailureRegions* evac_failure_regions); ~G1ParScanThreadStateSet(); - G1RedirtyCardsQueueSet* rdcqs() { return &_rdcqs; } - BufferNodeList* rdc_buffers() { return _rdc_buffers; } - void flush_stats(); void record_unused_optional_region(G1HeapRegion* hr); #if TASKQUEUE_STATS diff --git a/src/hotspot/share/gc/g1/g1ParScanThreadState.inline.hpp b/src/hotspot/share/gc/g1/g1ParScanThreadState.inline.hpp index 148284e7ef7..ee5bc93290e 100644 --- a/src/hotspot/share/gc/g1/g1ParScanThreadState.inline.hpp +++ b/src/hotspot/share/gc/g1/g1ParScanThreadState.inline.hpp @@ -96,25 +96,24 @@ G1OopStarChunkedList* G1ParScanThreadState::oops_into_optional_region(const G1He return &_oops_into_optional_regions[hr->index_in_opt_cset()]; } -template bool G1ParScanThreadState::enqueue_if_new(T* p) { - size_t card_index = ct()->index_for(p); - // If the card hasn't been added to the buffer, do it. - if (_last_enqueued_card != card_index) { - _rdc_local_qset.enqueue(ct()->byte_for_index(card_index)); - _last_enqueued_card = card_index; +template bool G1ParScanThreadState::mark_if_new(T* p, bool into_new_survivor) { + G1CardTable::CardValue* card = ct()->byte_for(p); + G1CardTable::CardValue value = *card; + if (value == G1CardTable::clean_card_val()) { + *card = into_new_survivor ? G1CardTable::g1_to_cset_card : G1CardTable::g1_dirty_card; return true; } else { return false; } } -template void G1ParScanThreadState::enqueue_card_into_evac_fail_region(T* p, oop obj) { +template void G1ParScanThreadState::mark_card_into_evac_fail_region(T* p, oop obj) { assert(!G1HeapRegion::is_in_same_region(p, obj), "Should have filtered out cross-region references already."); assert(!_g1h->heap_region_containing(p)->is_survivor(), "Should have filtered out from-newly allocated survivor references already."); assert(_g1h->heap_region_containing(obj)->in_collection_set(), "Only for enqeueing reference into collection set region"); - if (enqueue_if_new(p)) { - _evac_failure_enqueued_cards++; + if (mark_if_new(p, false /* into_new_survivor */)) { // The reference is never into survivor regions. + _num_cards_from_evac_failure++; } } @@ -137,18 +136,18 @@ template void G1ParScanThreadState::write_ref_field_post(T* p, oop obj if (dest_attr.is_in_cset()) { assert(obj->is_forwarded(), "evac-failed but not forwarded: " PTR_FORMAT, p2i(obj)); assert(obj->forwardee() == obj, "evac-failed but not self-forwarded: " PTR_FORMAT, p2i(obj)); - enqueue_card_into_evac_fail_region(p, obj); + mark_card_into_evac_fail_region(p, obj); return; } - enqueue_card_if_tracked(dest_attr, p, obj); + mark_card_if_tracked(dest_attr, p, obj); } -template void G1ParScanThreadState::enqueue_card_if_tracked(G1HeapRegionAttr region_attr, T* p, oop o) { +template void G1ParScanThreadState::mark_card_if_tracked(G1HeapRegionAttr region_attr, T* p, oop o) { assert(!G1HeapRegion::is_in_same_region(p, o), "Should have filtered out cross-region references already."); assert(!_g1h->heap_region_containing(p)->is_survivor(), "Should have filtered out from-newly allocated survivor references already."); // We relabel all regions that failed evacuation as old gen without remembered, // and so pre-filter them out in the caller. - assert(!_g1h->heap_region_containing(o)->in_collection_set(), "Should not try to enqueue reference into collection set region"); + assert(!_g1h->heap_region_containing(o)->in_collection_set(), "Should not try to mark reference into collection set region"); #ifdef ASSERT G1HeapRegion* const hr_obj = _g1h->heap_region_containing(o); @@ -161,7 +160,14 @@ template void G1ParScanThreadState::enqueue_card_if_tracked(G1HeapRegi if (!region_attr.remset_is_tracked()) { return; } - enqueue_if_new(p); + bool into_survivor = region_attr.is_new_survivor(); + if (mark_if_new(p, into_survivor)) { + if (into_survivor) { + _num_cards_marked_to_cset++; + } else { + _num_cards_marked_dirty++; + } + } } #endif // SHARE_GC_G1_G1PARSCANTHREADSTATE_INLINE_HPP diff --git a/src/hotspot/share/gc/g1/g1Policy.cpp b/src/hotspot/share/gc/g1/g1Policy.cpp index 9f872aa6ccd..754cc502031 100644 --- a/src/hotspot/share/gc/g1/g1Policy.cpp +++ b/src/hotspot/share/gc/g1/g1Policy.cpp @@ -67,8 +67,7 @@ G1Policy::G1Policy(STWGCTimer* gc_timer) : _reserve_regions(0), _young_gen_sizer(), _free_regions_at_end_of_collection(0), - _card_rs_length(0), - _pending_cards_at_gc_start(0), + _pending_cards_from_gc(0), _concurrent_start_to_mixed(), _collection_set(nullptr), _g1h(nullptr), @@ -553,12 +552,9 @@ G1GCPhaseTimes* G1Policy::phase_times() const { return _phase_times; } -void G1Policy::revise_young_list_target_length(size_t card_rs_length, size_t code_root_rs_length) { +void G1Policy::revise_young_list_target_length(size_t pending_cards, size_t card_rs_length, size_t code_root_rs_length) { guarantee(use_adaptive_young_list_length(), "should not call this otherwise" ); - size_t thread_buffer_cards = _analytics->predict_dirtied_cards_in_thread_buffers(); - G1DirtyCardQueueSet& dcqs = G1BarrierSet::dirty_card_queue_set(); - size_t pending_cards = dcqs.num_cards() + thread_buffer_cards; update_young_length_bounds(pending_cards, card_rs_length, code_root_rs_length); } @@ -567,7 +563,7 @@ void G1Policy::record_full_collection_start() { // Release the future to-space so that it is available for compaction into. collector_state()->set_in_young_only_phase(false); collector_state()->set_in_full_gc(true); - _pending_cards_at_gc_start = 0; + _collection_set->abandon_all_candidates(); } void G1Policy::record_full_collection_end() { @@ -600,59 +596,70 @@ void G1Policy::record_full_collection_end() { record_pause(G1GCPauseType::FullGC, start_time_sec, end_sec); } -static void log_refinement_stats(const char* kind, const G1ConcurrentRefineStats& stats) { +static void log_refinement_stats(const G1ConcurrentRefineStats& stats) { log_debug(gc, refine, stats) - ("%s refinement: %.2fms, refined: %zu" - ", precleaned: %zu, dirtied: %zu", - kind, - stats.refinement_time().seconds() * MILLIUNITS, + ("Refinement: sweep: %.2fms, yield: %.2fms refined: %zu, dirtied: %zu", + TimeHelper::counter_to_millis(stats.sweep_duration()), + TimeHelper::counter_to_millis(stats.yield_during_sweep_duration()), stats.refined_cards(), - stats.precleaned_cards(), - stats.dirtied_cards()); + stats.cards_pending()); } -void G1Policy::record_concurrent_refinement_stats(size_t pending_cards, - size_t thread_buffer_cards) { - _pending_cards_at_gc_start = pending_cards; - _analytics->report_dirtied_cards_in_thread_buffers(thread_buffer_cards); - - // Collect per-thread stats, mostly from mutator activity. - G1DirtyCardQueueSet& dcqs = G1BarrierSet::dirty_card_queue_set(); - G1ConcurrentRefineStats mut_stats = dcqs.concatenated_refinement_stats(); - - // Collect specialized concurrent refinement thread stats. - G1ConcurrentRefine* cr = _g1h->concurrent_refine(); - G1ConcurrentRefineStats cr_stats = cr->get_and_reset_refinement_stats(); - - G1ConcurrentRefineStats total_stats = mut_stats + cr_stats; - - log_refinement_stats("Mutator", mut_stats); - log_refinement_stats("Concurrent", cr_stats); - log_refinement_stats("Total", total_stats); +void G1Policy::record_refinement_stats(G1ConcurrentRefineStats* refine_stats) { + log_refinement_stats(*refine_stats); // Record the rate at which cards were refined. - // Don't update the rate if the current sample is empty or time is zero. - Tickspan refinement_time = total_stats.refinement_time(); - size_t refined_cards = total_stats.refined_cards(); - if ((refined_cards > 0) && (refinement_time > Tickspan())) { - double rate = refined_cards / (refinement_time.seconds() * MILLIUNITS); + // Don't update the rate if the current sample is empty or time is zero (which is + // the case during GC). + double refinement_time = TimeHelper::counter_to_millis(refine_stats->sweep_duration()); + size_t refined_cards = refine_stats->refined_cards(); + if ((refined_cards > 0) && (refinement_time > 0)) { + double rate = refined_cards / refinement_time; _analytics->report_concurrent_refine_rate_ms(rate); - log_debug(gc, refine, stats)("Concurrent refinement rate: %.2f cards/ms", rate); + log_debug(gc, refine, stats)("Concurrent refinement rate: %.2f cards/ms predicted: %.2f cards/ms", rate, _analytics->predict_concurrent_refine_rate_ms()); } +} +template +static T saturated_sub(T x, T y) { + return (x < y) ? T() : (x - y); +} + +void G1Policy::record_dirtying_stats(double last_mutator_start_dirty_ms, + double last_mutator_end_dirty_ms, + size_t pending_cards, + double yield_duration_ms, + size_t next_pending_cards_from_gc, + size_t next_to_collection_set_cards) { + assert(SafepointSynchronize::is_at_safepoint() || G1ReviseYoungLength_lock->is_locked(), + "must be (at safepoint %s locked %s)", + BOOL_TO_STR(SafepointSynchronize::is_at_safepoint()), BOOL_TO_STR(G1ReviseYoungLength_lock->is_locked())); // Record mutator's card logging rate. - double mut_start_time = _analytics->prev_collection_pause_end_ms(); - double mut_end_time = cur_pause_start_sec() * MILLIUNITS; - double mut_time = mut_end_time - mut_start_time; + // Unlike above for conc-refine rate, here we should not require a // non-empty sample, since an application could go some time with only // young-gen or filtered out writes. But we'll ignore unusually short // sample periods, as they may just pollute the predictions. - if (mut_time > 1.0) { // Require > 1ms sample time. - double dirtied_rate = total_stats.dirtied_cards() / mut_time; + double const mutator_dirty_time_ms = (last_mutator_end_dirty_ms - last_mutator_start_dirty_ms) - yield_duration_ms; + assert(mutator_dirty_time_ms >= 0.0, + "must be (start: %.2f end: %.2f yield: %.2f)", + last_mutator_start_dirty_ms, last_mutator_end_dirty_ms, yield_duration_ms); + + if (mutator_dirty_time_ms > 1.0) { // Require > 1ms sample time. + // The subtractive term is pending_cards_from_gc() which includes both dirtied and dirty-as-young cards, + // which can be larger than what is actually considered as "pending" (dirty cards only). + size_t dirtied_cards = saturated_sub(pending_cards, pending_cards_from_gc()); + double dirtied_rate = dirtied_cards / mutator_dirty_time_ms; _analytics->report_dirtied_cards_rate_ms(dirtied_rate); - log_debug(gc, refine, stats)("Generate dirty cards rate: %.2f cards/ms", dirtied_rate); + log_debug(gc, refine, stats)("Generate dirty cards rate: %.2f cards/ms dirtying time %.2f (start %.2f end %.2f yield %.2f) dirtied %zu (pending %zu during_gc %zu)", + dirtied_rate, + mutator_dirty_time_ms, + last_mutator_start_dirty_ms, last_mutator_end_dirty_ms, yield_duration_ms, + dirtied_cards, pending_cards, pending_cards_from_gc()); } + + _pending_cards_from_gc = next_pending_cards_from_gc; + _to_collection_set_cards = next_to_collection_set_cards; } bool G1Policy::should_retain_evac_failed_region(uint index) const { @@ -761,27 +768,27 @@ bool G1Policy::concurrent_operation_is_full_mark(const char* msg) { ((_g1h->gc_cause() != GCCause::_g1_humongous_allocation) || need_to_start_conc_mark(msg)); } -double G1Policy::logged_cards_processing_time() const { +double G1Policy::pending_cards_processing_time() const { double all_cards_processing_time = average_time_ms(G1GCPhaseTimes::ScanHR) + average_time_ms(G1GCPhaseTimes::OptScanHR); - size_t logged_dirty_cards = phase_times()->sum_thread_work_items(G1GCPhaseTimes::MergeLB, G1GCPhaseTimes::MergeLBDirtyCards); + size_t pending_cards = phase_times()->sum_thread_work_items(G1GCPhaseTimes::ScanHR, G1GCPhaseTimes::ScanHRPendingCards) + + phase_times()->sum_thread_work_items(G1GCPhaseTimes::OptScanHR, G1GCPhaseTimes::ScanHRPendingCards); size_t scan_heap_roots_cards = phase_times()->sum_thread_work_items(G1GCPhaseTimes::ScanHR, G1GCPhaseTimes::ScanHRScannedCards) + phase_times()->sum_thread_work_items(G1GCPhaseTimes::OptScanHR, G1GCPhaseTimes::ScanHRScannedCards); - double merge_logged_cards_time = average_time_ms(G1GCPhaseTimes::MergeLB) + - phase_times()->cur_distribute_log_buffers_time_ms(); + double merge_pending_cards_time = phase_times()->cur_merge_refinement_table_time(); - // Approximate the time spent processing cards from log buffers by scaling - // the total processing time by the ratio of logged cards to total cards + // Approximate the time spent processing cards from pending cards by scaling + // the total processing time by the ratio of pending cards to total cards // processed. There might be duplicate cards in different log buffers, // leading to an overestimate. That effect should be relatively small // unless there are few cards to process, because cards in buffers are // dirtied to limit duplication. Also need to avoid scaling when both // counts are zero, which happens especially during early GCs. So ascribe - // all of the time to the logged cards unless there are more total cards. - if (logged_dirty_cards >= scan_heap_roots_cards) { - return all_cards_processing_time + merge_logged_cards_time; + // all of the time to the pending cards unless there are more total cards. + if (pending_cards >= scan_heap_roots_cards) { + return all_cards_processing_time + merge_pending_cards_time; } - return (all_cards_processing_time * logged_dirty_cards / scan_heap_roots_cards) + merge_logged_cards_time; + return (all_cards_processing_time * pending_cards / scan_heap_roots_cards) + merge_pending_cards_time; } // Anything below that is considered to be zero @@ -815,6 +822,22 @@ void G1Policy::record_young_collection_end(bool concurrent_operation_is_full_mar // We make the assumption that these are rare. bool update_stats = !allocation_failure; + size_t const total_cards_scanned = p->sum_thread_work_items(G1GCPhaseTimes::ScanHR, G1GCPhaseTimes::ScanHRScannedCards) + + p->sum_thread_work_items(G1GCPhaseTimes::OptScanHR, G1GCPhaseTimes::ScanHRScannedCards); + + // Number of scanned cards with "Dirty" value (and nothing else). + size_t const pending_cards_from_refinement_table = p->sum_thread_work_items(G1GCPhaseTimes::ScanHR, G1GCPhaseTimes::ScanHRPendingCards) + + p->sum_thread_work_items(G1GCPhaseTimes::OptScanHR, G1GCPhaseTimes::ScanHRPendingCards); + // Number of cards actually merged in the Merge RS phase. MergeRSCards below includes the cards from the Eager Reclaim phase. + size_t const merged_cards_from_card_rs = p->sum_thread_work_items(G1GCPhaseTimes::MergeRS, G1GCPhaseTimes::MergeRSFromRemSetCards) + + p->sum_thread_work_items(G1GCPhaseTimes::OptMergeRS, G1GCPhaseTimes::MergeRSFromRemSetCards); + // Number of cards attempted to merge in the Merge RS phase. + size_t const total_cards_from_rs = p->sum_thread_work_items(G1GCPhaseTimes::MergeRS, G1GCPhaseTimes::MergeRSTotalCards) + + p->sum_thread_work_items(G1GCPhaseTimes::OptMergeRS, G1GCPhaseTimes::MergeRSTotalCards); + + // Cards marked as being to collection set. May be inaccurate due to races. + size_t const total_non_young_rs_cards = MIN2(pending_cards_from_refinement_table + merged_cards_from_card_rs, total_cards_scanned); + if (update_stats) { // We maintain the invariant that all objects allocated by mutator // threads will be allocated out of eden regions. So, we can use @@ -827,6 +850,98 @@ void G1Policy::record_young_collection_end(bool concurrent_operation_is_full_mar uint regions_allocated = _collection_set->eden_region_length(); double alloc_rate_ms = (double) regions_allocated / app_time_ms; _analytics->report_alloc_rate_ms(alloc_rate_ms); + + double merge_refinement_table_time = p->cur_merge_refinement_table_time(); + if (merge_refinement_table_time != 0.0) { + _analytics->report_merge_refinement_table_time_ms(merge_refinement_table_time); + } + if (merged_cards_from_card_rs >= G1NumCardsCostSampleThreshold) { + double avg_time_merge_cards = average_time_ms(G1GCPhaseTimes::MergeER) + + average_time_ms(G1GCPhaseTimes::MergeRS) + + average_time_ms(G1GCPhaseTimes::OptMergeRS); + _analytics->report_cost_per_card_merge_ms(avg_time_merge_cards / merged_cards_from_card_rs, is_young_only_pause); + log_debug(gc, ergo, cset)("cost per card merge (young %s): avg time %.2f merged cards %zu cost(1m) %.2f pred_cost(1m-yo) %.2f pred_cost(1m-old) %.2f", + BOOL_TO_STR(is_young_only_pause), + avg_time_merge_cards, merged_cards_from_card_rs, 1e6 * avg_time_merge_cards / merged_cards_from_card_rs, _analytics->predict_card_merge_time_ms(1e6, true), _analytics->predict_card_merge_time_ms(1e6, false)); + } else { + log_debug(gc, ergo, cset)("cost per card merge (young: %s): skipped, total cards %zu", BOOL_TO_STR(is_young_only_pause), total_non_young_rs_cards); + } + + // Update prediction for card scan + + if (total_cards_scanned >= G1NumCardsCostSampleThreshold) { + double avg_card_scan_time = average_time_ms(G1GCPhaseTimes::ScanHR) + + average_time_ms(G1GCPhaseTimes::OptScanHR); + + _analytics->report_cost_per_card_scan_ms(avg_card_scan_time / total_cards_scanned, is_young_only_pause); + + log_debug(gc, ergo, cset)("cost per card scan (young: %s): avg time %.2f total cards %zu cost(1m) %.2f pred_cost(1m-yo) %.2f pred_cost(1m-old) %.2f", + BOOL_TO_STR(is_young_only_pause), + avg_card_scan_time, total_cards_scanned, 1e6 * avg_card_scan_time / total_cards_scanned, _analytics->predict_card_scan_time_ms(1e6, true), _analytics->predict_card_scan_time_ms(1e6, false)); + } else { + log_debug(gc, ergo, cset)("cost per card scan (young: %s): skipped, total cards %zu", BOOL_TO_STR(is_young_only_pause), total_cards_scanned); + } + + // Update prediction for the ratio between cards actually merged onto the card + // table from the remembered sets and the total number of cards attempted to + // merge. + double merge_to_scan_ratio = 1.0; + if (total_cards_from_rs > 0) { + merge_to_scan_ratio = (double)merged_cards_from_card_rs / total_cards_from_rs; + } + _analytics->report_card_merge_to_scan_ratio(merge_to_scan_ratio, is_young_only_pause); + + // Update prediction for code root scan + size_t const total_code_roots_scanned = p->sum_thread_work_items(G1GCPhaseTimes::CodeRoots, G1GCPhaseTimes::CodeRootsScannedNMethods) + + p->sum_thread_work_items(G1GCPhaseTimes::OptCodeRoots, G1GCPhaseTimes::CodeRootsScannedNMethods); + + if (total_code_roots_scanned >= G1NumCodeRootsCostSampleThreshold) { + double avg_time_code_root_scan = average_time_ms(G1GCPhaseTimes::CodeRoots) + + average_time_ms(G1GCPhaseTimes::OptCodeRoots); + + _analytics->report_cost_per_code_root_scan_ms(avg_time_code_root_scan / total_code_roots_scanned, is_young_only_pause); + } + + // Update prediction for copy cost per byte + size_t copied_bytes = p->sum_thread_work_items(G1GCPhaseTimes::MergePSS, G1GCPhaseTimes::MergePSSCopiedBytes); + + if (copied_bytes > 0) { + double avg_copy_time = average_time_ms(G1GCPhaseTimes::ObjCopy) + average_time_ms(G1GCPhaseTimes::OptObjCopy); + double cost_per_byte_ms = avg_copy_time / copied_bytes; + _analytics->report_cost_per_byte_ms(cost_per_byte_ms, is_young_only_pause); + } + + if (_collection_set->young_region_length() > 0) { + _analytics->report_young_other_cost_per_region_ms(young_other_time_ms() / + _collection_set->young_region_length()); + } + + if (_collection_set->initial_old_region_length() > 0) { + _analytics->report_non_young_other_cost_per_region_ms(non_young_other_time_ms() / + _collection_set->initial_old_region_length()); + } + + _analytics->report_constant_other_time_ms(constant_other_time_ms(pause_time_ms)); + + _analytics->report_pending_cards(pending_cards_from_refinement_table, is_young_only_pause); + + _analytics->report_card_rs_length(total_cards_scanned - total_non_young_rs_cards, is_young_only_pause); + _analytics->report_code_root_rs_length((double)total_code_roots_scanned, is_young_only_pause); + } + + { + double mutator_end_time = cur_pause_start_sec() * MILLIUNITS; + G1ConcurrentRefineStats* stats = _g1h->concurrent_refine()->sweep_state().stats(); + // Record any available refinement statistics. + record_refinement_stats(stats); + + double yield_duration_ms = TimeHelper::counter_to_millis(_g1h->yield_duration_in_refinement_epoch()); + record_dirtying_stats(TimeHelper::counter_to_millis(_g1h->last_refinement_epoch_start()), + mutator_end_time, + pending_cards_from_refinement_table, + yield_duration_ms, + phase_times()->sum_thread_work_items(G1GCPhaseTimes::MergePSS, G1GCPhaseTimes::MergePSSPendingCards), + phase_times()->sum_thread_work_items(G1GCPhaseTimes::MergePSS, G1GCPhaseTimes::MergePSSToYoungGenCards)); } record_pause(this_pause, start_time_sec, end_time_sec, allocation_failure); @@ -857,82 +972,6 @@ void G1Policy::record_young_collection_end(bool concurrent_operation_is_full_mar _eden_surv_rate_group->start_adding_regions(); - if (update_stats) { - // Update prediction for card merge. - size_t const merged_cards_from_log_buffers = p->sum_thread_work_items(G1GCPhaseTimes::MergeLB, G1GCPhaseTimes::MergeLBDirtyCards); - // MergeRSCards includes the cards from the Eager Reclaim phase. - size_t const merged_cards_from_rs = p->sum_thread_work_items(G1GCPhaseTimes::MergeRS, G1GCPhaseTimes::MergeRSCards) + - p->sum_thread_work_items(G1GCPhaseTimes::OptMergeRS, G1GCPhaseTimes::MergeRSCards); - size_t const total_cards_merged = merged_cards_from_rs + - merged_cards_from_log_buffers; - - if (total_cards_merged >= G1NumCardsCostSampleThreshold) { - double avg_time_merge_cards = average_time_ms(G1GCPhaseTimes::MergeER) + - average_time_ms(G1GCPhaseTimes::MergeRS) + - average_time_ms(G1GCPhaseTimes::MergeLB) + - p->cur_distribute_log_buffers_time_ms() + - average_time_ms(G1GCPhaseTimes::OptMergeRS); - _analytics->report_cost_per_card_merge_ms(avg_time_merge_cards / total_cards_merged, is_young_only_pause); - } - - // Update prediction for card scan - size_t const total_cards_scanned = p->sum_thread_work_items(G1GCPhaseTimes::ScanHR, G1GCPhaseTimes::ScanHRScannedCards) + - p->sum_thread_work_items(G1GCPhaseTimes::OptScanHR, G1GCPhaseTimes::ScanHRScannedCards); - - if (total_cards_scanned >= G1NumCardsCostSampleThreshold) { - double avg_time_dirty_card_scan = average_time_ms(G1GCPhaseTimes::ScanHR) + - average_time_ms(G1GCPhaseTimes::OptScanHR); - - _analytics->report_cost_per_card_scan_ms(avg_time_dirty_card_scan / total_cards_scanned, is_young_only_pause); - } - - // Update prediction for the ratio between cards from the remembered - // sets and actually scanned cards from the remembered sets. - // Due to duplicates in the log buffers, the number of scanned cards - // can be smaller than the cards in the log buffers. - const size_t scanned_cards_from_rs = (total_cards_scanned > merged_cards_from_log_buffers) ? total_cards_scanned - merged_cards_from_log_buffers : 0; - double scan_to_merge_ratio = 0.0; - if (merged_cards_from_rs > 0) { - scan_to_merge_ratio = (double)scanned_cards_from_rs / merged_cards_from_rs; - } - _analytics->report_card_scan_to_merge_ratio(scan_to_merge_ratio, is_young_only_pause); - - // Update prediction for code root scan - size_t const total_code_roots_scanned = p->sum_thread_work_items(G1GCPhaseTimes::CodeRoots, G1GCPhaseTimes::CodeRootsScannedNMethods) + - p->sum_thread_work_items(G1GCPhaseTimes::OptCodeRoots, G1GCPhaseTimes::CodeRootsScannedNMethods); - - if (total_code_roots_scanned >= G1NumCodeRootsCostSampleThreshold) { - double avg_time_code_root_scan = average_time_ms(G1GCPhaseTimes::CodeRoots) + - average_time_ms(G1GCPhaseTimes::OptCodeRoots); - - _analytics->report_cost_per_code_root_scan_ms(avg_time_code_root_scan / total_code_roots_scanned, is_young_only_pause); - } - - // Update prediction for copy cost per byte - size_t copied_bytes = p->sum_thread_work_items(G1GCPhaseTimes::MergePSS, G1GCPhaseTimes::MergePSSCopiedBytes); - - if (copied_bytes > 0) { - double cost_per_byte_ms = (average_time_ms(G1GCPhaseTimes::ObjCopy) + average_time_ms(G1GCPhaseTimes::OptObjCopy)) / copied_bytes; - _analytics->report_cost_per_byte_ms(cost_per_byte_ms, is_young_only_pause); - } - - if (_collection_set->young_region_length() > 0) { - _analytics->report_young_other_cost_per_region_ms(young_other_time_ms() / - _collection_set->young_region_length()); - } - - if (_collection_set->initial_old_region_length() > 0) { - _analytics->report_non_young_other_cost_per_region_ms(non_young_other_time_ms() / - _collection_set->initial_old_region_length()); - } - - _analytics->report_constant_other_time_ms(constant_other_time_ms(pause_time_ms)); - - _analytics->report_pending_cards((double)pending_cards_at_gc_start(), is_young_only_pause); - _analytics->report_card_rs_length((double)_card_rs_length, is_young_only_pause); - _analytics->report_code_root_rs_length((double)total_code_roots_scanned, is_young_only_pause); - } - assert(!(G1GCPauseTypeHelper::is_concurrent_start_pause(this_pause) && collector_state()->mark_or_rebuild_in_progress()), "If the last pause has been concurrent start, we should not have been in the marking window"); if (G1GCPauseTypeHelper::is_concurrent_start_pause(this_pause)) { @@ -963,29 +1002,26 @@ void G1Policy::record_young_collection_end(bool concurrent_operation_is_full_mar } // Note that _mmu_tracker->max_gc_time() returns the time in seconds. - double logged_cards_time_goal_ms = _mmu_tracker->max_gc_time() * MILLIUNITS * G1RSetUpdatingPauseTimePercent / 100.0; + double pending_cards_time_goal_ms = _mmu_tracker->max_gc_time() * MILLIUNITS * G1RSetUpdatingPauseTimePercent / 100.0; - double const logged_cards_time_ms = logged_cards_processing_time(); - size_t logged_cards = - phase_times()->sum_thread_work_items(G1GCPhaseTimes::MergeLB, - G1GCPhaseTimes::MergeLBDirtyCards); - bool exceeded_goal = logged_cards_time_goal_ms < logged_cards_time_ms; - size_t predicted_thread_buffer_cards = _analytics->predict_dirtied_cards_in_thread_buffers(); + double const pending_cards_time_ms = pending_cards_processing_time(); + size_t pending_cards = phase_times()->sum_thread_work_items(G1GCPhaseTimes::ScanHR, G1GCPhaseTimes::ScanHRPendingCards) + + phase_times()->sum_thread_work_items(G1GCPhaseTimes::OptScanHR, G1GCPhaseTimes::ScanHRPendingCards); + + bool exceeded_goal = pending_cards_time_goal_ms < pending_cards_time_ms; G1ConcurrentRefine* cr = _g1h->concurrent_refine(); log_debug(gc, ergo, refine) - ("GC refinement: goal: %zu + %zu / %1.2fms, actual: %zu / %1.2fms, %s", + ("GC refinement: goal: %zu / %1.2fms, actual: %zu / %1.2fms, %s", cr->pending_cards_target(), - predicted_thread_buffer_cards, - logged_cards_time_goal_ms, - logged_cards, - logged_cards_time_ms, + pending_cards_time_goal_ms, + pending_cards, + pending_cards_time_ms, (exceeded_goal ? " (exceeded goal)" : "")); - cr->adjust_after_gc(logged_cards_time_ms, - logged_cards, - predicted_thread_buffer_cards, - logged_cards_time_goal_ms); + cr->adjust_after_gc(pending_cards_time_ms, + pending_cards, + pending_cards_time_goal_ms); } G1IHOPControl* G1Policy::create_ihop_control(const G1OldGenAllocationTracker* old_gen_alloc_tracker, @@ -1057,34 +1093,27 @@ double G1Policy::predict_base_time_ms(size_t pending_cards, size_t code_root_rs_length) const { bool in_young_only_phase = collector_state()->in_young_only_phase(); - size_t unique_cards_from_rs = _analytics->predict_scan_card_num(card_rs_length, in_young_only_phase); - // Assume that all cards from the log buffers will be scanned, i.e. there are no - // duplicates in that set. - size_t effective_scanned_cards = unique_cards_from_rs + pending_cards; + // Cards from the refinement table and the cards from the young gen remset are + // unique to each other as they are located on the card table. + size_t effective_scanned_cards = card_rs_length + pending_cards; - double card_merge_time = _analytics->predict_card_merge_time_ms(pending_cards + card_rs_length, in_young_only_phase); + double refinement_table_merge_time = _analytics->predict_merge_refinement_table_time_ms(); double card_scan_time = _analytics->predict_card_scan_time_ms(effective_scanned_cards, in_young_only_phase); double code_root_scan_time = _analytics->predict_code_root_scan_time_ms(code_root_rs_length, in_young_only_phase); double constant_other_time = _analytics->predict_constant_other_time_ms(); double survivor_evac_time = predict_survivor_regions_evac_time(); - double total_time = card_merge_time + card_scan_time + code_root_scan_time + constant_other_time + survivor_evac_time; + double total_time = refinement_table_merge_time + card_scan_time + code_root_scan_time + constant_other_time + survivor_evac_time; log_trace(gc, ergo, heap)("Predicted base time: total %f lb_cards %zu card_rs_length %zu effective_scanned_cards %zu " - "card_merge_time %f card_scan_time %f code_root_rs_length %zu code_root_scan_time %f " + "refinement_table_merge_time %f card_scan_time %f code_root_rs_length %zu code_root_scan_time %f " "constant_other_time %f survivor_evac_time %f", total_time, pending_cards, card_rs_length, effective_scanned_cards, - card_merge_time, card_scan_time, code_root_rs_length, code_root_scan_time, + refinement_table_merge_time, card_scan_time, code_root_rs_length, code_root_scan_time, constant_other_time, survivor_evac_time); return total_time; } -double G1Policy::predict_base_time_ms(size_t pending_cards) const { - bool for_young_only_phase = collector_state()->in_young_only_phase(); - size_t card_rs_length = _analytics->predict_card_rs_length(for_young_only_phase); - return predict_base_time_ms(pending_cards, card_rs_length); -} - double G1Policy::predict_base_time_ms(size_t pending_cards, size_t card_rs_length) const { bool for_young_only_phase = collector_state()->in_young_only_phase(); size_t code_root_rs_length = _analytics->predict_code_root_rs_length(for_young_only_phase); @@ -1428,6 +1457,64 @@ size_t G1Policy::allowed_waste_in_collection_set() const { return G1HeapWastePercent * _g1h->capacity() / 100; } +bool G1Policy::try_get_available_bytes_estimate(size_t& available_bytes) const { + // Getting used young bytes requires holding Heap_lock. But we can't use + // normal lock and block until available. Blocking on the lock could + // deadlock with a GC VMOp that is holding the lock and requesting a + // safepoint. Instead try to lock, and return the result of that attempt, + // and the estimate if successful. + if (Heap_lock->try_lock()) { + size_t used_bytes = estimate_used_young_bytes_locked(); + Heap_lock->unlock(); + + size_t young_bytes = young_list_target_length() * G1HeapRegion::GrainBytes; + available_bytes = young_bytes - MIN2(young_bytes, used_bytes); + return true; + } else { + available_bytes = 0; + return false; + } +} + +double G1Policy::predict_time_to_next_gc_ms(size_t available_bytes) const { + double alloc_region_rate = _analytics->predict_alloc_rate_ms(); + double alloc_bytes_rate = alloc_region_rate * G1HeapRegion::GrainBytes; + if (alloc_bytes_rate == 0.0) { + // A zero rate indicates we don't yet have data to use for predictions. + // Since we don't have any idea how long until the next GC, use a time of + // zero. + return 0.0; + } else { + // If the heap size is large and the allocation rate is small, we can get + // a predicted time until next GC that is so large it can cause problems + // (such as overflow) in other calculations. Limit the prediction to one + // hour, which is still large in this context. + const double one_hour_ms = 60.0 * 60.0 * MILLIUNITS; + double raw_time_ms = available_bytes / alloc_bytes_rate; + return MIN2(raw_time_ms, one_hour_ms); + } +} + +uint64_t G1Policy::adjust_wait_time_ms(double wait_time_ms, uint64_t min_time_ms) { + return MAX2(static_cast(sqrt(wait_time_ms) * 4.0), min_time_ms); +} + +double G1Policy::last_mutator_dirty_start_time_ms() { + return TimeHelper::counter_to_millis(_g1h->last_refinement_epoch_start()); +} + +size_t G1Policy::current_pending_cards() { + double now = os::elapsedTime() * MILLIUNITS; + return _pending_cards_from_gc + _analytics->predict_dirtied_cards_rate_ms() * (now - last_mutator_dirty_start_time_ms()); +} + +size_t G1Policy::current_to_collection_set_cards() { + // The incremental part is covered by the dirtied_cards_rate, i.e. pending cards + // cover both to collection set cards and other interesting cards because we do not + // know which is which until we look. + return _to_collection_set_cards; +} + uint G1Policy::min_retained_old_cset_length() const { // Guarantee some progress with retained regions regardless of available time by // taking at least one region. diff --git a/src/hotspot/share/gc/g1/g1Policy.hpp b/src/hotspot/share/gc/g1/g1Policy.hpp index e9f7529e509..01bad97ab84 100644 --- a/src/hotspot/share/gc/g1/g1Policy.hpp +++ b/src/hotspot/share/gc/g1/g1Policy.hpp @@ -48,6 +48,7 @@ class G1HeapRegion; class G1CollectionSet; class G1CollectionSetCandidates; class G1CollectionSetChooser; +class G1ConcurrentRefineStats; class G1IHOPControl; class G1Analytics; class G1SurvivorRegions; @@ -101,9 +102,18 @@ class G1Policy: public CHeapObj { uint _free_regions_at_end_of_collection; - size_t _card_rs_length; - - size_t _pending_cards_at_gc_start; + // Tracks the number of cards marked as dirty (only) during garbage collection + // (evacuation) on the card table. + // This is needed to properly account for those cards in the heuristics to start + // refinement at the correct time which needs to know how many cards are currently + // approximately on the card table. + // After the first completed refinement sweep of the refinement table between two + // garbage collections this value is reset to zero as that refinement processed all + // those cards. + size_t _pending_cards_from_gc; + // Tracks the approximate number of cards found as to-collection-set by either the + // garbage collection or the most recent refinement sweep. + size_t _to_collection_set_cards; G1ConcurrentStartToMixedTimeTracker _concurrent_start_to_mixed; @@ -111,7 +121,7 @@ class G1Policy: public CHeapObj { return collector_state()->in_young_only_phase() && !collector_state()->mark_or_rebuild_in_progress(); } - double logged_cards_processing_time() const; + double pending_cards_processing_time() const; public: const G1Predictions& predictor() const { return _predictor; } const G1Analytics* analytics() const { return const_cast(_analytics); } @@ -129,16 +139,10 @@ public: hr->install_surv_rate_group(_survivor_surv_rate_group); } - void record_card_rs_length(size_t num_cards) { - _card_rs_length = num_cards; - } - double cur_pause_start_sec() const { return _cur_pause_start_sec; } - double predict_base_time_ms(size_t pending_cards) const; - double predict_base_time_ms(size_t pending_cards, size_t card_rs_length) const; // Base time contains handling remembered sets and constant other time of the @@ -239,7 +243,13 @@ private: public: size_t predict_bytes_to_copy(G1HeapRegion* hr) const; - size_t pending_cards_at_gc_start() const { return _pending_cards_at_gc_start; } + + double last_mutator_dirty_start_time_ms(); + size_t pending_cards_from_gc() const { return _pending_cards_from_gc; } + + size_t current_pending_cards(); + + size_t current_to_collection_set_cards(); // GC efficiency for collecting the region based on the time estimate for // merging and scanning incoming references. @@ -286,7 +296,7 @@ public: // Check the current value of the young list RSet length and // compare it against the last prediction. If the current value is // higher, recalculate the young list target length prediction. - void revise_young_list_target_length(size_t card_rs_length, size_t code_root_rs_length); + void revise_young_list_target_length(size_t pending_cards, size_t card_rs_length, size_t code_root_rs_length); // This should be called after the heap is resized. void record_new_heap_size(uint new_number_of_regions); @@ -325,7 +335,6 @@ public: // Amount of allowed waste in bytes in the collection set. size_t allowed_waste_in_collection_set() const; - private: // Predict the number of bytes of surviving objects from survivor and old @@ -359,17 +368,39 @@ public: bool use_adaptive_young_list_length() const; + // Try to get an estimate of the currently available bytes in the young gen. This + // operation considers itself low-priority: if other threads need the resources + // required to get the information, return false to indicate that the caller + // should retry "soon". + bool try_get_available_bytes_estimate(size_t& bytes) const; + // Estimate time until next GC, based on remaining bytes available for + // allocation and the allocation rate. + double predict_time_to_next_gc_ms(size_t available_bytes) const; + + // Adjust wait times to make them less frequent the longer the next GC is away. + // But don't increase the wait time too rapidly, further bound it by min_time_ms. + // This reduces the number of thread wakeups that just immediately + // go back to waiting, while still being responsive to behavior changes. + uint64_t adjust_wait_time_ms(double wait_time_ms, uint64_t min_time_ms); + +private: // Return an estimate of the number of bytes used in young gen. // precondition: holding Heap_lock size_t estimate_used_young_bytes_locked() const; +public: + void transfer_survivors_to_cset(const G1SurvivorRegions* survivors); - // Record and log stats and pending cards before not-full collection. - // thread_buffer_cards is the number of cards that were in per-thread - // buffers. pending_cards includes thread_buffer_cards. - void record_concurrent_refinement_stats(size_t pending_cards, - size_t thread_buffer_cards); + // Record and log stats and pending cards to update predictors. + void record_refinement_stats(G1ConcurrentRefineStats* stats); + + void record_dirtying_stats(double last_mutator_start_dirty_ms, + double last_mutator_end_dirty_ms, + size_t pending_cards, + double yield_duration, + size_t next_pending_cards_from_gc, + size_t next_to_collection_set_cards); bool should_retain_evac_failed_region(G1HeapRegion* r) const { return should_retain_evac_failed_region(r->hrm_index()); diff --git a/src/hotspot/share/gc/g1/g1RedirtyCardsQueue.cpp b/src/hotspot/share/gc/g1/g1RedirtyCardsQueue.cpp deleted file mode 100644 index 45e262c440a..00000000000 --- a/src/hotspot/share/gc/g1/g1RedirtyCardsQueue.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#include "gc/g1/g1RedirtyCardsQueue.hpp" -#include "gc/shared/bufferNode.hpp" -#include "runtime/atomicAccess.hpp" -#include "utilities/debug.hpp" -#include "utilities/macros.hpp" - -// G1RedirtyCardsLocalQueueSet - -G1RedirtyCardsLocalQueueSet::G1RedirtyCardsLocalQueueSet(G1RedirtyCardsQueueSet* shared_qset) : - PtrQueueSet(shared_qset->allocator()), - _shared_qset(shared_qset), - _buffers(), - _queue(this) -{} - -#ifdef ASSERT -G1RedirtyCardsLocalQueueSet::~G1RedirtyCardsLocalQueueSet() { - assert(_buffers._head == nullptr, "unflushed qset"); - assert(_buffers._tail == nullptr, "invariant"); - assert(_buffers._entry_count == 0, "invariant"); -} -#endif // ASSERT - -void G1RedirtyCardsLocalQueueSet::enqueue_completed_buffer(BufferNode* node) { - _buffers._entry_count += node->size(); - node->set_next(_buffers._head); - _buffers._head = node; - if (_buffers._tail == nullptr) { - _buffers._tail = node; - } -} - -void G1RedirtyCardsLocalQueueSet::enqueue(void* value) { - if (!try_enqueue(_queue, value)) { - BufferNode* old_node = exchange_buffer_with_new(_queue); - if (old_node != nullptr) { - enqueue_completed_buffer(old_node); - } - retry_enqueue(_queue, value); - } -} - -BufferNodeList G1RedirtyCardsLocalQueueSet::flush() { - flush_queue(_queue); - BufferNodeList cur_buffers = _buffers; - _shared_qset->add_bufferlist(_buffers); - _buffers = BufferNodeList(); - return cur_buffers; -} - -// G1RedirtyCardsLocalQueueSet::Queue - -G1RedirtyCardsLocalQueueSet::Queue::Queue(G1RedirtyCardsLocalQueueSet* qset) : - PtrQueue(qset) -{} - -#ifdef ASSERT -G1RedirtyCardsLocalQueueSet::Queue::~Queue() { - assert(buffer() == nullptr, "unflushed queue"); -} -#endif // ASSERT - -// G1RedirtyCardsQueueSet - -G1RedirtyCardsQueueSet::G1RedirtyCardsQueueSet(BufferNode::Allocator* allocator) : - PtrQueueSet(allocator), - _list(), - _entry_count(0), - _tail(nullptr) - DEBUG_ONLY(COMMA _collecting(true)) -{} - -G1RedirtyCardsQueueSet::~G1RedirtyCardsQueueSet() { - verify_empty(); -} - -#ifdef ASSERT -void G1RedirtyCardsQueueSet::verify_empty() const { - assert(_list.empty(), "precondition"); - assert(_tail == nullptr, "invariant"); - assert(_entry_count == 0, "invariant"); -} -#endif // ASSERT - -BufferNode* G1RedirtyCardsQueueSet::all_completed_buffers() const { - DEBUG_ONLY(_collecting = false;) - return _list.top(); -} - -BufferNodeList G1RedirtyCardsQueueSet::take_all_completed_buffers() { - DEBUG_ONLY(_collecting = false;) - BufferNodeList result(_list.pop_all(), _tail, _entry_count); - _tail = nullptr; - _entry_count = 0; - DEBUG_ONLY(_collecting = true;) - return result; -} - -void G1RedirtyCardsQueueSet::update_tail(BufferNode* node) { - // Node is the tail of a (possibly single element) list just prepended to - // _list. If, after that prepend, node's follower is null, then node is - // also the tail of _list, so record it as such. - if (node->next() == nullptr) { - assert(_tail == nullptr, "invariant"); - _tail = node; - } -} - -void G1RedirtyCardsQueueSet::enqueue_completed_buffer(BufferNode* node) { - assert(_collecting, "precondition"); - AtomicAccess::add(&_entry_count, node->size()); - _list.push(*node); - update_tail(node); -} - -void G1RedirtyCardsQueueSet::add_bufferlist(const BufferNodeList& buffers) { - assert(_collecting, "precondition"); - if (buffers._head != nullptr) { - assert(buffers._tail != nullptr, "invariant"); - AtomicAccess::add(&_entry_count, buffers._entry_count); - _list.prepend(*buffers._head, *buffers._tail); - update_tail(buffers._tail); - } -} diff --git a/src/hotspot/share/gc/g1/g1RedirtyCardsQueue.hpp b/src/hotspot/share/gc/g1/g1RedirtyCardsQueue.hpp deleted file mode 100644 index add66f24cca..00000000000 --- a/src/hotspot/share/gc/g1/g1RedirtyCardsQueue.hpp +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef SHARE_GC_G1_G1REDIRTYCARDSQUEUE_HPP -#define SHARE_GC_G1_G1REDIRTYCARDSQUEUE_HPP - -#include "gc/shared/bufferNode.hpp" -#include "gc/shared/bufferNodeList.hpp" -#include "gc/shared/ptrQueue.hpp" -#include "memory/padded.hpp" -#include "utilities/macros.hpp" - -class G1RedirtyCardsQueueSet; - -// A thread-local qset and queue. It provides an uncontended staging -// area for completed buffers, to be flushed to the shared qset en masse. -class G1RedirtyCardsLocalQueueSet : private PtrQueueSet { - class Queue : public PtrQueue { - public: - Queue(G1RedirtyCardsLocalQueueSet* qset); - ~Queue() NOT_DEBUG(= default); - }; - - G1RedirtyCardsQueueSet* _shared_qset; - BufferNodeList _buffers; - Queue _queue; - - // Add the buffer to the local list. - virtual void enqueue_completed_buffer(BufferNode* node); - -public: - G1RedirtyCardsLocalQueueSet(G1RedirtyCardsQueueSet* shared_qset); - ~G1RedirtyCardsLocalQueueSet() NOT_DEBUG(= default); - - void enqueue(void* value); - - // Transfer all completed buffers to the shared qset. - // Returns the flushed BufferNodeList which is later used - // as a shortcut into the shared qset. - BufferNodeList flush(); -}; - -// Card table entries to be redirtied and the cards reprocessed later. -// Has two phases, collecting and processing. During the collecting -// phase buffers are added to the set. Once collecting is complete and -// processing starts, buffers can no longer be added. Taking all the -// collected (and processed) buffers reverts back to collecting, allowing -// the set to be reused for another round of redirtying. -class G1RedirtyCardsQueueSet : public PtrQueueSet { - DEFINE_PAD_MINUS_SIZE(1, DEFAULT_PADDING_SIZE, 0); - BufferNode::Stack _list; - DEFINE_PAD_MINUS_SIZE(2, DEFAULT_PADDING_SIZE, sizeof(size_t)); - volatile size_t _entry_count; - DEFINE_PAD_MINUS_SIZE(3, DEFAULT_PADDING_SIZE, sizeof(BufferNode*)); - BufferNode* _tail; - DEBUG_ONLY(mutable bool _collecting;) - - void update_tail(BufferNode* node); - -public: - G1RedirtyCardsQueueSet(BufferNode::Allocator* allocator); - ~G1RedirtyCardsQueueSet(); - - void verify_empty() const NOT_DEBUG_RETURN; - - // Collect buffers. These functions are thread-safe. - // precondition: Must not be concurrent with buffer processing. - virtual void enqueue_completed_buffer(BufferNode* node); - void add_bufferlist(const BufferNodeList& buffers); - - // Processing phase operations. - // precondition: Must not be concurrent with buffer collection. - BufferNode* all_completed_buffers() const; - BufferNodeList take_all_completed_buffers(); -}; - -#endif // SHARE_GC_G1_G1REDIRTYCARDSQUEUE_HPP diff --git a/src/hotspot/share/gc/g1/g1RemSet.cpp b/src/hotspot/share/gc/g1/g1RemSet.cpp index 2a09512730c..d2df416edc2 100644 --- a/src/hotspot/share/gc/g1/g1RemSet.cpp +++ b/src/hotspot/share/gc/g1/g1RemSet.cpp @@ -27,11 +27,12 @@ #include "gc/g1/g1BlockOffsetTable.inline.hpp" #include "gc/g1/g1CardSet.inline.hpp" #include "gc/g1/g1CardTable.inline.hpp" +#include "gc/g1/g1CardTableClaimTable.inline.hpp" #include "gc/g1/g1CardTableEntryClosure.hpp" #include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1CollectionSet.inline.hpp" #include "gc/g1/g1ConcurrentRefine.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" +#include "gc/g1/g1ConcurrentRefineSweepTask.hpp" #include "gc/g1/g1FromCardCache.hpp" #include "gc/g1/g1GCParPhaseTimesTracker.hpp" #include "gc/g1/g1GCPhaseTimes.hpp" @@ -42,8 +43,6 @@ #include "gc/g1/g1Policy.hpp" #include "gc/g1/g1RemSet.hpp" #include "gc/g1/g1RootClosures.hpp" -#include "gc/shared/bufferNode.hpp" -#include "gc/shared/bufferNodeList.hpp" #include "gc/shared/gc_globals.hpp" #include "gc/shared/gcTraceTime.inline.hpp" #include "jfr/jfrEvents.hpp" @@ -63,7 +62,7 @@ // Collects information about the overall heap root scan progress during an evacuation. // // Scanning the remembered sets works by first merging all sources of cards to be -// scanned (log buffers, remembered sets) into a single data structure to remove +// scanned (refinement table, remembered sets) into a single data structure to remove // duplicates and simplify work distribution. // // During the following card scanning we not only scan this combined set of cards, but @@ -89,37 +88,13 @@ class G1RemSetScanState : public CHeapObj { class G1DirtyRegions; - size_t _max_reserved_regions; - - // Card table iteration claim for each heap region, from 0 (completely unscanned) - // to (>=) G1HeapRegion::CardsPerRegion (completely scanned). - uint volatile* _card_table_scan_state; - - uint _scan_chunks_per_region; // Number of chunks per region. - uint8_t _log_scan_chunks_per_region; // Log of number of chunks per region. - bool* _region_scan_chunks; - size_t _num_total_scan_chunks; // Total number of elements in _region_scan_chunks. - uint8_t _scan_chunks_shift; // For conversion between card index and chunk index. -public: - uint scan_chunk_size_in_cards() const { return (uint)1 << _scan_chunks_shift; } - - // Returns whether the chunk corresponding to the given region/card in region contain a - // dirty card, i.e. actually needs scanning. - bool chunk_needs_scan(uint const region_idx, uint const card_in_region) const { - size_t const idx = ((size_t)region_idx << _log_scan_chunks_per_region) + (card_in_region >> _scan_chunks_shift); - assert(idx < _num_total_scan_chunks, "Index %zu out of bounds %zu", - idx, _num_total_scan_chunks); - return _region_scan_chunks[idx]; - } - -private: + G1CardTableClaimTable _card_claim_table; // The complete set of regions which card table needs to be cleared at the end - // of GC because we scribbled over these card tables. + // of GC because we scribbled over these card table entries. // // Regions may be added for two reasons: - // - they were part of the collection set: they may contain g1_young_card_val - // or regular card marks that we never scan so we must always clear their card - // table + // - they were part of the collection set: they may contain regular card marks + // that we never scan so we must always clear their card table. // - or in case g1 does an optional evacuation pass, g1 marks the cards in there // as g1_scanned_card_val. If G1 only did an initial evacuation pass, the // scanning already cleared these cards. In that case they are not in this set @@ -129,7 +104,7 @@ private: // in the current evacuation pass. G1DirtyRegions* _next_dirty_regions; - // Set of (unique) regions that can be added to concurrently. +// Set of (unique) regions that can be added to concurrently. class G1DirtyRegions : public CHeapObj { uint* _buffer; uint _cur_idx; @@ -147,8 +122,6 @@ private: reset(); } - static size_t chunk_size() { return M; } - ~G1DirtyRegions() { FREE_C_HEAP_ARRAY(uint, _buffer); FREE_C_HEAP_ARRAY(bool, _contains); @@ -197,7 +170,7 @@ private: // entries from free regions. HeapWord** _scan_top; - class G1ClearCardTableTask : public G1AbstractSubTask { +class G1ClearCardTableTask : public G1AbstractSubTask { G1CollectedHeap* _g1h; G1DirtyRegions* _regions; uint volatile _cur_dirty_regions; @@ -229,9 +202,9 @@ private: virtual ~G1ClearCardTableTask() { _scan_state->cleanup(); -#ifndef PRODUCT - G1CollectedHeap::heap()->verifier()->verify_card_table_cleanup(); -#endif + if (VerifyDuringGC) { + G1CollectedHeap::heap()->verifier()->verify_card_table_cleanup(); + } } void do_work(uint worker_id) override { @@ -243,7 +216,15 @@ private: for (uint i = next; i < max; i++) { G1HeapRegion* r = _g1h->region_at(_regions->at(i)); - r->clear_cardtable(); + // The card table contains "dirty" card marks. Clear unconditionally. + // + // Humongous reclaim candidates are not in the dirty set. This is fine because + // their card and refinement table should always be clear as they are typeArrays. + r->clear_card_table(); + // There is no need to clear the refinement table here: at the start of the collection + // we had to clear the refinement card table for collection set regions already, and any + // old regions use it for old->collection set candidates, so they should not be cleared + // either. } } } @@ -251,56 +232,41 @@ private: public: G1RemSetScanState() : - _max_reserved_regions(0), - _card_table_scan_state(nullptr), - _scan_chunks_per_region(G1CollectedHeap::get_chunks_per_region()), - _log_scan_chunks_per_region(log2i(_scan_chunks_per_region)), - _region_scan_chunks(nullptr), - _num_total_scan_chunks(0), - _scan_chunks_shift(0), + _card_claim_table(G1CollectedHeap::get_chunks_per_region_for_scan()), _all_dirty_regions(nullptr), _next_dirty_regions(nullptr), - _scan_top(nullptr) { - } + _scan_top(nullptr) { } ~G1RemSetScanState() { - FREE_C_HEAP_ARRAY(uint, _card_table_scan_state); - FREE_C_HEAP_ARRAY(bool, _region_scan_chunks); FREE_C_HEAP_ARRAY(HeapWord*, _scan_top); } - void initialize(size_t max_reserved_regions) { - assert(_card_table_scan_state == nullptr, "Must not be initialized twice"); - _max_reserved_regions = max_reserved_regions; - _card_table_scan_state = NEW_C_HEAP_ARRAY(uint, max_reserved_regions, mtGC); - _num_total_scan_chunks = max_reserved_regions * _scan_chunks_per_region; - _region_scan_chunks = NEW_C_HEAP_ARRAY(bool, _num_total_scan_chunks, mtGC); - - _scan_chunks_shift = (uint8_t)log2i(G1HeapRegion::CardsPerRegion / _scan_chunks_per_region); + void initialize(uint max_reserved_regions) { + _card_claim_table.initialize(max_reserved_regions); _scan_top = NEW_C_HEAP_ARRAY(HeapWord*, max_reserved_regions, mtGC); } + // Reset the claim and clear scan top for all regions, including + // regions currently not available or free. Since regions might + // become used during the collection these values must be valid + // for those regions as well. void prepare() { - // Reset the claim and clear scan top for all regions, including - // regions currently not available or free. Since regions might - // become used during the collection these values must be valid - // for those regions as well. - for (size_t i = 0; i < _max_reserved_regions; i++) { + size_t max_reserved_regions = _card_claim_table.max_reserved_regions(); + + for (size_t i = 0; i < max_reserved_regions; i++) { clear_scan_top((uint)i); } - _all_dirty_regions = new G1DirtyRegions(_max_reserved_regions); - _next_dirty_regions = new G1DirtyRegions(_max_reserved_regions); + _all_dirty_regions = new G1DirtyRegions(max_reserved_regions); + _next_dirty_regions = new G1DirtyRegions(max_reserved_regions); } void prepare_for_merge_heap_roots() { - assert(_next_dirty_regions->size() == 0, "next dirty regions must be empty"); + // We populate the next dirty regions at the start of GC with all old/humongous + // regions. + //assert(_next_dirty_regions->size() == 0, "next dirty regions must be empty"); - for (size_t i = 0; i < _max_reserved_regions; i++) { - _card_table_scan_state[i] = 0; - } - - ::memset(_region_scan_chunks, false, _num_total_scan_chunks * sizeof(*_region_scan_chunks)); + _card_claim_table.reset_all_to_unclaimed(); } void complete_evac_phase(bool merge_dirty_regions) { @@ -321,38 +287,10 @@ public: return (hr != nullptr && !hr->in_collection_set() && hr->is_old_or_humongous()); } - size_t num_visited_cards() const { - size_t result = 0; - for (uint i = 0; i < _num_total_scan_chunks; i++) { - if (_region_scan_chunks[i]) { - result++; - } - } - return result * (G1HeapRegion::CardsPerRegion / _scan_chunks_per_region); - } - size_t num_cards_in_dirty_regions() const { return _next_dirty_regions->size() * G1HeapRegion::CardsPerRegion; } - void set_chunk_range_dirty(size_t const region_card_idx, size_t const card_length) { - size_t chunk_idx = region_card_idx >> _scan_chunks_shift; - // Make sure that all chunks that contain the range are marked. Calculate the - // chunk of the last card that is actually marked. - size_t const end_chunk = (region_card_idx + card_length - 1) >> _scan_chunks_shift; - for (; chunk_idx <= end_chunk; chunk_idx++) { - _region_scan_chunks[chunk_idx] = true; - } - } - - void set_chunk_dirty(size_t const card_idx) { - assert((card_idx >> _scan_chunks_shift) < _num_total_scan_chunks, - "Trying to access index %zu out of bounds %zu", - card_idx >> _scan_chunks_shift, _num_total_scan_chunks); - size_t const chunk_idx = card_idx >> _scan_chunks_shift; - _region_scan_chunks[chunk_idx] = true; - } - G1AbstractSubTask* create_cleanup_after_scan_heap_roots_task() { return new G1ClearCardTableTask(G1CollectedHeap::heap(), _all_dirty_regions, this); } @@ -391,22 +329,16 @@ public: } bool has_cards_to_scan(uint region) { - assert(region < _max_reserved_regions, "Tried to access invalid region %u", region); - return _card_table_scan_state[region] < G1HeapRegion::CardsPerRegion; - } - - uint claim_cards_to_scan(uint region, uint increment) { - assert(region < _max_reserved_regions, "Tried to access invalid region %u", region); - return AtomicAccess::fetch_then_add(&_card_table_scan_state[region], increment, memory_order_relaxed); + return _card_claim_table.has_unclaimed_cards(region); } void add_dirty_region(uint const region) { -#ifdef ASSERT + #ifdef ASSERT G1HeapRegion* hr = G1CollectedHeap::heap()->region_at(region); assert(!hr->in_collection_set() && hr->is_old_or_humongous(), "Region %u is not suitable for scanning, is %sin collection set or %s", hr->hrm_index(), hr->in_collection_set() ? "" : "not ", hr->get_short_type_str()); -#endif + #endif _next_dirty_regions->add_dirty_region(region); } @@ -431,14 +363,16 @@ public: void clear_scan_top(uint region_idx) { set_scan_top(region_idx, nullptr); } + + G1CardTableChunkClaimer claimer(uint region_idx) { + return G1CardTableChunkClaimer(&_card_claim_table, region_idx); + } }; -G1RemSet::G1RemSet(G1CollectedHeap* g1h, - G1CardTable* ct) : +G1RemSet::G1RemSet(G1CollectedHeap* g1h) : _scan_state(new G1RemSetScanState()), _prev_period_summary(false), _g1h(g1h), - _ct(ct), _g1p(_g1h->policy()) { } @@ -450,36 +384,6 @@ void G1RemSet::initialize(uint max_reserved_regions) { _scan_state->initialize(max_reserved_regions); } -// Helper class to claim dirty chunks within the card table. -class G1CardTableChunkClaimer { - G1RemSetScanState* _scan_state; - uint _region_idx; - uint _cur_claim; - -public: - G1CardTableChunkClaimer(G1RemSetScanState* scan_state, uint region_idx) : - _scan_state(scan_state), - _region_idx(region_idx), - _cur_claim(0) { - guarantee(size() <= G1HeapRegion::CardsPerRegion, "Should not claim more space than possible."); - } - - bool has_next() { - while (true) { - _cur_claim = _scan_state->claim_cards_to_scan(_region_idx, size()); - if (_cur_claim >= G1HeapRegion::CardsPerRegion) { - return false; - } - if (_scan_state->chunk_needs_scan(_region_idx, _cur_claim)) { - return true; - } - } - } - - uint value() const { return _cur_claim; } - uint size() const { return _scan_state->scan_chunk_size_in_cards(); } -}; - // Scans a heap region for dirty cards. class G1ScanHRForRegionClosure : public G1HeapRegionClosure { using CardValue = CardTable::CardValue; @@ -495,6 +399,8 @@ class G1ScanHRForRegionClosure : public G1HeapRegionClosure { uint _worker_id; + size_t _cards_pending; + size_t _cards_empty; size_t _cards_scanned; size_t _blocks_scanned; size_t _chunks_claimed; @@ -508,9 +414,9 @@ class G1ScanHRForRegionClosure : public G1HeapRegionClosure { HeapWord* _scanned_to; CardValue _scanned_card_value; - HeapWord* scan_memregion(uint region_idx_for_card, MemRegion mr) { + HeapWord* scan_memregion(uint region_idx_for_card, MemRegion mr, size_t &roots_found) { G1HeapRegion* const card_region = _g1h->region_at(region_idx_for_card); - G1ScanCardClosure card_cl(_g1h, _pss, _heap_roots_found); + G1ScanCardClosure card_cl(_g1h, _pss, roots_found); HeapWord* const scanned_to = card_region->oops_on_memregion_seq_iterate_careful(mr, &card_cl); assert(scanned_to != nullptr, "Should be able to scan range"); @@ -520,8 +426,8 @@ class G1ScanHRForRegionClosure : public G1HeapRegionClosure { return scanned_to; } - void do_claimed_block(uint const region_idx, CardValue* const dirty_l, CardValue* const dirty_r) { - _ct->change_dirty_cards_to(dirty_l, dirty_r, _scanned_card_value); + void do_claimed_block(uint const region_idx, CardValue* const dirty_l, CardValue* const dirty_r, size_t& pending_cards) { + pending_cards += _ct->change_dirty_cards_to(dirty_l, dirty_r, _scanned_card_value); size_t num_cards = pointer_delta(dirty_r, dirty_l, sizeof(CardValue)); _blocks_scanned++; @@ -536,115 +442,22 @@ class G1ScanHRForRegionClosure : public G1HeapRegionClosure { return; } MemRegion mr(MAX2(card_start, _scanned_to), scan_end); - _scanned_to = scan_memregion(region_idx, mr); + size_t roots_found = 0; + _scanned_to = scan_memregion(region_idx, mr, roots_found); + if (roots_found == 0) { + _cards_empty += num_cards; + } _cards_scanned += num_cards; + _heap_roots_found += roots_found; } - // To locate consecutive dirty cards inside a chunk. - class ChunkScanner { - using Word = size_t; - - CardValue* const _start_card; - CardValue* const _end_card; - - static const size_t ExpandedToScanMask = G1CardTable::WordAlreadyScanned; - static const size_t ToScanMask = G1CardTable::g1_card_already_scanned; - - static bool is_card_dirty(const CardValue* const card) { - return (*card & ToScanMask) == 0; - } - - static bool is_word_aligned(const void* const addr) { - return ((uintptr_t)addr) % sizeof(Word) == 0; - } - - CardValue* find_first_dirty_card(CardValue* i_card) const { - while (!is_word_aligned(i_card)) { - if (is_card_dirty(i_card)) { - return i_card; - } - i_card++; - } - - for (/* empty */; i_card < _end_card; i_card += sizeof(Word)) { - Word word_value = *reinterpret_cast(i_card); - bool has_dirty_cards_in_word = (~word_value & ExpandedToScanMask) != 0; - - if (has_dirty_cards_in_word) { - for (uint i = 0; i < sizeof(Word); ++i) { - if (is_card_dirty(i_card)) { - return i_card; - } - i_card++; - } - assert(false, "should have early-returned"); - } - } - - return _end_card; - } - - CardValue* find_first_non_dirty_card(CardValue* i_card) const { - while (!is_word_aligned(i_card)) { - if (!is_card_dirty(i_card)) { - return i_card; - } - i_card++; - } - - for (/* empty */; i_card < _end_card; i_card += sizeof(Word)) { - Word word_value = *reinterpret_cast(i_card); - bool all_cards_dirty = (word_value == G1CardTable::WordAllDirty); - - if (!all_cards_dirty) { - for (uint i = 0; i < sizeof(Word); ++i) { - if (!is_card_dirty(i_card)) { - return i_card; - } - i_card++; - } - assert(false, "should have early-returned"); - } - } - - return _end_card; - } - - public: - ChunkScanner(CardValue* const start_card, CardValue* const end_card) : - _start_card(start_card), - _end_card(end_card) { - assert(is_word_aligned(start_card), "precondition"); - assert(is_word_aligned(end_card), "precondition"); - } - - template - void on_dirty_cards(Func&& f) { - for (CardValue* cur_card = _start_card; cur_card < _end_card; /* empty */) { - CardValue* dirty_l = find_first_dirty_card(cur_card); - CardValue* dirty_r = find_first_non_dirty_card(dirty_l); - - assert(dirty_l <= dirty_r, "inv"); - - if (dirty_l == dirty_r) { - assert(dirty_r == _end_card, "finished the entire chunk"); - return; - } - - f(dirty_l, dirty_r); - - cur_card = dirty_r + 1; - } - } - }; - void scan_heap_roots(G1HeapRegion* r) { uint const region_idx = r->hrm_index(); ResourceMark rm; - G1CardTableChunkClaimer claim(_scan_state, region_idx); + G1CardTableChunkClaimer claim = _scan_state->claimer(region_idx); // Set the current scan "finger" to null for every heap region to scan. Since // the claim value is monotonically increasing, the check to not scan below this @@ -652,6 +465,8 @@ class G1ScanHRForRegionClosure : public G1HeapRegionClosure { // to resetting this value for every claim. _scanned_to = nullptr; + size_t pending_cards = 0; + while (claim.has_next()) { _chunks_claimed++; @@ -660,11 +475,12 @@ class G1ScanHRForRegionClosure : public G1HeapRegionClosure { CardValue* const start_card = _ct->byte_for_index(region_card_base_idx); CardValue* const end_card = start_card + claim.size(); - ChunkScanner chunk_scanner{start_card, end_card}; + G1ChunkScanner chunk_scanner{start_card, end_card}; chunk_scanner.on_dirty_cards([&] (CardValue* dirty_l, CardValue* dirty_r) { - do_claimed_block(region_idx, dirty_l, dirty_r); + do_claimed_block(region_idx, dirty_l, dirty_r, pending_cards); }); } + _cards_pending += pending_cards; } public: @@ -679,6 +495,8 @@ public: _scan_state(scan_state), _phase(phase), _worker_id(worker_id), + _cards_pending(0), + _cards_empty(0), _cards_scanned(0), _blocks_scanned(0), _chunks_claimed(0), @@ -706,6 +524,8 @@ public: Tickspan rem_set_root_scan_time() const { return _rem_set_root_scan_time; } Tickspan rem_set_trim_partially_time() const { return _rem_set_trim_partially_time; } + size_t cards_pending() const { return _cards_pending; } + size_t cards_scanned_empty() const { return _cards_empty; } size_t cards_scanned() const { return _cards_scanned; } size_t blocks_scanned() const { return _blocks_scanned; } size_t chunks_claimed() const { return _chunks_claimed; } @@ -728,6 +548,9 @@ void G1RemSet::scan_heap_roots(G1ParScanThreadState* pss, p->record_or_add_time_secs(objcopy_phase, worker_id, cl.rem_set_trim_partially_time().seconds()); p->record_or_add_time_secs(scan_phase, worker_id, cl.rem_set_root_scan_time().seconds()); + + p->record_or_add_thread_work_item(scan_phase, worker_id, cl.cards_pending(), G1GCPhaseTimes::ScanHRPendingCards); + p->record_or_add_thread_work_item(scan_phase, worker_id, cl.cards_scanned_empty(), G1GCPhaseTimes::ScanHRScannedEmptyCards); p->record_or_add_thread_work_item(scan_phase, worker_id, cl.cards_scanned(), G1GCPhaseTimes::ScanHRScannedCards); p->record_or_add_thread_work_item(scan_phase, worker_id, cl.blocks_scanned(), G1GCPhaseTimes::ScanHRScannedBlocks); p->record_or_add_thread_work_item(scan_phase, worker_id, cl.chunks_claimed(), G1GCPhaseTimes::ScanHRClaimedChunks); @@ -901,6 +724,7 @@ void G1RemSet::prepare_region_for_scan(G1HeapRegion* r) { assert_scan_top_is_null(hrm_index); } else if (r->is_old_or_humongous()) { _scan_state->set_scan_top(hrm_index, r->top()); + _scan_state->add_dirty_region(hrm_index); } else { assert_scan_top_is_null(hrm_index); assert(r->is_free(), @@ -956,6 +780,90 @@ public: } }; +// Task to merge a non-dirty refinement table into the (primary) card table. +class MergeRefinementTableTask : public WorkerTask { + + G1CardTableClaimTable* _scan_state; + uint _max_workers; + + class G1MergeRefinementTableRegionClosure : public G1HeapRegionClosure { + G1CardTableClaimTable* _scan_state; + + bool do_heap_region(G1HeapRegion* r) override { + if (!_scan_state->has_unclaimed_cards(r->hrm_index())) { + return false; + } + + // We can blindly clear all collection set region's refinement tables: these + // regions will be evacuated and need their refinement table reset in case + // of evacuation failure. + // Young regions contain random marks, which are obvious to just clear. The + // card marks of other collection set region's refinement tables are also + // uninteresting. + if (r->in_collection_set()) { + uint claim = _scan_state->claim_all_cards(r->hrm_index()); + // Concurrent refinement may have started merging this region (we also + // get here for non-young regions), the claim may be non-zero for those. + // We could get away here with just clearing the area from the current + // claim to the last card in the region, but for now just do it all. + if (claim < G1HeapRegion::CardsPerRegion) { + r->clear_refinement_table(); + } + return false; + } + + assert(r->is_old_or_humongous(), "must be"); + + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + G1CardTable* card_table = g1h->card_table(); + G1CardTable* refinement_table = g1h->refinement_table(); + + size_t const region_card_base_idx = (size_t)r->hrm_index() << G1HeapRegion::LogCardsPerRegion; + + G1CardTableChunkClaimer claim(_scan_state, r->hrm_index()); + + while (claim.has_next()) { + size_t const start_idx = region_card_base_idx + claim.value(); + + size_t* card_cur_word = (size_t*)card_table->byte_for_index(start_idx); + + size_t* refinement_cur_word = (size_t*)refinement_table->byte_for_index(start_idx); + size_t* const refinement_end_word = refinement_cur_word + claim.size() / (sizeof(size_t) / sizeof(G1CardTable::CardValue)); + + for (; refinement_cur_word < refinement_end_word; ++refinement_cur_word, ++card_cur_word) { + size_t value = *refinement_cur_word; + *refinement_cur_word = G1CardTable::WordAllClean; + // Dirty is "0", so we need to logically-and here. This is also safe + // for all other possible values in the card table; at this point this + // can be either g1_dirty_card or g1_to_cset_card which will both be + // scanned. + size_t new_value = *card_cur_word & value; + *card_cur_word = new_value; + } + } + + return false; + } + + public: + G1MergeRefinementTableRegionClosure(G1CardTableClaimTable* scan_state) : G1HeapRegionClosure(), _scan_state(scan_state) { + } + }; + +public: + MergeRefinementTableTask(G1CardTableClaimTable* scan_state, uint max_workers) : + WorkerTask("Merge Refinement Table"), _scan_state(scan_state), _max_workers(max_workers) { guarantee(_scan_state != nullptr, "must be"); } + + void work(uint worker_id) override { + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + + G1GCParPhaseTimesTracker x(g1h->phase_times(), G1GCPhaseTimes::SweepRT, worker_id, false /* allow multiple invocation */); + + G1MergeRefinementTableRegionClosure cl(_scan_state); + _scan_state->heap_region_iterate_from_worker_offset(&cl, worker_id, _max_workers); + } +}; + class G1MergeHeapRootsTask : public WorkerTask { class G1MergeCardSetStats { @@ -973,12 +881,16 @@ class G1MergeHeapRootsTask : public WorkerTask { _merged[tag]++; } - void inc_remset_cards(size_t increment = 1) { - _merged[G1GCPhaseTimes::MergeRSCards] += increment; + void inc_merged_cards(size_t increment = 1) { + _merged[G1GCPhaseTimes::MergeRSFromRemSetCards] += increment; + } + + void inc_total_cards(size_t increment = 1) { + _merged[G1GCPhaseTimes::MergeRSTotalCards] += increment; } void dec_remset_cards(size_t decrement) { - _merged[G1GCPhaseTimes::MergeRSCards] -= decrement; + _merged[G1GCPhaseTimes::MergeRSTotalCards] -= decrement; } size_t merged(uint i) const { return _merged[i]; } @@ -1031,10 +943,10 @@ class G1MergeHeapRootsTask : public WorkerTask { } void mark_card(G1CardTable::CardValue* value) { - if (_ct->mark_clean_as_dirty(value)) { - _scan_state->set_chunk_dirty(_ct->index_for_cardvalue(value)); + if (_ct->mark_clean_as_from_remset(value)) { + _stats.inc_merged_cards(); } - _stats.inc_remset_cards(); + _stats.inc_total_cards(); } public: @@ -1054,7 +966,7 @@ class G1MergeHeapRootsTask : public WorkerTask { // Returns whether the given region actually needs iteration. bool start_iterate(uint const tag, uint const region_idx) { - assert(tag < G1GCPhaseTimes::MergeRSCards, "invalid tag %u", tag); + assert(tag < G1GCPhaseTimes::MergeRSFromRemSetCards, "invalid tag %u", tag); if (remember_if_interesting(region_idx)) { _region_base_idx = (size_t)region_idx << G1HeapRegion::LogCardsPerRegion; _stats.inc_card_set_merged(tag); @@ -1064,9 +976,9 @@ class G1MergeHeapRootsTask : public WorkerTask { } void do_card_range(uint const start_card_idx, uint const length) { - _ct->mark_range_dirty(_region_base_idx + start_card_idx, length); - _stats.inc_remset_cards(length); - _scan_state->set_chunk_range_dirty(_region_base_idx + start_card_idx, length); + size_t cards_changed = _ct->mark_clean_range_as_from_remset(_region_base_idx + start_card_idx, length); + _stats.inc_merged_cards(cards_changed); + _stats.inc_total_cards(length); } G1MergeCardSetStats stats() { @@ -1086,12 +998,19 @@ class G1MergeHeapRootsTask : public WorkerTask { class G1ClearBitmapClosure : public G1HeapRegionClosure { G1CollectedHeap* _g1h; G1RemSetScanState* _scan_state; + bool _initial_evacuation; void assert_bitmap_clear(G1HeapRegion* hr, const G1CMBitMap* bitmap) { assert(bitmap->get_next_marked_addr(hr->bottom(), hr->end()) == hr->end(), "Bitmap should have no mark for region %u (%s)", hr->hrm_index(), hr->get_short_type_str()); } + void assert_refinement_table_clear(G1HeapRegion* hr) { +#ifdef ASSERT + _g1h->refinement_table()->verify_region(MemRegion(hr->bottom(), hr->end()), G1CardTable::clean_card_val(), true); +#endif + } + bool should_clear_region(G1HeapRegion* hr) const { // The bitmap for young regions must obviously be clear as we never mark through them; // old regions that are currently being marked through are only in the collection set @@ -1110,14 +1029,31 @@ class G1MergeHeapRootsTask : public WorkerTask { } public: - G1ClearBitmapClosure(G1CollectedHeap* g1h, G1RemSetScanState* scan_state) : + G1ClearBitmapClosure(G1CollectedHeap* g1h, G1RemSetScanState* scan_state, bool initial_evacuation) : _g1h(g1h), - _scan_state(scan_state) + _scan_state(scan_state), + _initial_evacuation(initial_evacuation) { } bool do_heap_region(G1HeapRegion* hr) { assert(_g1h->is_in_cset(hr), "Should only be used iterating the collection set"); + // Collection set regions after the initial evacuation need their refinement + // table cleared because + // * we use the refinement table for recording references to other regions + // during evacuation failure handling + // * during previous passes we used the refinement table to contain marks for + // cross-region references. Now that we evacuate the region, they need to be + // cleared. + // + // We do not need to do this extra work for initial evacuation because we + // make sure the refinement table is clean for all regions either in + // concurrent refinement or in the merge refinement table phase earlier. + if (!_initial_evacuation) { + hr->clear_refinement_table(); + } else { + assert_refinement_table_clear(hr); + } // Evacuation failure uses the bitmap to record evacuation failed objects, // so the bitmap for the regions in the collection set must be cleared if not already. if (should_clear_region(hr)) { @@ -1177,145 +1113,23 @@ class G1MergeHeapRootsTask : public WorkerTask { } }; - // Visitor for the log buffer entries to merge them into the card table. - class G1MergeLogBufferCardsClosure : public G1CardTableEntryClosure { - - G1RemSetScanState* _scan_state; - G1CardTable* _ct; - - size_t _cards_dirty; - size_t _cards_skipped; - - void process_card(CardValue* card_ptr) { - if (*card_ptr == G1CardTable::dirty_card_val()) { - uint const region_idx = _ct->region_idx_for(card_ptr); - _scan_state->add_dirty_region(region_idx); - _scan_state->set_chunk_dirty(_ct->index_for_cardvalue(card_ptr)); - _cards_dirty++; - } - } - - public: - G1MergeLogBufferCardsClosure(G1CollectedHeap* g1h, G1RemSetScanState* scan_state) : - _scan_state(scan_state), - _ct(g1h->card_table()), - _cards_dirty(0), - _cards_skipped(0) - {} - - void do_card_ptr(CardValue* card_ptr) override { - // The only time we care about recording cards that - // contain references that point into the collection set - // is during RSet updating within an evacuation pause. - assert(SafepointSynchronize::is_at_safepoint(), "not during an evacuation pause"); - - uint const region_idx = _ct->region_idx_for(card_ptr); - - // The second clause must come after - the log buffers might contain cards to uncommitted - // regions. - // This code may count duplicate entries in the log buffers (even if rare) multiple - // times. - if (_scan_state->contains_cards_to_process(region_idx)) { - process_card(card_ptr); - } else { - // We may have had dirty cards in the (initial) collection set (or the - // young regions which are always in the initial collection set). We do - // not fix their cards here: we already added these regions to the set of - // regions to clear the card table at the end during the prepare() phase. - _cards_skipped++; - } - } - - size_t cards_dirty() const { return _cards_dirty; } - size_t cards_skipped() const { return _cards_skipped; } - }; - uint _num_workers; G1HeapRegionClaimer _hr_claimer; G1RemSetScanState* _scan_state; - // To mitigate contention due multiple threads accessing and popping BufferNodes from a shared - // G1DirtyCardQueueSet, we implement a sequential distribution phase. Here, BufferNodes are - // distributed to worker threads in a sequential manner utilizing the _dirty_card_buffers. By doing - // so, we effectively alleviate the bottleneck encountered during pop operations on the - // G1DirtyCardQueueSet. Importantly, this approach preserves the helping aspect among worker - // threads, allowing them to assist one another in case of imbalances in work distribution. - BufferNode::Stack* _dirty_card_buffers; - bool _initial_evacuation; volatile bool _fast_reclaim_handled; - void apply_closure_to_dirty_card_buffers(G1MergeLogBufferCardsClosure* cl, uint worker_id) { - G1DirtyCardQueueSet& dcqs = G1BarrierSet::dirty_card_queue_set(); - for (uint i = 0; i < _num_workers; i++) { - uint index = (worker_id + i) % _num_workers; - while (BufferNode* node = _dirty_card_buffers[index].pop()) { - cl->apply_to_buffer(node, worker_id); - dcqs.deallocate_buffer(node); - } - } - } - public: G1MergeHeapRootsTask(G1RemSetScanState* scan_state, uint num_workers, bool initial_evacuation) : WorkerTask("G1 Merge Heap Roots"), _num_workers(num_workers), _hr_claimer(num_workers), _scan_state(scan_state), - _dirty_card_buffers(nullptr), _initial_evacuation(initial_evacuation), _fast_reclaim_handled(false) - { - if (initial_evacuation) { - Ticks start = Ticks::now(); - - _dirty_card_buffers = NEW_C_HEAP_ARRAY(BufferNode::Stack, num_workers, mtGC); - for (uint i = 0; i < num_workers; i++) { - new (&_dirty_card_buffers[i]) BufferNode::Stack(); - } - - G1DirtyCardQueueSet& dcqs = G1BarrierSet::dirty_card_queue_set(); - BufferNodeList buffers = dcqs.take_all_completed_buffers(); - - size_t entries_per_thread = ceil(buffers._entry_count / (double)num_workers); - - BufferNode* head = buffers._head; - BufferNode* tail = head; - - uint worker = 0; - while (tail != nullptr) { - size_t count = tail->size(); - BufferNode* cur = tail->next(); - - while (count < entries_per_thread && cur != nullptr) { - tail = cur; - count += tail->size(); - cur = tail->next(); - } - - tail->set_next(nullptr); - _dirty_card_buffers[worker++ % num_workers].prepend(*head, *tail); - - assert(cur != nullptr || tail == buffers._tail, "Must be"); - head = cur; - tail = cur; - } - - Tickspan total = Ticks::now() - start; - G1CollectedHeap::heap()->phase_times()->record_distribute_log_buffers_time_ms(total.seconds() * 1000.0); - } - } - - ~G1MergeHeapRootsTask() { - if (_dirty_card_buffers != nullptr) { - using Stack = BufferNode::Stack; - for (uint i = 0; i < _num_workers; i++) { - _dirty_card_buffers[i].~Stack(); - } - FREE_C_HEAP_ARRAY(Stack, _dirty_card_buffers); - } - } + { } virtual void work(uint worker_id) { G1CollectedHeap* g1h = G1CollectedHeap::heap(); @@ -1368,50 +1182,28 @@ public: // Preparation for evacuation failure handling. { - G1ClearBitmapClosure clear(g1h, _scan_state); + G1ClearBitmapClosure clear(g1h, _scan_state, _initial_evacuation); g1h->collection_set_iterate_increment_from(&clear, &_hr_claimer, worker_id); } - - // Now apply the closure to all remaining log entries. - if (_initial_evacuation) { - assert(merge_remset_phase == G1GCPhaseTimes::MergeRS, "Wrong merge phase"); - G1GCParPhaseTimesTracker x(p, G1GCPhaseTimes::MergeLB, worker_id); - - G1MergeLogBufferCardsClosure cl(g1h, _scan_state); - apply_closure_to_dirty_card_buffers(&cl, worker_id); - - p->record_thread_work_item(G1GCPhaseTimes::MergeLB, worker_id, cl.cards_dirty(), G1GCPhaseTimes::MergeLBDirtyCards); - p->record_thread_work_item(G1GCPhaseTimes::MergeLB, worker_id, cl.cards_skipped(), G1GCPhaseTimes::MergeLBSkippedCards); - } } }; -void G1RemSet::print_merge_heap_roots_stats() { - LogTarget(Debug, gc, remset) lt; - if (lt.is_enabled()) { - LogStream ls(lt); +static void merge_refinement_table() { + G1CollectedHeap* g1h = G1CollectedHeap::heap(); - size_t num_visited_cards = _scan_state->num_visited_cards(); + G1ConcurrentRefineSweepState& state = g1h->concurrent_refine()->sweep_state_for_merge(); + WorkerThreads* workers = g1h->workers(); - size_t total_dirty_region_cards = _scan_state->num_cards_in_dirty_regions(); - - G1CollectedHeap* g1h = G1CollectedHeap::heap(); - size_t total_old_region_cards = - (g1h->num_committed_regions() - (g1h->num_free_regions() - g1h->collection_set()->cur_length())) * G1HeapRegion::CardsPerRegion; - - ls.print_cr("Visited cards %zu Total dirty %zu (%.2lf%%) Total old %zu (%.2lf%%)", - num_visited_cards, - total_dirty_region_cards, - percent_of(num_visited_cards, total_dirty_region_cards), - total_old_region_cards, - percent_of(num_visited_cards, total_old_region_cards)); - } + MergeRefinementTableTask cl(state.sweep_table(), workers->active_workers()); + log_debug(gc, ergo)("Running %s using %u workers", cl.name(), workers->active_workers()); + workers->run_task(&cl); } void G1RemSet::merge_heap_roots(bool initial_evacuation) { G1CollectedHeap* g1h = G1CollectedHeap::heap(); G1GCPhaseTimes* pt = g1h->phase_times(); + // 1. Prepare the merging process { Ticks start = Ticks::now(); @@ -1425,28 +1217,42 @@ void G1RemSet::merge_heap_roots(bool initial_evacuation) { } } - WorkerThreads* workers = g1h->workers(); - size_t const increment_length = g1h->collection_set()->regions_cur_length(); + // 2. (Optionally) Merge the refinement table into the card table (if needed). + G1ConcurrentRefineSweepState& state = g1h->concurrent_refine()->sweep_state(); + if (initial_evacuation && state.is_in_progress()) { + Ticks start = Ticks::now(); - uint const num_workers = initial_evacuation ? workers->active_workers() : - MIN2(workers->active_workers(), (uint)increment_length); + merge_refinement_table(); + g1h->phase_times()->record_merge_refinement_table_time((Ticks::now() - start).seconds() * MILLIUNITS); + } + + // 3. Merge other heap roots. Ticks start = Ticks::now(); { + WorkerThreads* workers = g1h->workers(); + + size_t const increment_length = g1h->collection_set()->groups_increment_length(); + + uint const num_workers = initial_evacuation ? workers->active_workers() : + MIN2(workers->active_workers(), (uint)increment_length); + G1MergeHeapRootsTask cl(_scan_state, num_workers, initial_evacuation); log_debug(gc, ergo)("Running %s using %u workers for %zu regions", cl.name(), num_workers, increment_length); workers->run_task(&cl, num_workers); } - print_merge_heap_roots_stats(); - if (initial_evacuation) { pt->record_merge_heap_roots_time((Ticks::now() - start).seconds() * 1000.0); } else { pt->record_or_add_optional_merge_heap_roots_time((Ticks::now() - start).seconds() * 1000.0); } + + if (VerifyDuringGC && initial_evacuation) { + g1h->verifier()->verify_card_tables_clean(false /* both_card_tables */); + } } void G1RemSet::complete_evac_phase(bool has_more_than_one_evacuation_phase) { @@ -1482,86 +1288,20 @@ inline void check_card_ptr(CardTable::CardValue* card_ptr, G1CardTable* ct) { #endif } -bool G1RemSet::clean_card_before_refine(CardValue** const card_ptr_addr) { - assert(!SafepointSynchronize::is_at_safepoint(), "Only call concurrently"); - - CardValue* card_ptr = *card_ptr_addr; - // Find the start address represented by the card. - HeapWord* start = _ct->addr_for(card_ptr); - // And find the region containing it. - G1HeapRegion* r = _g1h->heap_region_containing_or_null(start); - - // If this is a (stale) card into an uncommitted region, exit. - if (r == nullptr) { - return false; - } - - check_card_ptr(card_ptr, _ct); - - // If the card is no longer dirty, nothing to do. - // We cannot load the card value before the "r == nullptr" check above, because G1 - // could uncommit parts of the card table covering uncommitted regions. - if (*card_ptr != G1CardTable::dirty_card_val()) { - return false; - } - - // This check is needed for some uncommon cases where we should - // ignore the card. - // - // The region could be young. Cards for young regions are - // distinctly marked (set to g1_young_gen), so the post-barrier will - // filter them out. However, that marking is performed - // concurrently. A write to a young object could occur before the - // card has been marked young, slipping past the filter. - // - // The card could be stale, because the region has been freed since - // the card was recorded. In this case the region type could be - // anything. If (still) free or (reallocated) young, just ignore - // it. If (reallocated) old or humongous, the later card trimming - // and additional checks in iteration may detect staleness. At - // worst, we end up processing a stale card unnecessarily. - // - // In the normal (non-stale) case, the synchronization between the - // enqueueing of the card and processing it here will have ensured - // we see the up-to-date region type here. - if (!r->is_old_or_humongous()) { - return false; - } - - // Trim the region designated by the card to what's been allocated - // in the region. The card could be stale, or the card could cover - // (part of) an object at the end of the allocated space and extend - // beyond the end of allocation. - - // Non-humongous objects are either allocated in the old regions during GC. - // So if region is old then top is stable. - // Humongous object allocation sets top last; if top has not yet been set, - // this is a stale card and we'll end up with an empty intersection. - // If this is not a stale card, the synchronization between the - // enqueuing of the card and processing it here will have ensured - // we see the up-to-date top here. - HeapWord* scan_limit = r->top(); - - if (scan_limit <= start) { - // If the trimmed region is empty, the card must be stale. - return false; - } - - // Okay to clean and process the card now. There are still some - // stale card cases that may be detected by iteration and dealt with - // as iteration failure. - *const_cast(card_ptr) = G1CardTable::clean_card_val(); - - return true; -} - -void G1RemSet::refine_card_concurrently(CardValue* const card_ptr, - const uint worker_id) { +G1RemSet::RefineResult G1RemSet::refine_card_concurrently(CardValue* const card_ptr, + const uint worker_id) { assert(!_g1h->is_stw_gc_active(), "Only call concurrently"); - check_card_ptr(card_ptr, _ct); + G1CardTable* ct = _g1h->refinement_table(); + check_card_ptr(card_ptr, ct); + + // That card is already known to contain a reference to the collection set. Skip + // further processing. + if (*card_ptr == G1CardTable::g1_to_cset_card) { + return AlreadyToCSet; + } // Construct the MemRegion representing the card. - HeapWord* start = _ct->addr_for(card_ptr); + HeapWord* start = ct->addr_for(card_ptr); // And find the region containing it. G1HeapRegion* r = _g1h->heap_region_containing(start); // This reload of the top is safe even though it happens after the full @@ -1571,7 +1311,7 @@ void G1RemSet::refine_card_concurrently(CardValue* const card_ptr, // cannot span across safepoint, so we don't need to worry about top being // changed during safepoint. HeapWord* scan_limit = r->top(); - assert(scan_limit > start, "sanity"); + assert(scan_limit > start, "sanity region %u (%s) scan_limit " PTR_FORMAT " start " PTR_FORMAT, r->hrm_index(), r->get_short_type_str(), p2i(scan_limit), p2i(start)); // Don't use addr_for(card_ptr + 1) which can ask for // a card beyond the heap. @@ -1581,43 +1321,21 @@ void G1RemSet::refine_card_concurrently(CardValue* const card_ptr, G1ConcurrentRefineOopClosure conc_refine_cl(_g1h, worker_id); if (r->oops_on_memregion_seq_iterate_careful(dirty_region, &conc_refine_cl) != nullptr) { - return; + if (conc_refine_cl.has_ref_to_cset()) { + return HasRefToCSet; + } else if (conc_refine_cl.has_ref_to_old()) { + return HasRefToOld; + } else { + return NoCrossRegion; + } } - // If unable to process the card then we encountered an unparsable // part of the heap (e.g. a partially allocated object, so only // temporarily a problem) while processing a stale card. Despite // the card being stale, we can't simply ignore it, because we've - // already marked the card cleaned, so taken responsibility for + // already marked the card as cleaned, so taken responsibility for // ensuring the card gets scanned. - // - // However, the card might have gotten re-dirtied and re-enqueued - // while we worked. (In fact, it's pretty likely.) - if (*card_ptr == G1CardTable::dirty_card_val()) { - return; - } - - enqueue_for_reprocessing(card_ptr); -} - -// Re-dirty and re-enqueue the card to retry refinement later. -// This is used to deal with a rare race condition in concurrent refinement. -void G1RemSet::enqueue_for_reprocessing(CardValue* card_ptr) { - // We can't use the thread-local queue, because that might be the queue - // that is being processed by us; we could be a Java thread conscripted to - // perform refinement on our queue's current buffer. This situation only - // arises from rare race condition, so it's not worth any significant - // development effort or clever lock-free queue implementation. Instead - // we use brute force, allocating and enqueuing an entire buffer for just - // this card. Since buffers are processed in FIFO order and we try to - // keep some in the queue, it is likely that the racing state will have - // resolved by the time this card comes up for reprocessing. - *card_ptr = G1CardTable::dirty_card_val(); - G1DirtyCardQueueSet& dcqs = G1BarrierSet::dirty_card_queue_set(); - void** buffer = dcqs.allocate_buffer(); - size_t index = dcqs.buffer_capacity() - 1; - buffer[index] = card_ptr; - dcqs.enqueue_completed_buffer(BufferNode::make_node_from_buffer(buffer, index)); + return CouldNotParse; } void G1RemSet::print_periodic_summary_info(const char* header, uint period_count, bool show_thread_times) { diff --git a/src/hotspot/share/gc/g1/g1RemSet.hpp b/src/hotspot/share/gc/g1/g1RemSet.hpp index 50cc029a9a1..8b2353cdbb3 100644 --- a/src/hotspot/share/gc/g1/g1RemSet.hpp +++ b/src/hotspot/share/gc/g1/g1RemSet.hpp @@ -26,6 +26,7 @@ #define SHARE_GC_G1_G1REMSET_HPP #include "gc/g1/g1CardTable.hpp" +#include "gc/g1/g1CardTableClaimTable.hpp" #include "gc/g1/g1GCPhaseTimes.hpp" #include "gc/g1/g1HeapRegion.hpp" #include "gc/g1/g1OopClosures.hpp" @@ -65,20 +66,15 @@ private: G1CollectedHeap* _g1h; - G1CardTable* _ct; - G1Policy* _g1p; - - void print_merge_heap_roots_stats(); + G1Policy* _g1p; void assert_scan_top_is_null(uint hrm_index) NOT_DEBUG_RETURN; - void enqueue_for_reprocessing(CardValue* card_ptr); - public: // Initialize data that depends on the heap size being known. void initialize(uint max_num_regions); - G1RemSet(G1CollectedHeap* g1h, G1CardTable* ct); + G1RemSet(G1CollectedHeap* g1h); ~G1RemSet(); // Scan all cards in the non-collection set regions that potentially contain @@ -101,7 +97,7 @@ public: // Print coarsening stats. void print_coarsen_stats(); - // Creates a task for cleaining up temporary data structures and the + // Creates a task for cleaning up temporary data structures and the // card table, removing temporary duplicate detection information. G1AbstractSubTask* create_cleanup_after_scan_heap_roots_task(); // Excludes the given region from heap root scanning. @@ -122,16 +118,19 @@ public: G1GCPhaseTimes::GCParPhases scan_phase, G1GCPhaseTimes::GCParPhases objcopy_phase); - // Two methods for concurrent refinement support, executed concurrently to - // the mutator: - // Cleans the card at "*card_ptr_addr" before refinement, returns true iff the - // card needs later refinement. - bool clean_card_before_refine(CardValue** const card_ptr_addr); + enum RefineResult { + HasRefToCSet, // The (dirty) card has a reference to the collection set. + AlreadyToCSet, // The card is already one marked as having a reference to the collection set. + HasRefToOld, // The dirty card contains references to other old regions (not the collection set). + NoCrossRegion, // There is no interesting reference in the card any more. The mutator changed all + // references to such after dirtying the card. + CouldNotParse // The card is unparsable, need to retry later. + }; // Refine the region corresponding to "card_ptr". Must be called after // being filtered by clean_card_before_refine(), and after proper // fence/synchronization. - void refine_card_concurrently(CardValue* const card_ptr, - const uint worker_id); + RefineResult refine_card_concurrently(CardValue* const card_ptr, + const uint worker_id); // Print accumulated summary info from the start of the VM. void print_summary_info(); diff --git a/src/hotspot/share/gc/g1/g1RemSetSummary.cpp b/src/hotspot/share/gc/g1/g1RemSetSummary.cpp index 49cc993dac2..3e9cf938097 100644 --- a/src/hotspot/share/gc/g1/g1RemSetSummary.cpp +++ b/src/hotspot/share/gc/g1/g1RemSetSummary.cpp @@ -27,7 +27,6 @@ #include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1ConcurrentRefine.hpp" #include "gc/g1/g1ConcurrentRefineThread.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1HeapRegion.hpp" #include "gc/g1/g1HeapRegionRemSet.inline.hpp" #include "gc/g1/g1RemSet.hpp" @@ -37,39 +36,61 @@ #include "runtime/javaThread.hpp" void G1RemSetSummary::update() { - class CollectData : public ThreadClosure { + G1ConcurrentRefine* refine = G1CollectedHeap::heap()->concurrent_refine(); + + class CollectWorkerData : public ThreadClosure { G1RemSetSummary* _summary; uint _counter; public: - CollectData(G1RemSetSummary * summary) : _summary(summary), _counter(0) {} + CollectWorkerData(G1RemSetSummary* summary) : _summary(summary), _counter(0) {} virtual void do_thread(Thread* t) { G1ConcurrentRefineThread* crt = static_cast(t); - _summary->set_refine_thread_cpu_time(_counter, crt->cpu_time()); + _summary->set_worker_thread_cpu_time(_counter, crt->cpu_time()); _counter++; } } collector(this); - G1CollectedHeap* g1h = G1CollectedHeap::heap(); - g1h->concurrent_refine()->threads_do(&collector); + refine->worker_threads_do(&collector); + + class CollectControlData : public ThreadClosure { + G1RemSetSummary* _summary; + public: + CollectControlData(G1RemSetSummary* summary) : _summary(summary) {} + virtual void do_thread(Thread* t) { + G1ConcurrentRefineThread* crt = static_cast(t); + _summary->set_control_thread_cpu_time(crt->cpu_time()); + } + } control(this); + + refine->control_thread_do(&control); } -void G1RemSetSummary::set_refine_thread_cpu_time(uint thread, jlong value) { - assert(_refine_threads_cpu_times != nullptr, "just checking"); - assert(thread < _num_refine_threads, "just checking"); - _refine_threads_cpu_times[thread] = value; +void G1RemSetSummary::set_worker_thread_cpu_time(uint thread, jlong value) { + assert(_worker_threads_cpu_times != nullptr, "just checking"); + assert(thread < _num_worker_threads, "just checking"); + _worker_threads_cpu_times[thread] = value; } -jlong G1RemSetSummary::refine_thread_cpu_time(uint thread) const { - assert(_refine_threads_cpu_times != nullptr, "just checking"); - assert(thread < _num_refine_threads, "just checking"); - return _refine_threads_cpu_times[thread]; +void G1RemSetSummary::set_control_thread_cpu_time(jlong value) { + _control_thread_cpu_time = value; +} + +jlong G1RemSetSummary::worker_thread_cpu_time(uint thread) const { + assert(_worker_threads_cpu_times != nullptr, "just checking"); + assert(thread < _num_worker_threads, "just checking"); + return _worker_threads_cpu_times[thread]; +} + +jlong G1RemSetSummary::control_thread_cpu_time() const { + return _control_thread_cpu_time; } G1RemSetSummary::G1RemSetSummary(bool should_update) : - _num_refine_threads(G1ConcRefinementThreads), - _refine_threads_cpu_times(NEW_C_HEAP_ARRAY(jlong, _num_refine_threads, mtGC)) { + _num_worker_threads(G1ConcRefinementThreads), + _worker_threads_cpu_times(NEW_C_HEAP_ARRAY(jlong, _num_worker_threads, mtGC)), + _control_thread_cpu_time(0) { - memset(_refine_threads_cpu_times, 0, sizeof(jlong) * _num_refine_threads); + memset(_worker_threads_cpu_times, 0, sizeof(jlong) * _num_worker_threads); if (should_update) { update(); @@ -77,23 +98,25 @@ G1RemSetSummary::G1RemSetSummary(bool should_update) : } G1RemSetSummary::~G1RemSetSummary() { - FREE_C_HEAP_ARRAY(jlong, _refine_threads_cpu_times); + FREE_C_HEAP_ARRAY(jlong, _worker_threads_cpu_times); } void G1RemSetSummary::set(G1RemSetSummary* other) { assert(other != nullptr, "just checking"); - assert(_num_refine_threads == other->_num_refine_threads, "just checking"); + assert(_num_worker_threads == other->_num_worker_threads, "just checking"); - memcpy(_refine_threads_cpu_times, other->_refine_threads_cpu_times, sizeof(jlong) * _num_refine_threads); + memcpy(_worker_threads_cpu_times, other->_worker_threads_cpu_times, sizeof(jlong) * _num_worker_threads); + _control_thread_cpu_time = other->_control_thread_cpu_time; } void G1RemSetSummary::subtract_from(G1RemSetSummary* other) { assert(other != nullptr, "just checking"); - assert(_num_refine_threads == other->_num_refine_threads, "just checking"); + assert(_num_worker_threads == other->_num_worker_threads, "just checking"); - for (uint i = 0; i < _num_refine_threads; i++) { - set_refine_thread_cpu_time(i, other->refine_thread_cpu_time(i) - refine_thread_cpu_time(i)); + for (uint i = 0; i < _num_worker_threads; i++) { + set_worker_thread_cpu_time(i, other->worker_thread_cpu_time(i) - worker_thread_cpu_time(i)); } + _control_thread_cpu_time = other->_control_thread_cpu_time - _control_thread_cpu_time; } class G1PerRegionTypeRemSetCounters { @@ -376,9 +399,10 @@ public: void G1RemSetSummary::print_on(outputStream* out, bool show_thread_times) { if (show_thread_times) { out->print_cr(" Concurrent refinement threads times (s)"); + out->print_cr(" Control %5.2f Workers", (double)control_thread_cpu_time() / NANOSECS_PER_SEC); out->print(" "); - for (uint i = 0; i < _num_refine_threads; i++) { - out->print(" %5.2f", (double)refine_thread_cpu_time(i) / NANOSECS_PER_SEC); + for (uint i = 0; i < _num_worker_threads; i++) { + out->print(" %5.2f", (double)worker_thread_cpu_time(i) / NANOSECS_PER_SEC); } out->cr(); } diff --git a/src/hotspot/share/gc/g1/g1RemSetSummary.hpp b/src/hotspot/share/gc/g1/g1RemSetSummary.hpp index 373f38952c8..dd7d55d5a2e 100644 --- a/src/hotspot/share/gc/g1/g1RemSetSummary.hpp +++ b/src/hotspot/share/gc/g1/g1RemSetSummary.hpp @@ -33,10 +33,12 @@ class G1RemSet; // A G1RemSetSummary manages statistical information about the remembered set. class G1RemSetSummary { - size_t _num_refine_threads; - jlong* _refine_threads_cpu_times; + size_t _num_worker_threads; + jlong* _worker_threads_cpu_times; + jlong _control_thread_cpu_time; - void set_refine_thread_cpu_time(uint thread, jlong value); + void set_worker_thread_cpu_time(uint thread, jlong value); + void set_control_thread_cpu_time(jlong value); // Update this summary with current data from various places. void update(); @@ -53,7 +55,8 @@ public: void print_on(outputStream* out, bool show_thread_times); - jlong refine_thread_cpu_time(uint thread) const; + jlong worker_thread_cpu_time(uint thread) const; + jlong control_thread_cpu_time() const; }; #endif // SHARE_GC_G1_G1REMSETSUMMARY_HPP diff --git a/src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.cpp b/src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.cpp new file mode 100644 index 00000000000..2f7acd9b710 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "gc/g1/g1CollectedHeap.hpp" +#include "gc/g1/g1Policy.hpp" +#include "gc/g1/g1ReviseYoungLengthTask.hpp" +#include "gc/g1/g1ServiceThread.hpp" +#include "gc/shared/suspendibleThreadSet.hpp" + + +jlong G1ReviseYoungLengthTask::reschedule_delay_ms() const { + G1Policy* policy = G1CollectedHeap::heap()->policy(); + size_t available_bytes; + if (policy->try_get_available_bytes_estimate(available_bytes)) { + double predicted_time_to_next_gc_ms = policy->predict_time_to_next_gc_ms(available_bytes); + + // Use a prime number close to 50ms as minimum time, different to other components + // that derive their wait time from the try_get_available_bytes_estimate() call + // to minimize interference. + uint64_t const min_wait_time_ms = 47; + + return policy->adjust_wait_time_ms(predicted_time_to_next_gc_ms, min_wait_time_ms); + } else { + // Failed to get estimate of available bytes. Try again asap. + return 1; + } +} + +class G1ReviseYoungLengthTask::RemSetSamplingClosure : public G1HeapRegionClosure { + size_t _sampled_code_root_rs_length; + +public: + RemSetSamplingClosure() : _sampled_code_root_rs_length(0) { } + + bool do_heap_region(G1HeapRegion* r) override { + G1HeapRegionRemSet* rem_set = r->rem_set(); + _sampled_code_root_rs_length += rem_set->code_roots_list_length(); + return false; + } + + size_t sampled_code_root_rs_length() const { return _sampled_code_root_rs_length; } +}; + +void G1ReviseYoungLengthTask::adjust_young_list_target_length() { + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + G1Policy* policy = g1h->policy(); + + assert(policy->use_adaptive_young_list_length(), "should not call otherwise"); + + size_t pending_cards; + size_t current_to_collection_set_cards; + { + MutexLocker x(G1ReviseYoungLength_lock, Mutex::_no_safepoint_check_flag); + pending_cards = policy->current_pending_cards(); + current_to_collection_set_cards = policy->current_to_collection_set_cards(); + } + + RemSetSamplingClosure cl; + g1h->collection_set()->iterate(&cl); + + policy->revise_young_list_target_length(pending_cards, + current_to_collection_set_cards, + cl.sampled_code_root_rs_length()); +} + +G1ReviseYoungLengthTask::G1ReviseYoungLengthTask(const char* name) : + G1ServiceTask(name) { } + +void G1ReviseYoungLengthTask::execute() { + SuspendibleThreadSetJoiner sts; + + adjust_young_list_target_length(); + + schedule(reschedule_delay_ms()); +} diff --git a/src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.hpp b/src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.hpp new file mode 100644 index 00000000000..baa8af75fb7 --- /dev/null +++ b/src/hotspot/share/gc/g1/g1ReviseYoungLengthTask.hpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#ifndef SHARE_GC_G1_G1REVISEYOUNGLENGTHTASK_HPP +#define SHARE_GC_G1_G1REVISEYOUNGLENGTHTASK_HPP + +#include "gc/g1/g1CardSetMemory.hpp" +#include "gc/g1/g1HeapRegionRemSet.hpp" +#include "gc/g1/g1MonotonicArenaFreePool.hpp" +#include "gc/g1/g1ServiceThread.hpp" +#include "utilities/growableArray.hpp" +#include "utilities/ticks.hpp" + +// ServiceTask to revise the young generation target length. +class G1ReviseYoungLengthTask : public G1ServiceTask { + + // The delay used to reschedule this task. + jlong reschedule_delay_ms() const; + + class RemSetSamplingClosure; // Helper class for calculating remembered set summary. + + // Adjust the target length (in regions) of the young gen, based on the + // current length of the remembered sets. + // + // At the end of the GC G1 determines the length of the young gen based on + // how much time the next GC can take, and when the next GC may occur + // according to the MMU. + // + // The assumption is that a significant part of the GC is spent on scanning + // the remembered sets (and many other components), so this thread constantly + // reevaluates the prediction for the remembered set scanning costs, and potentially + // resizes the young gen. This may do a premature GC or even increase the young + // gen size to keep pause time length goal. + void adjust_young_list_target_length(); + +public: + explicit G1ReviseYoungLengthTask(const char* name); + + void execute() override; +}; + +#endif // SHARE_GC_G1_G1REVISEYOUNGLENGTHTASK_HPP \ No newline at end of file diff --git a/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp b/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp index d0dcb59d7f0..858081b0581 100644 --- a/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp +++ b/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ #define SHARE_GC_G1_G1THREADLOCALDATA_HPP #include "gc/g1/g1BarrierSet.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" +#include "gc/g1/g1CardTable.hpp" #include "gc/g1/g1RegionPinCache.hpp" #include "gc/shared/gc_globals.hpp" #include "gc/shared/satbMarkQueue.hpp" @@ -36,7 +36,7 @@ class G1ThreadLocalData { private: SATBMarkQueue _satb_mark_queue; - G1DirtyCardQueue _dirty_card_queue; + G1CardTable::CardValue* _byte_map_base; // Per-thread cache of pinned object count to reduce atomic operation traffic // due to region pinning. Holds the last region where the mutator pinned an @@ -45,8 +45,8 @@ private: G1ThreadLocalData() : _satb_mark_queue(&G1BarrierSet::satb_mark_queue_set()), - _dirty_card_queue(&G1BarrierSet::dirty_card_queue_set()), - _pin_cache() {} + _byte_map_base(nullptr), + _pin_cache() { } static G1ThreadLocalData* data(Thread* thread) { assert(UseG1GC, "Sanity"); @@ -57,10 +57,6 @@ private: return Thread::gc_data_offset() + byte_offset_of(G1ThreadLocalData, _satb_mark_queue); } - static ByteSize dirty_card_queue_offset() { - return Thread::gc_data_offset() + byte_offset_of(G1ThreadLocalData, _dirty_card_queue); - } - public: static void create(Thread* thread) { new (data(thread)) G1ThreadLocalData(); @@ -74,10 +70,6 @@ public: return data(thread)->_satb_mark_queue; } - static G1DirtyCardQueue& dirty_card_queue(Thread* thread) { - return data(thread)->_dirty_card_queue; - } - static ByteSize satb_mark_queue_active_offset() { return satb_mark_queue_offset() + SATBMarkQueue::byte_offset_of_active(); } @@ -90,14 +82,20 @@ public: return satb_mark_queue_offset() + SATBMarkQueue::byte_offset_of_buf(); } - static ByteSize dirty_card_queue_index_offset() { - return dirty_card_queue_offset() + G1DirtyCardQueue::byte_offset_of_index(); + static ByteSize card_table_base_offset() { + return Thread::gc_data_offset() + byte_offset_of(G1ThreadLocalData, _byte_map_base); } - static ByteSize dirty_card_queue_buffer_offset() { - return dirty_card_queue_offset() + G1DirtyCardQueue::byte_offset_of_buf(); + static void set_byte_map_base(Thread* thread, G1CardTable::CardValue* new_byte_map_base) { + data(thread)->_byte_map_base = new_byte_map_base; } +#ifndef PRODUCT + static G1CardTable::CardValue* get_byte_map_base(Thread* thread) { + return data(thread)->_byte_map_base; + } +#endif + static G1RegionPinCache& pin_count_cache(Thread* thread) { return data(thread)->_pin_cache; } diff --git a/src/hotspot/share/gc/g1/g1YoungCollector.cpp b/src/hotspot/share/gc/g1/g1YoungCollector.cpp index ee25e5fc028..e97e59575e3 100644 --- a/src/hotspot/share/gc/g1/g1YoungCollector.cpp +++ b/src/hotspot/share/gc/g1/g1YoungCollector.cpp @@ -39,7 +39,6 @@ #include "gc/g1/g1MonitoringSupport.hpp" #include "gc/g1/g1ParScanThreadState.inline.hpp" #include "gc/g1/g1Policy.hpp" -#include "gc/g1/g1RedirtyCardsQueue.hpp" #include "gc/g1/g1RegionPinCache.inline.hpp" #include "gc/g1/g1RemSet.hpp" #include "gc/g1/g1RootProcessor.hpp" @@ -914,13 +913,8 @@ class G1STWRefProcProxyTask : public RefProcProxyTask { TaskTerminator _terminator; G1ScannerTasksQueueSet& _task_queues; - // Special closure for enqueuing discovered fields: during enqueue the card table - // may not be in shape to properly handle normal barrier calls (e.g. card marks - // in regions that failed evacuation, scribbling of various values by card table - // scan code). Additionally the regular barrier enqueues into the "global" - // DCQS, but during GC we need these to-be-refined entries in the GC local queue - // so that after clearing the card table, the redirty cards phase will properly - // mark all dirty cards to be picked up by refinement. + // G1 specific closure for marking discovered fields. Need to mark the card in the + // refinement table as the card table is in use by garbage collection. class G1EnqueueDiscoveredFieldClosure : public EnqueueDiscoveredFieldClosure { G1CollectedHeap* _g1h; G1ParScanThreadState* _pss; diff --git a/src/hotspot/share/gc/g1/g1YoungCollector.hpp b/src/hotspot/share/gc/g1/g1YoungCollector.hpp index 2c4929958fe..76d443b1a9f 100644 --- a/src/hotspot/share/gc/g1/g1YoungCollector.hpp +++ b/src/hotspot/share/gc/g1/g1YoungCollector.hpp @@ -45,7 +45,6 @@ class G1MonotonicArenaMemoryStats; class G1NewTracer; class G1ParScanThreadStateSet; class G1Policy; -class G1RedirtyCardsQueueSet; class G1RemSet; class G1SurvivorRegions; class G1YoungGCAllocationFailureInjector; diff --git a/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp b/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp index 5b13e8fc206..2737def7e84 100644 --- a/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp +++ b/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp @@ -287,7 +287,7 @@ public: _chunk_bitmap(mtGC) { _num_evac_fail_regions = _evac_failure_regions->num_regions_evac_failed(); - _num_chunks_per_region = G1CollectedHeap::get_chunks_per_region(); + _num_chunks_per_region = G1CollectedHeap::get_chunks_per_region_for_scan(); _chunk_size = static_cast(G1HeapRegion::GrainWords / _num_chunks_per_region); @@ -300,7 +300,7 @@ public: double worker_cost() const override { assert(_evac_failure_regions->has_regions_evac_failed(), "Should not call this if there were no evacuation failures"); - double workers_per_region = (double)G1CollectedHeap::get_chunks_per_region() / G1RestoreRetainedRegionChunksPerWorker; + double workers_per_region = (double)G1CollectedHeap::get_chunks_per_region_for_scan() / G1RestoreRetainedRegionChunksPerWorker; return workers_per_region * _evac_failure_regions->num_regions_evac_failed(); } @@ -480,43 +480,6 @@ public: } }; -class RedirtyLoggedCardTableEntryClosure : public G1CardTableEntryClosure { - size_t _num_dirtied; - G1CollectedHeap* _g1h; - G1CardTable* _g1_ct; - G1EvacFailureRegions* _evac_failure_regions; - - G1HeapRegion* region_for_card(CardValue* card_ptr) const { - return _g1h->heap_region_containing(_g1_ct->addr_for(card_ptr)); - } - - bool will_become_free(G1HeapRegion* hr) const { - // A region will be freed by during the FreeCollectionSet phase if the region is in the - // collection set and has not had an evacuation failure. - return _g1h->is_in_cset(hr) && !_evac_failure_regions->contains(hr->hrm_index()); - } - -public: - RedirtyLoggedCardTableEntryClosure(G1CollectedHeap* g1h, G1EvacFailureRegions* evac_failure_regions) : - G1CardTableEntryClosure(), - _num_dirtied(0), - _g1h(g1h), - _g1_ct(g1h->card_table()), - _evac_failure_regions(evac_failure_regions) { } - - void do_card_ptr(CardValue* card_ptr) override { - G1HeapRegion* hr = region_for_card(card_ptr); - - // Should only dirty cards in regions that won't be freed. - if (!will_become_free(hr)) { - *card_ptr = G1CardTable::dirty_card_val(); - _num_dirtied++; - } - } - - size_t num_dirtied() const { return _num_dirtied; } -}; - class G1PostEvacuateCollectionSetCleanupTask2::ProcessEvacuationFailedRegionsTask : public G1AbstractSubTask { G1EvacFailureRegions* _evac_failure_regions; G1HeapRegionClaimer _claimer; @@ -572,48 +535,6 @@ public: } }; -class G1PostEvacuateCollectionSetCleanupTask2::RedirtyLoggedCardsTask : public G1AbstractSubTask { - BufferNodeList* _rdc_buffers; - uint _num_buffer_lists; - G1EvacFailureRegions* _evac_failure_regions; - -public: - RedirtyLoggedCardsTask(G1EvacFailureRegions* evac_failure_regions, BufferNodeList* rdc_buffers, uint num_buffer_lists) : - G1AbstractSubTask(G1GCPhaseTimes::RedirtyCards), - _rdc_buffers(rdc_buffers), - _num_buffer_lists(num_buffer_lists), - _evac_failure_regions(evac_failure_regions) { } - - double worker_cost() const override { - // Needs more investigation. - return G1CollectedHeap::heap()->workers()->active_workers(); - } - - void do_work(uint worker_id) override { - RedirtyLoggedCardTableEntryClosure cl(G1CollectedHeap::heap(), _evac_failure_regions); - - uint start = worker_id; - for (uint i = 0; i < _num_buffer_lists; i++) { - uint index = (start + i) % _num_buffer_lists; - - BufferNode* next = AtomicAccess::load(&_rdc_buffers[index]._head); - BufferNode* tail = AtomicAccess::load(&_rdc_buffers[index]._tail); - - while (next != nullptr) { - BufferNode* node = next; - next = AtomicAccess::cmpxchg(&_rdc_buffers[index]._head, node, (node != tail ) ? node->next() : nullptr); - if (next == node) { - cl.apply_to_buffer(node, worker_id); - next = (node != tail ) ? node->next() : nullptr; - } else { - break; // If there is contention, move to the next BufferNodeList - } - } - } - record_work_item(worker_id, 0, cl.num_dirtied()); - } -}; - // Helper class to keep statistics for the collection set freeing class FreeCSetStats { size_t _before_used_bytes; // Usage in regions successfully evacuate @@ -797,7 +718,6 @@ public: JFREventForRegion event(r, _worker_id); TimerForRegion timer(timer_for_region(r)); - if (r->is_young()) { assert_tracks_surviving_words(r); r->record_surv_words_in_group(_surviving_young_words[r->young_index_in_cset()]); @@ -908,24 +828,34 @@ public: } }; -class G1PostEvacuateCollectionSetCleanupTask2::ResizeTLABsTask : public G1AbstractSubTask { +class G1PostEvacuateCollectionSetCleanupTask2::ResizeTLABsAndSwapCardTableTask : public G1AbstractSubTask { G1JavaThreadsListClaimer _claimer; // There is not much work per thread so the number of threads per worker is high. static const uint ThreadsPerWorker = 250; public: - ResizeTLABsTask() : G1AbstractSubTask(G1GCPhaseTimes::ResizeThreadLABs), _claimer(ThreadsPerWorker) { } + ResizeTLABsAndSwapCardTableTask() + : G1AbstractSubTask(G1GCPhaseTimes::ResizeThreadLABs), _claimer(ThreadsPerWorker) + { + G1BarrierSet::g1_barrier_set()->swap_global_card_table(); + } void do_work(uint worker_id) override { - class ResizeClosure : public ThreadClosure { + + class ResizeAndSwapCardTableClosure : public ThreadClosure { public: void do_thread(Thread* thread) { - static_cast(thread)->tlab().resize(); + if (UseTLAB && ResizeTLAB) { + static_cast(thread)->tlab().resize(); + } + + G1BarrierSet::g1_barrier_set()->update_card_table_base(thread); } - } cl; - _claimer.apply(&cl); + } resize_and_swap_cl; + + _claimer.apply(&resize_and_swap_cl); } double worker_cost() const override { @@ -968,13 +898,8 @@ G1PostEvacuateCollectionSetCleanupTask2::G1PostEvacuateCollectionSetCleanupTask2 if (evac_failure_regions->has_regions_evac_failed()) { add_parallel_task(new ProcessEvacuationFailedRegionsTask(evac_failure_regions)); } - add_parallel_task(new RedirtyLoggedCardsTask(evac_failure_regions, - per_thread_states->rdc_buffers(), - per_thread_states->num_workers())); - if (UseTLAB && ResizeTLAB) { - add_parallel_task(new ResizeTLABsTask()); - } + add_parallel_task(new ResizeTLABsAndSwapCardTableTask()); add_parallel_task(new FreeCollectionSetTask(evacuation_info, per_thread_states->surviving_young_words(), evac_failure_regions)); diff --git a/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.hpp b/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.hpp index ad850af2eac..bc3a08e2080 100644 --- a/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.hpp +++ b/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -55,9 +55,8 @@ public: // - Eagerly Reclaim Humongous Objects (s) // - Update Derived Pointers (s) // - Clear Retained Region Data (on evacuation failure) -// - Redirty Logged Cards // - Free Collection Set -// - Resize TLABs +// - Resize TLABs and Swap Card Table // - Reset the reusable PartialArrayStateManager. class G1PostEvacuateCollectionSetCleanupTask2 : public G1BatchedTask { class EagerlyReclaimHumongousObjectsTask; @@ -66,9 +65,8 @@ class G1PostEvacuateCollectionSetCleanupTask2 : public G1BatchedTask { #endif class ProcessEvacuationFailedRegionsTask; - class RedirtyLoggedCardsTask; class FreeCollectionSetTask; - class ResizeTLABsTask; + class ResizeTLABsAndSwapCardTableTask; class ResetPartialArrayStateManagerTask; public: diff --git a/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.cpp b/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.cpp index 7214d624def..b11213ddeb3 100644 --- a/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.cpp +++ b/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.cpp @@ -24,7 +24,6 @@ #include "gc/g1/g1CollectedHeap.inline.hpp" #include "gc/g1/g1ConcurrentRefineStats.hpp" -#include "gc/g1/g1DirtyCardQueue.hpp" #include "gc/g1/g1RegionPinCache.inline.hpp" #include "gc/g1/g1ThreadLocalData.hpp" #include "gc/g1/g1YoungGCPreEvacuateTasks.hpp" @@ -35,23 +34,21 @@ #include "runtime/thread.inline.hpp" #include "runtime/threads.hpp" -class G1PreEvacuateCollectionSetBatchTask::JavaThreadRetireTLABAndFlushLogs : public G1AbstractSubTask { +class G1PreEvacuateCollectionSetBatchTask::JavaThreadRetireTLABs : public G1AbstractSubTask { G1JavaThreadsListClaimer _claimer; // Per worker thread statistics. ThreadLocalAllocStats* _local_tlab_stats; - G1ConcurrentRefineStats* _local_refinement_stats; uint _num_workers; // There is relatively little work to do per thread. static const uint ThreadsPerWorker = 250; - struct RetireTLABAndFlushLogsClosure : public ThreadClosure { + struct RetireTLABClosure : public ThreadClosure { ThreadLocalAllocStats _tlab_stats; - G1ConcurrentRefineStats _refinement_stats; - RetireTLABAndFlushLogsClosure() : _tlab_stats(), _refinement_stats() { } + RetireTLABClosure() : _tlab_stats() { } void do_thread(Thread* thread) override { assert(thread->is_Java_thread(), "must be"); @@ -61,37 +58,29 @@ class G1PreEvacuateCollectionSetBatchTask::JavaThreadRetireTLABAndFlushLogs : pu if (UseTLAB) { thread->retire_tlab(&_tlab_stats); } - // Concatenate logs. - G1DirtyCardQueueSet& qset = G1BarrierSet::dirty_card_queue_set(); - _refinement_stats += qset.concatenate_log_and_stats(thread); // Flush region pin count cache. G1ThreadLocalData::pin_count_cache(thread).flush(); } }; public: - JavaThreadRetireTLABAndFlushLogs() : - G1AbstractSubTask(G1GCPhaseTimes::RetireTLABsAndFlushLogs), + JavaThreadRetireTLABs() : + G1AbstractSubTask(G1GCPhaseTimes::RetireTLABs), _claimer(ThreadsPerWorker), _local_tlab_stats(nullptr), - _local_refinement_stats(nullptr), _num_workers(0) { } - ~JavaThreadRetireTLABAndFlushLogs() { - static_assert(std::is_trivially_destructible::value, "must be"); - FREE_C_HEAP_ARRAY(G1ConcurrentRefineStats, _local_refinement_stats); - + ~JavaThreadRetireTLABs() { static_assert(std::is_trivially_destructible::value, "must be"); FREE_C_HEAP_ARRAY(ThreadLocalAllocStats, _local_tlab_stats); } void do_work(uint worker_id) override { - RetireTLABAndFlushLogsClosure tc; + RetireTLABClosure tc; _claimer.apply(&tc); _local_tlab_stats[worker_id] = tc._tlab_stats; - _local_refinement_stats[worker_id] = tc._refinement_stats; } double worker_cost() const override { @@ -101,11 +90,9 @@ public: void set_max_workers(uint max_workers) override { _num_workers = max_workers; _local_tlab_stats = NEW_C_HEAP_ARRAY(ThreadLocalAllocStats, _num_workers, mtGC); - _local_refinement_stats = NEW_C_HEAP_ARRAY(G1ConcurrentRefineStats, _num_workers, mtGC); for (uint i = 0; i < _num_workers; i++) { ::new (&_local_tlab_stats[i]) ThreadLocalAllocStats(); - ::new (&_local_refinement_stats[i]) G1ConcurrentRefineStats(); } } @@ -116,85 +103,15 @@ public: } return result; } - - G1ConcurrentRefineStats refinement_stats() const { - G1ConcurrentRefineStats result; - for (uint i = 0; i < _num_workers; i++) { - result += _local_refinement_stats[i]; - } - return result; - } -}; - -class G1PreEvacuateCollectionSetBatchTask::NonJavaThreadFlushLogs : public G1AbstractSubTask { - struct FlushLogsClosure : public ThreadClosure { - G1ConcurrentRefineStats _refinement_stats; - - FlushLogsClosure() : _refinement_stats() { } - - void do_thread(Thread* thread) override { - G1DirtyCardQueueSet& qset = G1BarrierSet::dirty_card_queue_set(); - _refinement_stats += qset.concatenate_log_and_stats(thread); - - assert(G1ThreadLocalData::pin_count_cache(thread).count() == 0, "NonJava thread has pinned Java objects"); - } - } _tc; - -public: - NonJavaThreadFlushLogs() : G1AbstractSubTask(G1GCPhaseTimes::NonJavaThreadFlushLogs), _tc() { } - - void do_work(uint worker_id) override { - Threads::non_java_threads_do(&_tc); - } - - double worker_cost() const override { - return 1.0; - } - - G1ConcurrentRefineStats refinement_stats() const { return _tc._refinement_stats; } }; G1PreEvacuateCollectionSetBatchTask::G1PreEvacuateCollectionSetBatchTask() : G1BatchedTask("Pre Evacuate Prepare", G1CollectedHeap::heap()->phase_times()), - _old_pending_cards(G1BarrierSet::dirty_card_queue_set().num_cards()), - _java_retire_task(new JavaThreadRetireTLABAndFlushLogs()), - _non_java_retire_task(new NonJavaThreadFlushLogs()) { + _java_retire_task(new JavaThreadRetireTLABs()) { - // Disable mutator refinement until concurrent refinement decides otherwise. - G1BarrierSet::dirty_card_queue_set().set_mutator_refinement_threshold(SIZE_MAX); - - add_serial_task(_non_java_retire_task); add_parallel_task(_java_retire_task); } -static void verify_empty_dirty_card_logs() { -#ifdef ASSERT - ResourceMark rm; - - struct Verifier : public ThreadClosure { - Verifier() {} - void do_thread(Thread* t) override { - G1DirtyCardQueue& queue = G1ThreadLocalData::dirty_card_queue(t); - assert(queue.is_empty(), "non-empty dirty card queue for thread %s", t->name()); - } - } verifier; - Threads::threads_do(&verifier); -#endif -} - G1PreEvacuateCollectionSetBatchTask::~G1PreEvacuateCollectionSetBatchTask() { _java_retire_task->tlab_stats().publish(); - - G1DirtyCardQueueSet& qset = G1BarrierSet::dirty_card_queue_set(); - - G1ConcurrentRefineStats total_refinement_stats; - total_refinement_stats += _java_retire_task->refinement_stats(); - total_refinement_stats += _non_java_retire_task->refinement_stats(); - qset.update_refinement_stats(total_refinement_stats); - - verify_empty_dirty_card_logs(); - - size_t pending_cards = qset.num_cards(); - size_t thread_buffer_cards = pending_cards - _old_pending_cards; - G1CollectedHeap::heap()->policy()->record_concurrent_refinement_stats(pending_cards, thread_buffer_cards); } diff --git a/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.hpp b/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.hpp index 791031d979f..7574862872c 100644 --- a/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.hpp +++ b/src/hotspot/share/gc/g1/g1YoungGCPreEvacuateTasks.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,18 +28,13 @@ #include "gc/g1/g1BatchedTask.hpp" // Set of pre evacuate collection set tasks containing ("s" means serial): -// - Retire TLAB and Flush Logs (Java threads) +// - Retire TLABs (Java threads) // - Flush pin count cache (Java threads) -// - Flush Logs (s) (Non-Java threads) class G1PreEvacuateCollectionSetBatchTask : public G1BatchedTask { - class JavaThreadRetireTLABAndFlushLogs; - class NonJavaThreadFlushLogs; - - size_t _old_pending_cards; + class JavaThreadRetireTLABs; // References to the tasks to retain access to statistics. - JavaThreadRetireTLABAndFlushLogs* _java_retire_task; - NonJavaThreadFlushLogs* _non_java_retire_task; + JavaThreadRetireTLABs* _java_retire_task; public: G1PreEvacuateCollectionSetBatchTask(); diff --git a/src/hotspot/share/gc/g1/g1_globals.hpp b/src/hotspot/share/gc/g1/g1_globals.hpp index 1c712492f74..b338c11d5be 100644 --- a/src/hotspot/share/gc/g1/g1_globals.hpp +++ b/src/hotspot/share/gc/g1/g1_globals.hpp @@ -162,6 +162,11 @@ "a single expand attempt.") \ range(0, 100) \ \ + product(size_t, G1PerThreadPendingCardThreshold, 256, DIAGNOSTIC, \ + "Number of pending cards allowed on the card table per GC " \ + "worker thread before considering starting refinement.") \ + range(0, UINT_MAX) \ + \ product(uint, G1ShrinkByPercentOfAvailable, 50, DIAGNOSTIC, \ "When shrinking, maximum % of free space to free for a single " \ "shrink attempt.") \ @@ -188,10 +193,6 @@ "bound of acceptable deviation range.") \ constraint(G1CPUUsageShrinkConstraintFunc, AfterErgo) \ \ - product(size_t, G1UpdateBufferSize, 256, \ - "Size of an update buffer") \ - constraint(G1UpdateBufferSizeConstraintFunc, AfterErgo) \ - \ product(uint, G1RSetUpdatingPauseTimePercent, 10, \ "A target percentage of time that is allowed to be spend on " \ "processing remembered set update buffers during the collection " \ diff --git a/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.cpp b/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.cpp index 488a9c7aac9..2b084b387bc 100644 --- a/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.cpp +++ b/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.cpp @@ -206,12 +206,6 @@ JVMFlag::Error G1SATBBufferSizeConstraintFunc(size_t value, bool verbose) { verbose); } -JVMFlag::Error G1UpdateBufferSizeConstraintFunc(size_t value, bool verbose) { - return buffer_size_constraint_helper(FLAG_MEMBER_ENUM(G1UpdateBufferSize), - value, - verbose); -} - JVMFlag::Error gc_cpu_usage_threshold_helper(JVMFlagsEnum flagid, uint value, bool verbose) { diff --git a/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.hpp b/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.hpp index 89f05d73dcc..b2c7bb6dc96 100644 --- a/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.hpp +++ b/src/hotspot/share/gc/g1/jvmFlagConstraintsG1.hpp @@ -47,7 +47,6 @@ \ /* G1 PtrQueue buffer size constraints */ \ f(size_t, G1SATBBufferSizeConstraintFunc) \ - f(size_t, G1UpdateBufferSizeConstraintFunc) \ \ /* G1 GC deviation counter threshold constraints */ \ f(uint, G1CPUUsageExpandConstraintFunc) \ diff --git a/src/hotspot/share/gc/g1/vmStructs_g1.hpp b/src/hotspot/share/gc/g1/vmStructs_g1.hpp index 651808b4ba0..67c930e1b63 100644 --- a/src/hotspot/share/gc/g1/vmStructs_g1.hpp +++ b/src/hotspot/share/gc/g1/vmStructs_g1.hpp @@ -82,8 +82,7 @@ declare_constant(G1HeapRegionType::StartsHumongousTag) \ declare_constant(G1HeapRegionType::ContinuesHumongousTag) \ declare_constant(G1HeapRegionType::OldMask) \ - declare_constant(BarrierSet::G1BarrierSet) \ - declare_constant(G1CardTable::g1_young_gen) + declare_constant(BarrierSet::G1BarrierSet) #define VM_TYPES_G1GC(declare_type, \ declare_toplevel_type, \ @@ -100,7 +99,6 @@ declare_toplevel_type(PtrQueue) \ declare_toplevel_type(G1HeapRegionType) \ declare_toplevel_type(SATBMarkQueue) \ - declare_toplevel_type(G1DirtyCardQueue) \ \ declare_toplevel_type(G1CollectedHeap*) \ declare_toplevel_type(G1HeapRegion*) \ diff --git a/src/hotspot/share/gc/shared/bufferNodeList.cpp b/src/hotspot/share/gc/shared/bufferNodeList.cpp deleted file mode 100644 index 768f40e0985..00000000000 --- a/src/hotspot/share/gc/shared/bufferNodeList.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#include "gc/shared/bufferNodeList.hpp" -#include "utilities/debug.hpp" - -BufferNodeList::BufferNodeList() : - _head(nullptr), _tail(nullptr), _entry_count(0) {} - -BufferNodeList::BufferNodeList(BufferNode* head, - BufferNode* tail, - size_t entry_count) : - _head(head), _tail(tail), _entry_count(entry_count) -{ - assert((_head == nullptr) == (_tail == nullptr), "invariant"); - assert((_head == nullptr) == (_entry_count == 0), "invariant"); -} diff --git a/src/hotspot/share/gc/shared/cardTable.cpp b/src/hotspot/share/gc/shared/cardTable.cpp index 76b8eb4d718..34f1847befe 100644 --- a/src/hotspot/share/gc/shared/cardTable.cpp +++ b/src/hotspot/share/gc/shared/cardTable.cpp @@ -225,6 +225,9 @@ uintx CardTable::ct_max_alignment_constraint() { #ifndef PRODUCT void CardTable::verify_region(MemRegion mr, CardValue val, bool val_equals) { + if (mr.is_empty()) { + return; + } CardValue* start = byte_for(mr.start()); CardValue* end = byte_for(mr.last()); bool failures = false; @@ -255,7 +258,8 @@ void CardTable::verify_dirty_region(MemRegion mr) { } #endif -void CardTable::print_on(outputStream* st) const { - st->print_cr("Card table byte_map: [" PTR_FORMAT "," PTR_FORMAT "] _byte_map_base: " PTR_FORMAT, +void CardTable::print_on(outputStream* st, const char* description) const { + st->print_cr("%s table byte_map: [" PTR_FORMAT "," PTR_FORMAT "] _byte_map_base: " PTR_FORMAT, + description, p2i(_byte_map), p2i(_byte_map + _byte_map_size), p2i(_byte_map_base)); } diff --git a/src/hotspot/share/gc/shared/cardTable.hpp b/src/hotspot/share/gc/shared/cardTable.hpp index ee41be06be0..63dcfe7aecb 100644 --- a/src/hotspot/share/gc/shared/cardTable.hpp +++ b/src/hotspot/share/gc/shared/cardTable.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -203,12 +203,12 @@ public: virtual bool is_in_young(const void* p) const = 0; - // Print a description of the memory for the card table - virtual void print_on(outputStream* st) const; + // Print card table information. + void print_on(outputStream* st, const char* description = "Card") const; // val_equals -> it will check that all cards covered by mr equal val // !val_equals -> it will check that all cards covered by mr do not equal val - void verify_region(MemRegion mr, CardValue val, bool val_equals) PRODUCT_RETURN; + virtual void verify_region(MemRegion mr, CardValue val, bool val_equals) PRODUCT_RETURN; void verify_not_dirty_region(MemRegion mr) PRODUCT_RETURN; void verify_dirty_region(MemRegion mr) PRODUCT_RETURN; }; diff --git a/src/hotspot/share/gc/shared/workerDataArray.hpp b/src/hotspot/share/gc/shared/workerDataArray.hpp index b2a81bc9482..587f9bbd167 100644 --- a/src/hotspot/share/gc/shared/workerDataArray.hpp +++ b/src/hotspot/share/gc/shared/workerDataArray.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,7 +34,7 @@ template class WorkerDataArray : public CHeapObj { friend class WDAPrinter; public: - static const uint MaxThreadWorkItems = 9; + static const uint MaxThreadWorkItems = 10; private: T* _data; uint _length; diff --git a/src/hotspot/share/jvmci/jvmciRuntime.cpp b/src/hotspot/share/jvmci/jvmciRuntime.cpp index 137782f93ef..e75527235f0 100644 --- a/src/hotspot/share/jvmci/jvmciRuntime.cpp +++ b/src/hotspot/share/jvmci/jvmciRuntime.cpp @@ -589,10 +589,6 @@ void JVMCIRuntime::write_barrier_pre(JavaThread* thread, oopDesc* obj) { G1BarrierSetRuntime::write_ref_field_pre_entry(obj, thread); } -void JVMCIRuntime::write_barrier_post(JavaThread* thread, volatile CardValue* card_addr) { - G1BarrierSetRuntime::write_ref_field_post_entry(card_addr, thread); -} - #endif // INCLUDE_G1GC JRT_LEAF(jboolean, JVMCIRuntime::validate_object(JavaThread* thread, oopDesc* parent, oopDesc* child)) diff --git a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp index 3ddf7de0510..7ddb9be540a 100644 --- a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp +++ b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp @@ -560,6 +560,7 @@ declare_constant(BranchData::not_taken_off_set) \ \ declare_constant_with_value("CardTable::dirty_card", CardTable::dirty_card_val()) \ + declare_constant_with_value("CardTable::clean_card", CardTable::clean_card_val()) \ declare_constant_with_value("LockStack::_end_offset", LockStack::end_offset()) \ declare_constant_with_value("OMCache::oop_to_oop_difference", OMCache::oop_to_oop_difference()) \ declare_constant_with_value("OMCache::oop_to_monitor_difference", OMCache::oop_to_monitor_difference()) \ @@ -928,7 +929,6 @@ declare_function(JVMCIRuntime::vm_error) \ declare_function(JVMCIRuntime::load_and_clear_exception) \ G1GC_ONLY(declare_function(JVMCIRuntime::write_barrier_pre)) \ - G1GC_ONLY(declare_function(JVMCIRuntime::write_barrier_post)) \ SHENANDOAHGC_ONLY(declare_function(ShenandoahRuntime::load_reference_barrier_strong)) \ SHENANDOAHGC_ONLY(declare_function(ShenandoahRuntime::load_reference_barrier_strong_narrow)) \ SHENANDOAHGC_ONLY(declare_function(ShenandoahRuntime::load_reference_barrier_weak)) \ @@ -947,12 +947,10 @@ static_field(G1HeapRegion, LogOfHRGrainBytes, uint) #define VM_INT_CONSTANTS_JVMCI_G1GC(declare_constant, declare_constant_with_value, declare_preprocessor_constant) \ - declare_constant_with_value("G1CardTable::g1_young_gen", G1CardTable::g1_young_card_val()) \ declare_constant_with_value("G1ThreadLocalData::satb_mark_queue_active_offset", in_bytes(G1ThreadLocalData::satb_mark_queue_active_offset())) \ declare_constant_with_value("G1ThreadLocalData::satb_mark_queue_index_offset", in_bytes(G1ThreadLocalData::satb_mark_queue_index_offset())) \ declare_constant_with_value("G1ThreadLocalData::satb_mark_queue_buffer_offset", in_bytes(G1ThreadLocalData::satb_mark_queue_buffer_offset())) \ - declare_constant_with_value("G1ThreadLocalData::dirty_card_queue_index_offset", in_bytes(G1ThreadLocalData::dirty_card_queue_index_offset())) \ - declare_constant_with_value("G1ThreadLocalData::dirty_card_queue_buffer_offset", in_bytes(G1ThreadLocalData::dirty_card_queue_buffer_offset())) + declare_constant_with_value("G1ThreadLocalData::card_table_base_offset", in_bytes(G1ThreadLocalData::card_table_base_offset())) \ #endif // INCLUDE_G1GC diff --git a/src/hotspot/share/oops/oop.cpp b/src/hotspot/share/oops/oop.cpp index 51480c68c22..f874a39bf31 100644 --- a/src/hotspot/share/oops/oop.cpp +++ b/src/hotspot/share/oops/oop.cpp @@ -87,7 +87,16 @@ void oopDesc::print_value_on(outputStream* st) const { java_lang_String::print(obj, st); print_address_on(st); } else { - klass()->oop_print_value_on(obj, st); + Klass* k = klass_without_asserts(); + if (k == nullptr) { + st->print("null klass"); + } else if (!Metaspace::contains(k)) { + st->print("klass not in Metaspace"); + } else if (!k->is_klass()) { + st->print("klass not a Klass"); + } else { + k->oop_print_value_on(obj, st); + } } } diff --git a/src/hotspot/share/runtime/arguments.cpp b/src/hotspot/share/runtime/arguments.cpp index 6cfeb1dcb0f..b7ab68e143c 100644 --- a/src/hotspot/share/runtime/arguments.cpp +++ b/src/hotspot/share/runtime/arguments.cpp @@ -546,6 +546,7 @@ static SpecialFlag const special_jvm_flags[] = { { "MetaspaceReclaimPolicy", JDK_Version::undefined(), JDK_Version::jdk(21), JDK_Version::undefined() }, { "ZGenerational", JDK_Version::jdk(23), JDK_Version::jdk(24), JDK_Version::undefined() }, { "ZMarkStackSpaceLimit", JDK_Version::undefined(), JDK_Version::jdk(25), JDK_Version::undefined() }, + { "G1UpdateBufferSize", JDK_Version::undefined(), JDK_Version::jdk(26), JDK_Version::jdk(27) }, #if defined(AARCH64) { "NearCpool", JDK_Version::undefined(), JDK_Version::jdk(25), JDK_Version::undefined() }, #endif diff --git a/src/hotspot/share/runtime/cpuTimeCounters.cpp b/src/hotspot/share/runtime/cpuTimeCounters.cpp index c7e48441662..e5364550b6c 100644 --- a/src/hotspot/share/runtime/cpuTimeCounters.cpp +++ b/src/hotspot/share/runtime/cpuTimeCounters.cpp @@ -36,6 +36,8 @@ const char* CPUTimeGroups::to_string(CPUTimeType val) { return "gc_conc_mark"; case CPUTimeType::gc_conc_refine: return "gc_conc_refine"; + case CPUTimeType::gc_conc_refine_control: + return "gc_conc_refine_control"; case CPUTimeType::gc_service: return "gc_service"; case CPUTimeType::vm: @@ -53,6 +55,7 @@ bool CPUTimeGroups::is_gc_counter(CPUTimeType val) { case CPUTimeType::gc_parallel_workers: case CPUTimeType::gc_conc_mark: case CPUTimeType::gc_conc_refine: + case CPUTimeType::gc_conc_refine_control: case CPUTimeType::gc_service: return true; default: diff --git a/src/hotspot/share/runtime/cpuTimeCounters.hpp b/src/hotspot/share/runtime/cpuTimeCounters.hpp index efa44f9173d..9ad00492731 100644 --- a/src/hotspot/share/runtime/cpuTimeCounters.hpp +++ b/src/hotspot/share/runtime/cpuTimeCounters.hpp @@ -40,6 +40,7 @@ public: gc_parallel_workers, gc_conc_mark, gc_conc_refine, + gc_conc_refine_control, gc_service, vm, conc_dedup, diff --git a/src/hotspot/share/runtime/mutexLocker.cpp b/src/hotspot/share/runtime/mutexLocker.cpp index e0eafbc416b..8274d767e4e 100644 --- a/src/hotspot/share/runtime/mutexLocker.cpp +++ b/src/hotspot/share/runtime/mutexLocker.cpp @@ -98,15 +98,15 @@ Mutex* PerfDataManager_lock = nullptr; #if INCLUDE_G1GC Monitor* G1CGC_lock = nullptr; -Mutex* G1DetachedRefinementStats_lock = nullptr; Mutex* G1FreeList_lock = nullptr; Mutex* G1MarkStackChunkList_lock = nullptr; Mutex* G1MarkStackFreeList_lock = nullptr; Monitor* G1OldGCCount_lock = nullptr; Mutex* G1OldSets_lock = nullptr; -Mutex* G1Uncommit_lock = nullptr; +Mutex* G1ReviseYoungLength_lock = nullptr; Monitor* G1RootRegionScan_lock = nullptr; Mutex* G1RareEvent_lock = nullptr; +Mutex* G1Uncommit_lock = nullptr; #endif Mutex* Management_lock = nullptr; @@ -211,7 +211,6 @@ void mutex_init() { #if INCLUDE_G1GC if (UseG1GC) { MUTEX_DEFN(G1CGC_lock , PaddedMonitor, nosafepoint); - MUTEX_DEFN(G1DetachedRefinementStats_lock, PaddedMutex , nosafepoint-2); MUTEX_DEFN(G1FreeList_lock , PaddedMutex , service-1); MUTEX_DEFN(G1MarkStackChunkList_lock , PaddedMutex , nosafepoint); MUTEX_DEFN(G1MarkStackFreeList_lock , PaddedMutex , nosafepoint); @@ -341,8 +340,9 @@ void mutex_init() { #if INCLUDE_G1GC if (UseG1GC) { - MUTEX_DEFL(G1OldGCCount_lock , PaddedMonitor, Threads_lock, true); - MUTEX_DEFL(G1RareEvent_lock , PaddedMutex , Threads_lock, true); + MUTEX_DEFL(G1OldGCCount_lock , PaddedMonitor, Threads_lock, true); + MUTEX_DEFL(G1RareEvent_lock , PaddedMutex , Threads_lock, true); + MUTEX_DEFL(G1ReviseYoungLength_lock , PaddedMutex , Threads_lock, true); } #endif diff --git a/src/hotspot/share/runtime/mutexLocker.hpp b/src/hotspot/share/runtime/mutexLocker.hpp index 3a73edc7bf2..8cd408c99c9 100644 --- a/src/hotspot/share/runtime/mutexLocker.hpp +++ b/src/hotspot/share/runtime/mutexLocker.hpp @@ -93,13 +93,13 @@ extern Mutex* FullGCALot_lock; // a lock to make FullGCALot MT #if INCLUDE_G1GC extern Monitor* G1CGC_lock; // used for coordination between fore- & background G1 concurrent GC threads. -extern Mutex* G1DetachedRefinementStats_lock; // Lock protecting detached refinement stats for G1. extern Mutex* G1FreeList_lock; // protects the G1 free region list during safepoints extern Mutex* G1MarkStackChunkList_lock; // Protects access to the G1 global mark stack chunk list. extern Mutex* G1MarkStackFreeList_lock; // Protects access to the G1 global mark stack free list. extern Monitor* G1OldGCCount_lock; // in support of "concurrent" full gc extern Mutex* G1OldSets_lock; // protects the G1 old region sets extern Mutex* G1RareEvent_lock; // Synchronizes (rare) parallel GC operations. +extern Mutex* G1ReviseYoungLength_lock; // Protects access to young gen length revising operations. extern Monitor* G1RootRegionScan_lock; // used to notify that the G1 CM threads have finished scanning the root regions extern Mutex* G1Uncommit_lock; // protects the G1 uncommit list when not at safepoints #endif diff --git a/src/hotspot/share/runtime/vmOperation.hpp b/src/hotspot/share/runtime/vmOperation.hpp index 89a806bb75d..ada5014beee 100644 --- a/src/hotspot/share/runtime/vmOperation.hpp +++ b/src/hotspot/share/runtime/vmOperation.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -59,6 +59,7 @@ template(G1PauseRemark) \ template(G1PauseCleanup) \ template(G1TryInitiateConcMark) \ + template(G1RendezvousGCThreads) \ template(ZMarkEndOld) \ template(ZMarkEndYoung) \ template(ZMarkFlushOperation) \ diff --git a/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java b/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java index a9df0019ab1..01e015d50cb 100644 --- a/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java +++ b/test/hotspot/jtreg/compiler/gcbarriers/TestG1BarrierGeneration.java @@ -506,10 +506,10 @@ public class TestG1BarrierGeneration { @Test @IR(failOn = IRNode.SAFEPOINT) @IR(applyIfAnd = {"UseCompressedOops", "false", "ReduceInitialCardMarks", "false"}, - counts = {IRNode.G1_STORE_P_WITH_BARRIER_FLAG, POST_ONLY, "1"}, + counts = {IRNode.G1_STORE_P_WITH_BARRIER_FLAG, POST_ONLY, ">1"}, phase = CompilePhase.FINAL_CODE) @IR(applyIfAnd = {"UseCompressedOops", "true", "ReduceInitialCardMarks", "false"}, - counts = {IRNode.G1_ENCODE_P_AND_STORE_N_WITH_BARRIER_FLAG, POST_ONLY, "1"}, + counts = {IRNode.G1_ENCODE_P_AND_STORE_N_WITH_BARRIER_FLAG, POST_ONLY, ">1"}, phase = CompilePhase.FINAL_CODE) @IR(applyIfAnd = {"UseCompressedOops", "false", "ReduceInitialCardMarks", "true"}, failOn = {IRNode.G1_STORE_P_WITH_BARRIER_FLAG, ANY}, diff --git a/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java b/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java index 17ae437358d..d28c0888579 100644 --- a/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java +++ b/test/hotspot/jtreg/gc/g1/TestGCLogMessages.java @@ -108,8 +108,7 @@ public class TestGCLogMessages { new LogMessageWithLevel("Other:", Level.INFO), // Pre Evacuate Collection Set - new LogMessageWithLevel("JT Retire TLABs And Flush Logs \\(ms\\):", Level.DEBUG), - new LogMessageWithLevel("Non-JT Flush Logs \\(ms\\):", Level.DEBUG), + new LogMessageWithLevel("JavaThread Retire TLABs \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Choose Collection Set:", Level.DEBUG), new LogMessageWithLevel("Region Register:", Level.DEBUG), new LogMessageWithLevel("Prepare Heap Roots:", Level.DEBUG), @@ -126,10 +125,11 @@ public class TestGCLogMessages { new LogMessageWithLevel("Merged Howl ArrayOfCards:", Level.DEBUG), new LogMessageWithLevel("Merged Howl BitMap:", Level.DEBUG), new LogMessageWithLevel("Merged Howl Full:", Level.DEBUG), - new LogMessageWithLevel("Log Buffers \\(ms\\):", Level.DEBUG), - new LogMessageWithLevel("Dirty Cards:", Level.DEBUG), - new LogMessageWithLevel("Merged Cards:", Level.DEBUG), - new LogMessageWithLevel("Skipped Cards:", Level.DEBUG), + new LogMessageWithLevel("Merged From RS Cards:", Level.DEBUG), + new LogMessageWithLevel("Total Cards:", Level.DEBUG), + new LogMessageWithLevel("Merge Refinement Table:", Level.DEBUG), + new LogMessageWithLevel("Sweep \\(ms\\):", Level.DEBUG), + // Evacuate Collection Set new LogMessageWithLevel("Ext Root Scanning \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Thread Roots \\(ms\\):", Level.TRACE), @@ -173,15 +173,16 @@ public class TestGCLogMessages { new LogMessageWithLevel("Merge Per-Thread State \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("LAB Waste:", Level.DEBUG), new LogMessageWithLevel("LAB Undo Waste:", Level.DEBUG), - new LogMessageWithLevel("Evac Fail Extra Cards:", Level.DEBUG), - new LogMessageWithLevel("Clear Logged Cards \\(ms\\):", Level.DEBUG), + new LogMessageWithLevel("Pending Cards:", Level.DEBUG), + new LogMessageWithLevel("To-Young-Gen Cards:", Level.DEBUG), + new LogMessageWithLevel("Evac-Fail Cards:", Level.DEBUG), + new LogMessageWithLevel("Marked Cards:", Level.DEBUG), + new LogMessageWithLevel("Clear Pending Cards \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Recalculate Used Memory \\(ms\\):", Level.DEBUG), // Post Evacuate Cleanup 2 new LogMessageWithLevel("Post Evacuate Cleanup 2:", Level.DEBUG), new LogMessageWithLevelC2OrJVMCIOnly("Update Derived Pointers", Level.DEBUG), - new LogMessageWithLevel("Redirty Logged Cards \\(ms\\):", Level.DEBUG), - new LogMessageWithLevel("Redirtied Cards:", Level.DEBUG), new LogMessageWithLevel("Resize TLABs \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Free Collection Set \\(ms\\):", Level.DEBUG), new LogMessageWithLevel("Serial Free Collection Set:", Level.TRACE), @@ -243,9 +244,7 @@ public class TestGCLogMessages { } LogMessageWithLevel concRefineMessages[] = new LogMessageWithLevel[] { - new LogMessageWithLevel("Mutator refinement: ", Level.DEBUG), - new LogMessageWithLevel("Concurrent refinement: ", Level.DEBUG), - new LogMessageWithLevel("Total refinement: ", Level.DEBUG), + new LogMessageWithLevel("Refinement: sweep: ", Level.DEBUG), // "Concurrent refinement rate" optionally printed if any. // "Generate dirty cards rate" optionally printed if any. }; diff --git a/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java b/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java index 033b74f7eb1..d4b47422c38 100644 --- a/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java +++ b/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/TestOptionsWithRanges.java @@ -235,7 +235,6 @@ public class TestOptionsWithRanges { */ excludeTestMaxRange("ConcGCThreads"); excludeTestMaxRange("G1ConcRefinementThreads"); - excludeTestMaxRange("G1UpdateBufferSize"); excludeTestMaxRange("InitialHeapSize"); excludeTestMaxRange("MaxHeapSize"); excludeTestMaxRange("MaxRAM"); diff --git a/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java b/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java index c7f8badf83b..5615cce983a 100644 --- a/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java +++ b/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestIRMatching.java @@ -1486,7 +1486,7 @@ class CompilationOutputOfFails { @Test @IR(failOn = IRNode.ALLOC) - @IR(counts = {IRNode.COUNTED_LOOP, "1"}) // not fail + @IR(counts = {IRNode.COUNTED_LOOP, ">1"}) // not fail public void macro3() { for (int i = 0; i < 100; i++) { obj = new Object(); diff --git a/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.java b/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.java index 1ab01e8179f..eff35559626 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.java +++ b/test/hotspot/jtreg/vmTestbase/gc/ArrayJuggle/Juggle2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,6 +30,11 @@ /* @test @key stress randomness @library /vmTestbase /test/lib @run main/othervm -Xlog:gc=debug:gc.log gc.ArrayJuggle.Juggle2 */ /* @test @key stress randomness @library /vmTestbase /test/lib @run main/othervm -Xlog:gc=debug:gc.log gc.ArrayJuggle.Juggle2 -tg */ +/* + * The next test stresses the interaction between (mostly) full garbage collections and refinement. + */ +/* @test @key stress randomness @library /vmTestbase /test/lib @run main/othervm -XX:-G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=0 -XX:G1HeapRegionSize=1m -XX:G1RSetUpdatingPauseTimePercent=0 -XX:+UnlockDiagnosticVMOptions -XX:G1PerThreadPendingCardThreshold=0 -XX:+VerifyAfterGC -Xlog:gc=debug,gc+refine=debug:gc.log gc.ArrayJuggle.Juggle2 -tg */ + package gc.ArrayJuggle; import nsk.share.test.*; diff --git a/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java b/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java index 568104a7b50..d69d47f1911 100644 --- a/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java +++ b/test/jdk/jdk/jfr/event/gc/collection/TestG1ParallelPhases.java @@ -87,8 +87,6 @@ public class TestG1ParallelPhases { .collect(toSet()); Set allPhases = of( - "RetireTLABsAndFlushLogs", - "NonJavaThreadFlushLogs", "ExtRootScan", "ThreadRoots", "VM Global", @@ -100,31 +98,32 @@ public class TestG1ParallelPhases { "CMRefRoots", "MergeER", "MergeRS", - "MergeLB", "ScanHR", "CodeRoots", "ObjCopy", "Termination", - "RedirtyCards", "RecalculateUsed", "ResizeTLABs", "FreeCSet", "UpdateDerivedPointers", "EagerlyReclaimHumongousObjects", "ResetPartialArrayStateManager", - "ClearLoggedCards", + "ClearPendingCards", "MergePSS", "NonYoungFreeCSet", "YoungFreeCSet", "RebuildFreeList", "SampleCandidates", "ResetMarkingState", - "NoteStartOfMark" + "NoteStartOfMark", + "RetireTLABs" ); // Some GC phases may or may not occur depending on environment. Filter them out // since we can not reliably guarantee that they occur (or not). Set optPhases = of( + // Does not always occur + "SweepRT", // The following phases only occur on evacuation failure. "RestoreEvacuationFailedRegions", "RemoveSelfForwards", From 3c6ef5e27ae3585b48e9599020e4323bf9ed381e Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Mon, 22 Sep 2025 14:15:51 +0000 Subject: [PATCH 156/556] 8368102: Don't store macros in spec.gmk Reviewed-by: erikj --- make/autoconf/flags-cflags.m4 | 31 ------------------ make/autoconf/flags-ldflags.m4 | 2 +- make/autoconf/spec.gmk.template | 9 ------ make/common/JdkNativeCompilation.gmk | 45 ++++++++++++++++++++++++-- make/common/modules/LauncherCommon.gmk | 4 +-- make/common/native/Link.gmk | 22 ++++++++++++- make/hotspot/lib/CompileGtest.gmk | 2 +- make/hotspot/lib/JvmFeatures.gmk | 2 +- 8 files changed, 69 insertions(+), 48 deletions(-) diff --git a/make/autoconf/flags-cflags.m4 b/make/autoconf/flags-cflags.m4 index 0e2825d14b0..9d58a280998 100644 --- a/make/autoconf/flags-cflags.m4 +++ b/make/autoconf/flags-cflags.m4 @@ -37,56 +37,25 @@ AC_DEFUN([FLAGS_SETUP_SHARED_LIBS], if test "x$TOOLCHAIN_TYPE" = xgcc; then # Default works for linux, might work on other platforms as well. SHARED_LIBRARY_FLAGS='-shared' - # --disable-new-dtags forces use of RPATH instead of RUNPATH for rpaths. - # This protects internal library dependencies within the JDK from being - # overridden using LD_LIBRARY_PATH. See JDK-8326891 for more information. - SET_EXECUTABLE_ORIGIN='-Wl,-rpath,\$$ORIGIN[$]1 -Wl,--disable-new-dtags' - SET_SHARED_LIBRARY_ORIGIN="-Wl,-z,origin $SET_EXECUTABLE_ORIGIN" - SET_SHARED_LIBRARY_NAME='-Wl,-soname=[$]1' elif test "x$TOOLCHAIN_TYPE" = xclang; then if test "x$OPENJDK_TARGET_OS" = xmacosx; then # Linking is different on MacOSX SHARED_LIBRARY_FLAGS="-dynamiclib -compatibility_version 1.0.0 -current_version 1.0.0" - SET_EXECUTABLE_ORIGIN='-Wl,-rpath,@loader_path$(or [$]1,/.)' - SET_SHARED_LIBRARY_ORIGIN="$SET_EXECUTABLE_ORIGIN" - SET_SHARED_LIBRARY_NAME='-Wl,-install_name,@rpath/[$]1' elif test "x$OPENJDK_TARGET_OS" = xaix; then # Linking is different on aix SHARED_LIBRARY_FLAGS="-shared -Wl,-bM:SRE -Wl,-bnoentry" - SET_EXECUTABLE_ORIGIN="" - SET_SHARED_LIBRARY_ORIGIN='' - SET_SHARED_LIBRARY_NAME='' else # Default works for linux, might work on other platforms as well. SHARED_LIBRARY_FLAGS='-shared' - SET_EXECUTABLE_ORIGIN='-Wl,-rpath,\$$ORIGIN[$]1' - if test "x$OPENJDK_TARGET_OS" = xlinux; then - SET_EXECUTABLE_ORIGIN="$SET_EXECUTABLE_ORIGIN -Wl,--disable-new-dtags" - fi - SET_SHARED_LIBRARY_NAME='-Wl,-soname=[$]1' - - # arm specific settings - if test "x$OPENJDK_TARGET_CPU" = "xarm"; then - # '-Wl,-z,origin' isn't used on arm. - SET_SHARED_LIBRARY_ORIGIN='-Wl,-rpath,\$$$$ORIGIN[$]1' - else - SET_SHARED_LIBRARY_ORIGIN="-Wl,-z,origin $SET_EXECUTABLE_ORIGIN" - fi fi elif test "x$TOOLCHAIN_TYPE" = xmicrosoft; then SHARED_LIBRARY_FLAGS="-dll" - SET_EXECUTABLE_ORIGIN='' - SET_SHARED_LIBRARY_ORIGIN='' - SET_SHARED_LIBRARY_NAME='' fi - AC_SUBST(SET_EXECUTABLE_ORIGIN) - AC_SUBST(SET_SHARED_LIBRARY_ORIGIN) - AC_SUBST(SET_SHARED_LIBRARY_NAME) AC_SUBST(SHARED_LIBRARY_FLAGS) ]) diff --git a/make/autoconf/flags-ldflags.m4 b/make/autoconf/flags-ldflags.m4 index a12a6e7f9a6..651be3a1913 100644 --- a/make/autoconf/flags-ldflags.m4 +++ b/make/autoconf/flags-ldflags.m4 @@ -98,7 +98,7 @@ AC_DEFUN([FLAGS_SETUP_LDFLAGS_HELPER], # Setup OS-dependent LDFLAGS if test "x$OPENJDK_TARGET_OS" = xmacosx && test "x$TOOLCHAIN_TYPE" = xclang; then - # FIXME: We should really generalize SET_SHARED_LIBRARY_ORIGIN instead. + # FIXME: We should really generalize SetSharedLibraryOrigin instead. OS_LDFLAGS_JVM_ONLY="-Wl,-rpath,@loader_path/. -Wl,-rpath,@loader_path/.." OS_LDFLAGS="-mmacosx-version-min=$MACOSX_VERSION_MIN -Wl,-reproducible" fi diff --git a/make/autoconf/spec.gmk.template b/make/autoconf/spec.gmk.template index ab6bb51c27e..4eb5aa2f66d 100644 --- a/make/autoconf/spec.gmk.template +++ b/make/autoconf/spec.gmk.template @@ -624,17 +624,8 @@ ASFLAGS_DEBUG_SYMBOLS := @ASFLAGS_DEBUG_SYMBOLS@ # Compress (or not) jars COMPRESS_JARS := @COMPRESS_JARS@ -# Options to linker to specify the library name. -# (Note absence of := assignment, because we do not want to evaluate the macro body here) -SET_SHARED_LIBRARY_NAME = @SET_SHARED_LIBRARY_NAME@ - SHARED_LIBRARY_FLAGS := @SHARED_LIBRARY_FLAGS@ -# Set origin using the linker, ie use the relative path to the dependent library to find the dependencies. -# (Note absence of := assignment, because we do not want to evaluate the macro body here) -SET_SHARED_LIBRARY_ORIGIN = @SET_SHARED_LIBRARY_ORIGIN@ -SET_EXECUTABLE_ORIGIN = @SET_EXECUTABLE_ORIGIN@ - LIBRARY_PREFIX := @LIBRARY_PREFIX@ SHARED_LIBRARY_SUFFIX := @SHARED_LIBRARY_SUFFIX@ STATIC_LIBRARY_SUFFIX := @STATIC_LIBRARY_SUFFIX@ diff --git a/make/common/JdkNativeCompilation.gmk b/make/common/JdkNativeCompilation.gmk index 0285669ffd8..29001e09bd0 100644 --- a/make/common/JdkNativeCompilation.gmk +++ b/make/common/JdkNativeCompilation.gmk @@ -30,6 +30,47 @@ ifeq ($(INCLUDE), true) include NativeCompilation.gmk +ifeq ($(call isCompiler, gcc), true) + # --disable-new-dtags forces use of RPATH instead of RUNPATH for rpaths. + # This protects internal library dependencies within the JDK from being + # overridden using LD_LIBRARY_PATH. See JDK-8326891 for more information. + SetExecutableOrigin = \ + -Wl,-rpath,\$(DOLLAR)ORIGIN$1 -Wl,--disable-new-dtags + SetSharedLibraryOrigin = \ + -Wl,-z,origin -Wl,-rpath,\$(DOLLAR)ORIGIN$1 -Wl,--disable-new-dtags +else ifeq ($(call isCompiler, clang), true) + ifeq ($(call isTargetOs, macosx), true) + SetExecutableOrigin = \ + -Wl,-rpath,@loader_path$(or $1,/.) + SetSharedLibraryOrigin = \ + -Wl,-rpath,@loader_path$(or $1,/.) + else ifeq ($(call isTargetOs, aix), true) + SetExecutableOrigin = + SetSharedLibraryOrigin = + else + ifeq ($(call isTargetOs, linux), true) + SetExecutableOrigin = \ + -Wl,-rpath,\$(DOLLAR)ORIGIN$1 -Wl,--disable-new-dtags + else + SetExecutableOrigin = \ + -Wl,-rpath,\$(DOLLAR)ORIGIN$1 + endif + + ifeq ($(call isTargetOs, arm), true) + SetSharedLibraryOrigin = \ + -Wl,-rpath,\$(DOLLAR)ORIGIN$1 + else + SetSharedLibraryOrigin = \ + -Wl,-z,origin -Wl,-rpath,\$(DOLLAR)ORIGIN$1 + endif + endif +else ifeq ($(call isCompiler, microsoft), true) + SetExecutableOrigin = + SetSharedLibraryOrigin = +else + $(error Unknown toolchain) +endif + FindSrcDirsForComponent += \ $(call uniq, $(wildcard \ $(TOPDIR)/src/$(strip $1)/$(OPENJDK_TARGET_OS)/native/$(strip $2) \ @@ -444,9 +485,9 @@ define SetupJdkNativeCompilationBody ifneq ($$($1_LD_SET_ORIGIN), false) ifeq ($$($1_TYPE), EXECUTABLE) - $1_LDFLAGS += $$(call SET_EXECUTABLE_ORIGIN) + $1_LDFLAGS += $$(call SetExecutableOrigin) else - $1_LDFLAGS += $$(call SET_SHARED_LIBRARY_ORIGIN) + $1_LDFLAGS += $$(call SetSharedLibraryOrigin) endif endif # APPEND_LDFLAGS, if it exists, must be set after the origin flags diff --git a/make/common/modules/LauncherCommon.gmk b/make/common/modules/LauncherCommon.gmk index 700c0de74d5..0b420df5684 100644 --- a/make/common/modules/LauncherCommon.gmk +++ b/make/common/modules/LauncherCommon.gmk @@ -156,8 +156,8 @@ define SetupBuildLauncherBody DISABLED_WARNINGS_gcc := unused-function unused-variable, \ DISABLED_WARNINGS_clang := unused-function, \ LDFLAGS := $$($1_LDFLAGS), \ - LDFLAGS_linux := $$(call SET_EXECUTABLE_ORIGIN,/../lib), \ - LDFLAGS_macosx := $$(call SET_EXECUTABLE_ORIGIN,/../lib), \ + LDFLAGS_linux := $$(call SetExecutableOrigin,/../lib), \ + LDFLAGS_macosx := $$(call SetExecutableOrigin,/../lib), \ LDFLAGS_FILTER_OUT := $$($1_LDFLAGS_FILTER_OUT), \ JDK_LIBS := $$($1_JDK_LIBS), \ JDK_LIBS_windows := $$($1_JDK_LIBS_windows), \ diff --git a/make/common/native/Link.gmk b/make/common/native/Link.gmk index e888edfcc4c..855e50bddfb 100644 --- a/make/common/native/Link.gmk +++ b/make/common/native/Link.gmk @@ -50,6 +50,26 @@ GetEntitlementsFile = \ $(if $(wildcard $f), $f, $(DEFAULT_ENTITLEMENTS_FILE)) \ ) +ifeq ($(call isCompiler, gcc), true) + SetSharedLibraryName = \ + -Wl,-soname=$1 +else ifeq ($(call isCompiler, clang), true) + ifeq ($(call isTargetOs, macosx), true) + SetSharedLibraryName = \ + -Wl,-install_name,@rpath/$1 + else ifeq ($(call isTargetOs, aix), true) + SetSharedLibraryName = + else + # Default works for linux, might work on other platforms as well. + SetSharedLibraryName = \ + -Wl,-soname=$1 + endif +else ifeq ($(call isCompiler, microsoft), true) + SetSharedLibraryName = +else + $(error Unknown toolchain) +endif + ################################################################################ define SetupLinking # Unless specifically set, stripping should only happen if symbols are also @@ -131,7 +151,7 @@ define CreateDynamicLibraryOrExecutable # A shared dynamic library or an executable binary has been specified ifeq ($$($1_TYPE), LIBRARY) # Generating a dynamic library. - $1_EXTRA_LDFLAGS += $$(call SET_SHARED_LIBRARY_NAME,$$($1_BASENAME)) + $1_EXTRA_LDFLAGS += $$(call SetSharedLibraryName,$$($1_BASENAME)) endif ifeq ($(MACOSX_CODESIGN_MODE), hardened) diff --git a/make/hotspot/lib/CompileGtest.gmk b/make/hotspot/lib/CompileGtest.gmk index 30d3e3b524c..d615e254f5a 100644 --- a/make/hotspot/lib/CompileGtest.gmk +++ b/make/hotspot/lib/CompileGtest.gmk @@ -152,7 +152,7 @@ $(eval $(call SetupJdkExecutable, BUILD_GTEST_LAUNCHER, \ -I$(GTEST_FRAMEWORK_SRC)/googlemock \ -I$(GTEST_FRAMEWORK_SRC)/googlemock/include, \ LD_SET_ORIGIN := false, \ - LDFLAGS_unix := $(call SET_SHARED_LIBRARY_ORIGIN), \ + LDFLAGS_unix := $(call SetSharedLibraryOrigin), \ JDK_LIBS := gtest:libjvm, \ COPY_DEBUG_SYMBOLS := $(GTEST_COPY_DEBUG_SYMBOLS), \ ZIP_EXTERNAL_DEBUG_SYMBOLS := false, \ diff --git a/make/hotspot/lib/JvmFeatures.gmk b/make/hotspot/lib/JvmFeatures.gmk index 0fd1c752174..79bbd6a4106 100644 --- a/make/hotspot/lib/JvmFeatures.gmk +++ b/make/hotspot/lib/JvmFeatures.gmk @@ -57,7 +57,7 @@ ifeq ($(call check-jvm-feature, zero), true) -DZERO_LIBARCH='"$(OPENJDK_TARGET_CPU_LEGACY_LIB)"' $(LIBFFI_CFLAGS) JVM_LIBS_FEATURES += $(LIBFFI_LIBS) ifeq ($(ENABLE_LIBFFI_BUNDLING), true) - JVM_LDFLAGS_FEATURES += $(call SET_EXECUTABLE_ORIGIN,/..) + JVM_LDFLAGS_FEATURES += $(call SetExecutableOrigin,/..) endif else JVM_EXCLUDE_PATTERNS += /zero/ From bf726e821790fad6ee304c1c36bddedbfe4152ff Mon Sep 17 00:00:00 2001 From: Leo Korinth Date: Mon, 22 Sep 2025 14:22:57 +0000 Subject: [PATCH 157/556] 8285984: G1: Use standard idiom for inlined payload in G1MonotonicArena::Segment Reviewed-by: aboldtch, tschatzl --- src/hotspot/share/gc/g1/g1MonotonicArena.cpp | 2 +- src/hotspot/share/gc/g1/g1MonotonicArena.hpp | 18 ++++++++++-------- .../share/gc/g1/g1MonotonicArena.inline.hpp | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1MonotonicArena.cpp b/src/hotspot/share/gc/g1/g1MonotonicArena.cpp index 1ee299e2ee4..a9c6462680f 100644 --- a/src/hotspot/share/gc/g1/g1MonotonicArena.cpp +++ b/src/hotspot/share/gc/g1/g1MonotonicArena.cpp @@ -34,7 +34,7 @@ G1MonotonicArena::Segment::Segment(uint slot_size, uint num_slots, Segment* next _next(next), _next_allocate(0), _mem_tag(mem_tag) { - _bottom = ((char*) this) + header_size(); + guarantee(is_aligned(this, SegmentPayloadMaxAlignment), "Make sure Segments are always created at correctly aligned memory"); } G1MonotonicArena::Segment* G1MonotonicArena::Segment::create_segment(uint slot_size, diff --git a/src/hotspot/share/gc/g1/g1MonotonicArena.hpp b/src/hotspot/share/gc/g1/g1MonotonicArena.hpp index 828c7f4e86d..211820c5254 100644 --- a/src/hotspot/share/gc/g1/g1MonotonicArena.hpp +++ b/src/hotspot/share/gc/g1/g1MonotonicArena.hpp @@ -110,9 +110,10 @@ protected: void deallocate(void* slot) override { ShouldNotReachHere(); } }; +static constexpr uint SegmentPayloadMaxAlignment = 8; // A single segment/arena containing _num_slots blocks of memory of _slot_size. // Segments can be linked together using a singly linked list. -class G1MonotonicArena::Segment { +class alignas(SegmentPayloadMaxAlignment) G1MonotonicArena::Segment { const uint _slot_size; const uint _num_slots; Segment* volatile _next; @@ -122,16 +123,15 @@ class G1MonotonicArena::Segment { uint volatile _next_allocate; const MemTag _mem_tag; - char* _bottom; // Actual data. - // Do not add class member variables beyond this point - - static size_t header_size() { return align_up(sizeof(Segment), DEFAULT_PADDING_SIZE); } + static size_t header_size() { return align_up(sizeof(Segment), SegmentPayloadMaxAlignment); } static size_t payload_size(uint slot_size, uint num_slots) { - // The cast (size_t) is required to guard against overflow wrap around. - return (size_t)slot_size * num_slots; + // The cast is required to guard against overflow wrap around. + return static_cast(slot_size) * num_slots; } + void* payload(size_t octet) { return &reinterpret_cast(this)[header_size() + octet]; } + size_t payload_size() const { return payload_size(_slot_size, _num_slots); } NONCOPYABLE(Segment); @@ -156,7 +156,7 @@ public: _next_allocate = 0; assert(next != this, " loop condition"); set_next(next); - memset((void*)_bottom, 0, payload_size()); + memset(payload(0), 0, payload_size()); } uint slot_size() const { return _slot_size; } @@ -179,6 +179,7 @@ public: bool is_full() const { return _next_allocate >= _num_slots; } }; +static_assert(alignof(G1MonotonicArena::Segment) >= SegmentPayloadMaxAlignment, "assert alignment of Segment (and indirectly its payload)"); // Set of (free) Segments. The assumed usage is that allocation // to it and removal of segments is strictly separate, but every action may be @@ -235,6 +236,7 @@ public: assert(_initial_num_slots > 0, "Must be"); assert(_max_num_slots > 0, "Must be"); assert(_slot_alignment > 0, "Must be"); + assert(SegmentPayloadMaxAlignment % _slot_alignment == 0, "ensure that _slot_alignment is a divisor of SegmentPayloadMaxAlignment"); } virtual uint next_num_slots(uint prev_num_slots) const { diff --git a/src/hotspot/share/gc/g1/g1MonotonicArena.inline.hpp b/src/hotspot/share/gc/g1/g1MonotonicArena.inline.hpp index d4c1aa8c4e3..dd9ccae1849 100644 --- a/src/hotspot/share/gc/g1/g1MonotonicArena.inline.hpp +++ b/src/hotspot/share/gc/g1/g1MonotonicArena.inline.hpp @@ -39,7 +39,7 @@ inline void* G1MonotonicArena::Segment::allocate_slot() { if (result >= _num_slots) { return nullptr; } - void* r = _bottom + (size_t)result * _slot_size; + void* r = payload(static_cast(result) * _slot_size); return r; } From 2b28c28384feac1d01b8b789c63f18e69fdf6ba4 Mon Sep 17 00:00:00 2001 From: Roger Riggs Date: Mon, 22 Sep 2025 15:05:07 +0000 Subject: [PATCH 158/556] 8368298: ProblemList: Test java/lang/ProcessBuilder/Basic.java Reviewed-by: jpai --- test/jdk/ProblemList.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 8b99564b967..d4685743fd7 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -529,6 +529,7 @@ java/lang/invoke/LFCaching/LFMultiThreadCachingTest.java 8151492 generic- java/lang/invoke/LFCaching/LFGarbageCollectedTest.java 8078602 generic-all java/lang/invoke/lambda/LambdaFileEncodingSerialization.java 8249079 linux-all java/lang/invoke/RicochetTest.java 8251969 generic-all +java/lang/ProcessBuilder/Basic.java#id0 8368192 macosx-all ############################################################################ From e365b7d69c58f8a4d85dde15b6ca335f9d85c0b2 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Mon, 22 Sep 2025 15:08:45 +0000 Subject: [PATCH 159/556] 8366369: Add @requires linux for GTK L&F tests Reviewed-by: serb, prr, tr --- .../swing/plaf/gtk/4928019/bug4928019.java | 20 +++++------ .../sun/java/swing/plaf/gtk/Test6635110.java | 36 ++++++++++++------- .../sun/java/swing/plaf/gtk/Test6963870.java | 30 +++++++++------- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.java b/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.java index cb90f374d84..7824b030d41 100644 --- a/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.java +++ b/test/jdk/com/sun/java/swing/plaf/gtk/4928019/bug4928019.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,25 +26,25 @@ * @bug 4928019 * @key headful * @summary Makes sure all the basic classes can be created with GTK. - * @author Scott Violet + * @requires (os.family != "windows" & os.family != "mac") + * @library /test/lib + * @build jtreg.SkippedException + * @run main bug4928019 */ import javax.swing.*; import javax.swing.plaf.basic.*; +import jtreg.SkippedException; + public class bug4928019 { public static void main(String[] args) throws Throwable { try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"); - } catch (UnsupportedLookAndFeelException ex) { - System.err.println("GTKLookAndFeel is not supported on this platform." + - " Test is considered passed."); - return; - } catch (ClassNotFoundException ex) { - System.err.println("GTKLookAndFeel class is not found." + - " Test is considered passed."); - return; + } catch (Exception e) { + throw new SkippedException("GTKLookAndFeel isn't supported", e); } + new JButton() { public void updateUI() { setUI(new BasicButtonUI()); diff --git a/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java b/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java index ddf8363e2ee..eb3e1e7ca62 100644 --- a/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java +++ b/test/jdk/com/sun/java/swing/plaf/gtk/Test6635110.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,18 +21,28 @@ * questions. */ -/* @test - @bug 6635110 - @key headful - @summary GTK icons should not throw NPE when called by non-GTK UI - @author Peter Zhelezniakov - @run main Test6635110 +/* + * @test + * @bug 6635110 + * @key headful + * @summary GTK icons should not throw NPE when called by non-GTK UI + * @requires (os.family != "windows" & os.family != "mac") + * @library /test/lib + * @build jtreg.SkippedException + * @run main Test6635110 */ -import javax.swing.*; -import java.awt.*; +import java.awt.Component; import java.awt.image.BufferedImage; -import javax.swing.plaf.basic.*; + +import javax.swing.JMenu; +import javax.swing.JToolBar; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.plaf.basic.BasicMenuUI; +import javax.swing.plaf.basic.BasicToolBarUI; + +import jtreg.SkippedException; public class Test6635110 implements Runnable { @@ -53,7 +63,7 @@ public class Test6635110 implements Runnable { paint(tb); } - void paint(Component c) { + private void paint(Component c) { c.setSize(WIDTH, HEIGHT); c.paint(IMAGE.getGraphics()); } @@ -62,9 +72,9 @@ public class Test6635110 implements Runnable { try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"); } catch (Exception e) { - System.out.println("GTKLookAndFeel cannot be set, skipping this test"); - return; + throw new SkippedException("GTKLookAndFeel isn't supported", e); } + SwingUtilities.invokeAndWait(new Test6635110()); } } diff --git a/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.java b/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.java index 962a2d1a0af..ea072419a59 100644 --- a/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.java +++ b/test/jdk/com/sun/java/swing/plaf/gtk/Test6963870.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,23 +21,28 @@ * questions. */ -/* @test - @bug 6963870 - @key headful - @summary Tests that GTKPainter.ListTableFocusBorder.getBorderInsets() - doesn't return null - @author Peter Zhelezniakov - @run main Test6963870 -*/ +/* + * @test + * @bug 6963870 + * @key headful + * @summary Tests that GTKPainter.ListTableFocusBorder.getBorderInsets() + * doesn't return null + * @requires (os.family != "windows" & os.family != "mac") + * @library /test/lib + * @build jtreg.SkippedException + * @run main Test6963870 + */ import java.awt.Insets; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; +import jtreg.SkippedException; + public class Test6963870 implements Runnable { - final static String[] UI_NAMES = { + static final String[] UI_NAMES = { "List.focusCellHighlightBorder", "List.focusSelectedCellHighlightBorder", "List.noFocusBorder", @@ -45,6 +50,7 @@ public class Test6963870 implements Runnable { "Table.focusSelectedCellHighlightBorder", }; + @Override public void run() { for (String uiName: UI_NAMES) { test(uiName); @@ -63,11 +69,9 @@ public class Test6963870 implements Runnable { try { UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"); } catch (Exception e) { - System.out.println("GTKLookAndFeel cannot be set, skipping this test"); - return; + throw new SkippedException("GTKLookAndFeel isn't supported", e); } SwingUtilities.invokeAndWait(new Test6963870()); } } - From 2f74e1433489bccf1fe493417715c0861f88a995 Mon Sep 17 00:00:00 2001 From: Kerem Kat Date: Mon, 22 Sep 2025 15:31:21 +0000 Subject: [PATCH 160/556] 8367862: debug.cpp: Do not print help message for methods ifdef'd out Reviewed-by: mhaessig, kevinw, shade, phh --- src/hotspot/share/utilities/debug.cpp | 61 +++++++++++++++++++-------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/src/hotspot/share/utilities/debug.cpp b/src/hotspot/share/utilities/debug.cpp index 462762bb0c6..6d8a82b6798 100644 --- a/src/hotspot/share/utilities/debug.cpp +++ b/src/hotspot/share/utilities/debug.cpp @@ -651,33 +651,58 @@ void pp(oop p) { pp((void*)p); } void help() { Command c("help"); tty->print_cr("basic"); - tty->print_cr(" pp(void* p) - try to make sense of p"); - tty->print_cr(" ps() - print current thread stack"); - tty->print_cr(" pss() - print all thread stacks"); - tty->print_cr(" pm(int pc) - print Method* given compiled PC"); - tty->print_cr(" findm(intptr_t pc) - finds Method*"); - tty->print_cr(" find(intptr_t x) - finds & prints nmethod/stub/bytecode/oop based on pointer into it"); - tty->print_cr(" pns(void* sp, void* fp, void* pc) - print native (i.e. mixed) stack trace. E.g."); - tty->print_cr(" pns($sp, $rbp, $pc) on Linux/amd64 or"); - tty->print_cr(" pns($sp, $ebp, $pc) on Linux/x86 or"); - tty->print_cr(" pns($sp, $fp, $pc) on Linux/AArch64 or"); - tty->print_cr(" pns($sp, 0, $pc) on Linux/ppc64 or"); - tty->print_cr(" pns($sp, $s8, $pc) on Linux/mips or"); - tty->print_cr(" pns($sp, $fp, $pc) on Linux/RISC-V"); + tty->print_cr(" pp(void* p) - try to make sense of p"); + tty->print_cr(" ps() - print current thread stack"); + tty->print_cr(" pss() - print all thread stacks"); + tty->print_cr(" pm(int pc) - print Method* given compiled PC"); + tty->print_cr(" findnm(intptr_t pc) - find nmethod*"); + tty->print_cr(" findm(intptr_t pc) - find Method*"); + tty->print_cr(" find(intptr_t x) - find & print nmethod/stub/bytecode/oop based on pointer into it"); + tty->print_cr(" findpc(intptr_t x) - find & print nmethod/stub/bytecode/oop based on pointer into it (verbose)"); + +#ifndef PRODUCT + tty->print_cr(" pns(void* sp, void* fp, void* pc) - print native (i.e. mixed) stack trace, e.g."); +#ifdef LINUX + AMD64_ONLY( tty->print_cr(" pns($sp, $rbp, $pc) on Linux/amd64")); + IA32_ONLY( tty->print_cr(" pns($sp, $ebp, $pc) on Linux/x86")); + AARCH64_ONLY(tty->print_cr(" pns($sp, $fp, $pc) on Linux/AArch64")); + RISCV_ONLY( tty->print_cr(" pns($sp, $fp, $pc) on Linux/RISC-V")); + PPC64_ONLY( tty->print_cr(" pns($sp, 0, $pc) on Linux/ppc64")); +#endif // LINUX tty->print_cr(" - in gdb do 'set overload-resolution off' before calling pns()"); tty->print_cr(" - in dbx do 'frame 1' before calling pns()"); +#endif // !PRODUCT + + tty->print_cr("universe."); + tty->print_cr(" verify(intptr_t p) - run verify on Universe"); + tty->print_cr(" threads() - print all threads"); + tty->print_cr(" psd() - print system dictionary"); + tty->print_cr("class metadata."); tty->print_cr(" findclass(name_pattern, flags)"); tty->print_cr(" findmethod(class_name_pattern, method_pattern, flags)"); - tty->print_cr("misc."); - tty->print_cr(" flush() - flushes the log file"); - tty->print_cr(" events() - dump events from ring buffers"); + tty->print_cr("method metadata."); + tty->print_cr(" blob(CodeBlob* p) - print CodeBlob"); + tty->print_cr(" dump_vtable(address p) - dump vtable of the Klass"); + tty->print_cr(" nm(intptr_t p) - find & print CodeBlob details"); + tty->print_cr(" disnm(intptr_t p) - find & print disassembly of CodeBlob"); + tty->print_cr(" printnm(intptr_t p) - print nmethod details"); + tty->print_cr(" findbcp(method, bcp) - find & prints bcp"); + tty->print_cr("stack frame details."); + tty->print_cr(" pfl() - print frame layout"); + tty->print_cr(" psf() - print stack frames"); + + tty->print_cr("misc."); + tty->print_cr(" flush() - flush the log file"); + tty->print_cr(" events() - dump events from ring buffers"); + tty->print_cr(" u5decode(intptr_t addr) - decode a single u5 value"); + tty->print_cr(" u5p(intptr_t addr, intptr_t limit, int count) - decode u5 values"); tty->print_cr("compiler debugging"); - tty->print_cr(" debug() - to set things up for compiler debugging"); - tty->print_cr(" ndebug() - undo debug"); + tty->print_cr(" debug() - set things up for compiler debugging"); + tty->print_cr(" ndebug() - undo debug"); } #ifndef PRODUCT From ced3f13f4e036513444d1fea3958be11741c2b8e Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Mon, 22 Sep 2025 16:58:00 +0000 Subject: [PATCH 161/556] 8367901: Calendar.roll(hour, 24) returns wrong result Reviewed-by: naoto, iris --- .../classes/java/util/GregorianCalendar.java | 8 +- .../jdk/java/util/Calendar/RollHoursTest.java | 139 ++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 test/jdk/java/util/Calendar/RollHoursTest.java diff --git a/src/java.base/share/classes/java/util/GregorianCalendar.java b/src/java.base/share/classes/java/util/GregorianCalendar.java index 9c75cbc5732..26b0c7c1f2f 100644 --- a/src/java.base/share/classes/java/util/GregorianCalendar.java +++ b/src/java.base/share/classes/java/util/GregorianCalendar.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1214,8 +1214,10 @@ public class GregorianCalendar extends Calendar { d.setHours(hourOfDay); time = calsys.getTime(d); - // If we stay on the same wall-clock time, try the next or previous hour. - if (internalGet(HOUR_OF_DAY) == d.getHours()) { + // If the rolled amount is not a full HOUR/HOUR_OF_DAY (12/24-hour) cycle and + // if we stay on the same wall-clock time, try the next or previous hour. + if (((field == HOUR_OF_DAY && amount % 24 != 0) || (field == HOUR && amount % 12 != 0)) + && internalGet(HOUR_OF_DAY) == d.getHours()) { hourOfDay = getRolledValue(rolledValue, amount > 0 ? +1 : -1, min, max); if (field == HOUR && internalGet(AM_PM) == PM) { hourOfDay += 12; diff --git a/test/jdk/java/util/Calendar/RollHoursTest.java b/test/jdk/java/util/Calendar/RollHoursTest.java new file mode 100644 index 00000000000..2eb85caab85 --- /dev/null +++ b/test/jdk/java/util/Calendar/RollHoursTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8367901 + * @summary Ensure hour rolling is correct. Particularly, when the HOUR/HOUR_OF_DAY + * amount rolled would cause the calendar to originate on the same hour as before + * the call. + * @run junit RollHoursTest + */ + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RollHoursTest { + + // Should trigger multiple full HOUR/HOUR_OF_DAY cycles + private static final List hours = + IntStream.rangeClosed(-55, 55).boxed().toList(); + // Various calendars to test against + private static final List calendars = List.of( + // GMT, independent of daylight saving time transitions + new GregorianCalendar(TimeZone.getTimeZone("GMT")), + // Daylight saving observing calendars + new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")), + new GregorianCalendar(TimeZone.getTimeZone("America/Chicago")), + new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")), + new GregorianCalendar(TimeZone.getTimeZone("America/Los_Angeles")) + ); + + // Reset the times of each calendar. These calendars provide testing under + // daylight saving transitions (or the lack thereof) and different AM/PM hours. + @BeforeEach + void clear() { + // Reset all calendars each iteration for clean slate + calendars.forEach(Calendar::clear); + + // Basic test, independent of daylight saving transitions + calendars.get(0).set(2005, 8, 20, 12, 10, 25); + + // Transition to daylight saving time (CST/CDT) --- + // Day of transition: 03/13/2016 (Sunday) + // Most interesting test case due to 2 AM skip + calendars.get(1).set(2016, 2, 13, 3, 30, 55); + // Day before transition: 03/12/2016 (Saturday) + calendars.get(2).set(2016, 2, 12, 15, 20, 45); + + // Transition back to standard time (PST/PDT) ---- + // Day of transition: 11/06/2016 (Sunday) + calendars.get(3).set(2016, 10, 6, 4, 15, 20); + // Day before transition: 11/05/2016 (Saturday) + calendars.get(4).set(2016, 10, 5, 12, 25, 30); + } + + // Rolling the HOUR_OF_DAY field. + @ParameterizedTest + @FieldSource("hours") + void HourOfDayTest(int hoursToRoll) { + for (var cal : calendars) { + var savedTime = cal.getTime(); + var savedHour = cal.get(Calendar.HOUR_OF_DAY); + cal.roll(Calendar.HOUR_OF_DAY, hoursToRoll); + assertEquals(getExpectedHour(hoursToRoll, savedHour, 24, cal, savedTime), + cal.get(Calendar.HOUR_OF_DAY), + getMessage(cal.getTimeZone(), savedTime, hoursToRoll)); + } + } + + // Rolling the HOUR field. + @ParameterizedTest + @FieldSource("hours") + void HourTest(int hoursToRoll) { + for (var cal : calendars) { + var savedTime = cal.getTime(); + var savedHour = cal.get(Calendar.HOUR_OF_DAY); + cal.roll(Calendar.HOUR, hoursToRoll); + assertEquals(getExpectedHour(hoursToRoll, savedHour, 12, cal, savedTime), + cal.get(Calendar.HOUR), + getMessage(cal.getTimeZone(), savedTime, hoursToRoll)); + } + } + + // Gets the expected hour after rolling by X hours. Supports 12/24-hour cycle. + // Special handling for non-existent 2 AM case on transition day. + private static int getExpectedHour(int roll, int hour, int hourCycle, Calendar cal, Date oldDate) { + // For example with HOUR_OF_DAY at 15 and a 24-hour cycle + // For rolling forwards 50 hours -> (50 + 15) % 24 = 17 + // For hour backwards 50 hours -> (24 + (15 - 50) % 24) % 24 + // -> (24 - 11) % 24 = 13 + var result = (roll >= 0 ? (hour + roll) : (hourCycle + (hour + roll) % hourCycle)) % hourCycle; + + // 2 AM does not exist on transition day. Calculate normalized value accordingly + if (cal.getTimeZone().inDaylightTime(oldDate) && cal.get(Calendar.MONTH) == Calendar.MARCH && result == 2) { + return roll > 0 ? result + 1 : result - 1; + } else { + // Normal return value + return result; + } + } + + // Get a TimeZone adapted error message + private static String getMessage(TimeZone tz, Date date, int hoursToRoll) { + var fmt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); + fmt.setTimeZone(tz); + return fmt.format(date) + " incorrectly rolled " + hoursToRoll; + } +} From 58270b757c0bdf82bf753fa304b829e3b64196e4 Mon Sep 17 00:00:00 2001 From: Harshitha Onkar Date: Mon, 22 Sep 2025 17:02:17 +0000 Subject: [PATCH 162/556] 8346839: [TESTBUG] "java/awt/textfield/setechochartest4/setechochartest4.java" failed because the test frame disappears on clicking "Click Several Times" button Reviewed-by: psadhukhan, serb --- .../SetEchoCharTest4/SetEchoCharTest4.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.java b/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.java index c1cc07afac0..e96485dcb54 100644 --- a/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.java +++ b/test/jdk/java/awt/TextField/SetEchoCharTest4/SetEchoCharTest4.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ import java.awt.Frame; import java.awt.TextField; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.lang.reflect.InvocationTargetException; /* * @test @@ -54,16 +53,17 @@ public class SetEchoCharTest4 extends Frame implements ActionListener { Make sure the actual text matches what you typed in for each field. Press Pass if everything's ok, otherwise Fail - """; + """; public SetEchoCharTest4() { + super("SetEchoCharTest4"); setLayout(new FlowLayout()); tf1.setEchoChar('*'); tf2.setEchoChar('$'); tf3.setEchoChar('#'); addStuff(); b.addActionListener(this); - setSize (200,200); + setSize (300, 150); } private void addStuff() { @@ -78,7 +78,6 @@ public class SetEchoCharTest4 extends Frame implements ActionListener { PassFailJFrame.log("TextField2 = " + tf2.getText()); PassFailJFrame.log("TextField3 = " + tf3.getText()); PassFailJFrame.log("--------------"); - setVisible(false); remove(tf1); remove(tf2); remove(tf3); @@ -86,16 +85,14 @@ public class SetEchoCharTest4 extends Frame implements ActionListener { addStuff(); } - public static void main(String[] args) throws InterruptedException, - InvocationTargetException { + public static void main(String[] args) throws Exception { PassFailJFrame.builder() - .title("Set Echo Character Test") - .testUI(SetEchoCharTest4::new) - .instructions(INSTRUCTIONS) - .rows((int) INSTRUCTIONS.lines().count() + 1) - .columns(40) - .logArea() - .build() - .awaitAndCheck(); + .title("Set Echo Character Test") + .testUI(SetEchoCharTest4::new) + .instructions(INSTRUCTIONS) + .columns(40) + .logArea() + .build() + .awaitAndCheck(); } } From c3aaa8751acfd795207f1a509b6e170e6a753c69 Mon Sep 17 00:00:00 2001 From: Chris Plummer Date: Mon, 22 Sep 2025 17:22:01 +0000 Subject: [PATCH 163/556] 8361955: [GCC static analyzer] libjdwp/threadControl.c threadControl_setPendingInterrupt error: dereference of NULL 'node' Reviewed-by: dholmes, sspitsyn, lmesnik --- src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c b/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c index 4b037142d52..bfeffe85678 100644 --- a/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c +++ b/src/jdk.jdwp.agent/share/native/libjdwp/threadControl.c @@ -2317,8 +2317,9 @@ threadControl_setPendingInterrupt(jthread thread) debugMonitorEnter(threadLock); node = findRunningThread(thread); - JDI_ASSERT(node != NULL); - node->pendingInterrupt = JNI_TRUE; + if (node != NULL) { + node->pendingInterrupt = JNI_TRUE; + } debugMonitorExit(threadLock); } From bdfe05b595d86c62f7dad78549023a3426423679 Mon Sep 17 00:00:00 2001 From: Man Cao Date: Mon, 22 Sep 2025 18:05:48 +0000 Subject: [PATCH 164/556] 8368071: Compilation throughput regressed 2X-8X after JDK-8355003 Reviewed-by: iveresov, shade --- src/hotspot/share/compiler/compilationPolicy.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/hotspot/share/compiler/compilationPolicy.cpp b/src/hotspot/share/compiler/compilationPolicy.cpp index 36b597b6e37..c91d299510d 100644 --- a/src/hotspot/share/compiler/compilationPolicy.cpp +++ b/src/hotspot/share/compiler/compilationPolicy.cpp @@ -1350,17 +1350,24 @@ CompLevel CompilationPolicy::standard_transition(const methodHandle& method, Com return next_level; } +template static inline bool apply_predicate(const methodHandle& method, CompLevel cur_level, int i, int b, bool delay_profiling, double delay_profiling_scale) { + if (delay_profiling) { + return Predicate::apply_scaled(method, cur_level, i, b, delay_profiling_scale); + } else { + return Predicate::apply(method, cur_level, i, b); + } +} + template CompLevel CompilationPolicy::transition_from_none(const methodHandle& method, CompLevel cur_level, bool delay_profiling, bool disable_feedback) { precond(cur_level == CompLevel_none); CompLevel next_level = cur_level; int i = method->invocation_count(); int b = method->backedge_count(); - double scale = delay_profiling ? Tier0ProfileDelayFactor : 1.0; // If we were at full profile level, would we switch to full opt? if (transition_from_full_profile(method, CompLevel_full_profile) == CompLevel_full_optimization) { next_level = CompLevel_full_optimization; - } else if (!CompilationModeFlag::disable_intermediate() && Predicate::apply_scaled(method, cur_level, i, b, scale)) { + } else if (!CompilationModeFlag::disable_intermediate() && apply_predicate(method, cur_level, i, b, delay_profiling, Tier0ProfileDelayFactor)) { // C1-generated fully profiled code is about 30% slower than the limited profile // code that has only invocation and backedge counters. The observation is that // if C2 queue is large enough we can spend too much time in the fully profiled code @@ -1402,13 +1409,12 @@ CompLevel CompilationPolicy::transition_from_limited_profile(const methodHandle& CompLevel next_level = cur_level; int i = method->invocation_count(); int b = method->backedge_count(); - double scale = delay_profiling ? Tier2ProfileDelayFactor : 1.0; MethodData* mdo = method->method_data(); if (mdo != nullptr) { if (mdo->would_profile()) { if (disable_feedback || (CompileBroker::queue_size(CompLevel_full_optimization) <= Tier3DelayOff * compiler_count(CompLevel_full_optimization) && - Predicate::apply_scaled(method, cur_level, i, b, scale))) { + apply_predicate(method, cur_level, i, b, delay_profiling, Tier2ProfileDelayFactor))) { next_level = CompLevel_full_profile; } } else { @@ -1418,7 +1424,7 @@ CompLevel CompilationPolicy::transition_from_limited_profile(const methodHandle& // If there is no MDO we need to profile if (disable_feedback || (CompileBroker::queue_size(CompLevel_full_optimization) <= Tier3DelayOff * compiler_count(CompLevel_full_optimization) && - Predicate::apply_scaled(method, cur_level, i, b, scale))) { + apply_predicate(method, cur_level, i, b, delay_profiling, Tier2ProfileDelayFactor))) { next_level = CompLevel_full_profile; } } From 47efe3c794c241b7534eac597b3dd03d571677f1 Mon Sep 17 00:00:00 2001 From: Sean Coffey Date: Mon, 22 Sep 2025 18:41:07 +0000 Subject: [PATCH 165/556] 8343395: SSLLogger doesn't work for formatted messages Reviewed-by: weijun --- .../classes/sun/security/ssl/SSLLogger.java | 36 +++++++++---------- .../classes/sun/security/ssl/Utilities.java | 5 +-- .../SSLLogger/DebugPropertyValuesTest.java | 9 ++++- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/java.base/share/classes/sun/security/ssl/SSLLogger.java b/src/java.base/share/classes/sun/security/ssl/SSLLogger.java index 47afee8ddf2..f55ab27d297 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLLogger.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLLogger.java @@ -46,6 +46,7 @@ import sun.security.util.Debug; import sun.security.x509.*; import static java.nio.charset.StandardCharsets.UTF_8; +import static sun.security.ssl.Utilities.LINE_SEP; /** * Implementation of SSL logger. @@ -62,6 +63,7 @@ public final class SSLLogger { private static final String property; public static final boolean isOn; + static { String p = System.getProperty("javax.net.debug"); if (p != null) { @@ -190,7 +192,12 @@ public final class SSLLogger { try { String formatted = SSLSimpleFormatter.formatParameters(params); - logger.log(level, msg, formatted); + // use the customized log method for SSLConsoleLogger + if (logger instanceof SSLConsoleLogger) { + logger.log(level, msg, formatted); + } else { + logger.log(level, msg + ":" + LINE_SEP + formatted); + } } catch (Exception exp) { // ignore it, just for debugging. } @@ -282,7 +289,7 @@ public final class SSLLogger { """, Locale.ENGLISH); - private static final MessageFormat extendedCertFormart = + private static final MessageFormat extendedCertFormat = new MessageFormat( """ "version" : "v{0}", @@ -299,15 +306,6 @@ public final class SSLLogger { """, Locale.ENGLISH); - // - // private static MessageFormat certExtFormat = new MessageFormat( - // "{0} [{1}] '{'\n" + - // " critical: {2}\n" + - // " value: {3}\n" + - // "'}'", - // Locale.ENGLISH); - // - private static final MessageFormat messageFormatNoParas = new MessageFormat( """ @@ -325,7 +323,7 @@ public final class SSLLogger { private static final MessageFormat messageCompactFormatNoParas = new MessageFormat( - "{0}|{1}|{2}|{3}|{4}|{5}|{6}\n", + "{0}|{1}|{2}|{3}|{4}|{5}|{6}" + LINE_SEP, Locale.ENGLISH); private static final MessageFormat messageFormatWithParas = @@ -423,7 +421,7 @@ public final class SSLLogger { if (isFirst) { isFirst = false; } else { - builder.append(",\n"); + builder.append("," + LINE_SEP); } if (parameter instanceof Throwable) { @@ -504,10 +502,10 @@ public final class SSLLogger { if (isFirst) { isFirst = false; } else { - extBuilder.append(",\n"); + extBuilder.append("," + LINE_SEP); } - extBuilder.append("{\n" + - Utilities.indent(certExt.toString()) + "\n}"); + extBuilder.append("{" + LINE_SEP + + Utilities.indent(certExt.toString()) + LINE_SEP +"}"); } Object[] certFields = { x509.getVersion(), @@ -521,7 +519,7 @@ public final class SSLLogger { Utilities.indent(extBuilder.toString()) }; builder.append(Utilities.indent( - extendedCertFormart.format(certFields))); + extendedCertFormat.format(certFields))); } } catch (Exception ce) { // ignore the exception @@ -578,7 +576,7 @@ public final class SSLLogger { // "string c" // ] StringBuilder builder = new StringBuilder(512); - builder.append("\"" + key + "\": [\n"); + builder.append("\"" + key + "\": [" + LINE_SEP); int len = strings.length; for (int i = 0; i < len; i++) { String string = strings[i]; @@ -586,7 +584,7 @@ public final class SSLLogger { if (i != len - 1) { builder.append(","); } - builder.append("\n"); + builder.append(LINE_SEP); } builder.append(" ]"); diff --git a/src/java.base/share/classes/sun/security/ssl/Utilities.java b/src/java.base/share/classes/sun/security/ssl/Utilities.java index 3ed022db382..50cd3bee751 100644 --- a/src/java.base/share/classes/sun/security/ssl/Utilities.java +++ b/src/java.base/share/classes/sun/security/ssl/Utilities.java @@ -40,6 +40,7 @@ final class Utilities { Pattern.compile("\\r\\n|\\n|\\r"); private static final HexFormat HEX_FORMATTER = HexFormat.of().withUpperCase(); + static final String LINE_SEP = System.lineSeparator(); /** * Puts {@code hostname} into the {@code serverNames} list. @@ -150,7 +151,7 @@ final class Utilities { static String indent(String source, String prefix) { StringBuilder builder = new StringBuilder(); if (source == null) { - builder.append("\n").append(prefix).append(""); + builder.append(LINE_SEP).append(prefix).append(""); } else { String[] lines = lineBreakPatern.split(source); boolean isFirst = true; @@ -158,7 +159,7 @@ final class Utilities { if (isFirst) { isFirst = false; } else { - builder.append("\n"); + builder.append(LINE_SEP); } builder.append(prefix).append(line); } diff --git a/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java b/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java index e27321770b7..726cc516267 100644 --- a/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java +++ b/test/jdk/sun/security/ssl/SSLLogger/DebugPropertyValuesTest.java @@ -70,9 +70,11 @@ public class DebugPropertyValuesTest extends SSLSocketTemplate { debugMessages.put("verbose", List.of("Ignore unsupported cipher suite:")); debugMessages.put("handshake-expand", List.of("\"logger\".*: \"javax.net.ssl\",", + "\"specifics\" : \\[", "\"message\".*: \"Produced ClientHello handshake message")); debugMessages.put("record-expand", List.of("\"logger\".*: \"javax.net.ssl\",", + "\"specifics\" : \\[", "\"message\".*: \"READ: TLSv1.2 application_data")); debugMessages.put("help", List.of("print the help messages", @@ -83,7 +85,12 @@ public class DebugPropertyValuesTest extends SSLSocketTemplate { // "ALL" shouldn't be seen as a valid Level debugMessages.put("javax.net.debug.logger.ALL", List.of("ALL:")); debugMessages.put("javax.net.debug.logger", - List.of("FINE: adding as trusted certificates", + List.of("FINE: adding as trusted certificates:" + + System.lineSeparator() + + " \"certificate\" : \\{", + "FINE: Produced ClientHello handshake message:" + + System.lineSeparator() + + "\"ClientHello\": \\{", "FINE: WRITE: TLSv1.3 application_data")); } From 9f7b9887cb0950bc24aa7a43b43aa5666cb405a4 Mon Sep 17 00:00:00 2001 From: Damon Nguyen Date: Mon, 22 Sep 2025 20:29:48 +0000 Subject: [PATCH 166/556] 8366149: JNI exception pending in Java_sun_awt_X11GraphicsDevice_pGetBounds of awt_GraphicsEnv.c:1484 Reviewed-by: aivanov, prr --- .../unix/native/libawt_xawt/awt/awt_GraphicsEnv.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c b/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c index 1f73a3256b0..5985dd93226 100644 --- a/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c +++ b/src/java.desktop/unix/native/libawt_xawt/awt/awt_GraphicsEnv.c @@ -1268,11 +1268,15 @@ Java_sun_awt_X11GraphicsDevice_pGetBounds(JNIEnv *env, jobject this, jint screen xinInfo[screen].width, xinInfo[screen].height); XFree(xinInfo); + if (!bounds) { + return NULL; + } } } else { jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); if (exceptionClass != NULL) { (*env)->ThrowNew(env, exceptionClass, "Illegal screen index"); + return NULL; } } } From d0fe8f7ede7c2426438c7d6dc5a24cfd2f1d094e Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Mon, 22 Sep 2025 21:03:15 +0000 Subject: [PATCH 167/556] 8368312: Move CC_OUT_OPTION out of spec.gmk Reviewed-by: erikj --- make/Hsdis.gmk | 1 - make/autoconf/flags.m4 | 8 +------- make/autoconf/spec.gmk.template | 1 - make/common/native/CompileFile.gmk | 12 ++++++++++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/make/Hsdis.gmk b/make/Hsdis.gmk index a0fc031be1b..469cc488f16 100644 --- a/make/Hsdis.gmk +++ b/make/Hsdis.gmk @@ -114,7 +114,6 @@ ifeq ($(HSDIS_BACKEND), binutils) TOOLCHAIN_TYPE := gcc OPENJDK_TARGET_OS := linux OPENJDK_TARGET_OS_TYPE := unix - CC_OUT_OPTION := -o$(SPACE) GENDEPS_FLAGS := -MMD -MF CFLAGS_DEBUG_SYMBOLS := -g DISABLED_WARNINGS := diff --git a/make/autoconf/flags.m4 b/make/autoconf/flags.m4 index 10647305757..c47947154c2 100644 --- a/make/autoconf/flags.m4 +++ b/make/autoconf/flags.m4 @@ -1,5 +1,5 @@ # -# Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -319,16 +319,10 @@ AC_DEFUN_ONCE([FLAGS_PRE_TOOLCHAIN], AC_DEFUN([FLAGS_SETUP_TOOLCHAIN_CONTROL], [ if test "x$TOOLCHAIN_TYPE" = xmicrosoft; then - CC_OUT_OPTION=-Fo if test "x$OPENJDK_TARGET_CPU" != xaarch64; then AS_NON_ASM_EXTENSION_OPTION=-Ta fi - else - # The option used to specify the target .o,.a or .so file. - # When compiling, how to specify the to be created object file. - CC_OUT_OPTION='-o$(SPACE)' fi - AC_SUBST(CC_OUT_OPTION) AC_SUBST(AS_NON_ASM_EXTENSION_OPTION) # Generate make dependency files diff --git a/make/autoconf/spec.gmk.template b/make/autoconf/spec.gmk.template index 4eb5aa2f66d..e2e8b24db6e 100644 --- a/make/autoconf/spec.gmk.template +++ b/make/autoconf/spec.gmk.template @@ -491,7 +491,6 @@ CXX_VERSION_NUMBER := @CXX_VERSION_NUMBER@ # Legacy support HOTSPOT_TOOLCHAIN_TYPE := @HOTSPOT_TOOLCHAIN_TYPE@ -CC_OUT_OPTION := @CC_OUT_OPTION@ AS_NON_ASM_EXTENSION_OPTION := @AS_NON_ASM_EXTENSION_OPTION@ # Flags used for overriding the default opt setting for a C/C++ source file. diff --git a/make/common/native/CompileFile.gmk b/make/common/native/CompileFile.gmk index a8d46949788..10326d3066c 100644 --- a/make/common/native/CompileFile.gmk +++ b/make/common/native/CompileFile.gmk @@ -93,6 +93,14 @@ DEPENDENCY_TARGET_SED_PATTERN := \ -e 's/$$$$/ :/' \ # +################################################################################ +# Setup compiler-specific argument to specify output file +ifeq ($(call isCompiler, microsoft), true) + CC_OUT_OPTION := -Fo +else + CC_OUT_OPTION := -o$(SPACE) +endif + ################################################################################ # Create the recipe needed to compile a single native source file. # @@ -334,7 +342,7 @@ define CreateWindowsResourceFile $$(call LogInfo, Compiling resource $$(notdir $$($1_VERSIONINFO_RESOURCE)) (for $$($1_BASENAME))) $$(call MakeDir, $$(@D) $$($1_OBJECT_DIR)) $$(call ExecuteWithLog, $$@, $$(call MakeCommandRelative, \ - $$($1_RC) $$($1_RCFLAGS) $$($1_SYSROOT_CFLAGS) $(CC_OUT_OPTION)$$@ \ + $$($1_RC) $$($1_RCFLAGS) $$($1_SYSROOT_CFLAGS) -Fo$$@ \ $$($1_VERSIONINFO_RESOURCE) 2>&1 )) # Windows RC compiler does not support -showIncludes, so we mis-use CL # for this. Filter out RC specific arguments that are unknown to CL. @@ -344,7 +352,7 @@ define CreateWindowsResourceFile $$(call ExecuteWithLog, $$($1_RES_DEPS_FILE)$(OBJ_SUFFIX), \ $$($1_CC) $$(filter-out -l%, $$($1_RCFLAGS)) \ $$($1_SYSROOT_CFLAGS) -showIncludes -nologo -TC \ - $(CC_OUT_OPTION)$$($1_RES_DEPS_FILE)$(OBJ_SUFFIX) -P -Fi$$($1_RES_DEPS_FILE).pp \ + -Fo$$($1_RES_DEPS_FILE)$(OBJ_SUFFIX) -P -Fi$$($1_RES_DEPS_FILE).pp \ $$($1_VERSIONINFO_RESOURCE)) 2>&1 \ | $(TR) -d '\r' | $(GREP) -v -e "^Note: including file:" \ -e "^$$(notdir $$($1_VERSIONINFO_RESOURCE))$$$$" || test "$$$$?" = "1" ; \ From 4882559ae34e3fee2fd1fd14cb9617df68664281 Mon Sep 17 00:00:00 2001 From: Joe Darcy Date: Mon, 22 Sep 2025 21:30:47 +0000 Subject: [PATCH 168/556] 8367942: Add API note discussing Double.compareTo total order and IEEE 754 total order Reviewed-by: rgiulietti --- .../share/classes/java/lang/Double.java | 18 +++++++++++++++++- .../share/classes/java/lang/Float.java | 4 ++++ .../classes/jdk/incubator/vector/Float16.java | 5 +++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/java.base/share/classes/java/lang/Double.java b/src/java.base/share/classes/java/lang/Double.java index c43ec054661..661a0ceb42b 100644 --- a/src/java.base/share/classes/java/lang/Double.java +++ b/src/java.base/share/classes/java/lang/Double.java @@ -1425,13 +1425,29 @@ public final class Double extends Number * This method chooses to define positive zero ({@code +0.0d}), * to be greater than negative zero ({@code -0.0d}). * - + * * This ensures that the natural ordering of {@code Double} * objects imposed by this method is consistent with * equals; see {@linkplain ##equivalenceRelation this * discussion for details of floating-point comparison and * ordering}. * + * @apiNote + * The inclusion of a total order idiom in the Java SE API + * predates the inclusion of that functionality in the IEEE 754 + * standard. The ordering of the totalOrder predicate chosen by + * IEEE 754 differs from the total order chosen by this method. + * While this method treats all NaN representations as being in + * the same equivalence class, the IEEE 754 total order defines an + * ordering based on the bit patterns of the NaN among the + * different NaN representations. The IEEE 754 order regards + * "negative" NaN representations, that is NaN representations + * whose sign bit is set, to be less than any finite or infinite + * value and less than any "positive" NaN. In addition, the IEEE + * order regards all positive NaN values as greater than positive + * infinity. See the IEEE 754 standard for full details of its + * total ordering. + * * @param anotherDouble the {@code Double} to be compared. * @return the value {@code 0} if {@code anotherDouble} is * numerically equal to this {@code Double}; a value diff --git a/src/java.base/share/classes/java/lang/Float.java b/src/java.base/share/classes/java/lang/Float.java index 3ee4f4ce619..ac5ab9ba055 100644 --- a/src/java.base/share/classes/java/lang/Float.java +++ b/src/java.base/share/classes/java/lang/Float.java @@ -1253,6 +1253,10 @@ public final class Float extends Number * discussion for details of floating-point comparison and * ordering}. * + * @apiNote + * For a discussion of differences between the total order of this + * method compared to the total order defined by the IEEE 754 + * standard, see the note in {@link Double#compareTo(Double)}. * * @param anotherFloat the {@code Float} to be compared. * @return the value {@code 0} if {@code anotherFloat} is diff --git a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java index 3202ca2ba25..45dd52175cc 100644 --- a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java +++ b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java @@ -969,6 +969,11 @@ public final class Float16 * to be greater than negative zero. * * + * @apiNote + * For a discussion of differences between the total order of this + * method compared to the total order defined by the IEEE 754 + * standard, see the note in {@link Double#compareTo(Double)}. + * * @param anotherFloat16 the {@code Float16} to be compared. * @return the value {@code 0} if {@code anotherFloat16} is * numerically equal to this {@code Float16}; a value From b11b1f1186e00cce6c5490db8976ead2226fa4ba Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 22 Sep 2025 23:56:49 +0000 Subject: [PATCH 169/556] 8343221: IOUtils.copyRecursive() doesn't create parent directories Reviewed-by: almatvee --- .../jdk/jpackage/internal/util/FileUtils.java | 50 ++++---- .../jpackage/internal/util/FileUtilsTest.java | 115 ++++++++++++++++++ .../tools/jpackage/share/AppContentTest.java | 1 - 3 files changed, 143 insertions(+), 23 deletions(-) create mode 100644 test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/FileUtilsTest.java diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/FileUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/FileUtils.java index 71b8c3d6ddc..8ac88c13e1d 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/FileUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/FileUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.util.function.ExceptionBox; import jdk.jpackage.internal.util.function.ThrowingConsumer; @@ -64,29 +65,34 @@ public final class FileUtils { List copyActions = new ArrayList<>(); - Files.walkFileTree(src, new SimpleFileVisitor() { - @Override - public FileVisitResult preVisitDirectory(final Path dir, - final BasicFileAttributes attrs) { - if (isPathMatch(dir, excludes)) { - return FileVisitResult.SKIP_SUBTREE; - } else { - copyActions.add(new CopyAction(null, dest.resolve(src. - relativize(dir)))); + if (Files.isDirectory(src)) { + Files.walkFileTree(src, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(final Path dir, + final BasicFileAttributes attrs) { + if (isPathMatch(dir, excludes)) { + return FileVisitResult.SKIP_SUBTREE; + } else { + copyActions.add(new CopyAction(null, dest.resolve(src.relativize(dir)))); + return FileVisitResult.CONTINUE; + } + } + + @Override + public FileVisitResult visitFile(final Path file, + final BasicFileAttributes attrs) { + if (!isPathMatch(file, excludes)) { + copyActions.add(new CopyAction(file, dest.resolve(src.relativize(file)))); + } return FileVisitResult.CONTINUE; } - } - - @Override - public FileVisitResult visitFile(final Path file, - final BasicFileAttributes attrs) { - if (!isPathMatch(file, excludes)) { - copyActions.add(new CopyAction(file, dest.resolve(src. - relativize(file)))); - } - return FileVisitResult.CONTINUE; - } - }); + }); + } else if (!isPathMatch(src, excludes)) { + Optional.ofNullable(dest.getParent()).ifPresent(dstDir -> { + copyActions.add(new CopyAction(null, dstDir)); + }); + copyActions.add(new CopyAction(src, dest)); + } for (var copyAction : copyActions) { copyAction.apply(options); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/FileUtilsTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/FileUtilsTest.java new file mode 100644 index 00000000000..2067f0fa628 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/FileUtilsTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + + +public class FileUtilsTest { + + @ParameterizedTest + @EnumSource(ExcludeType.class) + public void test_copyRecursive_dir(ExcludeType exclude, @TempDir Path workdir) throws IOException { + Files.createDirectories(workdir.resolve("from/foo/bar")); + Files.createDirectories(workdir.resolve("from/foo/buz")); + Files.writeString(workdir.resolve("from/foo/bar/file.txt"), "Hello"); + + List excludes = new ArrayList<>(); + switch (exclude) { + case EXCLUDE_FILE -> { + excludes.add(Path.of("file.txt")); + } + case EXCLUDE_DIR -> { + excludes.add(Path.of("bar")); + } + case EXCLUDE_SUBDIR -> { + excludes.add(Path.of("foo")); + } + case EXCLUDE_NONE -> { + } + } + + FileUtils.copyRecursive(workdir.resolve("from"), workdir.resolve("to"), excludes); + + assertEquals("Hello", Files.readString(workdir.resolve("from/foo/bar/file.txt"))); + + switch (exclude) { + case EXCLUDE_FILE -> { + assertFalse(Files.exists(workdir.resolve("to/foo/bar/file.txt"))); + assertTrue(Files.isDirectory(workdir.resolve("to/foo/bar"))); + } + case EXCLUDE_DIR -> { + assertFalse(Files.exists(workdir.resolve("to/foo/bar"))); + assertTrue(Files.isDirectory(workdir.resolve("to/foo/buz"))); + } + case EXCLUDE_SUBDIR -> { + assertFalse(Files.exists(workdir.resolve("to/foo"))); + assertTrue(Files.isDirectory(workdir.resolve("to"))); + } + case EXCLUDE_NONE -> { + assertEquals("Hello", Files.readString(workdir.resolve("to/foo/bar/file.txt"))); + } + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_copyRecursive_file(boolean exclude, @TempDir Path workdir) throws IOException { + Files.createDirectories(workdir.resolve("from/foo/bar")); + Files.writeString(workdir.resolve("from/foo/bar/file.txt"), "Hello"); + + List excludes = new ArrayList<>(); + if (exclude) { + excludes.add(Path.of("bar/file.txt")); + } + + FileUtils.copyRecursive(workdir.resolve("from/foo/bar/file.txt"), workdir.resolve("to/foo/bar/file.txt"), excludes); + + assertEquals("Hello", Files.readString(workdir.resolve("from/foo/bar/file.txt"))); + if (exclude) { + assertFalse(Files.exists(workdir.resolve("to"))); + } else { + assertEquals("Hello", Files.readString(workdir.resolve("to/foo/bar/file.txt"))); + } + } + + enum ExcludeType { + EXCLUDE_NONE, + EXCLUDE_FILE, + EXCLUDE_DIR, + EXCLUDE_SUBDIR, + } +} diff --git a/test/jdk/tools/jpackage/share/AppContentTest.java b/test/jdk/tools/jpackage/share/AppContentTest.java index edb7e2918da..2c6498a631b 100644 --- a/test/jdk/tools/jpackage/share/AppContentTest.java +++ b/test/jdk/tools/jpackage/share/AppContentTest.java @@ -169,7 +169,6 @@ public class AppContentTest { var appContentArg = TKit.createTempDirectory("app-content").resolve(RESOURCES_DIR); var srcPath = TKit.TEST_SRC_ROOT.resolve(appContentPath); var dstPath = appContentArg.resolve(srcPath.getFileName()); - Files.createDirectories(dstPath.getParent()); FileUtils.copyRecursive(srcPath, dstPath); return appContentArg; } From 61c5245bf7d6626b0c816612adcb0d94d6863644 Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Tue, 23 Sep 2025 00:33:05 +0000 Subject: [PATCH 170/556] 8367869: Test java/io/FileDescriptor/Sync.java timed out Reviewed-by: jpai, shade, rriggs --- test/jdk/java/io/FileDescriptor/Sync.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/java/io/FileDescriptor/Sync.java b/test/jdk/java/io/FileDescriptor/Sync.java index 587d0bbe758..5864d5798df 100644 --- a/test/jdk/java/io/FileDescriptor/Sync.java +++ b/test/jdk/java/io/FileDescriptor/Sync.java @@ -39,7 +39,7 @@ import jdk.test.lib.thread.VThreadRunner; public class Sync { static final String TEST_DIR = System.getProperty("test.dir", "."); - static final int TRIES = 10_000; + static final int TRIES = 1_000; public static void testWith(File file) throws Exception { try (FileOutputStream fos = new FileOutputStream(file)) { From 942b21772a05e30af344742a02db1643ad0e0227 Mon Sep 17 00:00:00 2001 From: Dingli Zhang Date: Tue, 23 Sep 2025 07:00:17 +0000 Subject: [PATCH 171/556] 8368247: RISC-V: enable vectorapi test for expand operation Reviewed-by: mli, fyang --- .../jtreg/compiler/vectorapi/VectorExpandTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java index ce8fc0fb7b0..0da2eb9fc19 100644 --- a/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorExpandTest.java @@ -88,7 +88,7 @@ public class VectorExpandTest { } @Test - @IR(counts = { IRNode.EXPAND_VB, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + @IR(counts = { IRNode.EXPAND_VB, "= 1" }, applyIfCPUFeatureOr = { "asimd", "true", "rvv", "true" }) public static void testVectorExpandByte(ByteVector av, VectorMask m) { av.expand(m).intoArray(bb, 0); } @@ -105,7 +105,7 @@ public class VectorExpandTest { } @Test - @IR(counts = { IRNode.EXPAND_VS, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + @IR(counts = { IRNode.EXPAND_VS, "= 1" }, applyIfCPUFeatureOr = { "asimd", "true", "rvv", "true" }) public static void testVectorExpandShort(ShortVector av, VectorMask m) { av.expand(m).intoArray(sb, 0); } @@ -122,7 +122,7 @@ public class VectorExpandTest { } @Test - @IR(counts = { IRNode.EXPAND_VI, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + @IR(counts = { IRNode.EXPAND_VI, "= 1" }, applyIfCPUFeatureOr = { "asimd", "true", "rvv", "true" }) public static void testVectorExpandInt(IntVector av, VectorMask m) { av.expand(m).intoArray(ib, 0); } @@ -139,7 +139,7 @@ public class VectorExpandTest { } @Test - @IR(counts = { IRNode.EXPAND_VL, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + @IR(counts = { IRNode.EXPAND_VL, "= 1" }, applyIfCPUFeatureOr = { "asimd", "true", "rvv", "true" }) public static void testVectorExpandLong(LongVector av, VectorMask m) { av.expand(m).intoArray(lb, 0); } @@ -156,7 +156,7 @@ public class VectorExpandTest { } @Test - @IR(counts = { IRNode.EXPAND_VF, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + @IR(counts = { IRNode.EXPAND_VF, "= 1" }, applyIfCPUFeatureOr = { "asimd", "true", "rvv", "true" }) public static void testVectorExpandFloat(FloatVector av, VectorMask m) { av.expand(m).intoArray(fb, 0); } @@ -173,7 +173,7 @@ public class VectorExpandTest { } @Test - @IR(counts = { IRNode.EXPAND_VD, "= 1" }, applyIfCPUFeature = { "asimd", "true" }) + @IR(counts = { IRNode.EXPAND_VD, "= 1" }, applyIfCPUFeatureOr = { "asimd", "true", "rvv", "true" }) public static void testVectorExpandDouble(DoubleVector av, VectorMask m) { av.expand(m).intoArray(db, 0); } From 43531064c290928cbbac9ee3662674a0ea3b0240 Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Tue, 23 Sep 2025 07:11:56 +0000 Subject: [PATCH 172/556] 8368214: ZGC: Remove double newlines Reviewed-by: stefank, jsikstro --- src/hotspot/cpu/aarch64/gc/z/zBarrierSetAssembler_aarch64.hpp | 1 - src/hotspot/cpu/aarch64/gc/z/z_aarch64.ad | 1 - src/hotspot/cpu/ppc/gc/z/zBarrierSetAssembler_ppc.cpp | 3 --- src/hotspot/cpu/x86/gc/z/zBarrierSetAssembler_x86.cpp | 2 -- src/hotspot/os_cpu/linux_ppc/gc/z/zSyscall_linux_ppc.hpp | 1 - src/hotspot/share/gc/z/zBarrierSet.inline.hpp | 1 - src/hotspot/share/gc/z/zGeneration.cpp | 2 -- src/hotspot/share/gc/z/zMark.cpp | 1 - src/hotspot/share/gc/z/zPage.inline.hpp | 1 - src/hotspot/share/gc/z/zPageAllocator.cpp | 1 - src/hotspot/share/gc/z/zRelocate.cpp | 1 - src/hotspot/share/gc/z/zValue.inline.hpp | 1 - src/hotspot/share/gc/z/zVirtualMemoryManager.inline.hpp | 1 - test/hotspot/jtreg/gc/z/TestZNMT.java | 1 - 14 files changed, 18 deletions(-) diff --git a/src/hotspot/cpu/aarch64/gc/z/zBarrierSetAssembler_aarch64.hpp b/src/hotspot/cpu/aarch64/gc/z/zBarrierSetAssembler_aarch64.hpp index ad3a171c103..ae2819e78ca 100644 --- a/src/hotspot/cpu/aarch64/gc/z/zBarrierSetAssembler_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/gc/z/zBarrierSetAssembler_aarch64.hpp @@ -275,7 +275,6 @@ public: Label* entry(); }; - class ZStoreBarrierStubC2Aarch64 : public ZStoreBarrierStubC2 { private: bool _deferred_emit; diff --git a/src/hotspot/cpu/aarch64/gc/z/z_aarch64.ad b/src/hotspot/cpu/aarch64/gc/z/z_aarch64.ad index 78dc5d56bbd..8149c7e4c97 100644 --- a/src/hotspot/cpu/aarch64/gc/z/z_aarch64.ad +++ b/src/hotspot/cpu/aarch64/gc/z/z_aarch64.ad @@ -239,7 +239,6 @@ instruct zCompareAndSwapPAcq(iRegINoSp res, indirect mem, iRegP oldval, iRegP ne ins_pipe(pipe_slow); %} - instruct zCompareAndExchangeP(iRegPNoSp res, indirect mem, iRegP oldval, iRegP newval, iRegPNoSp oldval_tmp, iRegPNoSp newval_tmp, rFlagsReg cr) %{ match(Set res (CompareAndExchangeP mem (Binary oldval newval))); predicate(UseZGC && !needs_acquiring_load_exclusive(n) && n->as_LoadStore()->barrier_data() != 0); diff --git a/src/hotspot/cpu/ppc/gc/z/zBarrierSetAssembler_ppc.cpp b/src/hotspot/cpu/ppc/gc/z/zBarrierSetAssembler_ppc.cpp index e30e8c82d23..0aa5858c8e6 100644 --- a/src/hotspot/cpu/ppc/gc/z/zBarrierSetAssembler_ppc.cpp +++ b/src/hotspot/cpu/ppc/gc/z/zBarrierSetAssembler_ppc.cpp @@ -114,7 +114,6 @@ public: } }; - void ZBarrierSetAssembler::load_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type, Register base, RegisterOrConstant ind_or_offs, Register dst, Register tmp1, Register tmp2, @@ -561,7 +560,6 @@ void ZBarrierSetAssembler::generate_conjoint_oop_copy(MacroAssembler* masm, bool copy_store_at_slow(masm, R4_ARG2, tmp, store_bad, store_good, dest_uninitialized); } - // Verify a colored pointer. void ZBarrierSetAssembler::check_oop(MacroAssembler *masm, Register obj, const char* msg) { if (!VerifyOops) { @@ -583,7 +581,6 @@ void ZBarrierSetAssembler::check_oop(MacroAssembler *masm, Register obj, const c __ bind(done); } - void ZBarrierSetAssembler::try_resolve_jobject_in_native(MacroAssembler* masm, Register dst, Register jni_env, Register obj, Register tmp, Label& slowpath) { __ block_comment("try_resolve_jobject_in_native (zgc) {"); diff --git a/src/hotspot/cpu/x86/gc/z/zBarrierSetAssembler_x86.cpp b/src/hotspot/cpu/x86/gc/z/zBarrierSetAssembler_x86.cpp index 0b19180ea06..ae93cca8c19 100644 --- a/src/hotspot/cpu/x86/gc/z/zBarrierSetAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/gc/z/zBarrierSetAssembler_x86.cpp @@ -1410,11 +1410,9 @@ void ZBarrierSetAssembler::patch_barriers() { } } - #undef __ #define __ masm-> - void ZBarrierSetAssembler::check_oop(MacroAssembler* masm, Register obj, Register tmp1, Register tmp2, Label& error) { // C1 calls verfy_oop in the middle of barriers, before they have been uncolored // and after being colored. Therefore, we must deal with colored oops as well. diff --git a/src/hotspot/os_cpu/linux_ppc/gc/z/zSyscall_linux_ppc.hpp b/src/hotspot/os_cpu/linux_ppc/gc/z/zSyscall_linux_ppc.hpp index 5950b52136d..1b2b5dad7d7 100644 --- a/src/hotspot/os_cpu/linux_ppc/gc/z/zSyscall_linux_ppc.hpp +++ b/src/hotspot/os_cpu/linux_ppc/gc/z/zSyscall_linux_ppc.hpp @@ -31,7 +31,6 @@ // Support for building on older Linux systems // - #ifndef SYS_memfd_create #define SYS_memfd_create 360 #endif diff --git a/src/hotspot/share/gc/z/zBarrierSet.inline.hpp b/src/hotspot/share/gc/z/zBarrierSet.inline.hpp index 16f2a303cb2..7d8f74ede57 100644 --- a/src/hotspot/share/gc/z/zBarrierSet.inline.hpp +++ b/src/hotspot/share/gc/z/zBarrierSet.inline.hpp @@ -349,7 +349,6 @@ inline bool ZBarrierSet::AccessBarrier::oop_copy_one_ch return true; } - template inline bool ZBarrierSet::AccessBarrier::oop_arraycopy_in_heap_check_cast(zpointer* dst, zpointer* src, size_t length, Klass* dst_klass) { // Check cast and copy each elements diff --git a/src/hotspot/share/gc/z/zGeneration.cpp b/src/hotspot/share/gc/z/zGeneration.cpp index 4a17e80d665..2a7d0af966a 100644 --- a/src/hotspot/share/gc/z/zGeneration.cpp +++ b/src/hotspot/share/gc/z/zGeneration.cpp @@ -661,7 +661,6 @@ public: } }; - bool ZGenerationYoung::pause_mark_end() { return VM_ZMarkEndYoung().pause(); } @@ -1320,7 +1319,6 @@ class ZRendezvousGCThreads: public VM_Operation { }; }; - void ZGenerationOld::process_non_strong_references() { // Process Soft/Weak/Final/PhantomReferences _reference_processor.process_references(); diff --git a/src/hotspot/share/gc/z/zMark.cpp b/src/hotspot/share/gc/z/zMark.cpp index cdd4ac16179..8ddab4c9c3d 100644 --- a/src/hotspot/share/gc/z/zMark.cpp +++ b/src/hotspot/share/gc/z/zMark.cpp @@ -388,7 +388,6 @@ void ZMark::follow_object(oop obj, bool finalizable) { } } - void ZMark::mark_and_follow(ZMarkContext* context, ZMarkStackEntry entry) { // Decode flags const bool finalizable = entry.finalizable(); diff --git a/src/hotspot/share/gc/z/zPage.inline.hpp b/src/hotspot/share/gc/z/zPage.inline.hpp index 970ee6600a5..5ff6278ea09 100644 --- a/src/hotspot/share/gc/z/zPage.inline.hpp +++ b/src/hotspot/share/gc/z/zPage.inline.hpp @@ -368,7 +368,6 @@ inline bool ZPage::was_remembered(volatile zpointer* p) { return _remembered_set.at_previous(l_offset); } - inline zaddress_unsafe ZPage::find_base_unsafe(volatile zpointer* p) { if (is_large()) { return ZOffset::address_unsafe(start()); diff --git a/src/hotspot/share/gc/z/zPageAllocator.cpp b/src/hotspot/share/gc/z/zPageAllocator.cpp index 9f762dd403e..1eb0ed67a86 100644 --- a/src/hotspot/share/gc/z/zPageAllocator.cpp +++ b/src/hotspot/share/gc/z/zPageAllocator.cpp @@ -1051,7 +1051,6 @@ void ZPartition::copy_physical_segments_from_partition(const ZVirtualMemory& at, ZPhysicalMemoryManager& manager = physical_memory_manager(); - // Copy segments manager.copy_physical_segments(to, at); } diff --git a/src/hotspot/share/gc/z/zRelocate.cpp b/src/hotspot/share/gc/z/zRelocate.cpp index 3726534588b..69233da6f54 100644 --- a/src/hotspot/share/gc/z/zRelocate.cpp +++ b/src/hotspot/share/gc/z/zRelocate.cpp @@ -671,7 +671,6 @@ private: assert(ZHeap::heap()->is_in_page_relaxed(from_page, from_addr), "Must be"); assert(to_page->is_in(to_addr), "Must be"); - // Read the size from the to-object, since the from-object // could have been overwritten during in-place relocation. const size_t size = ZUtils::object_size(to_addr); diff --git a/src/hotspot/share/gc/z/zValue.inline.hpp b/src/hotspot/share/gc/z/zValue.inline.hpp index f0ff891c3bc..190015c89f3 100644 --- a/src/hotspot/share/gc/z/zValue.inline.hpp +++ b/src/hotspot/share/gc/z/zValue.inline.hpp @@ -238,5 +238,4 @@ inline bool ZValueConstIterator::next(const T** value) { return false; } - #endif // SHARE_GC_Z_ZVALUE_INLINE_HPP diff --git a/src/hotspot/share/gc/z/zVirtualMemoryManager.inline.hpp b/src/hotspot/share/gc/z/zVirtualMemoryManager.inline.hpp index 27159b4eff8..ca474196cc6 100644 --- a/src/hotspot/share/gc/z/zVirtualMemoryManager.inline.hpp +++ b/src/hotspot/share/gc/z/zVirtualMemoryManager.inline.hpp @@ -29,7 +29,6 @@ #include "gc/z/zRangeRegistry.inline.hpp" #include "utilities/globalDefinitions.hpp" - inline bool ZVirtualMemoryManager::is_multi_partition_enabled() const { return _is_multi_partition_enabled; } diff --git a/test/hotspot/jtreg/gc/z/TestZNMT.java b/test/hotspot/jtreg/gc/z/TestZNMT.java index 45f10c0133c..0182170cdd0 100644 --- a/test/hotspot/jtreg/gc/z/TestZNMT.java +++ b/test/hotspot/jtreg/gc/z/TestZNMT.java @@ -59,7 +59,6 @@ public class TestZNMT { } } - private static void testValue(int zForceDiscontiguousHeapReservations) throws Exception { /** * Xmx is picked so that it is divisible by 'ZForceDiscontiguousHeapReservations * ZGranuleSize' From 7ed72d943b8d4c5cd0d3707c0c95148db74401bf Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Tue, 23 Sep 2025 07:12:27 +0000 Subject: [PATCH 173/556] 8368212: ZGC: Fix spelling and typos in comments Reviewed-by: jsikstro, stefank --- src/hotspot/share/gc/z/zArguments.cpp | 2 +- src/hotspot/share/gc/z/zBarrierSet.cpp | 2 +- src/hotspot/share/gc/z/zDirector.cpp | 2 +- src/hotspot/share/gc/z/zHeapIterator.cpp | 2 +- src/hotspot/share/gc/z/zJNICritical.cpp | 2 +- src/hotspot/share/gc/z/zNMethod.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hotspot/share/gc/z/zArguments.cpp b/src/hotspot/share/gc/z/zArguments.cpp index 03aa8e5acee..2435c6fb31f 100644 --- a/src/hotspot/share/gc/z/zArguments.cpp +++ b/src/hotspot/share/gc/z/zArguments.cpp @@ -45,7 +45,7 @@ void ZArguments::initialize_heap_flags_and_sizes() { !FLAG_IS_CMDLINE(SoftMaxHeapSize)) { // We are really just guessing how much memory the program needs. // When that is the case, we don't want the soft and hard limits to be the same - // as it can cause flakyness in the number of GC threads used, in order to keep + // as it can cause flakiness in the number of GC threads used, in order to keep // to a random number we just pulled out of thin air. FLAG_SET_ERGO(SoftMaxHeapSize, MaxHeapSize * 90 / 100); } diff --git a/src/hotspot/share/gc/z/zBarrierSet.cpp b/src/hotspot/share/gc/z/zBarrierSet.cpp index c71c404712c..31996def9c1 100644 --- a/src/hotspot/share/gc/z/zBarrierSet.cpp +++ b/src/hotspot/share/gc/z/zBarrierSet.cpp @@ -117,7 +117,7 @@ static void deoptimize_allocation(JavaThread* thread) { const nmethod* const nm = caller_frame.cb()->as_nmethod(); if ((nm->is_compiled_by_c2() || nm->is_compiled_by_jvmci()) && !caller_frame.is_deoptimized_frame()) { // The JIT might have elided barriers on this object so deoptimize the frame and let the - // intepreter deal with it. + // interpreter deal with it. Deoptimization::deoptimize_frame(thread, caller_frame.id()); } } diff --git a/src/hotspot/share/gc/z/zDirector.cpp b/src/hotspot/share/gc/z/zDirector.cpp index 380a1e48ab4..7c5bb907edf 100644 --- a/src/hotspot/share/gc/z/zDirector.cpp +++ b/src/hotspot/share/gc/z/zDirector.cpp @@ -302,7 +302,7 @@ static bool is_young_small(const ZDirectorStats& stats) { // If the freeable memory isn't even 5% of the heap, we can't expect to free up // all that much memory, so let's not even try - it will likely be a wasted effort - // that takes away CPU power to the hopefullt more profitable major colelction. + // that takes away CPU power to the hopefully more profitable major collection. return young_used_percent <= 5.0; } diff --git a/src/hotspot/share/gc/z/zHeapIterator.cpp b/src/hotspot/share/gc/z/zHeapIterator.cpp index 8e423fe0fca..d6289178ea8 100644 --- a/src/hotspot/share/gc/z/zHeapIterator.cpp +++ b/src/hotspot/share/gc/z/zHeapIterator.cpp @@ -364,7 +364,7 @@ public: virtual void do_nmethod(nmethod* nm) { // If ClassUnloading is turned off, all nmethods are considered strong, // not only those on the call stacks. The heap iteration might happen - // before the concurrent processign of the code cache, make sure that + // before the concurrent processing of the code cache, make sure that // all nmethods have been processed before visiting the oops. _bs_nm->nmethod_entry_barrier(nm); diff --git a/src/hotspot/share/gc/z/zJNICritical.cpp b/src/hotspot/share/gc/z/zJNICritical.cpp index 01c0a0dd6b8..28cc70cb3b5 100644 --- a/src/hotspot/share/gc/z/zJNICritical.cpp +++ b/src/hotspot/share/gc/z/zJNICritical.cpp @@ -160,7 +160,7 @@ void ZJNICritical::exit_inner() { // and we should signal that all Java threads have now exited the // critical region and we are now blocked. if (count == -2) { - // Nofity blocked + // Notify blocked ZLocker locker(_lock); _lock->notify_all(); } diff --git a/src/hotspot/share/gc/z/zNMethod.cpp b/src/hotspot/share/gc/z/zNMethod.cpp index a5d94fc1e5e..780bc9e3bf7 100644 --- a/src/hotspot/share/gc/z/zNMethod.cpp +++ b/src/hotspot/share/gc/z/zNMethod.cpp @@ -314,7 +314,7 @@ oop ZNMethod::oop_load_phantom(const nmethod* nm, int index) { oop ZNMethod::oop_load(const nmethod* const_nm, int index, bool keep_alive) { // The rest of the code is not ready to handle const nmethod, so cast it away - // until we are more consistent with our const corectness. + // until we are more consistent with our const correctness. nmethod* nm = const_cast(const_nm); if (!is_armed(nm)) { From 47ed1a8d1768ef0623fd9d1ff68d39df5dffaad0 Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Tue, 23 Sep 2025 07:15:06 +0000 Subject: [PATCH 174/556] 8368213: ZGC: Cleanup dead code, unimplemented declarations, unused private fields Reviewed-by: stefank, jsikstro --- .../share/gc/shared/gcThreadLocalData.hpp | 2 +- src/hotspot/share/gc/z/zBarrier.hpp | 2 -- src/hotspot/share/gc/z/zDirector.cpp | 22 +++++-------------- src/hotspot/share/gc/z/zGeneration.cpp | 3 --- src/hotspot/share/gc/z/zGeneration.hpp | 1 - src/hotspot/share/gc/z/zMark.cpp | 12 ++++------ src/hotspot/share/gc/z/zNMethodTable.hpp | 6 ----- src/hotspot/share/gc/z/zPageAllocator.cpp | 5 ----- src/hotspot/share/gc/z/zThreadLocalData.hpp | 2 -- 9 files changed, 10 insertions(+), 45 deletions(-) diff --git a/src/hotspot/share/gc/shared/gcThreadLocalData.hpp b/src/hotspot/share/gc/shared/gcThreadLocalData.hpp index ba0e6d8fb1a..2632e806bab 100644 --- a/src/hotspot/share/gc/shared/gcThreadLocalData.hpp +++ b/src/hotspot/share/gc/shared/gcThreadLocalData.hpp @@ -40,6 +40,6 @@ // should consider placing frequently accessed fields first in // T, so that field offsets relative to Thread are small, which // often allows for a more compact instruction encoding. -typedef uint64_t GCThreadLocalData[43]; // 344 bytes +typedef uint64_t GCThreadLocalData[40]; // 320 bytes #endif // SHARE_GC_SHARED_GCTHREADLOCALDATA_HPP diff --git a/src/hotspot/share/gc/z/zBarrier.hpp b/src/hotspot/share/gc/z/zBarrier.hpp index 3f9e6c78b04..3071061c997 100644 --- a/src/hotspot/share/gc/z/zBarrier.hpp +++ b/src/hotspot/share/gc/z/zBarrier.hpp @@ -71,8 +71,6 @@ typedef zpointer (*ZBarrierColor)(zaddress, zpointer); class ZGeneration; -void z_assert_is_barrier_safe(); - class ZBarrier : public AllStatic { friend class ZContinuation; friend class ZStoreBarrierBuffer; diff --git a/src/hotspot/share/gc/z/zDirector.cpp b/src/hotspot/share/gc/z/zDirector.cpp index 7c5bb907edf..cd5474b22ba 100644 --- a/src/hotspot/share/gc/z/zDirector.cpp +++ b/src/hotspot/share/gc/z/zDirector.cpp @@ -306,8 +306,7 @@ static bool is_young_small(const ZDirectorStats& stats) { return young_used_percent <= 5.0; } -template -static bool is_high_usage(const ZDirectorStats& stats, PrintFn* print_function = nullptr) { +static bool is_high_usage(const ZDirectorStats& stats, bool log = false) { // Calculate amount of free memory available. Note that we take the // relocation headroom into account to avoid in-place relocation. const size_t soft_max_capacity = stats._heap._soft_max_heap_size; @@ -316,8 +315,9 @@ static bool is_high_usage(const ZDirectorStats& stats, PrintFn* print_function = const size_t free = free_including_headroom - MIN2(free_including_headroom, ZHeuristics::relocation_headroom()); const double free_percent = percent_of(free, soft_max_capacity); - if (print_function != nullptr) { - (*print_function)(free, free_percent); + if (log) { + log_debug(gc, director)("Rule Minor: High Usage, Free: %zuMB(%.1f%%)", + free / M, free_percent); } // The heap has high usage if there is less than 5% free memory left @@ -377,19 +377,7 @@ static bool rule_minor_high_usage(const ZDirectorStats& stats) { // such that the allocation rate rule doesn't trigger, but the amount of free // memory is still slowly but surely heading towards zero. In this situation, // we start a GC cycle to avoid a potential allocation stall later. - - const size_t soft_max_capacity = stats._heap._soft_max_heap_size; - const size_t used = stats._heap._used; - const size_t free_including_headroom = soft_max_capacity - MIN2(soft_max_capacity, used); - const size_t free = free_including_headroom - MIN2(free_including_headroom, ZHeuristics::relocation_headroom()); - const double free_percent = percent_of(free, soft_max_capacity); - - auto print_function = [&](size_t free, double free_percent) { - log_debug(gc, director)("Rule Minor: High Usage, Free: %zuMB(%.1f%%)", - free / M, free_percent); - }; - - return is_high_usage(stats, &print_function); + return is_high_usage(stats, true /* log */); } // Major GC rules diff --git a/src/hotspot/share/gc/z/zGeneration.cpp b/src/hotspot/share/gc/z/zGeneration.cpp index 2a7d0af966a..d1680b6c336 100644 --- a/src/hotspot/share/gc/z/zGeneration.cpp +++ b/src/hotspot/share/gc/z/zGeneration.cpp @@ -701,13 +701,11 @@ uint ZGenerationYoung::compute_tenuring_threshold(ZRelocationSetSelectorStats st double young_life_expectancy_sum = 0.0; uint young_life_expectancy_samples = 0; uint last_populated_age = 0; - size_t last_populated_live = 0; for (ZPageAge age : ZPageAgeRangeAll) { const size_t young_live = stats.small(age).live() + stats.medium(age).live() + stats.large(age).live(); if (young_live > 0) { last_populated_age = untype(age); - last_populated_live = young_live; if (young_live_last > 0) { young_life_expectancy_sum += double(young_live) / double(young_live_last); young_life_expectancy_samples++; @@ -721,7 +719,6 @@ uint ZGenerationYoung::compute_tenuring_threshold(ZRelocationSetSelectorStats st return 0; } - const size_t young_used_at_mark_start = ZGeneration::young()->stat_heap()->used_generation_at_mark_start(); const size_t young_garbage = ZGeneration::young()->stat_heap()->garbage_at_mark_end(); const size_t young_allocated = ZGeneration::young()->stat_heap()->allocated_at_mark_end(); const size_t soft_max_capacity = ZHeap::heap()->soft_max_capacity(); diff --git a/src/hotspot/share/gc/z/zGeneration.hpp b/src/hotspot/share/gc/z/zGeneration.hpp index 13adc06b123..7ce096de6db 100644 --- a/src/hotspot/share/gc/z/zGeneration.hpp +++ b/src/hotspot/share/gc/z/zGeneration.hpp @@ -87,7 +87,6 @@ protected: void free_empty_pages(ZRelocationSetSelector* selector, int bulk); void flip_age_pages(const ZRelocationSetSelector* selector); - void flip_age_pages(const ZArray* pages); void mark_free(); diff --git a/src/hotspot/share/gc/z/zMark.cpp b/src/hotspot/share/gc/z/zMark.cpp index 8ddab4c9c3d..3b247fdd35e 100644 --- a/src/hotspot/share/gc/z/zMark.cpp +++ b/src/hotspot/share/gc/z/zMark.cpp @@ -789,7 +789,6 @@ typedef ClaimingCLDToOopClosure ZMarkOldCLDClosu class ZMarkOldRootsTask : public ZTask { private: - ZMark* const _mark; ZRootsIteratorStrongColored _roots_colored; ZRootsIteratorStrongUncolored _roots_uncolored; @@ -800,9 +799,8 @@ private: ZMarkNMethodClosure _nm_cl; public: - ZMarkOldRootsTask(ZMark* mark) + ZMarkOldRootsTask() : ZTask("ZMarkOldRootsTask"), - _mark(mark), _roots_colored(ZGenerationIdOptional::old), _roots_uncolored(ZGenerationIdOptional::old), _cl_colored(), @@ -847,7 +845,6 @@ public: class ZMarkYoungRootsTask : public ZTask { private: - ZMark* const _mark; ZRootsIteratorAllColored _roots_colored; ZRootsIteratorAllUncolored _roots_uncolored; @@ -858,9 +855,8 @@ private: ZMarkYoungNMethodClosure _nm_cl; public: - ZMarkYoungRootsTask(ZMark* mark) + ZMarkYoungRootsTask() : ZTask("ZMarkYoungRootsTask"), - _mark(mark), _roots_colored(ZGenerationIdOptional::young), _roots_uncolored(ZGenerationIdOptional::young), _cl_colored(), @@ -928,13 +924,13 @@ void ZMark::resize_workers(uint nworkers) { void ZMark::mark_young_roots() { SuspendibleThreadSetJoiner sts_joiner; - ZMarkYoungRootsTask task(this); + ZMarkYoungRootsTask task; workers()->run(&task); } void ZMark::mark_old_roots() { SuspendibleThreadSetJoiner sts_joiner; - ZMarkOldRootsTask task(this); + ZMarkOldRootsTask task; workers()->run(&task); } diff --git a/src/hotspot/share/gc/z/zNMethodTable.hpp b/src/hotspot/share/gc/z/zNMethodTable.hpp index a8b9029caeb..3626a67312c 100644 --- a/src/hotspot/share/gc/z/zNMethodTable.hpp +++ b/src/hotspot/share/gc/z/zNMethodTable.hpp @@ -43,9 +43,6 @@ private: static ZNMethodTableIteration _iteration_secondary; static ZSafeDelete _safe_delete; - static ZNMethodTableEntry* create(size_t size); - static void destroy(ZNMethodTableEntry* table); - static size_t first_index(const nmethod* nm, size_t size); static size_t next_index(size_t prev_index, size_t size); @@ -67,9 +64,6 @@ public: static void nmethods_do_begin(bool secondary); static void nmethods_do_end(bool secondary); static void nmethods_do(bool secondary, NMethodClosure* cl); - - static void unlink(ZWorkers* workers, bool unloading_occurred); - static void purge(ZWorkers* workers); }; #endif // SHARE_GC_Z_ZNMETHODTABLE_HPP diff --git a/src/hotspot/share/gc/z/zPageAllocator.cpp b/src/hotspot/share/gc/z/zPageAllocator.cpp index 1eb0ed67a86..dbda9e9e9a2 100644 --- a/src/hotspot/share/gc/z/zPageAllocator.cpp +++ b/src/hotspot/share/gc/z/zPageAllocator.cpp @@ -1060,7 +1060,6 @@ void ZPartition::commit_increased_capacity(ZMemoryAllocation* allocation, const const size_t already_committed = allocation->harvested(); - const ZVirtualMemory already_committed_vmem = vmem.first_part(already_committed); const ZVirtualMemory to_be_committed_vmem = vmem.last_part(already_committed); // Try to commit the uncommitted physical memory @@ -1422,7 +1421,6 @@ ZPage* ZPageAllocator::alloc_page(ZPageType type, size_t size, ZAllocationFlags const ZPageAllocationStats stats = allocation.stats(); const int num_harvested_vmems = stats._num_harvested_vmems; const size_t harvested = stats._total_harvested; - const size_t committed = stats._total_committed_capacity; if (harvested > 0) { ZStatInc(ZCounterMappedCacheHarvest, harvested); @@ -1963,9 +1961,6 @@ void ZPageAllocator::cleanup_failed_commit_multi_partition(ZMultiPartitionAlloca continue; } - // Remove the harvested part - const ZVirtualMemory non_harvest_vmem = partial_vmem.last_part(allocation->harvested()); - ZArray* const partial_vmems = allocation->partial_vmems(); // Keep track of the start index diff --git a/src/hotspot/share/gc/z/zThreadLocalData.hpp b/src/hotspot/share/gc/z/zThreadLocalData.hpp index 8ff8196fbe2..111a64e978e 100644 --- a/src/hotspot/share/gc/z/zThreadLocalData.hpp +++ b/src/hotspot/share/gc/z/zThreadLocalData.hpp @@ -39,7 +39,6 @@ private: uintptr_t _mark_bad_mask; uintptr_t _store_good_mask; uintptr_t _store_bad_mask; - uintptr_t _uncolor_mask; uintptr_t _nmethod_disarmed; ZStoreBarrierBuffer* _store_barrier_buffer; ZMarkThreadLocalStacks _mark_stacks[2]; @@ -51,7 +50,6 @@ private: _mark_bad_mask(0), _store_good_mask(0), _store_bad_mask(0), - _uncolor_mask(0), _nmethod_disarmed(0), _store_barrier_buffer(new ZStoreBarrierBuffer()), _mark_stacks(), From 360b6af1b1c39e6d3a01c4a32473cf007ed632c6 Mon Sep 17 00:00:00 2001 From: Shawn M Emery Date: Tue, 23 Sep 2025 07:54:51 +0000 Subject: [PATCH 175/556] 8364657: Crash for SecureRandom.generateSeed(0) on Windows x86-64 Co-authored-by: Jaikiran Pai Reviewed-by: weijun, jpai --- .../windows/native/libsunmscapi/security.cpp | 8 +++- .../security/SecureRandom/TestStrong.java | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 test/jdk/java/security/SecureRandom/TestStrong.java diff --git a/src/jdk.crypto.mscapi/windows/native/libsunmscapi/security.cpp b/src/jdk.crypto.mscapi/windows/native/libsunmscapi/security.cpp index 3808e6e1e48..ff011dca889 100644 --- a/src/jdk.crypto.mscapi/windows/native/libsunmscapi/security.cpp +++ b/src/jdk.crypto.mscapi/windows/native/libsunmscapi/security.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -366,7 +366,11 @@ JNIEXPORT jbyteArray JNICALL Java_sun_security_mscapi_PRNG_generateSeed } else { - if (length > 0) { + /* + * seed could be NULL here when SecureRandom.generateSeed() is + * called with a length of zero. + */ + if ((length > 0) || (seed == NULL)) { seed = env->NewByteArray(length); if (seed == NULL) { __leave; diff --git a/test/jdk/java/security/SecureRandom/TestStrong.java b/test/jdk/java/security/SecureRandom/TestStrong.java new file mode 100644 index 00000000000..f5de07244e1 --- /dev/null +++ b/test/jdk/java/security/SecureRandom/TestStrong.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.security.SecureRandom; +import java.util.Arrays; + +/* + * @test + * @bug 8364657 + * @summary verify the behavior of SecureRandom instance returned by + * SecureRandom.getInstanceStrong() + * @run main TestStrong + */ +public class TestStrong { + + public static void main(String[] args) throws Exception { + + final SecureRandom random = SecureRandom.getInstanceStrong(); + System.out.println("going to generate random seed using " + random); + final byte[] seed = random.generateSeed(0); + System.out.println("random seed generated"); + System.out.println("seed: " + Arrays.toString(seed)); + } +} From 3e5094ed12dbfad7587b85ae2168565682c1f1db Mon Sep 17 00:00:00 2001 From: Ivan Walulya Date: Tue, 23 Sep 2025 08:19:12 +0000 Subject: [PATCH 176/556] 8366865: Allocation GC Pauses Triggered after JVM has started shutdown Reviewed-by: ayang, tschatzl --- src/hotspot/share/gc/g1/g1CollectedHeap.cpp | 16 +++++++++---- src/hotspot/share/gc/g1/g1CollectedHeap.hpp | 3 --- src/hotspot/share/gc/g1/g1ConcurrentMark.cpp | 2 +- src/hotspot/share/gc/g1/g1Policy.cpp | 3 ++- src/hotspot/share/gc/g1/g1RemSet.cpp | 2 +- src/hotspot/share/gc/g1/g1VMOperations.cpp | 21 ++++++++--------- src/hotspot/share/gc/g1/g1VMOperations.hpp | 2 -- .../gc/parallel/parallelScavengeHeap.cpp | 5 ++++ src/hotspot/share/gc/serial/serialHeap.cpp | 5 ++++ src/hotspot/share/gc/shared/collectedHeap.cpp | 23 +++++++++++++++++++ src/hotspot/share/gc/shared/collectedHeap.hpp | 7 ++++++ .../share/gc/shared/gcVMOperations.cpp | 2 +- src/hotspot/share/memory/universe.cpp | 10 +++++++- src/hotspot/share/memory/universe.hpp | 5 ++++ src/hotspot/share/services/cpuTimeUsage.cpp | 1 + 15 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index ed21c9aa370..28330425511 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -480,6 +480,11 @@ HeapWord* G1CollectedHeap::attempt_allocation_slow(uint node_index, size_t word_ log_warning(gc, alloc)("%s: Retried allocation %u times for %zu words", Thread::current()->name(), try_count, word_size); } + + if (is_shutting_down()) { + stall_for_vm_shutdown(); + return nullptr; + } } ShouldNotReachHere(); @@ -714,6 +719,11 @@ HeapWord* G1CollectedHeap::attempt_allocation_humongous(size_t word_size) { log_warning(gc, alloc)("%s: Retried allocation %u times for %zu words", Thread::current()->name(), try_count, word_size); } + + if (is_shutting_down()) { + stall_for_vm_shutdown(); + return nullptr; + } } ShouldNotReachHere(); @@ -1551,10 +1561,6 @@ jint G1CollectedHeap::initialize() { return JNI_OK; } -bool G1CollectedHeap::concurrent_mark_is_terminating() const { - return _cm_thread->should_terminate(); -} - void G1CollectedHeap::stop() { // Stop all concurrent threads. We do this to make sure these threads // do not continue to execute and access resources (e.g. logging) @@ -1881,7 +1887,7 @@ bool G1CollectedHeap::try_collect_concurrently(GCCause::Cause cause, // If VMOp skipped initiating concurrent marking cycle because // we're terminating, then we're done. - if (op.terminating()) { + if (is_shutting_down()) { LOG_COLLECT_CONCURRENTLY(cause, "skipped: terminating"); return false; } diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp index 43839cc48d5..322b37188d3 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp @@ -914,9 +914,6 @@ public: // specified by the policy object. jint initialize() override; - // Returns whether concurrent mark threads (and the VM) are about to terminate. - bool concurrent_mark_is_terminating() const; - void safepoint_synchronize_begin() override; void safepoint_synchronize_end() override; diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp index 97386cb9720..475917f91c7 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp @@ -1884,7 +1884,7 @@ bool G1ConcurrentMark::concurrent_cycle_abort() { // nothing, but this situation should be extremely rare (a full gc after shutdown // has been signalled is already rare), and this work should be negligible compared // to actual full gc work. - if (!cm_thread()->in_progress() && !_g1h->concurrent_mark_is_terminating()) { + if (!cm_thread()->in_progress() && !_g1h->is_shutting_down()) { return false; } diff --git a/src/hotspot/share/gc/g1/g1Policy.cpp b/src/hotspot/share/gc/g1/g1Policy.cpp index 754cc502031..88a2689e68f 100644 --- a/src/hotspot/share/gc/g1/g1Policy.cpp +++ b/src/hotspot/share/gc/g1/g1Policy.cpp @@ -669,6 +669,7 @@ bool G1Policy::should_retain_evac_failed_region(uint index) const { } void G1Policy::record_pause_start_time() { + assert(!_g1h->is_shutting_down(), "Invariant!"); Ticks now = Ticks::now(); _cur_pause_start_sec = now.seconds(); @@ -1275,7 +1276,7 @@ void G1Policy::decide_on_concurrent_start_pause() { // We should not be starting a concurrent start pause if the concurrent mark // thread is terminating. - if (_g1h->concurrent_mark_is_terminating()) { + if (_g1h->is_shutting_down()) { return; } diff --git a/src/hotspot/share/gc/g1/g1RemSet.cpp b/src/hotspot/share/gc/g1/g1RemSet.cpp index d2df416edc2..d2bfd22d830 100644 --- a/src/hotspot/share/gc/g1/g1RemSet.cpp +++ b/src/hotspot/share/gc/g1/g1RemSet.cpp @@ -1024,7 +1024,7 @@ class G1MergeHeapRootsTask : public WorkerTask { // There might actually have been scheduled multiple collections, but at that point we do // not care that much about performance and just do the work multiple times if needed. return (_g1h->collector_state()->clear_bitmap_in_progress() || - _g1h->concurrent_mark_is_terminating()) && + _g1h->is_shutting_down()) && hr->is_old(); } diff --git a/src/hotspot/share/gc/g1/g1VMOperations.cpp b/src/hotspot/share/gc/g1/g1VMOperations.cpp index 6757172b625..34e14f742ae 100644 --- a/src/hotspot/share/gc/g1/g1VMOperations.cpp +++ b/src/hotspot/share/gc/g1/g1VMOperations.cpp @@ -64,7 +64,6 @@ VM_G1TryInitiateConcMark::VM_G1TryInitiateConcMark(uint gc_count_before, _mark_in_progress(false), _cycle_already_in_progress(false), _whitebox_attached(false), - _terminating(false), _gc_succeeded(false) {} @@ -83,19 +82,10 @@ void VM_G1TryInitiateConcMark::doit() { GCCauseSetter x(g1h, _gc_cause); - // Record for handling by caller. - _terminating = g1h->concurrent_mark_is_terminating(); - _mark_in_progress = g1h->collector_state()->mark_in_progress(); _cycle_already_in_progress = g1h->concurrent_mark()->cm_thread()->in_progress(); - if (_terminating && GCCause::is_user_requested_gc(_gc_cause)) { - // When terminating, the request to initiate a concurrent cycle will be - // ignored by do_collection_pause_at_safepoint; instead it will just do - // a young-only or mixed GC (depending on phase). For a user request - // there's no point in even doing that much, so done. For some non-user - // requests the alternative GC might still be needed. - } else if (!g1h->policy()->force_concurrent_start_if_outside_cycle(_gc_cause)) { + if (!g1h->policy()->force_concurrent_start_if_outside_cycle(_gc_cause)) { // Failure to force the next GC pause to be a concurrent start indicates // there is already a concurrent marking cycle in progress. Flags to indicate // that were already set, so return immediately. @@ -119,7 +109,6 @@ VM_G1CollectForAllocation::VM_G1CollectForAllocation(size_t word_size, void VM_G1CollectForAllocation::doit() { G1CollectedHeap* g1h = G1CollectedHeap::heap(); - GCCauseSetter x(g1h, _gc_cause); // Try a partial collection of some kind. g1h->do_collection_pause_at_safepoint(); @@ -156,6 +145,14 @@ void VM_G1PauseConcurrent::doit() { bool VM_G1PauseConcurrent::doit_prologue() { Heap_lock->lock(); + G1CollectedHeap* g1h = G1CollectedHeap::heap(); + if (g1h->is_shutting_down()) { + Heap_lock->unlock(); + // JVM shutdown has started. This ensures that any further operations will be properly aborted + // and will not interfere with the shutdown process. + g1h->concurrent_mark()->abort_marking_threads(); + return false; + } return true; } diff --git a/src/hotspot/share/gc/g1/g1VMOperations.hpp b/src/hotspot/share/gc/g1/g1VMOperations.hpp index 05b27c4508c..2adeaed04d9 100644 --- a/src/hotspot/share/gc/g1/g1VMOperations.hpp +++ b/src/hotspot/share/gc/g1/g1VMOperations.hpp @@ -48,7 +48,6 @@ class VM_G1TryInitiateConcMark : public VM_GC_Collect_Operation { bool _mark_in_progress; bool _cycle_already_in_progress; bool _whitebox_attached; - bool _terminating; // The concurrent start pause may be cancelled for some reasons. Keep track of // this. bool _gc_succeeded; @@ -63,7 +62,6 @@ public: bool mark_in_progress() const { return _mark_in_progress; } bool cycle_already_in_progress() const { return _cycle_already_in_progress; } bool whitebox_attached() const { return _whitebox_attached; } - bool terminating() const { return _terminating; } bool gc_succeeded() const { return _gc_succeeded && VM_GC_Operation::gc_succeeded(); } }; diff --git a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp index 213e8f95d63..ae2c36e44c5 100644 --- a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp +++ b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp @@ -326,6 +326,11 @@ HeapWord* ParallelScavengeHeap::mem_allocate_work(size_t size, bool is_tlab) { assert(is_in_or_null(op.result()), "result not in heap"); return op.result(); } + + if (is_shutting_down()) { + stall_for_vm_shutdown(); + return nullptr; + } } // Was the gc-overhead reached inside the safepoint? If so, this mutator diff --git a/src/hotspot/share/gc/serial/serialHeap.cpp b/src/hotspot/share/gc/serial/serialHeap.cpp index f26e4427062..89218648fe0 100644 --- a/src/hotspot/share/gc/serial/serialHeap.cpp +++ b/src/hotspot/share/gc/serial/serialHeap.cpp @@ -340,6 +340,11 @@ HeapWord* SerialHeap::mem_allocate_work(size_t size, bool is_tlab) { break; } + if (is_shutting_down()) { + stall_for_vm_shutdown(); + return nullptr; + } + // Give a warning if we seem to be looping forever. if ((QueuedAllocationWarningCount > 0) && (try_count % QueuedAllocationWarningCount == 0)) { diff --git a/src/hotspot/share/gc/shared/collectedHeap.cpp b/src/hotspot/share/gc/shared/collectedHeap.cpp index 9b6956ca75a..8ef73c6a0b8 100644 --- a/src/hotspot/share/gc/shared/collectedHeap.cpp +++ b/src/hotspot/share/gc/shared/collectedHeap.cpp @@ -385,6 +385,12 @@ MetaWord* CollectedHeap::satisfy_failed_metadata_allocation(ClassLoaderData* loa if (op.gc_succeeded()) { return op.result(); } + + if (is_shutting_down()) { + stall_for_vm_shutdown(); + return nullptr; + } + loop_count++; if ((QueuedAllocationWarningCount > 0) && (loop_count % QueuedAllocationWarningCount == 0)) { @@ -603,6 +609,23 @@ void CollectedHeap::post_initialize() { initialize_serviceability(); } +bool CollectedHeap::is_shutting_down() const { + return Universe::is_shutting_down(); +} + +void CollectedHeap::stall_for_vm_shutdown() { + assert(is_shutting_down(), "Precondition"); + // Stall the thread (2 seconds) instead of an indefinite wait to avoid deadlock + // if the VM shutdown triggers a GC. + // The 2-seconds sleep is: + // - long enough to keep daemon threads stalled, while the shutdown + // sequence completes in the common case. + // - short enough to avoid excessive stall time if the shutdown itself + // triggers a GC. + JavaThread::current()->sleep(2 * MILLIUNITS); + log_warning(gc, alloc)("%s: Stall for VM-Shutdown timed out; allocation may fail with OOME", Thread::current()->name()); +} + void CollectedHeap::before_exit() { print_tracing_info(); diff --git a/src/hotspot/share/gc/shared/collectedHeap.hpp b/src/hotspot/share/gc/shared/collectedHeap.hpp index f4f5ce79074..05160912283 100644 --- a/src/hotspot/share/gc/shared/collectedHeap.hpp +++ b/src/hotspot/share/gc/shared/collectedHeap.hpp @@ -246,6 +246,13 @@ protected: // This is the correct place to place such initialization methods. virtual void post_initialize(); + bool is_shutting_down() const; + + // If the VM is shutting down, we may have skipped VM_CollectForAllocation. + // In this case, stall the allocation request briefly in the hope that + // the VM shutdown completes before the allocation request returns. + void stall_for_vm_shutdown(); + void before_exit(); // Stop and resume concurrent GC threads interfering with safepoint operations diff --git a/src/hotspot/share/gc/shared/gcVMOperations.cpp b/src/hotspot/share/gc/shared/gcVMOperations.cpp index 97bae4f6d44..36aa0c9843d 100644 --- a/src/hotspot/share/gc/shared/gcVMOperations.cpp +++ b/src/hotspot/share/gc/shared/gcVMOperations.cpp @@ -111,7 +111,7 @@ bool VM_GC_Operation::doit_prologue() { VM_Heap_Sync_Operation::doit_prologue(); // Check invocations - if (skip_operation()) { + if (skip_operation() || Universe::is_shutting_down()) { // skip collection Heap_lock->unlock(); if (should_use_gclocker()) { diff --git a/src/hotspot/share/memory/universe.cpp b/src/hotspot/share/memory/universe.cpp index 9251fecbb6c..a3afcc5ba64 100644 --- a/src/hotspot/share/memory/universe.cpp +++ b/src/hotspot/share/memory/universe.cpp @@ -183,6 +183,7 @@ int Universe::_base_vtable_size = 0; bool Universe::_bootstrapping = false; bool Universe::_module_initialized = false; bool Universe::_fully_initialized = false; +volatile bool Universe::_is_shutting_down = false; OopStorage* Universe::_vm_weak = nullptr; OopStorage* Universe::_vm_global = nullptr; @@ -1344,7 +1345,14 @@ static void log_cpu_time() { } void Universe::before_exit() { - log_cpu_time(); + { + // Acquire the Heap_lock to synchronize with VM_Heap_Sync_Operations, + // which may depend on the value of _is_shutting_down flag. + MutexLocker hl(Heap_lock); + log_cpu_time(); + AtomicAccess::release_store(&_is_shutting_down, true); + } + heap()->before_exit(); // Print GC/heap related information. diff --git a/src/hotspot/share/memory/universe.hpp b/src/hotspot/share/memory/universe.hpp index 90874d2392d..3b1f2523ed8 100644 --- a/src/hotspot/share/memory/universe.hpp +++ b/src/hotspot/share/memory/universe.hpp @@ -128,6 +128,9 @@ class Universe: AllStatic { static bool _module_initialized; // true after call_initPhase2 called static bool _fully_initialized; // true after universe_init and initialize_vtables called + // Shutdown + static volatile bool _is_shutting_down; + // the array of preallocated errors with backtraces static objArrayOop preallocated_out_of_memory_errors(); @@ -324,6 +327,8 @@ class Universe: AllStatic { static bool is_module_initialized() { return _module_initialized; } static bool is_fully_initialized() { return _fully_initialized; } + static bool is_shutting_down() { return AtomicAccess::load_acquire(&_is_shutting_down); } + static bool on_page_boundary(void* addr); static bool should_fill_in_stack_trace(Handle throwable); static void check_alignment(uintx size, uintx alignment, const char* name); diff --git a/src/hotspot/share/services/cpuTimeUsage.cpp b/src/hotspot/share/services/cpuTimeUsage.cpp index d6b01bcbf9a..27b5e90fbaf 100644 --- a/src/hotspot/share/services/cpuTimeUsage.cpp +++ b/src/hotspot/share/services/cpuTimeUsage.cpp @@ -28,6 +28,7 @@ #include "memory/universe.hpp" #include "runtime/globals.hpp" #include "runtime/os.hpp" +#include "runtime/osThread.hpp" #include "runtime/perfData.hpp" #include "runtime/vmThread.hpp" #include "services/cpuTimeUsage.hpp" From 2e99ed64223e48f4173f00ce56d28473dba31a83 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Tue, 23 Sep 2025 09:07:09 +0000 Subject: [PATCH 177/556] 8368080: G1: Unnecessary initialization of G1CMTask's mark stats table Reviewed-by: iwalulya, shade --- src/hotspot/share/gc/g1/g1FullGCMarker.cpp | 1 + src/hotspot/share/gc/g1/g1RegionMarkStatsCache.cpp | 1 - src/hotspot/share/gc/g1/g1RegionMarkStatsCache.hpp | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hotspot/share/gc/g1/g1FullGCMarker.cpp b/src/hotspot/share/gc/g1/g1FullGCMarker.cpp index f3a8d23cbba..aa8f12a2d1b 100644 --- a/src/hotspot/share/gc/g1/g1FullGCMarker.cpp +++ b/src/hotspot/share/gc/g1/g1FullGCMarker.cpp @@ -43,6 +43,7 @@ G1FullGCMarker::G1FullGCMarker(G1FullCollector* collector, _cld_closure(mark_closure(), ClassLoaderData::_claim_stw_fullgc_mark), _mark_stats_cache(mark_stats, G1RegionMarkStatsCache::RegionMarkStatsCacheSize) { ClassLoaderDataGraph::verify_claimed_marks_cleared(ClassLoaderData::_claim_stw_fullgc_mark); + _mark_stats_cache.reset(); } G1FullGCMarker::~G1FullGCMarker() { diff --git a/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.cpp b/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.cpp index eca0f78aed9..d9b7ec294bd 100644 --- a/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.cpp +++ b/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.cpp @@ -35,7 +35,6 @@ G1RegionMarkStatsCache::G1RegionMarkStatsCache(G1RegionMarkStats* target, uint n guarantee(is_power_of_2(num_cache_entries), "Number of cache entries must be power of two, but is %u", num_cache_entries); _cache = NEW_C_HEAP_ARRAY(G1RegionMarkStatsCacheEntry, _num_cache_entries, mtGC); - reset(); } G1RegionMarkStatsCache::~G1RegionMarkStatsCache() { diff --git a/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.hpp b/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.hpp index f94285b06e9..61253c94ddd 100644 --- a/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.hpp +++ b/src/hotspot/share/gc/g1/g1RegionMarkStatsCache.hpp @@ -103,6 +103,8 @@ public: // to have a very low cache miss rate. static const uint RegionMarkStatsCacheSize = 1024; + // Initialize cache. Does not reset the cache immediately to avoid the cost + // during startup. G1RegionMarkStatsCache(G1RegionMarkStats* target, uint num_cache_entries); ~G1RegionMarkStatsCache(); From d316d3f74fd951613eef3870ee3da2c2dc5b719c Mon Sep 17 00:00:00 2001 From: Adam Sotona Date: Tue, 23 Sep 2025 09:09:46 +0000 Subject: [PATCH 178/556] 8366926: Unexpected exception occurs when executing code in a "local" JShell environment Reviewed-by: liach, jlahoda --- .../execution/LocalExecutionControl.java | 28 ++++++-- .../LocalExecutionInstrumentationCHRTest.java | 64 +++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java diff --git a/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java b/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java index 3db08b80bff..f3c98c561e2 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java @@ -24,6 +24,7 @@ */ package jdk.jshell.execution; +import java.io.ByteArrayInputStream; import java.lang.constant.ClassDesc; import java.lang.constant.ConstantDescs; import java.lang.reflect.Field; @@ -34,6 +35,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassHierarchyResolver; import java.lang.classfile.ClassTransform; import java.lang.classfile.CodeBuilder; import java.lang.classfile.CodeElement; @@ -85,9 +87,7 @@ public class LocalExecutionControl extends DirectExecutionControl { @Override public void load(ClassBytecodes[] cbcs) throws ClassInstallException, NotImplementedException, EngineTerminationException { - super.load(Stream.of(cbcs) - .map(cbc -> new ClassBytecodes(cbc.name(), instrument(cbc.bytecodes()))) - .toArray(ClassBytecodes[]::new)); + super.load(instrument(cbcs)); } private static final String CANCEL_CLASS = "REPL.$Cancel$"; @@ -95,8 +95,26 @@ public class LocalExecutionControl extends DirectExecutionControl { private static final String STOP_CHECK = "stopCheck"; private static final ClassDesc CD_ThreadDeath = ClassDesc.of("java.lang.ThreadDeath"); - private static byte[] instrument(byte[] classFile) { - var cc = ClassFile.of(); + private static ClassBytecodes[] instrument(ClassBytecodes[] cbcs) { + var cc = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of( + ClassHierarchyResolver.defaultResolver().orElse( + ClassHierarchyResolver.ofResourceParsing(cd -> { + String cName = cd.descriptorString(); + cName = cName.substring(1, cName.length() - 1).replace('/', '.'); + for (ClassBytecodes cbc : cbcs) { + if (cName.equals(cbc.name())) { + return new ByteArrayInputStream(cbc.bytecodes()); + } + } + return null; + })))); + + return Stream.of(cbcs) + .map(cbc -> new ClassBytecodes(cbc.name(), instrument(cc, cbc.bytecodes()))) + .toArray(ClassBytecodes[]::new); + } + + private static byte[] instrument(ClassFile cc, byte[] classFile) { return cc.transformClass(cc.parse(classFile), ClassTransform.transformingMethodBodies( CodeTransform.ofStateful(StopCheckWeaver::new))); diff --git a/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java b/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java new file mode 100644 index 00000000000..682c1814c66 --- /dev/null +++ b/test/langtools/jdk/jshell/LocalExecutionInstrumentationCHRTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8366926 + * @summary Verify the instrumenation class hierarchy resolution works properly in local execution mode + * @library /tools/lib + * @modules + * jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * @build KullaTesting + * @run junit/othervm LocalExecutionInstrumentationCHRTest + */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class LocalExecutionInstrumentationCHRTest extends ReplToolTesting { + + @Test + public void verifyMyClassFoundOnClassPath() { + test(new String[] { "--execution", "local" }, + a -> assertCommand(a, "public interface TestInterface {}", "| created interface TestInterface"), + a -> assertCommand(a, + "public class TestClass {" + + "public TestInterface foo(boolean b) {" + + "TestInterface test; " + + "if (b) {" + + "test = new TestInterfaceImpl1();" + + "} else {" + + "test = new TestInterfaceImpl2();" + + "}" + + "return test;" + + "}" + + "private class TestInterfaceImpl1 implements TestInterface {}" + + "private class TestInterfaceImpl2 implements TestInterface {}" + + "}", "| created class TestClass"), + a -> assertCommand(a, "new TestClass().foo(true).getClass();", "$3 ==> class TestClass$TestInterfaceImpl1"), + a -> assertCommand(a, "new TestClass().foo(false).getClass();", "$4 ==> class TestClass$TestInterfaceImpl2") + ); + } +} From b48f51932fb4c83f9ff102b286fb65e9a0e12de0 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Tue, 23 Sep 2025 09:40:28 +0000 Subject: [PATCH 179/556] 8368345: Remove leftover includes of strongRootsScope.hpp Reviewed-by: stefank, tschatzl, ayang --- src/hotspot/share/gc/g1/g1ConcurrentMark.cpp | 1 - src/hotspot/share/gc/parallel/psScavenge.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp index 475917f91c7..d37fe9ea7ba 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMark.cpp @@ -52,7 +52,6 @@ #include "gc/shared/gcTraceTime.inline.hpp" #include "gc/shared/gcVMOperations.hpp" #include "gc/shared/referencePolicy.hpp" -#include "gc/shared/strongRootsScope.hpp" #include "gc/shared/suspendibleThreadSet.hpp" #include "gc/shared/taskqueue.inline.hpp" #include "gc/shared/taskTerminator.hpp" diff --git a/src/hotspot/share/gc/parallel/psScavenge.cpp b/src/hotspot/share/gc/parallel/psScavenge.cpp index 0af2ab1fd68..f633f40ef7f 100644 --- a/src/hotspot/share/gc/parallel/psScavenge.cpp +++ b/src/hotspot/share/gc/parallel/psScavenge.cpp @@ -51,7 +51,6 @@ #include "gc/shared/referenceProcessorPhaseTimes.hpp" #include "gc/shared/scavengableNMethods.hpp" #include "gc/shared/spaceDecorator.hpp" -#include "gc/shared/strongRootsScope.hpp" #include "gc/shared/taskTerminator.hpp" #include "gc/shared/weakProcessor.inline.hpp" #include "gc/shared/workerPolicy.hpp" From 29908148f819281dc6d1ef1274ca4d67a47754c0 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Tue, 23 Sep 2025 09:42:56 +0000 Subject: [PATCH 180/556] 8367598: Switch to CRC32C for SEED calculation in jdk.test.lib.Utils Reviewed-by: weijun --- test/lib/jdk/test/lib/Utils.java | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/test/lib/jdk/test/lib/Utils.java b/test/lib/jdk/test/lib/Utils.java index c4a42dc61ba..2f46ed87340 100644 --- a/test/lib/jdk/test/lib/Utils.java +++ b/test/lib/jdk/test/lib/Utils.java @@ -38,7 +38,6 @@ import java.net.ServerSocket; import java.net.URL; import java.net.URLClassLoader; import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.CopyOption; import java.nio.file.Files; @@ -50,8 +49,6 @@ import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.FileAttribute; import java.nio.channels.SocketChannel; import java.nio.file.attribute.PosixFilePermissions; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -71,6 +68,7 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.zip.CRC32C; import static java.lang.System.lineSeparator; import static jdk.test.lib.Asserts.assertTrue; @@ -170,16 +168,11 @@ public final class Utils { var v = Runtime.version(); // promotable builds have build number, and it's greater than 0 if (v.build().orElse(0) > 0) { - // promotable build -> use 1st 8 bytes of md5($version) - try { - var md = MessageDigest.getInstance("MD5"); - var bytes = v.toString() - .getBytes(StandardCharsets.UTF_8); - bytes = md.digest(bytes); - SEED = ByteBuffer.wrap(bytes).getLong(); - } catch (NoSuchAlgorithmException e) { - throw new Error(e); - } + // promotable build -> generate a seed based on the version string + var bytes = v.toString().getBytes(StandardCharsets.UTF_8); + var crc = new CRC32C(); + crc.update(bytes); + SEED = crc.getValue(); } else { // "personal" build -> use random seed SEED = new Random().nextLong(); From e122f4dd0d00b6b7d95e5af118af72db2dfdcc85 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 23 Sep 2025 09:46:56 +0000 Subject: [PATCH 181/556] 8368024: Remove StringConcatFactory#generateMHInlineCopy Reviewed-by: redestad --- .../share/classes/java/lang/String.java | 2 +- .../classes/java/lang/StringConcatHelper.java | 306 +----------- .../share/classes/java/lang/System.java | 12 - .../java/lang/invoke/StringConcatFactory.java | 450 ------------------ .../jdk/internal/access/JavaLangAccess.java | 15 - 5 files changed, 3 insertions(+), 782 deletions(-) diff --git a/src/java.base/share/classes/java/lang/String.java b/src/java.base/share/classes/java/lang/String.java index 24ead22e283..a18ac3250dc 100644 --- a/src/java.base/share/classes/java/lang/String.java +++ b/src/java.base/share/classes/java/lang/String.java @@ -3710,7 +3710,7 @@ public final class String if (len < 0L || (len <<= coder) != (int) len) { throw new OutOfMemoryError("Requested string length exceeds VM limit"); } - byte[] value = StringConcatHelper.newArray(len); + byte[] value = StringConcatHelper.newArray((int) len); int off = 0; prefix.getBytes(value, off, coder); off += prefix.length(); diff --git a/src/java.base/share/classes/java/lang/StringConcatHelper.java b/src/java.base/share/classes/java/lang/StringConcatHelper.java index a5f6abbfdf1..aeff494cabb 100644 --- a/src/java.base/share/classes/java/lang/StringConcatHelper.java +++ b/src/java.base/share/classes/java/lang/StringConcatHelper.java @@ -141,262 +141,6 @@ final class StringConcatHelper { // no instantiation } - /** - * Return the coder for the character. - * @param value character - * @return coder - */ - static long coder(char value) { - return StringLatin1.canEncode(value) ? LATIN1 : UTF16; - } - - /** - * Check for overflow, throw exception on overflow. - * - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @return the given parameter value, if valid - */ - private static long checkOverflow(long lengthCoder) { - if ((int)lengthCoder >= 0) { - return lengthCoder; - } - throw new OutOfMemoryError("Overflow: String length out of range"); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, boolean value) { - return checkOverflow(lengthCoder + (value ? 4 : 5)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, char value) { - return checkOverflow(lengthCoder + 1) | coder(value); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, int value) { - return checkOverflow(lengthCoder + DecimalDigits.stringSize(value)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, long value) { - return checkOverflow(lengthCoder + DecimalDigits.stringSize(value)); - } - - /** - * Mix value length and coder into current length and coder. - * @param lengthCoder String length with coder packed into higher bits - * the upper word. - * @param value value to mix in - * @return new length and coder - */ - static long mix(long lengthCoder, String value) { - lengthCoder += value.length(); - if (!value.isLatin1()) { - lengthCoder |= UTF16; - } - return checkOverflow(lengthCoder); - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value boolean value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, boolean value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - if (value) { - index -= 4; - buf[index] = 't'; - buf[index + 1] = 'r'; - buf[index + 2] = 'u'; - buf[index + 3] = 'e'; - } else { - index -= 5; - buf[index] = 'f'; - buf[index + 1] = 'a'; - buf[index + 2] = 'l'; - buf[index + 3] = 's'; - buf[index + 4] = 'e'; - } - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - if (value) { - index -= 4; - StringUTF16.putChar(buf, index, 't'); - StringUTF16.putChar(buf, index + 1, 'r'); - StringUTF16.putChar(buf, index + 2, 'u'); - StringUTF16.putChar(buf, index + 3, 'e'); - } else { - index -= 5; - StringUTF16.putChar(buf, index, 'f'); - StringUTF16.putChar(buf, index + 1, 'a'); - StringUTF16.putChar(buf, index + 2, 'l'); - StringUTF16.putChar(buf, index + 3, 's'); - StringUTF16.putChar(buf, index + 4, 'e'); - } - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value char value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, char value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - buf[--index] = (byte) (value & 0xFF); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - StringUTF16.putChar(buf, --index, value); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value int value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, int value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - index = DecimalDigits.uncheckedGetCharsLatin1(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - index = DecimalDigits.uncheckedGetCharsUTF16(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value long value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, long value, String prefix) { - int index = (int)indexCoder; - if (indexCoder < UTF16) { - index = DecimalDigits.uncheckedGetCharsLatin1(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - index = DecimalDigits.uncheckedGetCharsUTF16(value, index, buf); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Prepends constant and the stringly representation of value into buffer, - * given the coder and final index. Index is measured in chars, not in bytes! - * - * @param indexCoder final char index in the buffer, along with coder packed - * into higher bits. - * @param buf buffer to append to - * @param value boolean value to encode - * @param prefix a constant to prepend before value - * @return updated index (coder value retained) - */ - static long prepend(long indexCoder, byte[] buf, String value, String prefix) { - int index = ((int)indexCoder) - value.length(); - if (indexCoder < UTF16) { - value.getBytes(buf, index, String.LATIN1); - index -= prefix.length(); - prefix.getBytes(buf, index, String.LATIN1); - return index; - } else { - value.getBytes(buf, index, String.UTF16); - index -= prefix.length(); - prefix.getBytes(buf, index, String.UTF16); - return index | UTF16; - } - } - - /** - * Instantiates the String with given buffer and coder - * @param buf buffer to use - * @param indexCoder remaining index (should be zero) and coder - * @return String resulting string - */ - static String newString(byte[] buf, long indexCoder) { - // Use the private, non-copying constructor (unsafe!) - if (indexCoder == LATIN1) { - return new String(buf, String.LATIN1); - } else if (indexCoder == UTF16) { - return new String(buf, String.UTF16); - } else { - throw new InternalError("Storage is not completely initialized, " + - (int)indexCoder + " bytes left"); - } - } - /** * Perform a simple concatenation between two objects. Added for startup * performance, but also demonstrates the code that would be emitted by @@ -466,10 +210,6 @@ final class StringConcatHelper { return (value == null || (s = value.toString()) == null) ? "null" : s; } - private static final long LATIN1 = (long)String.LATIN1 << 32; - - private static final long UTF16 = (long)String.UTF16 << 32; - private static final Unsafe UNSAFE = Unsafe.getUnsafe(); static String stringOf(float value) { @@ -530,41 +270,6 @@ final class StringConcatHelper { return checkOverflow(length + value.length()); } - /** - * Allocates an uninitialized byte array based on the length and coder - * information, then prepends the given suffix string at the end of the - * byte array before returning it. The calling code must adjust the - * indexCoder so that it's taken the coder of the suffix into account, but - * subtracted the length of the suffix. - * - * @param suffix - * @param indexCoder - * @return the newly allocated byte array - */ - @ForceInline - static byte[] newArrayWithSuffix(String suffix, long indexCoder) { - byte[] buf = newArray(indexCoder + suffix.length()); - if (indexCoder < UTF16) { - suffix.getBytes(buf, (int)indexCoder, String.LATIN1); - } else { - suffix.getBytes(buf, (int)indexCoder, String.UTF16); - } - return buf; - } - - /** - * Allocates an uninitialized byte array based on the length and coder information - * in indexCoder - * @param indexCoder - * @return the newly allocated byte array - */ - @ForceInline - static byte[] newArray(long indexCoder) { - byte coder = (byte)(indexCoder >> 32); - int index = ((int)indexCoder) << coder; - return newArray(index); - } - /** * Allocates an uninitialized byte array based on the length * @param length @@ -578,14 +283,6 @@ final class StringConcatHelper { return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length); } - /** - * Provides the initial coder for the String. - * @return initial coder, adjusted into the upper half - */ - static long initialCoder() { - return String.COMPACT_STRINGS ? LATIN1 : UTF16; - } - static MethodHandle lookupStatic(String name, MethodType methodType) { try { return MethodHandles.lookup() @@ -603,7 +300,8 @@ final class StringConcatHelper { * subtracted the length of the suffix. * * @param suffix - * @param indexCoder + * @param index final char index in the buffer + * @param coder coder of the buffer * @return the newly allocated byte array */ @ForceInline diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index bb1775fbc6b..c88cf4ac797 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -2185,18 +2185,6 @@ public final class System { return StringConcatHelper.lookupStatic(name, methodType); } - public long stringConcatInitialCoder() { - return StringConcatHelper.initialCoder(); - } - - public long stringConcatMix(long lengthCoder, String constant) { - return StringConcatHelper.mix(lengthCoder, constant); - } - - public long stringConcatMix(long lengthCoder, char value) { - return StringConcatHelper.mix(lengthCoder, value); - } - public Object uncheckedStringConcat1(String[] constants) { return new StringConcatHelper.Concat1(constants); } diff --git a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java index 1c7995c4ec7..733714b5786 100644 --- a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java +++ b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java @@ -37,7 +37,6 @@ import jdk.internal.util.ReferenceKey; import jdk.internal.util.ReferencedKeyMap; import jdk.internal.vm.annotation.AOTSafeClassInitializer; import jdk.internal.vm.annotation.Stable; -import sun.invoke.util.Wrapper; import java.lang.classfile.Annotation; import java.lang.classfile.ClassBuilder; @@ -119,14 +118,10 @@ import static java.lang.invoke.MethodType.methodType; */ @AOTSafeClassInitializer public final class StringConcatFactory { - private static final int HIGH_ARITY_THRESHOLD; private static final int CACHE_THRESHOLD; private static final int FORCE_INLINE_THRESHOLD; static { - String highArity = VM.getSavedProperty("java.lang.invoke.StringConcat.highArityThreshold"); - HIGH_ARITY_THRESHOLD = highArity != null ? Integer.parseInt(highArity) : 0; - String cacheThreshold = VM.getSavedProperty("java.lang.invoke.StringConcat.cacheThreshold"); CACHE_THRESHOLD = cacheThreshold != null ? Integer.parseInt(cacheThreshold) : 256; @@ -391,9 +386,6 @@ public final class StringConcatFactory { try { MethodHandle mh = makeSimpleConcat(concatType, constantStrings); - if (mh == null && concatType.parameterCount() <= HIGH_ARITY_THRESHOLD) { - mh = generateMHInlineCopy(concatType, constantStrings); - } if (mh == null) { mh = InlineHiddenClassStrategy.generate(lookup, concatType, constantStrings); @@ -518,385 +510,6 @@ public final class StringConcatFactory { return null; } - /** - *

        This strategy replicates what StringBuilders are doing: it builds the - * byte[] array on its own and passes that byte[] array to String - * constructor. This strategy requires access to some private APIs in JDK, - * most notably, the private String constructor that accepts byte[] arrays - * without copying. - */ - private static MethodHandle generateMHInlineCopy(MethodType mt, String[] constants) { - int paramCount = mt.parameterCount(); - String suffix = constants[paramCount]; - - - // else... fall-through to slow-path - - // Create filters and obtain filtered parameter types. Filters would be used in the beginning - // to convert the incoming arguments into the arguments we can process (e.g. Objects -> Strings). - // The filtered argument type list is used all over in the combinators below. - - Class[] ptypes = mt.erase().parameterArray(); - MethodHandle[] objFilters = null; - MethodHandle[] floatFilters = null; - MethodHandle[] doubleFilters = null; - for (int i = 0; i < ptypes.length; i++) { - Class cl = ptypes[i]; - // Use int as the logical type for subword integral types - // (byte and short). char and boolean require special - // handling so don't change the logical type of those - ptypes[i] = promoteToIntType(ptypes[i]); - // Object, float and double will be eagerly transformed - // into a (non-null) String as a first step after invocation. - // Set up to use String as the logical type for such arguments - // internally. - if (cl == Object.class) { - if (objFilters == null) { - objFilters = new MethodHandle[ptypes.length]; - } - objFilters[i] = objectStringifier(); - ptypes[i] = String.class; - } else if (cl == float.class) { - if (floatFilters == null) { - floatFilters = new MethodHandle[ptypes.length]; - } - floatFilters[i] = floatStringifier(); - ptypes[i] = String.class; - } else if (cl == double.class) { - if (doubleFilters == null) { - doubleFilters = new MethodHandle[ptypes.length]; - } - doubleFilters[i] = doubleStringifier(); - ptypes[i] = String.class; - } - } - - // Start building the combinator tree. The tree "starts" with ()String, and "finishes" - // with the (byte[], long)String shape to invoke newString in StringConcatHelper. The combinators are - // assembled bottom-up, which makes the code arguably hard to read. - - // Drop all remaining parameter types, leave only helper arguments: - MethodHandle mh = MethodHandles.dropArgumentsTrusted(newString(), 2, ptypes); - - // Calculate the initialLengthCoder value by looking at all constant values and summing up - // their lengths and adjusting the encoded coder bit if needed - long initialLengthCoder = INITIAL_CODER; - - for (String constant : constants) { - if (constant != null) { - initialLengthCoder = JLA.stringConcatMix(initialLengthCoder, constant); - } - } - - // Mix in prependers. This happens when (byte[], long) = (storage, indexCoder) is already - // known from the combinators below. We are assembling the string backwards, so the index coded - // into indexCoder is the *ending* index. - mh = filterInPrependers(mh, constants, ptypes); - - // Fold in byte[] instantiation at argument 0 - MethodHandle newArrayCombinator; - if (suffix == null || suffix.isEmpty()) { - suffix = ""; - } - // newArray variant that deals with prepending any trailing constant - // - // initialLengthCoder is adjusted to have the correct coder - // and length: The newArrayWithSuffix method expects only the coder of the - // suffix to be encoded into indexCoder - initialLengthCoder -= suffix.length(); - newArrayCombinator = newArrayWithSuffix(suffix); - - mh = MethodHandles.foldArgumentsWithCombiner(mh, 0, newArrayCombinator, - 1 // index - ); - - // Start combining length and coder mixers. - // - // Length is easy: constant lengths can be computed on the spot, and all non-constant - // shapes have been either converted to Strings, or explicit methods for getting the - // string length out of primitives are provided. - // - // Coders are more interesting. Only Object, String and char arguments (and constants) - // can have non-Latin1 encoding. It is easier to blindly convert constants to String, - // and deduce the coder from there. Arguments would be either converted to Strings - // during the initial filtering, or handled by specializations in MIXERS. - // - // The method handle shape before all mixers are combined in is: - // (long, )String = ("indexCoder", ) - // - // We will bind the initialLengthCoder value to the last mixer (the one that will be - // executed first), then fold that in. This leaves the shape after all mixers are - // combined in as: - // ()String = () - - mh = filterAndFoldInMixers(mh, initialLengthCoder, ptypes); - - // The method handle shape here is (). - - // Apply filters, converting the arguments: - if (objFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, objFilters); - } - if (floatFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, floatFilters); - } - if (doubleFilters != null) { - mh = MethodHandles.filterArguments(mh, 0, doubleFilters); - } - - return mh; - } - - // We need one prepender per argument, but also need to fold in constants. We do so by greedily - // creating prependers that fold in surrounding constants into the argument prepender. This reduces - // the number of unique MH combinator tree shapes we'll create in an application. - // Additionally we do this in chunks to reduce the number of combinators bound to the root tree, - // which simplifies the shape and makes construction of similar trees use less unique LF classes - private static MethodHandle filterInPrependers(MethodHandle mh, String[] constants, Class[] ptypes) { - int pos; - int[] argPositions = null; - MethodHandle prepend; - for (pos = 0; pos < ptypes.length - 3; pos += 4) { - prepend = prepender(pos, constants, ptypes, 4); - argPositions = filterPrependArgPositions(argPositions, pos, 4); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 1, prepend, argPositions); - } - if (pos < ptypes.length) { - int count = ptypes.length - pos; - prepend = prepender(pos, constants, ptypes, count); - argPositions = filterPrependArgPositions(argPositions, pos, count); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 1, prepend, argPositions); - } - return mh; - } - - static int[] filterPrependArgPositions(int[] argPositions, int pos, int count) { - if (argPositions == null || argPositions.length != count + 2) { - argPositions = new int[count + 2]; - argPositions[0] = 1; // indexCoder - argPositions[1] = 0; // storage - } - int limit = count + 2; - for (int i = 2; i < limit; i++) { - argPositions[i] = i + pos; - } - return argPositions; - } - - - // We need one mixer per argument. - private static MethodHandle filterAndFoldInMixers(MethodHandle mh, long initialLengthCoder, Class[] ptypes) { - int pos; - int[] argPositions = null; - for (pos = 0; pos < ptypes.length - 4; pos += 4) { - // Compute new "index" in-place pairwise using old value plus the appropriate arguments. - MethodHandle mix = mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - argPositions = filterMixerArgPositions(argPositions, pos, 4); - mh = MethodHandles.filterArgumentsWithCombiner(mh, 0, - mix, argPositions); - } - - if (pos < ptypes.length) { - // Mix in the last 1 to 4 parameters, insert the initialLengthCoder into the final mixer and - // fold the result into the main combinator - mh = foldInLastMixers(mh, initialLengthCoder, pos, ptypes, ptypes.length - pos); - } else if (ptypes.length == 0) { - // No mixer (constants only concat), insert initialLengthCoder directly - mh = MethodHandles.insertArguments(mh, 0, initialLengthCoder); - } - return mh; - } - - static int[] filterMixerArgPositions(int[] argPositions, int pos, int count) { - if (argPositions == null || argPositions.length != count + 2) { - argPositions = new int[count + 1]; - argPositions[0] = 0; // indexCoder - } - int limit = count + 1; - for (int i = 1; i < limit; i++) { - argPositions[i] = i + pos; - } - return argPositions; - } - - private static MethodHandle foldInLastMixers(MethodHandle mh, long initialLengthCoder, int pos, Class[] ptypes, int count) { - MethodHandle mix = switch (count) { - case 1 -> mixer(ptypes[pos]); - case 2 -> mixer(ptypes[pos], ptypes[pos + 1]); - case 3 -> mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2]); - case 4 -> mixer(ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - default -> throw new IllegalArgumentException("Unexpected count: " + count); - }; - mix = MethodHandles.insertArguments(mix,0, initialLengthCoder); - // apply selected arguments on the 1-4 arg mixer and fold in the result - return switch (count) { - case 1 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos); - case 2 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos); - case 3 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos, 3 + pos); - case 4 -> MethodHandles.foldArgumentsWithCombiner(mh, 0, mix, - 1 + pos, 2 + pos, 3 + pos, 4 + pos); - default -> throw new IllegalArgumentException(); - }; - } - - // Simple prependers, single argument. May be used directly or as a - // building block for complex prepender combinators. - private static MethodHandle prepender(String prefix, Class cl) { - if (prefix == null || prefix.isEmpty()) { - return noPrefixPrepender(cl); - } else { - return MethodHandles.insertArguments( - prepender(cl), 3, prefix); - } - } - - private static MethodHandle prepender(Class cl) { - int idx = classIndex(cl); - MethodHandle prepend = PREPENDERS[idx]; - if (prepend == null) { - PREPENDERS[idx] = prepend = JLA.stringConcatHelper("prepend", - methodType(long.class, long.class, byte[].class, - Wrapper.asPrimitiveType(cl), String.class)).rebind(); - } - return prepend; - } - - private static MethodHandle noPrefixPrepender(Class cl) { - int idx = classIndex(cl); - MethodHandle prepend = NO_PREFIX_PREPENDERS[idx]; - if (prepend == null) { - NO_PREFIX_PREPENDERS[idx] = prepend = MethodHandles.insertArguments(prepender(cl), 3, ""); - } - return prepend; - } - - private static final int INT_IDX = 0, - CHAR_IDX = 1, - LONG_IDX = 2, - BOOLEAN_IDX = 3, - STRING_IDX = 4, - TYPE_COUNT = 5; - private static int classIndex(Class cl) { - if (cl == String.class) return STRING_IDX; - if (cl == int.class) return INT_IDX; - if (cl == boolean.class) return BOOLEAN_IDX; - if (cl == char.class) return CHAR_IDX; - if (cl == long.class) return LONG_IDX; - throw new IllegalArgumentException("Unexpected class: " + cl); - } - - // Constant argument lists used by the prepender MH builders - private static final int[] PREPEND_FILTER_FIRST_ARGS = new int[] { 0, 1, 2 }; - private static final int[] PREPEND_FILTER_SECOND_ARGS = new int[] { 0, 1, 3 }; - private static final int[] PREPEND_FILTER_THIRD_ARGS = new int[] { 0, 1, 4 }; - private static final int[] PREPEND_FILTER_FIRST_PAIR_ARGS = new int[] { 0, 1, 2, 3 }; - private static final int[] PREPEND_FILTER_SECOND_PAIR_ARGS = new int[] { 0, 1, 4, 5 }; - - // Base MH for complex prepender combinators. - private static @Stable MethodHandle PREPEND_BASE; - private static MethodHandle prependBase() { - MethodHandle base = PREPEND_BASE; - if (base == null) { - base = PREPEND_BASE = MethodHandles.dropArguments( - MethodHandles.identity(long.class), 1, byte[].class); - } - return base; - } - - private static final @Stable MethodHandle[][] DOUBLE_PREPENDERS = new MethodHandle[TYPE_COUNT][TYPE_COUNT]; - - private static MethodHandle prepender(String prefix, Class cl, String prefix2, Class cl2) { - int idx1 = classIndex(cl); - int idx2 = classIndex(cl2); - MethodHandle prepend = DOUBLE_PREPENDERS[idx1][idx2]; - if (prepend == null) { - prepend = DOUBLE_PREPENDERS[idx1][idx2] = - MethodHandles.dropArguments(prependBase(), 2, cl, cl2); - } - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, prepender(prefix, cl), - PREPEND_FILTER_FIRST_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, prepender(prefix2, cl2), - PREPEND_FILTER_SECOND_ARGS); - } - - private static MethodHandle prepender(int pos, String[] constants, Class[] ptypes, int count) { - // build the simple cases directly - if (count == 1) { - return prepender(constants[pos], ptypes[pos]); - } - if (count == 2) { - return prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]); - } - // build a tree from an unbound prepender, allowing us to bind the constants in a batch as a final step - MethodHandle prepend = prependBase(); - if (count == 3) { - prepend = MethodHandles.dropArguments(prepend, 2, - ptypes[pos], ptypes[pos + 1], ptypes[pos + 2]); - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]), - PREPEND_FILTER_FIRST_PAIR_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos + 2], ptypes[pos + 2]), - PREPEND_FILTER_THIRD_ARGS); - } else if (count == 4) { - prepend = MethodHandles.dropArguments(prepend, 2, - ptypes[pos], ptypes[pos + 1], ptypes[pos + 2], ptypes[pos + 3]); - prepend = MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos], ptypes[pos], constants[pos + 1], ptypes[pos + 1]), - PREPEND_FILTER_FIRST_PAIR_ARGS); - return MethodHandles.filterArgumentsWithCombiner(prepend, 0, - prepender(constants[pos + 2], ptypes[pos + 2], constants[pos + 3], ptypes[pos + 3]), - PREPEND_FILTER_SECOND_PAIR_ARGS); - } else { - throw new IllegalArgumentException("Unexpected count: " + count); - } - } - - // Constant argument lists used by the mixer MH builders - private static final int[] MIX_FILTER_SECOND_ARGS = new int[] { 0, 2 }; - private static final int[] MIX_FILTER_THIRD_ARGS = new int[] { 0, 3 }; - private static final int[] MIX_FILTER_SECOND_PAIR_ARGS = new int[] { 0, 3, 4 }; - private static MethodHandle mixer(Class cl) { - int index = classIndex(cl); - MethodHandle mix = MIXERS[index]; - if (mix == null) { - MIXERS[index] = mix = JLA.stringConcatHelper("mix", - methodType(long.class, long.class, Wrapper.asPrimitiveType(cl))).rebind(); - } - return mix; - } - - private static final @Stable MethodHandle[][] DOUBLE_MIXERS = new MethodHandle[TYPE_COUNT][TYPE_COUNT]; - private static MethodHandle mixer(Class cl, Class cl2) { - int idx1 = classIndex(cl); - int idx2 = classIndex(cl2); - MethodHandle mix = DOUBLE_MIXERS[idx1][idx2]; - if (mix == null) { - mix = mixer(cl); - mix = MethodHandles.dropArguments(mix, 2, cl2); - DOUBLE_MIXERS[idx1][idx2] = mix = MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl2), MIX_FILTER_SECOND_ARGS); - } - return mix; - } - - private static MethodHandle mixer(Class cl, Class cl2, Class cl3) { - MethodHandle mix = mixer(cl, cl2); - mix = MethodHandles.dropArguments(mix, 3, cl3); - return MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl3), MIX_FILTER_THIRD_ARGS); - } - - private static MethodHandle mixer(Class cl, Class cl2, Class cl3, Class cl4) { - MethodHandle mix = mixer(cl, cl2); - mix = MethodHandles.dropArguments(mix, 3, cl3, cl4); - return MethodHandles.filterArgumentsWithCombiner(mix, 0, - mixer(cl3, cl4), MIX_FILTER_SECOND_PAIR_ARGS); - } - private @Stable static MethodHandle SIMPLE_CONCAT; private static MethodHandle simpleConcat() { MethodHandle mh = SIMPLE_CONCAT; @@ -908,42 +521,11 @@ public final class StringConcatFactory { return mh; } - private @Stable static MethodHandle NEW_STRING; - private static MethodHandle newString() { - MethodHandle mh = NEW_STRING; - if (mh == null) { - MethodHandle newString = JLA.stringConcatHelper("newString", - methodType(String.class, byte[].class, long.class)); - NEW_STRING = mh = newString.rebind(); - } - return mh; - } - - private @Stable static MethodHandle NEW_ARRAY_SUFFIX; - private static MethodHandle newArrayWithSuffix(String suffix) { - MethodHandle mh = NEW_ARRAY_SUFFIX; - if (mh == null) { - MethodHandle newArrayWithSuffix = JLA.stringConcatHelper("newArrayWithSuffix", - methodType(byte[].class, String.class, long.class)); - NEW_ARRAY_SUFFIX = mh = newArrayWithSuffix.rebind(); - } - return MethodHandles.insertArguments(mh, 0, suffix); - } - /** * Public gateways to public "stringify" methods. These methods have the * form String apply(T obj), and normally delegate to {@code String.valueOf}, * depending on argument's type. */ - private @Stable static MethodHandle OBJECT_STRINGIFIER; - private static MethodHandle objectStringifier() { - MethodHandle mh = OBJECT_STRINGIFIER; - if (mh == null) { - OBJECT_STRINGIFIER = mh = JLA.stringConcatHelper("stringOf", - methodType(String.class, Object.class)); - } - return mh; - } private @Stable static MethodHandle FLOAT_STRINGIFIER; private static MethodHandle floatStringifier() { MethodHandle mh = FLOAT_STRINGIFIER; @@ -1027,38 +609,6 @@ public final class StringConcatFactory { } } - private static final @Stable MethodHandle[] NO_PREFIX_PREPENDERS = new MethodHandle[TYPE_COUNT]; - private static final @Stable MethodHandle[] PREPENDERS = new MethodHandle[TYPE_COUNT]; - private static final @Stable MethodHandle[] MIXERS = new MethodHandle[TYPE_COUNT]; - private static final long INITIAL_CODER = JLA.stringConcatInitialCoder(); - - /** - * Promote integral types to int. - */ - private static Class promoteToIntType(Class t) { - // use int for subword integral types; still need special mixers - // and prependers for char, boolean - return t == byte.class || t == short.class ? int.class : t; - } - - /** - * Returns a stringifier for references and floats/doubles only. - * Always returns null for other primitives. - * - * @param t class to stringify - * @return stringifier; null, if not available - */ - private static MethodHandle stringifierFor(Class t) { - if (t == Object.class) { - return objectStringifier(); - } else if (t == float.class) { - return floatStringifier(); - } else if (t == double.class) { - return doubleStringifier(); - } - return null; - } - private static MethodHandle stringValueOf(Class ptype) { try { return MethodHandles.publicLookup() diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java index e529e8ba350..fa6e5b4aac3 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java @@ -452,21 +452,6 @@ public interface JavaLangAccess { */ MethodHandle stringConcatHelper(String name, MethodType methodType); - /** - * Get the string concat initial coder - */ - long stringConcatInitialCoder(); - - /** - * Update lengthCoder for constant - */ - long stringConcatMix(long lengthCoder, String constant); - - /** - * Mix value length and coder into current length and coder. - */ - long stringConcatMix(long lengthCoder, char value); - /** * Creates helper for string concatenation. *

        From dbf787c6b78669c69402d2a57d1ec462035d54c4 Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Tue, 23 Sep 2025 11:42:20 +0000 Subject: [PATCH 182/556] 8368326: Don't export unresolved make variables from configure Reviewed-by: erikj --- make/autoconf/boot-jdk.m4 | 40 ++++++--------------------------- make/autoconf/spec.gmk.template | 23 ++++++++++++++----- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/make/autoconf/boot-jdk.m4 b/make/autoconf/boot-jdk.m4 index 4ba1f9f2089..1dd768b2ae1 100644 --- a/make/autoconf/boot-jdk.m4 +++ b/make/autoconf/boot-jdk.m4 @@ -597,10 +597,9 @@ AC_DEFUN([BOOTJDK_SETUP_BUILD_JDK], AC_ARG_WITH(build-jdk, [AS_HELP_STRING([--with-build-jdk], [path to JDK of same version as is being built@<:@the newly built JDK@:>@])]) - CREATE_BUILDJDK=false - EXTERNAL_BUILDJDK=false - BUILD_JDK_FOUND="no" + EXTERNAL_BUILDJDK_PATH="" if test "x$with_build_jdk" != "x"; then + BUILD_JDK_FOUND=no BOOTJDK_CHECK_BUILD_JDK([ if test "x$with_build_jdk" != x; then BUILD_JDK=$with_build_jdk @@ -608,40 +607,15 @@ AC_DEFUN([BOOTJDK_SETUP_BUILD_JDK], AC_MSG_NOTICE([Found potential Build JDK using configure arguments]) fi ]) - EXTERNAL_BUILDJDK=true - else - if test "x$COMPILE_TYPE" = "xcross"; then - BUILD_JDK="\$(BUILDJDK_OUTPUTDIR)/jdk" - BUILD_JDK_FOUND=yes - CREATE_BUILDJDK=true + if test "x$BUILD_JDK_FOUND" != "xyes"; then AC_MSG_CHECKING([for Build JDK]) - AC_MSG_RESULT([yes, will build it for the host platform]) - else - BUILD_JDK="\$(JDK_OUTPUTDIR)" - BUILD_JDK_FOUND=yes - AC_MSG_CHECKING([for Build JDK]) - AC_MSG_RESULT([yes, will use output dir]) + AC_MSG_RESULT([no]) + AC_MSG_ERROR([Could not find a suitable Build JDK]) fi + EXTERNAL_BUILDJDK_PATH="$BUILD_JDK" fi - # Since these tools do not yet exist, we cannot use UTIL_FIXUP_EXECUTABLE to - # detect the need of fixpath - JMOD="$BUILD_JDK/bin/jmod" - UTIL_ADD_FIXPATH(JMOD) - JLINK="$BUILD_JDK/bin/jlink" - UTIL_ADD_FIXPATH(JLINK) - AC_SUBST(JMOD) - AC_SUBST(JLINK) - - if test "x$BUILD_JDK_FOUND" != "xyes"; then - AC_MSG_CHECKING([for Build JDK]) - AC_MSG_RESULT([no]) - AC_MSG_ERROR([Could not find a suitable Build JDK]) - fi - - AC_SUBST(CREATE_BUILDJDK) - AC_SUBST(BUILD_JDK) - AC_SUBST(EXTERNAL_BUILDJDK) + AC_SUBST(EXTERNAL_BUILDJDK_PATH) ]) # The docs-reference JDK is used to run javadoc for the docs-reference targets. diff --git a/make/autoconf/spec.gmk.template b/make/autoconf/spec.gmk.template index e2e8b24db6e..0b336721d65 100644 --- a/make/autoconf/spec.gmk.template +++ b/make/autoconf/spec.gmk.template @@ -386,9 +386,22 @@ CAPSTONE_ARCH_AARCH64_NAME := @CAPSTONE_ARCH_AARCH64_NAME@ # it in sync. BOOT_JDK := @BOOT_JDK@ -BUILD_JDK := @BUILD_JDK@ -CREATE_BUILDJDK := @CREATE_BUILDJDK@ -EXTERNAL_BUILDJDK := @EXTERNAL_BUILDJDK@ +EXTERNAL_BUILDJDK_PATH := @EXTERNAL_BUILDJDK_PATH@ + +ifneq ($(EXTERNAL_BUILDJDK_PATH), ) + EXTERNAL_BUILDJDK := true + CREATE_BUILDJDK := false + BUILD_JDK := $(EXTERNAL_BUILDJDK_PATH) +else + EXTERNAL_BUILDJDK := false + ifeq ($(COMPILE_TYPE), cross) + CREATE_BUILDJDK := true + BUILD_JDK := $(BUILDJDK_OUTPUTDIR)/jdk + else + CREATE_BUILDJDK := false + BUILD_JDK := $(JDK_OUTPUTDIR) + endif +endif # Whether the boot jdk jar supports --date=TIMESTAMP BOOT_JDK_JAR_SUPPORTS_DATE := @BOOT_JDK_JAR_SUPPORTS_DATE@ @@ -647,8 +660,8 @@ JAVA_CMD := @JAVA@ JAVAC_CMD := @JAVAC@ JAVADOC_CMD := @JAVADOC@ JAR_CMD := @JAR@ -JLINK_CMD := @JLINK@ -JMOD_CMD := @JMOD@ +JLINK_CMD := @FIXPATH@ $(BUILD_JDK)/bin/jlink +JMOD_CMD := @FIXPATH@ $(BUILD_JDK)/bin/jmod # These variables are meant to be used. They are defined with = instead of := to make # it possible to override only the *_CMD variables. JAVA = $(JAVA_CMD) $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) From 02c78bb47e3a9cc8760dd0d0970bb9855f9909d3 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Tue, 23 Sep 2025 11:46:37 +0000 Subject: [PATCH 183/556] 8367731: G1: Make G1CollectionSet manage the young gen cset group Reviewed-by: iwalulya, ayang --- src/hotspot/share/gc/g1/g1CollectedHeap.cpp | 21 ++----------------- src/hotspot/share/gc/g1/g1CollectedHeap.hpp | 5 ----- src/hotspot/share/gc/g1/g1CollectionSet.cpp | 14 ++++++++++++- src/hotspot/share/gc/g1/g1CollectionSet.hpp | 11 ++++++---- .../share/gc/g1/g1CollectionSet.inline.hpp | 12 +++++++---- .../share/gc/g1/g1CollectionSetCandidates.hpp | 4 ++++ src/hotspot/share/gc/g1/g1RemSet.cpp | 4 ---- src/hotspot/share/gc/g1/g1YoungCollector.cpp | 4 ++-- 8 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index 28330425511..2368b5b4fc4 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -2517,13 +2517,7 @@ void G1CollectedHeap::update_perf_counter_cpu_time() { } void G1CollectedHeap::start_new_collection_set() { - // Clear current young cset group to allow adding. - // It is fine to clear it this late - evacuation does not add any remembered sets - // by itself, but only marks cards. - // The regions had their association to this group already removed earlier. - young_regions_cset_group()->clear(); - - collection_set()->start_incremental_building(); + collection_set()->start(); clear_region_attr(); @@ -2879,12 +2873,7 @@ void G1CollectedHeap::abandon_collection_set() { G1AbandonCollectionSetClosure cl; collection_set_iterate_all(&cl); - collection_set()->clear(); - collection_set()->stop_incremental_building(); - - collection_set()->abandon_all_candidates(); - - young_regions_cset_group()->clear(true /* uninstall_group_cardset */); + collection_set()->abandon(); } bool G1CollectedHeap::is_old_gc_alloc_region(G1HeapRegion* hr) { @@ -3247,9 +3236,3 @@ void G1CollectedHeap::finish_codecache_marking_cycle() { CodeCache::on_gc_marking_cycle_finish(); CodeCache::arm_all_nmethods(); } - -void G1CollectedHeap::prepare_group_cardsets_for_scan() { - young_regions_cardset()->reset_table_scanner_for_groups(); - - collection_set()->prepare_for_scan(); -} diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp index 322b37188d3..845f8a257a1 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.hpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.hpp @@ -805,11 +805,6 @@ public: G1CardSetConfiguration* card_set_config() { return &_card_set_config; } G1CSetCandidateGroup* young_regions_cset_group() { return &_young_regions_cset_group; } - G1CardSet* young_regions_cardset() { return _young_regions_cset_group.card_set(); }; - - G1MonotonicArenaMemoryStats young_regions_card_set_memory_stats() { return _young_regions_cset_group.card_set_memory_stats(); } - - void prepare_group_cardsets_for_scan(); // After a collection pause, reset eden and the collection set. void clear_eden(); diff --git a/src/hotspot/share/gc/g1/g1CollectionSet.cpp b/src/hotspot/share/gc/g1/g1CollectionSet.cpp index abfb620d626..037eb071072 100644 --- a/src/hotspot/share/gc/g1/g1CollectionSet.cpp +++ b/src/hotspot/share/gc/g1/g1CollectionSet.cpp @@ -99,12 +99,21 @@ void G1CollectionSet::initialize(uint max_region_length) { _candidates.initialize(max_region_length); } +void G1CollectionSet::abandon() { + _g1h->young_regions_cset_group()->clear(true /* uninstall_cset_group */); + clear(); + abandon_all_candidates(); + + stop_incremental_building(); +} + void G1CollectionSet::abandon_all_candidates() { _candidates.clear(); _initial_old_region_length = 0; } void G1CollectionSet::prepare_for_scan () { + _g1h->young_regions_cset_group()->card_set()->reset_table_scanner_for_groups(); _groups.prepare_for_scan(); } @@ -127,12 +136,15 @@ void G1CollectionSet::add_old_region(G1HeapRegion* hr) { _g1h->old_set_remove(hr); } -void G1CollectionSet::start_incremental_building() { +void G1CollectionSet::start() { assert(_regions_cur_length == 0, "Collection set must be empty before starting a new collection set."); assert(groups_cur_length() == 0, "Collection set groups must be empty before starting a new collection set."); assert(_optional_groups.length() == 0, "Collection set optional gorups must be empty before starting a new collection set."); continue_incremental_building(); + + G1CSetCandidateGroup* young_group = _g1h->young_regions_cset_group(); + young_group->clear(); } void G1CollectionSet::continue_incremental_building() { diff --git a/src/hotspot/share/gc/g1/g1CollectionSet.hpp b/src/hotspot/share/gc/g1/g1CollectionSet.hpp index db1824e44cb..e1d6c40f0ce 100644 --- a/src/hotspot/share/gc/g1/g1CollectionSet.hpp +++ b/src/hotspot/share/gc/g1/g1CollectionSet.hpp @@ -227,6 +227,7 @@ class G1CollectionSet { uint& num_optional_regions, double& predicted_optional_time_ms, double predicted_time_ms); + public: G1CollectionSet(G1CollectedHeap* g1h, G1Policy* policy); ~G1CollectionSet(); @@ -234,6 +235,8 @@ public: // Initializes the collection set giving the maximum possible length of the collection set. void initialize(uint max_region_length); + // Drop the collection set and collection set candidates. + void abandon(); // Drop all collection set candidates (only the candidates). void abandon_all_candidates(); @@ -261,13 +264,15 @@ public: template inline void merge_cardsets_for_collection_groups(CardOrRangeVisitor& cl, uint worker_id, uint num_workers); + uint groups_increment_length() const; + // Reset the contents of the collection set. void clear(); // Incremental collection set support - // Initialize incremental collection set info. - void start_incremental_building(); + // Start a new collection set for the next mutator phase. + void start(); // Start a new collection set increment, continuing the incremental building. void continue_incremental_building(); // Stop adding regions to the current collection set increment. @@ -282,8 +287,6 @@ public: // Returns the length of the whole current collection set in number of regions size_t cur_length() const { return _regions_cur_length; } - uint groups_increment_length() const; - // Iterate over the entire collection set (all increments calculated so far), applying // the given G1HeapRegionClosure on all of the regions. void iterate(G1HeapRegionClosure* cl) const; diff --git a/src/hotspot/share/gc/g1/g1CollectionSet.inline.hpp b/src/hotspot/share/gc/g1/g1CollectionSet.inline.hpp index 56fe9bbcc88..8b2086f2109 100644 --- a/src/hotspot/share/gc/g1/g1CollectionSet.inline.hpp +++ b/src/hotspot/share/gc/g1/g1CollectionSet.inline.hpp @@ -31,20 +31,24 @@ template inline void G1CollectionSet::merge_cardsets_for_collection_groups(CardOrRangeVisitor& cl, uint worker_id, uint num_workers) { - uint length = groups_increment_length(); uint offset = _groups_inc_part_start; - if (length == 0) { + if (offset == 0) { + G1HeapRegionRemSet::iterate_for_merge(_g1h->young_regions_cset_group()->card_set(), cl); + } + + uint next_increment_length = groups_increment_length(); + if (next_increment_length == 0) { return; } - uint start_pos = (worker_id * length) / num_workers; + uint start_pos = (worker_id * next_increment_length) / num_workers; uint cur_pos = start_pos; uint count = 0; do { G1HeapRegionRemSet::iterate_for_merge(_groups.at(offset + cur_pos)->card_set(), cl); cur_pos++; count++; - if (cur_pos == length) { + if (cur_pos == next_increment_length) { cur_pos = 0; } } while (cur_pos != start_pos); diff --git a/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp b/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp index 0f4e92968fa..8987ee6b338 100644 --- a/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp +++ b/src/hotspot/share/gc/g1/g1CollectionSetCandidates.hpp @@ -122,6 +122,10 @@ public: return _card_set_mm.memory_stats(); } + size_t cards_occupied() const { + return _card_set.occupied(); + } + void clear(bool uninstall_group_cardset = false); G1CSetCandidateGroupIterator begin() const { diff --git a/src/hotspot/share/gc/g1/g1RemSet.cpp b/src/hotspot/share/gc/g1/g1RemSet.cpp index d2bfd22d830..16eef67d1fc 100644 --- a/src/hotspot/share/gc/g1/g1RemSet.cpp +++ b/src/hotspot/share/gc/g1/g1RemSet.cpp @@ -1166,10 +1166,6 @@ public: // 2. collection set G1MergeCardSetClosure merge(_scan_state); - if (_initial_evacuation) { - G1HeapRegionRemSet::iterate_for_merge(g1h->young_regions_cardset(), merge); - } - g1h->collection_set()->merge_cardsets_for_collection_groups(merge, worker_id, _num_workers); G1MergeCardSetStats stats = merge.stats(); diff --git a/src/hotspot/share/gc/g1/g1YoungCollector.cpp b/src/hotspot/share/gc/g1/g1YoungCollector.cpp index e97e59575e3..6a7d4717a6f 100644 --- a/src/hotspot/share/gc/g1/g1YoungCollector.cpp +++ b/src/hotspot/share/gc/g1/g1YoungCollector.cpp @@ -506,7 +506,7 @@ void G1YoungCollector::pre_evacuate_collection_set(G1EvacInfo* evacuation_info) Ticks start = Ticks::now(); rem_set()->prepare_for_scan_heap_roots(); - _g1h->prepare_group_cardsets_for_scan(); + _g1h->collection_set()->prepare_for_scan(); phase_times()->record_prepare_heap_roots_time_ms((Ticks::now() - start).seconds() * 1000.0); } @@ -516,7 +516,7 @@ void G1YoungCollector::pre_evacuate_collection_set(G1EvacInfo* evacuation_info) Tickspan task_time = run_task_timed(&g1_prep_task); G1MonotonicArenaMemoryStats sampled_card_set_stats = g1_prep_task.all_card_set_stats(); - sampled_card_set_stats.add(_g1h->young_regions_card_set_memory_stats()); + sampled_card_set_stats.add(_g1h->young_regions_cset_group()->card_set_memory_stats()); _g1h->set_young_gen_card_set_stats(sampled_card_set_stats); _g1h->set_humongous_stats(g1_prep_task.humongous_total(), g1_prep_task.humongous_candidates()); From 4bc86a26db1eb3d054d80c9759fe04686e1e36b3 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Tue, 23 Sep 2025 12:36:13 +0000 Subject: [PATCH 184/556] 8367948: JFR: MethodTrace threshold setting has no effect Reviewed-by: shade --- .../jdk/jfr/events/MethodTraceEvent.java | 2 +- .../classes/jdk/jfr/tracing/MethodTracer.java | 8 ++--- .../jfr/event/tracing/TestMethodTrace.java | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/jdk.jfr/share/classes/jdk/jfr/events/MethodTraceEvent.java b/src/jdk.jfr/share/classes/jdk/jfr/events/MethodTraceEvent.java index a56de3e6fca..fa9d1345cd6 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/events/MethodTraceEvent.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/events/MethodTraceEvent.java @@ -49,7 +49,7 @@ public final class MethodTraceEvent extends AbstractJDKEvent { return 0; } - public static boolean enabled() { + public static boolean shouldCommit(long duration) { // Generated by JFR return false; } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/tracing/MethodTracer.java b/src/jdk.jfr/share/classes/jdk/jfr/tracing/MethodTracer.java index 0d7b8072194..c4cb0e4fa2c 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/tracing/MethodTracer.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/tracing/MethodTracer.java @@ -50,7 +50,7 @@ public final class MethodTracer { public static void traceObjectInit(long startTime, long methodId) { long endTime = JVM.counterTime(); long duration = endTime - startTime; - if (MethodTraceEvent.enabled() && JVM.getEventWriter() != null) { + if (MethodTraceEvent.shouldCommit(duration) && JVM.getEventWriter() != null) { MethodTraceEvent.commit(startTime, duration, methodId); } } @@ -66,7 +66,7 @@ public final class MethodTracer { public static void traceTimingObjectInit(long startTime, long methodId) { long endTime = JVM.counterTime(); long duration = endTime - startTime; - if (MethodTraceEvent.enabled() && JVM.getEventWriter() != null) { + if (MethodTraceEvent.shouldCommit(duration) && JVM.getEventWriter() != null) { MethodTraceEvent.commit(startTime, duration, methodId); } if (MethodTimingEvent.enabled()) { @@ -77,7 +77,7 @@ public final class MethodTracer { public static void trace(long startTime, long methodId) { long endTime = JVM.counterTime(); long duration = endTime - startTime; - if (MethodTraceEvent.enabled()) { + if (MethodTraceEvent.shouldCommit(duration)) { MethodTraceEvent.commit(startTime, duration, methodId); } } @@ -96,7 +96,7 @@ public final class MethodTracer { if (MethodTimingEvent.enabled()) { PlatformTracer.addTiming(methodId, duration); } - if (MethodTraceEvent.enabled()) { + if (MethodTraceEvent.shouldCommit(duration)) { MethodTraceEvent.commit(startTime, duration, methodId); } } diff --git a/test/jdk/jdk/jfr/event/tracing/TestMethodTrace.java b/test/jdk/jdk/jfr/event/tracing/TestMethodTrace.java index a5fd1430627..5f26e5588f3 100644 --- a/test/jdk/jdk/jfr/event/tracing/TestMethodTrace.java +++ b/test/jdk/jdk/jfr/event/tracing/TestMethodTrace.java @@ -22,6 +22,8 @@ */ package jdk.jfr.event.tracing; +import java.time.Duration; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import jdk.jfr.Event; @@ -29,7 +31,9 @@ import jdk.jfr.StackTrace; import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.consumer.RecordedMethod; import jdk.jfr.consumer.RecordingStream; +import jdk.jfr.Recording; +import jdk.test.lib.jfr.Events; /** * @test * @summary Basic test of the MethodTrace event. @@ -52,6 +56,31 @@ public class TestMethodTrace { } public static void main(String... args) throws Exception { + testWithoutThreshold(); + testWithThreshold(); + } + + private static void testWithThreshold() throws Exception { + try (Recording r = new Recording()) { + r.enable(EVENT_NAME) + .with("filter", CLASS_NAME + "::printHello") + .withThreshold(Duration.ofHours(1)); + r.start(); + printHello(); + r.stop(); + List events = Events.fromRecording(r); + if (!events.isEmpty()) { + System.out.println(events); + throw new Exception("Unexpected MethodTrace event"); + } + } + } + + public static void printHello() { + System.out.println("Hello!"); + } + + private static void testWithoutThreshold() throws Exception { AtomicReference o = new AtomicReference<>(); AtomicReference i = new AtomicReference<>(); AtomicReference e = new AtomicReference<>(); From f9b91a7836766189e1ccefabdd39d30ad440146b Mon Sep 17 00:00:00 2001 From: Chen Liang Date: Tue, 23 Sep 2025 12:47:56 +0000 Subject: [PATCH 185/556] 8368050: Validation missing in ClassFile signature factories Reviewed-by: asotona --- .../java/lang/classfile/MethodSignature.java | 6 +- .../java/lang/classfile/Signature.java | 90 +++++++++++++++---- .../classfile/impl/ClassRemapperImpl.java | 4 +- .../classfile/impl/SignaturesImpl.java | 89 +++++++++++++++--- test/jdk/jdk/classfile/SignaturesTest.java | 70 ++++++++++++++- 5 files changed, 227 insertions(+), 32 deletions(-) diff --git a/src/java.base/share/classes/java/lang/classfile/MethodSignature.java b/src/java.base/share/classes/java/lang/classfile/MethodSignature.java index f81820a7f77..76251cd4d8e 100644 --- a/src/java.base/share/classes/java/lang/classfile/MethodSignature.java +++ b/src/java.base/share/classes/java/lang/classfile/MethodSignature.java @@ -113,13 +113,14 @@ public sealed interface MethodSignature * * @param result signature for the return type * @param arguments signatures for the method parameters + * @throws IllegalArgumentException if any of {@code arguments} is void */ public static MethodSignature of(Signature result, Signature... arguments) { return new SignaturesImpl.MethodSignatureImpl(List.of(), List.of(), requireNonNull(result), - List.of(arguments)); + SignaturesImpl.validateArgumentList(arguments)); } /** @@ -131,6 +132,7 @@ public sealed interface MethodSignature * @param exceptions signatures for the exceptions * @param result signature for the return type * @param arguments signatures for the method parameters + * @throws IllegalArgumentException if any of {@code arguments} is void */ public static MethodSignature of(List typeParameters, List exceptions, @@ -140,7 +142,7 @@ public sealed interface MethodSignature List.copyOf(requireNonNull(typeParameters)), List.copyOf(requireNonNull(exceptions)), requireNonNull(result), - List.of(arguments)); + SignaturesImpl.validateArgumentList(arguments)); } /** diff --git a/src/java.base/share/classes/java/lang/classfile/Signature.java b/src/java.base/share/classes/java/lang/classfile/Signature.java index f9af71083f9..594c7822e98 100644 --- a/src/java.base/share/classes/java/lang/classfile/Signature.java +++ b/src/java.base/share/classes/java/lang/classfile/Signature.java @@ -43,6 +43,11 @@ import static java.util.Objects.requireNonNull; /** * Models generic Java type signatures, as defined in JVMS {@jvms 4.7.9.1}. + *

        + * Names in signatures are identifiers, which must + * not be empty and must not contain any of the ASCII characters {@code + * . ; [ / < > :}. Top-level class and interface names are denoted by + * slash-separated identifiers. * * @see Type * @see SignatureAttribute @@ -73,6 +78,8 @@ public sealed interface Signature { * signature represents a reifiable type (JLS {@jls 4.7}). * * @param classDesc the symbolic description of the Java type + * @throws IllegalArgumentException if the field descriptor cannot be + * {@linkplain ##identifier denoted} */ public static Signature of(ClassDesc classDesc) { requireNonNull(classDesc); @@ -139,6 +146,31 @@ public sealed interface Signature { /** * Models the signature of a possibly-parameterized class or interface type. + *

        + * These are examples of class type signatures: + *

        + *

        + * If the {@linkplain #outerType() outer type} exists, the {@linkplain + * #className() class name} is the simple name of the nested type. + * Otherwise, it is a {@linkplain ClassEntry##internalname binary name in + * internal form} (separated by {@code /}). + *

        + * If a nested type does not have any enclosing parameterization, it may + * be represented without an outer type and as an internal binary name, + * in which nesting is represented by {@code $} instead of {@code .}. * * @see Type * @see ParameterizedType @@ -152,7 +184,8 @@ public sealed interface Signature { /** * {@return the signature of the class that this class is a member of, * only if this is a member class} Note that the outer class may be - * absent if it is not a parameterized type. + * absent if this is a member class without any parameterized enclosing + * type. * * @jls 4.5 Parameterized Types */ @@ -161,7 +194,8 @@ public sealed interface Signature { /** * {@return the class or interface name; includes the {@linkplain * ClassEntry##internalname slash-separated} package name if there is no - * outer type} + * outer type} Note this may indicate a nested class name with {@code $} + * separators if there is no parameterized enclosing type. */ String className(); @@ -188,10 +222,11 @@ public sealed interface Signature { * @param className the name of the class or interface * @param typeArgs the type arguments * @throws IllegalArgumentException if {@code className} does not - * represent a class or interface + * represent a class or interface, or if it cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(ClassDesc className, TypeArg... typeArgs) { - return of(null, className, typeArgs); + return of(null, Util.toInternalName(className), typeArgs); } /** @@ -201,8 +236,15 @@ public sealed interface Signature { * @param className the name of this class or interface * @param typeArgs the type arguments * @throws IllegalArgumentException if {@code className} does not - * represent a class or interface + * represent a class or interface, or if it cannot be + * {@linkplain Signature##identifier denoted} + * @deprecated + * The resulting signature does not denote the class represented by + * {@code className} when {@code outerType} is not null. Use {@link + * #of(ClassTypeSig, String, TypeArg...) of(ClassTypeSig, String, TypeArg...)} + * instead. */ + @Deprecated(since = "26", forRemoval = true) public static ClassTypeSig of(ClassTypeSig outerType, ClassDesc className, TypeArg... typeArgs) { requireNonNull(className); return of(outerType, Util.toInternalName(className), typeArgs); @@ -211,8 +253,11 @@ public sealed interface Signature { /** * {@return a class or interface signature without an outer type} * - * @param className the name of the class or interface + * @param className the name of the class or interface, may use + * {@code /} to separate * @param typeArgs the type arguments + * @throws IllegalArgumentException if {@code className} cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(String className, TypeArg... typeArgs) { return of(null, className, typeArgs); @@ -222,12 +267,19 @@ public sealed interface Signature { * {@return a class type signature} * * @param outerType signature of the outer type, may be {@code null} - * @param className the name of this class or interface + * @param className the name of this class or interface, may use + * {@code /} to separate if outer type is absent * @param typeArgs the type arguments + * @throws IllegalArgumentException if {@code className} cannot be + * {@linkplain Signature##identifier denoted} */ public static ClassTypeSig of(ClassTypeSig outerType, String className, TypeArg... typeArgs) { - requireNonNull(className); - return new SignaturesImpl.ClassTypeSigImpl(Optional.ofNullable(outerType), className.replace(".", "/"), List.of(typeArgs)); + if (outerType != null) { + SignaturesImpl.validateIdentifier(className); + } else { + SignaturesImpl.validatePackageSpecifierPlusIdentifier(className); + } + return new SignaturesImpl.ClassTypeSigImpl(Optional.ofNullable(outerType), className, List.of(typeArgs)); } } @@ -383,9 +435,11 @@ public sealed interface Signature { * {@return a signature for a type variable} * * @param identifier the name of the type variable + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeVarSig of(String identifier) { - return new SignaturesImpl.TypeVarSigImpl(requireNonNull(identifier)); + return new SignaturesImpl.TypeVarSigImpl(SignaturesImpl.validateIdentifier(identifier)); } } @@ -408,9 +462,10 @@ public sealed interface Signature { /** * {@return an array type with the given component type} * @param componentSignature the component type + * @throws IllegalArgumentException if the component type is void */ public static ArrayTypeSig of(Signature componentSignature) { - return of(1, requireNonNull(componentSignature)); + return of(1, SignaturesImpl.validateNonVoid(componentSignature)); } /** @@ -418,10 +473,11 @@ public sealed interface Signature { * @param dims the dimension of the array * @param componentSignature the component type * @throws IllegalArgumentException if {@code dims < 1} or the - * resulting array type exceeds 255 dimensions + * resulting array type exceeds 255 dimensions or the component + * type is void */ public static ArrayTypeSig of(int dims, Signature componentSignature) { - requireNonNull(componentSignature); + SignaturesImpl.validateNonVoid(componentSignature); if (componentSignature instanceof SignaturesImpl.ArrayTypeSigImpl arr) { if (dims < 1 || dims > 255 - arr.arrayDepth()) throw new IllegalArgumentException("illegal array depth value"); @@ -469,10 +525,12 @@ public sealed interface Signature { * @param identifier the name of the type parameter * @param classBound the class bound of the type parameter, may be {@code null} * @param interfaceBounds the interface bounds of the type parameter + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeParam of(String identifier, RefTypeSig classBound, RefTypeSig... interfaceBounds) { return new SignaturesImpl.TypeParamImpl( - requireNonNull(identifier), + SignaturesImpl.validateIdentifier(identifier), Optional.ofNullable(classBound), List.of(interfaceBounds)); } @@ -483,10 +541,12 @@ public sealed interface Signature { * @param identifier the name of the type parameter * @param classBound the optional class bound of the type parameter * @param interfaceBounds the interface bounds of the type parameter + * @throws IllegalArgumentException if the name cannot be {@linkplain + * Signature##identifier denoted} */ public static TypeParam of(String identifier, Optional classBound, RefTypeSig... interfaceBounds) { return new SignaturesImpl.TypeParamImpl( - requireNonNull(identifier), + SignaturesImpl.validateIdentifier(identifier), requireNonNull(classBound), List.of(interfaceBounds)); } diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java index 6dbde898ee5..921255d5398 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/ClassRemapperImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -309,7 +309,7 @@ public record ClassRemapperImpl(Function mapFunction) impl case Signature.ClassTypeSig cts -> Signature.ClassTypeSig.of( cts.outerType().map(this::mapSignature).orElse(null), - map(cts.classDesc()), + Util.toInternalName(map(cts.classDesc())), cts.typeArgs().stream().map(ta -> switch (ta) { case Signature.TypeArg.Unbounded u -> u; case Signature.TypeArg.Bounded bta -> Signature.TypeArg.bounded( diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java index 5486b58f04e..d0dbf89551f 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -73,7 +73,7 @@ public final class SignaturesImpl { while (!match(')')) { if (paramTypes == null) paramTypes = new ArrayList<>(); - paramTypes.add(typeSig()); + paramTypes.add(validateNonVoid(typeSig())); } Signature returnType = typeSig(); ArrayList throwsTypes = null; @@ -226,13 +226,9 @@ public final class SignaturesImpl { */ private int requireIdentifier() { int start = sigp; - l: while (sigp < sig.length()) { - switch (sig.charAt(sigp)) { - case '.', ';', '[', '/', '<', '>', ':' -> { - break l; - } - } + if (isNonIdentifierChar(sig.charAt(sigp))) + break; sigp++; } if (start == sigp) { @@ -241,6 +237,77 @@ public final class SignaturesImpl { return sigp; } + // Non-identifier chars in ascii 0 to 63, note [ is larger + private static final long SMALL_NON_IDENTIFIER_CHARS_SET = (1L << '.') + | (1L << ';') + | (1L << '/') + | (1L << '<') + | (1L << '>') + | (1L << ':'); + + private static boolean isNonIdentifierChar(char c) { + return c < Long.SIZE ? (SMALL_NON_IDENTIFIER_CHARS_SET & (1L << c)) != 0 : c == '['; + } + + /// {@return exclusive end of the next identifier} + public static int nextIdentifierEnd(String st, int start) { + int end = st.length(); + for (int i = start; i < end; i++) { + if (isNonIdentifierChar(st.charAt(i))) { + return i; + } + } + return end; + } + + /// Validates this string as a simple identifier. + public static String validateIdentifier(String st) { + var len = st.length(); // implicit null check + if (len == 0 || nextIdentifierEnd(st, 0) != len) { + throw new IllegalArgumentException("Not a valid identifier: " + st); + } + return st; + } + + /// Validates this string as slash-separated one or more identifiers. + public static String validatePackageSpecifierPlusIdentifier(String st) { + int nextIdentifierStart = 0; + int len = st.length(); + while (nextIdentifierStart < len) { + int end = nextIdentifierEnd(st, nextIdentifierStart); + if (end == len) + return st; + if (end == nextIdentifierStart || st.charAt(end) != '/') + throw new IllegalArgumentException("Not a class name: " + st); + nextIdentifierStart = end + 1; + } + // Couldn't get an identifier initially or after a separator. + throw new IllegalArgumentException("Not a class name: " + st); + } + + /// Validates the signature to be non-void (a valid field type). + public static Signature validateNonVoid(Signature incoming) { + Objects.requireNonNull(incoming); + if (incoming instanceof Signature.BaseTypeSig baseType && baseType.baseType() == 'V') + throw new IllegalArgumentException("void"); + return incoming; + } + + /// Returns the validated immutable argument list or fails with IAE. + public static List validateArgumentList(Signature[] signatures) { + return validateArgumentList(List.of(signatures)); + } + + /// Returns the validated immutable argument list or fails with IAE. + public static List validateArgumentList(List signatures) { + var res = List.copyOf(signatures); // deep null checks + for (var sig : signatures) { + if (sig instanceof Signature.BaseTypeSig baseType && baseType.baseType() == 'V') + throw new IllegalArgumentException("void"); + } + return res; + } + public static record BaseTypeSigImpl(char baseType) implements Signature.BaseTypeSig { @Override @@ -316,13 +383,13 @@ public final class SignaturesImpl { private static StringBuilder printTypeParameters(List typeParameters) { var sb = new StringBuilder(); - if (typeParameters != null && !typeParameters.isEmpty()) { + if (!typeParameters.isEmpty()) { sb.append('<'); for (var tp : typeParameters) { sb.append(tp.identifier()).append(':'); if (tp.classBound().isPresent()) sb.append(tp.classBound().get().signatureString()); - if (tp.interfaceBounds() != null) for (var is : tp.interfaceBounds()) + for (var is : tp.interfaceBounds()) sb.append(':').append(is.signatureString()); } sb.append('>'); @@ -337,7 +404,7 @@ public final class SignaturesImpl { public String signatureString() { var sb = printTypeParameters(typeParameters); sb.append(superclassSignature.signatureString()); - if (superinterfaceSignatures != null) for (var in : superinterfaceSignatures) + for (var in : superinterfaceSignatures) sb.append(in.signatureString()); return sb.toString(); } diff --git a/test/jdk/jdk/classfile/SignaturesTest.java b/test/jdk/jdk/classfile/SignaturesTest.java index 692db1dbb29..333c6dd2f29 100644 --- a/test/jdk/jdk/classfile/SignaturesTest.java +++ b/test/jdk/jdk/classfile/SignaturesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -48,6 +48,9 @@ import java.lang.classfile.Attributes; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import static helpers.ClassRecord.assertEqualsDeep; import static java.lang.constant.ConstantDescs.*; import java.util.function.Consumer; @@ -63,7 +66,7 @@ class SignaturesTest { ClassSignature.of( ClassTypeSig.of( ClassTypeSig.of(ClassDesc.of("java.util.LinkedHashMap"), TypeArg.of(TypeVarSig.of("K")), TypeArg.of(TypeVarSig.of("V"))), - ClassDesc.of("LinkedHashIterator")), + "LinkedHashIterator"), ClassTypeSig.of(ClassDesc.of("java.util.Iterator"), TypeArg.of(ClassTypeSig.of(ClassDesc.of("java.util.Map$Entry"), TypeArg.of(TypeVarSig.of("K")), TypeArg.of(TypeVarSig.of("V")))))), ClassSignature.parseFrom("Ljava/util/LinkedHashMap.LinkedHashIterator;Ljava/util/Iterator;>;")); @@ -119,6 +122,67 @@ class SignaturesTest { Signature.parseFrom("[[TE;")); } + @Test + void testGenericCreationChecks() { + var weirdNameClass = ClassDesc.of(""); + var voidSig = BaseTypeSig.of('V'); + Assertions.assertThrows(IllegalArgumentException.class, () -> Signature.of(weirdNameClass)); + Assertions.assertThrows(IllegalArgumentException.class, () -> BaseTypeSig.of(CD_Object)); + Assertions.assertThrows(IllegalArgumentException.class, () -> BaseTypeSig.of(CD_int.arrayType())); + Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(CD_int)); + Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(CD_Object.arrayType())); + Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(weirdNameClass)); + Assertions.assertThrows(IllegalArgumentException.class, () -> ArrayTypeSig.of(voidSig)); + Assertions.assertThrows(IllegalArgumentException.class, () -> ArrayTypeSig.of(255, voidSig)); + Assertions.assertThrows(IllegalArgumentException.class, () -> MethodSignature.of(voidSig, voidSig)); + Assertions.assertThrows(IllegalArgumentException.class, () -> MethodSignature.of(List.of(), List.of(), voidSig, voidSig)); + } + + static Stream goodIdentifiers() { + return Stream.of("T", "Hello", "Mock", "(Weird)", " Huh? "); + } + + static Stream badIdentifiers() { + return Stream.of("", ";", ".", "/", "<", ">", "[", ":", + "", "has.chars", "/Outer", "test/", "test//Outer"); + } + + static Stream slashedIdentifiers() { + return Stream.of("test/Outer", "java/lang/Integer"); + } + + @ParameterizedTest + @MethodSource({"badIdentifiers", "slashedIdentifiers"}) + void testBadSimpleIdentifier(String st) { + ClassTypeSig outer = ClassTypeSig.of("test/Outer"); + Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(outer, st)); + Assertions.assertThrows(IllegalArgumentException.class, () -> TypeVarSig.of(st)); + Assertions.assertThrows(IllegalArgumentException.class, () -> TypeParam.of(st, (RefTypeSig) null)); + Assertions.assertThrows(IllegalArgumentException.class, () -> TypeParam.of(st, Optional.empty())); + } + + @ParameterizedTest + @MethodSource("goodIdentifiers") + void testGoodSimpleIdentifier(String st) { + ClassTypeSig outer = ClassTypeSig.of("test/Outer"); + ClassTypeSig.of(outer, st); + TypeVarSig.of(st); + TypeParam.of(st, (RefTypeSig) null); + TypeParam.of(st, Optional.empty()); + } + + @ParameterizedTest + @MethodSource("badIdentifiers") + void testBadSlashedIdentifier(String st) { + Assertions.assertThrows(IllegalArgumentException.class, () -> ClassTypeSig.of(st)); + } + + @ParameterizedTest + @MethodSource({"goodIdentifiers", "slashedIdentifiers"}) + void testGoodSlashedIdentifier(String st) { + ClassTypeSig.of(st); + } + @Test void testParseAndPrintSignatures() throws Exception { var csc = new AtomicInteger(); @@ -231,6 +295,7 @@ class SignaturesTest { Lcom/example/Outer Lcom/example/Outer. Lcom/example/Outer.Inner<[I> + [V """.lines().forEach(assertThrows(Signature::parseFrom)); } @@ -297,6 +362,7 @@ class SignaturesTest { (LSet;>;)V ()V (TT;I)VI + (V)V """.lines().forEach(assertThrows(MethodSignature::parseFrom)); } From 61acdf6512c6ea3123edb9017ef99d851c917b90 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Tue, 23 Sep 2025 13:09:15 +0000 Subject: [PATCH 186/556] 8365065: cancelled ForkJoinPool tasks no longer throw CancellationException Co-authored-by: Doug Lea Reviewed-by: alanb --- .../java/util/concurrent/ForkJoinTask.java | 19 +++--- .../util/concurrent/tck/ForkJoinPoolTest.java | 61 ++++++++++++++++++- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java index 51b2488264e..137cac45ed0 100644 --- a/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java +++ b/src/java.base/share/classes/java/util/concurrent/ForkJoinTask.java @@ -531,19 +531,22 @@ public abstract class ForkJoinTask implements Future, Serializable { * still correct, although it may contain a misleading stack * trace. * - * @param asExecutionException true if wrap as ExecutionException + * @param asExecutionException true if wrap the result as an + * ExecutionException. This applies only to actual exceptions, not + * implicit CancellationExceptions issued when not THROWN or + * available, which are not wrapped because by default they are + * issued separately from ExecutionExceptions by callers. Which + * may require further handling when this is not true (currently + * only in InvokeAnyTask). * @return the exception, or null if none */ private Throwable getException(boolean asExecutionException) { int s; Throwable ex; Aux a; if ((s = status) >= 0 || (s & ABNORMAL) == 0) return null; - else if ((s & THROWN) == 0 || (a = aux) == null || (ex = a.ex) == null) { - ex = new CancellationException(); - if (!asExecutionException || !(this instanceof InterruptibleTask)) - return ex; // else wrap below - } - else if (a.thread != Thread.currentThread()) { + if ((s & THROWN) == 0 || (a = aux) == null || (ex = a.ex) == null) + return new CancellationException(); + if (a.thread != Thread.currentThread()) { try { Constructor noArgCtor = null, oneArgCtor = null; for (Constructor c : ex.getClass().getConstructors()) { @@ -1814,6 +1817,8 @@ public abstract class ForkJoinTask implements Future, Serializable { (t = new InvokeAnyTask(c, this, t))); } return timed ? get(nanos, TimeUnit.NANOSECONDS) : get(); + } catch (CancellationException ce) { + throw new ExecutionException(ce); } finally { for (; t != null; t = t.pred) t.onRootCompletion(); diff --git a/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java b/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java index 9422128511e..de1caaab2d9 100644 --- a/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java +++ b/test/jdk/java/util/concurrent/tck/ForkJoinPoolTest.java @@ -31,9 +31,6 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.NANOSECONDS; - import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; @@ -41,6 +38,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -51,13 +49,19 @@ import java.util.concurrent.ForkJoinWorkerThread; import java.util.concurrent.Future; import java.util.concurrent.RecursiveTask; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import junit.framework.Test; import junit.framework.TestSuite; +import static java.util.concurrent.TimeUnit.*; + public class ForkJoinPoolTest extends JSR166TestCase { public static void main(String[] args) { main(suite(), args); @@ -482,6 +486,57 @@ public class ForkJoinPoolTest extends JSR166TestCase { } } + public void testCancellationExceptionInGet() throws Exception { + final ExecutorService e = new ForkJoinPool(1); + try (var cleaner = cleaner(e)) { + assertCancellationExceptionFrom( + e::submit, + f -> () -> f.get(1000, TimeUnit.SECONDS) + ); + assertCancellationExceptionFrom( + e::submit, + f -> f::get + ); + assertCancellationExceptionFrom( + c -> e.submit(() -> { try { c.call(); } catch (Exception ex) { throw new RuntimeException(ex); } }), + f -> () -> f.get(1000, TimeUnit.SECONDS) + ); + assertCancellationExceptionFrom( + c -> e.submit(() -> { try { c.call(); } catch (Exception ex) { throw new RuntimeException(ex); } }), + f -> f::get + ); + } + } + + private void assertCancellationExceptionFrom( + Function, Future> createTask, + Function, Callable> getResult) throws Exception { + final var t = new AtomicReference(); + final var c = new CountDownLatch(1); // Only used to induce WAITING state (never counted down) + final var task = createTask.apply(() -> { + try { + t.set(Thread.currentThread()); + c.await(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt();; + } + return null; + }); + Thread taskThread; + while((taskThread = t.get()) == null || taskThread.getState() != Thread.State.WAITING) { + if (Thread.interrupted()) + throw new InterruptedException(); + Thread.onSpinWait(); + } + task.cancel(true); + try { + getResult.apply(task).call(); + } catch (CancellationException ce) { + return; // Success + } + shouldThrow(); + } + /** * Completed submit(runnable, result) returns result */ From fd30ae988bc512b5d2a5a3fd1bc1ed351af974c7 Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Tue, 23 Sep 2025 14:43:16 +0000 Subject: [PATCH 187/556] 8350550: Preload classes from AOT cache during VM bootstrap Reviewed-by: kvn, heidinga, asmehra --- src/hotspot/share/cds/aotClassInitializer.cpp | 1 + src/hotspot/share/cds/aotClassLinker.cpp | 2 +- .../share/cds/aotLinkedClassBulkLoader.cpp | 440 +++++++----------- .../share/cds/aotLinkedClassBulkLoader.hpp | 46 +- src/hotspot/share/cds/aotLinkedClassTable.cpp | 2 +- src/hotspot/share/cds/aotLinkedClassTable.hpp | 13 +- src/hotspot/share/cds/aotMetaspace.cpp | 20 +- src/hotspot/share/cds/aotMetaspace.hpp | 6 +- src/hotspot/share/cds/aotOopChecker.cpp | 80 ++++ src/hotspot/share/cds/aotOopChecker.hpp | 40 ++ src/hotspot/share/cds/cdsConfig.cpp | 3 +- src/hotspot/share/cds/cdsConfig.hpp | 11 +- src/hotspot/share/cds/cdsEnumKlass.cpp | 5 +- src/hotspot/share/cds/cdsEnumKlass.hpp | 4 +- src/hotspot/share/cds/cdsHeapVerifier.cpp | 20 +- src/hotspot/share/cds/dynamicArchive.cpp | 2 - src/hotspot/share/cds/heapShared.cpp | 18 +- src/hotspot/share/cds/heapShared.hpp | 1 + .../share/classfile/classLoaderDataShared.cpp | 13 + .../share/classfile/classLoaderDataShared.hpp | 2 + src/hotspot/share/classfile/javaClasses.cpp | 34 +- src/hotspot/share/classfile/javaClasses.hpp | 1 + src/hotspot/share/classfile/moduleEntry.cpp | 2 + src/hotspot/share/classfile/modules.cpp | 39 +- src/hotspot/share/classfile/modules.hpp | 2 + .../share/classfile/systemDictionary.cpp | 67 ++- .../share/classfile/systemDictionary.hpp | 3 +- src/hotspot/share/classfile/vmClasses.cpp | 13 +- .../share/compiler/compilationPolicy.cpp | 14 +- src/hotspot/share/memory/iterator.inline.hpp | 10 +- src/hotspot/share/memory/universe.cpp | 5 + src/hotspot/share/runtime/java.cpp | 2 +- src/hotspot/share/runtime/threads.cpp | 8 +- .../share/classes/java/lang/ClassLoader.java | 47 +- .../java/lang/module/ModuleDescriptor.java | 8 + src/java.base/share/classes/java/net/URI.java | 9 + src/java.base/share/classes/java/net/URL.java | 8 + .../java/security/SecureClassLoader.java | 17 +- .../jdk/internal/loader/BootLoader.java | 12 +- .../jdk/internal/loader/NativeLibraries.java | 21 +- .../share/classes/jdk/internal/misc/CDS.java | 16 +- test/hotspot/jtreg/ProblemList-AotJdk.txt | 4 + .../aotClassLinking/MethodHandleTest.java | 43 ++ 43 files changed, 706 insertions(+), 408 deletions(-) create mode 100644 src/hotspot/share/cds/aotOopChecker.cpp create mode 100644 src/hotspot/share/cds/aotOopChecker.hpp diff --git a/src/hotspot/share/cds/aotClassInitializer.cpp b/src/hotspot/share/cds/aotClassInitializer.cpp index 0c8ea97fba0..4a98ccbf990 100644 --- a/src/hotspot/share/cds/aotClassInitializer.cpp +++ b/src/hotspot/share/cds/aotClassInitializer.cpp @@ -23,6 +23,7 @@ */ #include "cds/aotClassInitializer.hpp" +#include "cds/aotLinkedClassBulkLoader.hpp" #include "cds/archiveBuilder.hpp" #include "cds/cdsConfig.hpp" #include "cds/heapShared.hpp" diff --git a/src/hotspot/share/cds/aotClassLinker.cpp b/src/hotspot/share/cds/aotClassLinker.cpp index 0eb8f141c20..c66435b03bb 100644 --- a/src/hotspot/share/cds/aotClassLinker.cpp +++ b/src/hotspot/share/cds/aotClassLinker.cpp @@ -192,7 +192,7 @@ void AOTClassLinker::write_to_archive() { if (CDSConfig::is_dumping_aot_linked_classes()) { AOTLinkedClassTable* table = AOTLinkedClassTable::get(); - table->set_boot(write_classes(nullptr, true)); + table->set_boot1(write_classes(nullptr, true)); table->set_boot2(write_classes(nullptr, false)); table->set_platform(write_classes(SystemDictionary::java_platform_loader(), false)); table->set_app(write_classes(SystemDictionary::java_system_loader(), false)); diff --git a/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp b/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp index 8795a29fd5c..e7145b25457 100644 --- a/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp +++ b/src/hotspot/share/cds/aotLinkedClassBulkLoader.cpp @@ -29,6 +29,8 @@ #include "cds/cdsConfig.hpp" #include "cds/heapShared.hpp" #include "classfile/classLoaderData.hpp" +#include "classfile/classLoaderDataShared.hpp" +#include "classfile/javaClasses.hpp" #include "classfile/systemDictionary.hpp" #include "classfile/systemDictionaryShared.hpp" #include "classfile/vmClasses.hpp" @@ -41,66 +43,197 @@ #include "runtime/handles.inline.hpp" #include "runtime/java.hpp" -bool AOTLinkedClassBulkLoader::_boot2_completed = false; -bool AOTLinkedClassBulkLoader::_platform_completed = false; -bool AOTLinkedClassBulkLoader::_app_completed = false; -bool AOTLinkedClassBulkLoader::_all_completed = false; - void AOTLinkedClassBulkLoader::serialize(SerializeClosure* soc) { AOTLinkedClassTable::get()->serialize(soc); } -bool AOTLinkedClassBulkLoader::class_preloading_finished() { - if (!CDSConfig::is_using_aot_linked_classes()) { - return true; - } else { - // The ConstantPools of preloaded classes have references to other preloaded classes. We don't - // want any Java code (including JVMCI compiler) to use these classes until all of them - // are loaded. - return AtomicAccess::load_acquire(&_all_completed); +// This function is called before the VM executes any Java code (include AOT-compiled Java methods). +// +// We populate the boot/platform/app class loaders with classes from the AOT cache. This is a fundamental +// step in restoring the JVM's state from the snapshot recorded in the AOT cache: other AOT optimizations +// such as AOT compiled methods can make direct references to the preloaded classes, knowing that +// these classes are guaranteed to be in at least the "loaded" state. +void AOTLinkedClassBulkLoader::preload_classes(JavaThread* current) { + preload_classes_impl(current); + if (current->has_pending_exception()) { + exit_on_exception(current); } } -void AOTLinkedClassBulkLoader::load_javabase_classes(JavaThread* current) { - assert(CDSConfig::is_using_aot_linked_classes(), "sanity"); - load_classes_in_loader(current, AOTLinkedClassCategory::BOOT1, nullptr); // only java.base classes +void AOTLinkedClassBulkLoader::preload_classes_impl(TRAPS) { + precond(CDSConfig::is_using_aot_linked_classes()); + + ClassLoaderDataShared::restore_archived_modules_for_preloading_classes(THREAD); + Handle h_platform_loader(THREAD, SystemDictionary::java_platform_loader()); + Handle h_system_loader(THREAD, SystemDictionary::java_system_loader()); + + AOTLinkedClassTable* table = AOTLinkedClassTable::get(); + + preload_classes_in_table(table->boot1(), "boot1", Handle(), CHECK); + preload_classes_in_table(table->boot2(), "boot2", Handle(), CHECK); + + initiate_loading(THREAD, "plat", h_platform_loader, table->boot1()); + initiate_loading(THREAD, "plat", h_platform_loader, table->boot2()); + preload_classes_in_table(table->platform(), "plat", h_platform_loader, CHECK); + + initiate_loading(THREAD, "app", h_system_loader, table->boot1()); + initiate_loading(THREAD, "app", h_system_loader, table->boot2()); + initiate_loading(THREAD, "app", h_system_loader, table->platform()); + preload_classes_in_table(table->app(), "app", h_system_loader, CHECK); } -void AOTLinkedClassBulkLoader::load_non_javabase_classes(JavaThread* current) { +void AOTLinkedClassBulkLoader::preload_classes_in_table(Array* classes, + const char* category_name, Handle loader, TRAPS) { + if (classes == nullptr) { + return; + } + + for (int i = 0; i < classes->length(); i++) { + InstanceKlass* ik = classes->at(i); + if (log_is_enabled(Info, aot, load)) { + ResourceMark rm(THREAD); + log_info(aot, load)("%-5s %s%s", category_name, ik->external_name(), + ik->is_hidden() ? " (hidden)" : ""); + } + + SystemDictionary::preload_class(loader, ik, CHECK); + + if (ik->is_hidden()) { + DEBUG_ONLY({ + // Make sure we don't make this hidden class available by name, even if we don't + // use any special ClassLoaderData. + ResourceMark rm(THREAD); + assert(SystemDictionary::find_instance_klass(THREAD, ik->name(), loader) == nullptr, + "hidden classes cannot be accessible by name: %s", ik->external_name()); + }); + } else { + precond(SystemDictionary::find_instance_klass(THREAD, ik->name(), loader) == ik); + } + } +} + +#ifdef ASSERT +void AOTLinkedClassBulkLoader::validate_module_of_preloaded_classes() { + oop javabase_module_oop = ModuleEntryTable::javabase_moduleEntry()->module_oop(); + for (int i = T_BOOLEAN; i < T_LONG+1; i++) { + TypeArrayKlass* tak = Universe::typeArrayKlass((BasicType)i); + validate_module(tak, "boot1", javabase_module_oop); + } + + JavaThread* current = JavaThread::current(); + Handle h_platform_loader(current, SystemDictionary::java_platform_loader()); + Handle h_system_loader(current, SystemDictionary::java_system_loader()); + AOTLinkedClassTable* table = AOTLinkedClassTable::get(); + + validate_module_of_preloaded_classes_in_table(table->boot1(), "boot1", Handle()); + validate_module_of_preloaded_classes_in_table(table->boot2(), "boot2", Handle()); + validate_module_of_preloaded_classes_in_table(table->platform(), "plat", h_platform_loader); + validate_module_of_preloaded_classes_in_table(table->app(), "app", h_system_loader); +} + +void AOTLinkedClassBulkLoader::validate_module_of_preloaded_classes_in_table(Array* classes, + const char* category_name, Handle loader) { + if (classes == nullptr) { + return; + } + + ClassLoaderData* loader_data = ClassLoaderData::class_loader_data(loader()); + for (int i = 0; i < classes->length(); i++) { + InstanceKlass* ik = classes->at(i); + PackageEntry* pkg_entry = ik->package(); + oop module_oop; + if (pkg_entry == nullptr) { + module_oop = loader_data->unnamed_module()->module_oop(); + } else { + module_oop = pkg_entry->module()->module_oop(); + } + + validate_module(ik, category_name, module_oop); + } +} + +void AOTLinkedClassBulkLoader::validate_module(Klass* k, const char* category_name, oop module_oop) { + assert(module_oop != nullptr, "module system must have been initialized"); + + if (log_is_enabled(Debug, aot, module)) { + ResourceMark rm; + log_debug(aot, module)("Validate module of %-5s %s", category_name, k->external_name()); + } + precond(java_lang_Class::module(k->java_mirror()) == module_oop); + + ArrayKlass* ak = k->array_klass_or_null(); + while (ak != nullptr) { + if (log_is_enabled(Debug, aot, module)) { + ResourceMark rm; + log_debug(aot, module)("Validate module of %-5s %s", category_name, ak->external_name()); + } + precond(java_lang_Class::module(ak->java_mirror()) == module_oop); + ak = ak->array_klass_or_null(); + } +} +#endif + +// Link all java.base classes in the AOTLinkedClassTable. Of those classes, +// move the ones that have been AOT-initialized to the "initialized" state. +void AOTLinkedClassBulkLoader::link_or_init_javabase_classes(JavaThread* current) { + link_or_init_classes_for_loader(Handle(), AOTLinkedClassTable::get()->boot1(), current); + if (current->has_pending_exception()) { + exit_on_exception(current); + } +} + +// Do the same thing as link_or_init_javabase_classes(), but for the classes that are not +// in the java.base module. +void AOTLinkedClassBulkLoader::link_or_init_non_javabase_classes(JavaThread* current) { + link_or_init_non_javabase_classes_impl(current); + if (current->has_pending_exception()) { + exit_on_exception(current); + } +} + +void AOTLinkedClassBulkLoader::link_or_init_non_javabase_classes_impl(TRAPS) { assert(CDSConfig::is_using_aot_linked_classes(), "sanity"); + DEBUG_ONLY(validate_module_of_preloaded_classes()); + // is_using_aot_linked_classes() requires is_using_full_module_graph(). As a result, // the platform/system class loader should already have been initialized as part // of the FMG support. assert(CDSConfig::is_using_full_module_graph(), "must be"); - assert(SystemDictionary::java_platform_loader() != nullptr, "must be"); - assert(SystemDictionary::java_system_loader() != nullptr, "must be"); - load_classes_in_loader(current, AOTLinkedClassCategory::BOOT2, nullptr); // all boot classes outside of java.base - _boot2_completed = true; + Handle h_platform_loader(THREAD, SystemDictionary::java_platform_loader()); + Handle h_system_loader(THREAD, SystemDictionary::java_system_loader()); - load_classes_in_loader(current, AOTLinkedClassCategory::PLATFORM, SystemDictionary::java_platform_loader()); - _platform_completed = true; + assert(h_platform_loader() != nullptr, "must be"); + assert(h_system_loader() != nullptr, "must be"); - load_classes_in_loader(current, AOTLinkedClassCategory::APP, SystemDictionary::java_system_loader()); + AOTLinkedClassTable* table = AOTLinkedClassTable::get(); + link_or_init_classes_for_loader(Handle(), table->boot2(), CHECK); + link_or_init_classes_for_loader(h_platform_loader, table->platform(), CHECK); + link_or_init_classes_for_loader(h_system_loader, table->app(), CHECK); + + if (Universe::is_fully_initialized() && VerifyDuringStartup) { + // Make sure we're still in a clean state. + VM_Verify verify_op; + VMThread::execute(&verify_op); + } if (AOTPrintTrainingInfo) { tty->print_cr("==================== archived_training_data ** after all classes preloaded ===================="); TrainingData::print_archived_training_data_on(tty); } - - _app_completed = true; - AtomicAccess::release_store(&_all_completed, true); } -void AOTLinkedClassBulkLoader::load_classes_in_loader(JavaThread* current, AOTLinkedClassCategory class_category, oop class_loader_oop) { - load_classes_in_loader_impl(class_category, class_loader_oop, current); - if (current->has_pending_exception()) { - // We cannot continue, as we might have loaded some of the aot-linked classes, which - // may have dangling C++ pointers to other aot-linked classes that we have failed to load. - exit_on_exception(current); - } -} +// For the AOT cache to function properly, all classes in the AOTLinkedClassTable +// must be loaded and linked. In addition, AOT-initialized classes must be moved to +// the initialized state. +// +// We can encounter a failure during the loading, linking, or initialization of +// classes in the AOTLinkedClassTable only if: +// - We ran out of memory, +// - There is a serious error in the VM implemenation +// When this happens, the VM may be in an inconsistent state (e.g., we have a cached +// heap object of class X, but X is not linked). We must exit the JVM now. void AOTLinkedClassBulkLoader::exit_on_exception(JavaThread* current) { assert(current->has_pending_exception(), "precondition"); @@ -115,117 +248,6 @@ void AOTLinkedClassBulkLoader::exit_on_exception(JavaThread* current) { vm_exit_during_initialization("Unexpected exception when loading aot-linked classes."); } -void AOTLinkedClassBulkLoader::load_classes_in_loader_impl(AOTLinkedClassCategory class_category, oop class_loader_oop, TRAPS) { - Handle h_loader(THREAD, class_loader_oop); - AOTLinkedClassTable* table = AOTLinkedClassTable::get(); - load_table(table, class_category, h_loader, CHECK); - - // Initialize the InstanceKlasses of all archived heap objects that are reachable from the - // archived java class mirrors. - switch (class_category) { - case AOTLinkedClassCategory::BOOT1: - // Delayed until finish_loading_javabase_classes(), as the VM is not ready to - // execute some of the methods. - break; - case AOTLinkedClassCategory::BOOT2: - init_required_classes_for_loader(h_loader, table->boot2(), CHECK); - break; - case AOTLinkedClassCategory::PLATFORM: - init_required_classes_for_loader(h_loader, table->platform(), CHECK); - break; - case AOTLinkedClassCategory::APP: - init_required_classes_for_loader(h_loader, table->app(), CHECK); - break; - case AOTLinkedClassCategory::UNREGISTERED: - ShouldNotReachHere(); - break; - } - - if (Universe::is_fully_initialized() && VerifyDuringStartup) { - // Make sure we're still in a clean state. - VM_Verify verify_op; - VMThread::execute(&verify_op); - } -} - -void AOTLinkedClassBulkLoader::load_table(AOTLinkedClassTable* table, AOTLinkedClassCategory class_category, Handle loader, TRAPS) { - if (class_category != AOTLinkedClassCategory::BOOT1) { - assert(Universe::is_module_initialized(), "sanity"); - } - - const char* category_name = AOTClassLinker::class_category_name(class_category); - switch (class_category) { - case AOTLinkedClassCategory::BOOT1: - load_classes_impl(class_category, table->boot(), category_name, loader, CHECK); - break; - - case AOTLinkedClassCategory::BOOT2: - load_classes_impl(class_category, table->boot2(), category_name, loader, CHECK); - break; - - case AOTLinkedClassCategory::PLATFORM: - { - initiate_loading(THREAD, category_name, loader, table->boot()); - initiate_loading(THREAD, category_name, loader, table->boot2()); - load_classes_impl(class_category, table->platform(), category_name, loader, CHECK); - } - break; - case AOTLinkedClassCategory::APP: - { - initiate_loading(THREAD, category_name, loader, table->boot()); - initiate_loading(THREAD, category_name, loader, table->boot2()); - initiate_loading(THREAD, category_name, loader, table->platform()); - load_classes_impl(class_category, table->app(), category_name, loader, CHECK); - } - break; - case AOTLinkedClassCategory::UNREGISTERED: - default: - ShouldNotReachHere(); // Currently aot-linked classes are not supported for this category. - break; - } -} - -void AOTLinkedClassBulkLoader::load_classes_impl(AOTLinkedClassCategory class_category, Array* classes, - const char* category_name, Handle loader, TRAPS) { - if (classes == nullptr) { - return; - } - - ClassLoaderData* loader_data = ClassLoaderData::class_loader_data(loader()); - - for (int i = 0; i < classes->length(); i++) { - InstanceKlass* ik = classes->at(i); - if (log_is_enabled(Info, aot, load)) { - ResourceMark rm(THREAD); - log_info(aot, load)("%-5s %s%s%s", category_name, ik->external_name(), - ik->is_loaded() ? " (already loaded)" : "", - ik->is_hidden() ? " (hidden)" : ""); - } - - if (!ik->is_loaded()) { - if (ik->is_hidden()) { - load_hidden_class(loader_data, ik, CHECK); - } else { - InstanceKlass* actual; - if (loader_data == ClassLoaderData::the_null_class_loader_data()) { - actual = SystemDictionary::load_instance_class(ik->name(), loader, CHECK); - } else { - actual = SystemDictionaryShared::find_or_load_shared_class(ik->name(), loader, CHECK); - } - - if (actual != ik) { - ResourceMark rm(THREAD); - log_error(aot)("Unable to resolve %s class from %s: %s", category_name, CDSConfig::type_of_archive_being_loaded(), ik->external_name()); - log_error(aot)("Expected: " INTPTR_FORMAT ", actual: " INTPTR_FORMAT, p2i(ik), p2i(actual)); - log_error(aot)("JVMTI class retransformation is not supported when archive was generated with -XX:+AOTClassLinking."); - AOTMetaspace::unrecoverable_loading_error(); - } - assert(actual->is_loaded(), "must be"); - } - } - } -} - // Initiate loading of the in the . The should have already been loaded // by a parent loader of the . This is necessary for handling pre-resolved CP entries. // @@ -255,7 +277,7 @@ void AOTLinkedClassBulkLoader::initiate_loading(JavaThread* current, const char* if (log_is_enabled(Info, aot, load)) { ResourceMark rm(current); const char* defining_loader = (ik->class_loader() == nullptr ? "boot" : "plat"); - log_info(aot, load)("%s %s (initiated, defined by %s)", category_name, ik->external_name(), + log_info(aot, load)("%-5s %s (initiated, defined by %s)", category_name, ik->external_name(), defining_loader); } SystemDictionary::add_to_initiating_loader(current, ik, loader_data); @@ -263,81 +285,11 @@ void AOTLinkedClassBulkLoader::initiate_loading(JavaThread* current, const char* } } -// Currently, we archive only three types of hidden classes: -// - LambdaForms -// - lambda proxy classes -// - StringConcat classes -// See HeapShared::is_archivable_hidden_klass(). -// -// LambdaForm classes (with names like java/lang/invoke/LambdaForm$MH+0x800000015) logically -// belong to the boot loader, but they are usually stored in their own special ClassLoaderData to -// facilitate class unloading, as a LambdaForm may refer to a class loaded by a custom loader -// that may be unloaded. -// -// We only support AOT-resolution of indys in the boot/platform/app loader, so there's no need -// to support class unloading. For simplicity, we put all archived LambdaForm classes in the -// "main" ClassLoaderData of the boot loader. -// -// (Even if we were to support other loaders, we would still feel free to ignore any requirement -// of class unloading, for any class asset in the AOT cache. Anything that makes it into the AOT -// cache has a lifetime dispensation from unloading. After all, the AOT cache never grows, and -// we can assume that the user is content with its size, and doesn't need its footprint to shrink.) -// -// Lambda proxy classes are normally stored in the same ClassLoaderData as their nest hosts, and -// StringConcat are normally stored in the main ClassLoaderData of the boot class loader. We -// do the same for the archived copies of such classes. -void AOTLinkedClassBulkLoader::load_hidden_class(ClassLoaderData* loader_data, InstanceKlass* ik, TRAPS) { - assert(HeapShared::is_lambda_form_klass(ik) || - HeapShared::is_lambda_proxy_klass(ik) || - HeapShared::is_string_concat_klass(ik), "sanity"); - DEBUG_ONLY({ - assert(ik->super()->is_loaded(), "must be"); - for (int i = 0; i < ik->local_interfaces()->length(); i++) { - assert(ik->local_interfaces()->at(i)->is_loaded(), "must be"); - } - }); - - Handle pd; - PackageEntry* pkg_entry = nullptr; - - // Since a hidden class does not have a name, it cannot be reloaded - // normally via the system dictionary. Instead, we have to finish the - // loading job here. - - if (HeapShared::is_lambda_proxy_klass(ik)) { - InstanceKlass* nest_host = ik->nest_host_not_null(); - assert(nest_host->is_loaded(), "must be"); - pd = Handle(THREAD, nest_host->protection_domain()); - pkg_entry = nest_host->package(); - } - - ik->restore_unshareable_info(loader_data, pd, pkg_entry, CHECK); - SystemDictionary::load_shared_class_misc(ik, loader_data); - ik->add_to_hierarchy(THREAD); - assert(ik->is_loaded(), "Must be in at least loaded state"); - - DEBUG_ONLY({ - // Make sure we don't make this hidden class available by name, even if we don't - // use any special ClassLoaderData. - Handle loader(THREAD, loader_data->class_loader()); - ResourceMark rm(THREAD); - assert(SystemDictionary::resolve_or_null(ik->name(), loader, THREAD) == nullptr, - "hidden classes cannot be accessible by name: %s", ik->external_name()); - if (HAS_PENDING_EXCEPTION) { - CLEAR_PENDING_EXCEPTION; - } - }); -} - -void AOTLinkedClassBulkLoader::finish_loading_javabase_classes(TRAPS) { - init_required_classes_for_loader(Handle(), AOTLinkedClassTable::get()->boot(), CHECK); -} - // Some AOT-linked classes for must be initialized early. This includes // - classes that were AOT-initialized by AOTClassInitializer // - the classes of all objects that are reachable from the archived mirrors of // the AOT-linked classes for . -void AOTLinkedClassBulkLoader::init_required_classes_for_loader(Handle class_loader, Array* classes, TRAPS) { +void AOTLinkedClassBulkLoader::link_or_init_classes_for_loader(Handle class_loader, Array* classes, TRAPS) { if (classes != nullptr) { for (int i = 0; i < classes->length(); i++) { InstanceKlass* ik = classes->at(i); @@ -361,56 +313,6 @@ void AOTLinkedClassBulkLoader::init_required_classes_for_loader(Handle class_loa HeapShared::init_classes_for_special_subgraph(class_loader, CHECK); } -bool AOTLinkedClassBulkLoader::is_pending_aot_linked_class(Klass* k) { - if (!CDSConfig::is_using_aot_linked_classes()) { - return false; - } - - if (_all_completed) { // no more pending aot-linked classes - return false; - } - - if (k->is_objArray_klass()) { - k = ObjArrayKlass::cast(k)->bottom_klass(); - } - if (!k->is_instance_klass()) { - // type array klasses (and their higher dimensions), - // must have been loaded before a GC can ever happen. - return false; - } - - // There's a small window during VM start-up where a not-yet loaded aot-linked - // class k may be discovered by the GC during VM initialization. This can happen - // when the heap contains an aot-cached instance of k, but k is not ready to be - // loaded yet. (TODO: JDK-8342429 eliminates this possibility) - // - // The following checks try to limit this window as much as possible for each of - // the four AOTLinkedClassCategory of classes that can be aot-linked. - - InstanceKlass* ik = InstanceKlass::cast(k); - if (ik->defined_by_boot_loader()) { - if (ik->module() != nullptr && ik->in_javabase_module()) { - // AOTLinkedClassCategory::BOOT1 -- all aot-linked classes in - // java.base must have been loaded before a GC can ever happen. - return false; - } else { - // AOTLinkedClassCategory::BOOT2 classes cannot be loaded until - // module system is ready. - return !_boot2_completed; - } - } else if (ik->defined_by_platform_loader()) { - // AOTLinkedClassCategory::PLATFORM classes cannot be loaded until - // the platform class loader is initialized. - return !_platform_completed; - } else if (ik->defined_by_app_loader()) { - // AOTLinkedClassCategory::APP cannot be loaded until the app class loader - // is initialized. - return !_app_completed; - } else { - return false; - } -} - void AOTLinkedClassBulkLoader::replay_training_at_init(Array* classes, TRAPS) { if (classes != nullptr) { for (int i = 0; i < classes->length(); i++) { @@ -425,7 +327,7 @@ void AOTLinkedClassBulkLoader::replay_training_at_init(Array* cl void AOTLinkedClassBulkLoader::replay_training_at_init_for_preloaded_classes(TRAPS) { if (CDSConfig::is_using_aot_linked_classes() && TrainingData::have_data()) { AOTLinkedClassTable* table = AOTLinkedClassTable::get(); - replay_training_at_init(table->boot(), CHECK); + replay_training_at_init(table->boot1(), CHECK); replay_training_at_init(table->boot2(), CHECK); replay_training_at_init(table->platform(), CHECK); replay_training_at_init(table->app(), CHECK); diff --git a/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp b/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp index 95e64a7ddd4..77400a86104 100644 --- a/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp +++ b/src/hotspot/share/cds/aotLinkedClassBulkLoader.hpp @@ -39,34 +39,40 @@ template class Array; enum class AOTLinkedClassCategory : int; // During a Production Run, the AOTLinkedClassBulkLoader loads all classes from -// a AOTLinkedClassTable into their respective ClassLoaders. This happens very early -// in the JVM bootstrap stage, before any application code is executed. +// the AOTLinkedClassTable into their respective ClassLoaders. This happens very early +// in the JVM bootstrap stage, before any Java bytecode is executed. // +// IMPLEMENTATION NOTES: +// We also proactively link all the classes in the AOTLinkedClassTable, and move +// the AOT-initialized classes to the "initialized" state. Due to limitations +// of the current JVM bootstrap sequence, link_or_init_javabase_classes() and +// link_or_init_non_javabase_classes() need to be called after some Java bytecodes are +// executed. Future RFEs will move these calls to earlier stages. class AOTLinkedClassBulkLoader : AllStatic { - static bool _boot2_completed; - static bool _platform_completed; - static bool _app_completed; - static bool _all_completed; - static void load_classes_in_loader(JavaThread* current, AOTLinkedClassCategory class_category, oop class_loader_oop); - static void load_classes_in_loader_impl(AOTLinkedClassCategory class_category, oop class_loader_oop, TRAPS); - static void load_table(AOTLinkedClassTable* table, AOTLinkedClassCategory class_category, Handle loader, TRAPS); - static void initiate_loading(JavaThread* current, const char* category, Handle initiating_loader, Array* classes); - static void load_classes_impl(AOTLinkedClassCategory class_category, Array* classes, - const char* category_name, Handle loader, TRAPS); - static void load_hidden_class(ClassLoaderData* loader_data, InstanceKlass* ik, TRAPS); - static void init_required_classes_for_loader(Handle class_loader, Array* classes, TRAPS); + static void preload_classes_impl(TRAPS); + static void preload_classes_in_table(Array* classes, + const char* category_name, Handle loader, TRAPS); + static void initiate_loading(JavaThread* current, const char* category, Handle initiating_loader, + Array* classes); + static void link_or_init_non_javabase_classes_impl(TRAPS); + static void link_or_init_classes_for_loader(Handle class_loader, Array* classes, TRAPS); static void replay_training_at_init(Array* classes, TRAPS) NOT_CDS_RETURN; + +#ifdef ASSERT + static void validate_module_of_preloaded_classes(); + static void validate_module_of_preloaded_classes_in_table(Array* classes, + const char* category_name, Handle loader); + static void validate_module(Klass* k, const char* category_name, oop module_oop); +#endif + public: static void serialize(SerializeClosure* soc) NOT_CDS_RETURN; - - static void load_javabase_classes(JavaThread* current) NOT_CDS_RETURN; - static void load_non_javabase_classes(JavaThread* current) NOT_CDS_RETURN; - static void finish_loading_javabase_classes(TRAPS) NOT_CDS_RETURN; + static void preload_classes(JavaThread* current); + static void link_or_init_javabase_classes(JavaThread* current) NOT_CDS_RETURN; + static void link_or_init_non_javabase_classes(JavaThread* current) NOT_CDS_RETURN; static void exit_on_exception(JavaThread* current); static void replay_training_at_init_for_preloaded_classes(TRAPS) NOT_CDS_RETURN; - static bool class_preloading_finished(); - static bool is_pending_aot_linked_class(Klass* k) NOT_CDS_RETURN_(false); }; #endif // SHARE_CDS_AOTLINKEDCLASSBULKLOADER_HPP diff --git a/src/hotspot/share/cds/aotLinkedClassTable.cpp b/src/hotspot/share/cds/aotLinkedClassTable.cpp index 79d78b05be1..a16bdec8740 100644 --- a/src/hotspot/share/cds/aotLinkedClassTable.cpp +++ b/src/hotspot/share/cds/aotLinkedClassTable.cpp @@ -30,7 +30,7 @@ AOTLinkedClassTable AOTLinkedClassTable::_instance; void AOTLinkedClassTable::serialize(SerializeClosure* soc) { - soc->do_ptr((void**)&_boot); + soc->do_ptr((void**)&_boot1); soc->do_ptr((void**)&_boot2); soc->do_ptr((void**)&_platform); soc->do_ptr((void**)&_app); diff --git a/src/hotspot/share/cds/aotLinkedClassTable.hpp b/src/hotspot/share/cds/aotLinkedClassTable.hpp index 0ec733d1df7..a33e53b1d6a 100644 --- a/src/hotspot/share/cds/aotLinkedClassTable.hpp +++ b/src/hotspot/share/cds/aotLinkedClassTable.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,26 +41,27 @@ class SerializeClosure; class AOTLinkedClassTable { static AOTLinkedClassTable _instance; - Array* _boot; // only java.base classes - Array* _boot2; // boot classes in other modules + Array* _boot1; // boot classes in java.base module + Array* _boot2; // boot classes in all other (named and unnamed) modules, + // including classes from -Xbootclasspath/a Array* _platform; Array* _app; public: AOTLinkedClassTable() : - _boot(nullptr), _boot2(nullptr), + _boot1(nullptr), _boot2(nullptr), _platform(nullptr), _app(nullptr) {} static AOTLinkedClassTable* get() { return &_instance; } - Array* boot() const { return _boot; } + Array* boot1() const { return _boot1; } Array* boot2() const { return _boot2; } Array* platform() const { return _platform; } Array* app() const { return _app; } - void set_boot (Array* value) { _boot = value; } + void set_boot1 (Array* value) { _boot1 = value; } void set_boot2 (Array* value) { _boot2 = value; } void set_platform(Array* value) { _platform = value; } void set_app (Array* value) { _app = value; } diff --git a/src/hotspot/share/cds/aotMetaspace.cpp b/src/hotspot/share/cds/aotMetaspace.cpp index f866461e7d4..6035111416a 100644 --- a/src/hotspot/share/cds/aotMetaspace.cpp +++ b/src/hotspot/share/cds/aotMetaspace.cpp @@ -124,7 +124,7 @@ bool AOTMetaspace::_use_optimized_module_handling = true; // These regions are aligned with AOTMetaspace::core_region_alignment(). // // These 2 regions are populated in the following steps: -// [0] All classes are loaded in AOTMetaspace::preload_classes(). All metadata are +// [0] All classes are loaded in AOTMetaspace::load_classes(). All metadata are // temporarily allocated outside of the shared regions. // [1] We enter a safepoint and allocate a buffer for the rw/ro regions. // [2] C++ vtables are copied into the rw region. @@ -823,12 +823,10 @@ void AOTMetaspace::link_shared_classes(TRAPS) { } } -// Preload classes from a list, populate the shared spaces and dump to a -// file. -void AOTMetaspace::preload_and_dump(TRAPS) { +void AOTMetaspace::dump_static_archive(TRAPS) { CDSConfig::DumperThreadMark dumper_thread_mark(THREAD); ResourceMark rm(THREAD); - HandleMark hm(THREAD); + HandleMark hm(THREAD); if (CDSConfig::is_dumping_final_static_archive() && AOTPrintTrainingInfo) { tty->print_cr("==================== archived_training_data ** before dumping ===================="); @@ -836,7 +834,7 @@ void AOTMetaspace::preload_and_dump(TRAPS) { } StaticArchiveBuilder builder; - preload_and_dump_impl(builder, THREAD); + dump_static_archive_impl(builder, THREAD); if (HAS_PENDING_EXCEPTION) { if (PENDING_EXCEPTION->is_a(vmClasses::OutOfMemoryError_klass())) { aot_log_error(aot)("Out of memory. Please run with a larger Java heap, current MaxHeapSize = " @@ -902,7 +900,7 @@ void AOTMetaspace::get_default_classlist(char* default_classlist, const size_t b Arguments::get_java_home(), filesep, filesep); } -void AOTMetaspace::preload_classes(TRAPS) { +void AOTMetaspace::load_classes(TRAPS) { char default_classlist[JVM_MAXPATHLEN]; const char* classlist_path; @@ -929,8 +927,8 @@ void AOTMetaspace::preload_classes(TRAPS) { } } - // Some classes are used at CDS runtime but are not loaded, and therefore archived, at - // dumptime. We can perform dummmy calls to these classes at dumptime to ensure they + // Some classes are used at CDS runtime but are not yet loaded at this point. + // We can perform dummmy calls to these classes at dumptime to ensure they // are archived. exercise_runtime_cds_code(CHECK); @@ -946,10 +944,10 @@ void AOTMetaspace::exercise_runtime_cds_code(TRAPS) { CDSProtectionDomain::to_file_URL("dummy.jar", Handle(), CHECK); } -void AOTMetaspace::preload_and_dump_impl(StaticArchiveBuilder& builder, TRAPS) { +void AOTMetaspace::dump_static_archive_impl(StaticArchiveBuilder& builder, TRAPS) { if (CDSConfig::is_dumping_classic_static_archive()) { // We are running with -Xshare:dump - preload_classes(CHECK); + load_classes(CHECK); if (SharedArchiveConfigFile) { log_info(aot)("Reading extra data from %s ...", SharedArchiveConfigFile); diff --git a/src/hotspot/share/cds/aotMetaspace.hpp b/src/hotspot/share/cds/aotMetaspace.hpp index 1803199766d..379c684e939 100644 --- a/src/hotspot/share/cds/aotMetaspace.hpp +++ b/src/hotspot/share/cds/aotMetaspace.hpp @@ -72,15 +72,15 @@ class AOTMetaspace : AllStatic { n_regions = 5 // total number of regions }; - static void preload_and_dump(TRAPS) NOT_CDS_RETURN; + static void dump_static_archive(TRAPS) NOT_CDS_RETURN; #ifdef _LP64 static void adjust_heap_sizes_for_dumping() NOT_CDS_JAVA_HEAP_RETURN; #endif private: static void exercise_runtime_cds_code(TRAPS) NOT_CDS_RETURN; - static void preload_and_dump_impl(StaticArchiveBuilder& builder, TRAPS) NOT_CDS_RETURN; - static void preload_classes(TRAPS) NOT_CDS_RETURN; + static void dump_static_archive_impl(StaticArchiveBuilder& builder, TRAPS) NOT_CDS_RETURN; + static void load_classes(TRAPS) NOT_CDS_RETURN; public: static Symbol* symbol_rs_base() { diff --git a/src/hotspot/share/cds/aotOopChecker.cpp b/src/hotspot/share/cds/aotOopChecker.cpp new file mode 100644 index 00000000000..3c0a142a059 --- /dev/null +++ b/src/hotspot/share/cds/aotOopChecker.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "cds/aotMetaspace.hpp" +#include "cds/aotOopChecker.hpp" +#include "cds/heapShared.hpp" +#include "classfile/javaClasses.hpp" +#include "classfile/symbolTable.hpp" +#include "classfile/vmClasses.hpp" +#include "oops/instanceKlass.inline.hpp" +#include "runtime/fieldDescriptor.inline.hpp" +#include "utilities/debug.hpp" + +#if INCLUDE_CDS_JAVA_HEAP + +oop AOTOopChecker::get_oop_field(oop obj, const char* name, const char* sig) { + Symbol* name_sym = SymbolTable::probe(name, checked_cast(strlen(name))); + assert(name_sym != nullptr, "Symbol must have been resolved for an existing field of this obj"); + Symbol* sig_sym = SymbolTable::probe(sig, checked_cast(strlen(sig))); + assert(sig_sym != nullptr, "Symbol must have been resolved for an existing field of this obj"); + + fieldDescriptor fd; + Klass* k = InstanceKlass::cast(obj->klass())->find_field(name_sym, sig_sym, &fd); + assert(k != nullptr, "field must exist"); + precond(!fd.is_static()); + precond(fd.field_type() == T_OBJECT || fd.field_type() == T_ARRAY); + return obj->obj_field(fd.offset()); +} + +// Make sure we are not caching objects with assumptions that can be violated in +// the production run. +void AOTOopChecker::check(oop obj) { + // Currently we only check URL objects, but more rules may be added in the future. + + if (obj->klass()->is_subclass_of(vmClasses::URL_klass())) { + // If URL could be subclassed, obj may have new fields that we don't know about. + precond(vmClasses::URL_klass()->is_final()); + + // URLs are referenced by the CodeSources/ProtectDomains that are cached + // for AOT-linked classes loaded by the platform/app loaders. + // + // Do not cache any URLs whose URLStreamHandler can be overridden by the application. + // - "jrt" and "file" will always use the built-in URLStreamHandler. See + // java.net.URL::isOverrideable(). + // - When an AOT-linked class is loaded from a JAR file, its URL is something + // like file:HelloWorl.jar, and does NOT use the "jar" protocol. + oop protocol = get_oop_field(obj, "protocol", "Ljava/lang/String;"); + if (!java_lang_String::equals(protocol, "jrt", 3) && + !java_lang_String::equals(protocol, "file", 4)) { + ResourceMark rm; + log_error(aot)("Must cache only URLs with jrt/file protocols but got: %s", + java_lang_String::as_quoted_ascii(protocol)); + HeapShared::debug_trace(); + AOTMetaspace::unrecoverable_writing_error(); + } + } +} + +#endif //INCLUDE_CDS_JAVA_HEAP diff --git a/src/hotspot/share/cds/aotOopChecker.hpp b/src/hotspot/share/cds/aotOopChecker.hpp new file mode 100644 index 00000000000..1d4b9cd1a75 --- /dev/null +++ b/src/hotspot/share/cds/aotOopChecker.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#ifndef SHARE_CDS_AOTOOPCHECKER_HPP +#define SHARE_CDS_AOTOOPCHECKER_HPP + +#include "memory/allStatic.hpp" +#include "oops/oopsHierarchy.hpp" + +class AOTOopChecker : AllStatic { + static oop get_oop_field(oop obj, const char* name, const char* sig); + +public: + // obj is an object that's about to be stored into the AOT cache. Check if it + // can be safely cached. + static void check(oop obj); +}; + +#endif // SHARE_CDS_AOTOOPCHECKER_HPP diff --git a/src/hotspot/share/cds/cdsConfig.cpp b/src/hotspot/share/cds/cdsConfig.cpp index 5bb46deb9bc..7c6b925470a 100644 --- a/src/hotspot/share/cds/cdsConfig.cpp +++ b/src/hotspot/share/cds/cdsConfig.cpp @@ -67,7 +67,8 @@ JavaThread* CDSConfig::_dumper_thread = nullptr; int CDSConfig::get_status() { assert(Universe::is_fully_initialized(), "status is finalized only after Universe is initialized"); - return (is_dumping_archive() ? IS_DUMPING_ARCHIVE : 0) | + return (is_dumping_aot_linked_classes() ? IS_DUMPING_AOT_LINKED_CLASSES : 0) | + (is_dumping_archive() ? IS_DUMPING_ARCHIVE : 0) | (is_dumping_method_handles() ? IS_DUMPING_METHOD_HANDLES : 0) | (is_dumping_static_archive() ? IS_DUMPING_STATIC_ARCHIVE : 0) | (is_logging_lambda_form_invokers() ? IS_LOGGING_LAMBDA_FORM_INVOKERS : 0) | diff --git a/src/hotspot/share/cds/cdsConfig.hpp b/src/hotspot/share/cds/cdsConfig.hpp index cfa846f2b41..d199e97eefd 100644 --- a/src/hotspot/share/cds/cdsConfig.hpp +++ b/src/hotspot/share/cds/cdsConfig.hpp @@ -80,11 +80,12 @@ class CDSConfig : public AllStatic { public: // Used by jdk.internal.misc.CDS.getCDSConfigStatus(); - static const int IS_DUMPING_ARCHIVE = 1 << 0; - static const int IS_DUMPING_METHOD_HANDLES = 1 << 1; - static const int IS_DUMPING_STATIC_ARCHIVE = 1 << 2; - static const int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 3; - static const int IS_USING_ARCHIVE = 1 << 4; + static const int IS_DUMPING_AOT_LINKED_CLASSES = 1 << 0; + static const int IS_DUMPING_ARCHIVE = 1 << 1; + static const int IS_DUMPING_METHOD_HANDLES = 1 << 2; + static const int IS_DUMPING_STATIC_ARCHIVE = 1 << 3; + static const int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 4; + static const int IS_USING_ARCHIVE = 1 << 5; static int get_status() NOT_CDS_RETURN_(0); diff --git a/src/hotspot/share/cds/cdsEnumKlass.cpp b/src/hotspot/share/cds/cdsEnumKlass.cpp index e46a7eb84f4..f771eeec3d7 100644 --- a/src/hotspot/share/cds/cdsEnumKlass.cpp +++ b/src/hotspot/share/cds/cdsEnumKlass.cpp @@ -40,7 +40,9 @@ bool CDSEnumKlass::is_enum_obj(oop orig_obj) { InstanceKlass::cast(k)->is_enum_subclass(); } -// -- Handling of Enum objects +// !!! This is legacy support for enum classes before JEP 483. This file is not used when +// !!! CDSConfig::is_initing_classes_at_dump_time()==true. +// // Java Enum classes have synthetic methods that look like this // enum MyEnum {FOO, BAR} // MyEnum:: { @@ -62,6 +64,7 @@ bool CDSEnumKlass::is_enum_obj(oop orig_obj) { void CDSEnumKlass::handle_enum_obj(int level, KlassSubGraphInfo* subgraph_info, oop orig_obj) { + assert(!CDSConfig::is_initing_classes_at_dump_time(), "only for legacy support of enums"); assert(level > 1, "must never be called at the first (outermost) level"); assert(is_enum_obj(orig_obj), "must be"); diff --git a/src/hotspot/share/cds/cdsEnumKlass.hpp b/src/hotspot/share/cds/cdsEnumKlass.hpp index c898bfec60d..e6019ff705e 100644 --- a/src/hotspot/share/cds/cdsEnumKlass.hpp +++ b/src/hotspot/share/cds/cdsEnumKlass.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,6 +34,8 @@ class InstanceKlass; class JavaFieldStream; class KlassSubGraphInfo; +// This is legacy support for enum classes before JEP 483. This code is not needed when +// CDSConfig::is_initing_classes_at_dump_time()==true. class CDSEnumKlass: AllStatic { public: static bool is_enum_obj(oop orig_obj); diff --git a/src/hotspot/share/cds/cdsHeapVerifier.cpp b/src/hotspot/share/cds/cdsHeapVerifier.cpp index 9429a0d8264..3f72a7c6872 100644 --- a/src/hotspot/share/cds/cdsHeapVerifier.cpp +++ b/src/hotspot/share/cds/cdsHeapVerifier.cpp @@ -100,7 +100,7 @@ CDSHeapVerifier::CDSHeapVerifier() : _archived_objs(0), _problems(0) // you might need to fix the core library code, or fix the ADD_EXCL entries below. // // class field type - ADD_EXCL("java/lang/ClassLoader", "scl"); // A + ADD_EXCL("java/lang/ClassLoader$Holder", "scl"); // A ADD_EXCL("java/lang/Module", "ALL_UNNAMED_MODULE", // A "ALL_UNNAMED_MODULE_SET", // A "EVERYONE_MODULE", // A @@ -147,6 +147,10 @@ CDSHeapVerifier::CDSHeapVerifier() : _archived_objs(0), _problems(0) "ZERO"); // D } + if (CDSConfig::is_dumping_aot_linked_classes()) { + ADD_EXCL("java/lang/Package$VersionInfo", "NULL_VERSION_INFO"); // D + } + # undef ADD_EXCL ClassLoaderDataGraph::classes_do(this); @@ -228,10 +232,16 @@ public: return; } - if (field_ik == vmClasses::internal_Unsafe_klass() && ArchiveUtils::has_aot_initialized_mirror(field_ik)) { - // There's only a single instance of jdk/internal/misc/Unsafe, so all references will - // be pointing to this singleton, which has been archived. - return; + if (ArchiveUtils::has_aot_initialized_mirror(field_ik)) { + if (field_ik == vmClasses::internal_Unsafe_klass()) { + // There's only a single instance of jdk/internal/misc/Unsafe, so all references will + // be pointing to this singleton, which has been archived. + return; + } + if (field_ik == vmClasses::Boolean_klass()) { + // TODO: check if is TRUE or FALSE + return; + } } } diff --git a/src/hotspot/share/cds/dynamicArchive.cpp b/src/hotspot/share/cds/dynamicArchive.cpp index dd24f1e0c51..6fac1676b9f 100644 --- a/src/hotspot/share/cds/dynamicArchive.cpp +++ b/src/hotspot/share/cds/dynamicArchive.cpp @@ -438,8 +438,6 @@ void DynamicArchive::setup_array_klasses() { if (_dynamic_archive_array_klasses != nullptr) { for (int i = 0; i < _dynamic_archive_array_klasses->length(); i++) { ObjArrayKlass* oak = _dynamic_archive_array_klasses->at(i); - assert(!oak->is_typeArray_klass(), "all type array classes must be in static archive"); - Klass* elm = oak->element_klass(); assert(AOTMetaspace::in_aot_cache_static_region((void*)elm), "must be"); diff --git a/src/hotspot/share/cds/heapShared.cpp b/src/hotspot/share/cds/heapShared.cpp index 92f55ce5b33..de4d1c8a729 100644 --- a/src/hotspot/share/cds/heapShared.cpp +++ b/src/hotspot/share/cds/heapShared.cpp @@ -27,6 +27,7 @@ #include "cds/aotClassLocation.hpp" #include "cds/aotLogging.hpp" #include "cds/aotMetaspace.hpp" +#include "cds/aotOopChecker.hpp" #include "cds/aotReferenceObjSupport.hpp" #include "cds/archiveBuilder.hpp" #include "cds/archiveHeapLoader.hpp" @@ -325,6 +326,8 @@ bool HeapShared::archive_object(oop obj, oop referrer, KlassSubGraphInfo* subgra debug_trace(); return false; } else { + AOTOopChecker::check(obj); // Make sure contents of this oop are safe. + count_allocation(obj->size()); ArchiveHeapWriter::add_source_obj(obj); CachedOopInfo info = make_cached_oop_info(obj, referrer); @@ -612,7 +615,7 @@ void HeapShared::copy_and_rescan_aot_inited_mirror(InstanceKlass* ik) { } } -static void copy_java_mirror_hashcode(oop orig_mirror, oop scratch_m) { +void HeapShared::copy_java_mirror(oop orig_mirror, oop scratch_m) { // We need to retain the identity_hash, because it may have been used by some hashtables // in the shared heap. if (!orig_mirror->fast_no_hash_check()) { @@ -628,6 +631,11 @@ static void copy_java_mirror_hashcode(oop orig_mirror, oop scratch_m) { DEBUG_ONLY(intptr_t archived_hash = scratch_m->identity_hash()); assert(src_hash == archived_hash, "Different hash codes: original " INTPTR_FORMAT ", archived " INTPTR_FORMAT, src_hash, archived_hash); } + + if (CDSConfig::is_dumping_aot_linked_classes()) { + java_lang_Class::set_module(scratch_m, java_lang_Class::module(orig_mirror)); + java_lang_Class::set_protection_domain(scratch_m, java_lang_Class::protection_domain(orig_mirror)); + } } static objArrayOop get_archived_resolved_references(InstanceKlass* src_ik) { @@ -727,7 +735,7 @@ void HeapShared::write_heap(ArchiveHeapInfo *heap_info) { void HeapShared::scan_java_mirror(oop orig_mirror) { oop m = scratch_java_mirror(orig_mirror); if (m != nullptr) { // nullptr if for custom class loader - copy_java_mirror_hashcode(orig_mirror, m); + copy_java_mirror(orig_mirror, m); bool success = archive_reachable_objects_from(1, _dump_time_special_subgraph, m); assert(success, "sanity"); } @@ -1638,9 +1646,11 @@ bool HeapShared::walk_one_object(PendingOopStack* stack, int level, KlassSubGrap } if (CDSConfig::is_initing_classes_at_dump_time()) { - // The enum klasses are archived with aot-initialized mirror. - // See AOTClassInitializer::can_archive_initialized_mirror(). + // The classes of all archived enum instances have been marked as aot-init, + // so there's nothing else to be done in the production run. } else { + // This is legacy support for enum classes before JEP 483 -- we cannot rerun + // the enum's in the production run, so special handling is needed. if (CDSEnumKlass::is_enum_obj(orig_obj)) { CDSEnumKlass::handle_enum_obj(level + 1, subgraph_info, orig_obj); } diff --git a/src/hotspot/share/cds/heapShared.hpp b/src/hotspot/share/cds/heapShared.hpp index c9a810a6c0b..c877748fe9c 100644 --- a/src/hotspot/share/cds/heapShared.hpp +++ b/src/hotspot/share/cds/heapShared.hpp @@ -343,6 +343,7 @@ private: static void prepare_resolved_references(); static void archive_strings(); static void archive_subgraphs(); + static void copy_java_mirror(oop orig_mirror, oop scratch_m); // PendingOop and PendingOopStack are used for recursively discovering all cacheable // heap objects. The recursion is done using PendingOopStack so we won't overflow the diff --git a/src/hotspot/share/classfile/classLoaderDataShared.cpp b/src/hotspot/share/classfile/classLoaderDataShared.cpp index a495327864d..2a59a3339b6 100644 --- a/src/hotspot/share/classfile/classLoaderDataShared.cpp +++ b/src/hotspot/share/classfile/classLoaderDataShared.cpp @@ -280,4 +280,17 @@ void ClassLoaderDataShared::restore_java_system_loader_from_archive(ClassLoaderD _full_module_graph_loaded = true; } +// This is called before AOTLinkedClassBulkLoader starts preloading classes. It makes sure that +// when we preload any class, its module is already valid. +void ClassLoaderDataShared::restore_archived_modules_for_preloading_classes(JavaThread* current) { + precond(CDSConfig::is_using_aot_linked_classes()); + + precond(_platform_loader_root_index >= 0); + precond(_system_loader_root_index >= 0); + + Handle h_platform_loader(current, HeapShared::get_root(_platform_loader_root_index)); + Handle h_system_loader(current, HeapShared::get_root(_system_loader_root_index)); + Modules::init_archived_modules(current, h_platform_loader, h_system_loader); +} + #endif // INCLUDE_CDS_JAVA_HEAP diff --git a/src/hotspot/share/classfile/classLoaderDataShared.hpp b/src/hotspot/share/classfile/classLoaderDataShared.hpp index 6ef338f0f34..944d415af5c 100644 --- a/src/hotspot/share/classfile/classLoaderDataShared.hpp +++ b/src/hotspot/share/classfile/classLoaderDataShared.hpp @@ -27,6 +27,7 @@ #include "memory/allStatic.hpp" #include "oops/oopsHierarchy.hpp" +#include "utilities/macros.hpp" class ClassLoaderData; class MetaspaceClosure; @@ -37,6 +38,7 @@ class ClassLoaderDataShared : AllStatic { static bool _full_module_graph_loaded; CDS_JAVA_HEAP_ONLY(static void ensure_module_entry_table_exists(oop class_loader);) public: + static void restore_archived_modules_for_preloading_classes(JavaThread* current) NOT_CDS_JAVA_HEAP_RETURN; #if INCLUDE_CDS_JAVA_HEAP static void ensure_module_entry_tables_exist(); static void allocate_archived_tables(); diff --git a/src/hotspot/share/classfile/javaClasses.cpp b/src/hotspot/share/classfile/javaClasses.cpp index 60a63892518..75f54bfa549 100644 --- a/src/hotspot/share/classfile/javaClasses.cpp +++ b/src/hotspot/share/classfile/javaClasses.cpp @@ -1013,12 +1013,25 @@ void java_lang_Class::initialize_mirror_fields(InstanceKlass* ik, // Set the java.lang.Module module field in the java_lang_Class mirror void java_lang_Class::set_mirror_module_field(JavaThread* current, Klass* k, Handle mirror, Handle module) { + if (CDSConfig::is_using_aot_linked_classes()) { + oop archived_module = java_lang_Class::module(mirror()); + if (archived_module != nullptr) { + precond(module() == nullptr || module() == archived_module); + precond(AOTMetaspace::in_aot_cache_static_region((void*)k)); + return; + } + } + if (module.is_null()) { // During startup, the module may be null only if java.base has not been defined yet. // Put the class on the fixup_module_list to patch later when the java.lang.Module // for java.base is known. But note that since we captured the null module another // thread may have completed that initialization. + // With AOT-linked classes, java.base should have been defined before the + // VM loads any classes. + precond(!CDSConfig::is_using_aot_linked_classes()); + bool javabase_was_defined = false; { MutexLocker m1(current, Module_lock); @@ -1052,13 +1065,19 @@ void java_lang_Class::set_mirror_module_field(JavaThread* current, Klass* k, Han // Statically allocate fixup lists because they always get created. void java_lang_Class::allocate_fixup_lists() { - GrowableArray* mirror_list = - new (mtClass) GrowableArray(40, mtClass); - set_fixup_mirror_list(mirror_list); + if (!CDSConfig::is_using_aot_linked_classes()) { + // fixup_mirror_list() is not used when we have preloaded classes. See + // Universe::fixup_mirrors(). + GrowableArray* mirror_list = + new (mtClass) GrowableArray(40, mtClass); + set_fixup_mirror_list(mirror_list); - GrowableArray* module_list = - new (mtModule) GrowableArray(500, mtModule); - set_fixup_module_field_list(module_list); + // With AOT-linked classes, java.base module is defined before any class + // is loaded, so there's no need for fixup_module_field_list(). + GrowableArray* module_list = + new (mtModule) GrowableArray(500, mtModule); + set_fixup_module_field_list(module_list); + } } void java_lang_Class::allocate_mirror(Klass* k, bool is_scratch, Handle protection_domain, Handle classData, @@ -1158,6 +1177,7 @@ void java_lang_Class::create_mirror(Klass* k, Handle class_loader, create_scratch_mirror(k, CHECK); } } else { + assert(!CDSConfig::is_using_aot_linked_classes(), "should not come here"); assert(fixup_mirror_list() != nullptr, "fixup_mirror_list not initialized"); fixup_mirror_list()->push(k); } @@ -1203,7 +1223,7 @@ bool java_lang_Class::restore_archived_mirror(Klass *k, Handle protection_domain, TRAPS) { // Postpone restoring archived mirror until java.lang.Class is loaded. Please // see more details in vmClasses::resolve_all(). - if (!vmClasses::Class_klass_loaded()) { + if (!vmClasses::Class_klass_loaded() && !CDSConfig::is_using_aot_linked_classes()) { assert(fixup_mirror_list() != nullptr, "fixup_mirror_list not initialized"); fixup_mirror_list()->push(k); return true; diff --git a/src/hotspot/share/classfile/javaClasses.hpp b/src/hotspot/share/classfile/javaClasses.hpp index b137f1a8035..750f27fcf47 100644 --- a/src/hotspot/share/classfile/javaClasses.hpp +++ b/src/hotspot/share/classfile/javaClasses.hpp @@ -234,6 +234,7 @@ class java_lang_String : AllStatic { class java_lang_Class : AllStatic { friend class VMStructs; friend class JVMCIVMStructs; + friend class HeapShared; private: diff --git a/src/hotspot/share/classfile/moduleEntry.cpp b/src/hotspot/share/classfile/moduleEntry.cpp index ce887081cdb..88b59540d04 100644 --- a/src/hotspot/share/classfile/moduleEntry.cpp +++ b/src/hotspot/share/classfile/moduleEntry.cpp @@ -697,6 +697,8 @@ void ModuleEntryTable::finalize_javabase(Handle module_handle, Symbol* version, // classes needing their module field set are added to the fixup_module_list. // Their module field is set once java.base's java.lang.Module is known to the VM. void ModuleEntryTable::patch_javabase_entries(JavaThread* current, Handle module_handle) { + assert(!CDSConfig::is_using_aot_linked_classes(), "patching is not necessary with AOT-linked classes"); + if (module_handle.is_null()) { fatal("Unable to patch the module field of classes loaded prior to " JAVA_BASE_NAME "'s definition, invalid java.lang.Module"); diff --git a/src/hotspot/share/classfile/modules.cpp b/src/hotspot/share/classfile/modules.cpp index 062521e7495..522d1d0842d 100644 --- a/src/hotspot/share/classfile/modules.cpp +++ b/src/hotspot/share/classfile/modules.cpp @@ -700,6 +700,26 @@ void Modules::serialize_archived_module_info(SerializeClosure* soc) { void Modules::define_archived_modules(Handle h_platform_loader, Handle h_system_loader, TRAPS) { assert(CDSConfig::is_using_full_module_graph(), "must be"); + if (h_platform_loader.is_null()) { + THROW_MSG(vmSymbols::java_lang_NullPointerException(), "Null platform loader object"); + } + + if (h_system_loader.is_null()) { + THROW_MSG(vmSymbols::java_lang_NullPointerException(), "Null system loader object"); + } + + if (CDSConfig::is_using_aot_linked_classes()) { + // Already initialized + precond(SystemDictionary::java_platform_loader() == h_platform_loader()); + precond(SystemDictionary::java_system_loader() == h_system_loader()); + } else { + init_archived_modules(THREAD, h_platform_loader, h_system_loader); + } +} + +void Modules::init_archived_modules(JavaThread* current, Handle h_platform_loader, Handle h_system_loader) { + assert(CDSConfig::is_using_full_module_graph(), "must be"); + ExceptionMark em(current); // We don't want the classes used by the archived full module graph to be redefined by JVMTI. // Luckily, such classes are loaded in the JVMTI "early" phase, and CDS is disabled if a JVMTI @@ -708,16 +728,15 @@ void Modules::define_archived_modules(Handle h_platform_loader, Handle h_system_ assert(!(JvmtiExport::should_post_class_file_load_hook() && JvmtiExport::has_early_class_hook_env()), "CDS should be disabled if early class hooks are enabled"); - Handle java_base_module(THREAD, ClassLoaderDataShared::restore_archived_oops_for_null_class_loader_data()); - // Patch any previously loaded class's module field with java.base's java.lang.Module. - ModuleEntryTable::patch_javabase_entries(THREAD, java_base_module); - - if (h_platform_loader.is_null()) { - THROW_MSG(vmSymbols::java_lang_NullPointerException(), "Null platform loader object"); + if (CDSConfig::is_using_aot_linked_classes()) { + ClassLoaderData* boot_loader_data = ClassLoaderData::the_null_class_loader_data(); + ClassLoaderDataShared::archived_boot_unnamed_module()->restore_archived_oops(boot_loader_data); } - if (h_system_loader.is_null()) { - THROW_MSG(vmSymbols::java_lang_NullPointerException(), "Null system loader object"); + Handle java_base_module(current, ClassLoaderDataShared::restore_archived_oops_for_null_class_loader_data()); + if (!CDSConfig::is_using_aot_linked_classes()) { + // Patch any previously loaded class's module field with java.base's java.lang.Module. + ModuleEntryTable::patch_javabase_entries(current, java_base_module); } ClassLoaderData* platform_loader_data = SystemDictionary::register_loader(h_platform_loader); @@ -777,7 +796,9 @@ void Modules::set_bootloader_unnamed_module(Handle module, TRAPS) { #if INCLUDE_CDS_JAVA_HEAP if (CDSConfig::is_using_full_module_graph()) { precond(unnamed_module == ClassLoaderDataShared::archived_boot_unnamed_module()); - unnamed_module->restore_archived_oops(boot_loader_data); + if (!CDSConfig::is_using_aot_linked_classes()) { + unnamed_module->restore_archived_oops(boot_loader_data); + } } else #endif { diff --git a/src/hotspot/share/classfile/modules.hpp b/src/hotspot/share/classfile/modules.hpp index d6d81263449..27a22c1017a 100644 --- a/src/hotspot/share/classfile/modules.hpp +++ b/src/hotspot/share/classfile/modules.hpp @@ -57,6 +57,8 @@ public: static void check_archived_module_oop(oop orig_module_obj) NOT_CDS_JAVA_HEAP_RETURN; static void define_archived_modules(Handle h_platform_loader, Handle h_system_loader, TRAPS) NOT_CDS_JAVA_HEAP_RETURN; + static void init_archived_modules(JavaThread* current, Handle h_platform_loader, Handle h_system_loader) + NOT_CDS_JAVA_HEAP_RETURN; static void verify_archived_modules() NOT_CDS_JAVA_HEAP_RETURN; static void dump_archived_module_info() NOT_CDS_JAVA_HEAP_RETURN; static void serialize_archived_module_info(SerializeClosure* soc) NOT_CDS_JAVA_HEAP_RETURN; diff --git a/src/hotspot/share/classfile/systemDictionary.cpp b/src/hotspot/share/classfile/systemDictionary.cpp index 95f86a8950b..2f7001c86b3 100644 --- a/src/hotspot/share/classfile/systemDictionary.cpp +++ b/src/hotspot/share/classfile/systemDictionary.cpp @@ -196,14 +196,19 @@ ClassLoaderData* SystemDictionary::register_loader(Handle class_loader, bool cre } void SystemDictionary::set_system_loader(ClassLoaderData *cld) { - assert(_java_system_loader.is_empty(), "already set!"); - _java_system_loader = cld->class_loader_handle(); - + if (_java_system_loader.is_empty()) { + _java_system_loader = cld->class_loader_handle(); + } else { + assert(_java_system_loader.resolve() == cld->class_loader(), "sanity"); + } } void SystemDictionary::set_platform_loader(ClassLoaderData *cld) { - assert(_java_platform_loader.is_empty(), "already set!"); - _java_platform_loader = cld->class_loader_handle(); + if (_java_platform_loader.is_empty()) { + _java_platform_loader = cld->class_loader_handle(); + } else { + assert(_java_platform_loader.resolve() == cld->class_loader(), "sanity"); + } } // ---------------------------------------------------------------------------- @@ -1149,6 +1154,58 @@ void SystemDictionary::load_shared_class_misc(InstanceKlass* ik, ClassLoaderData } } +// This is much more lightweight than SystemDictionary::resolve_or_null +// - There's only a single Java thread at this point. No need for placeholder. +// - All supertypes of ik have been loaded +// - There's no circularity (checked in AOT assembly phase) +// - There's no need to call java.lang.ClassLoader::load_class() because the boot/platform/app +// loaders are well-behaved +void SystemDictionary::preload_class(Handle class_loader, InstanceKlass* ik, TRAPS) { + precond(Universe::is_bootstrapping()); + precond(java_platform_loader() != nullptr && java_system_loader() != nullptr); + precond(class_loader() == nullptr || class_loader() == java_platform_loader() ||class_loader() == java_system_loader()); + precond(CDSConfig::is_using_aot_linked_classes()); + precond(AOTMetaspace::in_aot_cache_static_region((void*)ik)); + precond(!ik->is_loaded()); + +#ifdef ASSERT + // preload_class() must be called in the correct order -- all super types must have + // already been loaded. + if (ik->java_super() != nullptr) { + assert(ik->java_super()->is_loaded(), "must be"); + } + + Array* interfaces = ik->local_interfaces(); + int num_interfaces = interfaces->length(); + for (int index = 0; index < num_interfaces; index++) { + assert(interfaces->at(index)->is_loaded(), "must be"); + } +#endif + + ClassLoaderData* loader_data = ClassLoaderData::class_loader_data(class_loader()); + oop java_mirror = ik->archived_java_mirror(); + precond(java_mirror != nullptr); + + Handle pd(THREAD, java_lang_Class::protection_domain(java_mirror)); + PackageEntry* pkg_entry = ik->package(); + assert(pkg_entry != nullptr || ClassLoader::package_from_class_name(ik->name()) == nullptr, + "non-empty packages must have been archived"); + + // TODO: the following assert requires JDK-8365580 + // assert(is_shared_class_visible(ik->name(), ik, pkg_entry, class_loader), "must be"); + + ik->restore_unshareable_info(loader_data, pd, pkg_entry, CHECK); + load_shared_class_misc(ik, loader_data); + ik->add_to_hierarchy(THREAD); + + if (!ik->is_hidden()) { + update_dictionary(THREAD, ik, loader_data); + } + + assert(java_lang_Class::module(java_mirror) != nullptr, "must have been archived"); + assert(ik->is_loaded(), "Must be in at least loaded state"); +} + #endif // INCLUDE_CDS InstanceKlass* SystemDictionary::load_instance_class_impl(Symbol* class_name, Handle class_loader, TRAPS) { diff --git a/src/hotspot/share/classfile/systemDictionary.hpp b/src/hotspot/share/classfile/systemDictionary.hpp index 8cf2cd83b82..99cb1d0b5d2 100644 --- a/src/hotspot/share/classfile/systemDictionary.hpp +++ b/src/hotspot/share/classfile/systemDictionary.hpp @@ -326,7 +326,7 @@ private: static void restore_archived_method_handle_intrinsics_impl(TRAPS) NOT_CDS_RETURN; protected: - // Used by SystemDictionaryShared and LambdaProxyClassDictionary + // Used by AOTLinkedClassBulkLoader, LambdaProxyClassDictionary, and SystemDictionaryShared static bool add_loader_constraint(Symbol* name, Klass* klass_being_linked, Handle loader1, Handle loader2); @@ -337,6 +337,7 @@ protected: const ClassFileStream *cfs, PackageEntry* pkg_entry, TRAPS); + static void preload_class(Handle class_loader, InstanceKlass* ik, TRAPS); static Handle get_loader_lock_or_null(Handle class_loader); static InstanceKlass* find_or_define_instance_class(Symbol* class_name, Handle class_loader, diff --git a/src/hotspot/share/classfile/vmClasses.cpp b/src/hotspot/share/classfile/vmClasses.cpp index 23bc054755a..e337d5569bc 100644 --- a/src/hotspot/share/classfile/vmClasses.cpp +++ b/src/hotspot/share/classfile/vmClasses.cpp @@ -101,7 +101,11 @@ bool vmClasses::resolve(vmClassID id, TRAPS) { void vmClasses::resolve_until(vmClassID limit_id, vmClassID &start_id, TRAPS) { assert((int)start_id <= (int)limit_id, "IDs are out of order!"); for (auto id : EnumRange{start_id, limit_id}) { // (inclusive start, exclusive end) - resolve(id, CHECK); + if (CDSConfig::is_using_aot_linked_classes()) { + precond(klass_at(id)->is_loaded()); + } else { + resolve(id, CHECK); + } } // move the starting value forward to the limit: @@ -115,6 +119,10 @@ void vmClasses::resolve_all(TRAPS) { // after vmSymbols::initialize() is called but before any classes are pre-loaded. ClassLoader::classLoader_init2(THREAD); + if (CDSConfig::is_using_aot_linked_classes()) { + AOTLinkedClassBulkLoader::preload_classes(THREAD); + } + // Preload commonly used klasses vmClassID scan = vmClassID::FIRST; // first do Object, then String, Class @@ -210,9 +218,6 @@ void vmClasses::resolve_all(TRAPS) { #endif InstanceStackChunkKlass::init_offset_of_stack(); - if (CDSConfig::is_using_aot_linked_classes()) { - AOTLinkedClassBulkLoader::load_javabase_classes(THREAD); - } } #if INCLUDE_CDS diff --git a/src/hotspot/share/compiler/compilationPolicy.cpp b/src/hotspot/share/compiler/compilationPolicy.cpp index c91d299510d..177fd04fbbc 100644 --- a/src/hotspot/share/compiler/compilationPolicy.cpp +++ b/src/hotspot/share/compiler/compilationPolicy.cpp @@ -852,13 +852,6 @@ nmethod* CompilationPolicy::event(const methodHandle& method, const methodHandle print_event(bci == InvocationEntryBci ? CALL : LOOP, method(), inlinee(), bci, comp_level); } -#if INCLUDE_JVMCI - if (EnableJVMCI && UseJVMCICompiler && - comp_level == CompLevel_full_optimization CDS_ONLY(&& !AOTLinkedClassBulkLoader::class_preloading_finished())) { - return nullptr; - } -#endif - if (comp_level == CompLevel_none && JvmtiExport::can_post_interpreter_events() && THREAD->is_interp_only_mode()) { @@ -1452,12 +1445,7 @@ CompLevel CompilationPolicy::call_event(const methodHandle& method, CompLevel cu } else { next_level = MAX2(osr_level, next_level); } -#if INCLUDE_JVMCI - if (EnableJVMCI && UseJVMCICompiler && - next_level == CompLevel_full_optimization CDS_ONLY(&& !AOTLinkedClassBulkLoader::class_preloading_finished())) { - next_level = cur_level; - } -#endif + return next_level; } diff --git a/src/hotspot/share/memory/iterator.inline.hpp b/src/hotspot/share/memory/iterator.inline.hpp index 498c74fd1d2..2975e050b70 100644 --- a/src/hotspot/share/memory/iterator.inline.hpp +++ b/src/hotspot/share/memory/iterator.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ #include "memory/iterator.hpp" -#include "cds/aotLinkedClassBulkLoader.hpp" #include "classfile/classLoaderData.hpp" #include "code/nmethod.hpp" #include "oops/access.inline.hpp" @@ -51,12 +50,7 @@ inline void ClaimMetadataVisitingOopIterateClosure::do_cld(ClassLoaderData* cld) } inline void ClaimMetadataVisitingOopIterateClosure::do_klass(Klass* k) { - ClassLoaderData* cld = k->class_loader_data(); - if (cld != nullptr) { - ClaimMetadataVisitingOopIterateClosure::do_cld(cld); - } else { - assert(AOTLinkedClassBulkLoader::is_pending_aot_linked_class(k), "sanity"); - } + ClaimMetadataVisitingOopIterateClosure::do_cld(k->class_loader_data()); } inline void ClaimMetadataVisitingOopIterateClosure::do_nmethod(nmethod* nm) { diff --git a/src/hotspot/share/memory/universe.cpp b/src/hotspot/share/memory/universe.cpp index a3afcc5ba64..a3b841a400b 100644 --- a/src/hotspot/share/memory/universe.cpp +++ b/src/hotspot/share/memory/universe.cpp @@ -584,6 +584,11 @@ void Universe::initialize_basic_type_mirrors(TRAPS) { } void Universe::fixup_mirrors(TRAPS) { + if (CDSConfig::is_using_aot_linked_classes()) { + // All mirrors of preloaded classes are already restored. No need to fix up. + return; + } + // Bootstrap problem: all classes gets a mirror (java.lang.Class instance) assigned eagerly, // but we cannot do that for classes created before java.lang.Class is loaded. Here we simply // walk over permanent objects created so far (mostly classes) and fixup their mirrors. Note diff --git a/src/hotspot/share/runtime/java.cpp b/src/hotspot/share/runtime/java.cpp index 997dd1f802a..9363f9055ba 100644 --- a/src/hotspot/share/runtime/java.cpp +++ b/src/hotspot/share/runtime/java.cpp @@ -450,7 +450,7 @@ void before_exit(JavaThread* thread, bool halt) { ClassListWriter::write_resolved_constants(); if (CDSConfig::is_dumping_preimage_static_archive()) { - AOTMetaspace::preload_and_dump(thread); + AOTMetaspace::dump_static_archive(thread); } #endif diff --git a/src/hotspot/share/runtime/threads.cpp b/src/hotspot/share/runtime/threads.cpp index 65eea9d5fb2..0172fe4d69b 100644 --- a/src/hotspot/share/runtime/threads.cpp +++ b/src/hotspot/share/runtime/threads.cpp @@ -772,7 +772,7 @@ jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) { if (CDSConfig::is_using_aot_linked_classes()) { SystemDictionary::restore_archived_method_handle_intrinsics(); - AOTLinkedClassBulkLoader::finish_loading_javabase_classes(CHECK_JNI_ERR); + AOTLinkedClassBulkLoader::link_or_init_javabase_classes(THREAD); } // Start string deduplication thread if requested. @@ -791,7 +791,7 @@ jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) { call_initPhase2(CHECK_JNI_ERR); if (CDSConfig::is_using_aot_linked_classes()) { - AOTLinkedClassBulkLoader::load_non_javabase_classes(THREAD); + AOTLinkedClassBulkLoader::link_or_init_non_javabase_classes(THREAD); } #ifndef PRODUCT HeapShared::initialize_test_class_from_archive(THREAD); @@ -889,10 +889,10 @@ jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) { if (CDSConfig::is_dumping_classic_static_archive()) { // Classic -Xshare:dump, aka "old workflow" - AOTMetaspace::preload_and_dump(CHECK_JNI_ERR); + AOTMetaspace::dump_static_archive(CHECK_JNI_ERR); } else if (CDSConfig::is_dumping_final_static_archive()) { tty->print_cr("Reading AOTConfiguration %s and writing AOTCache %s", AOTConfiguration, AOTCache); - AOTMetaspace::preload_and_dump(CHECK_JNI_ERR); + AOTMetaspace::dump_static_archive(CHECK_JNI_ERR); } if (log_is_enabled(Info, perf, class, link)) { diff --git a/src/java.base/share/classes/java/lang/ClassLoader.java b/src/java.base/share/classes/java/lang/ClassLoader.java index 511785a5ac8..47f624cb0f8 100644 --- a/src/java.base/share/classes/java/lang/ClassLoader.java +++ b/src/java.base/share/classes/java/lang/ClassLoader.java @@ -59,12 +59,15 @@ import jdk.internal.loader.ClassLoaders; import jdk.internal.loader.NativeLibrary; import jdk.internal.loader.NativeLibraries; import jdk.internal.perf.PerfCounter; +import jdk.internal.misc.CDS; import jdk.internal.misc.Unsafe; import jdk.internal.misc.VM; import jdk.internal.reflect.CallerSensitive; import jdk.internal.reflect.CallerSensitiveAdapter; import jdk.internal.reflect.Reflection; import jdk.internal.util.StaticProperty; +import jdk.internal.vm.annotation.AOTRuntimeSetup; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; /** * A class loader is an object that is responsible for loading classes. The @@ -221,10 +224,16 @@ import jdk.internal.util.StaticProperty; * @see #resolveClass(Class) * @since 1.0 */ +@AOTSafeClassInitializer public abstract class ClassLoader { private static native void registerNatives(); static { + runtimeSetup(); + } + + @AOTRuntimeSetup + private static void runtimeSetup() { registerNatives(); } @@ -1858,8 +1867,8 @@ public abstract class ClassLoader { throw new IllegalStateException(msg); default: // system fully initialized - assert VM.isBooted() && scl != null; - return scl; + assert VM.isBooted() && Holder.scl != null; + return Holder.scl; } } @@ -1884,7 +1893,7 @@ public abstract class ClassLoader { } // detect recursive initialization - if (scl != null) { + if (Holder.scl != null) { throw new IllegalStateException("recursive invocation"); } @@ -1895,7 +1904,7 @@ public abstract class ClassLoader { // custom class loader is only supported to be loaded from unnamed module Constructor ctor = Class.forName(cn, false, builtinLoader) .getDeclaredConstructor(ClassLoader.class); - scl = (ClassLoader) ctor.newInstance(builtinLoader); + Holder.scl = (ClassLoader) ctor.newInstance(builtinLoader); } catch (Exception e) { Throwable cause = e; if (e instanceof InvocationTargetException) { @@ -1910,9 +1919,9 @@ public abstract class ClassLoader { throw new Error(cause.getMessage(), cause); } } else { - scl = builtinLoader; + Holder.scl = builtinLoader; } - return scl; + return Holder.scl; } // Returns the class's class loader, or null if none. @@ -1925,9 +1934,13 @@ public abstract class ClassLoader { return caller.getClassLoader0(); } - // The system class loader - // @GuardedBy("ClassLoader.class") - private static volatile ClassLoader scl; + // Holder has the field(s) that need to be initialized during JVM bootstrap even if + // the outer is aot-initialized. + private static class Holder { + // The system class loader + // @GuardedBy("ClassLoader.class") + private static volatile ClassLoader scl; + } // -- Package -- @@ -2602,7 +2615,21 @@ public abstract class ClassLoader { if (parallelLockMap != null) { reinitObjectField("parallelLockMap", new ConcurrentHashMap<>()); } - reinitObjectField("packages", new ConcurrentHashMap<>()); + + if (CDS.isDumpingAOTLinkedClasses()) { + if (System.getProperty("cds.debug.archived.packages") != null) { + for (Map.Entry entry : packages.entrySet()) { + String key = entry.getKey(); + NamedPackage value = entry.getValue(); + System.out.println("Archiving " + + (value instanceof Package ? "Package" : "NamedPackage") + + " \"" + key + "\" for " + this); + } + } + } else { + reinitObjectField("packages", new ConcurrentHashMap<>()); + } + reinitObjectField("package2certs", new ConcurrentHashMap<>()); classes.clear(); classes.trimToSize(); diff --git a/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java b/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java index c55ccd735df..d5f4470d9c9 100644 --- a/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java +++ b/src/java.base/share/classes/java/lang/module/ModuleDescriptor.java @@ -54,6 +54,8 @@ import static java.util.Objects.*; import jdk.internal.module.Checks; import jdk.internal.module.ModuleInfo; +import jdk.internal.vm.annotation.AOTRuntimeSetup; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; /** @@ -91,6 +93,7 @@ import jdk.internal.module.ModuleInfo; * @since 9 */ +@AOTSafeClassInitializer public final class ModuleDescriptor implements Comparable { @@ -2665,6 +2668,11 @@ public final class ModuleDescriptor } static { + runtimeSetup(); + } + + @AOTRuntimeSetup + private static void runtimeSetup() { /** * Setup the shared secret to allow code in other packages access * private package methods in java.lang.module. diff --git a/src/java.base/share/classes/java/net/URI.java b/src/java.base/share/classes/java/net/URI.java index daf63d19032..d130dc3b460 100644 --- a/src/java.base/share/classes/java/net/URI.java +++ b/src/java.base/share/classes/java/net/URI.java @@ -43,6 +43,8 @@ import java.text.Normalizer; import jdk.internal.access.JavaNetUriAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.util.Exceptions; +import jdk.internal.vm.annotation.AOTRuntimeSetup; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; import sun.nio.cs.UTF_8; import static jdk.internal.util.Exceptions.filterNonSocketInfo; @@ -516,6 +518,7 @@ import static jdk.internal.util.Exceptions.formatMsg; * @see URISyntaxException */ +@AOTSafeClassInitializer public final class URI implements Comparable, Serializable { @@ -3726,7 +3729,13 @@ public final class URI } } + static { + runtimeSetup(); + } + + @AOTRuntimeSetup + private static void runtimeSetup() { SharedSecrets.setJavaNetUriAccess( new JavaNetUriAccess() { public URI create(String scheme, String path) { diff --git a/src/java.base/share/classes/java/net/URL.java b/src/java.base/share/classes/java/net/URL.java index 9266b6c94f1..1435d851f41 100644 --- a/src/java.base/share/classes/java/net/URL.java +++ b/src/java.base/share/classes/java/net/URL.java @@ -43,6 +43,8 @@ import jdk.internal.access.JavaNetURLAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.misc.ThreadTracker; import jdk.internal.misc.VM; +import jdk.internal.vm.annotation.AOTRuntimeSetup; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; import sun.net.util.IPAddressUtil; import static jdk.internal.util.Exceptions.filterNonSocketInfo; import static jdk.internal.util.Exceptions.formatMsg; @@ -214,6 +216,7 @@ import static jdk.internal.util.Exceptions.formatMsg; * @author James Gosling * @since 1.0 */ +@AOTSafeClassInitializer public final class URL implements java.io.Serializable { static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol"; @@ -1758,6 +1761,11 @@ public final class URL implements java.io.Serializable { } static { + runtimeSetup(); + } + + @AOTRuntimeSetup + private static void runtimeSetup() { SharedSecrets.setJavaNetURLAccess( new JavaNetURLAccess() { @Override diff --git a/src/java.base/share/classes/java/security/SecureClassLoader.java b/src/java.base/share/classes/java/security/SecureClassLoader.java index b398d7332d7..7b0420ec601 100644 --- a/src/java.base/share/classes/java/security/SecureClassLoader.java +++ b/src/java.base/share/classes/java/security/SecureClassLoader.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import jdk.internal.misc.CDS; /** * This class extends {@code ClassLoader} with additional support for defining @@ -243,6 +244,20 @@ public class SecureClassLoader extends ClassLoader { * Called by the VM, during -Xshare:dump */ private void resetArchivedStates() { - pdcache.clear(); + if (CDS.isDumpingAOTLinkedClasses()) { + for (CodeSourceKey key : pdcache.keySet()) { + if (key.cs.getCodeSigners() != null) { + // We don't archive any signed classes, so we don't need to cache their ProtectionDomains. + pdcache.remove(key); + } + } + if (System.getProperty("cds.debug.archived.protection.domains") != null) { + for (CodeSourceKey key : pdcache.keySet()) { + System.out.println("Archiving ProtectionDomain " + key.cs + " for " + this); + } + } + } else { + pdcache.clear(); + } } } diff --git a/src/java.base/share/classes/jdk/internal/loader/BootLoader.java b/src/java.base/share/classes/jdk/internal/loader/BootLoader.java index bc5bd9d4265..72c7e7e7451 100644 --- a/src/java.base/share/classes/jdk/internal/loader/BootLoader.java +++ b/src/java.base/share/classes/jdk/internal/loader/BootLoader.java @@ -75,9 +75,13 @@ public class BootLoader { private static final ConcurrentHashMap CLASS_LOADER_VALUE_MAP = new ConcurrentHashMap<>(); - // native libraries loaded by the boot class loader - private static final NativeLibraries NATIVE_LIBS - = NativeLibraries.newInstance(null); + // Holder has the field(s) that need to be initialized during JVM bootstrap even if + // the outer is aot-initialized. + private static class Holder { + // native libraries loaded by the boot class loader + private static final NativeLibraries NATIVE_LIBS + = NativeLibraries.newInstance(null); + } /** * Returns the unnamed module for the boot loader. @@ -104,7 +108,7 @@ public class BootLoader { * Returns NativeLibraries for the boot class loader. */ public static NativeLibraries getNativeLibraries() { - return NATIVE_LIBS; + return Holder.NATIVE_LIBS; } /** diff --git a/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java b/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java index 44eaab0e83a..98cedb0b3bf 100644 --- a/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java +++ b/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -153,7 +153,7 @@ public final class NativeLibraries { } // cannot be loaded by other class loaders - if (loadedLibraryNames.contains(name)) { + if (Holder.loadedLibraryNames.contains(name)) { throw new UnsatisfiedLinkError("Native Library " + name + " already loaded in another classloader"); } @@ -203,7 +203,7 @@ public final class NativeLibraries { NativeLibraryContext.pop(); } // register the loaded native library - loadedLibraryNames.add(name); + Holder.loadedLibraryNames.add(name); libraries.put(name, lib); return lib; } finally { @@ -243,6 +243,11 @@ public final class NativeLibraries { return lib; } + // Called at the end of AOTCache assembly phase. + public void clear() { + libraries.clear(); + } + private NativeLibrary findFromPaths(String[] paths, Class fromClass, String name) { for (String path : paths) { File libfile = new File(path, System.mapLibraryName(name)); @@ -368,7 +373,7 @@ public final class NativeLibraries { acquireNativeLibraryLock(name); try { /* remove the native library name */ - if (!loadedLibraryNames.remove(name)) { + if (!Holder.loadedLibraryNames.remove(name)) { throw new IllegalStateException(name + " has already been unloaded"); } NativeLibraryContext.push(UNLOADER); @@ -395,9 +400,13 @@ public final class NativeLibraries { static final String[] USER_PATHS = ClassLoaderHelper.parsePath(StaticProperty.javaLibraryPath()); } - // All native libraries we've loaded. - private static final Set loadedLibraryNames = + // Holder has the fields that need to be initialized during JVM bootstrap even if + // the outer is aot-initialized. + static class Holder { + // All native libraries we've loaded. + private static final Set loadedLibraryNames = ConcurrentHashMap.newKeySet(); + } // reentrant lock class that allows exact counting (with external synchronization) @SuppressWarnings("serial") diff --git a/src/java.base/share/classes/jdk/internal/misc/CDS.java b/src/java.base/share/classes/jdk/internal/misc/CDS.java index 72b8479de9a..b61743c1fb3 100644 --- a/src/java.base/share/classes/jdk/internal/misc/CDS.java +++ b/src/java.base/share/classes/jdk/internal/misc/CDS.java @@ -47,11 +47,13 @@ import jdk.internal.util.StaticProperty; public class CDS { // Must be in sync with cdsConfig.hpp - private static final int IS_DUMPING_ARCHIVE = 1 << 0; - private static final int IS_DUMPING_METHOD_HANDLES = 1 << 1; - private static final int IS_DUMPING_STATIC_ARCHIVE = 1 << 2; - private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 3; - private static final int IS_USING_ARCHIVE = 1 << 4; + private static final int IS_DUMPING_AOT_LINKED_CLASSES = 1 << 0; + private static final int IS_DUMPING_ARCHIVE = 1 << 1; + private static final int IS_DUMPING_METHOD_HANDLES = 1 << 2; + private static final int IS_DUMPING_STATIC_ARCHIVE = 1 << 3; + private static final int IS_LOGGING_LAMBDA_FORM_INVOKERS = 1 << 4; + private static final int IS_USING_ARCHIVE = 1 << 5; + private static final int configStatus = getCDSConfigStatus(); /** @@ -82,6 +84,10 @@ public class CDS { return (configStatus & IS_DUMPING_STATIC_ARCHIVE) != 0; } + public static boolean isDumpingAOTLinkedClasses() { + return (configStatus & IS_DUMPING_AOT_LINKED_CLASSES) != 0; + } + public static boolean isSingleThreadVM() { return isDumpingStaticArchive(); } diff --git a/test/hotspot/jtreg/ProblemList-AotJdk.txt b/test/hotspot/jtreg/ProblemList-AotJdk.txt index 047fc6d33f8..d292c23f690 100644 --- a/test/hotspot/jtreg/ProblemList-AotJdk.txt +++ b/test/hotspot/jtreg/ProblemList-AotJdk.txt @@ -17,3 +17,7 @@ compiler/intrinsics/klass/TestIsPrimitive.java 0000000 generic-all # It has the assumption about unresolved Integer. # However when AOTClassLinking is enabled, Integer is always resolved at JVM start-up. compiler/ciReplay/TestInliningProtectionDomain.java 0000000 generic-all + +# These tests fail often with AotJdk due to JDK-8323727 +compiler/arguments/TestStressReflectiveCode.java 8323727 generic-all +compiler/arraycopy/TestCloneWithStressReflectiveCode.java 8323727 generic-all diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/MethodHandleTest.java b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/MethodHandleTest.java index 972dc287af5..1d1984b5e4c 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/MethodHandleTest.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/MethodHandleTest.java @@ -33,6 +33,7 @@ * MethodHandleTestApp MethodHandleTestApp$A MethodHandleTestApp$B * UnsupportedBSMs UnsupportedBSMs$MyEnum * ObjectMethodsTest ObjectMethodsTest$C + * InterfaceWithEnum EnumWithClinit * @run driver MethodHandleTest AOT --two-step-training */ @@ -101,6 +102,11 @@ public class MethodHandleTest { out.shouldNotContain("MethodHandleTestApp."); out.shouldContain("intElm = 777"); } + + // For MethodHandleTestApp.testLambdaWithEnums() + if (runMode == RunMode.ASSEMBLY) { + out.shouldNotContain("EnumWithClinit."); + } } } } @@ -206,6 +212,8 @@ class MethodHandleTestApp { ObjectMethodsTest.testEqualsC(ObjectMethodsTest_handle); + testLambdaWithEnums(); + UnsupportedBSMs.invokeUnsupportedBSMs(); } @@ -275,6 +283,29 @@ class MethodHandleTestApp { } } } + + + static boolean InterfaceWithEnum_inited = false; + + // Enum types used in lambdas shouldn't be initialized during the assembly phase. + static void testLambdaWithEnums() { + if (InterfaceWithEnum_inited) { + throw new RuntimeException("InterfaceWithEnum should not be inited"); + } + + InterfaceWithEnum iwe = (x) -> { + System.out.println("Hello from testLambdaWithEnums"); + }; + + System.out.println(iwe); + if (InterfaceWithEnum_inited) { + throw new RuntimeException("InterfaceWithEnum should not be inited"); + } + iwe.func(EnumWithClinit.Dummy); + if (!InterfaceWithEnum_inited) { + throw new RuntimeException("InterfaceWithEnum should be inited"); + } + } } // Excerpt from test/jdk/java/lang/runtime/ObjectMethodsTest.java @@ -332,6 +363,18 @@ class ObjectMethodsTest { } } +interface InterfaceWithEnum { + void func(EnumWithClinit e); +} + +enum EnumWithClinit { + Dummy; + static { + MethodHandleTestApp.InterfaceWithEnum_inited = true; + System.out.println("EnumWithClinit."); + } +} + class UnsupportedBSMs { // This method is executed during the assembly phase. // From 4df04a254397836b1bfe384ac9e6413e1ff9b242 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Tue, 23 Sep 2025 14:53:04 +0000 Subject: [PATCH 188/556] 8366809: JFR: Use factory for aggregator functions Reviewed-by: mgronlun --- .../classes/jdk/jfr/internal/query/Field.java | 3 + .../jdk/jfr/internal/query/Function.java | 60 ++++++++++--------- .../jdk/jfr/internal/query/Histogram.java | 5 +- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Field.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Field.java index 90cd1c40a76..b7fc4381670 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Field.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Field.java @@ -33,6 +33,7 @@ import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.internal.query.Configuration.Truncate; import jdk.jfr.internal.query.Query.Grouper; import jdk.jfr.internal.query.Query.OrderElement; +import jdk.jfr.internal.query.Function.FunctionFactory; /** * Field is the core class of the package. @@ -137,6 +138,8 @@ final class Field { public int precision = -1; + public FunctionFactory functionFactory; + public Field(FilteredType type, String name) { this.type = type; this.name = name; diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Function.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Function.java index 0c9ca96ee20..f01105c698c 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Function.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,45 +35,49 @@ import java.util.Set; abstract class Function { + interface FunctionFactory { + Function newFunction(); + } + public abstract void add(Object value); public abstract Object result(); - public static Function create(Field field) { + public static FunctionFactory createFactory(Field field) { Aggregator aggregator = field.aggregator; if (field.grouper != null || aggregator == Aggregator.MISSING) { - return new FirstNonNull(); + return () -> new FirstNonNull(); } if (aggregator == Aggregator.LIST) { - return new Container(new ArrayList<>()); + return () -> new Container(new ArrayList<>()); } if (aggregator == Aggregator.SET) { - return new Container(new LinkedHashSet<>()); + return () -> new Container(new LinkedHashSet<>()); } if (aggregator == Aggregator.DIFFERENCE) { if (field.timestamp) { - return new TimeDifference(); + return () -> new TimeDifference(); } else { - return new Difference(); + return () -> new Difference(); } } if (aggregator == Aggregator.STANDARD_DEVIATION) { if (field.timespan) { - return new TimespanFunction(new StandardDeviation()); + return () -> new TimespanFunction(new StandardDeviation()); } else { - return new StandardDeviation(); + return () -> new StandardDeviation(); } } if (aggregator == Aggregator.MEDIAN) { if (field.timespan) { - return new TimespanFunction(new Median()); + return () -> new TimespanFunction(new Median()); } else { - return new Median(); + return () -> new Median(); } } @@ -83,7 +87,6 @@ abstract class Function { if (aggregator == Aggregator.P95) { return createPercentile(field, 0.95); - } if (aggregator == Aggregator.P99) { return createPercentile(field, 0.99); @@ -92,46 +95,46 @@ abstract class Function { return createPercentile(field, 0.999); } if (aggregator == Aggregator.MAXIMUM) { - return new Maximum(); + return () -> new Maximum(); } if (aggregator == Aggregator.MINIMUM) { - return new Minimum(); + return () -> new Minimum(); } if (aggregator == Aggregator.SUM) { if (field.timespan) { - return new SumDuration(); + return () -> new SumDuration(); } if (field.fractionalType) { - return new SumDouble(); + return () -> new SumDouble(); } if (field.integralType) { - return new SumLong(); + return () -> new SumLong(); } } if (aggregator == Aggregator.FIRST) { - return new First(); + return () -> new First(); } if (aggregator == Aggregator.LAST_BATCH) { - return new LastBatch(field); + return () -> new LastBatch(field); } if (aggregator == Aggregator.LAST) { - return new Last(); + return () -> new Last(); } if (aggregator == Aggregator.AVERAGE) { if (field.timespan) { - return new AverageDuration(); + return () -> new AverageDuration(); } else { - return new Average(); + return () -> new Average(); } } if (aggregator == Aggregator.COUNT) { - return new Count(); + return () -> new Count(); } if (aggregator == Aggregator.UNIQUE) { - return new Unique(); + return () -> new Unique(); } - return new Null(); + return () -> new Null(); } // **** AVERAGE **** @@ -507,12 +510,11 @@ abstract class Function { } // **** PERCENTILE **** - private static Function createPercentile(Field field, double percentile) { - Percentile p = new Percentile(percentile); + private static FunctionFactory createPercentile(Field field, double percentile) { if (field.timespan) { - return new TimespanFunction(p); + return () -> new TimespanFunction(new Percentile(percentile)); } else { - return p; + return () -> new Percentile(percentile); } } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Histogram.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Histogram.java index d3d596dc4b9..8b68c492c6e 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Histogram.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/Histogram.java @@ -122,6 +122,9 @@ final class Histogram { public void addFields(List fields) { this.fields.addAll(fields); + for (Field field : fields) { + field.functionFactory = Function.createFactory(field); + } } public void add(RecordedEvent e, FilteredType type, List sourceFields) { @@ -168,7 +171,7 @@ final class Histogram { private Function[] createFunctions() { Function[] functions = new Function[fields.size()]; for (int i = 0; i < functions.length; i++) { - functions[i] = Function.create(fields.get(i)); + functions[i] = fields.get(i).functionFactory.newFunction(); } return functions; } From 82bdef16390deaa6863cdf8ecf26e6e99f6cb0a2 Mon Sep 17 00:00:00 2001 From: Matthias Baesken Date: Tue, 23 Sep 2025 15:07:59 +0000 Subject: [PATCH 189/556] 8367913: LIBDL dependency seems to be not needed for some jdk libs Reviewed-by: aivanov, ihse, clanger, mdoerr --- make/modules/java.base/Lib.gmk | 4 +++- make/modules/java.desktop/lib/ClientLibraries.gmk | 4 ++-- make/modules/java.instrument/Lib.gmk | 2 -- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/make/modules/java.base/Lib.gmk b/make/modules/java.base/Lib.gmk index 51d323a0344..41da22f8cb2 100644 --- a/make/modules/java.base/Lib.gmk +++ b/make/modules/java.base/Lib.gmk @@ -154,6 +154,8 @@ endif ################################################################################ ## Build libsyslookup +## The LIBDL dependency on Linux is needed to dynamically access libdl symbols, +## which may be needed as part of resolving some standard symbols ################################################################################ $(eval $(call SetupJdkLibrary, BUILD_LIBSYSLOOKUP, \ @@ -196,7 +198,7 @@ ifeq ($(call isTargetOs, linux)+$(call isTargetCpu, x86_64)+$(INCLUDE_COMPILER2) OPTIMIZATION := HIGH, \ CXXFLAGS := -std=c++17, \ DISABLED_WARNINGS_gcc := unused-variable, \ - LIBS_linux := $(LIBDL) $(LIBM), \ + LIBS_linux := $(LIBM), \ )) TARGETS += $(BUILD_LIBSIMD_SORT) diff --git a/make/modules/java.desktop/lib/ClientLibraries.gmk b/make/modules/java.desktop/lib/ClientLibraries.gmk index a69b65180d7..2c29092cdd6 100644 --- a/make/modules/java.desktop/lib/ClientLibraries.gmk +++ b/make/modules/java.desktop/lib/ClientLibraries.gmk @@ -51,7 +51,7 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBMLIB_IMAGE, \ $(LIBMLIB_IMAGE_CFLAGS), \ DISABLED_WARNINGS_gcc := unused-function, \ DISABLED_WARNINGS_clang_mlib_ImageCreate.c := unused-function, \ - LIBS_unix := $(LIBDL) $(LIBM), \ + LIBS_unix := $(LIBM), \ )) TARGETS += $(BUILD_LIBMLIB_IMAGE) @@ -264,7 +264,7 @@ ifeq ($(ENABLE_HEADLESS_ONLY), false) JDK_LIBS_macosx := libosxapp, \ LIBS := $(GIFLIB_LIBS) $(LIBJPEG_LIBS) $(LIBZ_LIBS) $(PNG_LIBS) $(ICONV_LIBS), \ LIBS_unix := $(LIBM) $(LIBPTHREAD), \ - LIBS_linux := $(LIBDL) $(X_LIBS) -lX11 -lXext, \ + LIBS_linux := $(X_LIBS) -lX11 -lXext, \ LIBS_macosx := \ -framework ApplicationServices \ -framework Cocoa \ diff --git a/make/modules/java.instrument/Lib.gmk b/make/modules/java.instrument/Lib.gmk index 609814c86ed..4181cdf81c9 100644 --- a/make/modules/java.instrument/Lib.gmk +++ b/make/modules/java.instrument/Lib.gmk @@ -43,8 +43,6 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBINSTRUMENT, \ JDK_LIBS := java.base:libjava java.base:libjli java.base:libjvm, \ LIBS := $(ICONV_LIBS), \ LIBS_unix := $(LIBZ_LIBS), \ - LIBS_linux := $(LIBDL), \ - LIBS_aix := $(LIBDL), \ LIBS_macosx := \ -framework ApplicationServices \ -framework Cocoa \ From 218e82c875237f82a649a214c72d925a5ebf188c Mon Sep 17 00:00:00 2001 From: Roger Riggs Date: Tue, 23 Sep 2025 16:59:00 +0000 Subject: [PATCH 190/556] 8368192: Test java/lang/ProcessBuilder/Basic.java#id0 fails with Exception: Stack trace Reviewed-by: jpai, stuefe --- test/jdk/ProblemList.txt | 1 - test/jdk/java/lang/ProcessBuilder/Basic.java | 35 ++++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index d4685743fd7..8b99564b967 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -529,7 +529,6 @@ java/lang/invoke/LFCaching/LFMultiThreadCachingTest.java 8151492 generic- java/lang/invoke/LFCaching/LFGarbageCollectedTest.java 8078602 generic-all java/lang/invoke/lambda/LambdaFileEncodingSerialization.java 8249079 linux-all java/lang/invoke/RicochetTest.java 8251969 generic-all -java/lang/ProcessBuilder/Basic.java#id0 8368192 macosx-all ############################################################################ diff --git a/test/jdk/java/lang/ProcessBuilder/Basic.java b/test/jdk/java/lang/ProcessBuilder/Basic.java index 551a4869e86..4377752650d 100644 --- a/test/jdk/java/lang/ProcessBuilder/Basic.java +++ b/test/jdk/java/lang/ProcessBuilder/Basic.java @@ -28,7 +28,7 @@ * 6464154 6523983 6206031 4960438 6631352 6631966 6850957 6850958 * 4947220 7018606 7034570 4244896 5049299 8003488 8054494 8058464 * 8067796 8224905 8263729 8265173 8272600 8231297 8282219 8285517 - * 8352533 + * 8352533 8368192 * @key intermittent * @summary Basic tests for Process and Environment Variable code * @modules java.base/java.lang:open @@ -777,30 +777,29 @@ public class Basic { return Pattern.compile(regex).matcher(str).find(); } - private static String matchAndExtract(String str, String regex) { - Matcher matcher = Pattern.compile(regex).matcher(str); - if (matcher.find()) { - return matcher.group(); - } else { - return ""; - } + // Return the string with the matching regex removed + private static String matchAndRemove(String str, String regex) { + return Pattern.compile(regex) + .matcher(str) + .replaceAll(""); } /* Only used for Mac OS X -- - * Mac OS X (may) add the variable __CF_USER_TEXT_ENCODING to an empty - * environment. The environment variable JAVA_MAIN_CLASS_ may also - * be set in Mac OS X. - * Remove them both from the list of env variables + * Mac OS X (may) add the variables: __CF_USER_TEXT_ENCODING, JAVA_MAIN_CLASS_, + * and TMPDIR. + * Remove them from the list of env variables */ private static String removeMacExpectedVars(String vars) { // Check for __CF_USER_TEXT_ENCODING - String cleanedVars = vars.replace("__CF_USER_TEXT_ENCODING=" - +cfUserTextEncoding+",",""); + String cleanedVars = matchAndRemove(vars, + "__CF_USER_TEXT_ENCODING=" + cfUserTextEncoding + ","); // Check for JAVA_MAIN_CLASS_ - String javaMainClassStr - = matchAndExtract(cleanedVars, - "JAVA_MAIN_CLASS_\\d+=Basic.JavaChild,"); - return cleanedVars.replace(javaMainClassStr,""); + cleanedVars = matchAndRemove(cleanedVars, + "JAVA_MAIN_CLASS_\\d+=Basic.JavaChild,"); + // Check and remove TMPDIR + cleanedVars = matchAndRemove(cleanedVars, + "TMPDIR=[^,]*,"); + return cleanedVars; } /* Only used for AIX -- From f1ee1b4a3d7c47b6f61b36b78504e3ec997a925a Mon Sep 17 00:00:00 2001 From: Damon Nguyen Date: Tue, 23 Sep 2025 17:36:55 +0000 Subject: [PATCH 191/556] 8366844: Update and automate MouseDraggedOriginatedByScrollBarTest.java Reviewed-by: aivanov, honkar --- ...MouseDraggedOriginatedByScrollBarTest.java | 84 +++++++++++++------ 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/test/jdk/java/awt/List/MouseDraggedOriginatedByScrollBarTest.java b/test/jdk/java/awt/List/MouseDraggedOriginatedByScrollBarTest.java index 600d38fe393..6858359d6b4 100644 --- a/test/jdk/java/awt/List/MouseDraggedOriginatedByScrollBarTest.java +++ b/test/jdk/java/awt/List/MouseDraggedOriginatedByScrollBarTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,41 +24,46 @@ /* * @test * @bug 6240151 + * @key headful * @summary XToolkit: Dragging the List scrollbar initiates DnD - * @library /java/awt/regtesthelpers - * @build PassFailJFrame - * @run main/manual MouseDraggedOriginatedByScrollBarTest + * @requires os.family == "linux" + * @run main MouseDraggedOriginatedByScrollBarTest */ +import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Frame; import java.awt.List; -import java.awt.event.MouseMotionAdapter; +import java.awt.Point; +import java.awt.Robot; +import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; public class MouseDraggedOriginatedByScrollBarTest { - - private static final String INSTRUCTIONS = """ - 1) Click and drag the scrollbar of the list. - 2) Keep dragging till the mouse pointer goes out the scrollbar. - 3) The test failed if you see messages about events. The test passed if you don't."""; + private static Frame frame; + private static volatile Point loc; + private static List list; + private static final int XOFFSET = 10; + private static final int YOFFSET = 20; public static void main(String[] args) throws Exception { - PassFailJFrame.builder() - .title("MouseDraggedOriginatedByScrollBarTest Instructions") - .instructions(INSTRUCTIONS) - .rows((int) INSTRUCTIONS.lines().count() + 2) - .columns(35) - .testUI(MouseDraggedOriginatedByScrollBarTest::createTestUI) - .logArea() - .build() - .awaitAndCheck(); + try { + EventQueue.invokeAndWait(() -> createUI()); + test(); + } finally { + EventQueue.invokeAndWait(() -> { + if (frame != null) { + frame.dispose(); + } + }); + } } - private static Frame createTestUI() { - Frame frame = new Frame(); - List list = new List(4, false); + private static void createUI() { + frame = new Frame(); + list = new List(4, false); list.add("000"); list.add("111"); @@ -77,27 +82,52 @@ public class MouseDraggedOriginatedByScrollBarTest { new MouseMotionAdapter(){ @Override public void mouseDragged(MouseEvent me){ - PassFailJFrame.log(me.toString()); + System.out.println(me); + throw new RuntimeException("Mouse dragged event detected."); } }); list.addMouseListener( new MouseAdapter() { public void mousePressed(MouseEvent me) { - PassFailJFrame.log(me.toString()); + System.out.println(me); + throw new RuntimeException("Mouse pressed event detected."); } public void mouseReleased(MouseEvent me) { - PassFailJFrame.log(me.toString()); + System.out.println(me); + throw new RuntimeException("Mouse released event detected."); } public void mouseClicked(MouseEvent me){ - PassFailJFrame.log(me.toString()); + System.out.println(me); + throw new RuntimeException("Mouse clicked event detected."); } }); frame.setLayout(new FlowLayout()); frame.pack(); - return frame; + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + private static void test() throws Exception { + Robot robot = new Robot(); + robot.waitForIdle(); + robot.delay(1000); + robot.setAutoWaitForIdle(true); + + EventQueue.invokeAndWait(() -> { + Point p = list.getLocationOnScreen(); + p.translate(list.getWidth() - XOFFSET, YOFFSET); + loc = p; + }); + robot.mouseMove(loc.x, loc.y); + robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); + for (int i = 0; i < 30; i++) { + robot.mouseMove(loc.x, loc.y + i); + } + robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); + robot.delay(100); } } From f68cba3d2fe3554c3cf0c3edf60ab639d6b13a6f Mon Sep 17 00:00:00 2001 From: Mohamed Issa Date: Tue, 23 Sep 2025 20:20:49 +0000 Subject: [PATCH 192/556] 8367611: Enable vblendvp[sd] on Future ECore Reviewed-by: mhaessig, sviswanathan, vpaprotski --- src/hotspot/cpu/x86/macroAssembler_x86.cpp | 6 ++++-- src/hotspot/cpu/x86/vm_version_x86.cpp | 4 ++++ src/hotspot/cpu/x86/vm_version_x86.hpp | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/hotspot/cpu/x86/macroAssembler_x86.cpp b/src/hotspot/cpu/x86/macroAssembler_x86.cpp index 0b41d594194..c1319b2ef7f 100644 --- a/src/hotspot/cpu/x86/macroAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/macroAssembler_x86.cpp @@ -2897,7 +2897,8 @@ void MacroAssembler::vbroadcastss(XMMRegister dst, AddressLiteral src, int vecto // vblendvps(XMMRegister dst, XMMRegister nds, XMMRegister src, XMMRegister mask, int vector_len, bool compute_mask = true, XMMRegister scratch = xnoreg) void MacroAssembler::vblendvps(XMMRegister dst, XMMRegister src1, XMMRegister src2, XMMRegister mask, int vector_len, bool compute_mask, XMMRegister scratch) { // WARN: Allow dst == (src1|src2), mask == scratch - bool blend_emulation = EnableX86ECoreOpts && UseAVX > 1; + bool blend_emulation = EnableX86ECoreOpts && UseAVX > 1 && + !(VM_Version::is_intel_darkmont() && (dst == src1)); // partially fixed on Darkmont bool scratch_available = scratch != xnoreg && scratch != src1 && scratch != src2 && scratch != dst; bool dst_available = dst != mask && (dst != src1 || dst != src2); if (blend_emulation && scratch_available && dst_available) { @@ -2921,7 +2922,8 @@ void MacroAssembler::vblendvps(XMMRegister dst, XMMRegister src1, XMMRegister sr // vblendvpd(XMMRegister dst, XMMRegister nds, XMMRegister src, XMMRegister mask, int vector_len, bool compute_mask = true, XMMRegister scratch = xnoreg) void MacroAssembler::vblendvpd(XMMRegister dst, XMMRegister src1, XMMRegister src2, XMMRegister mask, int vector_len, bool compute_mask, XMMRegister scratch) { // WARN: Allow dst == (src1|src2), mask == scratch - bool blend_emulation = EnableX86ECoreOpts && UseAVX > 1; + bool blend_emulation = EnableX86ECoreOpts && UseAVX > 1 && + !(VM_Version::is_intel_darkmont() && (dst == src1)); // partially fixed on Darkmont bool scratch_available = scratch != xnoreg && scratch != src1 && scratch != src2 && scratch != dst && (!compute_mask || scratch != mask); bool dst_available = dst != mask && (dst != src1 || dst != src2); if (blend_emulation && scratch_available && dst_available) { diff --git a/src/hotspot/cpu/x86/vm_version_x86.cpp b/src/hotspot/cpu/x86/vm_version_x86.cpp index f0dfdb22745..83daf932a27 100644 --- a/src/hotspot/cpu/x86/vm_version_x86.cpp +++ b/src/hotspot/cpu/x86/vm_version_x86.cpp @@ -2093,6 +2093,10 @@ bool VM_Version::is_intel_cascade_lake() { return is_intel_skylake() && _stepping >= 5; } +bool VM_Version::is_intel_darkmont() { + return is_intel() && is_intel_server_family() && (_model == 0xCC || _model == 0xDD); +} + // avx3_threshold() sets the threshold at which 64-byte instructions are used // for implementing the array copy and clear operations. // The Intel platforms that supports the serialize instruction diff --git a/src/hotspot/cpu/x86/vm_version_x86.hpp b/src/hotspot/cpu/x86/vm_version_x86.hpp index cd8e957ca9a..eaa9412fe85 100644 --- a/src/hotspot/cpu/x86/vm_version_x86.hpp +++ b/src/hotspot/cpu/x86/vm_version_x86.hpp @@ -938,6 +938,8 @@ public: static bool is_intel_cascade_lake(); + static bool is_intel_darkmont(); + static int avx3_threshold(); static bool is_intel_tsc_synched_at_init(); From f36c33c86df0400d2155bfadd9a6b5ea56743133 Mon Sep 17 00:00:00 2001 From: William Kemper Date: Wed, 24 Sep 2025 00:46:45 +0000 Subject: [PATCH 193/556] 8368152: Shenandoah: Incorrect behavior at end of degenerated cycle Reviewed-by: kdnilsen, ysr --- .../shenandoah/shenandoahCollectorPolicy.cpp | 16 +++-- .../shenandoah/shenandoahCollectorPolicy.hpp | 30 +++++++- .../gc/shenandoah/shenandoahDegeneratedGC.cpp | 35 +++------ .../gc/shenandoah/shenandoahDegeneratedGC.hpp | 1 - .../test_shenandoahCollectorPolicy.cpp | 72 +++++++++++++++++++ 5 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 test/hotspot/gtest/gc/shenandoah/test_shenandoahCollectorPolicy.cpp diff --git a/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.cpp b/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.cpp index 5136987578a..bbd9dca1513 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.cpp @@ -37,6 +37,7 @@ ShenandoahCollectorPolicy::ShenandoahCollectorPolicy() : _abbreviated_degenerated_gcs(0), _success_full_gcs(0), _consecutive_degenerated_gcs(0), + _consecutive_degenerated_gcs_without_progress(0), _consecutive_young_gcs(0), _mixed_gcs(0), _success_old_gcs(0), @@ -67,14 +68,14 @@ void ShenandoahCollectorPolicy::record_alloc_failure_to_degenerated(ShenandoahGC } void ShenandoahCollectorPolicy::record_degenerated_upgrade_to_full() { - _consecutive_degenerated_gcs = 0; + reset_consecutive_degenerated_gcs(); _alloc_failure_degenerated_upgrade_to_full++; } void ShenandoahCollectorPolicy::record_success_concurrent(bool is_young, bool is_abbreviated) { update_young(is_young); - _consecutive_degenerated_gcs = 0; + reset_consecutive_degenerated_gcs(); _success_concurrent_gcs++; if (is_abbreviated) { _abbreviated_concurrent_gcs++; @@ -95,11 +96,18 @@ void ShenandoahCollectorPolicy::record_interrupted_old() { _interrupted_old_gcs++; } -void ShenandoahCollectorPolicy::record_success_degenerated(bool is_young, bool is_abbreviated) { +void ShenandoahCollectorPolicy::record_degenerated(bool is_young, bool is_abbreviated, bool progress) { update_young(is_young); _success_degenerated_gcs++; _consecutive_degenerated_gcs++; + + if (progress) { + _consecutive_degenerated_gcs_without_progress = 0; + } else { + _consecutive_degenerated_gcs_without_progress++; + } + if (is_abbreviated) { _abbreviated_degenerated_gcs++; } @@ -114,7 +122,7 @@ void ShenandoahCollectorPolicy::update_young(bool is_young) { } void ShenandoahCollectorPolicy::record_success_full() { - _consecutive_degenerated_gcs = 0; + reset_consecutive_degenerated_gcs(); _consecutive_young_gcs = 0; _success_full_gcs++; } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.hpp b/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.hpp index 68579508de5..5fe90f64f98 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahCollectorPolicy.hpp @@ -42,6 +42,7 @@ private: // Written by control thread, read by mutators volatile size_t _success_full_gcs; uint _consecutive_degenerated_gcs; + uint _consecutive_degenerated_gcs_without_progress; volatile size_t _consecutive_young_gcs; size_t _mixed_gcs; size_t _success_old_gcs; @@ -55,8 +56,25 @@ private: ShenandoahSharedFlag _in_shutdown; ShenandoahTracer* _tracer; + void reset_consecutive_degenerated_gcs() { + _consecutive_degenerated_gcs = 0; + _consecutive_degenerated_gcs_without_progress = 0; + } public: + // The most common scenario for lack of good progress following a degenerated GC is an accumulation of floating + // garbage during the most recently aborted concurrent GC effort. With generational GC, it is far more effective to + // reclaim this floating garbage with another degenerated cycle (which focuses on young generation and might require + // a pause of 200 ms) rather than a full GC cycle (which may require over 2 seconds with a 10 GB old generation). + // + // In generational mode, we'll only upgrade to full GC if we've done two degen cycles in a row and both indicated + // bad progress. In non-generational mode, we'll preserve the original behavior, which is to upgrade to full + // immediately following a degenerated cycle with bad progress. This preserves original behavior of non-generational + // Shenandoah to avoid introducing "surprising new behavior." It also makes less sense with non-generational + // Shenandoah to replace a full GC with a degenerated GC, because both have similar pause times in non-generational + // mode. + static constexpr size_t GENERATIONAL_CONSECUTIVE_BAD_DEGEN_PROGRESS_THRESHOLD = 2; + ShenandoahCollectorPolicy(); void record_mixed_cycle(); @@ -69,7 +87,12 @@ public: // cycles are very efficient and are worth tracking. Note that both degenerated and // concurrent cycles can be abbreviated. void record_success_concurrent(bool is_young, bool is_abbreviated); - void record_success_degenerated(bool is_young, bool is_abbreviated); + + // Record that a degenerated cycle has been completed. Note that such a cycle may or + // may not make "progress". We separately track the total number of degenerated cycles, + // the number of consecutive degenerated cycles and the number of consecutive cycles that + // fail to make good progress. + void record_degenerated(bool is_young, bool is_abbreviated, bool progress); void record_success_full(); void record_alloc_failure_to_degenerated(ShenandoahGC::ShenandoahDegenPoint point); void record_alloc_failure_to_full(); @@ -94,6 +117,11 @@ public: return _consecutive_degenerated_gcs; } + // Genshen will only upgrade to a full gc after the configured number of futile degenerated cycles. + bool generational_should_upgrade_degenerated_gc() const { + return _consecutive_degenerated_gcs_without_progress >= GENERATIONAL_CONSECUTIVE_BAD_DEGEN_PROGRESS_THRESHOLD; + } + static bool is_allocation_failure(GCCause::Cause cause); static bool is_shenandoah_gc(GCCause::Cause cause); static bool is_requested_gc(GCCause::Cause cause); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp b/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp index a99fa3ae62c..bbecd12f095 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp @@ -49,8 +49,7 @@ ShenandoahDegenGC::ShenandoahDegenGC(ShenandoahDegenPoint degen_point, Shenandoa ShenandoahGC(), _degen_point(degen_point), _generation(generation), - _abbreviated(false), - _consecutive_degen_with_bad_progress(0) { + _abbreviated(false) { } bool ShenandoahDegenGC::collect(GCCause::Cause cause) { @@ -247,7 +246,6 @@ void ShenandoahDegenGC::op_degenerated() { ShenandoahHeapRegion* r; while ((r = heap->collection_set()->next()) != nullptr) { if (r->is_pinned()) { - heap->cancel_gc(GCCause::_shenandoah_upgrade_to_full_gc); op_degenerated_fail(); return; } @@ -312,30 +310,14 @@ void ShenandoahDegenGC::op_degenerated() { metrics.snap_after(); - // The most common scenario for lack of good progress following a degenerated GC is an accumulation of floating - // garbage during the most recently aborted concurrent GC effort. With generational GC, it is far more effective to - // reclaim this floating garbage with another degenerated cycle (which focuses on young generation and might require - // a pause of 200 ms) rather than a full GC cycle (which may require over 2 seconds with a 10 GB old generation). - // - // In generational mode, we'll only upgrade to full GC if we've done two degen cycles in a row and both indicated - // bad progress. In non-generational mode, we'll preserve the original behavior, which is to upgrade to full - // immediately following a degenerated cycle with bad progress. This preserves original behavior of non-generational - // Shenandoah so as to avoid introducing "surprising new behavior." It also makes less sense with non-generational - // Shenandoah to replace a full GC with a degenerated GC, because both have similar pause times in non-generational - // mode. - if (!metrics.is_good_progress(_generation)) { - _consecutive_degen_with_bad_progress++; - } else { - _consecutive_degen_with_bad_progress = 0; - } - if (!heap->mode()->is_generational() || - ((heap->shenandoah_policy()->consecutive_degenerated_gc_count() > 1) && (_consecutive_degen_with_bad_progress >= 2))) { - heap->cancel_gc(GCCause::_shenandoah_upgrade_to_full_gc); - op_degenerated_futile(); - } else { + // Decide if this cycle made good progress, and, if not, should it upgrade to a full GC. + const bool progress = metrics.is_good_progress(_generation); + ShenandoahCollectorPolicy* policy = heap->shenandoah_policy(); + policy->record_degenerated(_generation->is_young(), _abbreviated, progress); + if (progress) { heap->notify_gc_progress(); - heap->shenandoah_policy()->record_success_degenerated(_generation->is_young(), _abbreviated); - _generation->heuristics()->record_success_degenerated(); + } else if (!heap->mode()->is_generational() || policy->generational_should_upgrade_degenerated_gc()) { + op_degenerated_futile(); } } @@ -483,6 +465,7 @@ const char* ShenandoahDegenGC::degen_event_message(ShenandoahDegenPoint point) c void ShenandoahDegenGC::upgrade_to_full() { log_info(gc)("Degenerated GC upgrading to Full GC"); ShenandoahHeap* heap = ShenandoahHeap::heap(); + heap->cancel_gc(GCCause::_shenandoah_upgrade_to_full_gc); heap->increment_total_collections(true); heap->shenandoah_policy()->record_degenerated_upgrade_to_full(); ShenandoahFullGC full_gc; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.hpp b/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.hpp index a2598fe4e8e..971bd67eb0d 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.hpp @@ -36,7 +36,6 @@ private: const ShenandoahDegenPoint _degen_point; ShenandoahGeneration* _generation; bool _abbreviated; - size_t _consecutive_degen_with_bad_progress; public: ShenandoahDegenGC(ShenandoahDegenPoint degen_point, ShenandoahGeneration* generation); diff --git a/test/hotspot/gtest/gc/shenandoah/test_shenandoahCollectorPolicy.cpp b/test/hotspot/gtest/gc/shenandoah/test_shenandoahCollectorPolicy.cpp new file mode 100644 index 00000000000..b5c974f65ad --- /dev/null +++ b/test/hotspot/gtest/gc/shenandoah/test_shenandoahCollectorPolicy.cpp @@ -0,0 +1,72 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +#include "gc/shenandoah/shenandoahCollectorPolicy.hpp" +#include "unittest.hpp" + +TEST(ShenandoahCollectorPolicyTest, track_degen_cycles_sanity) { + ShenandoahCollectorPolicy policy; + EXPECT_EQ(policy.consecutive_degenerated_gc_count(), 0UL); + EXPECT_EQ(policy.generational_should_upgrade_degenerated_gc(), false); +} + +TEST(ShenandoahCollectorPolicyTest, track_degen_cycles_no_upgrade) { + ShenandoahCollectorPolicy policy; + policy.record_degenerated(true, true, true); + policy.record_degenerated(true, true, true); + EXPECT_EQ(policy.consecutive_degenerated_gc_count(), 2UL); + EXPECT_EQ(policy.generational_should_upgrade_degenerated_gc(), false); +} + +TEST(ShenandoahCollectorPolicyTest, track_degen_cycles_upgrade) { + ShenandoahCollectorPolicy policy; + policy.record_degenerated(true, true, false); + policy.record_degenerated(true, true, false); + EXPECT_EQ(policy.consecutive_degenerated_gc_count(), 2UL); + EXPECT_EQ(policy.generational_should_upgrade_degenerated_gc(), true); +} + +TEST(ShenandoahCollectorPolicyTest, track_degen_cycles_reset_progress) { + ShenandoahCollectorPolicy policy; + policy.record_degenerated(true, true, false); + policy.record_degenerated(true, true, true); + EXPECT_EQ(policy.consecutive_degenerated_gc_count(), 2UL); + EXPECT_EQ(policy.generational_should_upgrade_degenerated_gc(), false); +} + +TEST(ShenandoahCollectorPolicyTest, track_degen_cycles_full_reset) { + ShenandoahCollectorPolicy policy; + policy.record_degenerated(true, true, false); + policy.record_success_full(); + EXPECT_EQ(policy.consecutive_degenerated_gc_count(), 0UL); + EXPECT_EQ(policy.generational_should_upgrade_degenerated_gc(), false); +} + +TEST(ShenandoahCollectorPolicyTest, track_degen_cycles_reset) { + ShenandoahCollectorPolicy policy; + policy.record_degenerated(true, true, false); + policy.record_success_concurrent(true, true); + EXPECT_EQ(policy.consecutive_degenerated_gc_count(), 0UL); + EXPECT_EQ(policy.generational_should_upgrade_degenerated_gc(), false); +} From 528f93f8cb9f1fb9c19f31ab80c8a546f47beed2 Mon Sep 17 00:00:00 2001 From: erifan Date: Wed, 24 Sep 2025 01:35:51 +0000 Subject: [PATCH 194/556] 8367391: Loss of precision on implicit conversion in vectornode.cpp Reviewed-by: chagedorn, roland --- src/hotspot/share/opto/vectornode.cpp | 4 +- .../vectorapi/VectorMaskFromLongTest.java | 56 ++++++++++--------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/hotspot/share/opto/vectornode.cpp b/src/hotspot/share/opto/vectornode.cpp index ae9ef552df4..d656bf3127b 100644 --- a/src/hotspot/share/opto/vectornode.cpp +++ b/src/hotspot/share/opto/vectornode.cpp @@ -439,8 +439,8 @@ bool VectorNode::is_maskall_type(const TypeLong* type, int vlen) { if (!type->is_con()) { return false; } - long mask = (-1ULL >> (64 - vlen)); - long bit = type->get_con() & mask; + jlong mask = (-1ULL >> (64 - vlen)); + jlong bit = type->get_con() & mask; return bit == 0 || bit == mask; } diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskFromLongTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskFromLongTest.java index a97ce2f9162..eaa6211efc5 100644 --- a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskFromLongTest.java +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskFromLongTest.java @@ -23,7 +23,7 @@ /* * @test -* @bug 8356760 +* @bug 8356760 8367391 * @library /test/lib / * @summary Optimize VectorMask.fromLong for all-true/all-false cases * @modules jdk.incubator.vector @@ -173,92 +173,98 @@ public class VectorMaskFromLongTest { @Test @IR(counts = { IRNode.MASK_ALL, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureOr = { "sve2", "true", "avx512", "true", "rvv", "true" }) @IR(counts = { IRNode.REPLICATE_B, "= 0", IRNode.VECTOR_LONG_TO_MASK, "= 0" }, applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) @IR(counts = { IRNode.REPLICATE_B, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureAnd = { "avx2", "true", "avx512", "false" }) public static void testMaskFromLongByte() { - // Test the case where some but not all bits are set. - testMaskFromLong(B_SPECIES, (-1L >>> (64 - B_SPECIES.length()))-1); + // Test cases where some but not all bits are set. + testMaskFromLong(B_SPECIES, (-1L >>> (64 - B_SPECIES.length())) - 1); + testMaskFromLong(B_SPECIES, (-1L >>> (64 - B_SPECIES.length())) >>> 1); } @Test @IR(counts = { IRNode.MASK_ALL, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureOr = { "sve2", "true", "avx512", "true", "rvv", "true" }) @IR(counts = { IRNode.REPLICATE_S, "= 0", IRNode.VECTOR_LONG_TO_MASK, "= 0" }, applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) @IR(counts = { IRNode.REPLICATE_S, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureAnd = { "avx2", "true", "avx512", "false" }) public static void testMaskFromLongShort() { - // Test the case where some but not all bits are set. - testMaskFromLong(S_SPECIES, (-1L >>> (64 - S_SPECIES.length()))-1); + // Test cases where some but not all bits are set. + testMaskFromLong(S_SPECIES, (-1L >>> (64 - S_SPECIES.length())) - 1); + testMaskFromLong(S_SPECIES, (-1L >>> (64 - S_SPECIES.length())) >>> 1); } @Test @IR(counts = { IRNode.MASK_ALL, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureOr = { "sve2", "true", "avx512", "true", "rvv", "true" }) @IR(counts = { IRNode.REPLICATE_I, "= 0", IRNode.VECTOR_LONG_TO_MASK, "= 0" }, applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) @IR(counts = { IRNode.REPLICATE_I, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureAnd = { "avx2", "true", "avx512", "false" }) public static void testMaskFromLongInt() { - // Test the case where some but not all bits are set. - testMaskFromLong(I_SPECIES, (-1L >>> (64 - I_SPECIES.length()))-1); + // Test cases where some but not all bits are set. + testMaskFromLong(I_SPECIES, (-1L >>> (64 - I_SPECIES.length())) - 1); + testMaskFromLong(I_SPECIES, (-1L >>> (64 - I_SPECIES.length())) >>> 1); } @Test @IR(counts = { IRNode.MASK_ALL, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureOr = { "sve2", "true", "avx512", "true", "rvv", "true" }) @IR(counts = { IRNode.REPLICATE_L, "= 0", IRNode.VECTOR_LONG_TO_MASK, "= 0" }, applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) @IR(counts = { IRNode.REPLICATE_L, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureAnd = { "avx2", "true", "avx512", "false" }) public static void testMaskFromLongLong() { - // Test the case where some but not all bits are set. - testMaskFromLong(L_SPECIES, (-1L >>> (64 - L_SPECIES.length()))-1); + // Test cases where some but not all bits are set. + testMaskFromLong(L_SPECIES, (-1L >>> (64 - L_SPECIES.length())) - 1); + testMaskFromLong(L_SPECIES, (-1L >>> (64 - L_SPECIES.length())) >>> 1); } @Test @IR(counts = { IRNode.MASK_ALL, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureOr = { "sve2", "true", "avx512", "true", "rvv", "true" }) @IR(counts = { IRNode.REPLICATE_I, "= 0", IRNode.VECTOR_LONG_TO_MASK, "= 0" }, applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) @IR(counts = { IRNode.REPLICATE_I, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureAnd = { "avx2", "true", "avx512", "false" }) public static void testMaskFromLongFloat() { - // Test the case where some but not all bits are set. - testMaskFromLong(F_SPECIES, (-1L >>> (64 - F_SPECIES.length()))-1); + // Test cases where some but not all bits are set. + testMaskFromLong(F_SPECIES, (-1L >>> (64 - F_SPECIES.length())) - 1); + testMaskFromLong(F_SPECIES, (-1L >>> (64 - F_SPECIES.length())) >>> 1); } @Test @IR(counts = { IRNode.MASK_ALL, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureOr = { "sve2", "true", "avx512", "true", "rvv", "true" }) @IR(counts = { IRNode.REPLICATE_L, "= 0", IRNode.VECTOR_LONG_TO_MASK, "= 0" }, applyIfCPUFeatureAnd = { "asimd", "true", "sve", "false" }) @IR(counts = { IRNode.REPLICATE_L, "= 0", - IRNode.VECTOR_LONG_TO_MASK, "> 0" }, + IRNode.VECTOR_LONG_TO_MASK, "= 2" }, applyIfCPUFeatureAnd = { "avx2", "true", "avx512", "false" }) public static void testMaskFromLongDouble() { - // Test the case where some but not all bits are set. - testMaskFromLong(D_SPECIES, (-1L >>> (64 - D_SPECIES.length()))-1); + // Test cases where some but not all bits are set. + testMaskFromLong(D_SPECIES, (-1L >>> (64 - D_SPECIES.length())) - 1); + testMaskFromLong(D_SPECIES, (-1L >>> (64 - D_SPECIES.length())) >>> 1); } public static void main(String[] args) { From 7d3452b37eceff7309dc6b5285e3da31a3c398ec Mon Sep 17 00:00:00 2001 From: Prasanta Sadhukhan Date: Wed, 24 Sep 2025 02:50:09 +0000 Subject: [PATCH 195/556] 8368181: ProblemList java/awt/Dialog/ModalExcludedTest/ModalExcludedTest.java Reviewed-by: serb, azvegint --- test/jdk/ProblemList.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 8b99564b967..3cc4fa835df 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -270,6 +270,7 @@ java/awt/Clipboard/ClipboardSecurity.java 8054809 macosx-all java/awt/Clipboard/GetAltContentsTest/SystemClipboardTest.java 8234140 macosx-all java/awt/Clipboard/ImageTransferTest.java 8030710 generic-all java/awt/Clipboard/NoDataConversionFailureTest.java 8234140 macosx-all +java/awt/Dialog/ModalExcludedTest.java 7125054 macosx-all java/awt/Frame/MiscUndecorated/RepaintTest.java 8266244 macosx-aarch64 java/awt/Modal/FileDialog/FileDialogAppModal1Test.java 7186009 macosx-all java/awt/Modal/FileDialog/FileDialogAppModal2Test.java 7186009 macosx-all From 5350ce105973945e899b82a4c066d6ec5439102d Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Wed, 24 Sep 2025 06:03:59 +0000 Subject: [PATCH 196/556] 8368373: Test H3MalformedResponseTest.testMalformedResponse intermittent timed out Reviewed-by: dfuchs, djelinski --- .../jdk/java/net/httpclient/http3/H3MalformedResponseTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java b/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java index 06b12b6a477..16e110b5fa0 100644 --- a/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java +++ b/test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java @@ -29,6 +29,7 @@ import jdk.internal.net.http.quic.TerminationCause; import jdk.internal.net.quic.QuicVersion; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; +import jdk.test.lib.Utils; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; @@ -368,7 +369,7 @@ public class H3MalformedResponseTest implements HttpServerAdapters { final HttpResponse response1 = client.sendAsync( request, BodyHandlers.discarding()) - .get(10, TimeUnit.SECONDS); + .get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response1); } catch (TimeoutException e) { throw e; From 3183a13f666ff38c03c0628e139998803be8a719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jeli=C5=84ski?= Date: Wed, 24 Sep 2025 06:19:24 +0000 Subject: [PATCH 197/556] 8368073: PKCS11 HKDF can't use byte array IKM in FIPS mode Reviewed-by: valeriep --- .../classes/sun/security/pkcs11/P11HKDF.java | 103 ++++++++++++++---- .../pkcs11/tls/tls12/FipsModeTLS12.java | 52 +++++---- .../jdk/sun/security/pkcs11/tls/tls12/nss.cfg | 8 ++ 3 files changed, 113 insertions(+), 50 deletions(-) diff --git a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11HKDF.java b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11HKDF.java index e8bef222d88..b93a8d9b98a 100644 --- a/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11HKDF.java +++ b/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11HKDF.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2025, Red Hat, Inc. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -167,32 +168,45 @@ final class P11HKDF extends KDFSpi { checkDerivedKeyType(ki, alg); P11KeyGenerator.checkKeySize(ki.keyGenMech, outLen * 8, token); - P11Key p11BaseKey = convertKey(baseKey, (isExtract ? "IKM" : "PRK") + - " could not be converted to a token key for HKDF derivation."); - + long baseKeyID; + P11Key p11BaseKey = null; + try { + p11BaseKey = convertKey(baseKey, (isExtract ? "IKM" : "PRK") + + " could not be converted to a token key for HKDF derivation."); + baseKeyID = p11BaseKey.getKeyID(); + } catch (ProviderException pe) { + if (p11BaseKey != null) { + throw pe; + } + // special handling for FIPS mode when key cannot be imported + if (isExtract) { + baseKeyID = convertKeyToData(baseKey, pe); + } else { + throw pe; + } + } + Session session = null; long saltType = CKF_HKDF_SALT_NULL; byte[] saltBytes = null; P11Key p11SaltKey = null; - if (salt instanceof SecretKeySpec) { - saltType = CKF_HKDF_SALT_DATA; - saltBytes = salt.getEncoded(); - } else if (salt != EMPTY_KEY) { - // consolidateKeyMaterial returns a salt from the token. - saltType = CKF_HKDF_SALT_KEY; - p11SaltKey = (P11Key.P11SecretKey) salt; - assert p11SaltKey.token == token : "salt must be from the same " + - "token as service."; - } - - long derivedKeyClass = isData ? CKO_DATA : CKO_SECRET_KEY; - CK_ATTRIBUTE[] attrs = new CK_ATTRIBUTE[] { - new CK_ATTRIBUTE(CKA_CLASS, derivedKeyClass), - new CK_ATTRIBUTE(CKA_KEY_TYPE, ki.keyType), - new CK_ATTRIBUTE(CKA_VALUE_LEN, outLen) - }; - Session session = null; - long baseKeyID = p11BaseKey.getKeyID(); try { + if (salt instanceof SecretKeySpec) { + saltType = CKF_HKDF_SALT_DATA; + saltBytes = salt.getEncoded(); + } else if (salt != EMPTY_KEY) { + // consolidateKeyMaterial returns a salt from the token. + saltType = CKF_HKDF_SALT_KEY; + p11SaltKey = (P11Key.P11SecretKey) salt; + assert p11SaltKey.token == token : "salt must be from the same " + + "token as service."; + } + + long derivedKeyClass = isData ? CKO_DATA : CKO_SECRET_KEY; + CK_ATTRIBUTE[] attrs = new CK_ATTRIBUTE[] { + new CK_ATTRIBUTE(CKA_CLASS, derivedKeyClass), + new CK_ATTRIBUTE(CKA_KEY_TYPE, ki.keyType), + new CK_ATTRIBUTE(CKA_VALUE_LEN, outLen) + }; session = token.getOpSession(); CK_HKDF_PARAMS params = new CK_HKDF_PARAMS(isExtract, isExpand, svcKi.hmacMech, saltType, saltBytes, p11SaltKey != null ? @@ -230,11 +244,54 @@ final class P11HKDF extends KDFSpi { if (p11SaltKey != null) { p11SaltKey.releaseKeyID(); } - p11BaseKey.releaseKeyID(); + if (p11BaseKey != null) { + p11BaseKey.releaseKeyID(); + } else { + destroyDataObject(baseKeyID); + } token.releaseSession(session); } } + private void destroyDataObject(long baseKeyID) { + try { + Session session = token.getObjSession(); + try { + token.p11.C_DestroyObject(session.id(), baseKeyID); + } finally { + token.releaseSession(session); + } + } catch (PKCS11Exception e) { + throw new ProviderException("Failed to destroy IKM data object.", e); + } + } + + private long convertKeyToData(SecretKey key, ProviderException pe) { + if (!"RAW".equalsIgnoreCase(key.getFormat())) { + throw pe; + } + byte[] keyBytes = key.getEncoded(); + if (keyBytes == null) { + throw pe; + } + CK_ATTRIBUTE[] inputAttributes = new CK_ATTRIBUTE[]{ + new CK_ATTRIBUTE(CKA_CLASS, CKO_DATA), + new CK_ATTRIBUTE(CKA_VALUE, keyBytes), + }; + try { + Session session = token.getObjSession(); + try { + return token.p11.C_CreateObject(session.id(), inputAttributes); + } finally { + token.releaseSession(session); + } + } catch (PKCS11Exception e) { + throw new ProviderException("Failed to create IKM data object.", e); + } finally { + Arrays.fill(keyBytes, (byte)0); + } + } + private static boolean canDeriveKeyInfoType(long t) { return (t == CKK_DES || t == CKK_DES3 || t == CKK_AES || t == CKK_RC4 || t == CKK_BLOWFISH || t == CKK_CHACHA20 || diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/FipsModeTLS12.java b/test/jdk/sun/security/pkcs11/tls/tls12/FipsModeTLS12.java index f9e9bd472c7..c0af4d71498 100644 --- a/test/jdk/sun/security/pkcs11/tls/tls12/FipsModeTLS12.java +++ b/test/jdk/sun/security/pkcs11/tls/tls12/FipsModeTLS12.java @@ -1,6 +1,6 @@ /* * Copyright (c) 2019, Red Hat, Inc. - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,13 +24,16 @@ /* * @test - * @bug 8029661 8325164 - * @summary Test TLS 1.2 + * @bug 8029661 8325164 8368073 + * @summary Test TLS 1.2 and TLS 1.3 * @modules java.base/sun.security.internal.spec * java.base/sun.security.util * java.base/com.sun.crypto.provider * @library /test/lib ../.. - * @run main/othervm/timeout=120 -Djdk.tls.useExtendedMasterSecret=false FipsModeTLS12 + * @run main/othervm/timeout=120 -Djdk.tls.client.protocols=TLSv1.2 + * -Djdk.tls.useExtendedMasterSecret=false FipsModeTLS12 + * @comment SunPKCS11 does not support (TLS1.2) SunTlsExtendedMasterSecret yet + * @run main/othervm/timeout=120 -Djdk.tls.client.protocols=TLSv1.3 FipsModeTLS12 */ import java.io.File; @@ -51,6 +54,7 @@ import java.util.LinkedList; import java.util.List; import javax.crypto.Cipher; +import javax.crypto.KDF; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -82,9 +86,6 @@ public final class FipsModeTLS12 extends SecmodTest { private static PublicKey publicKey; public static void main(String[] args) throws Exception { - // Re-enable TLS_RSA_* since test depends on it. - SecurityUtils.removeFromDisabledTlsAlgs("TLS_RSA_*"); - try { initialize(); } catch (Exception e) { @@ -115,11 +116,16 @@ public final class FipsModeTLS12 extends SecmodTest { return false; } try { - KeyGenerator.getInstance("SunTls12MasterSecret", - sunPKCS11NSSProvider); - KeyGenerator.getInstance( - "SunTls12RsaPremasterSecret", sunPKCS11NSSProvider); - KeyGenerator.getInstance("SunTls12Prf", sunPKCS11NSSProvider); + String proto = System.getProperty("jdk.tls.client.protocols"); + if ("TLSv1.3".equals(proto)) { + KDF.getInstance("HKDF-SHA256", sunPKCS11NSSProvider); + } else { + KeyGenerator.getInstance("SunTls12MasterSecret", + sunPKCS11NSSProvider); + KeyGenerator.getInstance( + "SunTls12RsaPremasterSecret", sunPKCS11NSSProvider); + KeyGenerator.getInstance("SunTls12Prf", sunPKCS11NSSProvider); + } } catch (NoSuchAlgorithmException e) { return false; } @@ -384,21 +390,9 @@ public final class FipsModeTLS12 extends SecmodTest { private static SSLEngine[][] getSSLEnginesToTest() throws Exception { SSLEngine[][] enginesToTest = new SSLEngine[2][2]; - // TLS_RSA_WITH_AES_128_GCM_SHA256 ciphersuite is available but - // must not be chosen for the TLS connection if not supported. - // See JDK-8222937. - String[][] preferredSuites = new String[][]{ new String[] { - "TLS_RSA_WITH_AES_128_GCM_SHA256", - "TLS_RSA_WITH_AES_128_CBC_SHA256" - }, new String[] { - "TLS_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256" - }}; for (int i = 0; i < enginesToTest.length; i++) { enginesToTest[i][0] = createSSLEngine(true); enginesToTest[i][1] = createSSLEngine(false); - // All CipherSuites enabled for the client. - enginesToTest[i][1].setEnabledCipherSuites(preferredSuites[i]); } return enginesToTest; } @@ -412,23 +406,27 @@ public final class FipsModeTLS12 extends SecmodTest { TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "SunJSSE"); tmf.init(ts); - SSLContext sslCtx = SSLContext.getInstance("TLSv1.2", "SunJSSE"); + SSLContext sslCtx = SSLContext.getInstance("TLS", "SunJSSE"); sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); ssle = sslCtx.createSSLEngine("localhost", 443); ssle.setUseClientMode(client); SSLParameters sslParameters = ssle.getSSLParameters(); // verify that FFDHE named groups are available - boolean ffdheAvailable = Arrays.stream(sslParameters.getNamedGroups()) + String[] namedGroups = sslParameters.getNamedGroups(); + boolean ffdheAvailable = Arrays.stream(namedGroups) .anyMatch(ng -> ng.startsWith("ffdhe")); if (!ffdheAvailable) { throw new RuntimeException("No FFDHE named groups available"); } // verify that ECDHE named groups are available - boolean ecdheAvailable = Arrays.stream(sslParameters.getNamedGroups()) + boolean ecdheAvailable = Arrays.stream(namedGroups) .anyMatch(ng -> ng.startsWith("secp")); if (!ecdheAvailable) { throw new RuntimeException("No ECDHE named groups available"); } + // remove XDH named groups - not available in PKCS11 + namedGroups = Arrays.stream(namedGroups).filter(s-> !s.startsWith("x")).toArray(String[]::new); + sslParameters.setNamedGroups(namedGroups); ssle.setSSLParameters(sslParameters); return ssle; diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/nss.cfg b/test/jdk/sun/security/pkcs11/tls/tls12/nss.cfg index 349c783af08..1f28ddf9ac8 100644 --- a/test/jdk/sun/security/pkcs11/tls/tls12/nss.cfg +++ b/test/jdk/sun/security/pkcs11/tls/tls12/nss.cfg @@ -7,6 +7,14 @@ nssLibraryDirectory = ${pkcs11test.nss.libdir} nssModule = fips +# NSS-FIPS needs sensitive=true for key extraction. +# TLS 1.3 needs CKA_SIGN to sign the Finished message. + +attributes(*,CKO_SECRET_KEY,CKK_GENERIC_SECRET) = { + CKA_SIGN = true + CKA_SENSITIVE=true +} + # NSS needs CKA_NETSCAPE_DB for DSA and DH private keys # just put an arbitrary value in there to make it happy From 303686684c23db465ccfb6a9b4861a673bfa5f4b Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Wed, 24 Sep 2025 06:28:13 +0000 Subject: [PATCH 198/556] 8367972: ZGC: Reduce ZBarrierSet includes Reviewed-by: stefank, eosterlund --- src/hotspot/share/gc/z/zBarrierSet.cpp | 107 +++++++++++++++--- src/hotspot/share/gc/z/zBarrierSet.hpp | 19 +++- src/hotspot/share/gc/z/zBarrierSet.inline.hpp | 74 ++++-------- src/hotspot/share/gc/z/zObjArrayAllocator.cpp | 1 + .../share/gc/z/zPhysicalMemoryManager.cpp | 1 + .../share/gc/z/zRangeRegistry.inline.hpp | 1 + src/hotspot/share/precompiled/precompiled.hpp | 1 - src/hotspot/share/prims/whitebox.cpp | 1 + src/hotspot/share/runtime/stackValue.cpp | 3 - .../hotspot/gtest/runtime/test_os_windows.cpp | 2 +- 10 files changed, 137 insertions(+), 73 deletions(-) diff --git a/src/hotspot/share/gc/z/zBarrierSet.cpp b/src/hotspot/share/gc/z/zBarrierSet.cpp index 31996def9c1..87f93043bdf 100644 --- a/src/hotspot/share/gc/z/zBarrierSet.cpp +++ b/src/hotspot/share/gc/z/zBarrierSet.cpp @@ -21,6 +21,8 @@ * questions. */ +#include "gc/z/zAddress.inline.hpp" +#include "gc/z/zBarrier.inline.hpp" #include "gc/z/zBarrierSet.hpp" #include "gc/z/zBarrierSetAssembler.hpp" #include "gc/z/zBarrierSetNMethod.hpp" @@ -30,6 +32,7 @@ #include "gc/z/zHeap.inline.hpp" #include "gc/z/zStackWatermark.hpp" #include "gc/z/zThreadLocalData.hpp" +#include "runtime/atomicAccess.hpp" #include "runtime/deoptimization.hpp" #include "runtime/frame.inline.hpp" #include "runtime/javaThread.hpp" @@ -46,6 +49,96 @@ class ZBarrierSetC1; class ZBarrierSetC2; +class ZColorStoreGoodOopClosure : public BasicOopIterateClosure { +public: + virtual void do_oop(oop* p_) { + volatile zpointer* const p = (volatile zpointer*)p_; + const zpointer ptr = ZBarrier::load_atomic(p); + const zaddress addr = ZPointer::uncolor(ptr); + AtomicAccess::store(p, ZAddress::store_good(addr)); + } + + virtual void do_oop(narrowOop* p) { + ShouldNotReachHere(); + } +}; + +class ZLoadBarrierOopClosure : public BasicOopIterateClosure { +public: + virtual void do_oop(oop* p) { + ZBarrier::load_barrier_on_oop_field((zpointer*)p); + } + + virtual void do_oop(narrowOop* p) { + ShouldNotReachHere(); + } +}; + +void ZBarrierSet::load_barrier_all(oop src, size_t size) { + check_is_valid_zaddress(src); + + ZLoadBarrierOopClosure cl; + ZIterator::oop_iterate(src, &cl); +} + +void ZBarrierSet::color_store_good_all(oop dst, size_t size) { + check_is_valid_zaddress(dst); + assert(dst->is_typeArray() || ZHeap::heap()->is_young(to_zaddress(dst)), "ZColorStoreGoodOopClosure is only valid for young objects"); + + ZColorStoreGoodOopClosure cl_sg; + ZIterator::oop_iterate(dst, &cl_sg); +} + +zaddress ZBarrierSet::load_barrier_on_oop_field_preloaded(volatile zpointer* p, zpointer o) { + return ZBarrier::load_barrier_on_oop_field_preloaded(p, o); +} + +zaddress ZBarrierSet::no_keep_alive_load_barrier_on_weak_oop_field_preloaded(volatile zpointer* p, zpointer o) { + return ZBarrier::no_keep_alive_load_barrier_on_weak_oop_field_preloaded(p, o); +} + +zaddress ZBarrierSet::no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(volatile zpointer* p, zpointer o) { + return ZBarrier::no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(p, o); +} + +zaddress ZBarrierSet::load_barrier_on_weak_oop_field_preloaded(volatile zpointer* p, zpointer o) { + return ZBarrier::load_barrier_on_weak_oop_field_preloaded(p, o); +} + +zaddress ZBarrierSet::load_barrier_on_phantom_oop_field_preloaded(volatile zpointer* p, zpointer o) { + return ZBarrier::load_barrier_on_phantom_oop_field_preloaded(p, o); +} + +void ZBarrierSet::store_barrier_on_heap_oop_field(volatile zpointer* p, bool heal) { + ZBarrier::store_barrier_on_heap_oop_field(p, heal); +} + +void ZBarrierSet::no_keep_alive_store_barrier_on_heap_oop_field(volatile zpointer* p) { + ZBarrier::no_keep_alive_store_barrier_on_heap_oop_field(p); +} + +void ZBarrierSet::store_barrier_on_native_oop_field(volatile zpointer* p, bool heal) { + ZBarrier::store_barrier_on_native_oop_field(p, heal); +} + +zaddress ZBarrierSet::load_barrier_on_oop_field(volatile zpointer* p) { + return ZBarrier::load_barrier_on_oop_field(p); +} + +void ZBarrierSet::clone_obj_array(objArrayOop src_obj, objArrayOop dst_obj) { + volatile zpointer* src = (volatile zpointer*)src_obj->base(); + volatile zpointer* dst = (volatile zpointer*)dst_obj->base(); + const int length = src_obj->length(); + + for (const volatile zpointer* const end = src + length; src < end; src++, dst++) { + zaddress elem = ZBarrier::load_barrier_on_oop_field(src); + // We avoid healing here because the store below colors the pointer store good, + // hence avoiding the cost of a CAS. + ZBarrier::store_barrier_on_heap_oop_field(dst, false /* heal */); + AtomicAccess::store(dst, ZAddress::store_good(elem)); + } +} + ZBarrierSet::ZBarrierSet() : BarrierSet(make_barrier_set_assembler(), make_barrier_set_c1(), @@ -153,20 +246,6 @@ void ZBarrierSet::on_slowpath_allocation_exit(JavaThread* thread, oop new_obj) { deoptimize_allocation(thread); } -void ZBarrierSet::clone_obj_array(objArrayOop src_obj, objArrayOop dst_obj) { - volatile zpointer* src = (volatile zpointer*)src_obj->base(); - volatile zpointer* dst = (volatile zpointer*)dst_obj->base(); - const int length = src_obj->length(); - - for (const volatile zpointer* const end = src + length; src < end; src++, dst++) { - zaddress elem = ZBarrier::load_barrier_on_oop_field(src); - // We avoid healing here because the store below colors the pointer store good, - // hence avoiding the cost of a CAS. - ZBarrier::store_barrier_on_heap_oop_field(dst, false /* heal */); - AtomicAccess::store(dst, ZAddress::store_good(elem)); - } -} - void ZBarrierSet::print_on(outputStream* st) const { st->print_cr("ZBarrierSet"); } diff --git a/src/hotspot/share/gc/z/zBarrierSet.hpp b/src/hotspot/share/gc/z/zBarrierSet.hpp index 51eb16319a0..502cc36d380 100644 --- a/src/hotspot/share/gc/z/zBarrierSet.hpp +++ b/src/hotspot/share/gc/z/zBarrierSet.hpp @@ -33,14 +33,29 @@ class ZBarrierSet : public BarrierSet { private: static zpointer store_good(oop obj); + static void load_barrier_all(oop src, size_t size); + static void color_store_good_all(oop dst, size_t size); + + static zaddress load_barrier_on_oop_field_preloaded(volatile zpointer* p, zpointer o); + static zaddress no_keep_alive_load_barrier_on_weak_oop_field_preloaded(volatile zpointer* p, zpointer o); + static zaddress no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(volatile zpointer* p, zpointer o); + static zaddress load_barrier_on_weak_oop_field_preloaded(volatile zpointer* p, zpointer o); + static zaddress load_barrier_on_phantom_oop_field_preloaded(volatile zpointer* p, zpointer o); + + static void store_barrier_on_heap_oop_field(volatile zpointer* p, bool heal); + static void no_keep_alive_store_barrier_on_heap_oop_field(volatile zpointer* p); + static void store_barrier_on_native_oop_field(volatile zpointer* p, bool heal); + + static zaddress load_barrier_on_oop_field(volatile zpointer* p); + + static void clone_obj_array(objArrayOop src, objArrayOop dst); + public: ZBarrierSet(); static ZBarrierSetAssembler* assembler(); static bool barrier_needed(DecoratorSet decorators, BasicType type); - static void clone_obj_array(objArrayOop src, objArrayOop dst); - virtual void on_thread_create(Thread* thread); virtual void on_thread_destroy(Thread* thread); virtual void on_thread_attach(Thread* thread); diff --git a/src/hotspot/share/gc/z/zBarrierSet.inline.hpp b/src/hotspot/share/gc/z/zBarrierSet.inline.hpp index 7d8f74ede57..9973b1d0131 100644 --- a/src/hotspot/share/gc/z/zBarrierSet.inline.hpp +++ b/src/hotspot/share/gc/z/zBarrierSet.inline.hpp @@ -28,10 +28,9 @@ #include "gc/shared/accessBarrierSupport.inline.hpp" #include "gc/z/zAddress.inline.hpp" -#include "gc/z/zBarrier.inline.hpp" -#include "gc/z/zIterator.inline.hpp" +#include "gc/z/zHeap.hpp" #include "gc/z/zNMethod.hpp" -#include "memory/iterator.inline.hpp" +#include "oops/objArrayOop.hpp" #include "utilities/debug.hpp" template @@ -68,21 +67,21 @@ inline zaddress ZBarrierSet::AccessBarrier::load_barrie if (HasDecorator::value) { if (HasDecorator::value) { // Load barriers on strong oop refs don't keep objects alive - return ZBarrier::load_barrier_on_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_oop_field_preloaded(p, o); } else if (HasDecorator::value) { - return ZBarrier::no_keep_alive_load_barrier_on_weak_oop_field_preloaded(p, o); + return ZBarrierSet::no_keep_alive_load_barrier_on_weak_oop_field_preloaded(p, o); } else { assert((HasDecorator::value), "Must be"); - return ZBarrier::no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(p, o); + return ZBarrierSet::no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(p, o); } } else { if (HasDecorator::value) { - return ZBarrier::load_barrier_on_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_oop_field_preloaded(p, o); } else if (HasDecorator::value) { - return ZBarrier::load_barrier_on_weak_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_weak_oop_field_preloaded(p, o); } else { assert((HasDecorator::value), "Must be"); - return ZBarrier::load_barrier_on_phantom_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_phantom_oop_field_preloaded(p, o); } } } @@ -97,21 +96,21 @@ inline zaddress ZBarrierSet::AccessBarrier::load_barrie if (HasDecorator::value) { if (decorators_known_strength & ON_STRONG_OOP_REF) { // Load barriers on strong oop refs don't keep objects alive - return ZBarrier::load_barrier_on_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_oop_field_preloaded(p, o); } else if (decorators_known_strength & ON_WEAK_OOP_REF) { - return ZBarrier::no_keep_alive_load_barrier_on_weak_oop_field_preloaded(p, o); + return ZBarrierSet::no_keep_alive_load_barrier_on_weak_oop_field_preloaded(p, o); } else { assert(decorators_known_strength & ON_PHANTOM_OOP_REF, "Must be"); - return ZBarrier::no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(p, o); + return ZBarrierSet::no_keep_alive_load_barrier_on_phantom_oop_field_preloaded(p, o); } } else { if (decorators_known_strength & ON_STRONG_OOP_REF) { - return ZBarrier::load_barrier_on_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_oop_field_preloaded(p, o); } else if (decorators_known_strength & ON_WEAK_OOP_REF) { - return ZBarrier::load_barrier_on_weak_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_weak_oop_field_preloaded(p, o); } else { assert(decorators_known_strength & ON_PHANTOM_OOP_REF, "Must be"); - return ZBarrier::load_barrier_on_phantom_oop_field_preloaded(p, o); + return ZBarrierSet::load_barrier_on_phantom_oop_field_preloaded(p, o); } } } @@ -126,7 +125,7 @@ inline zpointer ZBarrierSet::store_good(oop obj) { template inline void ZBarrierSet::AccessBarrier::store_barrier_heap_with_healing(zpointer* p) { if (!HasDecorator::value) { - ZBarrier::store_barrier_on_heap_oop_field(p, true /* heal */); + ZBarrierSet::store_barrier_on_heap_oop_field(p, true /* heal */); } else { assert(false, "Should not be used on uninitialized memory"); } @@ -135,21 +134,21 @@ inline void ZBarrierSet::AccessBarrier::store_barrier_h template inline void ZBarrierSet::AccessBarrier::store_barrier_heap_without_healing(zpointer* p) { if (!HasDecorator::value) { - ZBarrier::store_barrier_on_heap_oop_field(p, false /* heal */); + ZBarrierSet::store_barrier_on_heap_oop_field(p, false /* heal */); } } template inline void ZBarrierSet::AccessBarrier::no_keep_alive_store_barrier_heap(zpointer* p) { if (!HasDecorator::value) { - ZBarrier::no_keep_alive_store_barrier_on_heap_oop_field(p); + ZBarrierSet::no_keep_alive_store_barrier_on_heap_oop_field(p); } } template inline void ZBarrierSet::AccessBarrier::store_barrier_native_with_healing(zpointer* p) { if (!HasDecorator::value) { - ZBarrier::store_barrier_on_native_oop_field(p, true /* heal */); + ZBarrierSet::store_barrier_on_native_oop_field(p, true /* heal */); } else { assert(false, "Should not be used on uninitialized memory"); } @@ -158,7 +157,7 @@ inline void ZBarrierSet::AccessBarrier::store_barrier_n template inline void ZBarrierSet::AccessBarrier::store_barrier_native_without_healing(zpointer* p) { if (!HasDecorator::value) { - ZBarrier::store_barrier_on_native_oop_field(p, false /* heal */); + ZBarrierSet::store_barrier_on_native_oop_field(p, false /* heal */); } } @@ -325,7 +324,7 @@ template inline zaddress ZBarrierSet::AccessBarrier::oop_copy_one_barriers(zpointer* dst, zpointer* src) { store_barrier_heap_without_healing(dst); - return ZBarrier::load_barrier_on_oop_field(src); + return ZBarrierSet::load_barrier_on_oop_field(src); } template @@ -402,31 +401,6 @@ inline bool ZBarrierSet::AccessBarrier::oop_arraycopy_i return oop_arraycopy_in_heap_no_check_cast(dst, src, length); } -class ZColorStoreGoodOopClosure : public BasicOopIterateClosure { -public: - virtual void do_oop(oop* p_) { - volatile zpointer* const p = (volatile zpointer*)p_; - const zpointer ptr = ZBarrier::load_atomic(p); - const zaddress addr = ZPointer::uncolor(ptr); - AtomicAccess::store(p, ZAddress::store_good(addr)); - } - - virtual void do_oop(narrowOop* p) { - ShouldNotReachHere(); - } -}; - -class ZLoadBarrierOopClosure : public BasicOopIterateClosure { -public: - virtual void do_oop(oop* p) { - ZBarrier::load_barrier_on_oop_field((zpointer*)p); - } - - virtual void do_oop(narrowOop* p) { - ShouldNotReachHere(); - } -}; - template inline void ZBarrierSet::AccessBarrier::clone_in_heap(oop src, oop dst, size_t size) { check_is_valid_zaddress(src); @@ -443,17 +417,13 @@ inline void ZBarrierSet::AccessBarrier::clone_in_heap(o } // Fix the oops - ZLoadBarrierOopClosure cl; - ZIterator::oop_iterate(src, &cl); + ZBarrierSet::load_barrier_all(src, size); // Clone the object Raw::clone_in_heap(src, dst, size); - assert(dst->is_typeArray() || ZHeap::heap()->is_young(to_zaddress(dst)), "ZColorStoreGoodOopClosure is only valid for young objects"); - // Color store good before handing out - ZColorStoreGoodOopClosure cl_sg; - ZIterator::oop_iterate(dst, &cl_sg); + ZBarrierSet::color_store_good_all(dst, size); } // diff --git a/src/hotspot/share/gc/z/zObjArrayAllocator.cpp b/src/hotspot/share/gc/z/zObjArrayAllocator.cpp index ddb0ca49278..e3e364d5cd2 100644 --- a/src/hotspot/share/gc/z/zObjArrayAllocator.cpp +++ b/src/hotspot/share/gc/z/zObjArrayAllocator.cpp @@ -21,6 +21,7 @@ * questions. */ +#include "gc/z/zGeneration.inline.hpp" #include "gc/z/zObjArrayAllocator.hpp" #include "gc/z/zThreadLocalData.hpp" #include "gc/z/zUtils.inline.hpp" diff --git a/src/hotspot/share/gc/z/zPhysicalMemoryManager.cpp b/src/hotspot/share/gc/z/zPhysicalMemoryManager.cpp index 54477d19a27..1a38efb89fd 100644 --- a/src/hotspot/share/gc/z/zPhysicalMemoryManager.cpp +++ b/src/hotspot/share/gc/z/zPhysicalMemoryManager.cpp @@ -25,6 +25,7 @@ #include "gc/z/zAddress.inline.hpp" #include "gc/z/zArray.inline.hpp" #include "gc/z/zGlobals.hpp" +#include "gc/z/zGranuleMap.inline.hpp" #include "gc/z/zLargePages.inline.hpp" #include "gc/z/zList.inline.hpp" #include "gc/z/zNMT.hpp" diff --git a/src/hotspot/share/gc/z/zRangeRegistry.inline.hpp b/src/hotspot/share/gc/z/zRangeRegistry.inline.hpp index de34aca07c4..e466cc764b4 100644 --- a/src/hotspot/share/gc/z/zRangeRegistry.inline.hpp +++ b/src/hotspot/share/gc/z/zRangeRegistry.inline.hpp @@ -27,6 +27,7 @@ #include "gc/z/zRangeRegistry.hpp" #include "gc/z/zAddress.inline.hpp" +#include "gc/z/zArray.inline.hpp" #include "gc/z/zList.inline.hpp" #include "gc/z/zLock.inline.hpp" diff --git a/src/hotspot/share/precompiled/precompiled.hpp b/src/hotspot/share/precompiled/precompiled.hpp index 670bd55e423..1f503a96749 100644 --- a/src/hotspot/share/precompiled/precompiled.hpp +++ b/src/hotspot/share/precompiled/precompiled.hpp @@ -45,7 +45,6 @@ #include "gc/shenandoah/shenandoahHeap.inline.hpp" #endif #if INCLUDE_ZGC -#include "gc/z/zBarrier.inline.hpp" #include "gc/z/zGeneration.inline.hpp" #include "gc/z/zHeap.inline.hpp" #endif diff --git a/src/hotspot/share/prims/whitebox.cpp b/src/hotspot/share/prims/whitebox.cpp index e8e873fcb66..6d9ab57bb9a 100644 --- a/src/hotspot/share/prims/whitebox.cpp +++ b/src/hotspot/share/prims/whitebox.cpp @@ -120,6 +120,7 @@ #endif // INCLUDE_SERIALGC #if INCLUDE_ZGC #include "gc/z/zAddress.inline.hpp" +#include "gc/z/zHeap.inline.hpp" #endif // INCLUDE_ZGC #if INCLUDE_JVMCI #include "jvmci/jvmciEnv.hpp" diff --git a/src/hotspot/share/runtime/stackValue.cpp b/src/hotspot/share/runtime/stackValue.cpp index 5e01efc1622..52d2631f3c9 100644 --- a/src/hotspot/share/runtime/stackValue.cpp +++ b/src/hotspot/share/runtime/stackValue.cpp @@ -30,9 +30,6 @@ #include "runtime/globals.hpp" #include "runtime/handles.inline.hpp" #include "runtime/stackValue.hpp" -#if INCLUDE_ZGC -#include "gc/z/zBarrier.inline.hpp" -#endif #if INCLUDE_SHENANDOAHGC #include "gc/shenandoah/shenandoahBarrierSet.inline.hpp" #endif diff --git a/test/hotspot/gtest/runtime/test_os_windows.cpp b/test/hotspot/gtest/runtime/test_os_windows.cpp index 9fe0de55515..ad270848077 100644 --- a/test/hotspot/gtest/runtime/test_os_windows.cpp +++ b/test/hotspot/gtest/runtime/test_os_windows.cpp @@ -27,7 +27,7 @@ #include "logging/log.hpp" #include "runtime/flags/flagSetting.hpp" #include "runtime/globals_extension.hpp" -#include "runtime/os.hpp" +#include "runtime/os.inline.hpp" #include "concurrentTestRunner.inline.hpp" #include "unittest.hpp" From f993f90c86f89eb0c7f42ebecb45a68eae0bd9ea Mon Sep 17 00:00:00 2001 From: Joachim Kern Date: Wed, 24 Sep 2025 07:38:23 +0000 Subject: [PATCH 199/556] 8360401: [AIX] java/lang/ProcessBuilder/FDLeakTest/FDLeakTest.java fails since JDK-8210549 Reviewed-by: mdoerr, stuefe --- src/java.base/unix/native/libjava/childproc.c | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/java.base/unix/native/libjava/childproc.c b/src/java.base/unix/native/libjava/childproc.c index c4b5a2d7b29..9c6334e52d2 100644 --- a/src/java.base/unix/native/libjava/childproc.c +++ b/src/java.base/unix/native/libjava/childproc.c @@ -67,24 +67,39 @@ markCloseOnExec(int fd) return 0; } +#if !defined(_AIX) + /* The /proc file system on AIX does not contain open system files + * like /dev/random. Therefore we use a different approach and do + * not need isAsciiDigit() or FD_DIR */ static int isAsciiDigit(char c) { return c >= '0' && c <= '9'; } -#if defined(_AIX) - /* AIX does not understand '/proc/self' - it requires the real process ID */ - #define FD_DIR aix_fd_dir -#elif defined(_ALLBSD_SOURCE) - #define FD_DIR "/dev/fd" -#else - #define FD_DIR "/proc/self/fd" + #if defined(_ALLBSD_SOURCE) + #define FD_DIR "/dev/fd" + #else + #define FD_DIR "/proc/self/fd" + #endif #endif static int markDescriptorsCloseOnExec(void) { +#if defined(_AIX) + /* On AIX, we cannot rely on proc file system iteration to find all open files. Since + * iteration over all possible file descriptors, and subsequently closing them, can + * take a very long time, we use a bulk close via `ioctl` that is available on AIX. + * Since we hard-close, we need to make sure to keep the fail pipe file descriptor + * alive until the exec call. Therefore we mark the fail pipe fd with close on exec + * like the other OSes do, but then proceed to hard-close file descriptors beyond that. + */ + if (fcntl(FAIL_FILENO + 1, F_CLOSEM, 0) == -1 || + (markCloseOnExec(FAIL_FILENO) == -1 && errno != EBADF)) { + return -1; + } +#else DIR *dp; struct dirent *dirp; /* This function marks all file descriptors beyond stderr as CLOEXEC. @@ -93,12 +108,6 @@ markDescriptorsCloseOnExec(void) * execve. */ const int fd_from = STDERR_FILENO + 1; -#if defined(_AIX) - /* AIX does not understand '/proc/self' - it requires the real process ID */ - char aix_fd_dir[32]; /* the pid has at most 19 digits */ - snprintf(aix_fd_dir, 32, "/proc/%d/fd", getpid()); -#endif - if ((dp = opendir(FD_DIR)) == NULL) return -1; @@ -114,6 +123,7 @@ markDescriptorsCloseOnExec(void) } closedir(dp); +#endif return 0; } @@ -406,6 +416,10 @@ childProcess(void *arg) /* We moved the fail pipe fd */ fail_pipe_fd = FAIL_FILENO; + /* For AIX: The code in markDescriptorsCloseOnExec() relies on the current + * semantic of this function. When this point here is reached only the + * FDs 0,1,2 and 3 are further used until the exec() or the exit(-1). */ + /* close everything */ if (markDescriptorsCloseOnExec() == -1) { /* failed, close the old way */ int max_fd = (int)sysconf(_SC_OPEN_MAX); From 288822a5c2bbaba7b6b897faab1a9cc076c906cc Mon Sep 17 00:00:00 2001 From: Volkan Yazici Date: Wed, 24 Sep 2025 08:07:58 +0000 Subject: [PATCH 200/556] 8367068: Remove redundant HttpRequest.BodyPublisher tests Reviewed-by: dfuchs --- .../net/httpclient/FilePublisherTest.java | 22 ---- .../httpclient/FlowAdapterPublisherTest.java | 17 --- .../OfByteArraysTest.java | 2 +- .../HttpRequestBodyPublishers/OfFileTest.java | 2 +- .../net/httpclient/RelayingPublishers.java | 113 ------------------ ...ions.java => SubscriberAPIExceptions.java} | 51 +------- 6 files changed, 8 insertions(+), 199 deletions(-) delete mode 100644 test/jdk/java/net/httpclient/RelayingPublishers.java rename test/jdk/java/net/httpclient/{SubscriberPublisherAPIExceptions.java => SubscriberAPIExceptions.java} (75%) diff --git a/test/jdk/java/net/httpclient/FilePublisherTest.java b/test/jdk/java/net/httpclient/FilePublisherTest.java index 174afaf3f65..5ab9e6f8e7a 100644 --- a/test/jdk/java/net/httpclient/FilePublisherTest.java +++ b/test/jdk/java/net/httpclient/FilePublisherTest.java @@ -39,7 +39,6 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -60,7 +59,6 @@ import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.fail; public class FilePublisherTest implements HttpServerAdapters { SSLContext sslContext; @@ -156,26 +154,6 @@ public class FilePublisherTest implements HttpServerAdapters { send(uriString, path, expectedMsg, sameClient); } - @Test - public void testFileNotFound() throws Exception { - out.printf("\n\n--- testFileNotFound(): starting\n"); - try (FileSystem fs = newZipFs()) { - Path fileInZip = fs.getPath("non-existent.txt"); - BodyPublishers.ofFile(fileInZip); - fail(); - } catch (FileNotFoundException e) { - out.println("Caught expected: " + e); - } - var path = Path.of("fileNotFound.txt"); - try { - Files.deleteIfExists(path); - BodyPublishers.ofFile(path); - fail(); - } catch (FileNotFoundException e) { - out.println("Caught expected: " + e); - } - } - private static final int ITERATION_COUNT = 3; private void send(String uriString, diff --git a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java index ddefd2a9aa7..de511d6bd75 100644 --- a/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java +++ b/test/jdk/java/net/httpclient/FlowAdapterPublisherTest.java @@ -33,7 +33,6 @@ import java.nio.MappedByteBuffer; import java.util.Arrays; import java.util.Optional; import java.util.concurrent.Flow; -import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -55,8 +54,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.net.http.HttpRequest.BodyPublishers.fromPublisher; import static java.net.http.HttpResponse.BodyHandlers.ofString; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertThrows; -import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; /* @@ -123,20 +120,6 @@ public class FlowAdapterPublisherTest implements HttpServerAdapters { return builder; } - @Test - public void testAPIExceptions() { - assertThrows(NPE, () -> fromPublisher(null)); - assertThrows(NPE, () -> fromPublisher(null, 1)); - assertThrows(IAE, () -> fromPublisher(new BBPublisher(), 0)); - assertThrows(IAE, () -> fromPublisher(new BBPublisher(), -1)); - assertThrows(IAE, () -> fromPublisher(new BBPublisher(), Long.MIN_VALUE)); - - Publisher publisher = fromPublisher(new BBPublisher()); - assertThrows(NPE, () -> publisher.subscribe(null)); - } - - // Flow.Publisher - @Test(dataProvider = "uris") void testByteBufferPublisherUnknownLength(String uri) { String[] body = new String[] { "You know ", "it's summer ", "in Ireland ", diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java index 6b852407907..ab051d2020f 100644 --- a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfByteArraysTest.java @@ -44,7 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; /* * @test - * @bug 8364733 + * @bug 8226303 8364733 * @summary Verify all specified `HttpRequest.BodyPublishers::ofByteArrays` behavior * @build ByteBufferUtils * RecordingSubscriber diff --git a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java index 7705faef109..94b5a596fc6 100644 --- a/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java +++ b/test/jdk/java/net/httpclient/HttpRequestBodyPublishers/OfFileTest.java @@ -49,7 +49,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /* * @test - * @bug 8364733 + * @bug 8226303 8235459 8358688 8364733 * @summary Verify all specified `HttpRequest.BodyPublishers::ofFile` behavior * @build ByteBufferUtils * RecordingSubscriber diff --git a/test/jdk/java/net/httpclient/RelayingPublishers.java b/test/jdk/java/net/httpclient/RelayingPublishers.java deleted file mode 100644 index 83d74cd13ae..00000000000 --- a/test/jdk/java/net/httpclient/RelayingPublishers.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -import jdk.test.lib.util.FileUtils; -import org.testng.annotations.Test; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.http.HttpRequest.BodyPublisher; -import java.net.http.HttpRequest.BodyPublishers; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Flow; - -import static org.testng.Assert.assertEquals; - -/* - * @test - * @summary Verifies that some of the standard BodyPublishers relay exception - * rather than throw it - * @bug 8226303 8358688 - * @library /test/lib - * @run testng/othervm RelayingPublishers - */ -public class RelayingPublishers { - - @Test - public void ofFile0() throws IOException { - Path directory = Files.createDirectory(Path.of("d")); - // Even though the path exists, the publisher should not be able - // to read from it, as that path denotes a directory, not a file - BodyPublisher pub = BodyPublishers.ofFile(directory); - CompletableSubscriber s = new CompletableSubscriber<>(); - pub.subscribe(s); - s.future().join(); - // Interestingly enough, it's FileNotFoundException if a file - // is a directory - assertEquals(s.future().join().getClass(), FileNotFoundException.class); - } - - @Test - public void ofFile1() throws IOException { - Path file = Files.createFile(Path.of("f")); - BodyPublisher pub = BodyPublishers.ofFile(file); - FileUtils.deleteFileWithRetry(file); - CompletableSubscriber s = new CompletableSubscriber<>(); - pub.subscribe(s); - assertEquals(s.future().join().getClass(), FileNotFoundException.class); - } - - @Test - public void ofByteArrays() { - List bytes = new ArrayList<>(); - bytes.add(null); - BodyPublisher pub = BodyPublishers.ofByteArrays(bytes); - CompletableSubscriber s = new CompletableSubscriber<>(); - pub.subscribe(s); - assertEquals(s.future().join().getClass(), NullPointerException.class); - } - - static class CompletableSubscriber implements Flow.Subscriber { - - final CompletableFuture f = new CompletableFuture<>(); - - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscription.request(1); - } - - @Override - public void onNext(T item) { - f.completeExceptionally(new RuntimeException("Unexpected onNext")); - } - - @Override - public void onError(Throwable throwable) { - f.complete(throwable); - } - - @Override - public void onComplete() { - f.completeExceptionally(new RuntimeException("Unexpected onNext")); - } - - CompletableFuture future() { - return f.copy(); - } - } -} diff --git a/test/jdk/java/net/httpclient/SubscriberPublisherAPIExceptions.java b/test/jdk/java/net/httpclient/SubscriberAPIExceptions.java similarity index 75% rename from test/jdk/java/net/httpclient/SubscriberPublisherAPIExceptions.java rename to test/jdk/java/net/httpclient/SubscriberAPIExceptions.java index 62027095810..bf0f7c0bda7 100644 --- a/test/jdk/java/net/httpclient/SubscriberPublisherAPIExceptions.java +++ b/test/jdk/java/net/httpclient/SubscriberAPIExceptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,7 +21,6 @@ * questions. */ -import java.io.FileNotFoundException; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; @@ -30,17 +29,15 @@ import java.nio.file.OpenOption; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Flow; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.ResponseInfo; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; import java.util.function.Function; -import org.testng.annotations.DataProvider; + import org.testng.annotations.Test; -import static java.nio.charset.StandardCharsets.UTF_8; + import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.DELETE_ON_CLOSE; import static java.nio.file.StandardOpenOption.WRITE; @@ -49,53 +46,17 @@ import static org.testng.Assert.assertThrows; /* * @test - * @summary Basic tests for API specified exceptions from Publisher, Handler, + * @summary Basic tests for API specified exceptions from Handler, * and Subscriber convenience static factory methods. - * @run testng SubscriberPublisherAPIExceptions + * @run testng SubscriberAPIExceptions */ -public class SubscriberPublisherAPIExceptions { +public class SubscriberAPIExceptions { static final Class NPE = NullPointerException.class; static final Class IAE = IllegalArgumentException.class; static final Class IOB = IndexOutOfBoundsException.class; - @Test - public void publisherAPIExceptions() { - assertThrows(NPE, () -> BodyPublishers.ofByteArray(null)); - assertThrows(NPE, () -> BodyPublishers.ofByteArray(null, 0, 1)); - assertThrows(IOB, () -> BodyPublishers.ofByteArray(new byte[100], 0, 101)); - assertThrows(IOB, () -> BodyPublishers.ofByteArray(new byte[100], 1, 100)); - assertThrows(IOB, () -> BodyPublishers.ofByteArray(new byte[100], -1, 10)); - assertThrows(IOB, () -> BodyPublishers.ofByteArray(new byte[100], 99, 2)); - assertThrows(IOB, () -> BodyPublishers.ofByteArray(new byte[1], -100, 1)); - assertThrows(NPE, () -> BodyPublishers.ofByteArray(null)); - assertThrows(NPE, () -> BodyPublishers.ofFile(null)); - assertThrows(NPE, () -> BodyPublishers.ofInputStream(null)); - assertThrows(NPE, () -> BodyPublishers.ofString(null)); - assertThrows(NPE, () -> BodyPublishers.ofString("A", null)); - assertThrows(NPE, () -> BodyPublishers.ofString(null, UTF_8)); - assertThrows(NPE, () -> BodyPublishers.ofString(null, null)); - } - - @DataProvider(name = "nonExistentFiles") - public Object[][] nonExistentFiles() { - List paths = List.of(Paths.get("doesNotExist"), - Paths.get("tsixEtoNseod"), - Paths.get("doesNotExist2")); - paths.forEach(p -> { - if (Files.exists(p)) - throw new AssertionError("Unexpected " + p); - }); - - return paths.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); - } - - @Test(dataProvider = "nonExistentFiles", expectedExceptions = FileNotFoundException.class) - public void fromFileCheck(Path path) throws Exception { - BodyPublishers.ofFile(path); - } - @Test public void handlerAPIExceptions() throws Exception { Path path = Paths.get(".").resolve("tt"); From 2313f8e4ebe5b6d7542fa8a33fd08673cc0caf10 Mon Sep 17 00:00:00 2001 From: Fei Yang Date: Wed, 24 Sep 2025 11:31:09 +0000 Subject: [PATCH 201/556] 8368366: RISC-V: AlignVector is mistakenly set to AvoidUnalignedAccesses Reviewed-by: fjiang, rehn, mli --- src/hotspot/cpu/riscv/vm_version_riscv.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.cpp b/src/hotspot/cpu/riscv/vm_version_riscv.cpp index e0c3b303750..1bb9509cdde 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.cpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.cpp @@ -477,10 +477,6 @@ void VM_Version::c2_initialize() { warning("AES/CTR intrinsics are not available on this CPU"); FLAG_SET_DEFAULT(UseAESCTRIntrinsics, false); } - - if (FLAG_IS_DEFAULT(AlignVector)) { - FLAG_SET_DEFAULT(AlignVector, AvoidUnalignedAccesses); - } } #endif // COMPILER2 From e8adc1f81656126deae5bf7e0c912d5ad50dbbeb Mon Sep 17 00:00:00 2001 From: Coleen Phillimore Date: Wed, 24 Sep 2025 12:51:50 +0000 Subject: [PATCH 202/556] 8367989: Remove InstanceKlass::allocate_objArray and ArrayKlass::allocate_arrayArray Reviewed-by: stefank, fparain --- src/hotspot/share/memory/oopFactory.cpp | 8 ++------ src/hotspot/share/oops/arrayKlass.cpp | 10 ---------- src/hotspot/share/oops/arrayKlass.hpp | 1 - src/hotspot/share/oops/instanceKlass.cpp | 9 --------- src/hotspot/share/oops/instanceKlass.hpp | 1 - src/hotspot/share/oops/objArrayKlass.hpp | 7 +++---- 6 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/hotspot/share/memory/oopFactory.cpp b/src/hotspot/share/memory/oopFactory.cpp index e0b94a8caf8..d5e958ef29a 100644 --- a/src/hotspot/share/memory/oopFactory.cpp +++ b/src/hotspot/share/memory/oopFactory.cpp @@ -104,13 +104,9 @@ typeArrayOop oopFactory::new_typeArray_nozero(BasicType type, int length, TRAPS) return klass->allocate_common(length, false, THREAD); } - objArrayOop oopFactory::new_objArray(Klass* klass, int length, TRAPS) { - if (klass->is_array_klass()) { - return ArrayKlass::cast(klass)->allocate_arrayArray(1, length, THREAD); - } else { - return InstanceKlass::cast(klass)->allocate_objArray(1, length, THREAD); - } + ArrayKlass* ak = klass->array_klass(CHECK_NULL); + return ObjArrayKlass::cast(ak)->allocate_instance(length, THREAD); } objArrayHandle oopFactory::new_objArray_handle(Klass* klass, int length, TRAPS) { diff --git a/src/hotspot/share/oops/arrayKlass.cpp b/src/hotspot/share/oops/arrayKlass.cpp index dc64abd6cd7..cd929a3bfe1 100644 --- a/src/hotspot/share/oops/arrayKlass.cpp +++ b/src/hotspot/share/oops/arrayKlass.cpp @@ -183,16 +183,6 @@ GrowableArray* ArrayKlass::compute_secondary_supers(int num_extra_slots, return nullptr; } -objArrayOop ArrayKlass::allocate_arrayArray(int n, int length, TRAPS) { - check_array_allocation_length(length, arrayOopDesc::max_array_length(T_ARRAY), CHECK_NULL); - size_t size = objArrayOopDesc::object_size(length); - ArrayKlass* ak = array_klass(n + dimension(), CHECK_NULL); - objArrayOop o = (objArrayOop)Universe::heap()->array_allocate(ak, size, length, - /* do_zero */ true, CHECK_NULL); - // initialization to null not necessary, area already cleared - return o; -} - // JVMTI support jint ArrayKlass::jvmti_class_status() const { diff --git a/src/hotspot/share/oops/arrayKlass.hpp b/src/hotspot/share/oops/arrayKlass.hpp index 02d72c3cde8..6ee783fb510 100644 --- a/src/hotspot/share/oops/arrayKlass.hpp +++ b/src/hotspot/share/oops/arrayKlass.hpp @@ -85,7 +85,6 @@ class ArrayKlass: public Klass { // Sizes points to the first dimension of the array, subsequent dimensions // are always in higher memory. The callers of these set that up. virtual oop multi_allocate(int rank, jint* sizes, TRAPS); - objArrayOop allocate_arrayArray(int n, int length, TRAPS); // find field according to JVM spec 5.4.3.2, returns the klass in which the field is defined Klass* find_field(Symbol* name, Symbol* sig, fieldDescriptor* fd) const; diff --git a/src/hotspot/share/oops/instanceKlass.cpp b/src/hotspot/share/oops/instanceKlass.cpp index 932d1f246ad..9d6cb951aeb 100644 --- a/src/hotspot/share/oops/instanceKlass.cpp +++ b/src/hotspot/share/oops/instanceKlass.cpp @@ -1555,15 +1555,6 @@ bool InstanceKlass::is_same_or_direct_interface(Klass *k) const { return false; } -objArrayOop InstanceKlass::allocate_objArray(int n, int length, TRAPS) { - check_array_allocation_length(length, arrayOopDesc::max_array_length(T_OBJECT), CHECK_NULL); - size_t size = objArrayOopDesc::object_size(length); - ArrayKlass* ak = array_klass(n, CHECK_NULL); - objArrayOop o = (objArrayOop)Universe::heap()->array_allocate(ak, size, length, - /* do_zero */ true, CHECK_NULL); - return o; -} - instanceOop InstanceKlass::register_finalizer(instanceOop i, TRAPS) { if (TraceFinalizerRegistration) { tty->print("Registered "); diff --git a/src/hotspot/share/oops/instanceKlass.hpp b/src/hotspot/share/oops/instanceKlass.hpp index a24c38cf259..82c82714f9b 100644 --- a/src/hotspot/share/oops/instanceKlass.hpp +++ b/src/hotspot/share/oops/instanceKlass.hpp @@ -810,7 +810,6 @@ public: // additional member function to return a handle instanceHandle allocate_instance_handle(TRAPS); - objArrayOop allocate_objArray(int n, int length, TRAPS); // Helper function static instanceOop register_finalizer(instanceOop i, TRAPS); diff --git a/src/hotspot/share/oops/objArrayKlass.hpp b/src/hotspot/share/oops/objArrayKlass.hpp index 6db6630cff4..1a4f7f657d2 100644 --- a/src/hotspot/share/oops/objArrayKlass.hpp +++ b/src/hotspot/share/oops/objArrayKlass.hpp @@ -64,7 +64,9 @@ class ObjArrayKlass : public ArrayKlass { // Instance variables Klass* element_klass() const { return _element_klass; } void set_element_klass(Klass* k) { _element_klass = k; } - Klass** element_klass_addr() { return &_element_klass; } + + // Compiler/Interpreter offset + static ByteSize element_klass_offset() { return byte_offset_of(ObjArrayKlass, _element_klass); } Klass* bottom_klass() const { return _bottom_klass; } void set_bottom_klass(Klass* k) { _bottom_klass = k; } @@ -73,9 +75,6 @@ class ObjArrayKlass : public ArrayKlass { ModuleEntry* module() const; PackageEntry* package() const; - // Compiler/Interpreter offset - static ByteSize element_klass_offset() { return byte_offset_of(ObjArrayKlass, _element_klass); } - // Dispatched operation bool can_be_primary_super_slow() const; GrowableArray* compute_secondary_supers(int num_extra_slots, From 727d41d2882e972e19e6dd431a9080a2f9ad4a22 Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Wed, 24 Sep 2025 13:03:34 +0000 Subject: [PATCH 203/556] 8368226: Remove Thread.stop Reviewed-by: vklang, jpai, lancea, serb --- .../share/classes/java/lang/Thread.java | 30 ---- .../share/classes/java/lang/ThreadDeath.java | 14 +- .../doc-files/threadPrimitiveDeprecation.html | 161 ------------------ test/jdk/java/lang/Thread/ThreadStopTest.java | 112 ------------ .../java/lang/Thread/virtual/ThreadAPI.java | 29 ---- test/jdk/sun/security/krb5/auto/KDC.java | 6 +- 6 files changed, 9 insertions(+), 343 deletions(-) delete mode 100644 src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html delete mode 100644 test/jdk/java/lang/Thread/ThreadStopTest.java diff --git a/src/java.base/share/classes/java/lang/Thread.java b/src/java.base/share/classes/java/lang/Thread.java index 2fc20a94f1d..40f2dacadd2 100644 --- a/src/java.base/share/classes/java/lang/Thread.java +++ b/src/java.base/share/classes/java/lang/Thread.java @@ -1527,36 +1527,6 @@ public class Thread implements Runnable { } } - /** - * Throws {@code UnsupportedOperationException}. - * - * @throws UnsupportedOperationException always - * - * @deprecated This method was originally specified to "stop" a victim - * thread by causing the victim thread to throw a {@link ThreadDeath}. - * It was inherently unsafe. Stopping a thread caused it to unlock - * all of the monitors that it had locked (as a natural consequence - * of the {@code ThreadDeath} exception propagating up the stack). If - * any of the objects previously protected by these monitors were in - * an inconsistent state, the damaged objects became visible to - * other threads, potentially resulting in arbitrary behavior. - * Usages of {@code stop} should be replaced by code that simply - * modifies some variable to indicate that the target thread should - * stop running. The target thread should check this variable - * regularly, and return from its run method in an orderly fashion - * if the variable indicates that it is to stop running. If the - * target thread waits for long periods (on a condition variable, - * for example), the {@code interrupt} method should be used to - * interrupt the wait. - * For more information, see - * Why - * is Thread.stop deprecated and the ability to stop a thread removed?. - */ - @Deprecated(since="1.2", forRemoval=true) - public final void stop() { - throw new UnsupportedOperationException(); - } - /** * Interrupts this thread. * diff --git a/src/java.base/share/classes/java/lang/ThreadDeath.java b/src/java.base/share/classes/java/lang/ThreadDeath.java index 743f1178abe..e77029a28fc 100644 --- a/src/java.base/share/classes/java/lang/ThreadDeath.java +++ b/src/java.base/share/classes/java/lang/ThreadDeath.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,14 +27,12 @@ package java.lang; /** * An instance of {@code ThreadDeath} was originally specified to be thrown - * by a victim thread when "stopped" with {@link Thread#stop()}. + * by a victim thread when "stopped" with the {@link Thread} API. * - * @deprecated {@link Thread#stop()} was originally specified to "stop" a victim - * thread by causing the victim thread to throw a {@code ThreadDeath}. It - * was inherently unsafe and deprecated in an early JDK release. The ability - * to "stop" a thread with {@code Thread.stop} has been removed and the - * {@code Thread.stop} method changed to throw an exception. Consequently, - * {@code ThreadDeath} is also deprecated, for removal. + * @deprecated {@code Thread} originally specified a "{@code stop}" method to stop a + * victim thread by causing the victim thread to throw a {@code ThreadDeath}. It + * was inherently unsafe and deprecated in an early JDK release. The {@code stop} + * method has since been removed and {@code ThreadDeath} is deprecated, for removal. * * @since 1.0 */ diff --git a/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html b/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html deleted file mode 100644 index 9f3ad156727..00000000000 --- a/src/java.base/share/classes/java/lang/doc-files/threadPrimitiveDeprecation.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - Java Thread Primitive Deprecation - - -

        Java Thread Primitive Deprecation

        -
        -

        Why is Thread.stop deprecated and the ability to -stop a thread removed?

        -

        Because it was inherently unsafe. Stopping a thread caused it to -unlock all the monitors that it had locked. (The monitors were -unlocked as the ThreadDeath exception propagated up -the stack.) If any of the objects previously protected by these -monitors were in an inconsistent state, other threads may have viewed -these objects in an inconsistent state. Such objects are said to be -damaged. When threads operate on damaged objects, arbitrary -behavior can result. This behavior may be subtle and difficult to -detect, or it may be pronounced. Unlike other unchecked exceptions, -ThreadDeath killed threads silently; thus, the user had -no warning that their program may be corrupted. The corruption could -manifest itself at any time after the actual damage occurs, even -hours or days in the future.

        -
        -

        Couldn't I have just caught ThreadDeath and fixed -the damaged object?

        -

        In theory, perhaps, but it would vastly complicate the -task of writing correct multithreaded code. The task would be -nearly insurmountable for two reasons:

        -
          -
        1. A thread could throw a ThreadDeath exception -almost anywhere. All synchronized methods and blocks would -have to be studied in great detail, with this in mind.
        2. -
        3. A thread could throw a second ThreadDeath exception -while cleaning up from the first (in the catch or -finally clause). Cleanup would have to be repeated till -it succeeded. The code to ensure this would be quite complex.
        4. -
        -In sum, it just isn't practical. -
        -

        What should I use instead of Thread.stop?

        -

        Most uses of stop should be replaced by code that -simply modifies some variable to indicate that the target thread -should stop running. The target thread should check this variable -regularly, and return from its run method in an orderly fashion if -the variable indicates that it is to stop running. To ensure prompt -communication of the stop-request, the variable must be -volatile (or access to the variable must be -synchronized).

        -

        For example, suppose your application contains the following -start, stop and run -methods:

        -
        -    private Thread blinker;
        -
        -    public void start() {
        -        blinker = new Thread(this);
        -        blinker.start();
        -    }
        -
        -    public void stop() {
        -        blinker.stop();  // UNSAFE!
        -    }
        -
        -    public void run() {
        -        while (true) {
        -            try {
        -                Thread.sleep(interval);
        -            } catch (InterruptedException e){
        -            }
        -            blink();
        -        }
        -    }
        -
        -You can avoid the use of Thread.stop by replacing the -application's stop and run methods with: -
        -    private volatile Thread blinker;
        -
        -    public void stop() {
        -        blinker = null;
        -    }
        -
        -    public void run() {
        -        Thread thisThread = Thread.currentThread();
        -        while (blinker == thisThread) {
        -            try {
        -                Thread.sleep(interval);
        -            } catch (InterruptedException e){
        -            }
        -            blink();
        -        }
        -    }
        -
        -
        -

        How do I stop a thread that waits for long periods (e.g., for -input)?

        -

        That's what the Thread.interrupt method is for. The -same "state based" signaling mechanism shown above can be used, but -the state change (blinker = null, in the previous -example) can be followed by a call to -Thread.interrupt, to interrupt the wait:

        -
        -    public void stop() {
        -        Thread moribund = waiter;
        -        waiter = null;
        -        moribund.interrupt();
        -    }
        -
        -For this technique to work, it's critical that any method that -catches an interrupt exception and is not prepared to deal with it -immediately reasserts the exception. We say reasserts -rather than rethrows, because it is not always possible to -rethrow the exception. If the method that catches the -InterruptedException is not declared to throw this -(checked) exception, then it should "reinterrupt itself" with the -following incantation: -
        -    Thread.currentThread().interrupt();
        -
        -This ensures that the Thread will reraise the -InterruptedException as soon as it is able. -
        -

        What if a thread doesn't respond to -Thread.interrupt?

        -

        In some cases, you can use application specific tricks. For -example, if a thread is waiting on a known socket, you can close -the socket to cause the thread to return immediately. -Unfortunately, there really isn't any technique that works in -general. It should be noted that in all situations where a -waiting thread doesn't respond to Thread.interrupt, it -wouldn't respond to Thread.stop either. Such -cases include deliberate denial-of-service attacks, and I/O -operations for which thread.stop and thread.interrupt do not work -properly.

        - - diff --git a/test/jdk/java/lang/Thread/ThreadStopTest.java b/test/jdk/java/lang/Thread/ThreadStopTest.java deleted file mode 100644 index f3886a3312c..00000000000 --- a/test/jdk/java/lang/Thread/ThreadStopTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* @test - * @bug 8289610 8249627 8205132 8320532 - * @summary Test that Thread stops throws UOE - * @run junit ThreadStopTest - */ - -import java.time.Duration; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.LockSupport; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -class ThreadStopTest { - - /** - * Test current thread calling Thread.stop on itself. - */ - @Test - void testCurrentThread() { - var thread = Thread.currentThread(); - assertThrows(UnsupportedOperationException.class, thread::stop); - } - - /** - * Test Thread.stop on an unstarted thread. - */ - @Test - void testUnstartedThread() { - Thread thread = new Thread(() -> { }); - assertThrows(UnsupportedOperationException.class, thread::stop); - assertTrue(thread.getState() == Thread.State.NEW); - } - - /** - * Test Thread.stop on a thread spinning in a loop. - */ - @Test - void testRunnableThread() throws Exception { - AtomicBoolean done = new AtomicBoolean(); - Thread thread = new Thread(() -> { - while (!done.get()) { - Thread.onSpinWait(); - } - }); - thread.start(); - try { - assertThrows(UnsupportedOperationException.class, thread::stop); - - // thread should not terminate - boolean terminated = thread.join(Duration.ofMillis(500)); - assertFalse(terminated); - } finally { - done.set(true); - thread.join(); - } - } - - /** - * Test Thread.stop on a thread that is parked. - */ - @Test - void testWaitingThread() throws Exception { - Thread thread = new Thread(LockSupport::park); - thread.start(); - try { - // wait for thread to park - while ((thread.getState() != Thread.State.WAITING)) { - Thread.sleep(10); - } - assertThrows(UnsupportedOperationException.class, thread::stop); - assertTrue(thread.getState() == Thread.State.WAITING); - } finally { - LockSupport.unpark(thread); - thread.join(); - } - } - - /** - * Test Thread.stop on a terminated thread. - */ - @Test - void testTerminatedThread() throws Exception { - Thread thread = new Thread(() -> { }); - thread.start(); - thread.join(); - assertThrows(UnsupportedOperationException.class, thread::stop); - assertTrue(thread.getState() == Thread.State.TERMINATED); - } -} diff --git a/test/jdk/java/lang/Thread/virtual/ThreadAPI.java b/test/jdk/java/lang/Thread/virtual/ThreadAPI.java index b7fdeb8ec84..397514c3852 100644 --- a/test/jdk/java/lang/Thread/virtual/ThreadAPI.java +++ b/test/jdk/java/lang/Thread/virtual/ThreadAPI.java @@ -252,35 +252,6 @@ class ThreadAPI { assertThrows(NullPointerException.class, () -> Thread.startVirtualThread(null)); } - /** - * Test Thread::stop from current thread. - */ - @Test - void testStop1() throws Exception { - VThreadRunner.run(() -> { - Thread t = Thread.currentThread(); - assertThrows(UnsupportedOperationException.class, t::stop); - }); - } - - /** - * Test Thread::stop from another thread. - */ - @Test - void testStop2() throws Exception { - var thread = Thread.ofVirtual().start(() -> { - try { - Thread.sleep(20*1000); - } catch (InterruptedException e) { } - }); - try { - assertThrows(UnsupportedOperationException.class, thread::stop); - } finally { - thread.interrupt(); - thread.join(); - } - } - /** * Test Thread.join before thread starts, platform thread invokes join. */ diff --git a/test/jdk/sun/security/krb5/auto/KDC.java b/test/jdk/sun/security/krb5/auto/KDC.java index bb744281eb9..42c7299b443 100644 --- a/test/jdk/sun/security/krb5/auto/KDC.java +++ b/test/jdk/sun/security/krb5/auto/KDC.java @@ -1646,9 +1646,9 @@ public class KDC { System.out.println("Done"); } else { try { - thread1.stop(); - thread2.stop(); - thread3.stop(); + thread1.interrupt(); + thread2.interrupt(); + thread3.interrupt(); u1.close(); t1.close(); } catch (Exception e) { From 0a64358aa82930e2bd323fcec2c1cb269ee200e0 Mon Sep 17 00:00:00 2001 From: Matthias Baesken Date: Wed, 24 Sep 2025 13:07:45 +0000 Subject: [PATCH 204/556] 8368273: LIBPTHREAD dependency is not needed for some jdk libs Reviewed-by: ihse --- make/modules/jdk.sctp/Lib.gmk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make/modules/jdk.sctp/Lib.gmk b/make/modules/jdk.sctp/Lib.gmk index 327f4fa6104..9a70996d9cb 100644 --- a/make/modules/jdk.sctp/Lib.gmk +++ b/make/modules/jdk.sctp/Lib.gmk @@ -41,7 +41,7 @@ ifeq ($(call isTargetOs, linux), true) java.base:libnio \ java.base:libnio/ch, \ JDK_LIBS := java.base:libjava java.base:libnet, \ - LIBS_linux := $(LIBDL) $(LIBPTHREAD), \ + LIBS_linux := $(LIBDL), \ )) TARGETS += $(BUILD_LIBSCTP) From 1cd186c7f7ef572b599228acc3c87281b0c3bdf4 Mon Sep 17 00:00:00 2001 From: Chen Liang Date: Wed, 24 Sep 2025 13:14:32 +0000 Subject: [PATCH 205/556] 8368331: ClassFile Signature parsing fails for type parameter with no supertype Reviewed-by: asotona --- .../classfile/impl/SignaturesImpl.java | 18 ++++++++++++++++-- test/jdk/jdk/classfile/SignaturesTest.java | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java index d0dbf89551f..06c5abb95a8 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/SignaturesImpl.java @@ -113,8 +113,22 @@ public final class SignaturesImpl { RefTypeSig classBound = null; ArrayList interfaceBounds = null; require(':'); - if (sig.charAt(sigp) != ':') - classBound = referenceTypeSig(); + if (sig.charAt(sigp) != ':') { + int p = nextIdentifierEnd(sig, sigp); + // For non-identifier chars: + // . / < indicates class type (inner, package, type arg) + // [ indicates array type + // ; indicates class/type var type + // > and : are illegal, such as in + if (p < sig.length()) { + char limit = sig.charAt(p); + if (limit != '>' && limit != ':') { + classBound = referenceTypeSig(); + } + } + // If classBound is absent here, we start tokenizing + // next type parameter, which can trigger failures + } while (match(':')) { if (interfaceBounds == null) interfaceBounds = new ArrayList<>(); diff --git a/test/jdk/jdk/classfile/SignaturesTest.java b/test/jdk/jdk/classfile/SignaturesTest.java index 333c6dd2f29..95fa8be4602 100644 --- a/test/jdk/jdk/classfile/SignaturesTest.java +++ b/test/jdk/jdk/classfile/SignaturesTest.java @@ -24,7 +24,7 @@ /* * @test * @summary Testing Signatures. - * @bug 8321540 8319463 8357955 + * @bug 8321540 8319463 8357955 8368050 8368331 * @run junit SignaturesTest */ import java.io.IOException; @@ -120,6 +120,20 @@ class SignaturesTest { assertEqualsDeep( ArrayTypeSig.of(2, TypeVarSig.of("E")), Signature.parseFrom("[[TE;")); + + assertEqualsDeep( + MethodSignature.of( + List.of(TypeParam.of("A", ClassTypeSig.of("one/Two")), // / + TypeParam.of("B", ClassTypeSig.of(ClassTypeSig.of("Outer"), "Inner")), // . + TypeParam.of("C", ArrayTypeSig.of(BaseTypeSig.of('I'))), // [ + TypeParam.of("D", ClassTypeSig.of("Generic", TypeArg.unbounded())), // < + TypeParam.of("E", TypeVarSig.of("A")), // ; + TypeParam.of("F", (ClassTypeSig) null), // : + TypeParam.of("G", (ClassTypeSig) null)), // > + List.of(), + BaseTypeSig.of('V')), + MethodSignature.parseFrom(";E:TA;F:G:>()V") + ); } @Test From 2a232d0210015606da7207edab793760fdb61b57 Mon Sep 17 00:00:00 2001 From: Mikhail Yankelevich Date: Wed, 24 Sep 2025 13:21:31 +0000 Subject: [PATCH 206/556] 8360979: Remove use of Thread.stop in krb5/auto/KDC.java Reviewed-by: weijun --- test/jdk/sun/security/krb5/auto/KDC.java | 90 ++++++++++++------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/test/jdk/sun/security/krb5/auto/KDC.java b/test/jdk/sun/security/krb5/auto/KDC.java index 42c7299b443..37a4e828469 100644 --- a/test/jdk/sun/security/krb5/auto/KDC.java +++ b/test/jdk/sun/security/krb5/auto/KDC.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1547,65 +1547,65 @@ public class KDC { this.port = port; // The UDP consumer - thread1 = new Thread() { - public void run() { - udpConsumerReady = true; - while (true) { - try { - byte[] inbuf = new byte[8192]; - DatagramPacket p = new DatagramPacket(inbuf, inbuf.length); - udp.receive(p); - System.out.println("-----------------------------------------------"); - System.out.println(">>>>> UDP packet received"); - q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p)); - } catch (Exception e) { - e.printStackTrace(); - } + thread1 = new Thread(() -> { + udpConsumerReady = true; + while (true) { + try { + byte[] inbuf = new byte[8192]; + DatagramPacket p = new DatagramPacket(inbuf, inbuf.length); + udp.receive(p); + System.out.println("-----------------------------------------------"); + System.out.println(">>>>> UDP packet received"); + q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p)); + } catch (InterruptedException e){ + break; // Thread was stopped, so stopping the loop + } catch (Exception e) { + e.printStackTrace(); } } - }; + }); thread1.setDaemon(asDaemon); thread1.start(); // The TCP consumer - thread2 = new Thread() { - public void run() { - tcpConsumerReady = true; - while (true) { - try { - Socket socket = tcp.accept(); - System.out.println("-----------------------------------------------"); - System.out.println(">>>>> TCP connection established"); - DataInputStream in = new DataInputStream(socket.getInputStream()); - DataOutputStream out = new DataOutputStream(socket.getOutputStream()); - int len = in.readInt(); - if (len > 65535) { - throw new Exception("Huge request not supported"); - } - byte[] token = new byte[len]; - in.readFully(token); - q.put(new Job(processMessage(token), socket, out)); - } catch (Exception e) { - e.printStackTrace(); + thread2 = new Thread(() -> { + tcpConsumerReady = true; + while (true) { + try { + Socket socket = tcp.accept(); + System.out.println("-----------------------------------------------"); + System.out.println(">>>>> TCP connection established"); + DataInputStream in = new DataInputStream(socket.getInputStream()); + DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + int len = in.readInt(); + if (len > 65535) { + throw new Exception("Huge request not supported"); } + byte[] token = new byte[len]; + in.readFully(token); + q.put(new Job(processMessage(token), socket, out)); + } catch (InterruptedException e){ + break; // Thread was stopped, so stopping the loop + } catch (Exception e) { + e.printStackTrace(); } } - }; + }); thread2.setDaemon(asDaemon); thread2.start(); // The dispatcher - thread3 = new Thread() { - public void run() { - dispatcherReady = true; - while (true) { - try { - q.take().send(); - } catch (Exception e) { - } + thread3 = new Thread(() -> { + dispatcherReady = true; + while (true) { + try { + q.take().send(); + } catch (InterruptedException e){ + break; // Thread was stopped, so stopping the loop + } catch (Exception e) { } } - }; + }); thread3.setDaemon(true); thread3.start(); From f7c9fef9147ee3c6168469ec04d2946a34505d63 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Wed, 24 Sep 2025 13:37:16 +0000 Subject: [PATCH 207/556] 8352069: Renamings after JEP 522: G1 GC: Improve Throughput by Reducing Synchronization Reviewed-by: iwalulya, ayang --- .../gc/g1/g1BarrierSetAssembler_aarch64.cpp | 22 ++++++++-------- .../arm/gc/g1/g1BarrierSetAssembler_arm.cpp | 22 ++++++++-------- .../ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp | 22 ++++++++-------- .../gc/g1/g1BarrierSetAssembler_riscv.cpp | 22 ++++++++-------- .../s390/gc/g1/g1BarrierSetAssembler_s390.cpp | 26 +++++++++---------- .../x86/gc/g1/g1BarrierSetAssembler_x86.cpp | 18 ++++++------- 6 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp index 9950feb7470..974527f30e9 100644 --- a/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/gc/g1/g1BarrierSetAssembler_aarch64.cpp @@ -232,14 +232,14 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, } -static void generate_post_barrier_fast_path(MacroAssembler* masm, - const Register store_addr, - const Register new_val, - const Register thread, - const Register tmp1, - const Register tmp2, - Label& done, - bool new_val_may_be_null) { +static void generate_post_barrier(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register thread, + const Register tmp1, + const Register tmp2, + Label& done, + bool new_val_may_be_null) { assert(thread == rthread, "must be"); assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg, rscratch1); @@ -273,7 +273,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Register tmp1, Register tmp2) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, false /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, false /* new_val_may_be_null */); __ bind(done); } @@ -335,7 +335,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register tmp2, bool new_val_may_be_null) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); __ bind(done); } @@ -441,7 +441,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, Register tmp1, Register tmp2) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); masm->bind(done); } diff --git a/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp b/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp index 71f8931eb5f..74a9f273a43 100644 --- a/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp +++ b/src/hotspot/cpu/arm/gc/g1/g1BarrierSetAssembler_arm.cpp @@ -198,14 +198,14 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, __ bind(done); } -static void generate_post_barrier_fast_path(MacroAssembler* masm, - const Register store_addr, - const Register new_val, - const Register thread, - const Register tmp1, - const Register tmp2, - Label& done, - bool new_val_may_be_null) { +static void generate_post_barrier(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register thread, + const Register tmp1, + const Register tmp2, + Label& done, + bool new_val_may_be_null) { assert(thread == Rthread, "must be"); assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg); @@ -245,7 +245,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Register tmp2, Register tmp3) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, Rthread, tmp1, tmp2, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, Rthread, tmp1, tmp2, done, true /* new_val_may_be_null */); __ bind(done); } @@ -307,7 +307,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register tmp3, bool new_val_may_be_null) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); __ bind(done); } @@ -408,7 +408,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, Register tmp1, Register tmp2) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); masm->bind(done); } diff --git a/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp b/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp index 262bb1eae89..65bfd683abd 100644 --- a/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp +++ b/src/hotspot/cpu/ppc/gc/g1/g1BarrierSetAssembler_ppc.cpp @@ -229,14 +229,14 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, Decorator __ bind(filtered); } -static void generate_post_barrier_fast_path(MacroAssembler* masm, - const Register store_addr, - const Register new_val, - const Register thread, - const Register tmp1, - const Register tmp2, - Label& done, - bool new_val_may_be_null) { +static void generate_post_barrier(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register thread, + const Register tmp1, + const Register tmp2, + Label& done, + bool new_val_may_be_null) { assert_different_registers(store_addr, new_val, tmp1, R0); assert_different_registers(store_addr, tmp1, tmp2, R0); @@ -273,7 +273,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Decorato bool not_null = (decorators & IS_NOT_NULL) != 0; Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, R16_thread, tmp1, tmp2, done, !not_null); + generate_post_barrier(masm, store_addr, new_val, R16_thread, tmp1, tmp2, done, !not_null); __ bind(done); } @@ -450,7 +450,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, new_val_decoded = __ decode_heap_oop_not_null(tmp2, new_val); } - generate_post_barrier_fast_path(masm, store_addr, new_val_decoded, R16_thread, tmp1, tmp2, done, new_val_may_be_null); + generate_post_barrier(masm, store_addr, new_val_decoded, R16_thread, tmp1, tmp2, done, new_val_may_be_null); __ bind(done); } @@ -498,7 +498,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, Register tmp1, Register tmp2) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); masm->bind(done); } diff --git a/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp b/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp index 9c3bd93f8a6..64e9cbe1f47 100644 --- a/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp +++ b/src/hotspot/cpu/riscv/gc/g1/g1BarrierSetAssembler_riscv.cpp @@ -228,14 +228,14 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, } -static void generate_post_barrier_fast_path(MacroAssembler* masm, - const Register store_addr, - const Register new_val, - const Register thread, - const Register tmp1, - const Register tmp2, - Label& done, - bool new_val_may_be_null) { +static void generate_post_barrier(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register thread, + const Register tmp1, + const Register tmp2, + Label& done, + bool new_val_may_be_null) { assert(thread == xthread, "must be"); assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg); // Does store cross heap regions? @@ -271,7 +271,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Register tmp1, Register tmp2) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); __ bind(done); } @@ -333,7 +333,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register tmp2, bool new_val_may_be_null) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); __ bind(done); } @@ -438,7 +438,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, Register tmp1, Register tmp2) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); masm->bind(done); } diff --git a/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp b/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp index 3e176309c27..e4fe690663a 100644 --- a/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp +++ b/src/hotspot/cpu/s390/gc/g1/g1BarrierSetAssembler_s390.cpp @@ -204,16 +204,16 @@ void G1BarrierSetAssembler::generate_c2_pre_barrier_stub(MacroAssembler* masm, BLOCK_COMMENT("} generate_c2_pre_barrier_stub"); } -static void generate_post_barrier_fast_path(MacroAssembler* masm, - const Register store_addr, - const Register new_val, - const Register thread, - const Register tmp1, - const Register tmp2, - Label& done, - bool new_val_may_be_null) { +static void generate_post_barrier(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register thread, + const Register tmp1, + const Register tmp2, + Label& done, + bool new_val_may_be_null) { - __ block_comment("generate_post_barrier_fast_path {"); + __ block_comment("generate_post_barrier {"); assert(thread == Z_thread, "must be"); assert_different_registers(store_addr, new_val, thread, tmp1, tmp2, noreg); @@ -252,7 +252,7 @@ static void generate_post_barrier_fast_path(MacroAssembler* masm, static_assert(G1CardTable::dirty_card_val() == 0, "must be to use z_mvi"); __ z_mvi(0, tmp1, G1CardTable::dirty_card_val()); // *(card address) := dirty_card_val - __ block_comment("} generate_post_barrier_fast_path"); + __ block_comment("} generate_post_barrier"); } void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, @@ -264,7 +264,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, bool new_val_may_be_null) { BLOCK_COMMENT("g1_write_barrier_post_c2 {"); Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, new_val_may_be_null); __ bind(done); BLOCK_COMMENT("} g1_write_barrier_post_c2"); } @@ -418,7 +418,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Decorato bool not_null = (decorators & IS_NOT_NULL) != 0; Label done; - generate_post_barrier_fast_path(masm, Rstore_addr, Rnew_val, Z_thread, Rtmp1, Rtmp2, done, !not_null); + generate_post_barrier(masm, Rstore_addr, Rnew_val, Z_thread, Rtmp1, Rtmp2, done, !not_null); __ bind(done); BLOCK_COMMENT("} g1_write_barrier_post"); @@ -500,7 +500,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, Register tmp1, Register tmp2) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, thread, tmp1, tmp2, done, true /* new_val_may_be_null */); masm->bind(done); } diff --git a/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp b/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp index 31f27e140e0..a846289d91b 100644 --- a/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/gc/g1/g1BarrierSetAssembler_x86.cpp @@ -268,12 +268,12 @@ void G1BarrierSetAssembler::g1_write_barrier_pre(MacroAssembler* masm, __ bind(done); } -static void generate_post_barrier_fast_path(MacroAssembler* masm, - const Register store_addr, - const Register new_val, - const Register tmp1, - Label& done, - bool new_val_may_be_null) { +static void generate_post_barrier(MacroAssembler* masm, + const Register store_addr, + const Register new_val, + const Register tmp1, + Label& done, + bool new_val_may_be_null) { assert_different_registers(store_addr, new_val, tmp1, noreg); @@ -310,7 +310,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post(MacroAssembler* masm, Register new_val, Register tmp) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, tmp, done, true /* new_val_may_be_null */); __ bind(done); } @@ -375,7 +375,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c2(MacroAssembler* masm, Register tmp, bool new_val_may_be_null) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp, done, new_val_may_be_null); + generate_post_barrier(masm, store_addr, new_val, tmp, done, new_val_may_be_null); __ bind(done); } @@ -466,7 +466,7 @@ void G1BarrierSetAssembler::g1_write_barrier_post_c1(MacroAssembler* masm, Register tmp1, Register tmp2 /* unused on x86 */) { Label done; - generate_post_barrier_fast_path(masm, store_addr, new_val, tmp1, done, true /* new_val_may_be_null */); + generate_post_barrier(masm, store_addr, new_val, tmp1, done, true /* new_val_may_be_null */); masm->bind(done); } From ed31023fc5a96a6f9a16c8a5c0fc86e794ce4aa7 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Wed, 24 Sep 2025 13:37:33 +0000 Subject: [PATCH 208/556] 8368367: Test jdk/jfr/event/gc/detailed/TestGCHeapMemoryUsageEvent.java fails jdk.GCHeapMemoryUsage "expected 0 > 0" Reviewed-by: iwalulya, ayang, syan --- .../jdk/jfr/event/gc/detailed/TestGCHeapMemoryUsageEvent.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jdk/jdk/jfr/event/gc/detailed/TestGCHeapMemoryUsageEvent.java b/test/jdk/jdk/jfr/event/gc/detailed/TestGCHeapMemoryUsageEvent.java index d54ca7a723d..706dc45b6b7 100644 --- a/test/jdk/jdk/jfr/event/gc/detailed/TestGCHeapMemoryUsageEvent.java +++ b/test/jdk/jdk/jfr/event/gc/detailed/TestGCHeapMemoryUsageEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,7 +49,7 @@ public class TestGCHeapMemoryUsageEvent { List events = Events.fromRecording(recording); System.out.println(events); assertFalse(events.isEmpty()); - RecordedEvent event = events.getFirst(); + RecordedEvent event = events.getLast(); Events.assertField(event, "used").above(0L); Events.assertField(event, "committed").above(0L); Events.assertField(event, "max").above(0L); From 156eb767f13ddc2c0a250950e208340db5989e3a Mon Sep 17 00:00:00 2001 From: Ashutosh Mehra Date: Wed, 24 Sep 2025 13:37:58 +0000 Subject: [PATCH 209/556] 8366905: Store AdapterBlob pointer in AdapterHandlerEntry Reviewed-by: kvn, adinn --- .../cpu/aarch64/sharedRuntime_aarch64.cpp | 14 +- src/hotspot/cpu/arm/sharedRuntime_arm.cpp | 11 +- src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp | 19 +- src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp | 14 +- src/hotspot/cpu/s390/sharedRuntime_s390.cpp | 16 +- src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp | 14 +- src/hotspot/cpu/zero/sharedRuntime_zero.cpp | 15 +- src/hotspot/share/code/codeBlob.cpp | 24 +-- src/hotspot/share/code/codeBlob.hpp | 13 +- src/hotspot/share/oops/method.cpp | 2 + src/hotspot/share/runtime/sharedRuntime.cpp | 184 +++++++----------- src/hotspot/share/runtime/sharedRuntime.hpp | 89 +++++---- 12 files changed, 184 insertions(+), 231 deletions(-) diff --git a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp index 748c3e8fb11..4b3b8e58b9a 100644 --- a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp @@ -683,12 +683,12 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, int comp_args_on_stack, const BasicType *sig_bt, const VMRegPair *regs, - AdapterHandlerEntry* handler) { - address i2c_entry = __ pc(); + address entry_address[AdapterBlob::ENTRY_COUNT]) { + entry_address[AdapterBlob::I2C] = __ pc(); gen_i2c_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs); - address c2i_unverified_entry = __ pc(); + entry_address[AdapterBlob::C2I_Unverified] = __ pc(); Label skip_fixup; Register data = rscratch2; @@ -718,10 +718,10 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ block_comment("} c2i_unverified_entry"); } - address c2i_entry = __ pc(); + entry_address[AdapterBlob::C2I] = __ pc(); // Class initialization barrier for static methods - address c2i_no_clinit_check_entry = nullptr; + entry_address[AdapterBlob::C2I_No_Clinit_Check] = nullptr; if (VM_Version::supports_fast_class_init_checks()) { Label L_skip_barrier; @@ -736,15 +736,13 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ far_jump(RuntimeAddress(SharedRuntime::get_handle_wrong_method_stub())); __ bind(L_skip_barrier); - c2i_no_clinit_check_entry = __ pc(); + entry_address[AdapterBlob::C2I_No_Clinit_Check] = __ pc(); } BarrierSetAssembler* bs = BarrierSet::barrier_set()->barrier_set_assembler(); bs->c2i_entry_barrier(masm); gen_c2i_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs, skip_fixup); - - handler->set_entry_points(i2c_entry, c2i_entry, c2i_unverified_entry, c2i_no_clinit_check_entry); return; } diff --git a/src/hotspot/cpu/arm/sharedRuntime_arm.cpp b/src/hotspot/cpu/arm/sharedRuntime_arm.cpp index dcf631525ab..9e90b56f9f0 100644 --- a/src/hotspot/cpu/arm/sharedRuntime_arm.cpp +++ b/src/hotspot/cpu/arm/sharedRuntime_arm.cpp @@ -617,11 +617,11 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, int comp_args_on_stack, const BasicType *sig_bt, const VMRegPair *regs, - AdapterHandlerEntry* handler) { - address i2c_entry = __ pc(); + address entry_address[AdapterBlob::ENTRY_COUNT]) { + entry_address[AdapterBlob::I2C] = __ pc(); gen_i2c_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs); - address c2i_unverified_entry = __ pc(); + entry_address[AdapterBlob::C2I_Unverified] = __ pc(); Label skip_fixup; const Register receiver = R0; const Register holder_klass = Rtemp; // XXX should be OK for C2 but not 100% sure @@ -634,10 +634,9 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ b(skip_fixup, eq); __ jump(SharedRuntime::get_ic_miss_stub(), relocInfo::runtime_call_type, noreg, ne); - address c2i_entry = __ pc(); + entry_address[AdapterBlob::C2I] = __ pc(); + entry_address[AdapterBlob::C2I_No_Clinit_Check] = nullptr; gen_c2i_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs, skip_fixup); - - handler->set_entry_points(i2c_entry, c2i_entry, c2i_unverified_entry, nullptr); return; } diff --git a/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp b/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp index 2a9bad059ba..aec36b3f3f9 100644 --- a/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp +++ b/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp @@ -1199,16 +1199,11 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, int comp_args_on_stack, const BasicType *sig_bt, const VMRegPair *regs, - AdapterHandlerEntry* handler) { - address i2c_entry; - address c2i_unverified_entry; - address c2i_entry; - - + address entry_address[AdapterBlob::ENTRY_COUNT]) { // entry: i2c __ align(CodeEntryAlignment); - i2c_entry = __ pc(); + entry_address[AdapterBlob::I2C] = __ pc(); gen_i2c_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs); @@ -1216,7 +1211,7 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ align(CodeEntryAlignment); BLOCK_COMMENT("c2i unverified entry"); - c2i_unverified_entry = __ pc(); + entry_address[AdapterBlob::C2I_Unverified] = __ pc(); // inline_cache contains a CompiledICData const Register ic = R19_inline_cache_reg; @@ -1244,10 +1239,10 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, // entry: c2i - c2i_entry = __ pc(); + entry_address[AdapterBlob::C2I] = __ pc(); // Class initialization barrier for static methods - address c2i_no_clinit_check_entry = nullptr; + entry_address[AdapterBlob::C2I_No_Clinit_Check] = nullptr; if (VM_Version::supports_fast_class_init_checks()) { Label L_skip_barrier; @@ -1266,15 +1261,13 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ bctr(); __ bind(L_skip_barrier); - c2i_no_clinit_check_entry = __ pc(); + entry_address[AdapterBlob::C2I_No_Clinit_Check] = __ pc(); } BarrierSetAssembler* bs = BarrierSet::barrier_set()->barrier_set_assembler(); bs->c2i_entry_barrier(masm, /* tmp register*/ ic_klass, /* tmp register*/ receiver_klass, /* tmp register*/ code); gen_c2i_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs, call_interpreter, ientry); - - handler->set_entry_points(i2c_entry, c2i_entry, c2i_unverified_entry, c2i_no_clinit_check_entry); return; } diff --git a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp index 2cc1fb8eeff..01970c68090 100644 --- a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp +++ b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp @@ -602,11 +602,11 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, int comp_args_on_stack, const BasicType *sig_bt, const VMRegPair *regs, - AdapterHandlerEntry* handler) { - address i2c_entry = __ pc(); + address entry_address[AdapterBlob::ENTRY_COUNT]) { + entry_address[AdapterBlob::I2C] = __ pc(); gen_i2c_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs); - address c2i_unverified_entry = __ pc(); + entry_address[AdapterBlob::C2I_Unverified] = __ pc(); Label skip_fixup; const Register receiver = j_rarg0; @@ -633,10 +633,10 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ block_comment("} c2i_unverified_entry"); } - address c2i_entry = __ pc(); + entry_address[AdapterBlob::C2I] = __ pc(); // Class initialization barrier for static methods - address c2i_no_clinit_check_entry = nullptr; + entry_address[AdapterBlob::C2I_No_Clinit_Check] = nullptr; if (VM_Version::supports_fast_class_init_checks()) { Label L_skip_barrier; @@ -651,15 +651,13 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ far_jump(RuntimeAddress(SharedRuntime::get_handle_wrong_method_stub())); __ bind(L_skip_barrier); - c2i_no_clinit_check_entry = __ pc(); + entry_address[AdapterBlob::C2I_No_Clinit_Check] = __ pc(); } BarrierSetAssembler* bs = BarrierSet::barrier_set()->barrier_set_assembler(); bs->c2i_entry_barrier(masm); gen_c2i_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs, skip_fixup); - - handler->set_entry_points(i2c_entry, c2i_entry, c2i_unverified_entry, c2i_no_clinit_check_entry); return; } diff --git a/src/hotspot/cpu/s390/sharedRuntime_s390.cpp b/src/hotspot/cpu/s390/sharedRuntime_s390.cpp index 3b1d06cf560..db1ee8351ac 100644 --- a/src/hotspot/cpu/s390/sharedRuntime_s390.cpp +++ b/src/hotspot/cpu/s390/sharedRuntime_s390.cpp @@ -2347,13 +2347,11 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, int comp_args_on_stack, const BasicType *sig_bt, const VMRegPair *regs, - AdapterHandlerEntry* handler) { + address entry_address[AdapterBlob::ENTRY_COUNT]) { __ align(CodeEntryAlignment); - address i2c_entry = __ pc(); + entry_address[AdapterBlob::I2C] = __ pc(); gen_i2c_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs); - address c2i_unverified_entry; - Label skip_fixup; { Label ic_miss; @@ -2363,7 +2361,7 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, // Unverified Entry Point UEP __ align(CodeEntryAlignment); - c2i_unverified_entry = __ pc(); + entry_address[AdapterBlob::C2I_Unverified] = __ pc(); __ ic_check(2); __ z_lg(Z_method, Address(Z_inline_cache, CompiledICData::speculated_method_offset())); @@ -2376,10 +2374,10 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, // Fallthru to VEP. Duplicate LTG, but saved taken branch. } - address c2i_entry = __ pc(); + entry_address[AdapterBlob::C2I] = __ pc(); // Class initialization barrier for static methods - address c2i_no_clinit_check_entry = nullptr; + entry_address[AdapterBlob::C2I_No_Clinit_Check] = nullptr; if (VM_Version::supports_fast_class_init_checks()) { Label L_skip_barrier; @@ -2396,12 +2394,10 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ z_br(klass); __ bind(L_skip_barrier); - c2i_no_clinit_check_entry = __ pc(); + entry_address[AdapterBlob::C2I_No_Clinit_Check] = __ pc(); } gen_c2i_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs, skip_fixup); - - handler->set_entry_points(i2c_entry, c2i_entry, c2i_unverified_entry, c2i_no_clinit_check_entry); return; } diff --git a/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp b/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp index d60f535fefc..e2a8f36b050 100644 --- a/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp +++ b/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp @@ -1007,8 +1007,8 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, int comp_args_on_stack, const BasicType *sig_bt, const VMRegPair *regs, - AdapterHandlerEntry* handler) { - address i2c_entry = __ pc(); + address entry_address[AdapterBlob::ENTRY_COUNT]) { + entry_address[AdapterBlob::I2C] = __ pc(); gen_i2c_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs); @@ -1021,7 +1021,7 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, // On exit from the interpreter, the interpreter will restore our SP (lest the // compiled code, which relies solely on SP and not RBP, get sick). - address c2i_unverified_entry = __ pc(); + entry_address[AdapterBlob::C2I_Unverified] = __ pc(); Label skip_fixup; Register data = rax; @@ -1039,10 +1039,10 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ jump(RuntimeAddress(SharedRuntime::get_ic_miss_stub())); } - address c2i_entry = __ pc(); + entry_address[AdapterBlob::C2I] = __ pc(); // Class initialization barrier for static methods - address c2i_no_clinit_check_entry = nullptr; + entry_address[AdapterBlob::C2I_No_Clinit_Check] = nullptr; if (VM_Version::supports_fast_class_init_checks()) { Label L_skip_barrier; Register method = rbx; @@ -1061,15 +1061,13 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, __ jump(RuntimeAddress(SharedRuntime::get_handle_wrong_method_stub())); // slow path __ bind(L_skip_barrier); - c2i_no_clinit_check_entry = __ pc(); + entry_address[AdapterBlob::C2I_No_Clinit_Check] = __ pc(); } BarrierSetAssembler* bs = BarrierSet::barrier_set()->barrier_set_assembler(); bs->c2i_entry_barrier(masm); gen_c2i_adapter(masm, total_args_passed, comp_args_on_stack, sig_bt, regs, skip_fixup); - - handler->set_entry_points(i2c_entry, c2i_entry, c2i_unverified_entry, c2i_no_clinit_check_entry); return; } diff --git a/src/hotspot/cpu/zero/sharedRuntime_zero.cpp b/src/hotspot/cpu/zero/sharedRuntime_zero.cpp index b2e406c205b..6e18097c992 100644 --- a/src/hotspot/cpu/zero/sharedRuntime_zero.cpp +++ b/src/hotspot/cpu/zero/sharedRuntime_zero.cpp @@ -38,12 +38,6 @@ #include "opto/runtime.hpp" #endif - -static address zero_null_code_stub() { - address start = ShouldNotCallThisStub(); - return start; -} - int SharedRuntime::java_calling_convention(const BasicType *sig_bt, VMRegPair *regs, int total_args_passed) { @@ -55,12 +49,9 @@ void SharedRuntime::generate_i2c2i_adapters(MacroAssembler *masm, int comp_args_on_stack, const BasicType *sig_bt, const VMRegPair *regs, - AdapterHandlerEntry* handler) { - // foil any attempt to call the i2c, c2i or unverified c2i entries - handler->set_entry_points(CAST_FROM_FN_PTR(address,zero_null_code_stub), - CAST_FROM_FN_PTR(address,zero_null_code_stub), - CAST_FROM_FN_PTR(address,zero_null_code_stub), - nullptr); + address entry_address[AdapterBlob::ENTRY_COUNT]) { + ShouldNotCallThis(); + return; } nmethod *SharedRuntime::generate_native_wrapper(MacroAssembler *masm, diff --git a/src/hotspot/share/code/codeBlob.cpp b/src/hotspot/share/code/codeBlob.cpp index 9ec5478a071..18e77520139 100644 --- a/src/hotspot/share/code/codeBlob.cpp +++ b/src/hotspot/share/code/codeBlob.cpp @@ -448,16 +448,19 @@ void BufferBlob::free(BufferBlob *blob) { AdapterBlob::AdapterBlob(int size, CodeBuffer* cb, int entry_offset[AdapterBlob::ENTRY_COUNT]) : BufferBlob("I2C/C2I adapters", CodeBlobKind::Adapter, cb, size, sizeof(AdapterBlob)) { - assert(entry_offset[0] == 0, "sanity check"); + assert(entry_offset[I2C] == 0, "sanity check"); +#ifdef ASSERT for (int i = 1; i < AdapterBlob::ENTRY_COUNT; i++) { // The entry is within the adapter blob or unset. - assert((entry_offset[i] > 0 && entry_offset[i] < cb->insts()->size()) || - (entry_offset[i] == -1), - "invalid entry offset[%d] = 0x%x", i, entry_offset[i]); + int offset = entry_offset[i]; + assert((offset > 0 && offset < cb->insts()->size()) || + (i >= C2I_No_Clinit_Check && offset == -1), + "invalid entry offset[%d] = 0x%x", i, offset); } - _c2i_offset = entry_offset[1]; - _c2i_unverified_offset = entry_offset[2]; - _c2i_no_clinit_check_offset = entry_offset[3]; +#endif // ASSERT + _c2i_offset = entry_offset[C2I]; + _c2i_unverified_offset = entry_offset[C2I_Unverified]; + _c2i_no_clinit_check_offset = entry_offset[C2I_No_Clinit_Check]; CodeCache::commit(this); } @@ -478,13 +481,6 @@ AdapterBlob* AdapterBlob::create(CodeBuffer* cb, int entry_offset[AdapterBlob::E return blob; } -void AdapterBlob::get_offsets(int entry_offset[ENTRY_COUNT]) { - entry_offset[0] = 0; - entry_offset[1] = _c2i_offset; - entry_offset[2] = _c2i_unverified_offset; - entry_offset[3] = _c2i_no_clinit_check_offset; -} - //---------------------------------------------------------------------------------------------------- // Implementation of VtableBlob diff --git a/src/hotspot/share/code/codeBlob.hpp b/src/hotspot/share/code/codeBlob.hpp index 407974f0428..0b4760e7e1e 100644 --- a/src/hotspot/share/code/codeBlob.hpp +++ b/src/hotspot/share/code/codeBlob.hpp @@ -405,7 +405,13 @@ class BufferBlob: public RuntimeBlob { class AdapterBlob: public BufferBlob { public: - static const int ENTRY_COUNT = 4; + enum Entry { + I2C, + C2I, + C2I_Unverified, + C2I_No_Clinit_Check, + ENTRY_COUNT + }; private: AdapterBlob(int size, CodeBuffer* cb, int entry_offset[ENTRY_COUNT]); // _i2c_offset is always 0 so no need to store it @@ -415,7 +421,10 @@ private: public: // Creation static AdapterBlob* create(CodeBuffer* cb, int entry_offset[ENTRY_COUNT]); - void get_offsets(int entry_offset[ENTRY_COUNT]); + address i2c_entry() { return code_begin(); } + address c2i_entry() { return i2c_entry() + _c2i_offset; } + address c2i_unverified_entry() { return i2c_entry() + _c2i_unverified_offset; } + address c2i_no_clinit_check_entry() { return _c2i_no_clinit_check_offset == -1 ? nullptr : i2c_entry() + _c2i_no_clinit_check_offset; } }; //--------------------------------------------------------------------------------------------------- diff --git a/src/hotspot/share/oops/method.cpp b/src/hotspot/share/oops/method.cpp index c09a6c14485..cb1c8ea37e8 100644 --- a/src/hotspot/share/oops/method.cpp +++ b/src/hotspot/share/oops/method.cpp @@ -1287,7 +1287,9 @@ void Method::link_method(const methodHandle& h_method, TRAPS) { h_method->_from_compiled_entry = SharedRuntime::get_handle_wrong_method_abstract_stub(); } else if (_adapter == nullptr) { (void) make_adapters(h_method, CHECK); +#ifndef ZERO assert(adapter()->is_linked(), "Adapter must have been linked"); +#endif h_method->_from_compiled_entry = adapter()->get_c2i_entry(); } diff --git a/src/hotspot/share/runtime/sharedRuntime.cpp b/src/hotspot/share/runtime/sharedRuntime.cpp index 2f7161ff744..c3a6e0a4dc3 100644 --- a/src/hotspot/share/runtime/sharedRuntime.cpp +++ b/src/hotspot/share/runtime/sharedRuntime.cpp @@ -2526,21 +2526,21 @@ BufferBlob* AdapterHandlerLibrary::buffer_blob() { return _buffer; } -static void post_adapter_creation(const AdapterBlob* new_adapter, - const AdapterHandlerEntry* entry) { +static void post_adapter_creation(const AdapterHandlerEntry* entry) { if (Forte::is_enabled() || JvmtiExport::should_post_dynamic_code_generated()) { + AdapterBlob* adapter_blob = entry->adapter_blob(); char blob_id[256]; jio_snprintf(blob_id, sizeof(blob_id), "%s(%s)", - new_adapter->name(), + adapter_blob->name(), entry->fingerprint()->as_string()); if (Forte::is_enabled()) { - Forte::register_stub(blob_id, new_adapter->content_begin(), new_adapter->content_end()); + Forte::register_stub(blob_id, adapter_blob->content_begin(), adapter_blob->content_end()); } if (JvmtiExport::should_post_dynamic_code_generated()) { - JvmtiExport::post_dynamic_code_generated(blob_id, new_adapter->content_begin(), new_adapter->content_end()); + JvmtiExport::post_dynamic_code_generated(blob_id, adapter_blob->content_begin(), adapter_blob->content_end()); } } } @@ -2562,27 +2562,22 @@ void AdapterHandlerLibrary::initialize() { #endif // INCLUDE_CDS ResourceMark rm; - AdapterBlob* no_arg_blob = nullptr; - AdapterBlob* int_arg_blob = nullptr; - AdapterBlob* obj_arg_blob = nullptr; - AdapterBlob* obj_int_arg_blob = nullptr; - AdapterBlob* obj_obj_arg_blob = nullptr; { MutexLocker mu(AdapterHandlerLibrary_lock); - _no_arg_handler = create_adapter(no_arg_blob, 0, nullptr); + _no_arg_handler = create_adapter(0, nullptr); BasicType obj_args[] = { T_OBJECT }; - _obj_arg_handler = create_adapter(obj_arg_blob, 1, obj_args); + _obj_arg_handler = create_adapter(1, obj_args); BasicType int_args[] = { T_INT }; - _int_arg_handler = create_adapter(int_arg_blob, 1, int_args); + _int_arg_handler = create_adapter(1, int_args); BasicType obj_int_args[] = { T_OBJECT, T_INT }; - _obj_int_arg_handler = create_adapter(obj_int_arg_blob, 2, obj_int_args); + _obj_int_arg_handler = create_adapter(2, obj_int_args); BasicType obj_obj_args[] = { T_OBJECT, T_OBJECT }; - _obj_obj_arg_handler = create_adapter(obj_obj_arg_blob, 2, obj_obj_args); + _obj_obj_arg_handler = create_adapter(2, obj_obj_args); // we should always get an entry back but we don't have any // associated blob on Zero @@ -2596,11 +2591,11 @@ void AdapterHandlerLibrary::initialize() { // Outside of the lock #ifndef ZERO // no blobs to register when we are on Zero - post_adapter_creation(no_arg_blob, _no_arg_handler); - post_adapter_creation(obj_arg_blob, _obj_arg_handler); - post_adapter_creation(int_arg_blob, _int_arg_handler); - post_adapter_creation(obj_int_arg_blob, _obj_int_arg_handler); - post_adapter_creation(obj_obj_arg_blob, _obj_obj_arg_handler); + post_adapter_creation(_no_arg_handler); + post_adapter_creation(_obj_arg_handler); + post_adapter_creation(_int_arg_handler); + post_adapter_creation(_obj_int_arg_handler); + post_adapter_creation(_obj_obj_arg_handler); #endif // ZERO } @@ -2695,9 +2690,8 @@ const char* AdapterHandlerEntry::_entry_names[] = { void AdapterHandlerLibrary::verify_adapter_sharing(int total_args_passed, BasicType* sig_bt, AdapterHandlerEntry* cached_entry) { // we can only check for the same code if there is any #ifndef ZERO - AdapterBlob* comparison_blob = nullptr; - AdapterHandlerEntry* comparison_entry = create_adapter(comparison_blob, total_args_passed, sig_bt, true); - assert(comparison_blob == nullptr, "no blob should be created when creating an adapter for comparison"); + AdapterHandlerEntry* comparison_entry = create_adapter(total_args_passed, sig_bt, true); + assert(comparison_entry->adapter_blob() == nullptr, "no blob should be created when creating an adapter for comparison"); assert(comparison_entry->compare_code(cached_entry), "code must match"); // Release the one just created AdapterHandlerEntry::deallocate(comparison_entry); @@ -2719,7 +2713,7 @@ AdapterHandlerEntry* AdapterHandlerLibrary::get_adapter(const methodHandle& meth } ResourceMark rm; - AdapterBlob* adapter_blob = nullptr; + bool new_entry = false; // Fill in the signature array, for the calling-convention call. int total_args_passed = method->size_of_parameters(); // All args on stack @@ -2735,54 +2729,48 @@ AdapterHandlerEntry* AdapterHandlerLibrary::get_adapter(const methodHandle& meth entry = lookup(total_args_passed, sig_bt); if (entry != nullptr) { +#ifndef ZERO assert(entry->is_linked(), "AdapterHandlerEntry must have been linked"); +#endif #ifdef ASSERT if (!entry->in_aot_cache() && VerifyAdapterSharing) { verify_adapter_sharing(total_args_passed, sig_bt, entry); } #endif } else { - entry = create_adapter(adapter_blob, total_args_passed, sig_bt); + entry = create_adapter(total_args_passed, sig_bt); + if (entry != nullptr) { + new_entry = true; + } } } // Outside of the lock - if (adapter_blob != nullptr) { - post_adapter_creation(adapter_blob, entry); + if (new_entry) { + post_adapter_creation(entry); } return entry; } -AdapterBlob* AdapterHandlerLibrary::lookup_aot_cache(AdapterHandlerEntry* handler) { +void AdapterHandlerLibrary::lookup_aot_cache(AdapterHandlerEntry* handler) { ResourceMark rm; const char* name = AdapterHandlerLibrary::name(handler->fingerprint()); const uint32_t id = AdapterHandlerLibrary::id(handler->fingerprint()); - int offsets[AdapterBlob::ENTRY_COUNT]; - AdapterBlob* adapter_blob = nullptr; CodeBlob* blob = AOTCodeCache::load_code_blob(AOTCodeEntry::Adapter, id, name); if (blob != nullptr) { - adapter_blob = blob->as_adapter_blob(); - adapter_blob->get_offsets(offsets); - address i2c_entry = adapter_blob->content_begin(); - assert(offsets[0] == 0, "sanity check"); - handler->set_entry_points( - i2c_entry, - (offsets[1] != -1) ? (i2c_entry + offsets[1]) : nullptr, - (offsets[2] != -1) ? (i2c_entry + offsets[2]) : nullptr, - (offsets[3] != -1) ? (i2c_entry + offsets[3]) : nullptr - ); + handler->set_adapter_blob(blob->as_adapter_blob()); } - return adapter_blob; } #ifndef PRODUCT -void AdapterHandlerLibrary::print_adapter_handler_info(outputStream* st, AdapterHandlerEntry* handler, AdapterBlob* adapter_blob) { +void AdapterHandlerLibrary::print_adapter_handler_info(outputStream* st, AdapterHandlerEntry* handler) { ttyLocker ttyl; ResourceMark rm; int insts_size; // on Zero the blob may be null handler->print_adapter_on(tty); + AdapterBlob* adapter_blob = handler->adapter_blob(); if (adapter_blob == nullptr) { return; } @@ -2792,7 +2780,7 @@ void AdapterHandlerLibrary::print_adapter_handler_info(outputStream* st, Adapter handler->fingerprint()->as_string(), insts_size); st->print_cr("c2i argument handler starts at " INTPTR_FORMAT, p2i(handler->get_c2i_entry())); if (Verbose || PrintStubCode) { - address first_pc = handler->base_address(); + address first_pc = adapter_blob->content_begin(); if (first_pc != nullptr) { Disassembler::decode(first_pc, first_pc + insts_size, st, &adapter_blob->asm_remarks()); st->cr(); @@ -2801,8 +2789,19 @@ void AdapterHandlerLibrary::print_adapter_handler_info(outputStream* st, Adapter } #endif // PRODUCT -bool AdapterHandlerLibrary::generate_adapter_code(AdapterBlob*& adapter_blob, - AdapterHandlerEntry* handler, +void AdapterHandlerLibrary::address_to_offset(address entry_address[AdapterBlob::ENTRY_COUNT], + int entry_offset[AdapterBlob::ENTRY_COUNT]) { + entry_offset[AdapterBlob::I2C] = 0; + entry_offset[AdapterBlob::C2I] = entry_address[AdapterBlob::C2I] - entry_address[AdapterBlob::I2C]; + entry_offset[AdapterBlob::C2I_Unverified] = entry_address[AdapterBlob::C2I_Unverified] - entry_address[AdapterBlob::I2C]; + if (entry_address[AdapterBlob::C2I_No_Clinit_Check] == nullptr) { + entry_offset[AdapterBlob::C2I_No_Clinit_Check] = -1; + } else { + entry_offset[AdapterBlob::C2I_No_Clinit_Check] = entry_address[AdapterBlob::C2I_No_Clinit_Check] - entry_address[AdapterBlob::I2C]; + } +} + +bool AdapterHandlerLibrary::generate_adapter_code(AdapterHandlerEntry* handler, int total_args_passed, BasicType* sig_bt, bool is_transient) { @@ -2810,6 +2809,7 @@ bool AdapterHandlerLibrary::generate_adapter_code(AdapterBlob*& adapter_blob, ClassLoader::perf_method_adapters_count()->inc(); } +#ifndef ZERO BufferBlob* buf = buffer_blob(); // the temporary code buffer in CodeCache CodeBuffer buffer(buf); short buffer_locs[20]; @@ -2821,17 +2821,17 @@ bool AdapterHandlerLibrary::generate_adapter_code(AdapterBlob*& adapter_blob, // Get a description of the compiled java calling convention and the largest used (VMReg) stack slot usage int comp_args_on_stack = SharedRuntime::java_calling_convention(sig_bt, regs, total_args_passed); + address entry_address[AdapterBlob::ENTRY_COUNT]; SharedRuntime::generate_i2c2i_adapters(&masm, total_args_passed, comp_args_on_stack, sig_bt, regs, - handler); -#ifdef ZERO + entry_address); // On zero there is no code to save and no need to create a blob and // or relocate the handler. - adapter_blob = nullptr; -#else + int entry_offset[AdapterBlob::ENTRY_COUNT]; + address_to_offset(entry_address, entry_offset); #ifdef ASSERT if (VerifyAdapterSharing) { handler->save_code(buf->code_begin(), buffer.insts_size()); @@ -2840,25 +2840,14 @@ bool AdapterHandlerLibrary::generate_adapter_code(AdapterBlob*& adapter_blob, } } #endif - - int entry_offset[AdapterBlob::ENTRY_COUNT]; - assert(AdapterBlob::ENTRY_COUNT == 4, "sanity"); - address i2c_entry = handler->get_i2c_entry(); - entry_offset[0] = 0; // i2c_entry offset - entry_offset[1] = (handler->get_c2i_entry() != nullptr) ? - (handler->get_c2i_entry() - i2c_entry) : -1; - entry_offset[2] = (handler->get_c2i_unverified_entry() != nullptr) ? - (handler->get_c2i_unverified_entry() - i2c_entry) : -1; - entry_offset[3] = (handler->get_c2i_no_clinit_check_entry() != nullptr) ? - (handler->get_c2i_no_clinit_check_entry() - i2c_entry) : -1; - - adapter_blob = AdapterBlob::create(&buffer, entry_offset); + AdapterBlob* adapter_blob = AdapterBlob::create(&buffer, entry_offset); if (adapter_blob == nullptr) { // CodeCache is full, disable compilation // Ought to log this but compile log is only per compile thread // and we're some non descript Java thread. return false; } + handler->set_adapter_blob(adapter_blob); if (!is_transient && AOTCodeCache::is_dumping_adapter()) { // try to save generated code const char* name = AdapterHandlerLibrary::name(handler->fingerprint()); @@ -2866,26 +2855,24 @@ bool AdapterHandlerLibrary::generate_adapter_code(AdapterBlob*& adapter_blob, bool success = AOTCodeCache::store_code_blob(*adapter_blob, AOTCodeEntry::Adapter, id, name); assert(success || !AOTCodeCache::is_dumping_adapter(), "caching of adapter must be disabled"); } - handler->relocate(adapter_blob->content_begin()); #endif // ZERO #ifndef PRODUCT // debugging support if (PrintAdapterHandlers || PrintStubCode) { - print_adapter_handler_info(tty, handler, adapter_blob); + print_adapter_handler_info(tty, handler); } #endif return true; } -AdapterHandlerEntry* AdapterHandlerLibrary::create_adapter(AdapterBlob*& adapter_blob, - int total_args_passed, +AdapterHandlerEntry* AdapterHandlerLibrary::create_adapter(int total_args_passed, BasicType* sig_bt, bool is_transient) { AdapterFingerPrint* fp = AdapterFingerPrint::allocate(total_args_passed, sig_bt); AdapterHandlerEntry* handler = AdapterHandlerLibrary::new_entry(fp); - if (!generate_adapter_code(adapter_blob, handler, total_args_passed, sig_bt, is_transient)) { + if (!generate_adapter_code(handler, total_args_passed, sig_bt, is_transient)) { AdapterHandlerEntry::deallocate(handler); return nullptr; } @@ -2902,7 +2889,8 @@ void AdapterHandlerEntry::remove_unshareable_info() { _saved_code = nullptr; _saved_code_length = 0; #endif // ASSERT - set_entry_points(nullptr, nullptr, nullptr, nullptr, false); + _adapter_blob = nullptr; + _linked = false; } class CopyAdapterTableToArchive : StackObj { @@ -2952,26 +2940,24 @@ void AdapterHandlerLibrary::serialize_shared_table_header(SerializeClosure* soc) _aot_adapter_handler_table.serialize_header(soc); } -AdapterBlob* AdapterHandlerLibrary::link_aot_adapter_handler(AdapterHandlerEntry* handler) { +void AdapterHandlerLibrary::link_aot_adapter_handler(AdapterHandlerEntry* handler) { #ifdef ASSERT if (TestAOTAdapterLinkFailure) { - return nullptr; + return; } #endif - AdapterBlob* blob = lookup_aot_cache(handler); + lookup_aot_cache(handler); #ifndef PRODUCT // debugging support - if ((blob != nullptr) && (PrintAdapterHandlers || PrintStubCode)) { - print_adapter_handler_info(tty, handler, blob); + if (PrintAdapterHandlers || PrintStubCode) { + print_adapter_handler_info(tty, handler); } #endif - return blob; } // This method is used during production run to link archived adapters (stored in AOT Cache) // to their code in AOT Code Cache void AdapterHandlerEntry::link() { - AdapterBlob* adapter_blob = nullptr; ResourceMark rm; assert(_fingerprint != nullptr, "_fingerprint must not be null"); bool generate_code = false; @@ -2979,8 +2965,9 @@ void AdapterHandlerEntry::link() { // caching adapters is disabled, or we fail to link // the AdapterHandlerEntry to its code in the AOTCodeCache if (AOTCodeCache::is_using_adapter()) { - adapter_blob = AdapterHandlerLibrary::link_aot_adapter_handler(this); - if (adapter_blob == nullptr) { + AdapterHandlerLibrary::link_aot_adapter_handler(this); + // If link_aot_adapter_handler() succeeds, _adapter_blob will be non-null + if (_adapter_blob == nullptr) { log_warning(aot)("Failed to link AdapterHandlerEntry (fp=%s) to its code in the AOT code cache", _fingerprint->as_basic_args_string()); generate_code = true; } @@ -2990,16 +2977,15 @@ void AdapterHandlerEntry::link() { if (generate_code) { int nargs; BasicType* bt = _fingerprint->as_basic_type(nargs); - if (!AdapterHandlerLibrary::generate_adapter_code(adapter_blob, this, nargs, bt, /* is_transient */ false)) { + if (!AdapterHandlerLibrary::generate_adapter_code(this, nargs, bt, /* is_transient */ false)) { // Don't throw exceptions during VM initialization because java.lang.* classes // might not have been initialized, causing problems when constructing the // Java exception object. vm_exit_during_initialization("Out of space in CodeCache for adapters"); } } - // Outside of the lock - if (adapter_blob != nullptr) { - post_adapter_creation(adapter_blob, this); + if (_adapter_blob != nullptr) { + post_adapter_creation(this); } assert(_linked, "AdapterHandlerEntry must now be linked"); } @@ -3045,30 +3031,6 @@ void AdapterHandlerLibrary::lookup_simple_adapters() { } #endif // INCLUDE_CDS -address AdapterHandlerEntry::base_address() { - address base = _i2c_entry; - if (base == nullptr) base = _c2i_entry; - assert(base <= _c2i_entry || _c2i_entry == nullptr, ""); - assert(base <= _c2i_unverified_entry || _c2i_unverified_entry == nullptr, ""); - assert(base <= _c2i_no_clinit_check_entry || _c2i_no_clinit_check_entry == nullptr, ""); - return base; -} - -void AdapterHandlerEntry::relocate(address new_base) { - address old_base = base_address(); - assert(old_base != nullptr, ""); - ptrdiff_t delta = new_base - old_base; - if (_i2c_entry != nullptr) - _i2c_entry += delta; - if (_c2i_entry != nullptr) - _c2i_entry += delta; - if (_c2i_unverified_entry != nullptr) - _c2i_unverified_entry += delta; - if (_c2i_no_clinit_check_entry != nullptr) - _c2i_no_clinit_check_entry += delta; - assert(base_address() == new_base, ""); -} - void AdapterHandlerEntry::metaspace_pointers_do(MetaspaceClosure* it) { LogStreamHandle(Trace, aot) lsh; if (lsh.is_enabled()) { @@ -3443,17 +3405,13 @@ void AdapterHandlerLibrary::print_handler_on(outputStream* st, const CodeBlob* b void AdapterHandlerEntry::print_adapter_on(outputStream* st) const { st->print("AHE@" INTPTR_FORMAT ": %s", p2i(this), fingerprint()->as_string()); - if (get_i2c_entry() != nullptr) { + if (adapter_blob() != nullptr) { st->print(" i2c: " INTPTR_FORMAT, p2i(get_i2c_entry())); - } - if (get_c2i_entry() != nullptr) { st->print(" c2i: " INTPTR_FORMAT, p2i(get_c2i_entry())); - } - if (get_c2i_unverified_entry() != nullptr) { st->print(" c2iUV: " INTPTR_FORMAT, p2i(get_c2i_unverified_entry())); - } - if (get_c2i_no_clinit_check_entry() != nullptr) { - st->print(" c2iNCI: " INTPTR_FORMAT, p2i(get_c2i_no_clinit_check_entry())); + if (get_c2i_no_clinit_check_entry() != nullptr) { + st->print(" c2iNCI: " INTPTR_FORMAT, p2i(get_c2i_no_clinit_check_entry())); + } } st->cr(); } diff --git a/src/hotspot/share/runtime/sharedRuntime.hpp b/src/hotspot/share/runtime/sharedRuntime.hpp index 288bc0adc52..374985ad921 100644 --- a/src/hotspot/share/runtime/sharedRuntime.hpp +++ b/src/hotspot/share/runtime/sharedRuntime.hpp @@ -476,11 +476,11 @@ class SharedRuntime: AllStatic { // handshaking path with compiled code to keep the stack walking correct. static void generate_i2c2i_adapters(MacroAssembler *_masm, - int total_args_passed, - int max_arg, - const BasicType *sig_bt, - const VMRegPair *regs, - AdapterHandlerEntry* handler); + int total_args_passed, + int max_arg, + const BasicType *sig_bt, + const VMRegPair *regs, + address entry_address[AdapterBlob::ENTRY_COUNT]); static void gen_i2c_adapter(MacroAssembler *_masm, int total_args_passed, @@ -685,11 +685,8 @@ class AdapterHandlerEntry : public MetaspaceObj { private: AdapterFingerPrint* _fingerprint; - address _i2c_entry; - address _c2i_entry; - address _c2i_unverified_entry; - address _c2i_no_clinit_check_entry; - bool _linked; + AdapterBlob* _adapter_blob; + bool _linked; static const char *_entry_names[]; @@ -702,10 +699,7 @@ class AdapterHandlerEntry : public MetaspaceObj { AdapterHandlerEntry(AdapterFingerPrint* fingerprint) : _fingerprint(fingerprint), - _i2c_entry(nullptr), - _c2i_entry(nullptr), - _c2i_unverified_entry(nullptr), - _c2i_no_clinit_check_entry(nullptr), + _adapter_blob(nullptr), _linked(false) #ifdef ASSERT , _saved_code(nullptr), @@ -734,27 +728,49 @@ class AdapterHandlerEntry : public MetaspaceObj { handler->~AdapterHandlerEntry(); } - void set_entry_points(address i2c_entry, address c2i_entry, address c2i_unverified_entry, address c2i_no_clinit_check_entry, bool linked = true) { - _i2c_entry = i2c_entry; - _c2i_entry = c2i_entry; - _c2i_unverified_entry = c2i_unverified_entry; - _c2i_no_clinit_check_entry = c2i_no_clinit_check_entry; - _linked = linked; + void set_adapter_blob(AdapterBlob* blob) { + _adapter_blob = blob; + _linked = true; } - address get_i2c_entry() const { return _i2c_entry; } - address get_c2i_entry() const { return _c2i_entry; } - address get_c2i_unverified_entry() const { return _c2i_unverified_entry; } - address get_c2i_no_clinit_check_entry() const { return _c2i_no_clinit_check_entry; } - - static const char* entry_name(int i) { - assert(i >=0 && i < ENTRIES_COUNT, "entry id out of range"); - return _entry_names[i]; + address get_i2c_entry() const { +#ifndef ZERO + assert(_adapter_blob != nullptr, "must be"); + return _adapter_blob->i2c_entry(); +#else + return nullptr; +#endif // ZERO } + address get_c2i_entry() const { +#ifndef ZERO + assert(_adapter_blob != nullptr, "must be"); + return _adapter_blob->c2i_entry(); +#else + return nullptr; +#endif // ZERO + } + + address get_c2i_unverified_entry() const { +#ifndef ZERO + assert(_adapter_blob != nullptr, "must be"); + return _adapter_blob->c2i_unverified_entry(); +#else + return nullptr; +#endif // ZERO + } + + address get_c2i_no_clinit_check_entry() const { +#ifndef ZERO + assert(_adapter_blob != nullptr, "must be"); + return _adapter_blob->c2i_no_clinit_check_entry(); +#else + return nullptr; +#endif // ZERO + } + + AdapterBlob* adapter_blob() const { return _adapter_blob; } bool is_linked() const { return _linked; } - address base_address(); - void relocate(address new_base); AdapterFingerPrint* fingerprint() const { return _fingerprint; } @@ -795,14 +811,13 @@ class AdapterHandlerLibrary: public AllStatic { static BufferBlob* buffer_blob(); static void initialize(); static AdapterHandlerEntry* get_simple_adapter(const methodHandle& method); - static AdapterBlob* lookup_aot_cache(AdapterHandlerEntry* handler); - static AdapterHandlerEntry* create_adapter(AdapterBlob*& new_adapter, - int total_args_passed, + static void lookup_aot_cache(AdapterHandlerEntry* handler); + static AdapterHandlerEntry* create_adapter(int total_args_passed, BasicType* sig_bt, bool is_transient = false); static void lookup_simple_adapters() NOT_CDS_RETURN; #ifndef PRODUCT - static void print_adapter_handler_info(outputStream* st, AdapterHandlerEntry* handler, AdapterBlob* adapter_blob); + static void print_adapter_handler_info(outputStream* st, AdapterHandlerEntry* handler); #endif // PRODUCT public: @@ -810,8 +825,7 @@ class AdapterHandlerLibrary: public AllStatic { static void create_native_wrapper(const methodHandle& method); static AdapterHandlerEntry* get_adapter(const methodHandle& method); static AdapterHandlerEntry* lookup(int total_args_passed, BasicType* sig_bt); - static bool generate_adapter_code(AdapterBlob*& adapter_blob, - AdapterHandlerEntry* handler, + static bool generate_adapter_code(AdapterHandlerEntry* handler, int total_args_passed, BasicType* sig_bt, bool is_transient); @@ -829,10 +843,11 @@ class AdapterHandlerLibrary: public AllStatic { static void print_statistics(); #endif // PRODUCT - static AdapterBlob* link_aot_adapter_handler(AdapterHandlerEntry* handler) NOT_CDS_RETURN_(nullptr); + static void link_aot_adapter_handler(AdapterHandlerEntry* handler) NOT_CDS_RETURN; static void dump_aot_adapter_table() NOT_CDS_RETURN; static void serialize_shared_table_header(SerializeClosure* soc) NOT_CDS_RETURN; static void link_aot_adapters() NOT_CDS_RETURN; + static void address_to_offset(address entry_address[AdapterBlob::ENTRY_COUNT], int entry_offset[AdapterBlob::ENTRY_COUNT]); }; #endif // SHARE_RUNTIME_SHAREDRUNTIME_HPP From 735afd93bbdd63d53dc4cec0ac970026ac95cc64 Mon Sep 17 00:00:00 2001 From: Guanqiang Han Date: Wed, 24 Sep 2025 14:10:19 +0000 Subject: [PATCH 210/556] 8366421: ModifiedUtf.utfLen may overflow for giant string Reviewed-by: liach, rriggs --- .../classes/java/io/DataOutputStream.java | 17 +- .../classes/java/io/ObjectOutputStream.java | 8 +- .../classfile/impl/BufWriterImpl.java | 8 +- .../jdk/internal/util/ModifiedUtf.java | 12 +- .../jdk/internal/util/ModifiedUtfTest.java | 145 ++++++++++++++++++ 5 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 test/jdk/jdk/internal/util/ModifiedUtfTest.java diff --git a/src/java.base/share/classes/java/io/DataOutputStream.java b/src/java.base/share/classes/java/io/DataOutputStream.java index 4b22d65bd39..2a0a7526591 100644 --- a/src/java.base/share/classes/java/io/DataOutputStream.java +++ b/src/java.base/share/classes/java/io/DataOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1994, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1994, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,6 +26,8 @@ package java.io; +import java.lang.runtime.ExactConversionsSupport; + import jdk.internal.access.JavaLangAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.util.ByteArray; @@ -364,11 +366,12 @@ public class DataOutputStream extends FilterOutputStream implements DataOutput { static int writeUTF(String str, DataOutput out) throws IOException { final int strlen = str.length(); int countNonZeroAscii = JLA.countNonZeroAscii(str); - int utflen = utfLen(str, countNonZeroAscii); + long utflenLong = utfLen(str, countNonZeroAscii); - if (utflen > 65535 || /* overflow */ utflen < strlen) - throw new UTFDataFormatException(tooLongMsg(str, utflen)); + if (!ExactConversionsSupport.isLongToCharExact(utflenLong)) + throw new UTFDataFormatException(tooLongMsg(str, utflenLong)); + int utflen = (int)utflenLong; final byte[] bytearr; if (out instanceof DataOutputStream dos) { if (dos.bytearr == null || (dos.bytearr.length < (utflen + 2))) @@ -391,14 +394,12 @@ public class DataOutputStream extends FilterOutputStream implements DataOutput { return utflen + 2; } - private static String tooLongMsg(String s, int bits32) { + private static String tooLongMsg(String s, long utflen) { int slen = s.length(); String head = s.substring(0, 8); String tail = s.substring(slen - 8, slen); - // handle int overflow with max 3x expansion - long actualLength = (long)slen + Integer.toUnsignedLong(bits32 - slen); return "encoded string (" + head + "..." + tail + ") too long: " - + actualLength + " bytes"; + + utflen + " bytes"; } /** diff --git a/src/java.base/share/classes/java/io/ObjectOutputStream.java b/src/java.base/share/classes/java/io/ObjectOutputStream.java index 31413bcf8ed..40777ca1587 100644 --- a/src/java.base/share/classes/java/io/ObjectOutputStream.java +++ b/src/java.base/share/classes/java/io/ObjectOutputStream.java @@ -26,6 +26,8 @@ package java.io; +import java.lang.runtime.ExactConversionsSupport; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -1899,12 +1901,12 @@ public class ObjectOutputStream private void writeUTFInternal(String str, boolean writeHeader) throws IOException { int strlen = str.length(); int countNonZeroAscii = JLA.countNonZeroAscii(str); - int utflen = utfLen(str, countNonZeroAscii); - if (utflen <= 0xFFFF) { + long utflen = utfLen(str, countNonZeroAscii); + if (ExactConversionsSupport.isLongToCharExact(utflen)) { if(writeHeader) { writeByte(TC_STRING); } - writeShort(utflen); + writeShort((short)utflen); } else { if(writeHeader) { writeByte(TC_LONGSTRING); diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java b/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java index dda9accd8b9..b30592a4ebd 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/BufWriterImpl.java @@ -30,6 +30,7 @@ import java.lang.classfile.constantpool.ClassEntry; import java.lang.classfile.constantpool.ConstantPool; import java.lang.classfile.constantpool.ConstantPoolBuilder; import java.lang.classfile.constantpool.PoolEntry; +import java.lang.runtime.ExactConversionsSupport; import java.util.Arrays; import jdk.internal.access.JavaLangAccess; @@ -275,8 +276,11 @@ public final class BufWriterImpl implements BufWriter { void writeUtfEntry(String str) { int strlen = str.length(); int countNonZeroAscii = JLA.countNonZeroAscii(str); - int utflen = utfLen(str, countNonZeroAscii); - Util.checkU2(utflen, "utf8 length"); + long utflenLong = utfLen(str, countNonZeroAscii); + if (!ExactConversionsSupport.isLongToCharExact(utflenLong)) { + throw new IllegalArgumentException("utf8 length out of range of u2: " + utflenLong); + } + int utflen = (int)utflenLong; reserveSpace(utflen + 3); int offset = this.offset; diff --git a/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java b/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java index e8a4f27796f..46885e12adf 100644 --- a/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java +++ b/src/java.base/share/classes/jdk/internal/util/ModifiedUtf.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -63,12 +64,12 @@ public abstract class ModifiedUtf { * @param countNonZeroAscii the number of non-zero ascii characters in the prefix calculated by JLA.countNonZeroAscii(str) */ @ForceInline - public static int utfLen(String str, int countNonZeroAscii) { - int utflen = str.length(); - for (int i = utflen - 1; i >= countNonZeroAscii; i--) { + public static long utfLen(String str, int countNonZeroAscii) { + long utflen = str.length(); + for (int i = (int)utflen - 1; i >= countNonZeroAscii; i--) { int c = str.charAt(i); if (c >= 0x80 || c == 0) - utflen += (c >= 0x800) ? 2 : 1; + utflen += (c >= 0x800) ? 2L : 1L; } return utflen; } @@ -90,8 +91,7 @@ public abstract class ModifiedUtf { return false; } // Check exact Modified UTF-8 length. - // The check strLen > CONSTANT_POOL_UTF8_MAX_BYTES above ensures that utfLen can't overflow here. - int utfLen = utfLen(str, 0); + long utfLen = utfLen(str, 0); return utfLen <= CONSTANT_POOL_UTF8_MAX_BYTES; } } diff --git a/test/jdk/jdk/internal/util/ModifiedUtfTest.java b/test/jdk/jdk/internal/util/ModifiedUtfTest.java new file mode 100644 index 00000000000..7572acfba24 --- /dev/null +++ b/test/jdk/jdk/internal/util/ModifiedUtfTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8366421 + * @summary Test for ModifiedUtf.utfLen() return type change from int to long to avoid overflow + * @modules java.base/jdk.internal.classfile.impl:+open + * java.base/jdk.internal.util + * @run main/othervm -Xmx4g ModifiedUtfTest + */ + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.UTFDataFormatException; + +import java.lang.classfile.constantpool.ConstantPoolBuilder; +import java.lang.classfile.ClassFile; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import jdk.internal.classfile.impl.BufWriterImpl; +import jdk.internal.classfile.impl.ClassFileImpl; +import jdk.internal.util.ModifiedUtf; + +public class ModifiedUtfTest { + + static class HeaderCapturedException extends RuntimeException { + } + /** + * Keep only a fixed-length output and stop writing further data + * by throwing an exception when the limit is exceeded. + * For testing purposes only. + */ + static class HeaderCaptureOutputStream extends OutputStream { + private byte[] head; + private int count; + + public HeaderCaptureOutputStream(int headSize) { + this.head = new byte[headSize]; + } + + @Override + public void write(int b) { + if (count >= head.length) { + // Only reserve a fixed-length header and throw an exception to stop writing. + throw new HeaderCapturedException(); + } + head[count++] = (byte) b; + } + public byte[] get(){ + return head; + } + } + + private static final String THREE_BYTE = "\u2600"; // 3-byte UTF-8 + + public static void main(String[] args) throws Exception{ + int count = Integer.MAX_VALUE / 3 + 1; + long expected = 3L * count; + String largeString = THREE_BYTE.repeat(count); + + long total = ModifiedUtf.utfLen(largeString, 0); + if (total != expected) { + throw new RuntimeException("Expected total=" + expected + " but got " + total); + } + + /** + * Verifies that the following three methods that call ModifiedUtf.utfLen() + * correctly handle overflow: + * - DataOutputStream.writeUTF(String) + * - BufWriterImpl.writeUtfEntry(String) + * - ObjectOutputStream.writeUTF(String) + */ + try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(byteOut)) { + dataOut.writeUTF(largeString); + throw new RuntimeException("Expected UTFDataFormatException was not thrown."); + } catch (UTFDataFormatException e) { + } + + BufWriterImpl bufWriter = new BufWriterImpl(ConstantPoolBuilder.of(), (ClassFileImpl) ClassFile.of()); + Method writeUtfEntry = bufWriter.getClass().getDeclaredMethod("writeUtfEntry", String.class); + writeUtfEntry.setAccessible(true); + try { + writeUtfEntry.invoke(bufWriter, largeString); + throw new RuntimeException("Expected IllegalArgumentException was not thrown."); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (!(cause instanceof IllegalArgumentException)) { + throw new RuntimeException("Expected IllegalArgumentException was not thrown."); + } + } + + /** + * In the writeUTF function, utfLen is used to calculate the length of the string to be written + * and store it in the stream header. This test uses the HeaderCaptureOutputStream inner class + * to capture the header bytes and compare them with the expected length, + * verifying that utfLen returns the correct value. + */ + int lengthFieldSize = 8; + // Offset to UTF length field: 2 bytes STREAM_MAGIC + 2 bytes STREAM_VERSION + 5 bytes block data header + int lengthFieldOffset = 9; + int headerSize = 20; // greater than lengthFieldSize + lengthFieldOffset + HeaderCaptureOutputStream headerOut = new HeaderCaptureOutputStream(headerSize); + try (ObjectOutputStream objOut = new ObjectOutputStream(headerOut)) { + objOut.writeUTF(largeString); + } catch (HeaderCapturedException e) { + } + byte[] header = headerOut.get(); + ByteBuffer bf = ByteBuffer.wrap(header, lengthFieldOffset, lengthFieldSize); + bf.order(ByteOrder.BIG_ENDIAN); + long lenInHeader = bf.getLong(); + if ( lenInHeader != expected ) { + throw new RuntimeException("Header length mismatch: expected=" + expected + ", found=" + lenInHeader); + } + + System.out.println("PASSED"); + } +} \ No newline at end of file From faf6df5462d6c915434128a876e76fa48f7e3599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lund=C3=A9n?= Date: Wed, 24 Sep 2025 15:02:40 +0000 Subject: [PATCH 211/556] 8325467: Support methods with many arguments in C2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Roberto Castañeda Lozano Reviewed-by: rcastanedalo, kvn, epeter --- src/hotspot/share/adlc/formsopt.cpp | 22 +- src/hotspot/share/adlc/formsopt.hpp | 2 + src/hotspot/share/adlc/output_h.cpp | 2 + .../share/gc/shared/c2/barrierSetC2.cpp | 2 +- src/hotspot/share/memory/arena.hpp | 1 + src/hotspot/share/opto/chaitin.cpp | 122 +- src/hotspot/share/opto/chaitin.hpp | 9 +- src/hotspot/share/opto/coalesce.cpp | 17 +- src/hotspot/share/opto/compile.cpp | 4 + src/hotspot/share/opto/compile.hpp | 7 + src/hotspot/share/opto/gcm.cpp | 4 +- src/hotspot/share/opto/ifg.cpp | 23 +- src/hotspot/share/opto/lcm.cpp | 3 +- src/hotspot/share/opto/locknode.cpp | 18 +- src/hotspot/share/opto/locknode.hpp | 6 +- src/hotspot/share/opto/machnode.cpp | 2 +- src/hotspot/share/opto/machnode.hpp | 5 +- src/hotspot/share/opto/matcher.cpp | 90 +- src/hotspot/share/opto/node.cpp | 8 + src/hotspot/share/opto/optoreg.hpp | 50 +- src/hotspot/share/opto/postaloc.cpp | 17 +- src/hotspot/share/opto/reg_split.cpp | 5 +- src/hotspot/share/opto/regmask.cpp | 124 +- src/hotspot/share/opto/regmask.hpp | 771 ++++++++++-- .../share/utilities/globalDefinitions.hpp | 1 + test/hotspot/gtest/opto/test_regmask.cpp | 1099 ++++++++++++++++- .../arguments/TestMaxMethodArguments.java | 323 +++++ .../arguments/TestMethodArguments.java | 161 +++ .../compiler/locks/TestNestedSynchronize.java | 285 ++--- 29 files changed, 2608 insertions(+), 575 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/arguments/TestMaxMethodArguments.java create mode 100644 test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java diff --git a/src/hotspot/share/adlc/formsopt.cpp b/src/hotspot/share/adlc/formsopt.cpp index 92489da2f5a..fbd1043492e 100644 --- a/src/hotspot/share/adlc/formsopt.cpp +++ b/src/hotspot/share/adlc/formsopt.cpp @@ -162,22 +162,24 @@ bool RegisterForm::verify() { return valid; } +// Compute the least number of words required for registers in register masks. +int RegisterForm::words_for_regs() { + return (_reg_ctr + 31) >> 5; +} + // Compute RegMask size int RegisterForm::RegMask_Size() { - // Need at least this many words - int words_for_regs = (_reg_ctr + 31)>>5; - // The array of Register Mask bits should be large enough to cover - // all the machine registers and all parameters that need to be passed - // on the stack (stack registers) up to some interesting limit. Methods - // that need more parameters will NOT be compiled. On Intel, the limit - // is something like 90+ parameters. + // The array of Register Mask bits should be large enough to cover all the + // machine registers, as well as a certain number of parameters that need to + // be passed on the stack (stack registers). The number of parameters that can + // fit in the mask should be dimensioned to cover most common cases. // - Add a few (3 words == 96 bits) for incoming & outgoing arguments to // calls. // - Round up to the next doubleword size. // - Add one more word to accommodate a reasonable number of stack locations // in the register mask regardless of how much slack is created by rounding. // This was found necessary after adding 16 new registers for APX. - return (words_for_regs + 3 + 1 + 1) & ~1; + return (words_for_regs() + 3 + 1 + 1) & ~1; } void RegisterForm::dump() { // Debug printer @@ -369,14 +371,14 @@ void RegClass::build_register_masks(FILE* fp) { for(i = 0; i < len - 1; i++) { fprintf(fp," 0x%x,", regs_in_word(i, false)); } - fprintf(fp," 0x%x );\n", regs_in_word(i, false)); + fprintf(fp, " 0x%x, false );\n", regs_in_word(i, false)); if (_stack_or_reg) { fprintf(fp, "const RegMask _%sSTACK_OR_%s_mask(", prefix, rc_name_to_upper); for(i = 0; i < len - 1; i++) { fprintf(fp," 0x%x,", regs_in_word(i, true)); } - fprintf(fp," 0x%x );\n", regs_in_word(i, true)); + fprintf(fp, " 0x%x, true );\n", regs_in_word(i, true)); } delete[] rc_name_to_upper; } diff --git a/src/hotspot/share/adlc/formsopt.hpp b/src/hotspot/share/adlc/formsopt.hpp index 34cbc24bed0..9e0c9db854d 100644 --- a/src/hotspot/share/adlc/formsopt.hpp +++ b/src/hotspot/share/adlc/formsopt.hpp @@ -93,6 +93,8 @@ public: Dict _allocClass; // Dictionary of allocation classes static int _reg_ctr; // Register counter + static int words_for_regs(); // Compute the least number of words required for + // registers in register masks. static int RegMask_Size(); // Compute RegMask size // Public Methods diff --git a/src/hotspot/share/adlc/output_h.cpp b/src/hotspot/share/adlc/output_h.cpp index 0ef8b19c79f..8a5ad72137a 100644 --- a/src/hotspot/share/adlc/output_h.cpp +++ b/src/hotspot/share/adlc/output_h.cpp @@ -99,6 +99,8 @@ void ArchDesc::buildMachRegisterNumbers(FILE *fp_hpp) { fprintf(fp_hpp, "\n// Size of register-mask in ints\n"); fprintf(fp_hpp, "#define RM_SIZE_IN_INTS %d\n", RegisterForm::RegMask_Size()); + fprintf(fp_hpp, "// Minimum size of register-mask in ints\n"); + fprintf(fp_hpp, "#define RM_SIZE_IN_INTS_MIN %d\n", RegisterForm::words_for_regs()); fprintf(fp_hpp, "// Unroll factor for loops over the data in a RegMask\n"); fprintf(fp_hpp, "#define FORALL_BODY "); int len = RegisterForm::RegMask_Size(); diff --git a/src/hotspot/share/gc/shared/c2/barrierSetC2.cpp b/src/hotspot/share/gc/shared/c2/barrierSetC2.cpp index f0ed9687f11..e12e7b56e23 100644 --- a/src/hotspot/share/gc/shared/c2/barrierSetC2.cpp +++ b/src/hotspot/share/gc/shared/c2/barrierSetC2.cpp @@ -1220,7 +1220,7 @@ void BarrierSetC2::compute_liveness_at_stubs() const { // Now at block top, see if we have any changes new_live.SUBTRACT(old_live); - if (new_live.is_NotEmpty()) { + if (!new_live.is_Empty()) { // Liveness has refined, update and propagate to prior blocks old_live.OR(new_live); for (uint i = 1; i < block->num_preds(); ++i) { diff --git a/src/hotspot/share/memory/arena.hpp b/src/hotspot/share/memory/arena.hpp index 7a17873de93..e2169ee406e 100644 --- a/src/hotspot/share/memory/arena.hpp +++ b/src/hotspot/share/memory/arena.hpp @@ -104,6 +104,7 @@ public: FN(states, C2 Matcher States Arena) \ FN(reglive, C2 Register Allocation Live Arenas) \ FN(regsplit, C2 Register Allocation Split Arena) \ + FN(regmask, C2 Short-Lived Register Mask Arena) \ FN(superword, C2 SuperWord Arenas) \ FN(cienv, CI Env Arena) \ FN(ha, Handle area) \ diff --git a/src/hotspot/share/opto/chaitin.cpp b/src/hotspot/share/opto/chaitin.cpp index d650df45a8d..45a91350626 100644 --- a/src/hotspot/share/opto/chaitin.cpp +++ b/src/hotspot/share/opto/chaitin.cpp @@ -560,6 +560,9 @@ void PhaseChaitin::Register_Allocate() { // Select colors by re-inserting LRGs back into the IFG in reverse order. // Return whether or not something spills. uint spills = Select( ); + if (C->failing()) { + return; + } // If we spill, split and recycle the entire thing while( spills ) { @@ -637,6 +640,9 @@ void PhaseChaitin::Register_Allocate() { // Select colors by re-inserting LRGs back into the IFG in reverse order. // Return whether or not something spills. spills = Select(); + if (C->failing()) { + return; + } } C->print_method(PHASE_AFTER_ITERATIVE_SPILLING, 4); @@ -767,7 +773,7 @@ void PhaseChaitin::de_ssa() { Node *n = block->get_node(j); // Pre-color to the zero live range, or pick virtual register const RegMask &rm = n->out_RegMask(); - _lrg_map.map(n->_idx, rm.is_NotEmpty() ? lr_counter++ : 0); + _lrg_map.map(n->_idx, !rm.is_Empty() ? lr_counter++ : 0); } } @@ -788,7 +794,7 @@ void PhaseChaitin::mark_ssa() { Node *n = block->get_node(j); // Pre-color to the zero live range, or pick virtual register const RegMask &rm = n->out_RegMask(); - _lrg_map.map(n->_idx, rm.is_NotEmpty() ? n->_idx : 0); + _lrg_map.map(n->_idx, !rm.is_Empty() ? n->_idx : 0); max_idx = (n->_idx > max_idx) ? n->_idx : max_idx; } } @@ -1398,9 +1404,8 @@ void PhaseChaitin::Simplify( ) { } // Is 'reg' register legal for 'lrg'? -static bool is_legal_reg(LRG &lrg, OptoReg::Name reg, int chunk) { - if (reg >= chunk && reg < (chunk + RegMask::CHUNK_SIZE) && - lrg.mask().Member(OptoReg::add(reg,-chunk))) { +static bool is_legal_reg(LRG& lrg, OptoReg::Name reg) { + if (lrg.mask().can_represent(reg) && lrg.mask().Member(reg)) { // RA uses OptoReg which represent the highest element of a registers set. // For example, vectorX (128bit) on x86 uses [XMM,XMMb,XMMc,XMMd] set // in which XMMd is used by RA to represent such vectors. A double value @@ -1423,13 +1428,15 @@ static bool is_legal_reg(LRG &lrg, OptoReg::Name reg, int chunk) { return false; } -static OptoReg::Name find_first_set(LRG &lrg, RegMask mask, int chunk) { +static OptoReg::Name find_first_set(LRG& lrg, RegMask& mask) { int num_regs = lrg.num_regs(); OptoReg::Name assigned = mask.find_first_set(lrg, num_regs); if (lrg.is_scalable()) { // a physical register is found - if (chunk == 0 && OptoReg::is_reg(assigned)) { + if (OptoReg::is_reg(assigned)) { + assert(!lrg.mask().is_offset(), + "offset register masks can only contain stack slots"); return assigned; } @@ -1445,7 +1452,8 @@ static OptoReg::Name find_first_set(LRG &lrg, RegMask mask, int chunk) { // does not work for scalable size. We have to find adjacent scalable_reg_slots() bits // instead of SlotsPerVecA bits. assigned = mask.find_first_set(lrg, num_regs); // find highest valid reg - while (OptoReg::is_valid(assigned) && RegMask::can_represent(assigned)) { + while (OptoReg::is_valid(assigned)) { + assert(mask.can_represent(assigned), "sanity"); // Verify the found reg has scalable_reg_slots() bits set. if (mask.is_valid_reg(assigned, num_regs)) { return assigned; @@ -1469,7 +1477,7 @@ static OptoReg::Name find_first_set(LRG &lrg, RegMask mask, int chunk) { } // Choose a color using the biasing heuristic -OptoReg::Name PhaseChaitin::bias_color( LRG &lrg, int chunk ) { +OptoReg::Name PhaseChaitin::bias_color(LRG& lrg) { // Check for "at_risk" LRG's uint risk_lrg = _lrg_map.find(lrg._risk_bias); @@ -1483,8 +1491,9 @@ OptoReg::Name PhaseChaitin::bias_color( LRG &lrg, int chunk ) { while ((datum = elements.next()) != 0) { OptoReg::Name reg = lrgs(datum).reg(); // If this LRG's register is legal for us, choose it - if (is_legal_reg(lrg, reg, chunk)) + if (is_legal_reg(lrg, reg)) { return reg; + } } } @@ -1494,14 +1503,16 @@ OptoReg::Name PhaseChaitin::bias_color( LRG &lrg, int chunk ) { if(!_ifg->_yanked->test(copy_lrg)) { OptoReg::Name reg = lrgs(copy_lrg).reg(); // And it is legal for you, - if (is_legal_reg(lrg, reg, chunk)) + if (is_legal_reg(lrg, reg)) { return reg; - } else if( chunk == 0 ) { + } + } else if (!lrg.mask().is_offset()) { // Choose a color which is legal for him - RegMask tempmask = lrg.mask(); + ResourceMark rm(C->regmask_arena()); + RegMask tempmask(lrg.mask(), C->regmask_arena()); tempmask.AND(lrgs(copy_lrg).mask()); tempmask.clear_to_sets(lrg.num_regs()); - OptoReg::Name reg = find_first_set(lrg, tempmask, chunk); + OptoReg::Name reg = find_first_set(lrg, tempmask); if (OptoReg::is_valid(reg)) return reg; } @@ -1510,7 +1521,9 @@ OptoReg::Name PhaseChaitin::bias_color( LRG &lrg, int chunk ) { // If no bias info exists, just go with the register selection ordering if (lrg._is_vector || lrg.num_regs() == 2 || lrg.is_scalable()) { // Find an aligned set - return OptoReg::add(find_first_set(lrg, lrg.mask(), chunk), chunk); + ResourceMark rm(C->regmask_arena()); + RegMask tempmask(lrg.mask(), C->regmask_arena()); + return find_first_set(lrg, tempmask); } // CNC - Fun hack. Alternate 1st and 2nd selection. Enables post-allocate @@ -1523,21 +1536,22 @@ OptoReg::Name PhaseChaitin::bias_color( LRG &lrg, int chunk ) { lrg.Remove(reg); OptoReg::Name reg2 = lrg.mask().find_first_elem(); lrg.Insert(reg); - if( OptoReg::is_reg(reg2)) + if (OptoReg::is_reg(reg2)) { reg = reg2; + } } - return OptoReg::add( reg, chunk ); + return reg; } // Choose a color in the current chunk -OptoReg::Name PhaseChaitin::choose_color( LRG &lrg, int chunk ) { - assert( C->in_preserve_stack_slots() == 0 || chunk != 0 || lrg._is_bound || lrg.mask().is_bound1() || !lrg.mask().Member(OptoReg::Name(_matcher._old_SP-1)), "must not allocate stack0 (inside preserve area)"); - assert(C->out_preserve_stack_slots() == 0 || chunk != 0 || lrg._is_bound || lrg.mask().is_bound1() || !lrg.mask().Member(OptoReg::Name(_matcher._old_SP+0)), "must not allocate stack0 (inside preserve area)"); +OptoReg::Name PhaseChaitin::choose_color(LRG& lrg) { + assert(C->in_preserve_stack_slots() == 0 || lrg.mask().is_offset() || lrg._is_bound || lrg.mask().is_bound1() || !lrg.mask().Member(OptoReg::Name(_matcher._old_SP - 1)), "must not allocate stack0 (inside preserve area)"); + assert(C->out_preserve_stack_slots() == 0 || lrg.mask().is_offset() || lrg._is_bound || lrg.mask().is_bound1() || !lrg.mask().Member(OptoReg::Name(_matcher._old_SP + 0)), "must not allocate stack0 (inside preserve area)"); if( lrg.num_regs() == 1 || // Common Case !lrg._fat_proj ) // Aligned+adjacent pairs ok // Use a heuristic to "bias" the color choice - return bias_color(lrg, chunk); + return bias_color(lrg); assert(!lrg._is_vector, "should be not vector here" ); assert( lrg.num_regs() >= 2, "dead live ranges do not color" ); @@ -1545,7 +1559,7 @@ OptoReg::Name PhaseChaitin::choose_color( LRG &lrg, int chunk ) { // Fat-proj case or misaligned double argument. assert(lrg.compute_mask_size() == lrg.num_regs() || lrg.num_regs() == 2,"fat projs exactly color" ); - assert( !chunk, "always color in 1st chunk" ); + assert(!lrg.mask().is_offset(), "always color in 1st chunk"); // Return the highest element in the set. return lrg.mask().find_last_elem(); } @@ -1585,42 +1599,44 @@ uint PhaseChaitin::Select( ) { // be much clearer. We arrive here IFF we have a stack-based // live range that cannot color in the current chunk, and it // has to move into the next free stack chunk. - int chunk = 0; // Current chunk is first chunk retry_next_chunk: // Remove neighbor colors IndexSet *s = _ifg->neighbors(lidx); - DEBUG_ONLY(RegMask orig_mask = lrg->mask();) +#ifndef PRODUCT + ResourceMark rm(C->regmask_arena()); + RegMask orig_mask(lrg->mask(), C->regmask_arena()); +#endif if (!s->is_empty()) { IndexSetIterator elements(s); uint neighbor; while ((neighbor = elements.next()) != 0) { - // Note that neighbor might be a spill_reg. In this case, exclusion - // of its color will be a no-op, since the spill_reg chunk is in outer - // space. Also, if neighbor is in a different chunk, this exclusion - // will be a no-op. (Later on, if lrg runs out of possible colors in - // its chunk, a new chunk of color may be tried, in which case - // examination of neighbors is started again, at retry_next_chunk.) LRG &nlrg = lrgs(neighbor); OptoReg::Name nreg = nlrg.reg(); - // Only subtract masks in the same chunk - if (nreg >= chunk && nreg < chunk + RegMask::CHUNK_SIZE) { + // The neighbor might be a spill_reg. In this case, exclusion of its + // color will be a no-op, since the spill_reg is in outer space. In + // this case, do not exclude the corresponding mask. Later on, if lrg + // runs out of possible colors in its chunk, a new chunk of color may + // be tried, in which case examination of neighbors is started again, + // at retry_next_chunk. + if (nreg < LRG::SPILL_REG) { #ifndef PRODUCT uint size = lrg->mask().Size(); - RegMask rm = lrg->mask(); + ResourceMark rm(C->regmask_arena()); + RegMask trace_mask(lrg->mask(), C->regmask_arena()); #endif - lrg->SUBTRACT(nlrg.mask()); + lrg->SUBTRACT_inner(nlrg.mask()); #ifndef PRODUCT if (trace_spilling() && lrg->mask().Size() != size) { ttyLocker ttyl; tty->print("L%d ", lidx); - rm.dump(); + trace_mask.dump(); tty->print(" intersected L%d ", neighbor); nlrg.mask().dump(); tty->print(" removed "); - rm.SUBTRACT(lrg->mask()); - rm.dump(); + trace_mask.SUBTRACT(lrg->mask()); + trace_mask.dump(); tty->print(" leaving "); lrg->mask().dump(); tty->cr(); @@ -1637,15 +1653,25 @@ uint PhaseChaitin::Select( ) { } // Check if a color is available and if so pick the color - OptoReg::Name reg = choose_color( *lrg, chunk ); + OptoReg::Name reg = choose_color(*lrg); //--------------- - // If we fail to color and the infinite flag is set, trigger - // a chunk-rollover event - if (!OptoReg::is_valid(OptoReg::add(reg, -chunk)) && is_infinite_stack) { + // If we fail to color and the infinite flag is set, we must trigger + // a chunk-rollover event and continue searching for a color in the next set + // of slots (which are all necessarily stack slots, as registers are only in + // the initial chunk) + if (!OptoReg::is_valid(reg) && is_infinite_stack) { // Bump register mask up to next stack chunk - chunk += RegMask::CHUNK_SIZE; - lrg->Set_All(); + bool success = lrg->rollover(); + if (!success) { + // We should never get here in practice. Bail out in product, + // assert in debug. + assert(false, "the next available stack slots should be within the " + "OptoRegPair range"); + C->record_method_not_compilable( + "chunk-rollover outside of OptoRegPair range"); + return -1; + } goto retry_next_chunk; } @@ -1653,16 +1679,16 @@ uint PhaseChaitin::Select( ) { // Did we get a color? else if (OptoReg::is_valid(reg)) { #ifndef PRODUCT - RegMask avail_rm = lrg->mask(); + ResourceMark rm(C->regmask_arena()); + RegMask avail_rm(lrg->mask(), C->regmask_arena()); #endif // Record selected register lrg->set_reg(reg); - if( reg >= _max_reg ) // Compute max register limit - _max_reg = OptoReg::add(reg,1); - // Fold reg back into normal space - reg = OptoReg::add(reg,-chunk); + if (reg >= _max_reg) { // Compute max register limit + _max_reg = OptoReg::add(reg, 1); + } // If the live range is not bound, then we actually had some choices // to make. In this case, the mask has more bits in it than the colors diff --git a/src/hotspot/share/opto/chaitin.hpp b/src/hotspot/share/opto/chaitin.hpp index 8cef21c6cc2..9b3f8123ac2 100644 --- a/src/hotspot/share/opto/chaitin.hpp +++ b/src/hotspot/share/opto/chaitin.hpp @@ -129,10 +129,13 @@ public: int get_invalid_mask_size() const { return _mask_size; } const RegMask &mask() const { return _mask; } void set_mask( const RegMask &rm ) { _mask = rm; DEBUG_ONLY(_msize_valid=0;)} + void init_mask(Arena* arena) { new (&_mask) RegMask(arena); } void AND( const RegMask &rm ) { _mask.AND(rm); DEBUG_ONLY(_msize_valid=0;)} void SUBTRACT( const RegMask &rm ) { _mask.SUBTRACT(rm); DEBUG_ONLY(_msize_valid=0;)} + void SUBTRACT_inner(const RegMask& rm) { _mask.SUBTRACT_inner(rm); DEBUG_ONLY(_msize_valid = 0;) } void Clear() { _mask.Clear() ; DEBUG_ONLY(_msize_valid=1); _mask_size = 0; } - void Set_All() { _mask.Set_All(); DEBUG_ONLY(_msize_valid=1); _mask_size = RegMask::CHUNK_SIZE; } + void Set_All() { _mask.Set_All(); DEBUG_ONLY(_msize_valid = 1); _mask_size = _mask.rm_size_in_bits(); } + bool rollover() { DEBUG_ONLY(_msize_valid = 1); _mask_size = _mask.rm_size_in_bits(); return _mask.rollover(); } void Insert( OptoReg::Name reg ) { _mask.Insert(reg); DEBUG_ONLY(_msize_valid=0;) } void Remove( OptoReg::Name reg ) { _mask.Remove(reg); DEBUG_ONLY(_msize_valid=0;) } @@ -697,9 +700,9 @@ private: // Return TRUE if any spills occurred. uint Select( ); // Helper function for select which allows biased coloring - OptoReg::Name choose_color( LRG &lrg, int chunk ); + OptoReg::Name choose_color(LRG& lrg); // Helper function which implements biasing heuristic - OptoReg::Name bias_color( LRG &lrg, int chunk ); + OptoReg::Name bias_color(LRG& lrg); // Split uncolorable live ranges // Return new number of live ranges diff --git a/src/hotspot/share/opto/coalesce.cpp b/src/hotspot/share/opto/coalesce.cpp index 588adbfcf42..90a2dd0e152 100644 --- a/src/hotspot/share/opto/coalesce.cpp +++ b/src/hotspot/share/opto/coalesce.cpp @@ -693,12 +693,13 @@ bool PhaseConservativeCoalesce::copy_copy(Node *dst_copy, Node *src_copy, Block // Check for compatibility of the 2 live ranges by // intersecting their allowed register sets. - RegMask rm = lrgs(lr1).mask(); - rm.AND(lrgs(lr2).mask()); + ResourceMark rm(C->regmask_arena()); + RegMask mask(lrgs(lr1).mask(), C->regmask_arena()); + mask.AND(lrgs(lr2).mask()); // Number of bits free - uint rm_size = rm.Size(); + uint rm_size = mask.Size(); - if (UseFPUForSpilling && rm.is_infinite_stack()) { + if (UseFPUForSpilling && mask.is_infinite_stack() ) { // Don't coalesce when frequency difference is large Block *dst_b = _phc._cfg.get_block_for_node(dst_copy); Block *src_def_b = _phc._cfg.get_block_for_node(src_def); @@ -707,7 +708,7 @@ bool PhaseConservativeCoalesce::copy_copy(Node *dst_copy, Node *src_copy, Block } // If we can use any stack slot, then effective size is infinite - if (rm.is_infinite_stack()) { + if (mask.is_infinite_stack()) { rm_size += 1000000; } // Incompatible masks, no way to coalesce @@ -732,7 +733,7 @@ bool PhaseConservativeCoalesce::copy_copy(Node *dst_copy, Node *src_copy, Block } // Union the two interference sets together into '_ulr' - uint reg_degree = _ulr.lrg_union( lr1, lr2, rm_size, _phc._ifg, rm ); + uint reg_degree = _ulr.lrg_union( lr1, lr2, rm_size, _phc._ifg, mask ); if( reg_degree >= rm_size ) { record_bias( _phc._ifg, lr1, lr2 ); @@ -745,7 +746,7 @@ bool PhaseConservativeCoalesce::copy_copy(Node *dst_copy, Node *src_copy, Block // line of previous blocks. I give up at merge points or when I get // more interferences than my degree. I can stop when I find src_copy. if( dst_copy != src_copy ) { - reg_degree = compute_separating_interferences(dst_copy, src_copy, b, bindex, rm, rm_size, reg_degree, lr1, lr2 ); + reg_degree = compute_separating_interferences(dst_copy, src_copy, b, bindex, mask, rm_size, reg_degree, lr1, lr2 ); if( reg_degree == max_juint ) { record_bias( _phc._ifg, lr1, lr2 ); return false; @@ -792,7 +793,7 @@ bool PhaseConservativeCoalesce::copy_copy(Node *dst_copy, Node *src_copy, Block // union-find tree union_helper( lr1_node, lr2_node, lr1, lr2, src_def, dst_copy, src_copy, b, bindex ); // Combine register restrictions - lrgs(lr1).set_mask(rm); + lrgs(lr1).set_mask(mask); lrgs(lr1).compute_set_mask_size(); lrgs(lr1)._cost += lrgs(lr2)._cost; lrgs(lr1)._area += lrgs(lr2)._area; diff --git a/src/hotspot/share/opto/compile.cpp b/src/hotspot/share/opto/compile.cpp index cef7aa61219..f828dfb6928 100644 --- a/src/hotspot/share/opto/compile.cpp +++ b/src/hotspot/share/opto/compile.cpp @@ -693,7 +693,9 @@ Compile::Compile(ciEnv* ci_env, ciMethod* target, int osr_bci, _inline_printer(this), _java_calls(0), _inner_loops(0), + _FIRST_STACK_mask(comp_arena()), _interpreter_frame_size(0), + _regmask_arena(mtCompiler, Arena::Tag::tag_regmask), _output(nullptr) #ifndef PRODUCT , @@ -954,7 +956,9 @@ Compile::Compile(ciEnv* ci_env, _inline_printer(this), _java_calls(0), _inner_loops(0), + _FIRST_STACK_mask(comp_arena()), _interpreter_frame_size(0), + _regmask_arena(mtCompiler, Arena::Tag::tag_regmask), _output(nullptr), #ifndef PRODUCT _in_dump_cnt(0), diff --git a/src/hotspot/share/opto/compile.hpp b/src/hotspot/share/opto/compile.hpp index 2cada9c04c9..1cfcdca9610 100644 --- a/src/hotspot/share/opto/compile.hpp +++ b/src/hotspot/share/opto/compile.hpp @@ -526,6 +526,12 @@ public: void* _indexSet_free_block_list; // free list of IndexSet bit blocks int _interpreter_frame_size; + // Holds dynamically allocated extensions of short-lived register masks. Such + // extensions are potentially quite large and need tight resource marks which + // may conflict with other allocations in the default resource area. + // Therefore, we use a dedicated resource area for register masks. + ResourceArea _regmask_arena; + PhaseOutput* _output; public: @@ -1113,6 +1119,7 @@ public: Matcher* matcher() { return _matcher; } PhaseRegAlloc* regalloc() { return _regalloc; } RegMask& FIRST_STACK_mask() { return _FIRST_STACK_mask; } + ResourceArea* regmask_arena() { return &_regmask_arena; } Arena* indexSet_arena() { return _indexSet_arena; } void* indexSet_free_block_list() { return _indexSet_free_block_list; } DebugInformationRecorder* debug_info() { return env()->debug_info(); } diff --git a/src/hotspot/share/opto/gcm.cpp b/src/hotspot/share/opto/gcm.cpp index 8859263d8b6..72c001a64c4 100644 --- a/src/hotspot/share/opto/gcm.cpp +++ b/src/hotspot/share/opto/gcm.cpp @@ -1449,7 +1449,7 @@ Block* PhaseCFG::hoist_to_cheaper_block(Block* LCA, Block* early, Node* self) { // single register. Hoisting stretches the live range of the // single register and may force spilling. MachNode* mach = self->is_Mach() ? self->as_Mach() : nullptr; - if (mach && mach->out_RegMask().is_bound1() && mach->out_RegMask().is_NotEmpty()) + if (mach != nullptr && mach->out_RegMask().is_bound1() && !mach->out_RegMask().is_Empty()) in_latency = true; #ifndef PRODUCT @@ -1482,7 +1482,7 @@ Block* PhaseCFG::hoist_to_cheaper_block(Block* LCA, Block* early, Node* self) { } // Don't hoist machine instructions to the root basic block - if (mach && LCA == root_block) + if (mach != nullptr && LCA == root_block) break; if (self->is_memory_writer() && diff --git a/src/hotspot/share/opto/ifg.cpp b/src/hotspot/share/opto/ifg.cpp index a6bcb640c14..438209df8f8 100644 --- a/src/hotspot/share/opto/ifg.cpp +++ b/src/hotspot/share/opto/ifg.cpp @@ -54,6 +54,7 @@ void PhaseIFG::init( uint maxlrg ) { // Init all to empty for( uint i = 0; i < maxlrg; i++ ) { _adjs[i].initialize(maxlrg); + _lrgs[i].init_mask(_arena); _lrgs[i].Set_All(); } } @@ -652,7 +653,8 @@ bool PhaseChaitin::remove_node_if_not_used(Block* b, uint location, Node* n, uin * block. If we find a low to high transition, we record it. */ void PhaseChaitin::check_for_high_pressure_transition_at_fatproj(uint& block_reg_pressure, uint location, LRG& lrg, Pressure& pressure, const int op_regtype) { - RegMask mask_tmp = lrg.mask(); + ResourceMark rm(C->regmask_arena()); + RegMask mask_tmp(lrg.mask(), C->regmask_arena()); mask_tmp.AND(*Matcher::idealreg2regmask[op_regtype]); pressure.check_pressure_at_fatproj(location, mask_tmp); } @@ -709,7 +711,10 @@ void PhaseChaitin::remove_interference_from_copy(Block* b, uint location, uint l void PhaseChaitin::remove_bound_register_from_interfering_live_ranges(LRG& lrg, IndexSet* liveout, uint& must_spill) { if (liveout->is_empty()) return; // Check for common case - const RegMask& rm = lrg.mask(); + const RegMask& mask = lrg.mask(); + ResourceMark rm(C->regmask_arena()); + RegMask old(C->regmask_arena()); + RegMask r2mask(C->regmask_arena()); int r_size = lrg.num_regs(); // Smear odd bits IndexSetIterator elements(liveout); @@ -724,16 +729,16 @@ void PhaseChaitin::remove_bound_register_from_interfering_live_ranges(LRG& lrg, } // Remove bound register(s) from 'l's choices - RegMask old = interfering_lrg.mask(); + old = interfering_lrg.mask(); uint old_size = interfering_lrg.mask_size(); - // Remove the bits from LRG 'rm' from LRG 'l' so 'l' no - // longer interferes with 'rm'. If 'l' requires aligned + // Remove the bits from LRG 'mask' from LRG 'l' so 'l' no + // longer interferes with 'mask'. If 'l' requires aligned // adjacent pairs, subtract out bit pairs. assert(!interfering_lrg._is_vector || !interfering_lrg._fat_proj, "sanity"); if (interfering_lrg.num_regs() > 1 && !interfering_lrg._fat_proj) { - RegMask r2mask = rm; + r2mask = mask; // Leave only aligned set of bits. r2mask.smear_to_sets(interfering_lrg.num_regs()); // It includes vector case. @@ -741,11 +746,11 @@ void PhaseChaitin::remove_bound_register_from_interfering_live_ranges(LRG& lrg, interfering_lrg.compute_set_mask_size(); } else if (r_size != 1) { // fat proj - interfering_lrg.SUBTRACT(rm); + interfering_lrg.SUBTRACT(mask); interfering_lrg.compute_set_mask_size(); } else { // Common case: size 1 bound removal - OptoReg::Name r_reg = rm.find_first_elem(); + OptoReg::Name r_reg = mask.find_first_elem(); if (interfering_lrg.mask().Member(r_reg)) { interfering_lrg.Remove(r_reg); interfering_lrg.set_mask_size(interfering_lrg.mask().is_infinite_stack() ? LRG::INFINITE_STACK_SIZE : old_size - 1); @@ -928,7 +933,7 @@ uint PhaseChaitin::build_ifg_physical( ResourceArea *a ) { // Since rematerializable DEFs are not bound but the live range is, // some uses must be bound. If we spill live range 'r', it can // rematerialize at each use site according to its bindings. - if (lrg.is_bound() && !n->rematerialize() && lrg.mask().is_NotEmpty()) { + if (lrg.is_bound() && !n->rematerialize() && !lrg.mask().is_Empty()) { remove_bound_register_from_interfering_live_ranges(lrg, &liveout, must_spill); } interfere_with_live(lid, &liveout); diff --git a/src/hotspot/share/opto/lcm.cpp b/src/hotspot/share/opto/lcm.cpp index 66ef4b23e2d..ca1863b685e 100644 --- a/src/hotspot/share/opto/lcm.cpp +++ b/src/hotspot/share/opto/lcm.cpp @@ -869,7 +869,8 @@ static void add_call_kills(MachProjNode *proj, RegMask& regs, const char* save_p //------------------------------sched_call------------------------------------- uint PhaseCFG::sched_call(Block* block, uint node_cnt, Node_List& worklist, GrowableArray& ready_cnt, MachCallNode* mcall, VectorSet& next_call) { - RegMask regs; + ResourceMark rm(C->regmask_arena()); + RegMask regs(C->regmask_arena()); // Schedule all the users of the call right now. All the users are // projection Nodes, so they must be scheduled next to the call. diff --git a/src/hotspot/share/opto/locknode.cpp b/src/hotspot/share/opto/locknode.cpp index 4587bfd4fd6..ff8900289d4 100644 --- a/src/hotspot/share/opto/locknode.cpp +++ b/src/hotspot/share/opto/locknode.cpp @@ -24,6 +24,7 @@ #include "opto/locknode.hpp" #include "opto/parse.hpp" +#include "opto/regmask.hpp" #include "opto/rootnode.hpp" #include "opto/runtime.hpp" @@ -38,16 +39,21 @@ const RegMask &BoxLockNode::out_RegMask() const { uint BoxLockNode::size_of() const { return sizeof(*this); } -BoxLockNode::BoxLockNode( int slot ) : Node( Compile::current()->root() ), - _slot(slot), _kind(BoxLockNode::Regular) { +BoxLockNode::BoxLockNode(int slot) + : Node(Compile::current()->root()), + _slot(slot), + // In debug mode, signal that the register mask is constant. + _inmask(OptoReg::stack2reg(_slot), + Compile::current()->comp_arena() + DEBUG_ONLY(COMMA /*read_only*/ true)), + _kind(BoxLockNode::Regular) { init_class_id(Class_BoxLock); init_flags(Flag_rematerialize); - OptoReg::Name reg = OptoReg::stack2reg(_slot); - if (!RegMask::can_represent(reg, Compile::current()->sync_stack_slots())) { - Compile::current()->record_method_not_compilable("must be able to represent all monitor slots in reg mask"); + if (_slot > BoxLockNode_SLOT_LIMIT) { + Compile::current()->record_method_not_compilable( + "reached BoxLockNode slot limit"); return; } - _inmask.Insert(reg); } uint BoxLockNode::hash() const { diff --git a/src/hotspot/share/opto/locknode.hpp b/src/hotspot/share/opto/locknode.hpp index 229dcb73292..ee54e893e59 100644 --- a/src/hotspot/share/opto/locknode.hpp +++ b/src/hotspot/share/opto/locknode.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,8 +32,8 @@ //------------------------------BoxLockNode------------------------------------ class BoxLockNode : public Node { private: - const int _slot; // stack slot - RegMask _inmask; // OptoReg corresponding to stack slot + const int _slot; // stack slot + const RegMask _inmask; // OptoReg corresponding to stack slot enum { Regular = 0, // Normal locking region Local, // EA found that local not escaping object is used for locking diff --git a/src/hotspot/share/opto/machnode.cpp b/src/hotspot/share/opto/machnode.cpp index 2ae2c5de381..e5acad98c09 100644 --- a/src/hotspot/share/opto/machnode.cpp +++ b/src/hotspot/share/opto/machnode.cpp @@ -525,7 +525,7 @@ bool MachNode::rematerialize() const { uint idx = oper_input_base(); if (req() > idx) { const RegMask &rm = in_RegMask(idx); - if (rm.is_NotEmpty() && rm.is_bound(ideal_reg())) { + if (!rm.is_Empty() && rm.is_bound(ideal_reg())) { return false; } } diff --git a/src/hotspot/share/opto/machnode.hpp b/src/hotspot/share/opto/machnode.hpp index 3594806b91e..5490f717a25 100644 --- a/src/hotspot/share/opto/machnode.hpp +++ b/src/hotspot/share/opto/machnode.hpp @@ -754,7 +754,10 @@ public: // occasional callbacks to the machine model for important info. class MachProjNode : public ProjNode { public: - MachProjNode( Node *multi, uint con, const RegMask &out, uint ideal_reg ) : ProjNode(multi,con), _rout(out), _ideal_reg(ideal_reg) { + MachProjNode(Node* multi, uint con, const RegMask& out, uint ideal_reg) + : ProjNode(multi, con), + _rout(out, Compile::current()->comp_arena()), + _ideal_reg(ideal_reg) { init_class_id(Class_MachProj); } RegMask _rout; diff --git a/src/hotspot/share/opto/matcher.cpp b/src/hotspot/share/opto/matcher.cpp index 2608ba0af0b..b43bc05c465 100644 --- a/src/hotspot/share/opto/matcher.cpp +++ b/src/hotspot/share/opto/matcher.cpp @@ -80,7 +80,8 @@ Matcher::Matcher() _ruleName(ruleName), _register_save_policy(register_save_policy), _c_reg_save_policy(c_reg_save_policy), - _register_save_type(register_save_type) { + _register_save_type(register_save_type), + _return_addr_mask(C->comp_arena()) { C->set_matcher(this); idealreg2spillmask [Op_RegI] = nullptr; @@ -140,12 +141,6 @@ OptoReg::Name Matcher::warp_incoming_stk_arg( VMReg reg ) { warped = OptoReg::add(warped, C->out_preserve_stack_slots()); if( warped >= _in_arg_limit ) _in_arg_limit = OptoReg::add(warped, 1); // Bump max stack slot seen - if (!RegMask::can_represent_arg(warped)) { - // the compiler cannot represent this method's calling sequence - // Bailout. We do not have space to represent all arguments. - C->record_method_not_compilable("unsupported incoming calling sequence"); - return OptoReg::Bad; - } return warped; } return OptoReg::as_OptoReg(reg); @@ -198,7 +193,9 @@ void Matcher::match( ) { if (C->failing()) { return; } - _return_addr_mask = return_addr(); + assert(_return_addr_mask.is_Empty(), + "return address mask must be empty initially"); + _return_addr_mask.Insert(return_addr()); #ifdef _LP64 // Pointers take 2 slots in 64-bit land _return_addr_mask.Insert(OptoReg::add(return_addr(),1)); @@ -235,6 +232,7 @@ void Matcher::match( ) { uint i; for( i = 0; ifield_at(i+TypeFunc::Parms)->basic_type(); + new (_calling_convention_mask + i) RegMask(C->comp_arena()); } // Pass array of ideal registers and length to USER code (from the AD file) @@ -291,16 +289,10 @@ void Matcher::match( ) { // preserve area, locks & pad2. OptoReg::Name reg1 = warp_incoming_stk_arg(vm_parm_regs[i].first()); - if (C->failing()) { - return; - } if( OptoReg::is_valid(reg1)) _calling_convention_mask[i].Insert(reg1); OptoReg::Name reg2 = warp_incoming_stk_arg(vm_parm_regs[i].second()); - if (C->failing()) { - return; - } if( OptoReg::is_valid(reg2)) _calling_convention_mask[i].Insert(reg2); @@ -321,14 +313,6 @@ void Matcher::match( ) { _out_arg_limit = OptoReg::add(_new_SP, C->out_preserve_stack_slots()); assert( is_even(_out_arg_limit), "out_preserve must be even" ); - if (!RegMask::can_represent_arg(OptoReg::add(_out_arg_limit,-1))) { - // the compiler cannot represent this method's calling sequence - // Bailout. We do not have space to represent all arguments. - C->record_method_not_compilable("must be able to represent all call arguments in reg mask"); - } - - if (C->failing()) return; // bailed out on incoming arg failure - // --------------- // Collect roots of matcher trees. Every node for which // _shared[_idx] is cleared is guaranteed to not be shared, and thus @@ -451,6 +435,9 @@ void Matcher::match( ) { static RegMask *init_input_masks( uint size, RegMask &ret_adr, RegMask &fp ) { RegMask *rms = NEW_RESOURCE_ARRAY( RegMask, size ); + for (unsigned int i = 0; i < size; ++i) { + new (rms + i) RegMask(Compile::current()->comp_arena()); + } // Do all the pre-defined register masks rms[TypeFunc::Control ] = RegMask::Empty; rms[TypeFunc::I_O ] = RegMask::Empty; @@ -490,7 +477,7 @@ void Matcher::init_first_stack_mask() { // Initialize empty placeholder masks into the newly allocated arena for (int i = 0; i < NOF_STACK_MASKS; i++) { - new (rms + i) RegMask(); + new (rms + i) RegMask(C->comp_arena()); } idealreg2spillmask [Op_RegN] = &rms[0]; @@ -539,32 +526,23 @@ void Matcher::init_first_stack_mask() { idealreg2debugmask [Op_RegVectMask] = &rms[37]; idealreg2mhdebugmask[Op_RegVectMask] = &rms[38]; - OptoReg::Name i; - // At first, start with the empty mask C->FIRST_STACK_mask().Clear(); // Add in the incoming argument area OptoReg::Name init_in = OptoReg::add(_old_SP, C->out_preserve_stack_slots()); - for (i = init_in; i < _in_arg_limit; i = OptoReg::add(i,1)) { + for (OptoReg::Name i = init_in; i < _in_arg_limit; i = OptoReg::add(i, 1)) { C->FIRST_STACK_mask().Insert(i); } // Add in all bits past the outgoing argument area - guarantee(RegMask::can_represent_arg(OptoReg::add(_out_arg_limit,-1)), - "must be able to represent all call arguments in reg mask"); - OptoReg::Name init = _out_arg_limit; - for (i = init; RegMask::can_represent(i); i = OptoReg::add(i,1)) { - C->FIRST_STACK_mask().Insert(i); - } - // Finally, set the "infinite stack" bit. - C->FIRST_STACK_mask().set_infinite_stack(); + C->FIRST_STACK_mask().Set_All_From(_out_arg_limit); // Make spill masks. Registers for their class, plus FIRST_STACK_mask. - RegMask aligned_stack_mask = C->FIRST_STACK_mask(); + RegMask aligned_stack_mask(C->FIRST_STACK_mask(), C->comp_arena()); // Keep spill masks aligned. aligned_stack_mask.clear_to_pairs(); assert(aligned_stack_mask.is_infinite_stack(), "should be infinite stack"); - RegMask scalable_stack_mask = aligned_stack_mask; + RegMask scalable_stack_mask(aligned_stack_mask, C->comp_arena()); *idealreg2spillmask[Op_RegP] = *idealreg2regmask[Op_RegP]; #ifdef _LP64 @@ -984,7 +962,7 @@ void Matcher::init_spill_mask( Node *ret ) { if( idealreg2regmask[Op_RegI] ) return; // One time only init OptoReg::c_frame_pointer = c_frame_pointer(); - c_frame_ptr_mask = c_frame_pointer(); + c_frame_ptr_mask = RegMask(c_frame_pointer()); #ifdef _LP64 // pointers are twice as big c_frame_ptr_mask.Insert(OptoReg::add(c_frame_pointer(),1)); @@ -992,15 +970,11 @@ void Matcher::init_spill_mask( Node *ret ) { // Start at OptoReg::stack0() STACK_ONLY_mask.Clear(); - OptoReg::Name init = OptoReg::stack2reg(0); // STACK_ONLY_mask is all stack bits - OptoReg::Name i; - for (i = init; RegMask::can_represent(i); i = OptoReg::add(i,1)) - STACK_ONLY_mask.Insert(i); - // Also set the "infinite stack" bit. - STACK_ONLY_mask.set_infinite_stack(); + STACK_ONLY_mask.Set_All_From(OptoReg::stack2reg(0)); - for (i = OptoReg::Name(0); i < OptoReg::Name(_last_Mach_Reg); i = OptoReg::add(i, 1)) { + for (OptoReg::Name i = OptoReg::Name(0); i < OptoReg::Name(_last_Mach_Reg); + i = OptoReg::add(i, 1)) { // Copy the register names over into the shared world. // SharedInfo::regName[i] = regName[i]; // Handy RegMasks per machine register @@ -1277,12 +1251,8 @@ OptoReg::Name Matcher::warp_outgoing_stk_arg( VMReg reg, OptoReg::Name begin_out // Keep track of the largest numbered stack slot used for an arg. // Largest used slot per call-site indicates the amount of stack // that is killed by the call. - if( warped >= out_arg_limit_per_call ) - out_arg_limit_per_call = OptoReg::add(warped,1); - if (!RegMask::can_represent_arg(warped)) { - // Bailout. For example not enough space on stack for all arguments. Happens for methods with too many arguments. - C->record_method_not_compilable("unsupported calling sequence"); - return OptoReg::Bad; + if (warped >= out_arg_limit_per_call) { + out_arg_limit_per_call = OptoReg::add(warped, 1); } return warped; } @@ -1366,7 +1336,9 @@ MachNode *Matcher::match_sfpt( SafePointNode *sfpt ) { // Allocate a private array of RegMasks. These RegMasks are not shared. msfpt->_in_rms = NEW_RESOURCE_ARRAY( RegMask, cnt ); // Empty them all. - for (uint i = 0; i < cnt; i++) ::new (&(msfpt->_in_rms[i])) RegMask(); + for (uint i = 0; i < cnt; i++) { + ::new (msfpt->_in_rms + i) RegMask(C->comp_arena()); + } // Do all the pre-defined non-Empty register masks msfpt->_in_rms[TypeFunc::ReturnAdr] = _return_addr_mask; @@ -1449,16 +1421,10 @@ MachNode *Matcher::match_sfpt( SafePointNode *sfpt ) { } // Grab first register, adjust stack slots and insert in mask. OptoReg::Name reg1 = warp_outgoing_stk_arg(first, begin_out_arg_area, out_arg_limit_per_call ); - if (C->failing()) { - return nullptr; - } if (OptoReg::is_valid(reg1)) rm->Insert( reg1 ); // Grab second register (if any), adjust stack slots and insert in mask. OptoReg::Name reg2 = warp_outgoing_stk_arg(second, begin_out_arg_area, out_arg_limit_per_call ); - if (C->failing()) { - return nullptr; - } if (OptoReg::is_valid(reg2)) rm->Insert( reg2 ); } // End of for all arguments @@ -1478,14 +1444,10 @@ MachNode *Matcher::match_sfpt( SafePointNode *sfpt ) { // this killed area. uint r_cnt = mcall->tf()->range()->cnt(); MachProjNode *proj = new MachProjNode( mcall, r_cnt+10000, RegMask::Empty, MachProjNode::fat_proj ); - if (!RegMask::can_represent_arg(OptoReg::Name(out_arg_limit_per_call-1))) { - // Bailout. We do not have space to represent all arguments. - C->record_method_not_compilable("unsupported outgoing calling sequence"); - } else { - for (int i = begin_out_arg_area; i < out_arg_limit_per_call; i++) - proj->_rout.Insert(OptoReg::Name(i)); + for (int i = begin_out_arg_area; i < out_arg_limit_per_call; i++) { + proj->_rout.Insert(OptoReg::Name(i)); } - if (proj->_rout.is_NotEmpty()) { + if (!proj->_rout.is_Empty()) { push_projection(proj); } } diff --git a/src/hotspot/share/opto/node.cpp b/src/hotspot/share/opto/node.cpp index f5f7a4231f2..cca98bd8aba 100644 --- a/src/hotspot/share/opto/node.cpp +++ b/src/hotspot/share/opto/node.cpp @@ -549,6 +549,14 @@ Node *Node::clone() const { to[i] = from[i]->clone(); } } + if (this->is_MachProj()) { + // MachProjNodes contain register masks that may contain pointers to + // externally allocated memory. Make sure to use a proper constructor + // instead of just shallowly copying. + MachProjNode* mach = n->as_MachProj(); + MachProjNode* mthis = this->as_MachProj(); + new (&mach->_rout) RegMask(mthis->_rout); + } if (n->is_Call()) { // CallGenerator is linked to the original node. CallGenerator* cg = n->as_Call()->generator(); diff --git a/src/hotspot/share/opto/optoreg.hpp b/src/hotspot/share/opto/optoreg.hpp index 6298eeea39c..43715771f62 100644 --- a/src/hotspot/share/opto/optoreg.hpp +++ b/src/hotspot/share/opto/optoreg.hpp @@ -25,6 +25,7 @@ #ifndef SHARE_OPTO_OPTOREG_HPP #define SHARE_OPTO_OPTOREG_HPP +#include "utilities/globalDefinitions.hpp" #include "utilities/macros.hpp" // AdGlobals contains c2 specific register handling code as specified @@ -183,27 +184,54 @@ class OptoReg { class OptoRegPair { private: - short _second; - short _first; + typedef short Name; + Name _second; + Name _first; + public: - void set_bad ( ) { _second = OptoReg::Bad; _first = OptoReg::Bad; } - void set1 ( OptoReg::Name n ) { _second = OptoReg::Bad; _first = n; } - void set2 ( OptoReg::Name n ) { _second = n + 1; _first = n; } - void set_pair( OptoReg::Name second, OptoReg::Name first ) { _second= second; _first= first; } - void set_ptr ( OptoReg::Name ptr ) { + static constexpr bool can_fit(OptoReg::Name n) { + return n <= std::numeric_limits::max(); + } + void set_bad() { + _second = OptoReg::Bad; + _first = OptoReg::Bad; + } + void set1(OptoReg::Name n) { + assert(can_fit(n), "overflow"); + _second = OptoReg::Bad; + _first = n; + } + void set2(OptoReg::Name n) { + assert(can_fit(n + 1), "overflow"); + assert(can_fit(n), "overflow"); + _second = n + 1; + _first = n; + } + void set_pair(OptoReg::Name second, OptoReg::Name first) { + assert(can_fit(second), "overflow"); + assert(can_fit(first), "overflow"); + _second = second; + _first = first; + } + void set_ptr(OptoReg::Name ptr) { #ifdef _LP64 - _second = ptr+1; + assert(can_fit(ptr + 1), "overflow"); + _second = ptr + 1; #else _second = OptoReg::Bad; #endif + assert(can_fit(ptr), "overflow"); _first = ptr; } OptoReg::Name second() const { return _second; } OptoReg::Name first() const { return _first; } - OptoRegPair(OptoReg::Name second, OptoReg::Name first) { _second = second; _first = first; } - OptoRegPair(OptoReg::Name f) { _second = OptoReg::Bad; _first = f; } - OptoRegPair() { _second = OptoReg::Bad; _first = OptoReg::Bad; } + OptoRegPair(OptoReg::Name second, OptoReg::Name first) { + assert(can_fit(second), "overflow"); + assert(can_fit(first), "overflow"); + _second = second; + _first = first; + } }; #endif // SHARE_OPTO_OPTOREG_HPP diff --git a/src/hotspot/share/opto/postaloc.cpp b/src/hotspot/share/opto/postaloc.cpp index 23f16dbecfb..56d3ba6bbe0 100644 --- a/src/hotspot/share/opto/postaloc.cpp +++ b/src/hotspot/share/opto/postaloc.cpp @@ -173,8 +173,7 @@ int PhaseChaitin::use_prior_register( Node *n, uint idx, Node *def, Block *curre const LRG &def_lrg = lrgs(_lrg_map.live_range_id(def)); OptoReg::Name def_reg = def_lrg.reg(); const RegMask &use_mask = n->in_RegMask(idx); - bool can_use = (RegMask::can_represent(def_reg) ? (use_mask.Member(def_reg) != 0) - : (use_mask.is_infinite_stack() != 0)); + bool can_use = use_mask.Member(def_reg); if (!RegMask::is_vector(def->ideal_reg())) { // Check for a copy to or from a misaligned pair. // It is workaround for a sparc with misaligned pairs. @@ -665,7 +664,7 @@ void PhaseChaitin::post_allocate_copy_removal() { if( useidx ) { OptoReg::Name ureg = lrgs(useidx).reg(); - if( !value[ureg] ) { + if( value[ureg] == nullptr ) { int idx; // Skip occasional useless copy while( (idx=def->is_Copy()) != 0 && def->in(idx) != nullptr && // null should not happen @@ -679,9 +678,10 @@ void PhaseChaitin::post_allocate_copy_removal() { int n_regs = RegMask::num_registers(def_ideal_reg, lrgs(_lrg_map.live_range_id(def))); for (int l = 1; l < n_regs; l++) { OptoReg::Name ureg_lo = OptoReg::add(ureg,-l); - if (!value[ureg_lo] && - (!RegMask::can_represent(ureg_lo) || - lrgs(useidx).mask().Member(ureg_lo))) { // Nearly always adjacent + bool is_adjacent = lrgs(useidx).mask().Member(ureg_lo); + assert(is_adjacent || OptoReg::is_reg(ureg_lo), + "only registers can be non-adjacent"); + if (value[ureg_lo] == nullptr && is_adjacent) { // Nearly always adjacent value.map(ureg_lo,valdef); // record improved reaching-def info regnd.map(ureg_lo, def); } @@ -762,8 +762,9 @@ void PhaseChaitin::post_allocate_copy_removal() { // If the value occupies a register pair, record same info // in both registers. OptoReg::Name nreg_lo = OptoReg::add(nreg,-1); - if( RegMask::can_represent(nreg_lo) && // Either a spill slot, or - !lrgs(lidx).mask().Member(nreg_lo) ) { // Nearly always adjacent + bool is_adjacent = lrgs(lidx).mask().Member(nreg_lo); + assert(is_adjacent || OptoReg::is_reg(nreg_lo), "only registers can be non-adjacent"); + if (!is_adjacent) { // Nearly always adjacent // Sparc occasionally has non-adjacent pairs. // Find the actual other value RegMask tmp = lrgs(lidx).mask(); diff --git a/src/hotspot/share/opto/reg_split.cpp b/src/hotspot/share/opto/reg_split.cpp index 72110d01267..327c30b152e 100644 --- a/src/hotspot/share/opto/reg_split.cpp +++ b/src/hotspot/share/opto/reg_split.cpp @@ -476,7 +476,7 @@ bool PhaseChaitin::prompt_use( Block *b, uint lidx ) { return true; // Found 1st use! } } - if (n->out_RegMask().is_NotEmpty()) { + if (!n->out_RegMask().is_Empty()) { return false; } } @@ -1126,7 +1126,8 @@ uint PhaseChaitin::Split(uint maxlrg, ResourceArea* split_arena) { // If this node is already a SpillCopy, just patch the edge // except the case of spilling to stack. if( n->is_SpillCopy() ) { - RegMask tmp_rm(umask); + ResourceMark rm(C->regmask_arena()); + RegMask tmp_rm(umask, C->regmask_arena()); tmp_rm.SUBTRACT(Matcher::STACK_ONLY_mask); if( dmask.overlap(tmp_rm) ) { if( def != n->in(inpidx) ) { diff --git a/src/hotspot/share/opto/regmask.cpp b/src/hotspot/share/opto/regmask.cpp index 20fcaf9ccd4..57cf13a8b31 100644 --- a/src/hotspot/share/opto/regmask.cpp +++ b/src/hotspot/share/opto/regmask.cpp @@ -28,7 +28,6 @@ #include "opto/matcher.hpp" #include "opto/node.hpp" #include "opto/regmask.hpp" -#include "utilities/population_count.hpp" #include "utilities/powerOfTwo.hpp" //------------------------------dump------------------------------------------- @@ -52,10 +51,9 @@ const RegMask RegMask::Empty; const RegMask RegMask::All( # define BODY(I) -1, - FORALL_BODY + FORALL_BODY # undef BODY - 0 -); + true); //============================================================================= bool RegMask::is_vector(uint ireg) { @@ -119,10 +117,10 @@ static const uintptr_t low_bits[5] = { fives, // 0x5555..55 void RegMask::clear_to_pairs() { assert(valid_watermarks(), "sanity"); for (unsigned i = _lwm; i <= _hwm; i++) { - uintptr_t bits = _rm_word[i]; + uintptr_t bits = rm_word(i); bits &= ((bits & fives) << 1U); // 1 hi-bit set for each pair bits |= (bits >> 1U); // Smear 1 hi-bit into a pair - _rm_word[i] = bits; + rm_word(i) = bits; } assert(is_aligned_pairs(), "mask is not aligned, adjacent pairs"); } @@ -135,7 +133,7 @@ bool RegMask::is_aligned_pairs() const { // Assert that the register mask contains only bit pairs. assert(valid_watermarks(), "sanity"); for (unsigned i = _lwm; i <= _hwm; i++) { - uintptr_t bits = _rm_word[i]; + uintptr_t bits = rm_word(i); while (bits) { // Check bits for pairing uintptr_t bit = uintptr_t(1) << find_lowest_bit(bits); // Extract low bit // Low bit is not odd means its mis-aligned. @@ -156,7 +154,7 @@ bool RegMask::is_bound1() const { } for (unsigned i = _lwm; i <= _hwm; i++) { - uintptr_t v = _rm_word[i]; + uintptr_t v = rm_word(i); if (v != 0) { // Only one bit allowed -> v must be a power of two if (!is_power_of_2(v)) { @@ -165,7 +163,7 @@ bool RegMask::is_bound1() const { // A single bit was found - check there are no bits in the rest of the mask for (i++; i <= _hwm; i++) { - if (_rm_word[i] != 0) { + if (rm_word(i) != 0) { return false; } } @@ -185,24 +183,24 @@ bool RegMask::is_bound_pair() const { assert(valid_watermarks(), "sanity"); for (unsigned i = _lwm; i <= _hwm; i++) { - if (_rm_word[i] != 0) { // Found some bits - unsigned int bit_index = find_lowest_bit(_rm_word[i]); + if (rm_word(i) != 0) { // Found some bits + unsigned int bit_index = find_lowest_bit(rm_word(i)); if (bit_index != WORD_BIT_MASK) { // Bit pair stays in same word? uintptr_t bit = uintptr_t(1) << bit_index; // Extract lowest bit from mask - if ((bit | (bit << 1U)) != _rm_word[i]) { + if ((bit | (bit << 1U)) != rm_word(i)) { return false; // Require adjacent bit pair and no more bits } } else { // Else its a split-pair case - assert(is_power_of_2(_rm_word[i]), "invariant"); + assert(is_power_of_2(rm_word(i)), "invariant"); i++; // Skip iteration forward - if (i > _hwm || _rm_word[i] != 1) { + if (i > _hwm || rm_word(i) != 1) { return false; // Require 1 lo bit in next word } } // A matching pair was found - check there are no bits in the rest of the mask for (i++; i <= _hwm; i++) { - if (_rm_word[i] != 0) { + if (rm_word(i) != 0) { return false; } } @@ -248,9 +246,10 @@ OptoReg::Name RegMask::find_first_set(LRG &lrg, const int size) const { } assert(valid_watermarks(), "sanity"); for (unsigned i = _lwm; i <= _hwm; i++) { - if (_rm_word[i]) { // Found some bits + if (rm_word(i) != 0) { // Found some bits // Convert to bit number, return hi bit in pair - return OptoReg::Name((i << LogBitsPerWord) + find_lowest_bit(_rm_word[i]) + (size - 1)); + return OptoReg::Name(offset_bits() + (i << LogBitsPerWord) + + find_lowest_bit(rm_word(i)) + (size - 1)); } } return OptoReg::Bad; @@ -264,7 +263,7 @@ void RegMask::clear_to_sets(const unsigned int size) { assert(valid_watermarks(), "sanity"); uintptr_t low_bits_mask = low_bits[size >> 2U]; for (unsigned i = _lwm; i <= _hwm; i++) { - uintptr_t bits = _rm_word[i]; + uintptr_t bits = rm_word(i); uintptr_t sets = (bits & low_bits_mask); for (unsigned j = 1U; j < size; j++) { sets = (bits & (sets << 1U)); // filter bits which produce whole sets @@ -279,7 +278,7 @@ void RegMask::clear_to_sets(const unsigned int size) { } } } - _rm_word[i] = sets; + rm_word(i) = sets; } assert(is_aligned_sets(size), "mask is not aligned, adjacent sets"); } @@ -292,7 +291,7 @@ void RegMask::smear_to_sets(const unsigned int size) { assert(valid_watermarks(), "sanity"); uintptr_t low_bits_mask = low_bits[size >> 2U]; for (unsigned i = _lwm; i <= _hwm; i++) { - uintptr_t bits = _rm_word[i]; + uintptr_t bits = rm_word(i); uintptr_t sets = 0; for (unsigned j = 0; j < size; j++) { sets |= (bits & low_bits_mask); // collect partial bits @@ -308,7 +307,7 @@ void RegMask::smear_to_sets(const unsigned int size) { } } } - _rm_word[i] = sets; + rm_word(i) = sets; } assert(is_aligned_sets(size), "mask is not aligned, adjacent sets"); } @@ -321,8 +320,8 @@ bool RegMask::is_aligned_sets(const unsigned int size) const { uintptr_t low_bits_mask = low_bits[size >> 2U]; assert(valid_watermarks(), "sanity"); for (unsigned i = _lwm; i <= _hwm; i++) { - uintptr_t bits = _rm_word[i]; - while (bits) { // Check bits for pairing + uintptr_t bits = rm_word(i); + while (bits != 0) { // Check bits for pairing uintptr_t bit = uintptr_t(1) << find_lowest_bit(bits); // Low bit is not odd means its mis-aligned. if ((bit & low_bits_mask) == 0) { @@ -350,31 +349,31 @@ bool RegMask::is_bound_set(const unsigned int size) const { assert(1 <= size && size <= 16, "update low bits table"); assert(valid_watermarks(), "sanity"); for (unsigned i = _lwm; i <= _hwm; i++) { - if (_rm_word[i] != 0) { // Found some bits - unsigned bit_index = find_lowest_bit(_rm_word[i]); + if (rm_word(i) != 0) { // Found some bits + unsigned bit_index = find_lowest_bit(rm_word(i)); uintptr_t bit = uintptr_t(1) << bit_index; if (bit_index + size <= BitsPerWord) { // Bit set stays in same word? uintptr_t hi_bit = bit << (size - 1); uintptr_t set = hi_bit + ((hi_bit-1) & ~(bit-1)); - if (set != _rm_word[i]) { + if (set != rm_word(i)) { return false; // Require adjacent bit set and no more bits } } else { // Else its a split-set case // All bits from bit to highest bit in the word must be set - if ((all & ~(bit - 1)) != _rm_word[i]) { + if ((all & ~(bit - 1)) != rm_word(i)) { return false; } i++; // Skip iteration forward and check high part // The lower bits should be 1 since it is split case. uintptr_t set = (bit >> (BitsPerWord - size)) - 1; - if (i > _hwm || _rm_word[i] != set) { + if (i > _hwm || rm_word(i) != set) { return false; // Require expected low bits in next word } } // A matching set found - check there are no bits in the rest of the mask for (i++; i <= _hwm; i++) { - if (_rm_word[i] != 0) { + if (rm_word(i) != 0) { return false; } } @@ -399,21 +398,30 @@ bool RegMask::is_UP() const { return true; } -// Compute size of register mask in bits -uint RegMask::Size() const { - uint sum = 0; - assert(valid_watermarks(), "sanity"); - for (unsigned i = _lwm; i <= _hwm; i++) { - sum += population_count(_rm_word[i]); +#ifndef PRODUCT +bool RegMask::dump_end_run(outputStream* st, OptoReg::Name start, + OptoReg::Name last) const { + bool last_is_end = last == (int)offset_bits() + (int)rm_size_in_bits() - 1; + if (is_infinite_stack() && last_is_end) { + st->print("-..."); + return true; } - return sum; + if (start == last) { // 1-register run; no special printing + } else if (start + 1 == last) { + st->print(","); // 2-register run; print as "rX,rY" + OptoReg::dump(last, st); + } else { // Multi-register run; print as "rX-rZ" + st->print("-"); + OptoReg::dump(last, st); + } + return false; } -#ifndef PRODUCT void RegMask::dump(outputStream *st) const { st->print("["); RegMaskIterator rmi(*this); + bool printed_infinite_stack = false; if (rmi.has_next()) { OptoReg::Name start = rmi.next(); @@ -430,32 +438,38 @@ void RegMask::dump(outputStream *st) const { // Adjacent registers just collect into long runs, no printing. last = reg; } else { // Ending some kind of run - if (start == last) { // 1-register run; no special printing - } else if (start+1 == last) { - st->print(","); // 2-register run; print as "rX,rY" - OptoReg::dump(last, st); - } else { // Multi-register run; print as "rX-rZ" - st->print("-"); - OptoReg::dump(last, st); - } + printed_infinite_stack = dump_end_run(st, start, last); + assert(!printed_infinite_stack, ""); st->print(","); // Separate start of new run start = last = reg; // Start a new register run OptoReg::dump(start, st); // Print register } // End of if ending a register run or not } // End of while regmask not empty - - if (start == last) { // 1-register run; no special printing - } else if (start+1 == last) { - st->print(","); // 2-register run; print as "rX,rY" - OptoReg::dump(last, st); - } else { // Multi-register run; print as "rX-rZ" - st->print("-"); - OptoReg::dump(last, st); + printed_infinite_stack = dump_end_run(st, start, last); + // Print infinite_stack if not already done. + if (is_infinite_stack() && !printed_infinite_stack) { + st->print(","); + OptoReg::dump(offset_bits() + rm_size_in_bits(), st); + st->print("-..."); } - if (is_infinite_stack()) { - st->print("..."); + } else { + // Mask is infinite_stack only. + if (is_infinite_stack() && !printed_infinite_stack) { + OptoReg::dump(offset_bits() + rm_size_in_bits(), st); + st->print("-..."); } } st->print("]"); } + +void RegMask::dump_hex(outputStream* st) const { + st->print("...%x|", is_infinite_stack() ? 0xf : 0x0); + for (int i = rm_word_max_index(); i >= 0; i--) { + st->print(LP64_ONLY("%0*lx") NOT_LP64("%0*x"), + (int)sizeof(uintptr_t) * CHAR_BIT / 4, rm_word(i)); + if (i != 0) { + st->print("|"); + } + } +} #endif diff --git a/src/hotspot/share/opto/regmask.hpp b/src/hotspot/share/opto/regmask.hpp index fa1721bd45a..67e160940cc 100644 --- a/src/hotspot/share/opto/regmask.hpp +++ b/src/hotspot/share/opto/regmask.hpp @@ -26,13 +26,43 @@ #define SHARE_OPTO_REGMASK_HPP #include "code/vmreg.hpp" +#include "memory/arena.hpp" #include "opto/optoreg.hpp" #include "utilities/count_leading_zeros.hpp" #include "utilities/count_trailing_zeros.hpp" #include "utilities/globalDefinitions.hpp" +//------------------------------RegMask---------------------------------------- +// The register mask data structure (RegMask) provides a representation +// of sets of OptoReg::Name (i.e., machine registers and stack slots). The data +// structure tracks register availability and allocations during code +// generation, in particular during register allocation. Internally, RegMask +// uses a compact bitset representation. Further documentation, including an +// illustrative example, is available in source code comments throughout this +// file. + +// The ADLC defines 3 macros, RM_SIZE_IN_INTS, RM_SIZE_IN_INTS_MIN, and FORALL_BODY. +// RM_SIZE_IN_INTS is the base size of a register mask in 32-bit words. +// RM_SIZE_IN_INTS_MIN is the theoretical minimum size of a register mask in 32-bit +// words. +// FORALL_BODY replicates a BODY macro once per word in the register mask. +// The usage is somewhat clumsy and limited to the regmask.[h,c]pp files. +// However, it means the ADLC can redefine the unroll macro and all loops +// over register masks will be unrolled by the correct amount. +// +// The ADL file describes how to print the machine-specific registers, as well +// as any notion of register classes. + class LRG; +// To avoid unbounded RegMask growth and to be able to statically compute a +// register mask size upper bound (see RM_SIZE_IN_INTS_MAX below), we need to +// set some form of limit on the number of stack slots used by BoxLockNodes. The +// limit below is rather arbitrary but should be quite generous and cover all +// practical cases. We reach this limit by, e.g., deeply nesting synchronized +// statements in Java. +const int BoxLockNode_SLOT_LIMIT = 200; + //-------------Non-zero bit search methods used by RegMask--------------------- // Find lowest 1, undefined if empty/0 static unsigned int find_lowest_bit(uintptr_t mask) { @@ -43,18 +73,6 @@ static unsigned int find_highest_bit(uintptr_t mask) { return count_leading_zeros(mask) ^ (BitsPerWord - 1U); } -//------------------------------RegMask---------------------------------------- -// The ADL file describes how to print the machine-specific registers, as well -// as any notion of register classes. We provide a register mask, which is -// just a collection of Register numbers. - -// The ADLC defines 2 macros, RM_SIZE_IN_INTS and FORALL_BODY. -// RM_SIZE_IN_INTS is the size of a register mask in 32-bit words. -// FORALL_BODY replicates a BODY macro once per word in the register mask. -// The usage is somewhat clumsy and limited to the regmask.[h,c]pp files. -// However, it means the ADLC can redefine the unroll macro and all loops -// over register masks will be unrolled by the correct amount. - class RegMask { friend class RegMaskIterator; @@ -63,29 +81,310 @@ class RegMask { LP64_ONLY(STATIC_ASSERT(is_aligned(RM_SIZE_IN_INTS, 2))); static const unsigned int WORD_BIT_MASK = BitsPerWord - 1U; - static const unsigned int RM_SIZE_IN_WORDS = - LP64_ONLY(RM_SIZE_IN_INTS >> 1) NOT_LP64(RM_SIZE_IN_INTS); + + // RM_SIZE_IN_INTS, but in number of machine words + static const unsigned int RM_SIZE_IN_WORDS = LP64_ONLY(RM_SIZE_IN_INTS >> 1) NOT_LP64(RM_SIZE_IN_INTS); + + // The last index (in machine words) of the (static) array of register mask + // bits static const unsigned int RM_WORD_MAX_INDEX = RM_SIZE_IN_WORDS - 1U; + // Compute a best-effort (statically known) upper bound for register mask + // size in 32-bit words. When extending/growing register masks, we should + // never grow past this size. + static const unsigned int RM_SIZE_IN_INTS_MAX = + (((RM_SIZE_IN_INTS_MIN << 5) + // Slots for machine registers + (max_method_parameter_length * 2) + // Slots for incoming arguments (from caller) + (max_method_parameter_length * 2) + // Slots for outgoing arguments (to callee) + BoxLockNode_SLOT_LIMIT + // Slots for locks + 64 // Padding, reserved words, etc. + ) + 31) >> 5; // Number of bits -> number of 32-bit words + + // RM_SIZE_IN_INTS_MAX, but in number of machine words + static const unsigned int RM_SIZE_IN_WORDS_MAX = + LP64_ONLY(((RM_SIZE_IN_INTS_MAX + 1) & ~1) >> 1) NOT_LP64(RM_SIZE_IN_INTS_MAX); + + // Sanity check + STATIC_ASSERT(RM_SIZE_IN_INTS <= RM_SIZE_IN_INTS_MAX); + + // Ensure that register masks cannot grow beyond the point at which + // OptoRegPair can no longer index the whole mask + STATIC_ASSERT(OptoRegPair::can_fit((RM_SIZE_IN_INTS_MAX << 5) - 1)); + union { - // Array of Register Mask bits. This array is large enough to cover - // all the machine registers and all parameters that need to be passed - // on the stack (stack registers) up to some interesting limit. Methods - // that need more parameters will NOT be compiled. On Intel, the limit - // is something like 90+ parameters. + // Array of Register Mask bits. The array should be + // large enough to cover all the machine registers, as well as a certain + // number of parameters that need to be passed on the stack (stack + // registers). The number of parameters that can fit in the mask should be + // dimensioned to cover most common cases. We handle the uncommon cases by + // extending register masks dynamically (see below). + + // Viewed as an array of 32-bit words int _rm_int[RM_SIZE_IN_INTS]; + + // Viewed as an array of machine words uintptr_t _rm_word[RM_SIZE_IN_WORDS]; }; - // The low and high water marks represents the lowest and highest word - // that might contain set register mask bits, respectively. We guarantee - // that there are no bits in words outside this range, but any word at - // and between the two marks can still be 0. + // In rare situations (e.g., "more than 90+ parameters on Intel"), we need to + // extend the register mask with dynamically allocated memory. We keep the + // base statically allocated _rm_word, and arena allocate the extended mask + // (_rm_word_ext) separately. Another, perhaps more elegant, option would be to + // have two subclasses of RegMask, where one is statically allocated and one + // is (entirely) dynamically allocated. Given that register mask extension is + // rare, we decided to use the current approach (_rm_word and _rm_word_ext) to + // keep the common case fast. Most of the time, we will then not need to + // dynamically allocate anything. + // + // We could use a GrowableArray here, but there are currently some + // GrowableArray limitations that have a negative performance impact for our + // use case: + // + // - There is no efficient copy/clone operation. + // - GrowableArray construction currently default-initializes everything + // within the array's initial capacity, which is unnecessary in our case. + // + // After addressing these limitations, we should consider using a + // GrowableArray here. + uintptr_t* _rm_word_ext = nullptr; + + // Where to extend the register mask + Arena* _arena; + +#ifdef ASSERT + // Register masks may get shallowly copied without the use of constructors, + // for example as part of `Node::clone`. This is problematic when dealing with + // the externally allocated memory for _rm_word_ext. Therefore, we need some + // sanity checks to ensure we have addressed all such cases. The below + // variables enable such checks. + // + // The original address of the _rm_word_ext variable, set when using + // constructors. If we get copied/cloned, &_rm_word_ext will no longer equal + // _original_ext_address. + uintptr_t** _original_ext_address = &_rm_word_ext; + // + // If the original version, of which we may be a clone, is read-only. In such + // cases, we can allow read-only sharing. + bool _read_only = false; +#endif + + // Current *total* register mask size in machine words (both static and + // dynamic parts) + unsigned int _rm_size_in_words; + + // If _infinite_stack = true, we consider all registers beyond what the register + // mask can currently represent to be included. If _infinite_stack = false, we + // consider the registers not included. + bool _infinite_stack = false; + + // The low and high watermarks represent the lowest and highest word that + // might contain set register mask bits, respectively. We guarantee that + // there are no bits in words outside this range, but any word at and between + // the two marks can still be 0. We only use the watermarks to improve + // performance, and do not guarantee that the watermarks are optimal. If _hwm + // < _lwm, the register mask is necessarily empty. Indeed, when we construct + // empty register masks, we set _hwm = 0 and _lwm = max. The watermarks do not + // concern _infinite_stack-registers. unsigned int _lwm; unsigned int _hwm; - public: - enum { CHUNK_SIZE = RM_SIZE_IN_WORDS * BitsPerWord }; + // The following diagram illustrates the internal representation of a RegMask + // (for a made-up platform with 10 registers and 4-bit words) that has been + // extended with two additional words to represent more stack locations: + // + // _lwm=1 RM_SIZE_IN_WORDS=3 _hwm=3 _rm_size_in_words=5 + // | | | | + // r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 ... + // Content: [0 0 0 0 |0 1 1 0 |0 0 1 0 ] [1 1 0 1 |0 0 0 0] is is is + // Index: [0] [1] [2] [0] [1] + // + // \____________________________________/ \______________________/ + // | | + // _rm_word _rm_word_ext + // \_____________________________________________________________/ + // | + // _rm_size_in_words=5 + // + // In this example, registers {r5, r6} and stack locations {s0, s2, s3, s5} + // are included in the register mask. Depending on the value of + // _infinite_stack (denoted with is), {s10, s11, ...} are all included (is=1) + // or excluded (is=0). Note that all registers/stack locations under _lwm + // and over _hwm are excluded. The exception is {s10, s11, ...}, where the + // value is decided solely by _infinite_stack, regardless of the value of + // _hwm. + + // We support offsetting/shifting register masks to make explicit stack + // slots that originally are implicitly represented by _infinite_stack=true. + // The main use is in PhaseChaitin::Select, when selecting stack slots for + // spilled values. Spilled values *must* get a stack slot, and therefore have + // _infinite_stack=true. If we run out of stack slots in an + // _infinite_mask=true register mask, we roll over the register mask to make + // the next set of stack slots available for selection. + // + // The _offset variable indicates how many words we offset with. + // We consider all registers before the offset to not be included in the + // register mask. + unsigned int _offset; + // + // The only operation that may update the _offset attribute is + // RegMask::rollover(). This operation requires the register mask to be + // clean/empty (all zeroes), except for _infinite_stack, which must be true, + // and has the effect of increasing _offset by _rm_size_in_words and setting + // all bits (now necessarily representing stack locations) to 1. Here is how + // the above register mask looks like after clearing, setting _infinite_stack + // to true, and successfully rolling over: + // + // _lwm=0 RM_SIZE_IN_WORDS=3 _hwm=4 _rm_size_in_words=5 + // | | | | + // s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 ... + // Content: [1 1 1 1 |1 1 1 1 |1 1 1 1 ] [1 1 1 1 |1 1 1 1] 1 1 1 + // Index: [0] [1] [2] [0] [1] + // + // \_______________________________________________/ \_____________________________/ + // | | + // _rm_word _rm_word_ext + // \_______________________________________________________________________________/ + // | + // _rm_size_in_words=_offset=5 + + // Access word i in the register mask. + const uintptr_t& rm_word(unsigned int i) const { + assert(_read_only || _original_ext_address == &_rm_word_ext, "clone sanity check"); + assert(i < _rm_size_in_words, "sanity"); + if (i < RM_SIZE_IN_WORDS) { + return _rm_word[i]; + } else { + assert(_rm_word_ext != nullptr, "sanity"); + return _rm_word_ext[i - RM_SIZE_IN_WORDS]; + } + } + + // Non-const version of the above. + uintptr_t& rm_word(unsigned int i) { + assert(_original_ext_address == &_rm_word_ext, "clone sanity check"); + return const_cast(const_cast(this)->rm_word(i)); + } + + // The current maximum word index + unsigned int rm_word_max_index() const { + return _rm_size_in_words - 1U; + } + + // Grow the register mask to ensure it can fit at least min_size words. + void grow(unsigned int min_size, bool initialize_by_infinite_stack = true) { + if (min_size > _rm_size_in_words) { + assert(min_size <= RM_SIZE_IN_WORDS_MAX, "unexpected register mask growth"); + assert(_arena != nullptr, "register mask not growable"); + min_size = MIN2(RM_SIZE_IN_WORDS_MAX, round_up_power_of_2(min_size)); + unsigned int old_size = _rm_size_in_words; + unsigned int old_ext_size = old_size - RM_SIZE_IN_WORDS; + unsigned int new_ext_size = min_size - RM_SIZE_IN_WORDS; + _rm_size_in_words = min_size; + if (_rm_word_ext == nullptr) { + assert(old_ext_size == 0, "sanity"); + _rm_word_ext = NEW_ARENA_ARRAY(_arena, uintptr_t, new_ext_size); + } else { + assert(_original_ext_address == &_rm_word_ext, "clone sanity check"); + _rm_word_ext = REALLOC_ARENA_ARRAY(_arena, uintptr_t, _rm_word_ext, + old_ext_size, new_ext_size); + } + if (initialize_by_infinite_stack) { + int fill = 0; + if (is_infinite_stack()) { + fill = 0xFF; + _hwm = rm_word_max_index(); + } + set_range(old_size, fill, _rm_size_in_words - old_size); + } + } + } + + // Make us a copy of src + void copy(const RegMask& src) { + assert(_offset == src._offset, "offset mismatch"); + _hwm = src._hwm; + _lwm = src._lwm; + + // Copy base mask + memcpy(_rm_word, src._rm_word, sizeof(uintptr_t) * RM_SIZE_IN_WORDS); + _infinite_stack = src._infinite_stack; + + // Copy extension + if (src._rm_word_ext != nullptr) { + assert(src._rm_size_in_words > RM_SIZE_IN_WORDS, "sanity"); + assert(_original_ext_address == &_rm_word_ext, "clone sanity check"); + grow(src._rm_size_in_words, false); + memcpy(_rm_word_ext, src._rm_word_ext, + sizeof(uintptr_t) * (src._rm_size_in_words - RM_SIZE_IN_WORDS)); + } + + // If the source is smaller than us, we need to set the gap according to + // the sources infinite_stack flag. + if (src._rm_size_in_words < _rm_size_in_words) { + int value = 0; + if (src.is_infinite_stack()) { + value = 0xFF; + _hwm = rm_word_max_index(); + } + set_range(src._rm_size_in_words, value, _rm_size_in_words - src._rm_size_in_words); + } + + assert(valid_watermarks(), "post-condition"); + } + + // Make the watermarks as tight as possible. + void trim_watermarks() { + if (_hwm < _lwm) { + return; + } + while ((_hwm > _lwm) && rm_word(_hwm) == 0) { + _hwm--; + } + while ((_lwm < _hwm) && rm_word(_lwm) == 0) { + _lwm++; + } + if ((_lwm == _hwm) && rm_word(_lwm) == 0) { + _lwm = rm_word_max_index(); + _hwm = 0; + } + } + + // Set a span of words in the register mask to a given value. + void set_range(unsigned int start, int value, unsigned int length) { + if (start < RM_SIZE_IN_WORDS) { + memset(_rm_word + start, value, + sizeof(uintptr_t) * MIN2((int)length, (int)RM_SIZE_IN_WORDS - (int)start)); + } + if (start + length > RM_SIZE_IN_WORDS) { + assert(_rm_word_ext != nullptr, "sanity"); + assert(_original_ext_address == &_rm_word_ext, "clone sanity check"); + memset(_rm_word_ext + MAX2((int)start - (int)RM_SIZE_IN_WORDS, 0), value, + sizeof(uintptr_t) * + MIN2((int)length, (int)length - ((int)RM_SIZE_IN_WORDS - (int)start))); + } + } + +public: + unsigned int rm_size_in_words() const { + return _rm_size_in_words; + } + unsigned int rm_size_in_bits() const { + return _rm_size_in_words * BitsPerWord; + } + + bool is_offset() const { + return _offset > 0; + } + unsigned int offset_bits() const { + return _offset * BitsPerWord; + }; + + bool is_infinite_stack() const { + return _infinite_stack; + } + void set_infinite_stack(bool value) { + _infinite_stack = value; + } // SlotsPerLong is 2, since slots are 32 bits and longs are 64 bits. // Also, consider the maximum alignment size for a normally allocated @@ -110,12 +409,13 @@ class RegMask { }; // A constructor only used by the ADLC output. All mask fields are filled - // in directly. Calls to this look something like RM(1,2,3,4); + // in directly. Calls to this look something like RM(0xc0, 0x0, 0x0, false); RegMask( # define BODY(I) int a##I, - FORALL_BODY + FORALL_BODY # undef BODY - int dummy = 0) { + bool infinite_stack) + : _arena(nullptr), _rm_size_in_words(RM_SIZE_IN_WORDS), _infinite_stack(infinite_stack), _offset(0) { #if defined(VM_LITTLE_ENDIAN) || !defined(_LP64) # define BODY(I) _rm_int[I] = a##I; #else @@ -135,76 +435,91 @@ class RegMask { assert(valid_watermarks(), "post-condition"); } - // Handy copying constructor - RegMask(RegMask *rm) { - _hwm = rm->_hwm; - _lwm = rm->_lwm; - for (unsigned i = 0; i < RM_SIZE_IN_WORDS; i++) { - _rm_word[i] = rm->_rm_word[i]; - } + // Construct an empty mask + explicit RegMask(Arena* arena DEBUG_ONLY(COMMA bool read_only = false)) + : _rm_word(), _arena(arena) DEBUG_ONLY(COMMA _read_only(read_only)), + _rm_size_in_words(RM_SIZE_IN_WORDS), _infinite_stack(false), _lwm(RM_WORD_MAX_INDEX), _hwm(0), _offset(0) { assert(valid_watermarks(), "post-condition"); } - - // Construct an empty mask - RegMask() : _rm_word(), _lwm(RM_WORD_MAX_INDEX), _hwm(0) { + RegMask() : RegMask(nullptr) { assert(valid_watermarks(), "post-condition"); } // Construct a mask with a single bit - RegMask(OptoReg::Name reg) : RegMask() { + RegMask(OptoReg::Name reg, + Arena* arena DEBUG_ONLY(COMMA bool read_only = false)) + : RegMask(arena DEBUG_ONLY(COMMA read_only)) { Insert(reg); } + explicit RegMask(OptoReg::Name reg) : RegMask(reg, nullptr) {} + + // ---------------------------------------- + // Deep copying constructors and assignment + // ---------------------------------------- + + RegMask(const RegMask& rm, Arena* arena) + : _arena(arena), _rm_size_in_words(RM_SIZE_IN_WORDS), _offset(rm._offset) { + copy(rm); + } + + RegMask(const RegMask& rm) : RegMask(rm, nullptr) {} + + RegMask& operator=(const RegMask& rm) { + copy(rm); + return *this; + } + + // ---------------- + // End deep copying + // ---------------- - // Check for register being in mask bool Member(OptoReg::Name reg) const { - assert(reg < CHUNK_SIZE, ""); - - unsigned r = (unsigned)reg; - return _rm_word[r >> LogBitsPerWord] & (uintptr_t(1) << (r & WORD_BIT_MASK)); - } - - // The last bit in the register mask indicates that the mask should repeat - // indefinitely with ONE bits. Returns TRUE if mask is infinite or - // unbounded in size. Returns FALSE if mask is finite size. - bool is_infinite_stack() const { - return (_rm_word[RM_WORD_MAX_INDEX] & (uintptr_t(1) << WORD_BIT_MASK)) != 0; - } - - void set_infinite_stack() { - _rm_word[RM_WORD_MAX_INDEX] |= (uintptr_t(1) << WORD_BIT_MASK); - } - - // Test for being a not-empty mask. - bool is_NotEmpty() const { - assert(valid_watermarks(), "sanity"); - uintptr_t tmp = 0; - for (unsigned i = _lwm; i <= _hwm; i++) { - tmp |= _rm_word[i]; + reg = reg - offset_bits(); + if (reg < 0) { + return false; } - return tmp; + if (reg >= (int)rm_size_in_bits()) { + return is_infinite_stack(); + } + unsigned int r = (unsigned int)reg; + return rm_word(r >> LogBitsPerWord) & (uintptr_t(1) << (r & WORD_BIT_MASK)); + } + + // Empty mask check. Ignores registers included through the infinite_stack flag. + bool is_Empty() const { + assert(valid_watermarks(), "sanity"); + for (unsigned i = _lwm; i <= _hwm; i++) { + if (rm_word(i) != 0) { + return false; + } + } + return true; } // Find lowest-numbered register from mask, or BAD if mask is empty. OptoReg::Name find_first_elem() const { assert(valid_watermarks(), "sanity"); for (unsigned i = _lwm; i <= _hwm; i++) { - uintptr_t bits = _rm_word[i]; - if (bits) { - return OptoReg::Name((i << LogBitsPerWord) + find_lowest_bit(bits)); + uintptr_t bits = rm_word(i); + if (bits != 0) { + return OptoReg::Name(offset_bits() + (i << LogBitsPerWord) + + find_lowest_bit(bits)); } } return OptoReg::Name(OptoReg::Bad); } - // Get highest-numbered register from mask, or BAD if mask is empty. + // Get highest-numbered register from mask, or BAD if mask is empty. Ignores + // registers included through the infinite_stack flag. OptoReg::Name find_last_elem() const { assert(valid_watermarks(), "sanity"); // Careful not to overflow if _lwm == 0 unsigned i = _hwm + 1; while (i > _lwm) { - uintptr_t bits = _rm_word[--i]; - if (bits) { - return OptoReg::Name((i << LogBitsPerWord) + find_highest_bit(bits)); + uintptr_t bits = rm_word(--i); + if (bits != 0) { + return OptoReg::Name(offset_bits() + (i << LogBitsPerWord) + + find_highest_bit(bits)); } } return OptoReg::Name(OptoReg::Bad); @@ -217,13 +532,27 @@ class RegMask { // Verify watermarks are sane, i.e., within bounds and that no // register words below or above the watermarks have bits set. bool valid_watermarks() const { - assert(_hwm < RM_SIZE_IN_WORDS, "_hwm out of range: %d", _hwm); - assert(_lwm < RM_SIZE_IN_WORDS, "_lwm out of range: %d", _lwm); + assert(_hwm < _rm_size_in_words, "_hwm out of range: %d", _hwm); + assert(_lwm < _rm_size_in_words, "_lwm out of range: %d", _lwm); for (unsigned i = 0; i < _lwm; i++) { - assert(_rm_word[i] == 0, "_lwm too high: %d regs at: %d", _lwm, i); + assert(rm_word(i) == 0, "_lwm too high: %d regs at: %d", _lwm, i); } - for (unsigned i = _hwm + 1; i < RM_SIZE_IN_WORDS; i++) { - assert(_rm_word[i] == 0, "_hwm too low: %d regs at: %d", _hwm, i); + for (unsigned i = _hwm + 1; i < _rm_size_in_words; i++) { + assert(rm_word(i) == 0, "_hwm too low: %d regs at: %d", _hwm, i); + } + return true; + } + + bool is_infinite_stack_only() const { + assert(valid_watermarks(), "sanity"); + if (!is_infinite_stack()) { + return false; + } + uintptr_t tmp = 0; + for (unsigned int i = _lwm; i <= _hwm; i++) { + if (rm_word(i) != 0) { + return false; + } } return true; } @@ -264,115 +593,314 @@ class RegMask { static int num_registers(uint ireg); static int num_registers(uint ireg, LRG &lrg); - // Fast overlap test. Non-zero if any registers in common. + // Overlap test. Non-zero if any registers in common, including infinite_stack. bool overlap(const RegMask &rm) const { + assert(_offset == rm._offset, "offset mismatch"); assert(valid_watermarks() && rm.valid_watermarks(), "sanity"); + + // Very common overlap case: _rm_word overlap. Check first to reduce + // execution time. unsigned hwm = MIN2(_hwm, rm._hwm); unsigned lwm = MAX2(_lwm, rm._lwm); - uintptr_t result = 0; for (unsigned i = lwm; i <= hwm; i++) { - result |= _rm_word[i] & rm._rm_word[i]; + if ((rm_word(i) & rm.rm_word(i)) != 0) { + return true; + } } - return result; + + // Very rare overlap cases below. + + // We are both infinite_stack + if (is_infinite_stack() && rm.is_infinite_stack()) { + return true; + } + + // We are infinite_stack and rm _hwm is bigger than us + if (is_infinite_stack() && rm._hwm >= _rm_size_in_words) { + for (unsigned i = MAX2(rm._lwm, _rm_size_in_words); i <= rm._hwm; i++) { + if (rm.rm_word(i) != 0) { + return true; + } + } + } + + // rm is infinite_stack and our _hwm is bigger than rm + if (rm.is_infinite_stack() && _hwm >= rm._rm_size_in_words) { + for (unsigned i = MAX2(_lwm, rm._rm_size_in_words); i <= _hwm; i++) { + if (rm_word(i) != 0) { + return true; + } + } + } + + // No overlap (also very common) + return false; } // Special test for register pressure based splitting // UP means register only, Register plus stack, or stack only is DOWN bool is_UP() const; - // Clear a register mask + // Clear a register mask. Does not clear any offset. void Clear() { - _lwm = RM_WORD_MAX_INDEX; + _lwm = rm_word_max_index(); _hwm = 0; - memset(_rm_word, 0, sizeof(uintptr_t) * RM_SIZE_IN_WORDS); + set_range(0, 0, _rm_size_in_words); + set_infinite_stack(false); assert(valid_watermarks(), "sanity"); } // Fill a register mask with 1's void Set_All() { + assert(_offset == 0, "offset non-zero"); + Set_All_From_Offset(); + } + + // Fill a register mask with 1's from the current offset. + void Set_All_From_Offset() { _lwm = 0; - _hwm = RM_WORD_MAX_INDEX; - memset(_rm_word, 0xFF, sizeof(uintptr_t) * RM_SIZE_IN_WORDS); + _hwm = rm_word_max_index(); + set_range(0, 0xFF, _rm_size_in_words); + set_infinite_stack(true); assert(valid_watermarks(), "sanity"); } + // Fill a register mask with 1's starting from the given register. + void Set_All_From(OptoReg::Name reg) { + reg = reg - offset_bits(); + assert(reg != OptoReg::Bad, "sanity"); + assert(reg != OptoReg::Special, "sanity"); + assert(reg >= 0, "register outside mask"); + assert(valid_watermarks(), "pre-condition"); + unsigned int r = (unsigned int)reg; + unsigned int index = r >> LogBitsPerWord; + unsigned int min_size = index + 1; + grow(min_size); + rm_word(index) |= (uintptr_t(-1) << (r & WORD_BIT_MASK)); + if (index < rm_word_max_index()) { + set_range(index + 1, 0xFF, rm_word_max_index() - index); + } + if (index < _lwm) { + _lwm = index; + } + _hwm = rm_word_max_index(); + set_infinite_stack(true); + assert(valid_watermarks(), "post-condition"); + } + // Insert register into mask void Insert(OptoReg::Name reg) { + reg = reg - offset_bits(); assert(reg != OptoReg::Bad, "sanity"); assert(reg != OptoReg::Special, "sanity"); - assert(reg < CHUNK_SIZE, "sanity"); + assert(reg >= 0, "register outside mask"); assert(valid_watermarks(), "pre-condition"); - unsigned r = (unsigned)reg; - unsigned index = r >> LogBitsPerWord; + unsigned int r = (unsigned int)reg; + unsigned int index = r >> LogBitsPerWord; + unsigned int min_size = index + 1; + grow(min_size); if (index > _hwm) _hwm = index; if (index < _lwm) _lwm = index; - _rm_word[index] |= (uintptr_t(1) << (r & WORD_BIT_MASK)); + rm_word(index) |= (uintptr_t(1) << (r & WORD_BIT_MASK)); assert(valid_watermarks(), "post-condition"); } // Remove register from mask void Remove(OptoReg::Name reg) { - assert(reg < CHUNK_SIZE, ""); - unsigned r = (unsigned)reg; - _rm_word[r >> LogBitsPerWord] &= ~(uintptr_t(1) << (r & WORD_BIT_MASK)); + reg = reg - offset_bits(); + assert(reg >= 0, "register outside mask"); + assert(reg < (int)rm_size_in_bits(), "register outside mask"); + unsigned int r = (unsigned int)reg; + rm_word(r >> LogBitsPerWord) &= ~(uintptr_t(1) << (r & WORD_BIT_MASK)); } // OR 'rm' into 'this' void OR(const RegMask &rm) { + assert(_offset == rm._offset, "offset mismatch"); assert(valid_watermarks() && rm.valid_watermarks(), "sanity"); + grow(rm._rm_size_in_words); // OR widens the live range if (_lwm > rm._lwm) _lwm = rm._lwm; if (_hwm < rm._hwm) _hwm = rm._hwm; - for (unsigned i = _lwm; i <= _hwm; i++) { - _rm_word[i] |= rm._rm_word[i]; + // Compute OR with all words from rm + for (unsigned int i = _lwm; i <= _hwm && i < rm._rm_size_in_words; i++) { + rm_word(i) |= rm.rm_word(i); } + // If rm is smaller than us and has the infinite_stack flag set, we need to set + // all bits in the gap to 1. + if (rm.is_infinite_stack() && rm._rm_size_in_words < _rm_size_in_words) { + set_range(rm._rm_size_in_words, 0xFF, _rm_size_in_words - rm._rm_size_in_words); + _hwm = rm_word_max_index(); + } + set_infinite_stack(is_infinite_stack() || rm.is_infinite_stack()); assert(valid_watermarks(), "sanity"); } // AND 'rm' into 'this' void AND(const RegMask &rm) { + assert(_offset == rm._offset, "offset mismatch"); assert(valid_watermarks() && rm.valid_watermarks(), "sanity"); - // Do not evaluate words outside the current watermark range, as they are - // already zero and an &= would not change that - for (unsigned i = _lwm; i <= _hwm; i++) { - _rm_word[i] &= rm._rm_word[i]; + grow(rm._rm_size_in_words); + // Compute AND with all words from rm. Do not evaluate words outside the + // current watermark range, as they are already zero and an &= would not + // change that + for (unsigned int i = _lwm; i <= _hwm && i < rm._rm_size_in_words; i++) { + rm_word(i) &= rm.rm_word(i); } - // Narrow the watermarks if &rm spans a narrower range. - // Update after to ensure non-overlapping words are zeroed out. - if (_lwm < rm._lwm) _lwm = rm._lwm; - if (_hwm > rm._hwm) _hwm = rm._hwm; + // If rm is smaller than our high watermark and has the infinite_stack flag not + // set, we need to set all bits in the gap to 0. + if (!rm.is_infinite_stack() && _hwm > rm.rm_word_max_index()) { + set_range(rm._rm_size_in_words, 0, _hwm - rm.rm_word_max_index()); + _hwm = rm.rm_word_max_index(); + } + // Narrow the watermarks if rm spans a narrower range. Update after to + // ensure non-overlapping words are zeroed out. If rm has the infinite_stack + // flag set and is smaller than our high watermark, take care not to + // incorrectly lower the high watermark according to rm. + if (_lwm < rm._lwm) { + _lwm = rm._lwm; + } + if (_hwm > rm._hwm && !(rm.is_infinite_stack() && _hwm > rm.rm_word_max_index())) { + _hwm = rm._hwm; + } + set_infinite_stack(is_infinite_stack() && rm.is_infinite_stack()); + assert(valid_watermarks(), "sanity"); } - // Subtract 'rm' from 'this' + // Subtract 'rm' from 'this'. void SUBTRACT(const RegMask &rm) { + assert(_offset == rm._offset, "offset mismatch"); assert(valid_watermarks() && rm.valid_watermarks(), "sanity"); - unsigned hwm = MIN2(_hwm, rm._hwm); - unsigned lwm = MAX2(_lwm, rm._lwm); - for (unsigned i = lwm; i <= hwm; i++) { - _rm_word[i] &= ~rm._rm_word[i]; + grow(rm._rm_size_in_words); + unsigned int hwm = MIN2(_hwm, rm._hwm); + unsigned int lwm = MAX2(_lwm, rm._lwm); + for (unsigned int i = lwm; i <= hwm; i++) { + rm_word(i) &= ~rm.rm_word(i); } + // If rm is smaller than our high watermark and has the infinite_stack flag set, + // we need to set all bits in the gap to 0. + if (rm.is_infinite_stack() && _hwm > rm.rm_word_max_index()) { + set_range(rm.rm_size_in_words(), 0, _hwm - rm.rm_word_max_index()); + _hwm = rm.rm_word_max_index(); + } + set_infinite_stack(is_infinite_stack() && !rm.is_infinite_stack()); + trim_watermarks(); + assert(valid_watermarks(), "sanity"); + } + + // Subtract 'rm' from 'this', but ignore everything in 'rm' that does not + // overlap with us and do not modify our infinite_stack flag. Supports masks of + // differing offsets. Does not support 'rm' with the infinite_stack flag set. + void SUBTRACT_inner(const RegMask& rm) { + assert(valid_watermarks() && rm.valid_watermarks(), "sanity"); + assert(!rm.is_infinite_stack(), "not supported"); + // Various translations due to differing offsets + int rm_index_diff = _offset - rm._offset; + int rm_hwm_tr = (int)rm._hwm - rm_index_diff; + int rm_lwm_tr = (int)rm._lwm - rm_index_diff; + int rm_rm_max_tr = (int)rm.rm_word_max_index() - rm_index_diff; + int rm_rm_size_tr = (int)rm._rm_size_in_words - rm_index_diff; + int hwm = MIN2((int)_hwm, rm_hwm_tr); + int lwm = MAX2((int)_lwm, rm_lwm_tr); + for (int i = lwm; i <= hwm; i++) { + assert(i + rm_index_diff < (int)rm._rm_size_in_words, "sanity"); + assert(i + rm_index_diff >= 0, "sanity"); + rm_word(i) &= ~rm.rm_word(i + rm_index_diff); + } + trim_watermarks(); + assert(valid_watermarks(), "sanity"); + } + + // Roll over the register mask. The main use is to expose a new set of stack + // slots for the register allocator. Return if the rollover succeeded or not. + bool rollover() { + assert(is_infinite_stack(), "rolling over non-empty mask"); + if (!OptoRegPair::can_fit((_rm_size_in_words + _offset + _rm_size_in_words) * BitsPerWord - 1)) { + // Ensure that register masks cannot roll over beyond the point at which + // OptoRegPair can no longer index the whole mask. + return false; + } + _offset += _rm_size_in_words; + Set_All_From_Offset(); + return true; } // Compute size of register mask: number of bits - uint Size() const; + uint Size() const { + uint sum = 0; + assert(valid_watermarks(), "sanity"); + for (unsigned i = _lwm; i <= _hwm; i++) { + sum += population_count(rm_word(i)); + } + return sum; + } #ifndef PRODUCT +private: + bool dump_end_run(outputStream* st, OptoReg::Name start, + OptoReg::Name last) const; + +public: + + // ---------------------------------------------------------------------- + // The methods below are only for testing purposes (see test_regmask.cpp) + // ---------------------------------------------------------------------- + + unsigned int static gtest_basic_rm_size_in_words() { + return RM_SIZE_IN_WORDS; + } + + unsigned int static gtest_rm_size_in_bits_max() { + return RM_SIZE_IN_WORDS_MAX * BitsPerWord; + } + + bool gtest_equals(const RegMask& rm) const { + assert(_offset == rm._offset, "offset mismatch"); + if (_infinite_stack != rm._infinite_stack) { + return false; + } + // Shared segment + for (unsigned int i = 0; i < MIN2(_rm_size_in_words, rm._rm_size_in_words); i++) { + if (rm_word(i) != rm.rm_word(i)) { + return false; + } + } + // If there is a size difference, check the protruding segment against + // infinite_stack. + const unsigned int start = MIN2(_rm_size_in_words, rm._rm_size_in_words); + const uintptr_t value = _infinite_stack ? uintptr_t(-1) : 0; + for (unsigned int i = start; i < _rm_size_in_words; i++) { + if (rm_word(i) != value) { + return false; + } + } + for (unsigned int i = start; i < rm._rm_size_in_words; i++) { + if (rm.rm_word(i) != value) { + return false; + } + } + return true; + } + + void gtest_set_offset(unsigned int offset) { + _offset = offset; + } + + // ---------------------- + // End of testing methods + // ---------------------- + void print() const { dump(); } void dump(outputStream *st = tty) const; // Print a mask + void dump_hex(outputStream* st = tty) const; // Print a mask (raw hex) #endif static const RegMask Empty; // Common empty mask static const RegMask All; // Common all mask - static bool can_represent(OptoReg::Name reg, unsigned int size = 1) { - // NOTE: MAX2(1U,size) in computation reflects the usage of the last - // bit of the regmask as an infinite stack flag. - return (int)reg < (int)(CHUNK_SIZE - MAX2(1U,size)); - } - static bool can_represent_arg(OptoReg::Name reg) { - // NOTE: SlotsPerVecZ in computation reflects the need - // to keep mask aligned for largest value (VecZ). - return can_represent(reg, SlotsPerVecZ); + bool can_represent(OptoReg::Name reg, unsigned int size = 1) const { + reg = reg - offset_bits(); + return reg >= 0 && reg <= (int)(rm_size_in_bits() - size); } }; @@ -419,7 +947,7 @@ class RegMaskIterator { // Find the next word with bits while (_next_index <= _rm._hwm) { - _current_bits = _rm._rm_word[_next_index++]; + _current_bits = _rm.rm_word(_next_index++); if (_current_bits != 0) { // Found a word. Calculate the first register element and // prepare _current_bits by shifting it down and clearing @@ -427,7 +955,9 @@ class RegMaskIterator { unsigned int next_bit = find_lowest_bit(_current_bits); assert(((_current_bits >> next_bit) & 0x1) == 1, "lowest bit must be set after shift"); _current_bits = (_current_bits >> next_bit) - 1; - _reg = OptoReg::Name(((_next_index - 1) << LogBitsPerWord) + next_bit); + _reg = OptoReg::Name(_rm.offset_bits() + + ((_next_index - 1) << LogBitsPerWord) + + next_bit); return r; } } @@ -438,7 +968,8 @@ class RegMaskIterator { } }; -// Do not use this constant directly in client code! +// Do not use these constants directly in client code! #undef RM_SIZE_IN_INTS +#undef RM_SIZE_IN_INTS_MIN #endif // SHARE_OPTO_REGMASK_HPP diff --git a/src/hotspot/share/utilities/globalDefinitions.hpp b/src/hotspot/share/utilities/globalDefinitions.hpp index 8217451a5c6..ea684368888 100644 --- a/src/hotspot/share/utilities/globalDefinitions.hpp +++ b/src/hotspot/share/utilities/globalDefinitions.hpp @@ -536,6 +536,7 @@ const intptr_t NULL_WORD = 0; // JVM spec restrictions const int max_method_code_size = 64*K - 1; // JVM spec, 2nd ed. section 4.8.1 (p.134) +const int max_method_parameter_length = 255; // JVM spec, 22nd ed. section 4.3.3 (p.83) //---------------------------------------------------------------------------------------------------- // old CDS options diff --git a/test/hotspot/gtest/opto/test_regmask.cpp b/test/hotspot/gtest/opto/test_regmask.cpp index 81fee5089c4..0975314c33d 100644 --- a/test/hotspot/gtest/opto/test_regmask.cpp +++ b/test/hotspot/gtest/opto/test_regmask.cpp @@ -22,19 +22,22 @@ * */ +#include "opto/chaitin.hpp" #include "opto/opcodes.hpp" #include "opto/regmask.hpp" #include "unittest.hpp" -// Sanity tests for RegMask and RegMaskIterator +// Sanity tests for RegMask and RegMaskIterator. The file tests operations on +// combinations of different RegMask versions ("basic", i.e. only statically +// allocated and "extended", i.e. extended with dynamically allocated memory). static void contains_expected_num_of_registers(const RegMask& rm, unsigned int expected) { ASSERT_TRUE(rm.Size() == expected); if (expected > 0) { - ASSERT_TRUE(rm.is_NotEmpty()); + ASSERT_TRUE(!rm.is_Empty()); } else { - ASSERT_TRUE(!rm.is_NotEmpty()); + ASSERT_TRUE(rm.is_Empty()); ASSERT_TRUE(!rm.is_infinite_stack()); } @@ -79,14 +82,14 @@ TEST_VM(RegMask, iteration) { } TEST_VM(RegMask, Set_ALL) { - // Check that Set_All doesn't add bits outside of CHUNK_SIZE + // Check that Set_All doesn't add bits outside of rm.rm_size_bits() RegMask rm; rm.Set_All(); - ASSERT_TRUE(rm.Size() == RegMask::CHUNK_SIZE); - ASSERT_TRUE(rm.is_NotEmpty()); - // Set_All sets the infinite bit + ASSERT_TRUE(rm.Size() == rm.rm_size_in_bits()); + ASSERT_TRUE(!rm.is_Empty()); + // Set_All sets infinite_stack ASSERT_TRUE(rm.is_infinite_stack()); - contains_expected_num_of_registers(rm, RegMask::CHUNK_SIZE); + contains_expected_num_of_registers(rm, rm.rm_size_in_bits()); } TEST_VM(RegMask, Clear) { @@ -132,34 +135,47 @@ TEST_VM(RegMask, SUBTRACT) { RegMask rm2; rm2.Set_All(); - for (int i = 17; i < RegMask::CHUNK_SIZE; i++) { + for (int i = 17; i < (int)rm1.rm_size_in_bits(); i++) { rm1.Insert(i); } + rm1.set_infinite_stack(true); ASSERT_TRUE(rm1.is_infinite_stack()); rm2.SUBTRACT(rm1); - contains_expected_num_of_registers(rm1, RegMask::CHUNK_SIZE - 17); + contains_expected_num_of_registers(rm1, rm1.rm_size_in_bits() - 17); + contains_expected_num_of_registers(rm2, 17); +} + +TEST_VM(RegMask, SUBTRACT_inner) { + RegMask rm1; + RegMask rm2; + rm2.Set_All(); + for (int i = 17; i < (int)rm1.rm_size_in_bits(); i++) { + rm1.Insert(i); + } + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm1, rm1.rm_size_in_bits() - 17); contains_expected_num_of_registers(rm2, 17); } TEST_VM(RegMask, is_bound1) { RegMask rm; ASSERT_FALSE(rm.is_bound1()); - for (int i = 0; i < RegMask::CHUNK_SIZE - 1; i++) { + for (int i = 0; i < (int)rm.rm_size_in_bits() - 1; i++) { rm.Insert(i); ASSERT_TRUE(rm.is_bound1()) << "Index " << i; ASSERT_TRUE(rm.is_bound(Op_RegI)) << "Index " << i; contains_expected_num_of_registers(rm, 1); rm.Remove(i); } - // The infinite bit does not count as a bound register - rm.set_infinite_stack(); + // infinite_stack does not count as a bound register + rm.set_infinite_stack(true); ASSERT_FALSE(rm.is_bound1()); } TEST_VM(RegMask, is_bound_pair) { RegMask rm; ASSERT_TRUE(rm.is_bound_pair()); - for (int i = 0; i < RegMask::CHUNK_SIZE - 2; i++) { + for (int i = 0; i < (int)rm.rm_size_in_bits() - 2; i++) { rm.Insert(i); rm.Insert(i + 1); ASSERT_TRUE(rm.is_bound_pair()) << "Index " << i; @@ -170,8 +186,9 @@ TEST_VM(RegMask, is_bound_pair) { } // A pair with the infinite bit does not count as a bound pair rm.Clear(); - rm.Insert(RegMask::CHUNK_SIZE - 2); - rm.Insert(RegMask::CHUNK_SIZE - 1); + rm.Insert(rm.rm_size_in_bits() - 2); + rm.Insert(rm.rm_size_in_bits() - 1); + rm.set_infinite_stack(true); ASSERT_FALSE(rm.is_bound_pair()); } @@ -179,7 +196,7 @@ TEST_VM(RegMask, is_bound_set) { RegMask rm; for (int size = 1; size <= 16; size++) { ASSERT_TRUE(rm.is_bound_set(size)); - for (int i = 0; i < RegMask::CHUNK_SIZE - size; i++) { + for (int i = 0; i < (int)rm.rm_size_in_bits() - size; i++) { for (int j = i; j < i + size; j++) { rm.Insert(j); } @@ -187,11 +204,1053 @@ TEST_VM(RegMask, is_bound_set) { contains_expected_num_of_registers(rm, size); rm.Clear(); } - // A set with the infinite bit does not count as a bound set - for (int j = RegMask::CHUNK_SIZE - size; j < RegMask::CHUNK_SIZE; j++) { - rm.Insert(j); + // A set with infinite_stack does not count as a bound set + for (int j = rm.rm_size_in_bits() - size; j < (int)rm.rm_size_in_bits(); j++) { + rm.Insert(j); } + rm.set_infinite_stack(true); ASSERT_FALSE(rm.is_bound_set(size)); rm.Clear(); } } + +TEST_VM(RegMask, external_member) { + RegMask rm; + rm.set_infinite_stack(false); + ASSERT_FALSE(rm.Member(OptoReg::Name(rm.rm_size_in_bits()))); + rm.set_infinite_stack(true); + ASSERT_TRUE(rm.Member(OptoReg::Name(rm.rm_size_in_bits()))); +} + +TEST_VM(RegMask, find_element) { + RegMask rm; + rm.Insert(OptoReg::Name(44)); + rm.Insert(OptoReg::Name(30)); + rm.Insert(OptoReg::Name(54)); + ASSERT_EQ(rm.find_first_elem(), OptoReg::Name(30)); + ASSERT_EQ(rm.find_last_elem(), OptoReg::Name(54)); + rm.set_infinite_stack(true); + ASSERT_EQ(rm.find_last_elem(), OptoReg::Name(54)); + rm.Clear(); + ASSERT_EQ(rm.find_first_elem(), OptoReg::Bad); + ASSERT_EQ(rm.find_last_elem(), OptoReg::Bad); +} + +TEST_VM(RegMask, find_first_set) { + RegMask rm; + LRG lrg; + lrg._is_scalable = 0; + lrg._is_vector = 0; + ASSERT_EQ(rm.find_first_set(lrg, 2), OptoReg::Bad); + rm.Insert(OptoReg::Name(24)); + rm.Insert(OptoReg::Name(25)); + rm.Insert(OptoReg::Name(26)); + rm.Insert(OptoReg::Name(27)); + rm.Insert(OptoReg::Name(16)); + rm.Insert(OptoReg::Name(17)); + rm.Insert(OptoReg::Name(18)); + rm.Insert(OptoReg::Name(19)); + ASSERT_EQ(rm.find_first_set(lrg, 4), OptoReg::Name(19)); +} + +TEST_VM(RegMask, alignment) { + RegMask rm; + rm.Insert(OptoReg::Name(30)); + rm.Insert(OptoReg::Name(31)); + ASSERT_TRUE(rm.is_aligned_sets(2)); + rm.Insert(OptoReg::Name(32)); + rm.Insert(OptoReg::Name(37)); + rm.Insert(OptoReg::Name(62)); + rm.Insert(OptoReg::Name(71)); + rm.Insert(OptoReg::Name(74)); + rm.Insert(OptoReg::Name(75)); + ASSERT_FALSE(rm.is_aligned_pairs()); + rm.clear_to_pairs(); + ASSERT_TRUE(rm.is_aligned_sets(2)); + ASSERT_TRUE(rm.is_aligned_pairs()); + contains_expected_num_of_registers(rm, 4); + ASSERT_TRUE(rm.Member(OptoReg::Name(30))); + ASSERT_TRUE(rm.Member(OptoReg::Name(31))); + ASSERT_TRUE(rm.Member(OptoReg::Name(74))); + ASSERT_TRUE(rm.Member(OptoReg::Name(75))); + ASSERT_FALSE(rm.is_misaligned_pair()); + rm.Remove(OptoReg::Name(30)); + rm.Remove(OptoReg::Name(74)); + ASSERT_TRUE(rm.is_misaligned_pair()); +} + +TEST_VM(RegMask, clear_to_sets) { + RegMask rm; + rm.Insert(OptoReg::Name(3)); + rm.Insert(OptoReg::Name(20)); + rm.Insert(OptoReg::Name(21)); + rm.Insert(OptoReg::Name(22)); + rm.Insert(OptoReg::Name(23)); + rm.Insert(OptoReg::Name(25)); + rm.Insert(OptoReg::Name(26)); + rm.Insert(OptoReg::Name(27)); + rm.Insert(OptoReg::Name(40)); + rm.Insert(OptoReg::Name(42)); + rm.Insert(OptoReg::Name(43)); + rm.Insert(OptoReg::Name(44)); + rm.Insert(OptoReg::Name(45)); + rm.clear_to_sets(2); + ASSERT_TRUE(rm.is_aligned_sets(2)); + contains_expected_num_of_registers(rm, 10); + rm.clear_to_sets(4); + ASSERT_TRUE(rm.is_aligned_sets(4)); + contains_expected_num_of_registers(rm, 4); + rm.clear_to_sets(8); + ASSERT_TRUE(rm.is_aligned_sets(8)); + contains_expected_num_of_registers(rm, 0); +} + +TEST_VM(RegMask, smear_to_sets) { + RegMask rm; + rm.Insert(OptoReg::Name(3)); + rm.smear_to_sets(2); + ASSERT_TRUE(rm.is_aligned_sets(2)); + contains_expected_num_of_registers(rm, 2); + rm.smear_to_sets(4); + ASSERT_TRUE(rm.is_aligned_sets(4)); + contains_expected_num_of_registers(rm, 4); + rm.smear_to_sets(8); + ASSERT_TRUE(rm.is_aligned_sets(8)); + contains_expected_num_of_registers(rm, 8); + rm.smear_to_sets(16); + ASSERT_TRUE(rm.is_aligned_sets(16)); + contains_expected_num_of_registers(rm, 16); +} + +TEST_VM(RegMask, overlap) { + RegMask rm1; + RegMask rm2; + ASSERT_FALSE(rm1.overlap(rm2)); + ASSERT_FALSE(rm2.overlap(rm1)); + rm1.Insert(OptoReg::Name(23)); + rm1.Insert(OptoReg::Name(2)); + rm1.Insert(OptoReg::Name(12)); + rm2.Insert(OptoReg::Name(1)); + rm2.Insert(OptoReg::Name(4)); + ASSERT_FALSE(rm1.overlap(rm2)); + ASSERT_FALSE(rm2.overlap(rm1)); + rm1.Insert(OptoReg::Name(4)); + ASSERT_TRUE(rm1.overlap(rm2)); + ASSERT_TRUE(rm2.overlap(rm1)); +} + +TEST_VM(RegMask, valid_reg) { + RegMask rm; + ASSERT_FALSE(rm.is_valid_reg(OptoReg::Name(42), 1)); + rm.Insert(OptoReg::Name(3)); + rm.Insert(OptoReg::Name(5)); + rm.Insert(OptoReg::Name(6)); + rm.Insert(OptoReg::Name(7)); + ASSERT_FALSE(rm.is_valid_reg(OptoReg::Name(7), 4)); + ASSERT_TRUE(rm.is_valid_reg(OptoReg::Name(7), 2)); +} + +TEST_VM(RegMask, rollover_and_insert_remove) { + RegMask rm; + OptoReg::Name reg1(rm.rm_size_in_bits() + 42); + OptoReg::Name reg2(rm.rm_size_in_bits() * 2 + 42); + rm.set_infinite_stack(true); + ASSERT_TRUE(rm.Member(reg1)); + rm.rollover(); + rm.Clear(); + rm.Insert(reg1); + ASSERT_TRUE(rm.Member(reg1)); + rm.Remove(reg1); + ASSERT_FALSE(rm.Member(reg1)); + rm.set_infinite_stack(true); + rm.rollover(); + rm.Clear(); + rm.Insert(reg2); + ASSERT_FALSE(rm.Member(reg1)); + ASSERT_TRUE(rm.Member(reg2)); +} + +TEST_VM(RegMask, rollover_and_find) { + RegMask rm; + OptoReg::Name reg1(rm.rm_size_in_bits() + 42); + OptoReg::Name reg2(rm.rm_size_in_bits() + 7); + rm.set_infinite_stack(true); + rm.rollover(); + rm.Clear(); + ASSERT_EQ(rm.find_first_elem(), OptoReg::Bad); + ASSERT_EQ(rm.find_last_elem(), OptoReg::Bad); + rm.Insert(reg1); + rm.Insert(reg2); + ASSERT_EQ(rm.find_first_elem(), reg2); + ASSERT_EQ(rm.find_last_elem(), reg1); +} + +TEST_VM(RegMask, rollover_and_find_first_set) { + LRG lrg; + lrg._is_scalable = 0; + lrg._is_vector = 0; + RegMask rm; + OptoReg::Name reg1(rm.rm_size_in_bits() + 24); + OptoReg::Name reg2(rm.rm_size_in_bits() + 25); + OptoReg::Name reg3(rm.rm_size_in_bits() + 26); + OptoReg::Name reg4(rm.rm_size_in_bits() + 27); + OptoReg::Name reg5(rm.rm_size_in_bits() + 16); + OptoReg::Name reg6(rm.rm_size_in_bits() + 17); + OptoReg::Name reg7(rm.rm_size_in_bits() + 18); + OptoReg::Name reg8(rm.rm_size_in_bits() + 19); + rm.set_infinite_stack(true); + rm.rollover(); + rm.Clear(); + ASSERT_EQ(rm.find_first_set(lrg, 2), OptoReg::Bad); + rm.Insert(reg1); + rm.Insert(reg2); + rm.Insert(reg3); + rm.Insert(reg4); + rm.Insert(reg5); + rm.Insert(reg6); + rm.Insert(reg7); + rm.Insert(reg8); + ASSERT_EQ(rm.find_first_set(lrg, 4), reg8); +} + +TEST_VM(RegMask, rollover_and_Set_All_From) { + RegMask rm; + OptoReg::Name reg1(rm.rm_size_in_bits() + 42); + rm.set_infinite_stack(true); + rm.rollover(); + rm.Clear(); + rm.Set_All_From(reg1); + contains_expected_num_of_registers(rm, rm.rm_size_in_bits() - 42); +} + +TEST_VM(RegMask, rollover_and_Set_All_From_Offset) { + RegMask rm; + rm.set_infinite_stack(true); + rm.rollover(); + rm.Clear(); + rm.Set_All_From_Offset(); + contains_expected_num_of_registers(rm, rm.rm_size_in_bits()); +} + +TEST_VM(RegMask, rollover_and_iterate) { + RegMask rm; + OptoReg::Name reg1(rm.rm_size_in_bits() + 2); + OptoReg::Name reg2(rm.rm_size_in_bits() + 6); + OptoReg::Name reg3(rm.rm_size_in_bits() + 17); + OptoReg::Name reg4(rm.rm_size_in_bits() + 43); + rm.set_infinite_stack(true); + rm.rollover(); + rm.Clear(); + rm.Insert(reg1); + rm.Insert(reg2); + rm.Insert(reg3); + rm.Insert(reg4); + RegMaskIterator rmi(rm); + ASSERT_EQ(rmi.next(), reg1); + ASSERT_EQ(rmi.next(), reg2); + ASSERT_EQ(rmi.next(), reg3); + ASSERT_EQ(rmi.next(), reg4); + ASSERT_FALSE(rmi.has_next()); +} + +TEST_VM(RegMask, rollover_and_SUBTRACT_inner_disjoint) { + RegMask rm1; + RegMask rm2; + OptoReg::Name reg1(rm1.rm_size_in_bits() + 42); + rm1.set_infinite_stack(true); + rm1.rollover(); + rm1.Clear(); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 0); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 0); + rm1.Insert(reg1); + rm2.Insert(42); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 1); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 1); +} + +TEST_VM(RegMask, rollover_and_SUBTRACT_inner_overlap) { + RegMask rm1; + RegMask rm2; + OptoReg::Name reg1(rm1.rm_size_in_bits() + 42); + rm1.set_infinite_stack(true); + rm1.rollover(); + rm1.Clear(); + rm2.set_infinite_stack(true); + rm2.rollover(); + rm2.Clear(); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 0); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 0); + rm1.Insert(reg1); + rm2.Insert(reg1); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 0); + rm1.Insert(reg1); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 0); +} + +#ifdef ASSERT + +TEST_VM_ASSERT_MSG(RegMask, unexpected_clone, ".*clone sanity check") { + RegMask rm1; + RegMask rm2; + // Copy contents of rm1 to rm2 inappropriately (no copy constructor) + memcpy((void*)&rm2, (void*)&rm1, sizeof(RegMask)); + rm2.Member(0); // Safeguard in RegMask must catch this. +} + +TEST_VM_ASSERT_MSG(RegMask, unexpected_growth, ".*unexpected register mask growth") { + RegMask rm; + // Add clearly out of range OptoReg::Name + rm.Insert(std::numeric_limits::max()); +} + +TEST_VM_ASSERT_MSG(RegMask, not_growable, ".*register mask not growable") { + RegMask rm; + // Add a bit just outside the mask, without having specified an arena for + // extension. + rm.Insert(rm.rm_size_in_bits()); +} + +TEST_VM_ASSERT_MSG(RegMask, offset_mismatch, ".*offset mismatch") { + RegMask rm1; + RegMask rm2; + rm1.set_infinite_stack(true); + rm1.rollover(); + // Cannot copy with different offsets + rm2 = rm1; +} + +#endif + +#ifndef PRODUCT + +Arena* arena() { + return Thread::current()->resource_area(); +} + +static void is_basic(const RegMask& rm) { + ASSERT_EQ(rm.rm_size_in_words(), RegMask::gtest_basic_rm_size_in_words()); +} + +static void is_extended(const RegMask& rm) { + ASSERT_TRUE(rm.rm_size_in_words() > RegMask::gtest_basic_rm_size_in_words()); +} + +static int first_extended() { + return RegMask::gtest_basic_rm_size_in_words() * BitsPerWord; +} + +static void extend(RegMask& rm, unsigned int n = 4) { + // Extend the given RegMask with at least n dynamically-allocated words. + rm.Insert(OptoReg::Name(first_extended() + (BitsPerWord * n) - 1)); + rm.Clear(); + ASSERT_TRUE(rm.rm_size_in_words() >= RegMask::gtest_basic_rm_size_in_words() + n); +} + +TEST_VM(RegMask, static_by_default) { + // Check that a freshly created RegMask does not allocate dynamic memory. + RegMask rm; + is_basic(rm); +} + +TEST_VM(RegMask, iteration_extended) { + RegMask rm(arena()); + rm.Insert(30); + rm.Insert(31); + rm.Insert(33); + rm.Insert(62); + rm.Insert(first_extended()); + rm.Insert(first_extended() + 42); + rm.Insert(first_extended() + 55); + rm.Insert(first_extended() + 456); + + RegMaskIterator rmi(rm); + ASSERT_TRUE(rmi.next() == OptoReg::Name(30)); + ASSERT_TRUE(rmi.next() == OptoReg::Name(31)); + ASSERT_TRUE(rmi.next() == OptoReg::Name(33)); + ASSERT_TRUE(rmi.next() == OptoReg::Name(62)); + ASSERT_TRUE(rmi.next() == OptoReg::Name(first_extended())); + ASSERT_TRUE(rmi.next() == OptoReg::Name(first_extended() + 42)); + ASSERT_TRUE(rmi.next() == OptoReg::Name(first_extended() + 55)); + ASSERT_TRUE(rmi.next() == OptoReg::Name(first_extended() + 456)); + ASSERT_FALSE(rmi.has_next()); +} + +TEST_VM(RegMask, Set_ALL_extended) { + // Check that Set_All doesn't add bits outside of rm.rm_size_bits() on + // extended RegMasks. + RegMask rm(arena()); + extend(rm); + rm.Set_All(); + ASSERT_EQ(rm.Size(), rm.rm_size_in_bits()); + ASSERT_TRUE(!rm.is_Empty()); + // Set_All sets infinite_stack bit + ASSERT_TRUE(rm.is_infinite_stack()); + contains_expected_num_of_registers(rm, rm.rm_size_in_bits()); +} + +TEST_VM(RegMask, Set_ALL_From_extended) { + RegMask rm(arena()); + extend(rm); + rm.Set_All_From(OptoReg::Name(42)); + contains_expected_num_of_registers(rm, rm.rm_size_in_bits() - 42); +} + +TEST_VM(RegMask, Set_ALL_From_extended_grow) { + RegMask rm(arena()); + rm.Set_All_From(first_extended() + OptoReg::Name(42)); + is_extended(rm); + contains_expected_num_of_registers(rm, rm.rm_size_in_bits() - first_extended() - 42); +} + +TEST_VM(RegMask, Clear_extended) { + // Check that Clear doesn't leave any stray bits on extended RegMasks. + RegMask rm(arena()); + rm.Insert(first_extended()); + is_extended(rm); + rm.Set_All(); + rm.Clear(); + contains_expected_num_of_registers(rm, 0); +} + +TEST_VM(RegMask, AND_extended_basic) { + RegMask rm1(arena()); + rm1.Insert(OptoReg::Name(first_extended())); + is_extended(rm1); + contains_expected_num_of_registers(rm1, 1); + ASSERT_TRUE(rm1.Member(OptoReg::Name(first_extended()))); + + rm1.AND(rm1); + contains_expected_num_of_registers(rm1, 1); + + RegMask rm2; + is_basic(rm2); + rm1.AND(rm2); + contains_expected_num_of_registers(rm1, 0); + contains_expected_num_of_registers(rm2, 0); +} + +TEST_VM(RegMask, AND_extended_extended) { + RegMask rm1(arena()); + rm1.Insert(OptoReg::Name(first_extended())); + is_extended(rm1); + contains_expected_num_of_registers(rm1, 1); + ASSERT_TRUE(rm1.Member(OptoReg::Name(first_extended()))); + + rm1.AND(rm1); + contains_expected_num_of_registers(rm1, 1); + + RegMask rm2(arena()); + extend(rm2); + rm1.AND(rm2); + contains_expected_num_of_registers(rm1, 0); + contains_expected_num_of_registers(rm2, 0); +} + +TEST_VM(RegMask, OR_extended_basic) { + RegMask rm1(arena()); + rm1.Insert(OptoReg::Name(first_extended())); + is_extended(rm1); + contains_expected_num_of_registers(rm1, 1); + ASSERT_TRUE(rm1.Member(OptoReg::Name(first_extended()))); + + rm1.OR(rm1); + contains_expected_num_of_registers(rm1, 1); + + RegMask rm2; + is_basic(rm2); + rm1.OR(rm2); + contains_expected_num_of_registers(rm1, 1); + contains_expected_num_of_registers(rm2, 0); +} + +TEST_VM(RegMask, OR_extended_extended) { + RegMask rm1(arena()); + rm1.Insert(OptoReg::Name(first_extended())); + is_extended(rm1); + contains_expected_num_of_registers(rm1, 1); + ASSERT_TRUE(rm1.Member(OptoReg::Name(first_extended()))); + + rm1.OR(rm1); + contains_expected_num_of_registers(rm1, 1); + + RegMask rm2(arena()); + extend(rm2); + rm1.OR(rm2); + contains_expected_num_of_registers(rm1, 1); + contains_expected_num_of_registers(rm2, 0); +} + +TEST_VM(RegMask, SUBTRACT_extended) { + RegMask rm1(arena()); + extend(rm1); + RegMask rm2(arena()); + extend(rm2); + + rm2.Set_All(); + ASSERT_TRUE(rm2.is_infinite_stack()); + for (int i = first_extended() + 17; i < (int)rm1.rm_size_in_bits(); i++) { + rm1.Insert(i); + } + rm1.set_infinite_stack(true); + ASSERT_TRUE(rm1.is_infinite_stack()); + rm2.SUBTRACT(rm1); + contains_expected_num_of_registers(rm1, rm1.rm_size_in_bits() - first_extended() - 17); + contains_expected_num_of_registers(rm2, first_extended() + 17); +} + +TEST_VM(RegMask, external_member_extended) { + RegMask rm(arena()); + extend(rm); + rm.set_infinite_stack(false); + ASSERT_FALSE(rm.Member(OptoReg::Name(rm.rm_size_in_bits()))); + rm.set_infinite_stack(true); + ASSERT_TRUE(rm.Member(OptoReg::Name(rm.rm_size_in_bits()))); +} + +TEST_VM(RegMask, overlap_extended) { + RegMask rm1(arena()); + extend(rm1); + RegMask rm2(arena()); + extend(rm2); + ASSERT_FALSE(rm1.overlap(rm2)); + ASSERT_FALSE(rm2.overlap(rm1)); + rm1.Insert(OptoReg::Name(23)); + rm1.Insert(OptoReg::Name(2)); + rm1.Insert(OptoReg::Name(first_extended() + 12)); + rm2.Insert(OptoReg::Name(1)); + rm2.Insert(OptoReg::Name(first_extended() + 4)); + ASSERT_FALSE(rm1.overlap(rm2)); + ASSERT_FALSE(rm2.overlap(rm1)); + rm1.Insert(OptoReg::Name(first_extended() + 4)); + ASSERT_TRUE(rm1.overlap(rm2)); + ASSERT_TRUE(rm2.overlap(rm1)); +} + +TEST_VM(RegMask, up_extended) { + RegMask rm(arena()); + extend(rm); + ASSERT_TRUE(rm.is_UP()); + rm.Insert(OptoReg::Name(1)); + ASSERT_TRUE(rm.is_UP()); + rm.Insert(OptoReg::Name(first_extended())); + ASSERT_FALSE(rm.is_UP()); + rm.Clear(); + rm.set_infinite_stack(true); + ASSERT_FALSE(rm.is_UP()); +} + +TEST_VM(RegMask, SUBTRACT_inner_basic_extended) { + RegMask rm1; + RegMask rm2(arena()); + rm1.Insert(OptoReg::Name(1)); + rm1.Insert(OptoReg::Name(42)); + is_basic(rm1); + rm2.Insert(OptoReg::Name(1)); + rm2.Insert(OptoReg::Name(first_extended() + 20)); + is_extended(rm2); + rm1.SUBTRACT_inner(rm2); + is_basic(rm1); + contains_expected_num_of_registers(rm1, 1); + ASSERT_TRUE(rm1.Member(OptoReg::Name(42))); +} + +TEST_VM(RegMask, SUBTRACT_inner_extended_basic) { + RegMask rm1(arena()); + RegMask rm2; + rm1.Insert(OptoReg::Name(1)); + rm1.Insert(OptoReg::Name(42)); + rm1.Insert(OptoReg::Name(first_extended() + 20)); + is_extended(rm1); + rm2.Insert(OptoReg::Name(1)); + is_basic(rm2); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 2); + ASSERT_TRUE(rm1.Member(OptoReg::Name(42))); + ASSERT_TRUE(rm1.Member(OptoReg::Name(first_extended() + 20))); +} + +TEST_VM(RegMask, rollover_extended) { + RegMask rm(arena()); + extend(rm); + is_extended(rm); + OptoReg::Name reg1(rm.rm_size_in_bits() + 42); + rm.set_infinite_stack(true); + rm.rollover(); + rm.Insert(reg1); + ASSERT_TRUE(rm.Member(reg1)); +} + +TEST_VM(RegMask, rollover_and_SUBTRACT_inner_disjoint_extended) { + RegMask rm1(arena()); + RegMask rm2; + extend(rm1); + OptoReg::Name reg1(rm1.rm_size_in_bits() + 42); + rm1.set_infinite_stack(true); + rm1.rollover(); + rm1.Clear(); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 0); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 0); + rm1.Insert(reg1); + rm2.Insert(42); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 1); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 1); +} + +TEST_VM(RegMask, rollover_and_SUBTRACT_inner_overlap_extended) { + RegMask rm1(arena()); + RegMask rm2; + OptoReg::Name reg1(rm1.rm_size_in_bits() + 42); + extend(rm1); + rm2.set_infinite_stack(true); + rm2.rollover(); + rm2.Clear(); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 0); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 0); + rm1.Insert(reg1); + rm2.Insert(reg1); + rm1.SUBTRACT_inner(rm2); + contains_expected_num_of_registers(rm1, 0); + rm1.Insert(reg1); + rm2.SUBTRACT_inner(rm1); + contains_expected_num_of_registers(rm2, 0); +} + +const uint iterations = 50000; + +static uint r; +static uint next_random() { + r = os::next_random(r); + return r; +} +static void init_random() { + if (StressSeed == 0) { + r = static_cast(Ticks::now().nanoseconds()); + tty->print_cr("seed: %u", r); + } else { + r = StressSeed; + } +} + +static void print(const char* name, const RegMask& mask) { + tty->print("%s: ", name); + mask.print(); + tty->print_cr(", size: %u, offset: %u, infinite_stack: %u", mask.rm_size_in_bits(), + mask.offset_bits(), mask.is_infinite_stack()); +} + +static void assert_equivalent(const RegMask& mask, + const ResourceBitMap& mask_ref, + bool infinite_stack_ref) { + ASSERT_EQ(mask_ref.count_one_bits(), mask.Size()); + RegMaskIterator it(mask); + OptoReg::Name reg = OptoReg::Bad; + while (it.has_next()) { + reg = it.next(); + ASSERT_TRUE(OptoReg::is_valid(reg)); + ASSERT_TRUE(mask_ref.at(reg)); + } + ASSERT_EQ(infinite_stack_ref, mask.is_infinite_stack()); +} + +static void populate_auxiliary_sets(RegMask& mask_aux, + ResourceBitMap& mask_aux_ref, + uint reg_capacity, uint offset, + bool random_offset) { + mask_aux.Clear(); + mask_aux_ref.clear(); + if (random_offset) { + uint offset_in_words = offset / BitsPerWord; + uint capacity_in_words = reg_capacity / BitsPerWord; + uint new_offset_in_words; + uint offset_target = next_random() % 3; + switch (offset_target) { + case 0: // before + if (offset_in_words == 0) { + new_offset_in_words = 0; + } else { + new_offset_in_words = next_random() % offset_in_words; + } + break; + case 1: // within + new_offset_in_words = + (next_random() % capacity_in_words) + offset_in_words; + break; + case 2: // after + new_offset_in_words = offset_in_words + capacity_in_words + + (next_random() % (capacity_in_words)); + break; + default: + FAIL(); + } + offset = new_offset_in_words * BitsPerWord; + if (offset + RegMask::gtest_rm_size_in_bits_max() > mask_aux_ref.size()) { + // Ensure that there is space in the reference mask. + offset = 0; + } + } + mask_aux.gtest_set_offset(offset / BitsPerWord); + assert_equivalent(mask_aux, mask_aux_ref, false); + uint max_size; + uint size_target = next_random() % 3; + switch (size_target) { + case 0: // smaller + max_size = reg_capacity / 2; + break; + case 1: // equal + max_size = reg_capacity; + break; + case 2: // larger (if possible) + max_size = RegMask::gtest_rm_size_in_bits_max(); + break; + default: + FAIL(); + } + uint regs; + uint regs_target = next_random() % 3; + switch (regs_target) { + case 0: // sparse + regs = next_random() % 8; + break; + case 1: // medium + regs = next_random() % (max_size / 8); + break; + case 2: // dense + regs = next_random() % max_size; + break; + default: + FAIL(); + } + for (uint i = 0; i < regs; i++) { + uint reg = (next_random() % max_size) + offset; + mask_aux.Insert(reg); + mask_aux_ref.set_bit(reg); + } + mask_aux.set_infinite_stack(next_random() % 2); + assert_equivalent(mask_aux, mask_aux_ref, mask_aux.is_infinite_stack()); + + if (Verbose) { + print("mask_aux", mask_aux); + } +} + +static void stack_extend_ref_masks(ResourceBitMap& mask1, bool infinite_stack1, + uint size_bits1, uint offset1, + ResourceBitMap& mask2, bool infinite_stack2, + uint size_bits2, uint offset2) { + uint size_bits_after = MAX2(size_bits1, size_bits2); + if (infinite_stack1) { + mask1.set_range(size_bits1 + offset1, size_bits_after + offset1); + } + if (infinite_stack2) { + mask2.set_range(size_bits2 + offset2, size_bits_after + offset2); + } +} + +TEST_VM(RegMask, random) { + ResourceMark rm; + RegMask mask(arena()); + ResourceBitMap mask_ref(std::numeric_limits::max() + 1); + bool infinite_stack_ref = false; + uint offset_ref = 0; + init_random(); + + for (uint i = 0; i < iterations; i++) { + if (Verbose) { + print("mask ", mask); + tty->print("%u. ", i); + } + uint action = next_random() % 13; + uint reg; + uint size_bits_before = mask.rm_size_in_bits(); + // This copy is used for stack-extension in overlap. + ResourceBitMap mask_ref_copy(std::numeric_limits::max() + 1); + mask_ref_copy.clear(); + mask_ref.iterate([&](BitMap::idx_t index) { + mask_ref_copy.set_bit(index); + return true; + }); + ResourceBitMap mask_aux_ref(std::numeric_limits::max() + 1); + RegMask mask_aux(arena()); + switch (action) { + case 0: + reg = (next_random() % RegMask::gtest_rm_size_in_bits_max()) + offset_ref; + if (Verbose) { + tty->print_cr("action: Insert"); + tty->print("value : "); + OptoReg::dump(reg); + tty->cr(); + } + mask.Insert(reg); + mask_ref.set_bit(reg); + if (mask.is_infinite_stack() && reg >= size_bits_before) { + // Stack-extend reference bitset. + mask_ref.set_range(size_bits_before + offset_ref, + mask.rm_size_in_bits() + offset_ref); + } + break; + case 1: + reg = (next_random() % size_bits_before) + offset_ref; + if (Verbose) { + tty->print_cr("action: Remove"); + tty->print("value : "); + OptoReg::dump(reg); + tty->cr(); + } + mask.Remove(reg); + mask_ref.clear_bit(reg); + break; + case 2: + if (Verbose) { + tty->print_cr("action: Clear"); + } + mask.Clear(); + mask_ref.clear(); + infinite_stack_ref = false; + break; + case 3: + if (offset_ref > 0) { + // Set_All expects a zero-offset. + break; + } + if (Verbose) { + tty->print_cr("action: Set_All"); + } + mask.Set_All(); + mask_ref.set_range(0, size_bits_before); + infinite_stack_ref = true; + break; + case 4: + if (Verbose) { + tty->print_cr("action: AND"); + } + populate_auxiliary_sets(mask_aux, mask_aux_ref, mask.rm_size_in_bits(), + offset_ref, /*random_offset*/ false); + mask.AND(mask_aux); + stack_extend_ref_masks(mask_ref, infinite_stack_ref, size_bits_before, + offset_ref, mask_aux_ref, mask_aux.is_infinite_stack(), + mask_aux.rm_size_in_bits(), mask_aux.offset_bits()); + mask_ref.set_intersection(mask_aux_ref); + infinite_stack_ref = infinite_stack_ref && mask_aux.is_infinite_stack(); + break; + case 5: + if (Verbose) { + tty->print_cr("action: OR"); + } + populate_auxiliary_sets(mask_aux, mask_aux_ref, mask.rm_size_in_bits(), + offset_ref, /*random_offset*/ false); + mask.OR(mask_aux); + stack_extend_ref_masks(mask_ref, infinite_stack_ref, size_bits_before, + offset_ref, mask_aux_ref, mask_aux.is_infinite_stack(), + mask_aux.rm_size_in_bits(), mask_aux.offset_bits()); + mask_ref.set_union(mask_aux_ref); + infinite_stack_ref = infinite_stack_ref || mask_aux.is_infinite_stack(); + break; + case 6: + if (Verbose) { + tty->print_cr("action: SUBTRACT"); + } + populate_auxiliary_sets(mask_aux, mask_aux_ref, mask.rm_size_in_bits(), + offset_ref, /*random_offset*/ false); + mask.SUBTRACT(mask_aux); + stack_extend_ref_masks(mask_ref, infinite_stack_ref, size_bits_before, + offset_ref, mask_aux_ref, mask_aux.is_infinite_stack(), + mask_aux.rm_size_in_bits(), mask_aux.offset_bits()); + mask_ref.set_difference(mask_aux_ref); + if (mask_aux.is_infinite_stack()) { + infinite_stack_ref = false; + } + break; + case 7: + if (Verbose) { + tty->print_cr("action: SUBTRACT_inner"); + } + populate_auxiliary_sets(mask_aux, mask_aux_ref, mask.rm_size_in_bits(), + offset_ref, /*random_offset*/ true); + // SUBTRACT_inner expects an argument register mask with infinite_stack = + // false. + mask_aux.set_infinite_stack(false); + mask.SUBTRACT_inner(mask_aux); + // SUBTRACT_inner does not have "stack-extension semantics". + mask_ref.set_difference(mask_aux_ref); + break; + case 8: + if (Verbose) { + tty->print_cr("action: overlap"); + } + populate_auxiliary_sets(mask_aux, mask_aux_ref, mask.rm_size_in_bits(), + offset_ref, /*random_offset*/ false); + // Stack-extend a copy of mask_ref to avoid mutating the original. + stack_extend_ref_masks(mask_ref_copy, infinite_stack_ref, size_bits_before, + offset_ref, mask_aux_ref, mask_aux.is_infinite_stack(), + mask_aux.rm_size_in_bits(), mask_aux.offset_bits()); + ASSERT_EQ(mask_ref_copy.intersects(mask_aux_ref) || + (infinite_stack_ref && mask_aux.is_infinite_stack()), + mask.overlap(mask_aux)); + break; + case 9: + if (Verbose) { + tty->print_cr("action: rollover"); + } + // rollover expects the mask to be cleared and with infinite_stack = true + mask.Clear(); + mask.set_infinite_stack(true); + mask_ref.clear(); + infinite_stack_ref = true; + if (mask.rollover()) { + offset_ref += size_bits_before; + mask_ref.set_range(offset_ref, offset_ref + size_bits_before); + } + break; + case 10: + if (Verbose) { + tty->print_cr("action: reset"); + } + mask.gtest_set_offset(0); + mask.Clear(); + mask_ref.clear(); + infinite_stack_ref = false; + offset_ref = 0; + break; + case 11: + if (Verbose) { + tty->print_cr("action: Set_All_From_Offset"); + } + mask.Set_All_From_Offset(); + mask_ref.set_range(offset_ref, offset_ref + size_bits_before); + infinite_stack_ref = true; + break; + case 12: + reg = (next_random() % size_bits_before) + offset_ref; + if (Verbose) { + tty->print_cr("action: Set_All_From"); + tty->print("value : "); + OptoReg::dump(reg); + tty->cr(); + } + mask.Set_All_From(reg); + mask_ref.set_range(reg, offset_ref + size_bits_before); + infinite_stack_ref = true; + break; + default: + FAIL() << "Unimplemented action"; + } + ASSERT_NO_FATAL_FAILURE(assert_equivalent(mask, mask_ref, infinite_stack_ref)); + } +} + +// Randomly sets register mask contents. Does not change register mask size. +static void randomize(RegMask& rm) { + rm.Clear(); + // Uniform distribution over number of registers. + uint regs = next_random() % (rm.rm_size_in_bits() + 1); + for (uint i = 0; i < regs; i++) { + uint reg = (next_random() % rm.rm_size_in_bits()) + rm.offset_bits(); + rm.Insert(reg); + } + rm.set_infinite_stack(next_random() % 2); +} + +static uint grow_randomly(RegMask& rm, uint min_growth = 1, + uint max_growth = 3) { + // Grow between min_growth and max_growth times. + uint grow = min_growth + (max_growth > 0 ? next_random() % max_growth : 0); + for (uint i = 0; i < grow; ++i) { + uint reg = rm.rm_size_in_bits(); + if (reg >= RegMask::gtest_rm_size_in_bits_max()) { + // Cannot grow more + break; + } + // Force grow + rm.Insert(reg); + if (!rm.is_infinite_stack()) { + // Restore + rm.Remove(reg); + } + } + // Return how many times we grew + return grow; +} + +TEST_VM(RegMask, random_copy) { + init_random(); + + auto print_failure = [&](const RegMask& src, const RegMask& dst) { + tty->print_cr("Failure, src and dst not equal"); + tty->print("src: "); + src.dump_hex(); + tty->cr(); + tty->print("dst: "); + dst.dump_hex(); + tty->cr(); + }; + + // Test copying a larger register mask + for (uint i = 0; i < iterations; i++) { + ResourceMark rm; + + // Create source RegMask + RegMask src(arena()); + + // Grow source randomly + grow_randomly(src); + + // Randomly initialize source + randomize(src); + + // Copy construct source to destination + RegMask dst(src, arena()); + + // Check equality + bool passed = src.gtest_equals(dst); + if (Verbose && !passed) { + print_failure(src, dst); + } + ASSERT_TRUE(passed); + } + + // Test copying a smaller register mask + for (uint i = 0; i < iterations; i++) { + ResourceMark rm; + + // Create destination RegMask + RegMask dst(arena()); + + // Grow destination arbitrarily (1-3 times) + uint growth = grow_randomly(dst, 1, 3); + + // Create source RegMask + RegMask src(arena()); + + // Grow source arbitrarily, but not as much as destination + grow_randomly(src, 0, growth - 1); + + // Randomly initialize source + randomize(src); + + // Copy source to destination + dst = src; + + // Check equality + bool passed = src.gtest_equals(dst); + if (Verbose && !passed) { + print_failure(src, dst); + } + ASSERT_TRUE(passed); + } +} + +#endif // !PRODUCT diff --git a/test/hotspot/jtreg/compiler/arguments/TestMaxMethodArguments.java b/test/hotspot/jtreg/compiler/arguments/TestMaxMethodArguments.java new file mode 100644 index 00000000000..fa3086254d3 --- /dev/null +++ b/test/hotspot/jtreg/compiler/arguments/TestMaxMethodArguments.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8325467 + * @requires vm.opt.DeoptimizeALot == null | vm.opt.DeoptimizeALot == false + * @summary Ensure C2 can compile methods with the maximum number of parameters + * (according to the JVM spec). + * @run main/othervm -XX:CompileCommand=compileonly,compiler.arguments.TestMaxMethodArguments::test + * -Xcomp + * -XX:+UnlockDiagnosticVMOptions + * -XX:+AbortVMOnCompilationFailure + * compiler.arguments.TestMaxMethodArguments + */ + +/** + * @test + * @bug 8325467 + * @summary Same test as above but do not enforce that compilation succeeds + * (first run) or that the test method is compiled at all (second + * run). + * @run main/othervm -XX:CompileCommand=compileonly,compiler.arguments.TestMaxMethodArguments::test + * -Xcomp compiler.arguments.TestMaxMethodArguments + * @run main compiler.arguments.TestMaxMethodArguments + */ + +package compiler.arguments; + +public class TestMaxMethodArguments { + + static class TestException extends Exception {} + + public static void main(String[] args) { + try { + test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255); + } catch (TestException e) { + // Fine + } + } + + public static int test(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, int x9, int x10, int x11, int x12, int x13, int x14, int x15, int x16, int x17, int x18, int x19, int x20, int x21, int x22, int x23, int x24, int x25, int x26, int x27, int x28, int x29, int x30, int x31, int x32, int x33, int x34, int x35, int x36, int x37, int x38, int x39, int x40, int x41, int x42, int x43, int x44, int x45, int x46, int x47, int x48, int x49, int x50, int x51, int x52, int x53, int x54, int x55, int x56, int x57, int x58, int x59, int x60, int x61, int x62, int x63, int x64, int x65, int x66, int x67, int x68, int x69, int x70, int x71, int x72, int x73, int x74, int x75, int x76, int x77, int x78, int x79, int x80, int x81, int x82, int x83, int x84, int x85, int x86, int x87, int x88, int x89, int x90, int x91, int x92, int x93, int x94, int x95, int x96, int x97, int x98, int x99, int x100, int x101, int x102, int x103, int x104, int x105, int x106, int x107, int x108, int x109, int x110, int x111, int x112, int x113, int x114, int x115, int x116, int x117, int x118, int x119, int x120, int x121, int x122, int x123, int x124, int x125, int x126, int x127, int x128, int x129, int x130, int x131, int x132, int x133, int x134, int x135, int x136, int x137, int x138, int x139, int x140, int x141, int x142, int x143, int x144, int x145, int x146, int x147, int x148, int x149, int x150, int x151, int x152, int x153, int x154, int x155, int x156, int x157, int x158, int x159, int x160, int x161, int x162, int x163, int x164, int x165, int x166, int x167, int x168, int x169, int x170, int x171, int x172, int x173, int x174, int x175, int x176, int x177, int x178, int x179, int x180, int x181, int x182, int x183, int x184, int x185, int x186, int x187, int x188, int x189, int x190, int x191, int x192, int x193, int x194, int x195, int x196, int x197, int x198, int x199, int x200, int x201, int x202, int x203, int x204, int x205, int x206, int x207, int x208, int x209, int x210, int x211, int x212, int x213, int x214, int x215, int x216, int x217, int x218, int x219, int x220, int x221, int x222, int x223, int x224, int x225, int x226, int x227, int x228, int x229, int x230, int x231, int x232, int x233, int x234, int x235, int x236, int x237, int x238, int x239, int x240, int x241, int x242, int x243, int x244, int x245, int x246, int x247, int x248, int x249, int x250, int x251, int x252, int x253, int x254, int x255) throws TestException { + // Exceptions after every definition of a temporary force the + // evaluation order. + int t1 = x1 % 101; if(t1 == 0) { throw new TestException(); } + int t2 = x2 % 102; if(t2 == 0) { throw new TestException(); } + int t3 = x3 % 103; if(t3 == 0) { throw new TestException(); } + int t4 = x4 % 104; if(t4 == 0) { throw new TestException(); } + int t5 = x5 % 105; if(t5 == 0) { throw new TestException(); } + int t6 = x6 % 106; if(t6 == 0) { throw new TestException(); } + int t7 = x7 % 107; if(t7 == 0) { throw new TestException(); } + int t8 = x8 % 108; if(t8 == 0) { throw new TestException(); } + int t9 = x9 % 109; if(t9 == 0) { throw new TestException(); } + int t10 = x10 % 110; if(t10 == 0) { throw new TestException(); } + int t11 = x11 % 111; if(t11 == 0) { throw new TestException(); } + int t12 = x12 % 112; if(t12 == 0) { throw new TestException(); } + int t13 = x13 % 113; if(t13 == 0) { throw new TestException(); } + int t14 = x14 % 114; if(t14 == 0) { throw new TestException(); } + int t15 = x15 % 115; if(t15 == 0) { throw new TestException(); } + int t16 = x16 % 116; if(t16 == 0) { throw new TestException(); } + int t17 = x17 % 117; if(t17 == 0) { throw new TestException(); } + int t18 = x18 % 118; if(t18 == 0) { throw new TestException(); } + int t19 = x19 % 119; if(t19 == 0) { throw new TestException(); } + int t20 = x20 % 120; if(t20 == 0) { throw new TestException(); } + int t21 = x21 % 121; if(t21 == 0) { throw new TestException(); } + int t22 = x22 % 122; if(t22 == 0) { throw new TestException(); } + int t23 = x23 % 123; if(t23 == 0) { throw new TestException(); } + int t24 = x24 % 124; if(t24 == 0) { throw new TestException(); } + int t25 = x25 % 125; if(t25 == 0) { throw new TestException(); } + int t26 = x26 % 126; if(t26 == 0) { throw new TestException(); } + int t27 = x27 % 127; if(t27 == 0) { throw new TestException(); } + int t28 = x28 % 128; if(t28 == 0) { throw new TestException(); } + int t29 = x29 % 129; if(t29 == 0) { throw new TestException(); } + int t30 = x30 % 130; if(t30 == 0) { throw new TestException(); } + int t31 = x31 % 131; if(t31 == 0) { throw new TestException(); } + int t32 = x32 % 132; if(t32 == 0) { throw new TestException(); } + int t33 = x33 % 133; if(t33 == 0) { throw new TestException(); } + int t34 = x34 % 134; if(t34 == 0) { throw new TestException(); } + int t35 = x35 % 135; if(t35 == 0) { throw new TestException(); } + int t36 = x36 % 136; if(t36 == 0) { throw new TestException(); } + int t37 = x37 % 137; if(t37 == 0) { throw new TestException(); } + int t38 = x38 % 138; if(t38 == 0) { throw new TestException(); } + int t39 = x39 % 139; if(t39 == 0) { throw new TestException(); } + int t40 = x40 % 140; if(t40 == 0) { throw new TestException(); } + int t41 = x41 % 141; if(t41 == 0) { throw new TestException(); } + int t42 = x42 % 142; if(t42 == 0) { throw new TestException(); } + int t43 = x43 % 143; if(t43 == 0) { throw new TestException(); } + int t44 = x44 % 144; if(t44 == 0) { throw new TestException(); } + int t45 = x45 % 145; if(t45 == 0) { throw new TestException(); } + int t46 = x46 % 146; if(t46 == 0) { throw new TestException(); } + int t47 = x47 % 147; if(t47 == 0) { throw new TestException(); } + int t48 = x48 % 148; if(t48 == 0) { throw new TestException(); } + int t49 = x49 % 149; if(t49 == 0) { throw new TestException(); } + int t50 = x50 % 150; if(t50 == 0) { throw new TestException(); } + int t51 = x51 % 151; if(t51 == 0) { throw new TestException(); } + int t52 = x52 % 152; if(t52 == 0) { throw new TestException(); } + int t53 = x53 % 153; if(t53 == 0) { throw new TestException(); } + int t54 = x54 % 154; if(t54 == 0) { throw new TestException(); } + int t55 = x55 % 155; if(t55 == 0) { throw new TestException(); } + int t56 = x56 % 156; if(t56 == 0) { throw new TestException(); } + int t57 = x57 % 157; if(t57 == 0) { throw new TestException(); } + int t58 = x58 % 158; if(t58 == 0) { throw new TestException(); } + int t59 = x59 % 159; if(t59 == 0) { throw new TestException(); } + int t60 = x60 % 160; if(t60 == 0) { throw new TestException(); } + int t61 = x61 % 161; if(t61 == 0) { throw new TestException(); } + int t62 = x62 % 162; if(t62 == 0) { throw new TestException(); } + int t63 = x63 % 163; if(t63 == 0) { throw new TestException(); } + int t64 = x64 % 164; if(t64 == 0) { throw new TestException(); } + int t65 = x65 % 165; if(t65 == 0) { throw new TestException(); } + int t66 = x66 % 166; if(t66 == 0) { throw new TestException(); } + int t67 = x67 % 167; if(t67 == 0) { throw new TestException(); } + int t68 = x68 % 168; if(t68 == 0) { throw new TestException(); } + int t69 = x69 % 169; if(t69 == 0) { throw new TestException(); } + int t70 = x70 % 170; if(t70 == 0) { throw new TestException(); } + int t71 = x71 % 171; if(t71 == 0) { throw new TestException(); } + int t72 = x72 % 172; if(t72 == 0) { throw new TestException(); } + int t73 = x73 % 173; if(t73 == 0) { throw new TestException(); } + int t74 = x74 % 174; if(t74 == 0) { throw new TestException(); } + int t75 = x75 % 175; if(t75 == 0) { throw new TestException(); } + int t76 = x76 % 176; if(t76 == 0) { throw new TestException(); } + int t77 = x77 % 177; if(t77 == 0) { throw new TestException(); } + int t78 = x78 % 178; if(t78 == 0) { throw new TestException(); } + int t79 = x79 % 179; if(t79 == 0) { throw new TestException(); } + int t80 = x80 % 180; if(t80 == 0) { throw new TestException(); } + int t81 = x81 % 181; if(t81 == 0) { throw new TestException(); } + int t82 = x82 % 182; if(t82 == 0) { throw new TestException(); } + int t83 = x83 % 183; if(t83 == 0) { throw new TestException(); } + int t84 = x84 % 184; if(t84 == 0) { throw new TestException(); } + int t85 = x85 % 185; if(t85 == 0) { throw new TestException(); } + int t86 = x86 % 186; if(t86 == 0) { throw new TestException(); } + int t87 = x87 % 187; if(t87 == 0) { throw new TestException(); } + int t88 = x88 % 188; if(t88 == 0) { throw new TestException(); } + int t89 = x89 % 189; if(t89 == 0) { throw new TestException(); } + int t90 = x90 % 190; if(t90 == 0) { throw new TestException(); } + int t91 = x91 % 191; if(t91 == 0) { throw new TestException(); } + int t92 = x92 % 192; if(t92 == 0) { throw new TestException(); } + int t93 = x93 % 193; if(t93 == 0) { throw new TestException(); } + int t94 = x94 % 194; if(t94 == 0) { throw new TestException(); } + int t95 = x95 % 195; if(t95 == 0) { throw new TestException(); } + int t96 = x96 % 196; if(t96 == 0) { throw new TestException(); } + int t97 = x97 % 197; if(t97 == 0) { throw new TestException(); } + int t98 = x98 % 198; if(t98 == 0) { throw new TestException(); } + int t99 = x99 % 199; if(t99 == 0) { throw new TestException(); } + int t100 = x100 % 200; if(t100 == 0) { throw new TestException(); } + int t101 = x101 % 201; if(t101 == 0) { throw new TestException(); } + int t102 = x102 % 202; if(t102 == 0) { throw new TestException(); } + int t103 = x103 % 203; if(t103 == 0) { throw new TestException(); } + int t104 = x104 % 204; if(t104 == 0) { throw new TestException(); } + int t105 = x105 % 205; if(t105 == 0) { throw new TestException(); } + int t106 = x106 % 206; if(t106 == 0) { throw new TestException(); } + int t107 = x107 % 207; if(t107 == 0) { throw new TestException(); } + int t108 = x108 % 208; if(t108 == 0) { throw new TestException(); } + int t109 = x109 % 209; if(t109 == 0) { throw new TestException(); } + int t110 = x110 % 210; if(t110 == 0) { throw new TestException(); } + int t111 = x111 % 211; if(t111 == 0) { throw new TestException(); } + int t112 = x112 % 212; if(t112 == 0) { throw new TestException(); } + int t113 = x113 % 213; if(t113 == 0) { throw new TestException(); } + int t114 = x114 % 214; if(t114 == 0) { throw new TestException(); } + int t115 = x115 % 215; if(t115 == 0) { throw new TestException(); } + int t116 = x116 % 216; if(t116 == 0) { throw new TestException(); } + int t117 = x117 % 217; if(t117 == 0) { throw new TestException(); } + int t118 = x118 % 218; if(t118 == 0) { throw new TestException(); } + int t119 = x119 % 219; if(t119 == 0) { throw new TestException(); } + int t120 = x120 % 220; if(t120 == 0) { throw new TestException(); } + int t121 = x121 % 221; if(t121 == 0) { throw new TestException(); } + int t122 = x122 % 222; if(t122 == 0) { throw new TestException(); } + int t123 = x123 % 223; if(t123 == 0) { throw new TestException(); } + int t124 = x124 % 224; if(t124 == 0) { throw new TestException(); } + int t125 = x125 % 225; if(t125 == 0) { throw new TestException(); } + int t126 = x126 % 226; if(t126 == 0) { throw new TestException(); } + int t127 = x127 % 227; if(t127 == 0) { throw new TestException(); } + int t128 = x128 % 228; if(t128 == 0) { throw new TestException(); } + int t129 = x129 % 229; if(t129 == 0) { throw new TestException(); } + int t130 = x130 % 230; if(t130 == 0) { throw new TestException(); } + int t131 = x131 % 231; if(t131 == 0) { throw new TestException(); } + int t132 = x132 % 232; if(t132 == 0) { throw new TestException(); } + int t133 = x133 % 233; if(t133 == 0) { throw new TestException(); } + int t134 = x134 % 234; if(t134 == 0) { throw new TestException(); } + int t135 = x135 % 235; if(t135 == 0) { throw new TestException(); } + int t136 = x136 % 236; if(t136 == 0) { throw new TestException(); } + int t137 = x137 % 237; if(t137 == 0) { throw new TestException(); } + int t138 = x138 % 238; if(t138 == 0) { throw new TestException(); } + int t139 = x139 % 239; if(t139 == 0) { throw new TestException(); } + int t140 = x140 % 240; if(t140 == 0) { throw new TestException(); } + int t141 = x141 % 241; if(t141 == 0) { throw new TestException(); } + int t142 = x142 % 242; if(t142 == 0) { throw new TestException(); } + int t143 = x143 % 243; if(t143 == 0) { throw new TestException(); } + int t144 = x144 % 244; if(t144 == 0) { throw new TestException(); } + int t145 = x145 % 245; if(t145 == 0) { throw new TestException(); } + int t146 = x146 % 246; if(t146 == 0) { throw new TestException(); } + int t147 = x147 % 247; if(t147 == 0) { throw new TestException(); } + int t148 = x148 % 248; if(t148 == 0) { throw new TestException(); } + int t149 = x149 % 249; if(t149 == 0) { throw new TestException(); } + int t150 = x150 % 250; if(t150 == 0) { throw new TestException(); } + int t151 = x151 % 251; if(t151 == 0) { throw new TestException(); } + int t152 = x152 % 252; if(t152 == 0) { throw new TestException(); } + int t153 = x153 % 253; if(t153 == 0) { throw new TestException(); } + int t154 = x154 % 254; if(t154 == 0) { throw new TestException(); } + int t155 = x155 % 255; if(t155 == 0) { throw new TestException(); } + int t156 = x156 % 256; if(t156 == 0) { throw new TestException(); } + int t157 = x157 % 257; if(t157 == 0) { throw new TestException(); } + int t158 = x158 % 258; if(t158 == 0) { throw new TestException(); } + int t159 = x159 % 259; if(t159 == 0) { throw new TestException(); } + int t160 = x160 % 260; if(t160 == 0) { throw new TestException(); } + int t161 = x161 % 261; if(t161 == 0) { throw new TestException(); } + int t162 = x162 % 262; if(t162 == 0) { throw new TestException(); } + int t163 = x163 % 263; if(t163 == 0) { throw new TestException(); } + int t164 = x164 % 264; if(t164 == 0) { throw new TestException(); } + int t165 = x165 % 265; if(t165 == 0) { throw new TestException(); } + int t166 = x166 % 266; if(t166 == 0) { throw new TestException(); } + int t167 = x167 % 267; if(t167 == 0) { throw new TestException(); } + int t168 = x168 % 268; if(t168 == 0) { throw new TestException(); } + int t169 = x169 % 269; if(t169 == 0) { throw new TestException(); } + int t170 = x170 % 270; if(t170 == 0) { throw new TestException(); } + int t171 = x171 % 271; if(t171 == 0) { throw new TestException(); } + int t172 = x172 % 272; if(t172 == 0) { throw new TestException(); } + int t173 = x173 % 273; if(t173 == 0) { throw new TestException(); } + int t174 = x174 % 274; if(t174 == 0) { throw new TestException(); } + int t175 = x175 % 275; if(t175 == 0) { throw new TestException(); } + int t176 = x176 % 276; if(t176 == 0) { throw new TestException(); } + int t177 = x177 % 277; if(t177 == 0) { throw new TestException(); } + int t178 = x178 % 278; if(t178 == 0) { throw new TestException(); } + int t179 = x179 % 279; if(t179 == 0) { throw new TestException(); } + int t180 = x180 % 280; if(t180 == 0) { throw new TestException(); } + int t181 = x181 % 281; if(t181 == 0) { throw new TestException(); } + int t182 = x182 % 282; if(t182 == 0) { throw new TestException(); } + int t183 = x183 % 283; if(t183 == 0) { throw new TestException(); } + int t184 = x184 % 284; if(t184 == 0) { throw new TestException(); } + int t185 = x185 % 285; if(t185 == 0) { throw new TestException(); } + int t186 = x186 % 286; if(t186 == 0) { throw new TestException(); } + int t187 = x187 % 287; if(t187 == 0) { throw new TestException(); } + int t188 = x188 % 288; if(t188 == 0) { throw new TestException(); } + int t189 = x189 % 289; if(t189 == 0) { throw new TestException(); } + int t190 = x190 % 290; if(t190 == 0) { throw new TestException(); } + int t191 = x191 % 291; if(t191 == 0) { throw new TestException(); } + int t192 = x192 % 292; if(t192 == 0) { throw new TestException(); } + int t193 = x193 % 293; if(t193 == 0) { throw new TestException(); } + int t194 = x194 % 294; if(t194 == 0) { throw new TestException(); } + int t195 = x195 % 295; if(t195 == 0) { throw new TestException(); } + int t196 = x196 % 296; if(t196 == 0) { throw new TestException(); } + int t197 = x197 % 297; if(t197 == 0) { throw new TestException(); } + int t198 = x198 % 298; if(t198 == 0) { throw new TestException(); } + int t199 = x199 % 299; if(t199 == 0) { throw new TestException(); } + int t200 = x200 % 300; if(t200 == 0) { throw new TestException(); } + int t201 = x201 % 301; if(t201 == 0) { throw new TestException(); } + int t202 = x202 % 302; if(t202 == 0) { throw new TestException(); } + int t203 = x203 % 303; if(t203 == 0) { throw new TestException(); } + int t204 = x204 % 304; if(t204 == 0) { throw new TestException(); } + int t205 = x205 % 305; if(t205 == 0) { throw new TestException(); } + int t206 = x206 % 306; if(t206 == 0) { throw new TestException(); } + int t207 = x207 % 307; if(t207 == 0) { throw new TestException(); } + int t208 = x208 % 308; if(t208 == 0) { throw new TestException(); } + int t209 = x209 % 309; if(t209 == 0) { throw new TestException(); } + int t210 = x210 % 310; if(t210 == 0) { throw new TestException(); } + int t211 = x211 % 311; if(t211 == 0) { throw new TestException(); } + int t212 = x212 % 312; if(t212 == 0) { throw new TestException(); } + int t213 = x213 % 313; if(t213 == 0) { throw new TestException(); } + int t214 = x214 % 314; if(t214 == 0) { throw new TestException(); } + int t215 = x215 % 315; if(t215 == 0) { throw new TestException(); } + int t216 = x216 % 316; if(t216 == 0) { throw new TestException(); } + int t217 = x217 % 317; if(t217 == 0) { throw new TestException(); } + int t218 = x218 % 318; if(t218 == 0) { throw new TestException(); } + int t219 = x219 % 319; if(t219 == 0) { throw new TestException(); } + int t220 = x220 % 320; if(t220 == 0) { throw new TestException(); } + int t221 = x221 % 321; if(t221 == 0) { throw new TestException(); } + int t222 = x222 % 322; if(t222 == 0) { throw new TestException(); } + int t223 = x223 % 323; if(t223 == 0) { throw new TestException(); } + int t224 = x224 % 324; if(t224 == 0) { throw new TestException(); } + int t225 = x225 % 325; if(t225 == 0) { throw new TestException(); } + int t226 = x226 % 326; if(t226 == 0) { throw new TestException(); } + int t227 = x227 % 327; if(t227 == 0) { throw new TestException(); } + int t228 = x228 % 328; if(t228 == 0) { throw new TestException(); } + int t229 = x229 % 329; if(t229 == 0) { throw new TestException(); } + int t230 = x230 % 330; if(t230 == 0) { throw new TestException(); } + int t231 = x231 % 331; if(t231 == 0) { throw new TestException(); } + int t232 = x232 % 332; if(t232 == 0) { throw new TestException(); } + int t233 = x233 % 333; if(t233 == 0) { throw new TestException(); } + int t234 = x234 % 334; if(t234 == 0) { throw new TestException(); } + int t235 = x235 % 335; if(t235 == 0) { throw new TestException(); } + int t236 = x236 % 336; if(t236 == 0) { throw new TestException(); } + int t237 = x237 % 337; if(t237 == 0) { throw new TestException(); } + int t238 = x238 % 338; if(t238 == 0) { throw new TestException(); } + int t239 = x239 % 339; if(t239 == 0) { throw new TestException(); } + int t240 = x240 % 340; if(t240 == 0) { throw new TestException(); } + int t241 = x241 % 341; if(t241 == 0) { throw new TestException(); } + int t242 = x242 % 342; if(t242 == 0) { throw new TestException(); } + int t243 = x243 % 343; if(t243 == 0) { throw new TestException(); } + int t244 = x244 % 344; if(t244 == 0) { throw new TestException(); } + int t245 = x245 % 345; if(t245 == 0) { throw new TestException(); } + int t246 = x246 % 346; if(t246 == 0) { throw new TestException(); } + int t247 = x247 % 347; if(t247 == 0) { throw new TestException(); } + int t248 = x248 % 348; if(t248 == 0) { throw new TestException(); } + int t249 = x249 % 349; if(t249 == 0) { throw new TestException(); } + int t250 = x250 % 350; if(t250 == 0) { throw new TestException(); } + int t251 = x251 % 351; if(t251 == 0) { throw new TestException(); } + int t252 = x252 % 352; if(t252 == 0) { throw new TestException(); } + int t253 = x253 % 353; if(t253 == 0) { throw new TestException(); } + int t254 = x254 % 354; if(t254 == 0) { throw new TestException(); } + int t255 = x255 % 355; if(t255 == 0) { throw new TestException(); } + // All temporaries are live here, stressing the register allocator. + return t1 + t2 + t3 + t4 + t5 + t6 + t7 + t8 + t9 + t10 + t11 + t12 + t13 + t14 + t15 + t16 + t17 + t18 + t19 + t20 + t21 + t22 + t23 + t24 + t25 + t26 + t27 + t28 + t29 + t30 + t31 + t32 + t33 + t34 + t35 + t36 + t37 + t38 + t39 + t40 + t41 + t42 + t43 + t44 + t45 + t46 + t47 + t48 + t49 + t50 + t51 + t52 + t53 + t54 + t55 + t56 + t57 + t58 + t59 + t60 + t61 + t62 + t63 + t64 + t65 + t66 + t67 + t68 + t69 + t70 + t71 + t72 + t73 + t74 + t75 + t76 + t77 + t78 + t79 + t80 + t81 + t82 + t83 + t84 + t85 + t86 + t87 + t88 + t89 + t90 + t91 + t92 + t93 + t94 + t95 + t96 + t97 + t98 + t99 + t100 + t101 + t102 + t103 + t104 + t105 + t106 + t107 + t108 + t109 + t110 + t111 + t112 + t113 + t114 + t115 + t116 + t117 + t118 + t119 + t120 + t121 + t122 + t123 + t124 + t125 + t126 + t127 + t128 + t129 + t130 + t131 + t132 + t133 + t134 + t135 + t136 + t137 + t138 + t139 + t140 + t141 + t142 + t143 + t144 + t145 + t146 + t147 + t148 + t149 + t150 + t151 + t152 + t153 + t154 + t155 + t156 + t157 + t158 + t159 + t160 + t161 + t162 + t163 + t164 + t165 + t166 + t167 + t168 + t169 + t170 + t171 + t172 + t173 + t174 + t175 + t176 + t177 + t178 + t179 + t180 + t181 + t182 + t183 + t184 + t185 + t186 + t187 + t188 + t189 + t190 + t191 + t192 + t193 + t194 + t195 + t196 + t197 + t198 + t199 + t200 + t201 + t202 + t203 + t204 + t205 + t206 + t207 + t208 + t209 + t210 + t211 + t212 + t213 + t214 + t215 + t216 + t217 + t218 + t219 + t220 + t221 + t222 + t223 + t224 + t225 + t226 + t227 + t228 + t229 + t230 + t231 + t232 + t233 + t234 + t235 + t236 + t237 + t238 + t239 + t240 + t241 + t242 + t243 + t244 + t245 + t246 + t247 + t248 + t249 + t250 + t251 + t252 + t253 + t254 + t255; + } +} diff --git a/test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java b/test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java new file mode 100644 index 00000000000..6ff830e85c6 --- /dev/null +++ b/test/hotspot/jtreg/compiler/arguments/TestMethodArguments.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8325467 + * @summary Ensure that C2 can compile methods up to the maximum + * number of parameters (according to the JVM spec). + * @library /test/lib / + * @requires vm.compMode != "Xcomp" + * @run driver/timeout=1000 compiler.arguments.TestMethodArguments + */ + +package compiler.arguments; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import compiler.lib.compile_framework.CompileFramework; +import compiler.lib.template_framework.Template; +import compiler.lib.template_framework.library.CodeGenerationDataNameType; +import compiler.lib.template_framework.library.PrimitiveType; + +public class TestMethodArguments { + + static final int MIN = 0; + static final int MAX = 255; + static final int INPUT_SIZE = 100; + + public static Template.ZeroArgs generateTest(PrimitiveType type, int numberOfArguments) { + String arguments = IntStream.range(0, numberOfArguments) + .mapToObj(i -> "input[" + i + "]") + .collect(Collectors.joining(", ")); + String parameters = IntStream.range(0, numberOfArguments) + .mapToObj(i -> type.name() + " x" + i) + .collect(Collectors.joining(", ")); + String sum = numberOfArguments == 0 ? "0" + : IntStream.range(0, numberOfArguments) + .mapToObj(i -> "x" + i) + .collect(Collectors.joining(" + ")); + return Template.make(() -> Template.body( + Template.let("type", type.name()), + Template.let("boxedType", type.boxedTypeName()), + Template.let("arguments", arguments), + Template.let("parameters", parameters), + Template.let("sum", sum), + Template.let("inputSize", INPUT_SIZE), + Template.let("numberOfArguments", numberOfArguments), + """ + static #boxedType[][] $inputs = generateInput(generator#boxedType, new #boxedType[#inputSize][#numberOfArguments]); + static int $nextInput = 0; + static #type[] $golden = $init(); + + public static #type[] $init() { + #type[] golden = new #type[$inputs.length]; + for (int i = 0; i < golden.length; ++i) { + #boxedType[] input = $inputs[i]; + golden[i] = $test(#arguments); + } + return golden; + } + + @Setup + public static Object[] $setup() { + #boxedType[] input = $inputs[$nextInput]; + return new Object[]{#arguments}; + } + + @Test + @Arguments(setup = "$setup") + public static #type $test(#parameters) { + return #sum; + } + + @Check(test = "$test") + public static void $check(#type res) { + if (res != $golden[$nextInput]) { + throw new RuntimeException("wrong result " + res + "!=" + $golden[$nextInput]); + } + $nextInput = ($nextInput + 1) % $inputs.length; + } + """)); + } + + public static String generate(CompileFramework comp) { + List tests = new LinkedList<>(); + for (int i = MIN; i <= MAX; ++i) { + tests.add(generateTest(CodeGenerationDataNameType.ints(), i).asToken()); + tests.add(generateTest(CodeGenerationDataNameType.floats(), i).asToken()); + // Longs and doubles take up double as much space in the parameter list as other + // primitive types (e.g., int). We therefore have to divide by two to fill up + // the same amount of space as for ints and floats. + tests.add(generateTest(CodeGenerationDataNameType.longs(), i / 2).asToken()); + tests.add(generateTest(CodeGenerationDataNameType.doubles(), i / 2).asToken()); + } + return Template.make(() -> Template.body( + Template.let("classpath", comp.getEscapedClassPathOfCompiledClasses()), + """ + import java.util.Arrays; + import java.util.stream.*; + import compiler.lib.generators.*; + import compiler.lib.ir_framework.*; + import compiler.lib.template_framework.library.*; + + public class InnerTest { + + static RestrictableGenerator generatorInteger = Generators.G.uniformInts(); + static RestrictableGenerator generatorLong = Generators.G.uniformLongs(); + static RestrictableGenerator generatorFloat = Generators.G.uniformFloats(); + static RestrictableGenerator generatorDouble = Generators.G.uniformDoubles(); + + public static void main() { + TestFramework framework = new TestFramework(InnerTest.class); + framework.addFlags("-classpath", "#classpath"); + framework.start(); + } + + public static T[][] generateInput(Generator t, T[][] array) { + for (int i = 0; i < array.length; i++) { + for (int j = 0; j < array[i].length; j++) { + array[i][j] = t.next(); + } + } + return array; + } + """, + tests, + """ + } + """)).render(); + } + + public static void main(String[] args) { + CompileFramework comp = new CompileFramework(); + comp.addJavaSourceCode("InnerTest", generate(comp)); + comp.compile(); + comp.invoke("InnerTest", "main", new Object[] {}); + } +} diff --git a/test/hotspot/jtreg/compiler/locks/TestNestedSynchronize.java b/test/hotspot/jtreg/compiler/locks/TestNestedSynchronize.java index 874d56fc93b..fea0bc343ee 100644 --- a/test/hotspot/jtreg/compiler/locks/TestNestedSynchronize.java +++ b/test/hotspot/jtreg/compiler/locks/TestNestedSynchronize.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,216 +23,97 @@ /** * @test - * @bug 8322996 - * @summary Ensure no assert error in C2 with deeply nested synchronize - * statements. - * @run main/othervm -XX:CompileCommand=compileonly,compiler.locks.TestNestedSynchronize::test + * @bug 8322996 8324839 8325467 + * @summary Ensure C2 can compile deeply nested synchronize statements. + * Exercises C2 register masks, in particular. We incrementally + * increase the level of nesting (up to 100) to trigger potential edge + * cases. + * @modules java.base/jdk.internal.misc + * @library /test/lib / + * @run main/othervm -XX:CompileCommand=compileonly,Test::test* * -Xcomp + * -XX:+UnlockDiagnosticVMOptions + * -XX:+AbortVMOnCompilationFailure * compiler.locks.TestNestedSynchronize + * @run main compiler.locks.TestNestedSynchronize */ package compiler.locks; -public class TestNestedSynchronize { +import compiler.lib.compile_framework.*; +import java.util.LinkedList; - public static void main(String[] args) { - test(); +public class TestNestedSynchronize { + static int min = 1; + static int max = 100; + static String test_class_name = "Test"; + static String test_method_name = "test"; + + // The below method generates a program of the form: + // + // public class Test { + // public static void test1() { + // synchronized (Test.class) { + // } + // } + // + // public static void test2() { + // synchronized (Test.class) { + // synchronized (Test.class) { + // } + // } + // } + // + // public static void test3() { + // synchronized (Test.class) { + // synchronized (Test.class) { + // synchronized (Test.class) { + // } + // } + // } + // } + // + // ... + // + // public static void test100() { + // synchronized (Test.class) { + // synchronized (Test.class) { + // synchronized (Test.class) { + // ... + // } + // } + // } + // } + // } + // + // The above is a massive program. Therefore, we do not directly inline the + // program in TestNestedSynchronize and instead compile and run it via the + // CompileFramework. + public static String generate_test() { + LinkedList acc = new LinkedList(); + for (int i = min; i <= max; i++) { + LinkedList method = new LinkedList(); + for (int j = 0; j < i; j++) { + method.addFirst(String.format( + " synchronized (%s.class) {", test_class_name)); + method.addLast(" }"); + } + method.addFirst(String.format( + " public static void %s%d() {", test_method_name, i)); + method.addLast(" }"); + acc.addAll(method); + } + acc.addFirst(String.format("public class %s {", test_class_name)); + acc.addLast("}"); + return String.join("\n", acc); } - public static void test() { - - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - synchronized (TestNestedSynchronize.class) { - - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } - } + public static void main(String[] args) { + CompileFramework comp = new CompileFramework(); + comp.addJavaSourceCode(test_class_name, generate_test()); + comp.compile(); + for (int i = min; i <= max; i++) { + comp.invoke(test_class_name, test_method_name + i, new Object[] {}); } } } From 85f5bf3f415cc3d44d1618ec574e73f846bb91c4 Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Wed, 24 Sep 2025 16:06:41 +0000 Subject: [PATCH 212/556] 8368308: ISO 4217 Amendment 180 Update Reviewed-by: naoto, iris, coffeys --- .../share/data/currency/CurrencyData.properties | 8 ++++---- test/jdk/java/util/Currency/ISO4217-list-one.txt | 12 ++++++------ test/jdk/java/util/Currency/ValidateISO4217.java | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/java.base/share/data/currency/CurrencyData.properties b/src/java.base/share/data/currency/CurrencyData.properties index ff2d1f87ed3..9b82318776a 100644 --- a/src/java.base/share/data/currency/CurrencyData.properties +++ b/src/java.base/share/data/currency/CurrencyData.properties @@ -32,7 +32,7 @@ formatVersion=3 # Version of the currency code information in this class. # It is a serial number that accompanies with each amendment. -dataVersion=179 +dataVersion=180 # List of all valid ISO 4217 currency codes. # To ensure compatibility, do not remove codes. @@ -147,7 +147,7 @@ IO=USD # BRUNEI DARUSSALAM BN=BND # BULGARIA -BG=BGN +BG=BGN;2025-12-31-22-00-00;EUR # BURKINA FASO BF=XOF # BURUNDI @@ -193,7 +193,7 @@ HR=EUR # CUBA CU=CUP # Curaçao -CW=ANG;2025-04-01-04-00-00;XCG +CW=XCG # CYPRUS CY=EUR # CZECHIA @@ -510,7 +510,7 @@ SR=SRD # SVALBARD AND JAN MAYEN SJ=NOK # Sint Maarten (Dutch part) -SX=ANG;2025-04-01-04-00-00;XCG +SX=XCG # ESWATINI SZ=SZL # SWEDEN diff --git a/test/jdk/java/util/Currency/ISO4217-list-one.txt b/test/jdk/java/util/Currency/ISO4217-list-one.txt index aa8c2725899..ca4bfd6dc94 100644 --- a/test/jdk/java/util/Currency/ISO4217-list-one.txt +++ b/test/jdk/java/util/Currency/ISO4217-list-one.txt @@ -1,12 +1,12 @@ # # -# Amendments up until ISO 4217 AMENDMENT NUMBER 179 -# (As of 02 May 2025) +# Amendments up until ISO 4217 AMENDMENT NUMBER 180 +# (As of 22 September 2025) # # Version FILEVERSION=3 -DATAVERSION=179 +DATAVERSION=180 # ISO 4217 currency data AF AFN 971 2 @@ -44,7 +44,7 @@ BV NOK 578 2 BR BRL 986 2 IO USD 840 2 BN BND 96 2 -BG BGN 975 2 +BG BGN 975 2 2025-12-31-22-00-00 EUR 978 2 BF XOF 952 0 BI BIF 108 0 KH KHR 116 2 @@ -69,7 +69,7 @@ CR CRC 188 2 CI XOF 952 0 HR EUR 978 2 CU CUP 192 2 -CW ANG 532 2 2025-04-01-04-00-00 XCG 532 2 +CW XCG 532 2 CY EUR 978 2 CZ CZK 203 2 DK DKK 208 2 @@ -233,7 +233,7 @@ LK LKR 144 2 SD SDG 938 2 SR SRD 968 2 SJ NOK 578 2 -SX ANG 532 2 2025-04-01-04-00-00 XCG 532 2 +SX XCG 532 2 SZ SZL 748 2 SE SEK 752 2 CH CHF 756 2 diff --git a/test/jdk/java/util/Currency/ValidateISO4217.java b/test/jdk/java/util/Currency/ValidateISO4217.java index 67793dcbecb..fa11a4b4fc4 100644 --- a/test/jdk/java/util/Currency/ValidateISO4217.java +++ b/test/jdk/java/util/Currency/ValidateISO4217.java @@ -26,7 +26,7 @@ * @bug 4691089 4819436 4942982 5104960 6544471 6627549 7066203 7195759 * 8039317 8074350 8074351 8145952 8187946 8193552 8202026 8204269 * 8208746 8209775 8264792 8274658 8283277 8296239 8321480 8334653 - * 8354343 8354344 8356096 + * 8354343 8354344 8356096 8368308 * @summary Validate ISO 4217 data for Currency class. * @modules java.base/java.util:open * jdk.localedata @@ -116,7 +116,7 @@ public class ValidateISO4217 { private static final Set currenciesNotYetDefined = new HashSet<>(); // Codes that are obsolete, do not have related country, extra currency private static final String otherCodes = - "ADP-AFA-ATS-AYM-AZM-BEF-BGL-BOV-BYB-BYR-CHE-CHW-CLF-COU-CUC-CYP-" + "ADP-AFA-ATS-AYM-AZM-BEF-BGL-BGN-BOV-BYB-BYR-CHE-CHW-CLF-COU-CUC-CYP-" + "DEM-EEK-ESP-FIM-FRF-GHC-GRD-GWP-HRK-IEP-ITL-LTL-LUF-LVL-MGF-MRO-MTL-MXV-MZM-NLG-" + "PTE-ROL-RUR-SDD-SIT-SLL-SKK-SRG-STD-TMM-TPE-TRL-VEF-UYI-USN-USS-VEB-VED-" + "XAD-XAG-XAU-XBA-XBB-XBC-XBD-XDR-XFO-XFU-XPD-XPT-XSU-XTS-XUA-XXX-" From f489598d43e786aabcf0e26e9f9b9a840c699654 Mon Sep 17 00:00:00 2001 From: Phil Race Date: Wed, 24 Sep 2025 16:11:43 +0000 Subject: [PATCH 213/556] 8221451: PIT: sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.sh fails 7184899: Test sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.sh fail Reviewed-by: psadhukhan, azvegint --- test/jdk/ProblemList.txt | 1 - .../SharedMemoryPixmapsTest.java | 106 ++++++++---------- .../SharedMemoryPixmapsTest.sh | 3 +- 3 files changed, 47 insertions(+), 63 deletions(-) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 3cc4fa835df..381c0c8b0be 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -255,7 +255,6 @@ sun/java2d/SunGraphics2D/PolyVertTest.java 6986565 generic-all sun/java2d/SunGraphics2D/SimplePrimQuality.java 6992007 generic-all sun/java2d/SunGraphics2D/SourceClippingBlitTest/SourceClippingBlitTest.java 8196185 generic-all -sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.sh 7184899,8221451 linux-all,macosx-aarch64 java/awt/FullScreen/DisplayChangeVITest/DisplayChangeVITest.java 8169469,8273617 windows-all,macosx-aarch64 java/awt/FullScreen/UninitializedDisplayModeChangeTest/UninitializedDisplayModeChangeTest.java 8273617 macosx-all java/awt/print/PrinterJob/PSQuestionMark.java 7003378 generic-all diff --git a/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.java b/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.java index efa93803670..9b109e954eb 100644 --- a/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.java +++ b/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,10 +21,10 @@ * questions. */ -import java.awt.AWTException; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.EventQueue; import java.awt.Frame; import java.awt.Graphics; import java.awt.Point; @@ -42,18 +42,18 @@ import java.awt.image.VolatileImage; * make sure the pixels on the screen are red. * * Note that we force the use of shared memory pixmaps in the shell script. - * - * @author Dmitri.Trembovetski */ public class SharedMemoryPixmapsTest { - static final int IMAGE_SIZE = 100; - static boolean show = false; - final Frame testFrame; - /** Creates a new instance of SharedMemoryPixmapsTest */ - public SharedMemoryPixmapsTest() { + static final int IMAGE_SIZE = 200; + static volatile boolean show = false; + static volatile Frame testFrame; + static volatile TestComponent testComponent; + + static void createUI() { testFrame = new Frame("SharedMemoryPixmapsTest"); - testFrame.add(new TestComponent()); + testComponent = new TestComponent(); + testFrame.add(testComponent); testFrame.setUndecorated(true); testFrame.setResizable(false); testFrame.pack(); @@ -62,7 +62,7 @@ public class SharedMemoryPixmapsTest { testFrame.toFront(); } - public static void main(String[] args) { + public static void main(String[] args) throws Exception { for (String s : args) { if ("-show".equals(s)) { show = true; @@ -70,12 +70,43 @@ public class SharedMemoryPixmapsTest { System.err.println("Usage: SharedMemoryPixmapsTest [-show]"); } } - new SharedMemoryPixmapsTest(); + EventQueue.invokeAndWait(SharedMemoryPixmapsTest::createUI); + if (testRendering()) { + System.err.println("Test Passed"); + } else { + System.err.println("Test Failed"); + } + if (!show && testFrame != null) { + EventQueue.invokeAndWait(testFrame::dispose); + } } - private class TestComponent extends Component { + static boolean testRendering() throws Exception { + Robot r = new Robot(); + r.waitForIdle(); + r.delay(2000); + Point p = testComponent.getLocationOnScreen(); + BufferedImage b = + r.createScreenCapture(new Rectangle(p, testComponent.getPreferredSize())); + for (int y = 20; y < b.getHeight() - 40; y++) { + for (int x = 20; x < b.getWidth() - 40; x++) { + if (b.getRGB(x, y) != Color.red.getRGB()) { + System.err.println("Incorrect pixel at " + + x + "x" + y + " : " + + Integer.toHexString(b.getRGB(x, y))); + if (show) { + return false; + } + System.err.println("Test Failed"); + System.exit(1); + } + } + } + return true; + } + + static class TestComponent extends Component { VolatileImage vi = null; - boolean tested = false; void initVI() { int res; @@ -104,54 +135,10 @@ public class SharedMemoryPixmapsTest { g.setColor(Color.green); g.fillRect(0, 0, getWidth(), getHeight()); + vi = null; initVI(); g.drawImage(vi, 0, 0, null); } while (vi.contentsLost()); - - Toolkit.getDefaultToolkit().sync(); - if (!tested) { - if (testRendering()) { - System.err.println("Test Passed"); - } else { - System.err.println("Test Failed"); - } - tested = true; - } - if (!show) { - testFrame.setVisible(false); - testFrame.dispose(); - } - } - - private boolean testRendering() throws RuntimeException { - try { - Thread.sleep(2000); - } catch (InterruptedException ex) {} - Robot r = null; - try { - r = new Robot(); - } catch (AWTException ex) { - ex.printStackTrace(); - throw new RuntimeException("Can't create Robot"); - } - Point p = getLocationOnScreen(); - BufferedImage b = - r.createScreenCapture(new Rectangle(p, getPreferredSize())); - for (int y = 0; y < b.getHeight(); y++) { - for (int x = 0; x < b.getWidth(); x++) { - if (b.getRGB(x, y) != Color.red.getRGB()) { - System.err.println("Incorrect pixel" + " at " - + x + "x" + y + " : " + - Integer.toHexString(b.getRGB(x, y))); - if (show) { - return false; - } - System.err.println("Test Failed"); - System.exit(1); - } - } - } - return true; } @Override @@ -159,5 +146,4 @@ public class SharedMemoryPixmapsTest { return new Dimension(IMAGE_SIZE, IMAGE_SIZE); } } - } diff --git a/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.sh b/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.sh index 10dd58d90de..786957ca0eb 100644 --- a/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.sh +++ b/test/jdk/sun/java2d/X11SurfaceData/SharedMemoryPixmapsTest/SharedMemoryPixmapsTest.sh @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2005, 2008, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -29,7 +29,6 @@ # by filling a VolatileImage with red color and copying it # to the screen. # Note that we force the use of shared memory pixmaps. -# @author Dmitri.Trembovetski echo "TESTJAVA=${TESTJAVA}" echo "TESTSRC=${TESTSRC}" From 52a923f20cac85b2a35705f2d3d72d17c84db3f8 Mon Sep 17 00:00:00 2001 From: Phil Race Date: Wed, 24 Sep 2025 16:28:44 +0000 Subject: [PATCH 214/556] 8367702: PrintJob.getGraphics() should return null after PrintJob.end Reviewed-by: azvegint --- .../share/classes/sun/print/PrintJob2D.java | 7 ++- .../java/awt/PrintJob/GetGraphicsTest.java | 61 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 test/jdk/java/awt/PrintJob/GetGraphicsTest.java diff --git a/src/java.desktop/share/classes/sun/print/PrintJob2D.java b/src/java.desktop/share/classes/sun/print/PrintJob2D.java index 8128dae1057..ee0caaa54fe 100644 --- a/src/java.desktop/share/classes/sun/print/PrintJob2D.java +++ b/src/java.desktop/share/classes/sun/print/PrintJob2D.java @@ -79,7 +79,12 @@ public class PrintJob2D extends PrintJob { * needs to implement PrintGraphics, so we wrap * the Graphics2D instance. */ - return new ProxyPrintGraphics(printJobDelegate.getGraphics(), this); + Graphics g = printJobDelegate.getGraphics(); + if (g == null) { // PrintJob.end() has been called. + return null; + } else { + return new ProxyPrintGraphics(g, this); + } } /** diff --git a/test/jdk/java/awt/PrintJob/GetGraphicsTest.java b/test/jdk/java/awt/PrintJob/GetGraphicsTest.java new file mode 100644 index 00000000000..61abe37b66b --- /dev/null +++ b/test/jdk/java/awt/PrintJob/GetGraphicsTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + @test + @bug 50510568367702 + @key headful printer + @summary PrintJob.getGraphics() should return null after PrintJob.end() is called. + @run main GetGraphicsTest +*/ + +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.JobAttributes; +import java.awt.PageAttributes; +import java.awt.PrintJob; +import java.awt.Toolkit; + +public class GetGraphicsTest { + + public static void main(String[] args) { + + JobAttributes ja = new JobAttributes(); + ja.setDialog(JobAttributes.DialogType.NONE); + PageAttributes pa = new PageAttributes(); + pa.setOrigin( PageAttributes.OriginType.PRINTABLE); + + Toolkit tk = Toolkit.getDefaultToolkit(); + PrintJob pjob = tk.getPrintJob(new Frame(),"Printing Test", ja,pa); + if (pjob != null) { + pjob.end(); + Graphics pg = pjob.getGraphics(); + if (pg == null) { + System.out.println("Graphics is null, TEST PASSES"); + } + else { + throw new RuntimeException("Graphics is NOT null, TEST FAILED"); + } + } + } +} From 7fe71a78137991d26553b6d5e0d4d74900f01ba3 Mon Sep 17 00:00:00 2001 From: Mahendra Chhipa Date: Wed, 24 Sep 2025 17:48:11 +0000 Subject: [PATCH 215/556] 8318662: Refactor some jdk/java/net/httpclient/http2 tests to JUnit Reviewed-by: dfuchs --- .../net/httpclient/http2/BadHeadersTest.java | 47 +++++----- .../httpclient/http2/BadPushPromiseTest.java | 27 +++--- .../java/net/httpclient/http2/BasicTest.java | 7 +- .../http2/ConnectionFlowControlTest.java | 91 +++++++------------ .../http2/ContinuationFrameTest.java | 59 ++++++------ 5 files changed, 103 insertions(+), 128 deletions(-) diff --git a/test/jdk/java/net/httpclient/http2/BadHeadersTest.java b/test/jdk/java/net/httpclient/http2/BadHeadersTest.java index 35e1ea19950..4ee95678acb 100644 --- a/test/jdk/java/net/httpclient/http2/BadHeadersTest.java +++ b/test/jdk/java/net/httpclient/http2/BadHeadersTest.java @@ -29,7 +29,7 @@ * with bad header fields. * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext - * @run testng/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest + * @run junit/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest */ import jdk.internal.net.http.common.HttpHeadersBuilder; @@ -38,10 +38,10 @@ import jdk.internal.net.http.frame.HeaderFrame; import jdk.internal.net.http.frame.HeadersFrame; import jdk.internal.net.http.frame.Http2Frame; import jdk.test.lib.net.SimpleSSLContext; -import org.testng.annotations.AfterTest; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import java.io.IOException; @@ -61,6 +61,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.function.BiFunction; + import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; @@ -69,8 +70,9 @@ import jdk.httpclient.test.lib.http2.BodyOutputStream; import jdk.httpclient.test.lib.http2.Http2TestServerConnection; import static java.util.List.of; import static java.util.Map.entry; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; // Code copied from ContinuationFrameTest public class BadHeadersTest { @@ -85,11 +87,11 @@ public class BadHeadersTest { of(entry("hello", "world!"), entry(":status", "200")) // Pseudo header is not the first one ); - SSLContext sslContext; - Http2TestServer http2TestServer; // HTTP/2 ( h2c ) - Http2TestServer https2TestServer; // HTTP/2 ( h2 ) - String http2URI; - String https2URI; + private static SSLContext sslContext; + private static Http2TestServer http2TestServer; // HTTP/2 ( h2c ) + private static Http2TestServer https2TestServer; // HTTP/2 ( h2 ) + private static String http2URI; + private static String https2URI; /** * A function that returns a list of 1) one HEADERS frame ( with an empty @@ -127,8 +129,7 @@ public class BadHeadersTest { return frames; }; - @DataProvider(name = "variants") - public Object[][] variants() { + static Object[][] variants() { return new Object[][] { { http2URI, false, oneContinuation }, { https2URI, false, oneContinuation }, @@ -142,8 +143,8 @@ public class BadHeadersTest { }; } - - @Test(dataProvider = "variants") + @ParameterizedTest + @MethodSource("variants") void test(String uri, boolean sameClient, BiFunction,List> headerFramesSupplier) @@ -172,7 +173,8 @@ public class BadHeadersTest { } } - @Test(dataProvider = "variants") + @ParameterizedTest + @MethodSource("variants") void testAsync(String uri, boolean sameClient, BiFunction,List> headerFramesSupplier) @@ -211,8 +213,7 @@ public class BadHeadersTest { // sync with implementation. static void assertDetailMessage(Throwable throwable, int iterationIndex) { try { - assertTrue(throwable instanceof ProtocolException, - "Expected ProtocolException, got " + throwable); + assertInstanceOf(ProtocolException.class, throwable, "Expected ProtocolException, got " + throwable); assertTrue(throwable.getMessage().contains("malformed response"), "Expected \"malformed response\" in: " + throwable.getMessage()); @@ -239,8 +240,8 @@ public class BadHeadersTest { } } - @BeforeTest - public void setup() throws Exception { + @BeforeAll + static void setup() throws Exception { sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); @@ -264,8 +265,8 @@ public class BadHeadersTest { https2TestServer.start(); } - @AfterTest - public void teardown() throws Exception { + @AfterAll + static void teardown() throws Exception { http2TestServer.stop(); https2TestServer.stop(); } diff --git a/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java b/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java index c7edfd5fa8f..8eed173409b 100644 --- a/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java +++ b/test/jdk/java/net/httpclient/http2/BadPushPromiseTest.java @@ -26,16 +26,16 @@ * @bug 8354276 * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.http2.Http2TestServer - * @run testng/othervm + * @run junit/othervm * -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.HttpClient.log=errors,requests,responses,trace * BadPushPromiseTest */ import jdk.httpclient.test.lib.common.HttpServerAdapters; -import org.testng.annotations.AfterTest; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -59,7 +59,7 @@ import java.util.concurrent.ConcurrentMap; import static java.net.http.HttpClient.Version.HTTP_2; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.List.of; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.*; public class BadPushPromiseTest { @@ -73,11 +73,11 @@ public class BadPushPromiseTest { static final String MAIN_RESPONSE_BODY = "the main response body"; - HttpServerAdapters.HttpTestServer server; - URI uri; + static HttpServerAdapters.HttpTestServer server; + static URI uri; - @BeforeTest - public void setup() throws Exception { + @BeforeAll + static void setup() throws Exception { server = HttpServerAdapters.HttpTestServer.create(HTTP_2); HttpServerAdapters.HttpTestHandler handler = new ServerPushHandler(MAIN_RESPONSE_BODY); server.addHandler(handler, "/"); @@ -87,8 +87,8 @@ public class BadPushPromiseTest { uri = new URI("http://" + authority + "/foo/a/b/c"); } - @AfterTest - public void teardown() { + @AfterAll + static void teardown() { server.stop(); } @@ -96,7 +96,7 @@ public class BadPushPromiseTest { * Malformed push promise headers should kill the connection */ @Test - public void test() throws Exception { + void test() { HttpClient client = HttpClient.newHttpClient(); for (int i=0; i< BAD_HEADERS.size(); i++) { @@ -123,8 +123,7 @@ public class BadPushPromiseTest { // sync with implementation. static void assertDetailMessage(Throwable throwable, int iterationIndex) { try { - assertTrue(throwable instanceof ProtocolException, - "Expected ProtocolException, got " + throwable); + assertInstanceOf(ProtocolException.class, throwable, "Expected ProtocolException, got " + throwable); if (iterationIndex == 0) { // unknown assertTrue(throwable.getMessage().contains("Unknown pseudo-header"), diff --git a/test/jdk/java/net/httpclient/http2/BasicTest.java b/test/jdk/java/net/httpclient/http2/BasicTest.java index 9d54c8c39e0..0c64be84926 100644 --- a/test/jdk/java/net/httpclient/http2/BasicTest.java +++ b/test/jdk/java/net/httpclient/http2/BasicTest.java @@ -32,7 +32,7 @@ * jdk.test.lib.Asserts * jdk.test.lib.Utils * jdk.test.lib.net.SimpleSSLContext - * @run testng/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors BasicTest + * @run junit/othervm -Djdk.httpclient.HttpClient.log=ssl,requests,responses,errors BasicTest */ import java.io.IOException; @@ -53,14 +53,13 @@ import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2EchoHandler; import jdk.test.lib.net.SimpleSSLContext; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; import static java.net.http.HttpClient.Version.HTTP_2; import static jdk.test.lib.Asserts.assertFileContentsEqual; import static jdk.test.lib.Utils.createTempFile; import static jdk.test.lib.Utils.createTempFileOfSize; -@Test public class BasicTest { private static final String TEMP_FILE_PREFIX = @@ -127,7 +126,7 @@ public class BasicTest { } @Test - public static void test() throws Exception { + void test() throws Exception { try { initialize(); warmup(false); diff --git a/test/jdk/java/net/httpclient/http2/ConnectionFlowControlTest.java b/test/jdk/java/net/httpclient/http2/ConnectionFlowControlTest.java index 30cc9122d9d..1eaf331c261 100644 --- a/test/jdk/java/net/httpclient/http2/ConnectionFlowControlTest.java +++ b/test/jdk/java/net/httpclient/http2/ConnectionFlowControlTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,7 @@ * @summary checks connection flow control * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext - * @run testng/othervm -Djdk.internal.httpclient.debug=true + * @run junit/othervm -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.connectionWindowSize=65535 * -Djdk.httpclient.windowsize=16384 * ConnectionFlowControlTest @@ -43,23 +43,13 @@ import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; -import java.net.http.HttpResponse.BodySubscriber; -import java.net.http.HttpResponse.ResponseInfo; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Flow.Subscription; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; import java.util.function.Consumer; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; @@ -72,44 +62,34 @@ import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestServerConnection; import jdk.internal.net.http.common.HttpHeadersBuilder; -import jdk.internal.net.http.frame.ContinuationFrame; -import jdk.internal.net.http.frame.HeaderFrame; -import jdk.internal.net.http.frame.HeadersFrame; -import jdk.internal.net.http.frame.Http2Frame; import jdk.internal.net.http.frame.SettingsFrame; import jdk.test.lib.Utils; import jdk.test.lib.net.SimpleSSLContext; -import org.testng.annotations.AfterTest; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -import static java.util.List.of; -import static java.util.Map.entry; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotEquals; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; public class ConnectionFlowControlTest { - SSLContext sslContext; - HttpTestServer http2TestServer; // HTTP/2 ( h2c ) - HttpTestServer https2TestServer; // HTTP/2 ( h2 ) - String http2URI; - String https2URI; - final AtomicInteger reqid = new AtomicInteger(); + private static SSLContext sslContext; + private static HttpTestServer http2TestServer; // HTTP/2 ( h2c ) + private static HttpTestServer https2TestServer; // HTTP/2 ( h2 ) + private static String http2URI; + private static String https2URI; + private final AtomicInteger reqid = new AtomicInteger(); - - @DataProvider(name = "variants") - public Object[][] variants() { + static Object[][] variants() { return new Object[][] { { http2URI }, { https2URI }, }; } - @Test(dataProvider = "variants") + @ParameterizedTest + @MethodSource("variants") void test(String uri) throws Exception { System.out.printf("%ntesting %s%n", uri); ConcurrentHashMap> responseSent = new ConcurrentHashMap<>(); @@ -148,7 +128,7 @@ public class ConnectionFlowControlTest { if (i < max - 1) { // the connection window might be exceeded at i == max - 2, which // means that the last request could go on a new connection. - assertEquals(ckey, label, "Unexpected key for " + query); + assertEquals(label, ckey, "Unexpected key for " + query); } } catch (AssertionError ass) { // since we won't pull all responses, the client @@ -174,7 +154,7 @@ public class ConnectionFlowControlTest { if (i < max - 1) { // the connection window might be exceeded at i == max - 2, which // means that the last request could go on a new connection. - assertEquals(ckey, label, "Unexpected key for " + query); + assertEquals(label, ckey, "Unexpected key for " + query); } int wait = uri.startsWith("https://") ? 500 : 250; try (InputStream is = response.body()) { @@ -227,7 +207,7 @@ public class ConnectionFlowControlTest { var response = client.send(request, BodyHandlers.ofString()); if (label != null) { String ckey = response.headers().firstValue("X-Connection-Key").get(); - assertNotEquals(ckey, label); + assertNotEquals(label, ckey); System.out.printf("last request %s sent on different connection as expected:" + "\n\tlast: %s\n\tprevious: %s%n", query, ckey, label); } @@ -258,33 +238,33 @@ public class ConnectionFlowControlTest { } } - @BeforeTest - public void setup() throws Exception { + @BeforeAll + static void setup() throws Exception { sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); - var http2TestServer = new Http2TestServer("localhost", false, 0); - http2TestServer.addHandler(new Http2TestHandler(), "/http2/"); - this.http2TestServer = HttpTestServer.of(http2TestServer); - http2URI = "http://" + this.http2TestServer.serverAuthority() + "/http2/x"; + var http2TestServerLocal = new Http2TestServer("localhost", false, 0); + http2TestServerLocal.addHandler(new Http2TestHandler(), "/http2/"); + http2TestServer = HttpTestServer.of(http2TestServerLocal); + http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/x"; - var https2TestServer = new Http2TestServer("localhost", true, sslContext); - https2TestServer.addHandler(new Http2TestHandler(), "/https2/"); - this.https2TestServer = HttpTestServer.of(https2TestServer); - https2URI = "https://" + this.https2TestServer.serverAuthority() + "/https2/x"; + var https2TestServerLocal = new Http2TestServer("localhost", true, sslContext); + https2TestServerLocal.addHandler(new Http2TestHandler(), "/https2/"); + https2TestServer = HttpTestServer.of(https2TestServerLocal); + https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/x"; // Override the default exchange supplier with a custom one to enable // particular test scenarios - http2TestServer.setExchangeSupplier(FCHttp2TestExchange::new); - https2TestServer.setExchangeSupplier(FCHttp2TestExchange::new); + http2TestServerLocal.setExchangeSupplier(FCHttp2TestExchange::new); + https2TestServerLocal.setExchangeSupplier(FCHttp2TestExchange::new); - this.http2TestServer.start(); - this.https2TestServer.start(); + http2TestServer.start(); + https2TestServer.start(); } - @AfterTest - public void teardown() throws Exception { + @AfterAll + static void teardown() throws Exception { http2TestServer.stop(); https2TestServer.stop(); } @@ -364,6 +344,5 @@ public class ConnectionFlowControlTest { System.out.println("Server: response sent for " + query); responseSentCB.accept(query); } - } } diff --git a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java index f27fdb580dd..f8788da67a4 100644 --- a/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java +++ b/test/jdk/java/net/httpclient/http2/ContinuationFrameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,7 @@ * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * @compile ../ReferenceTracker.java - * @run testng/othervm ContinuationFrameTest + * @run junit/othervm ContinuationFrameTest */ import java.io.IOException; @@ -60,25 +60,24 @@ import jdk.httpclient.test.lib.http2.BodyOutputStream; import jdk.httpclient.test.lib.http2.Http2TestServerConnection; import jdk.test.lib.net.SimpleSSLContext; -import org.testng.annotations.AfterTest; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_2; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ContinuationFrameTest { - SSLContext sslContext; - Http2TestServer http2TestServer; // HTTP/2 ( h2c ) - Http2TestServer https2TestServer; // HTTP/2 ( h2 ) - String http2URI; - String https2URI; - String noBodyhttp2URI; - String noBodyhttps2URI; - final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; + private static SSLContext sslContext; + private static Http2TestServer http2TestServer; // HTTP/2 ( h2c ) + private static Http2TestServer https2TestServer; // HTTP/2 ( h2 ) + private static String http2URI; + private static String https2URI; + private static String noBodyhttp2URI; + private static String noBodyhttps2URI; + private final static ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; /** * A function that returns a list of 1) a HEADERS frame ( with an empty @@ -133,8 +132,7 @@ public class ContinuationFrameTest { return frames; }; - @DataProvider(name = "variants") - public Object[][] variants() { + static Object[][] variants() { return new Object[][] { { http2URI, false, oneContinuation }, { https2URI, false, oneContinuation }, @@ -155,7 +153,7 @@ public class ContinuationFrameTest { static final int ITERATION_COUNT = 20; - HttpClient sharedClient; + static HttpClient sharedClient; HttpClient httpClient(boolean shared) { if (!shared || sharedClient == null) { var client = HttpClient.newBuilder() @@ -171,7 +169,8 @@ public class ContinuationFrameTest { return sharedClient; } - @Test(dataProvider = "variants") + @ParameterizedTest + @MethodSource("variants") void test(String uri, boolean sameClient, BiFunction,List> headerFramesSupplier) @@ -197,22 +196,20 @@ public class ContinuationFrameTest { if(uri.contains("nobody")) { out.println("Got response: " + resp); - assertTrue(resp.statusCode() == 204, - "Expected 204, got:" + resp.statusCode()); - assertEquals(resp.version(), HTTP_2); + assertEquals(204, resp.statusCode(), "Expected 204, got:" + resp.statusCode()); + assertEquals(HTTP_2, resp.version()); continue; } out.println("Got response: " + resp); out.println("Got body: " + resp.body()); - assertTrue(resp.statusCode() == 200, - "Expected 200, got:" + resp.statusCode()); - assertEquals(resp.body(), "Hello there!"); - assertEquals(resp.version(), HTTP_2); + assertEquals(200, resp.statusCode(), "Expected 200, got:" + resp.statusCode()); + assertEquals("Hello there!", resp.body()); + assertEquals(HTTP_2, resp.version()); } } - @BeforeTest - public void setup() throws Exception { + @BeforeAll + static void setup() throws Exception { sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); @@ -240,8 +237,8 @@ public class ContinuationFrameTest { https2TestServer.start(); } - @AfterTest - public void teardown() throws Exception { + @AfterAll + static void teardown() throws Exception { sharedClient = null; AssertionError fail = TRACKER.check(500); try { From 4141534e4a59facf2cd95a799bba7d3c7cf7a1f2 Mon Sep 17 00:00:00 2001 From: Brian Burkhalter Date: Wed, 24 Sep 2025 18:47:42 +0000 Subject: [PATCH 216/556] 8368156: java/nio/file/Files/IsSameFile.java failing (win) Reviewed-by: vyazici, jpai --- test/jdk/java/nio/file/Files/IsSameFile.java | 97 +++++++------------- 1 file changed, 32 insertions(+), 65 deletions(-) diff --git a/test/jdk/java/nio/file/Files/IsSameFile.java b/test/jdk/java/nio/file/Files/IsSameFile.java index ae7cc563472..d3fedd6eab8 100644 --- a/test/jdk/java/nio/file/Files/IsSameFile.java +++ b/test/jdk/java/nio/file/Files/IsSameFile.java @@ -25,7 +25,6 @@ * @bug 8154364 8365626 8366254 * @summary Test of Files.isSameFile * @library .. /test/lib - * @build IsSameFile jdk.test.lib.util.FileUtils * @run junit IsSameFile */ import java.io.IOException; @@ -42,8 +41,6 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import jdk.test.lib.util.FileUtils; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -64,19 +61,18 @@ public class IsSameFile { private Path aa; private Path b; private Path c; - private List allFiles; private boolean supportsSymbolicLinks; @BeforeAll public void init() throws IOException { - home = Files.createTempDirectory("TestIsSameFile"); + Path cwd = Path.of(System.getProperty("user.dir")); + home = Files.createTempDirectory(cwd, IsSameFile.class.getSimpleName()); - allFiles = new ArrayList(); - allFiles.add(a = home.resolve("a")); - allFiles.add(aa = home.resolve("a")); - allFiles.add(b = home.resolve("b")); - allFiles.add(c = home.resolve("c")); + a = home.resolve("a"); + aa = home.resolve("a"); + b = home.resolve("b"); + c = home.resolve("c"); supportsSymbolicLinks = TestUtil.supportsSymbolicLinks(home); } @@ -86,7 +82,7 @@ public class IsSameFile { } public void deleteFiles() throws IOException { - for (Path p : allFiles) + for (Path p : Files.list(home).toList()) Files.deleteIfExists(p); } @@ -149,21 +145,14 @@ public class IsSameFile { fos.close(); } - private Stream obj2ZipSource() throws IOException { + @Test + public void obj2Zip() throws IOException { deleteFiles(); Files.createFile(a); zipStringToFile("quote.txt", "To be determined", b); - FileSystem zipfs = FileSystems.newFileSystem(b); - List list = new ArrayList(); - list.add(Arguments.of(false, a, zipfs.getPath(b.toString()))); - return list.stream(); - } - - @ParameterizedTest - @MethodSource("obj2ZipSource") - public void obj2Zip(boolean expect, Path x, Path y) - throws IOException { - test(expect, x, y); + try (FileSystem zipfs = FileSystems.newFileSystem(b)) { + test(false, a, zipfs.getPath(b.toString())); + } } @ParameterizedTest @@ -257,23 +246,18 @@ public class IsSameFile { deleteFiles(); Path target = home.resolve("target"); Files.createFile(target); - allFiles.add(target); - Path L2 = Path.of("link2"); + Path L2 = home.resolve("link2"); Files.createSymbolicLink(L2, target); - allFiles.add(L2); - Path L1 = Path.of("link1"); + Path L1 = home.resolve("link1"); Files.createSymbolicLink(L1, L2); - allFiles.add(L1); - Path L4 = Path.of("link4"); + Path L4 = home.resolve("link4"); Files.createSymbolicLink(L4, target); - allFiles.add(L4); - Path L3 = Path.of("link3"); + Path L3 = home.resolve("link3"); Files.createSymbolicLink(L3, L4); - allFiles.add(L3); List list = new ArrayList(); list.add(Arguments.of(true, L1, L3)); @@ -296,27 +280,21 @@ public class IsSameFile { deleteFiles(); Path target = home.resolve("target"); Files.createFile(target); - allFiles.add(target); - Path L2 = Path.of("link2"); + Path L2 = home.resolve("link2"); Files.createSymbolicLink(L2, target); - allFiles.add(L2); - Path L1 = Path.of("link1"); + Path L1 = home.resolve("link1"); Files.createSymbolicLink(L1, L2); - allFiles.add(L1); Path cible = home.resolve("cible"); Files.createFile(cible); - allFiles.add(cible); - Path L4 = Path.of("link4"); + Path L4 = home.resolve("link4"); Files.createSymbolicLink(L4, cible); - allFiles.add(L4); - Path L3 = Path.of("link3"); + Path L3 = home.resolve("link3"); Files.createSymbolicLink(L3, L4); - allFiles.add(L3); List list = new ArrayList(); list.add(Arguments.of(false, L1, L3)); @@ -338,23 +316,19 @@ public class IsSameFile { private Stream unequalNotFollowingSource() throws IOException { deleteFiles(); - Path doesNotExist = Path.of("doesNotExist"); + Path doesNotExist = home.resolve("doesNotExist"); - Path L2 = Path.of("link2"); + Path L2 = home.resolve("link2"); Files.createSymbolicLink(L2, doesNotExist); - allFiles.add(L2); - Path L1 = Path.of("link1"); + Path L1 = home.resolve("link1"); Files.createSymbolicLink(L1, L2); - allFiles.add(L1); - Path L4 = Path.of("link4"); + Path L4 = home.resolve("link4"); Files.createSymbolicLink(L4, doesNotExist); - allFiles.add(L4); - Path L3 = Path.of("link3"); + Path L3 = home.resolve("link3"); Files.createSymbolicLink(L3, L4); - allFiles.add(L3); List list = new ArrayList(); list.add(Arguments.of(false, L1, L3)); @@ -378,13 +352,10 @@ public class IsSameFile { deleteFiles(); Path target = home.resolve("target"); Files.createFile(target); - allFiles.add(target); Path[] links = new Path[4]; - links[3] = Files.createSymbolicLink(Path.of("link4"), target); - allFiles.add(links[3]); + links[3] = Files.createSymbolicLink(home.resolve("link4"), target); for (int i = 3; i > 0; i--) { - links[i-1] = Files.createSymbolicLink(Path.of("link"+i), links[i]); - allFiles.add(links[i-1]); + links[i-1] = Files.createSymbolicLink(home.resolve("link"+i), links[i]); } List list = new ArrayList(); @@ -414,15 +385,11 @@ public class IsSameFile { deleteFiles(); Path target = home.resolve("target"); Files.createFile(target); - allFiles.add(target); Path[] links = new Path[4]; - links[3] = Files.createSymbolicLink(Path.of("link4"), target); - allFiles.add(links[3]); + links[3] = Files.createSymbolicLink(home.resolve("link4"), target); Files.delete(target); - allFiles.remove(target); for (int i = 3; i > 0; i--) { - links[i-1] = Files.createSymbolicLink(Path.of("link"+i), links[i]); - allFiles.add(links[i-1]); + links[i-1] = Files.createSymbolicLink(home.resolve("link"+i), links[i]); } List list = new ArrayList(); @@ -454,9 +421,9 @@ public class IsSameFile { Path link1 = home.resolve("L1"); Path link2 = home.resolve("L2"); Path link3 = home.resolve("L3"); - allFiles.add(Files.createSymbolicLink(link1, link2)); - allFiles.add(Files.createSymbolicLink(link2, link3)); - allFiles.add(Files.createSymbolicLink(link3, link1)); + Files.createSymbolicLink(link1, link2); + Files.createSymbolicLink(link2, link3); + Files.createSymbolicLink(link3, link1); List list = new ArrayList(); list.add(Arguments.of(true, link1, link2)); From 5d93242028dfc68b838a8efb0fbc4de3fea7fa0d Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Wed, 24 Sep 2025 19:29:07 +0000 Subject: [PATCH 217/556] 8368335: Refactor the rest of Locale TestNG based tests to JUnit Reviewed-by: naoto, liach --- .../java/util/Locale/LocaleMatchingTest.java | 186 ++++++++++-------- .../Locale/RequiredAvailableLocalesTest.java | 33 ++-- test/jdk/java/util/Locale/TestOf.java | 50 ++--- .../util/Locale/bcp47u/CalendarTests.java | 63 +++--- .../Locale/bcp47u/CurrencyFormatTests.java | 19 +- .../util/Locale/bcp47u/CurrencyTests.java | 31 ++- .../util/Locale/bcp47u/DisplayNameTests.java | 19 +- .../java/util/Locale/bcp47u/FormatTests.java | 49 +++-- .../java/util/Locale/bcp47u/SymbolsTests.java | 33 ++-- .../Locale/bcp47u/SystemPropertyTests.java | 25 ++- 10 files changed, 262 insertions(+), 246 deletions(-) diff --git a/test/jdk/java/util/Locale/LocaleMatchingTest.java b/test/jdk/java/util/Locale/LocaleMatchingTest.java index 975e4567e18..4509968b4e4 100644 --- a/test/jdk/java/util/Locale/LocaleMatchingTest.java +++ b/test/jdk/java/util/Locale/LocaleMatchingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,9 +25,13 @@ * @test * @bug 7069824 8042360 8032842 8175539 8210443 8242010 8276302 * @summary Verify implementation for Locale matching. - * @run testng/othervm LocaleMatchingTest + * @run junit/othervm LocaleMatchingTest */ +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -40,15 +44,14 @@ import java.util.Map; import static java.util.Locale.FilteringMode.*; import static java.util.Locale.LanguageRange.*; -import static org.testng.Assert.*; - -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; public class LocaleMatchingTest { - @DataProvider(name = "LRConstructorData") - Object[][] LRConstructorData() { + static Object[][] LRConstructorData() { return new Object[][] { // Range, Weight {"elvish", MAX_WEIGHT}, @@ -62,8 +65,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LRConstructorNPEData") - Object[][] LRConstructorNPEData() { + static Object[][] LRConstructorNPEData() { return new Object[][] { // Range, Weight {null, MAX_WEIGHT}, @@ -71,8 +73,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LRConstructorIAEData") - Object[][] LRConstructorIAEData() { + static Object[][] LRConstructorIAEData() { return new Object[][] { // Range, Weight {"ja", -0.8}, @@ -93,8 +94,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LRParseData") - Object[][] LRParseData() { + static Object[][] LRParseData() { return new Object[][] { // Ranges, Expected result {"Accept-Language: fr-FX, de-DE;q=0.5, fr-tp-x-FOO;q=0.1, " @@ -139,8 +139,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LRParseIAEData") - Object[][] LRParseIAEData() { + static Object[][] LRParseIAEData() { return new Object[][] { // Ranges {""}, @@ -148,8 +147,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LRMapEquivalentsData") - Object[][] LRMapEquivalentsData() { + static Object[][] LRMapEquivalentsData() { return new Object[][] { // Ranges, Map, Expected result {LanguageRange.parse("zh, zh-TW;q=0.8, ar;q=0.9, EN, zh-HK, ja-JP;q=0.2, es;q=0.4"), @@ -181,8 +179,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LFilterData") - Object[][] LFilterData() { + static Object[][] LFilterData() { return new Object[][] { // Range, LanguageTags, FilteringMode, Expected locales {"ja-JP, fr-FR", "de-DE, en, ja-JP-hepburn, fr, he, ja-Latn-JP", @@ -203,8 +200,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LFilterNPEData") - Object[][] LFilterNPEData() { + static Object[][] LFilterNPEData() { return new Object[][] { // Range, LanguageTags, FilteringMode {"en;q=0.2, ja-*-JP, fr-JP", null, REJECT_EXTENDED_RANGES}, @@ -212,8 +208,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LFilterTagsData") - Object[][] LFilterTagsData() { + static Object[][] LFilterTagsData() { return new Object[][] { // Range, LanguageTags, FilteringMode, Expected language tags {"fr-FR, fr-BG;q=0.8, *;q=0.5, en;q=0", "en-US, fr-FR, fr-CA, fr-BG", @@ -274,8 +269,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LLookupData") - Object[][] LLookupData() { + static Object[][] LLookupData() { return new Object[][] { // Range, LanguageTags, Expected locale {"en;q=0.2, *-JP;q=0.6, iw", "de-DE, en, ja-JP-hepburn, fr-JP, he", "he"}, @@ -284,8 +278,7 @@ public class LocaleMatchingTest { }; } - @DataProvider(name = "LLookupTagData") - Object[][] LLookupTagData() { + static Object[][] LLookupTagData() { return new Object[][] { // Range, LanguageTags, Expected language tag {"en, *", "es, de, ja-JP", null}, @@ -298,117 +291,135 @@ public class LocaleMatchingTest { } @Test - public void testLRConstants() { - assertEquals(MIN_WEIGHT, 0.0, " MIN_WEIGHT should be 0.0 but got " + void testLRConstants() { + assertEquals(0.0, MIN_WEIGHT, " MIN_WEIGHT should be 0.0 but got " + MIN_WEIGHT); - assertEquals(MAX_WEIGHT, 1.0, " MAX_WEIGHT should be 1.0 but got " + assertEquals(1.0, MAX_WEIGHT, " MAX_WEIGHT should be 1.0 but got " + MAX_WEIGHT); } - @Test(dataProvider = "LRConstructorData") - public void testLRConstructors(String range, double weight) { + @MethodSource("LRConstructorData") + @ParameterizedTest + void testLRConstructors(String range, double weight) { LanguageRange lr; if (weight == MAX_WEIGHT) { lr = new LanguageRange(range); } else { lr = new LanguageRange(range, weight); } - assertEquals(lr.getRange(), range.toLowerCase(Locale.ROOT), + assertEquals(range.toLowerCase(Locale.ROOT), lr.getRange(), " LR.getRange() returned unexpected value. Expected: " + range.toLowerCase(Locale.ROOT) + ", got: " + lr.getRange()); - assertEquals(lr.getWeight(), weight, + assertEquals(weight, lr.getWeight(), " LR.getWeight() returned unexpected value. Expected: " + weight + ", got: " + lr.getWeight()); } - @Test(dataProvider = "LRConstructorNPEData", expectedExceptions = NullPointerException.class) - public void testLRConstructorNPE(String range, double weight) { + @MethodSource("LRConstructorNPEData") + @ParameterizedTest + void testLRConstructorNPE(String range, double weight) { if (weight == MAX_WEIGHT) { - new LanguageRange(range); + assertThrows(NullPointerException.class, () -> new LanguageRange(range)); } else { - new LanguageRange(range, weight); + assertThrows(NullPointerException.class, () -> new LanguageRange(range, weight)); } } - @Test(dataProvider = "LRConstructorIAEData", expectedExceptions = IllegalArgumentException.class) - public void testLRConstructorIAE(String range, double weight) { + @MethodSource("LRConstructorIAEData") + @ParameterizedTest + void testLRConstructorIAE(String range, double weight) { if (weight == MAX_WEIGHT) { - new LanguageRange(range); + assertThrows(IllegalArgumentException.class, () -> new LanguageRange(range)); } else { - new LanguageRange(range, weight); + assertThrows(IllegalArgumentException.class, () -> new LanguageRange(range, weight)); } } @Test - public void testLREquals() { + void testLREquals() { LanguageRange lr1 = new LanguageRange("ja", 1.0); LanguageRange lr2 = new LanguageRange("ja"); LanguageRange lr3 = new LanguageRange("ja", 0.1); LanguageRange lr4 = new LanguageRange("en", 1.0); - assertEquals(lr1, lr2, " LR(ja, 1.0).equals(LR(ja)) should return true."); - assertNotEquals(lr1, lr3, " LR(ja, 1.0).equals(LR(ja, 0.1)) should return false."); - assertNotEquals(lr1, lr4, " LR(ja, 1.0).equals(LR(en, 1.0)) should return false."); + assertEquals(lr2, lr1, " LR(ja, 1.0).equals(LR(ja)) should return true."); + assertNotEquals(lr3, lr1, " LR(ja, 1.0).equals(LR(ja, 0.1)) should return false."); + assertNotEquals(lr4, lr1, " LR(ja, 1.0).equals(LR(en, 1.0)) should return false."); assertNotNull(lr1, " LR(ja, 1.0).equals(null) should return false."); - assertNotEquals(lr1, "", " LR(ja, 1.0).equals(\"\") should return false."); + assertNotEquals("", lr1, " LR(ja, 1.0).equals(\"\") should return false."); } - @Test(dataProvider = "LRParseData") - public void testLRParse(String ranges, List expected) { - assertEquals(LanguageRange.parse(ranges), expected, + @MethodSource("LRParseData") + @ParameterizedTest + void testLRParse(String ranges, List expected) { + assertEquals(expected, LanguageRange.parse(ranges), " LR.parse(" + ranges + ") test failed."); } - @Test(expectedExceptions = NullPointerException.class) - public void testLRParseNPE() { - LanguageRange.parse(null); + @Test + void testLRParseNPE() { + assertThrows(NullPointerException.class, () -> LanguageRange.parse(null)); } - @Test(dataProvider = "LRParseIAEData", expectedExceptions = IllegalArgumentException.class) - public void testLRParseIAE(String ranges) { - LanguageRange.parse(ranges); + @MethodSource("LRParseIAEData") + @ParameterizedTest + void testLRParseIAE(String ranges) { + assertThrows(IllegalArgumentException.class, () -> LanguageRange.parse(ranges)); } - @Test(dataProvider = "LRMapEquivalentsData") - public void testLRMapEquivalents(List priorityList, + @MethodSource("LRMapEquivalentsData") + @ParameterizedTest + void testLRMapEquivalents(List priorityList, Map> map, List expected) { - assertEquals(LanguageRange.mapEquivalents(priorityList, map), expected, + assertEquals(expected, LanguageRange.mapEquivalents(priorityList, map), " LR.mapEquivalents() test failed."); } - @Test(expectedExceptions = NullPointerException.class) - public void testLRMapEquivalentsNPE() { - LanguageRange.mapEquivalents(null, Map.of("ja", List.of("ja", "ja-Hira"))); + @Test + void testLRMapEquivalentsNPE() { + assertThrows(NullPointerException.class, + () -> LanguageRange.mapEquivalents(null, Map.of("ja", List.of("ja", "ja-Hira")))); } - @Test(dataProvider = "LFilterData") - public void testLFilter(String ranges, String tags, FilteringMode mode, String expectedLocales) { + @MethodSource("LFilterData") + @ParameterizedTest + void testLFilter(String ranges, String tags, FilteringMode mode, String expectedLocales) { List priorityList = LanguageRange.parse(ranges); List tagList = generateLocales(tags); String actualLocales = showLocales(Locale.filter(priorityList, tagList, mode)); - assertEquals(actualLocales, expectedLocales, showErrorMessage(" L.Filter(" + mode + ")", + assertEquals(expectedLocales, actualLocales, showErrorMessage(" L.Filter(" + mode + ")", ranges, tags, expectedLocales, actualLocales)); } - @Test(dataProvider = "LFilterNPEData", expectedExceptions = NullPointerException.class) - public void testLFilterNPE(String ranges, String tags, FilteringMode mode) { - List priorityList = LanguageRange.parse(ranges); - List tagList = generateLocales(tags); - showLocales(Locale.filter(priorityList, tagList, mode)); + @MethodSource("LFilterNPEData") + @ParameterizedTest + void testLFilterNPE(String ranges, String tags, FilteringMode mode) { + if (ranges == null) { + // Ranges are null + assertThrows(NullPointerException.class, () -> LanguageRange.parse(ranges)); + } else { + // Tags are null + List priorityList = LanguageRange.parse(ranges); + List tagList = generateLocales(tags); + assertThrows(NullPointerException.class, + () -> showLocales(Locale.filter(priorityList, tagList, mode))); + } } - @Test(expectedExceptions = IllegalArgumentException.class) - public void testLFilterIAE() { + @Test + void testLFilterIAE() { String ranges = "en;q=0.2, ja-*-JP, fr-JP"; String tags = "de-DE, en, ja-JP-hepburn, fr, he, ja-Latn-JP"; List priorityList = LanguageRange.parse(ranges); List tagList = generateLocales(tags); - showLocales(Locale.filter(priorityList, tagList, REJECT_EXTENDED_RANGES)); + assertThrows(IllegalArgumentException.class, + () -> showLocales(Locale.filter(priorityList, tagList, REJECT_EXTENDED_RANGES))); } - @Test(dataProvider = "LFilterTagsData") - public void testLFilterTags(String ranges, String tags, FilteringMode mode, String expectedTags) { + @MethodSource("LFilterTagsData") + @ParameterizedTest + void testLFilterTags(String ranges, String tags, FilteringMode mode, String expectedTags) { List priorityList = LanguageRange.parse(ranges); List tagList = generateLanguageTags(tags); String actualTags; @@ -417,36 +428,39 @@ public class LocaleMatchingTest { } else { actualTags = showLanguageTags(Locale.filterTags(priorityList, tagList, mode)); } - assertEquals(actualTags, expectedTags, + assertEquals(expectedTags, actualTags, showErrorMessage(" L.FilterTags(" + (mode != null ? mode : "") + ")", ranges, tags, expectedTags, actualTags)); } - @Test(expectedExceptions = IllegalArgumentException.class) - public void testLFilterTagsIAE() { + @Test + void testLFilterTagsIAE() { String ranges = "de-*-DE"; String tags = "de-DE, de-de, de-Latn-DE, de-Latf-DE, de-DE-x-goethe, " + "de-Latn-DE-1996, de-Deva-DE, de, de-x-DE, de-Deva"; List priorityList = LanguageRange.parse(ranges); - showLanguageTags(Locale.filterTags(priorityList, generateLanguageTags(tags), REJECT_EXTENDED_RANGES)); + assertThrows(IllegalArgumentException.class, + () -> showLanguageTags(Locale.filterTags(priorityList, generateLanguageTags(tags), REJECT_EXTENDED_RANGES))); } - @Test(dataProvider = "LLookupData") - public void testLLookup(String ranges, String tags, String expectedLocale) { + @MethodSource("LLookupData") + @ParameterizedTest + void testLLookup(String ranges, String tags, String expectedLocale) { List priorityList = LanguageRange.parse(ranges); List localeList = generateLocales(tags); String actualLocale = Locale.lookup(priorityList, localeList).toLanguageTag(); - assertEquals(actualLocale, expectedLocale, showErrorMessage(" L.Lookup()", + assertEquals(expectedLocale, actualLocale, showErrorMessage(" L.Lookup()", ranges, tags, expectedLocale, actualLocale)); } - @Test(dataProvider = "LLookupTagData") - public void testLLookupTag(String ranges, String tags, String expectedTag) { + @MethodSource("LLookupTagData") + @ParameterizedTest + void testLLookupTag(String ranges, String tags, String expectedTag) { List priorityList = LanguageRange.parse(ranges); List tagList = generateLanguageTags(tags); String actualTag = Locale.lookupTag(priorityList, tagList); - assertEquals(actualTag, expectedTag, showErrorMessage(" L.LookupTag()", + assertEquals(expectedTag, actualTag, showErrorMessage(" L.LookupTag()", ranges, tags, expectedTag, actualTag)); } diff --git a/test/jdk/java/util/Locale/RequiredAvailableLocalesTest.java b/test/jdk/java/util/Locale/RequiredAvailableLocalesTest.java index 017e8af0fde..880facda93c 100644 --- a/test/jdk/java/util/Locale/RequiredAvailableLocalesTest.java +++ b/test/jdk/java/util/Locale/RequiredAvailableLocalesTest.java @@ -20,33 +20,41 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -/** + +/* * @test * @bug 8276186 8174269 * @summary Checks whether getAvailableLocales() returns at least Locale.ROOT and * Locale.US instances. - * @run testng RequiredAvailableLocalesTest + * @run junit RequiredAvailableLocalesTest */ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.text.*; +import java.text.BreakIterator; +import java.text.Collator; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; import java.time.format.DecimalStyle; -import java.util.*; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Locale; +import java.util.Set; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; -import static org.testng.Assert.assertTrue; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertTrue; -@Test public class RequiredAvailableLocalesTest { private static final Set REQUIRED_LOCALES = Set.of(Locale.ROOT, Locale.US); private static final MethodType ARRAY_RETURN_TYPE = MethodType.methodType(Locale.class.arrayType()); private static final MethodType SET_RETURN_TYPE = MethodType.methodType(Set.class); - @DataProvider - public Object[][] availableLocalesClasses() { + static Object[][] availableLocalesClasses() { return new Object[][] { {BreakIterator.class, ARRAY_RETURN_TYPE}, {Calendar.class, ARRAY_RETURN_TYPE}, @@ -60,8 +68,9 @@ public class RequiredAvailableLocalesTest { }; } - @Test (dataProvider = "availableLocalesClasses") - public void checkRequiredLocales(Class c, MethodType mt) throws Throwable { + @MethodSource("availableLocalesClasses") + @ParameterizedTest + void checkRequiredLocales(Class c, MethodType mt) throws Throwable { var ret = MethodHandles.lookup().findStatic(c, "getAvailableLocales", mt).invoke(); if (ret instanceof Locale[] a) { diff --git a/test/jdk/java/util/Locale/TestOf.java b/test/jdk/java/util/Locale/TestOf.java index c923da832bf..3e0e5e74207 100644 --- a/test/jdk/java/util/Locale/TestOf.java +++ b/test/jdk/java/util/Locale/TestOf.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,29 +20,30 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -/** + +/* * @test * @bug 8282819 * @summary Unit tests for Locale.of() method. Those tests check the equality * of obtained objects with ones that are gotten from other means with both * well-formed and ill-formed arguments. Also checks the possible NPEs * for error cases. - * @run testng TestOf + * @run junit TestOf */ -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.Locale; -import org.testng.annotations.Test; -import org.testng.annotations.DataProvider; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; @SuppressWarnings("deprecation") -@Test public class TestOf { - @DataProvider - public Object[][] data_1Arg() { + static Object[][] data_1Arg() { return new Object[][]{ // well-formed {Locale.ENGLISH, "en"}, @@ -55,8 +56,7 @@ public class TestOf { }; } - @DataProvider - public Object[][] data_2Args() { + static Object[][] data_2Args() { return new Object[][]{ // well-formed {Locale.US, "en", "US"}, @@ -69,8 +69,7 @@ public class TestOf { }; } - @DataProvider - public Object[][] data_3Args() { + static Object[][] data_3Args() { return new Object[][]{ // well-formed {Locale.forLanguageTag("en-US-POSIX"), "en", "US", "POSIX"}, @@ -87,23 +86,26 @@ public class TestOf { }; } - @Test (dataProvider = "data_1Arg") - public void test_1Arg(Locale expected, String lang) { - assertEquals(Locale.of(lang), expected); + @MethodSource("data_1Arg") + @ParameterizedTest + void test_1Arg(Locale expected, String lang) { + assertEquals(expected, Locale.of(lang)); } - @Test (dataProvider = "data_2Args") - public void test_2Args(Locale expected, String lang, String ctry) { - assertEquals(Locale.of(lang, ctry), expected); + @MethodSource("data_2Args") + @ParameterizedTest + void test_2Args(Locale expected, String lang, String ctry) { + assertEquals(expected, Locale.of(lang, ctry)); } - @Test (dataProvider = "data_3Args") - public void test_3Args(Locale expected, String lang, String ctry, String vrnt) { - assertEquals(Locale.of(lang, ctry, vrnt), expected); + @MethodSource("data_3Args") + @ParameterizedTest + void test_3Args(Locale expected, String lang, String ctry, String vrnt) { + assertEquals(expected, Locale.of(lang, ctry, vrnt)); } @Test - public void test_NPE() { + void test_NPE() { assertThrows(NullPointerException.class, () -> Locale.of(null)); assertThrows(NullPointerException.class, () -> Locale.of("", null)); assertThrows(NullPointerException.class, () -> Locale.of("", "", null)); diff --git a/test/jdk/java/util/Locale/bcp47u/CalendarTests.java b/test/jdk/java/util/Locale/bcp47u/CalendarTests.java index 92cfc6581c5..f04c4a4b1ff 100644 --- a/test/jdk/java/util/Locale/bcp47u/CalendarTests.java +++ b/test/jdk/java/util/Locale/bcp47u/CalendarTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,25 +28,24 @@ * @summary Tests Calendar class deals with Unicode extensions * correctly. * @modules jdk.localedata - * @run testng/othervm CalendarTests + * @run junit/othervm CalendarTests */ -import static org.testng.Assert.assertEquals; - import java.text.DateFormat; import java.util.Calendar; import java.util.Locale; import java.util.TimeZone; -import org.testng.annotations.AfterTest; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test Calendar with BCP47 U extensions */ -@Test public class CalendarTests { private static TimeZone defaultTZ; @@ -64,19 +63,18 @@ public class CalendarTests { private static final Locale FW_FRI = Locale.forLanguageTag("en-US-u-fw-fri"); private static final Locale FW_SAT = Locale.forLanguageTag("en-US-u-fw-sat"); - @BeforeTest - public void beforeTest() { + @BeforeAll + static void beforeTest() { defaultTZ = TimeZone.getDefault(); TimeZone.setDefault(AMLA); } - @AfterTest - public void afterTest() { + @AfterAll + static void afterTest() { TimeZone.setDefault(defaultTZ); } - @DataProvider(name="tz") - Object[][] tz() { + static Object[][] tz() { return new Object[][] { // Locale, Expected Zone, {JPTYO, ASIATOKYO}, @@ -87,8 +85,7 @@ public class CalendarTests { }; } - @DataProvider(name="firstDayOfWeek") - Object[][] firstDayOfWeek () { + static Object[][] firstDayOfWeek () { return new Object[][] { // Locale, Expected DayOfWeek, {Locale.US, Calendar.SUNDAY}, @@ -114,8 +111,7 @@ public class CalendarTests { }; } - @DataProvider(name="minDaysInFirstWeek") - Object[][] minDaysInFrstWeek () { + static Object[][] minDaysInFirstWeek () { return new Object[][] { // Locale, Expected minDay, {Locale.US, 1}, @@ -126,33 +122,36 @@ public class CalendarTests { }; } - @Test(dataProvider="tz") - public void test_tz(Locale locale, TimeZone zoneExpected) { + @MethodSource("tz") + @ParameterizedTest + void test_tz(Locale locale, TimeZone zoneExpected) { DateFormat df = DateFormat.getTimeInstance(DateFormat.FULL, locale); - assertEquals(df.getTimeZone(), zoneExpected); + assertEquals(zoneExpected, df.getTimeZone()); Calendar c = Calendar.getInstance(locale); - assertEquals(c.getTimeZone(), zoneExpected); + assertEquals(zoneExpected, c.getTimeZone()); c = new Calendar.Builder().setLocale(locale).build(); - assertEquals(c.getTimeZone(), zoneExpected); + assertEquals(zoneExpected, c.getTimeZone()); } - @Test(dataProvider="firstDayOfWeek") - public void test_firstDayOfWeek(Locale locale, int dowExpected) { + @MethodSource("firstDayOfWeek") + @ParameterizedTest + void test_firstDayOfWeek(Locale locale, int dowExpected) { Calendar c = Calendar.getInstance(locale); - assertEquals(c.getFirstDayOfWeek(), dowExpected); + assertEquals(dowExpected, c.getFirstDayOfWeek()); c = new Calendar.Builder().setLocale(locale).build(); - assertEquals(c.getFirstDayOfWeek(), dowExpected); + assertEquals(dowExpected, c.getFirstDayOfWeek()); } - @Test(dataProvider="minDaysInFirstWeek") - public void test_minDaysInFirstWeek(Locale locale, int minDaysExpected) { + @MethodSource("minDaysInFirstWeek") + @ParameterizedTest + void test_minDaysInFirstWeek(Locale locale, int minDaysExpected) { Calendar c = Calendar.getInstance(locale); - assertEquals(c.getMinimalDaysInFirstWeek(), minDaysExpected); + assertEquals(minDaysExpected, c.getMinimalDaysInFirstWeek()); c = new Calendar.Builder().setLocale(locale).build(); - assertEquals(c.getMinimalDaysInFirstWeek(), minDaysExpected); + assertEquals(minDaysExpected, c.getMinimalDaysInFirstWeek()); } } diff --git a/test/jdk/java/util/Locale/bcp47u/CurrencyFormatTests.java b/test/jdk/java/util/Locale/bcp47u/CurrencyFormatTests.java index feb52a49df5..863b4bb688f 100644 --- a/test/jdk/java/util/Locale/bcp47u/CurrencyFormatTests.java +++ b/test/jdk/java/util/Locale/bcp47u/CurrencyFormatTests.java @@ -27,26 +27,24 @@ * @bug 8215181 8230284 8231273 8284840 * @summary Tests the "u-cf" extension * @modules jdk.localedata - * @run testng CurrencyFormatTests + * @run junit CurrencyFormatTests */ -import static org.testng.Assert.assertEquals; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.text.NumberFormat; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test NumberFormat with BCP47 u-cf extensions. Note that this test depends * on the particular CLDR release. Results may vary on other CLDR releases. */ -@Test public class CurrencyFormatTests { - @DataProvider(name="getInstanceData") - Object[][] getInstanceData() { + static Object[][] getInstanceData() { return new Object[][] { // Locale, amount, expected // US dollar @@ -97,8 +95,9 @@ public class CurrencyFormatTests { }; } - @Test(dataProvider="getInstanceData") - public void test_getInstance(Locale locale, int amount, String expected) { - assertEquals(NumberFormat.getCurrencyInstance(locale).format(amount), expected); + @MethodSource("getInstanceData") + @ParameterizedTest + void test_getInstance(Locale locale, int amount, String expected) { + assertEquals(expected, NumberFormat.getCurrencyInstance(locale).format(amount)); } } diff --git a/test/jdk/java/util/Locale/bcp47u/CurrencyTests.java b/test/jdk/java/util/Locale/bcp47u/CurrencyTests.java index d807a0e4da3..a89aed4b0c5 100644 --- a/test/jdk/java/util/Locale/bcp47u/CurrencyTests.java +++ b/test/jdk/java/util/Locale/bcp47u/CurrencyTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,28 +28,26 @@ * @summary Tests Currency class instantiates correctly with Unicode * extensions * @modules jdk.localedata - * @run testng/othervm CurrencyTests + * @run junit/othervm CurrencyTests */ -import static org.testng.Assert.assertEquals; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.Currency; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test Currency with BCP47 U extensions */ -@Test public class CurrencyTests { private static final Currency USD = Currency.getInstance("USD"); private static final Currency CAD = Currency.getInstance("CAD"); private static final Currency JPY = Currency.getInstance("JPY"); - @DataProvider(name="getInstanceData") - Object[][] getInstanceData() { + static Object[][] getInstanceData() { return new Object[][] { // Locale, Expected Currency // "cu" @@ -76,8 +74,7 @@ public class CurrencyTests { }; } - @DataProvider(name="getSymbolData") - Object[][] getSymbolData() { + static Object[][] getSymbolData() { return new Object[][] { // Currency, DisplayLocale, expected Symbol {USD, Locale.forLanguageTag("en-US-u-rg-jpzzzz"), "$"}, @@ -94,13 +91,15 @@ public class CurrencyTests { }; } - @Test(dataProvider="getInstanceData") - public void test_getInstance(Locale locale, Currency currencyExpected) { - assertEquals(Currency.getInstance(locale), currencyExpected); + @MethodSource("getInstanceData") + @ParameterizedTest + void test_getInstance(Locale locale, Currency currencyExpected) { + assertEquals(currencyExpected, Currency.getInstance(locale)); } - @Test(dataProvider="getSymbolData") - public void test_getSymbol(Currency c, Locale locale, String expected) { - assertEquals(c.getSymbol(locale), expected); + @MethodSource("getSymbolData") + @ParameterizedTest + void test_getSymbol(Currency c, Locale locale, String expected) { + assertEquals(expected, c.getSymbol(locale)); } } diff --git a/test/jdk/java/util/Locale/bcp47u/DisplayNameTests.java b/test/jdk/java/util/Locale/bcp47u/DisplayNameTests.java index 1f1a4d3e8ca..71606933bfb 100644 --- a/test/jdk/java/util/Locale/bcp47u/DisplayNameTests.java +++ b/test/jdk/java/util/Locale/bcp47u/DisplayNameTests.java @@ -27,21 +27,20 @@ * @bug 8176841 8202537 * @summary Tests the display names for BCP 47 U extensions * @modules jdk.localedata - * @run testng DisplayNameTests + * @run junit DisplayNameTests */ -import static org.testng.Assert.assertEquals; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test Locale.getDisplayName() with BCP47 U extensions. Note that the * result may change depending on the CLDR releases. */ -@Test public class DisplayNameTests { private static final Locale loc1 = Locale.forLanguageTag("en-Latn-US-u" + "-ca-japanese" + @@ -75,8 +74,7 @@ public class DisplayNameTests { .build(); private static final Locale loc6 = Locale.forLanguageTag( "zh-CN-u-ca-dddd-nu-ddd-cu-ddd-fw-moq-tz-unknown-rg-twzz"); - @DataProvider(name="locales") - Object[][] tz() { + static Object[][] locales() { return new Object[][] { // Locale for display, Test Locale, Expected output, {Locale.US, loc1, @@ -101,9 +99,10 @@ public class DisplayNameTests { }; } - @Test(dataProvider="locales") - public void test_locales(Locale inLocale, Locale testLocale, String expected) { + @MethodSource("locales") + @ParameterizedTest + void test_locales(Locale inLocale, Locale testLocale, String expected) { String result = testLocale.getDisplayName(inLocale); - assertEquals(result, expected); + assertEquals(expected, result); } } diff --git a/test/jdk/java/util/Locale/bcp47u/FormatTests.java b/test/jdk/java/util/Locale/bcp47u/FormatTests.java index 3e4b0bf5ea0..4ad676e56c5 100644 --- a/test/jdk/java/util/Locale/bcp47u/FormatTests.java +++ b/test/jdk/java/util/Locale/bcp47u/FormatTests.java @@ -28,10 +28,13 @@ * @summary Tests *Format class deals with Unicode extensions * correctly. * @modules jdk.localedata - * @run testng FormatTests + * @run junit FormatTests */ -import static org.testng.Assert.assertEquals; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.text.DateFormat; import java.text.NumberFormat; @@ -40,15 +43,11 @@ import java.util.Date; import java.util.Locale; import java.util.TimeZone; -import org.testng.annotations.AfterTest; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test *Format classes with BCP47 U extensions */ -@Test public class FormatTests { private static TimeZone defaultTZ; @@ -80,19 +79,18 @@ public class FormatTests { .build() .getTime(); - @BeforeTest - public void beforeTest() { + @BeforeAll + static void beforeTest() { defaultTZ = TimeZone.getDefault(); TimeZone.setDefault(AMLA); } - @AfterTest - public void afterTest() { + @AfterAll + static void afterTest() { TimeZone.setDefault(defaultTZ); } - @DataProvider(name="dateFormatData") - Object[][] dateFormatData() { + static Object[][] dateFormatData() { return new Object[][] { // Locale, Expected calendar, Expected timezone, Expected formatted string @@ -116,8 +114,7 @@ public class FormatTests { }; } - @DataProvider(name="numberFormatData") - Object[][] numberFormatData() { + static Object[][] numberFormatData() { return new Object[][] { // Locale, number, expected format @@ -136,33 +133,35 @@ public class FormatTests { }; } - @Test(dataProvider="dateFormatData") - public void test_DateFormat(Locale locale, String calClass, TimeZone tz, + @MethodSource("dateFormatData") + @ParameterizedTest + void test_DateFormat(Locale locale, String calClass, TimeZone tz, String formatExpected) throws Exception { DateFormat df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, locale); if (calClass != null) { try { Class expected = Class.forName(calClass); - assertEquals(df.getCalendar().getClass(), expected); + assertEquals(expected, df.getCalendar().getClass()); } catch (Exception e) { throw e; } } if (tz != null) { - assertEquals(df.getTimeZone(), tz); + assertEquals(tz, df.getTimeZone()); } String formatted = df.format(testDate); - assertEquals(formatted, formatExpected); - assertEquals(df.parse(formatted), testDate); + assertEquals(formatExpected, formatted); + assertEquals(testDate, df.parse(formatted)); } - @Test(dataProvider="numberFormatData") - public void test_NumberFormat(Locale locale, double num, + @MethodSource("numberFormatData") + @ParameterizedTest + void test_NumberFormat(Locale locale, double num, String formatExpected) throws Exception { NumberFormat nf = NumberFormat.getNumberInstance(locale); nf.setMaximumFractionDigits(4); String formatted = nf.format(num); - assertEquals(nf.format(num), formatExpected); - assertEquals(nf.parse(formatted), num); + assertEquals(formatExpected, nf.format(num)); + assertEquals(num, nf.parse(formatted)); } } diff --git a/test/jdk/java/util/Locale/bcp47u/SymbolsTests.java b/test/jdk/java/util/Locale/bcp47u/SymbolsTests.java index 451eac4c050..9b25b803243 100644 --- a/test/jdk/java/util/Locale/bcp47u/SymbolsTests.java +++ b/test/jdk/java/util/Locale/bcp47u/SymbolsTests.java @@ -28,30 +28,28 @@ * @summary Tests *FormatSymbols class deals with Unicode extensions * correctly. * @modules jdk.localedata - * @run testng SymbolsTests + * @run junit SymbolsTests */ -import static org.testng.Assert.assertEquals; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.text.DateFormatSymbols; import java.text.DecimalFormatSymbols; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test *FormatSymbols classes with BCP47 U extensions */ -@Test public class SymbolsTests { private static final Locale RG_GB = Locale.forLanguageTag("en-US-u-rg-gbzzzz"); private static final Locale RG_IE = Locale.forLanguageTag("en-US-u-rg-iezzzz"); private static final Locale RG_AT = Locale.forLanguageTag("en-US-u-rg-atzzzz"); - @DataProvider(name="dateFormatSymbolsData") - Object[][] dateFormatSymbolsData() { + static Object[][] dateFormatSymbolsData() { return new Object[][] { // Locale, expected AM string, expected PM string @@ -61,8 +59,7 @@ public class SymbolsTests { }; } - @DataProvider(name="decimalFormatSymbolsData") - Object[][] decimalFormatSymbolsData() { + static Object[][] decimalFormatSymbolsData() { return new Object[][] { // Locale, expected decimal separator, expected grouping separator @@ -74,18 +71,20 @@ public class SymbolsTests { }; } - @Test(dataProvider="dateFormatSymbolsData") - public void test_DateFormatSymbols(Locale locale, String amExpected, String pmExpected) { + @MethodSource("dateFormatSymbolsData") + @ParameterizedTest + void test_DateFormatSymbols(Locale locale, String amExpected, String pmExpected) { DateFormatSymbols dfs = DateFormatSymbols.getInstance(locale); String[] ampm = dfs.getAmPmStrings(); - assertEquals(ampm[0], amExpected); - assertEquals(ampm[1], pmExpected); + assertEquals(amExpected, ampm[0]); + assertEquals(pmExpected, ampm[1]); } - @Test(dataProvider="decimalFormatSymbolsData") - public void test_DecimalFormatSymbols(Locale locale, char decimal, char grouping) { + @MethodSource("decimalFormatSymbolsData") + @ParameterizedTest + void test_DecimalFormatSymbols(Locale locale, char decimal, char grouping) { DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale); - assertEquals(dfs.getDecimalSeparator(), decimal); - assertEquals(dfs.getGroupingSeparator(), grouping); + assertEquals(decimal, dfs.getDecimalSeparator()); + assertEquals(grouping, dfs.getGroupingSeparator()); } } diff --git a/test/jdk/java/util/Locale/bcp47u/SystemPropertyTests.java b/test/jdk/java/util/Locale/bcp47u/SystemPropertyTests.java index 905fc9a79c8..3c34d74337d 100644 --- a/test/jdk/java/util/Locale/bcp47u/SystemPropertyTests.java +++ b/test/jdk/java/util/Locale/bcp47u/SystemPropertyTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,30 +28,26 @@ * @summary Tests the system properties * @modules jdk.localedata * @build DefaultLocaleTest - * @run testng/othervm SystemPropertyTests + * @run junit/othervm SystemPropertyTests */ +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import static jdk.test.lib.process.ProcessTools.executeTestJava; -import static org.testng.Assert.assertTrue; - -import java.util.Locale; - -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test Locale.getDefault() reflects the system property. Note that the * result may change depending on the CLDR releases. */ -@Test public class SystemPropertyTests { private static String LANGPROP = "-Duser.language=en"; private static String SCPTPROP = "-Duser.script="; private static String CTRYPROP = "-Duser.country=US"; - @DataProvider(name="data") - Object[][] data() { + static Object[][] data() { return new Object[][] { // system property, expected default, expected format, expected display {"-Duser.extensions=u-ca-japanese", @@ -86,8 +82,9 @@ public class SystemPropertyTests { }; } - @Test(dataProvider="data") - public void runTest(String extprop, String defLoc, + @MethodSource("data") + @ParameterizedTest + void runTest(String extprop, String defLoc, String defFmtLoc, String defDspLoc) throws Exception { int exitValue = executeTestJava(LANGPROP, SCPTPROP, CTRYPROP, extprop, "DefaultLocaleTest", defLoc, defFmtLoc, defDspLoc) @@ -95,6 +92,6 @@ public class SystemPropertyTests { .errorTo(System.out) .getExitValue(); - assertTrue(exitValue == 0); + assertEquals(0, exitValue); } } From 8f87fdce0b17f3edd453054461895330b82e8a71 Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Wed, 24 Sep 2025 20:52:28 +0000 Subject: [PATCH 218/556] 8368182: AOT cache creation fails with class defined by JNI Reviewed-by: dholmes, matsaave --- src/hotspot/share/cds/lambdaFormInvokers.cpp | 2 +- src/hotspot/share/classfile/classLoader.cpp | 5 +- .../cds/appcds/aotCache/JNIDefineClass.java | 138 ++++++++++++++++++ .../appcds/aotCache/libJNIDefineClassApp.c | 36 +++++ 4 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/aotCache/JNIDefineClass.java create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/aotCache/libJNIDefineClassApp.c diff --git a/src/hotspot/share/cds/lambdaFormInvokers.cpp b/src/hotspot/share/cds/lambdaFormInvokers.cpp index 946599dddbf..19dae28c5b5 100644 --- a/src/hotspot/share/cds/lambdaFormInvokers.cpp +++ b/src/hotspot/share/cds/lambdaFormInvokers.cpp @@ -200,7 +200,7 @@ void LambdaFormInvokers::regenerate_holder_classes(TRAPS) { // make a copy of class bytes so GC will not affect us. char *buf = NEW_RESOURCE_ARRAY(char, len); memcpy(buf, (char*)h_bytes->byte_at_addr(0), len); - ClassFileStream st((u1*)buf, len, nullptr); + ClassFileStream st((u1*)buf, len, "jrt:/java.base"); regenerate_class(class_name, st, CHECK); } } diff --git a/src/hotspot/share/classfile/classLoader.cpp b/src/hotspot/share/classfile/classLoader.cpp index 3c7f6f8130e..082c745f4c3 100644 --- a/src/hotspot/share/classfile/classLoader.cpp +++ b/src/hotspot/share/classfile/classLoader.cpp @@ -1192,10 +1192,7 @@ void ClassLoader::record_result(JavaThread* current, InstanceKlass* ik, oop loader = ik->class_loader(); char* src = (char*)stream->source(); if (src == nullptr) { - if (loader == nullptr) { - // JFR classes - ik->set_shared_classpath_index(0); - } + ik->set_shared_classpath_index(-1); // unsupported location return; } diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JNIDefineClass.java b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JNIDefineClass.java new file mode 100644 index 00000000000..58c2cb42681 --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JNIDefineClass.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary classes defined with JNI DefineClass should be excluded from the AOT config file and AOT cache. + * @bug 8368182 + * @requires vm.cds + * @requires vm.cds.supports.aot.class.linking + * @library /test/lib + * @build JNIDefineClass + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar + * JNIDefineClassApp ExcludedDummy ExcludedDummy2 + * @run main/native JNIDefineClass + */ + +import java.io.InputStream; +import jdk.test.lib.cds.CDSAppTester; +import jdk.test.lib.helpers.ClassFileInstaller; +import jdk.test.lib.process.OutputAnalyzer; + +public class JNIDefineClass { + static final String appJar = ClassFileInstaller.getJarPath("app.jar"); + static final String mainClass = "JNIDefineClassApp"; + + public static void main(String[] args) throws Exception { + Tester tester = new Tester(); + tester.run(new String[] {"AOT", "--two-step-training"} ); + } + + static class Tester extends CDSAppTester { + public Tester() { + super(mainClass); + } + + @Override + public String classpath(RunMode runMode) { + return appJar; + } + + @Override + public String[] vmArgs(RunMode runMode) { + return new String[] { + "--enable-native-access=ALL-UNNAMED", + "-Xlog:aot,aot+class=debug", + "-Djava.library.path=" + System.getProperty("java.library.path"), + }; + } + + @Override + public String[] appCommandLine(RunMode runMode) { + return new String[] {mainClass}; + } + + @Override + public void checkExecution(OutputAnalyzer out, RunMode runMode) { + if (runMode.isApplicationExecuted()) { + out.shouldContain("@@loader = null"); + out.shouldContain("@@name = ExcludedDummy"); + + out.shouldMatch("@@loader2 = .*AppClassLoader"); + out.shouldContain("@@name2 = ExcludedDummy2"); + } + if (runMode == RunMode.TRAINING) { + out.shouldContain("Skipping ExcludedDummy: Unsupported location"); + } + + // Must not have a log like this + /// [0.378s][debug ][aot,class] klasses[ 65] = 0x0000000800160490 boot ExcludedDummy + /// [0.378s][debug ][aot,class] klasses[ 66] = 0x0000000800160490 app ExcludedDummy2 + out.shouldNotContain("aot,class.* klasses.*ExcludedDummy"); + out.shouldNotContain("aot,class.* klasses.*ExcludedDummy2"); + } + } +} + +class JNIDefineClassApp { + + static native Class nativeDefineClass(String name, ClassLoader ldr, byte[] class_bytes); + + static { + System.loadLibrary("JNIDefineClassApp"); + } + + public static void main(java.lang.String[] unused) throws Exception { + ClassLoader appLoader = JNIDefineClassApp.class.getClassLoader(); + + try (InputStream in = appLoader.getResourceAsStream("ExcludedDummy.class")) { + byte[] b = in.readAllBytes(); + System.out.println(b.length); + Class c = nativeDefineClass("ExcludedDummy", null, b); + System.out.println("@@loader = " + c.getClassLoader()); + System.out.println("@@name = " + c.getName()); + } + + try (InputStream in = appLoader.getResourceAsStream("ExcludedDummy2.class")) { + byte[] b = in.readAllBytes(); + System.out.println(b.length); + Class c = nativeDefineClass("ExcludedDummy2", appLoader, b); + System.out.println("@@loader2 = " + c.getClassLoader()); + System.out.println("@@name2 = " + c.getName()); + } + + System.out.println("TEST PASSED"); + } +} + +// This class is loaded into the bootstrap loader using JNI DefineClass() with a null code source, +// so it should be excluded from the AOT configuration (and hence excluded from AOT cache) +class ExcludedDummy { + +} + +// This class is loaded into the app loader using JNI DefineClass() with a null code source, +// so it should be excluded from the AOT configuration (and hence excluded from AOT cache) +class ExcludedDummy2 { + +} diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/libJNIDefineClassApp.c b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/libJNIDefineClassApp.c new file mode 100644 index 00000000000..ec3ed3bc8f6 --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/libJNIDefineClassApp.c @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include + +JNIEXPORT jclass JNICALL +Java_JNIDefineClassApp_nativeDefineClass(JNIEnv* env, jclass clazz /*unused*/, + jstring className, jobject classLoader, jbyteArray bytecode) { + const char* classNameChar = (*env)->GetStringUTFChars(env, className, NULL); + jbyte* arrayContent = (*env)->GetByteArrayElements(env, bytecode, NULL); + jsize bytecodeLength = (*env)->GetArrayLength(env, bytecode); + jclass returnValue = (*env)->DefineClass(env, classNameChar, classLoader, arrayContent, bytecodeLength); + (*env)->ReleaseByteArrayElements(env, bytecode, arrayContent, JNI_ABORT); + (*env)->ReleaseStringUTFChars(env, className, classNameChar); + return returnValue; +} From 17accf4a06fe654fef6db8dbd0dcd3411729316f Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Wed, 24 Sep 2025 20:58:26 +0000 Subject: [PATCH 219/556] 8368174: Proactive initialization of @AOTSafeClassInitializer classes Reviewed-by: liach, adinn, asmehra --- src/hotspot/share/cds/aotClassInitializer.cpp | 57 ++++++++++++ src/hotspot/share/cds/aotClassInitializer.hpp | 3 + src/hotspot/share/cds/aotMetaspace.cpp | 21 ++++- src/hotspot/share/cds/finalImageRecipes.cpp | 34 ++++--- src/hotspot/share/cds/finalImageRecipes.hpp | 15 ++-- .../share/classfile/classFileParser.cpp | 40 --------- .../classes/jdk/internal/math/MathUtils.java | 2 + .../annotation/AOTSafeClassInitializer.java | 58 ++++++------ test/hotspot/jtreg/ProblemList-AotJdk.txt | 5 ++ test/hotspot/jtreg/TEST.groups | 2 + .../aotAnnotations/AOTAnnotationsTest.java | 90 +++++++++++++++++++ 11 files changed, 239 insertions(+), 88 deletions(-) create mode 100644 test/hotspot/jtreg/runtime/cds/appcds/aotAnnotations/AOTAnnotationsTest.java diff --git a/src/hotspot/share/cds/aotClassInitializer.cpp b/src/hotspot/share/cds/aotClassInitializer.cpp index 4a98ccbf990..00db747622f 100644 --- a/src/hotspot/share/cds/aotClassInitializer.cpp +++ b/src/hotspot/share/cds/aotClassInitializer.cpp @@ -48,7 +48,14 @@ bool AOTClassInitializer::can_archive_initialized_mirror(InstanceKlass* ik) { ik = RegeneratedClasses::get_original_object(ik); } + check_aot_annotations(ik); + if (!ik->is_initialized() && !ik->is_being_initialized()) { + if (ik->has_aot_safe_initializer()) { + ResourceMark rm; + log_info(aot, init)("Class %s is annotated with @AOTSafeClassInitializer but has not been initialized", + ik->external_name()); + } return false; } @@ -245,6 +252,56 @@ void AOTClassInitializer::call_runtime_setup(JavaThread* current, InstanceKlass* } } +template +void require_annotation_for_super_types(InstanceKlass* ik, const char* annotation, FUNCTION func) { + if (log_is_enabled(Info, aot, init)) { + ResourceMark rm; + log_info(aot, init)("Found %s class %s", annotation, ik->external_name()); + } + + // Since ik has this annotation, we require that + // - all super classes must have this annotation + // - all super interfaces that are interface_needs_clinit_execution_as_super() + // must have this annotation + // This avoid the situation where in the production run, we run the + // of a supertype but not the of ik + + InstanceKlass* super = ik->java_super(); + if (super != nullptr && !func(super)) { + ResourceMark rm; + log_error(aot, init)("Missing %s in superclass %s for class %s", + annotation, super->external_name(), ik->external_name()); + AOTMetaspace::unrecoverable_writing_error(); + } + + int len = ik->local_interfaces()->length(); + for (int i = 0; i < len; i++) { + InstanceKlass* intf = ik->local_interfaces()->at(i); + if (intf->interface_needs_clinit_execution_as_super() && !func(intf)) { + ResourceMark rm; + log_error(aot, init)("Missing %s in superinterface %s for class %s", + annotation, intf->external_name(), ik->external_name()); + AOTMetaspace::unrecoverable_writing_error(); + } + } +} + +void AOTClassInitializer::check_aot_annotations(InstanceKlass* ik) { + if (ik->has_aot_safe_initializer()) { + require_annotation_for_super_types(ik, "@AOTSafeClassInitializer", [&] (const InstanceKlass* supertype) { + return supertype->has_aot_safe_initializer(); + }); + } else { + // @AOTRuntimeSetup only meaningful in @AOTSafeClassInitializer + if (ik->is_runtime_setup_required()) { + ResourceMark rm; + log_error(aot, init)("@AOTRuntimeSetup meaningless in non-@AOTSafeClassInitializer class %s", + ik->external_name()); + } + } +} + + #ifdef ASSERT void AOTClassInitializer::init_test_class(TRAPS) { // -XX:AOTInitTestClass is used in regression tests for adding additional AOT-initialized classes diff --git a/src/hotspot/share/cds/aotClassInitializer.hpp b/src/hotspot/share/cds/aotClassInitializer.hpp index 974bbeb903c..b133e120146 100644 --- a/src/hotspot/share/cds/aotClassInitializer.hpp +++ b/src/hotspot/share/cds/aotClassInitializer.hpp @@ -31,6 +31,9 @@ class InstanceKlass; class AOTClassInitializer : AllStatic { + + static void check_aot_annotations(InstanceKlass* ik); + public: // Called by heapShared.cpp to see if src_ik->java_mirror() can be archived in // the initialized state. diff --git a/src/hotspot/share/cds/aotMetaspace.cpp b/src/hotspot/share/cds/aotMetaspace.cpp index 6035111416a..d80383be272 100644 --- a/src/hotspot/share/cds/aotMetaspace.cpp +++ b/src/hotspot/share/cds/aotMetaspace.cpp @@ -804,6 +804,23 @@ void AOTMetaspace::link_shared_classes(TRAPS) { AOTClassLinker::initialize(); AOTClassInitializer::init_test_class(CHECK); + if (CDSConfig::is_dumping_final_static_archive()) { + // - Load and link all classes used in the training run. + // - Initialize @AOTSafeClassInitializer classes that were + // initialized in the training run. + // - Perform per-class optimization such as AOT-resolution of + // constant pool entries that were resolved during the training run. + FinalImageRecipes::apply_recipes(CHECK); + + // Because the AOT assembly phase does not run the same exact code as in the + // training run (e.g., we use different lambda form invoker classes; + // generated lambda form classes are not recorded in FinalImageRecipes), + // the recipes do not cover all classes that have been loaded so far. As + // a result, we might have some unlinked classes at this point. Since we + // require cached classes to be linked, all such classes will be linked + // by the following step. + } + link_all_loaded_classes(THREAD); // Eargerly resolve all string constants in constant pools @@ -817,10 +834,6 @@ void AOTMetaspace::link_shared_classes(TRAPS) { AOTConstantPoolResolver::preresolve_string_cp_entries(ik, CHECK); } } - - if (CDSConfig::is_dumping_final_static_archive()) { - FinalImageRecipes::apply_recipes(CHECK); - } } void AOTMetaspace::dump_static_archive(TRAPS) { diff --git a/src/hotspot/share/cds/finalImageRecipes.cpp b/src/hotspot/share/cds/finalImageRecipes.cpp index 2d237edfd2d..dfe74acd6c1 100644 --- a/src/hotspot/share/cds/finalImageRecipes.cpp +++ b/src/hotspot/share/cds/finalImageRecipes.cpp @@ -57,7 +57,7 @@ void FinalImageRecipes::record_recipes_for_constantpool() { // ignored during the final image assembly. GrowableArray*> tmp_cp_recipes; - GrowableArray tmp_cp_flags; + GrowableArray tmp_flags; GrowableArray* klasses = ArchiveBuilder::current()->klasses(); for (int i = 0; i < klasses->length(); i++) { @@ -70,12 +70,16 @@ void FinalImageRecipes::record_recipes_for_constantpool() { ConstantPool* cp = ik->constants(); ConstantPoolCache* cp_cache = cp->cache(); + if (ik->is_initialized()) { + flags |= WAS_INITED; + } + for (int cp_index = 1; cp_index < cp->length(); cp_index++) { // Index 0 is unused if (cp->tag_at(cp_index).value() == JVM_CONSTANT_Class) { Klass* k = cp->resolved_klass_at(cp_index); if (k->is_instance_klass()) { cp_indices.append(cp_index); - flags |= HAS_CLASS; + flags |= CP_RESOLVE_CLASS; } } } @@ -88,7 +92,7 @@ void FinalImageRecipes::record_recipes_for_constantpool() { if (rfe->is_resolved(Bytecodes::_getfield) || rfe->is_resolved(Bytecodes::_putfield)) { cp_indices.append(rfe->constant_pool_index()); - flags |= HAS_FIELD_AND_METHOD; + flags |= CP_RESOLVE_FIELD_AND_METHOD; } } } @@ -103,7 +107,7 @@ void FinalImageRecipes::record_recipes_for_constantpool() { rme->is_resolved(Bytecodes::_invokestatic) || rme->is_resolved(Bytecodes::_invokehandle)) { cp_indices.append(rme->constant_pool_index()); - flags |= HAS_FIELD_AND_METHOD; + flags |= CP_RESOLVE_FIELD_AND_METHOD; } } } @@ -115,7 +119,7 @@ void FinalImageRecipes::record_recipes_for_constantpool() { int cp_index = rie->constant_pool_index(); if (rie->is_resolved()) { cp_indices.append(cp_index); - flags |= HAS_INDY; + flags |= CP_RESOLVE_INDY; } } } @@ -127,14 +131,14 @@ void FinalImageRecipes::record_recipes_for_constantpool() { } else { tmp_cp_recipes.append(nullptr); } - tmp_cp_flags.append(flags); + tmp_flags.append(flags); } _cp_recipes = ArchiveUtils::archive_array(&tmp_cp_recipes); ArchivePtrMarker::mark_pointer(&_cp_recipes); - _cp_flags = ArchiveUtils::archive_array(&tmp_cp_flags); - ArchivePtrMarker::mark_pointer(&_cp_flags); + _flags = ArchiveUtils::archive_array(&tmp_flags); + ArchivePtrMarker::mark_pointer(&_flags); } void FinalImageRecipes::apply_recipes_for_constantpool(JavaThread* current) { @@ -142,7 +146,7 @@ void FinalImageRecipes::apply_recipes_for_constantpool(JavaThread* current) { for (int i = 0; i < _all_klasses->length(); i++) { Array* cp_indices = _cp_recipes->at(i); - int flags = _cp_flags->at(i); + int flags = _flags->at(i); if (cp_indices != nullptr) { InstanceKlass* ik = InstanceKlass::cast(_all_klasses->at(i)); if (ik->is_loaded()) { @@ -152,13 +156,13 @@ void FinalImageRecipes::apply_recipes_for_constantpool(JavaThread* current) { for (int j = 0; j < cp_indices->length(); j++) { preresolve_list.at_put(cp_indices->at(j), true); } - if ((flags & HAS_CLASS) != 0) { + if ((flags & CP_RESOLVE_CLASS) != 0) { AOTConstantPoolResolver::preresolve_class_cp_entries(current, ik, &preresolve_list); } - if ((flags & HAS_FIELD_AND_METHOD) != 0) { + if ((flags & CP_RESOLVE_FIELD_AND_METHOD) != 0) { AOTConstantPoolResolver::preresolve_field_and_method_cp_entries(current, ik, &preresolve_list); } - if ((flags & HAS_INDY) != 0) { + if ((flags & CP_RESOLVE_INDY) != 0) { AOTConstantPoolResolver::preresolve_indy_cp_entries(current, ik, &preresolve_list); } } @@ -171,6 +175,7 @@ void FinalImageRecipes::load_all_classes(TRAPS) { Handle class_loader(THREAD, SystemDictionary::java_system_loader()); for (int i = 0; i < _all_klasses->length(); i++) { Klass* k = _all_klasses->at(i); + int flags = _flags->at(i); if (k->is_instance_klass()) { InstanceKlass* ik = InstanceKlass::cast(k); if (ik->defined_by_other_loaders()) { @@ -188,6 +193,11 @@ void FinalImageRecipes::load_all_classes(TRAPS) { } assert(ik->is_loaded(), "must be"); ik->link_class(CHECK); + + if (ik->has_aot_safe_initializer() && (flags & WAS_INITED) != 0) { + assert(ik->class_loader() == nullptr, "supported only for boot classes for now"); + ik->initialize(CHECK); + } } } } diff --git a/src/hotspot/share/cds/finalImageRecipes.hpp b/src/hotspot/share/cds/finalImageRecipes.hpp index 3af1e70772c..8c6038c2ab4 100644 --- a/src/hotspot/share/cds/finalImageRecipes.hpp +++ b/src/hotspot/share/cds/finalImageRecipes.hpp @@ -42,20 +42,21 @@ template class Array; // - The list of all classes that are stored in the AOTConfiguration file. // - The list of all classes that require AOT resolution of invokedynamic call sites. class FinalImageRecipes { - static constexpr int HAS_CLASS = 0x1; - static constexpr int HAS_FIELD_AND_METHOD = 0x2; - static constexpr int HAS_INDY = 0x4; + static constexpr int CP_RESOLVE_CLASS = 0x1 << 0; // CP has preresolved class entries + static constexpr int CP_RESOLVE_FIELD_AND_METHOD = 0x1 << 1; // CP has preresolved field/method entries + static constexpr int CP_RESOLVE_INDY = 0x1 << 2; // CP has preresolved indy entries + static constexpr int WAS_INITED = 0x1 << 3; // Class was initialized during training run // A list of all the archived classes from the preimage. We want to transfer all of these // into the final image. Array* _all_klasses; - // For each klass k _all_klasses->at(i), _cp_recipes->at(i) lists all the {klass,field,method,indy} - // cp indices that were resolved for k during the training run. + // For each klass k _all_klasses->at(i): _cp_recipes->at(i) lists all the {klass,field,method,indy} + // cp indices that were resolved for k during the training run; _flags->at(i) has extra info about k. Array*>* _cp_recipes; - Array* _cp_flags; + Array* _flags; - FinalImageRecipes() : _all_klasses(nullptr), _cp_recipes(nullptr), _cp_flags(nullptr) {} + FinalImageRecipes() : _all_klasses(nullptr), _cp_recipes(nullptr), _flags(nullptr) {} void* operator new(size_t size) throw(); diff --git a/src/hotspot/share/classfile/classFileParser.cpp b/src/hotspot/share/classfile/classFileParser.cpp index 11633c8cb11..52bbab01d61 100644 --- a/src/hotspot/share/classfile/classFileParser.cpp +++ b/src/hotspot/share/classfile/classFileParser.cpp @@ -5163,46 +5163,6 @@ void ClassFileParser::fill_instance_klass(InstanceKlass* ik, if (_parsed_annotations->has_any_annotations()) _parsed_annotations->apply_to(ik); - // AOT-related checks. - // Note we cannot check this in general due to instrumentation or module patching - if (CDSConfig::is_initing_classes_at_dump_time()) { - // Check the aot initialization safe status. - // @AOTSafeClassInitializer is used only to support ahead-of-time initialization of classes - // in the AOT assembly phase. - if (ik->has_aot_safe_initializer()) { - // If a type is included in the tables inside can_archive_initialized_mirror(), we require that - // - all super classes must be included - // - all super interfaces that have must be included. - // This ensures that in the production run, we don't run the of a supertype but skips - // ik's . - if (_super_klass != nullptr) { - guarantee_property(_super_klass->has_aot_safe_initializer(), - "Missing @AOTSafeClassInitializer in superclass %s for class %s", - _super_klass->external_name(), - CHECK); - } - - int len = _local_interfaces->length(); - for (int i = 0; i < len; i++) { - InstanceKlass* intf = _local_interfaces->at(i); - guarantee_property(intf->class_initializer() == nullptr || intf->has_aot_safe_initializer(), - "Missing @AOTSafeClassInitializer in superinterface %s for class %s", - intf->external_name(), - CHECK); - } - - if (log_is_enabled(Info, aot, init)) { - ResourceMark rm; - log_info(aot, init)("Found @AOTSafeClassInitializer class %s", ik->external_name()); - } - } else { - // @AOTRuntimeSetup only meaningful in @AOTClassInitializer - guarantee_property(!ik->is_runtime_setup_required(), - "@AOTRuntimeSetup meaningless in non-@AOTSafeClassInitializer class %s", - CHECK); - } - } - apply_parsed_class_attributes(ik); // Miranda methods diff --git a/src/java.base/share/classes/jdk/internal/math/MathUtils.java b/src/java.base/share/classes/jdk/internal/math/MathUtils.java index 0de07f8ed60..172090facc0 100644 --- a/src/java.base/share/classes/jdk/internal/math/MathUtils.java +++ b/src/java.base/share/classes/jdk/internal/math/MathUtils.java @@ -26,12 +26,14 @@ package jdk.internal.math; import jdk.internal.vm.annotation.Stable; +import jdk.internal.vm.annotation.AOTSafeClassInitializer; /** * This class exposes package private utilities for other classes. * Thus, all methods are assumed to be invoked with correct arguments, * so these are not checked at all. */ +@AOTSafeClassInitializer final class MathUtils { /* * For full details about this code see the following reference: diff --git a/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java b/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java index 3c2dab3209a..1305a7b6b88 100644 --- a/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java +++ b/src/java.base/share/classes/jdk/internal/vm/annotation/AOTSafeClassInitializer.java @@ -30,25 +30,34 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/// Indicates that the static initializer of this class or interface -/// (its `` method) is allowed to be _AOT-initialized_, -/// because its author considers it safe to execute during the AOT -/// assembly phase. +/// Indicates that the annotated class or interface is allowed to be _AOT-initialized_, +/// because its author considers it safe to execute the static initializer of +/// the class or interface during the AOT assembly phase. /// -/// This annotation directs the VM to expect that normal execution of Java code -/// during the assembly phase could trigger initialization of this class, -/// and if that happens, to store the resulting static field values in the -/// AOT cache. (These fields happen to be allocated in the `Class` mirror.) +/// For a class or interface _X_ annotated with `@AOTSafeClassInitializer`, it will +/// be initialized in the AOT assembly phase under two circumstances: /// -/// During the production run, the static initializer (``) of -/// this class or interface will not be executed, if it was already -/// executed during the assembling of the AOT being used to start the -/// production run. In that case the resulting static field states -/// (within the `Class` mirror) were already stored in the AOT cache. +/// 1. If _X_ was initialized during the AOT training run, the JVM will proactively +/// initialize _X_ in the assembly phase. +/// 2. If _X_ was not initialized during the AOT training run, the initialization of +/// _X_ can still be triggered by normal execution of Java code in the assembly +/// phase. At present this is usually the result of performing AOT optimizations for +/// the `java.lang.invoke` package but it may include other cases as well. /// -/// Currently, this annotation is used mainly for supporting AOT -/// linking of APIs, including bootstrap methods, in the -/// `java.lang.invoke` package. +/// If _X_ is initialized during the AOT assembly phase, the VM will store +/// the values of the static fields of _X_ in the AOT cache. Consequently, +/// during the production run that uses this AOT cache, the static initializer +/// (``) of _X_ will not be executed. _X_ will appear to be in the +/// "initialized" state and all the cached values of the static field of _X_ +/// will be available immediately upon the start of the prodcution run. +/// +/// Currently, this annotation is used mainly for two purposes: +/// +/// - To AOT-initialize complex static fields whose values are always the same +/// across JVM lifetimes. One example is the tables of constant values +/// in the `jdk.internal.math.MathUtils` class. +/// - To support AOT linking of APIs, including bootstrap methods, in the +/// `java.lang.invoke` package. /// /// In more detail, the AOT assembly phase performs the following: /// @@ -62,6 +71,8 @@ import java.lang.annotation.Target; /// along with every relevant superclass and implemented interface, along /// with classes for every object created during the course of static /// initialization (running `` for each such class or interface). +/// 5. In addition, any class/interface annotated with `@AOTSafeClassInitializer` +/// that was initialized during the training run is proactively initialized. /// /// Thus, in order to determine that a class or interface _X_ is safe to /// AOT-initialize requires evaluating every other class or interface _Y_ that @@ -112,21 +123,18 @@ import java.lang.annotation.Target; /// remotely) if the execution of such an API touches _X_ for initialization, /// or even if such an API request is in any way sensitive to values stored in /// the fields of _X_, even if the sensitivity is a simple reference identity -/// test. As noted above, all supertypes of _X_ must also have the -/// `@AOTSafeClassInitializer` annotation, and must also be safe for AOT -/// initialization. +/// test. /// /// The author of an AOT-initialized class may elect to patch some states at /// production startup, using an [AOTRuntimeSetup] method, as long as the /// pre-patched field values (present during AOT assembly) are determined to be /// compatible with the post-patched values that apply to the production run. /// -/// In the assembly phase, `classFileParser.cpp` performs checks on the annotated -/// classes, to ensure all supertypes of this class that must be initialized when -/// this class is initialized have the `@AOTSafeClassInitializer` annotation. -/// Otherwise, a [ClassFormatError] will be thrown. (This assembly phase restriction -/// allows module patching and instrumentation to work on annotated classes when -/// AOT is not enabled) +/// Before adding this annotation to a class _X_, the author must determine +/// that it's safe to execute the static initializer of _X_ during the AOT +/// assembly phase. In addition, all supertypes of _X_ must also have this +/// annotation. If a supertype of _X_ is found to be missing this annotation, +/// the AOT assembly phase will fail. /// /// This annotation is only recognized on privileged code and is ignored elsewhere. /// diff --git a/test/hotspot/jtreg/ProblemList-AotJdk.txt b/test/hotspot/jtreg/ProblemList-AotJdk.txt index d292c23f690..8be9a73359d 100644 --- a/test/hotspot/jtreg/ProblemList-AotJdk.txt +++ b/test/hotspot/jtreg/ProblemList-AotJdk.txt @@ -2,7 +2,12 @@ runtime/modules/PatchModule/PatchModuleClassList.java 0000000 generic-all runtime/NMT/NMTWithCDS.java 0000000 generic-all runtime/symbols/TestSharedArchiveConfigFile.java 0000000 generic-all +# The following tests use very small -Xmx and will not be able to +# use the AOT cache generated by "make test JTREG=AOT_JDK=onestep ..." +gc/arguments/TestG1HeapSizeFlags.java 0000000 generic-all +gc/arguments/TestParallelHeapSizeFlags.java 0000000 generic-all gc/arguments/TestSerialHeapSizeFlags.java 0000000 generic-all + gc/arguments/TestCompressedClassFlags.java 0000000 generic-all gc/TestAllocateHeapAtMultiple.java 0000000 generic-all gc/TestAllocateHeapAt.java 0000000 generic-all diff --git a/test/hotspot/jtreg/TEST.groups b/test/hotspot/jtreg/TEST.groups index 8169ca87f4e..580d9bd11d7 100644 --- a/test/hotspot/jtreg/TEST.groups +++ b/test/hotspot/jtreg/TEST.groups @@ -412,6 +412,7 @@ hotspot_cds_only = \ hotspot_appcds_dynamic = \ runtime/cds/appcds/ \ + -runtime/cds/appcds/aotAnnotations \ -runtime/cds/appcds/aotCache \ -runtime/cds/appcds/aotClassLinking \ -runtime/cds/appcds/aotCode \ @@ -512,6 +513,7 @@ hotspot_cds_epsilongc = \ # test AOT class linking, so there's no need to run them again with -XX:+AOTClassLinking. hotspot_aot_classlinking = \ runtime/cds \ + -runtime/cds/appcds/aotAnnotations \ -runtime/cds/appcds/aotCache \ -runtime/cds/appcds/aotClassLinking \ -runtime/cds/appcds/aotCode \ diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotAnnotations/AOTAnnotationsTest.java b/test/hotspot/jtreg/runtime/cds/appcds/aotAnnotations/AOTAnnotationsTest.java new file mode 100644 index 00000000000..96422702bd9 --- /dev/null +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotAnnotations/AOTAnnotationsTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ +/* + * @test + * @summary Tests the effect of jdk.internal.vm.annotation.AOTXXX annotations + * in the core Java library. + * @bug 8317269 + * @requires vm.cds.supports.aot.class.linking + * @library /test/jdk/lib/testlibrary /test/lib + * @build AOTAnnotationsTest + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar AOTAnnotationsTestApp + * @run driver AOTAnnotationsTest + */ + +import jdk.test.lib.cds.CDSAppTester; +import jdk.test.lib.helpers.ClassFileInstaller; +import jdk.test.lib.process.OutputAnalyzer; + +public class AOTAnnotationsTest { + static final String appJar = ClassFileInstaller.getJarPath("app.jar"); + static final String mainClass = AOTAnnotationsTestApp.class.getName(); + + public static void main(String[] args) throws Exception { + Tester tester = new Tester(); + tester.run(new String[] {"AOT", "--two-step-training"} ); + } + + static class Tester extends CDSAppTester { + public Tester() { + super(mainClass); + } + + @Override + public String classpath(RunMode runMode) { + return appJar; + } + + @Override + public String[] vmArgs(RunMode runMode) { + return new String[] { + "-Xlog:aot+class=debug", + "-Xlog:aot+init", + }; + } + + @Override + public String[] appCommandLine(RunMode runMode) { + return new String[] { mainClass}; + } + + @Override + public void checkExecution(OutputAnalyzer out, RunMode runMode) { + if (runMode == RunMode.ASSEMBLY) { + out.shouldMatch("jdk.internal.math.MathUtils .*inited"); + } + } + } +} + +class AOTAnnotationsTestApp { + public static void main(String args[]) { + double d = 12.34567; + + // Double.toString() uses jdk.internal.math.MathUtils. + // Because MathUtils has @AOTSafeClassInitializer and was initialized during + // the training run, it will be cached in aot-inited state. + System.out.println(Double.toString(d)); + } +} From a2870d6b4985a68beb3fe3bf6622e6245e9a82ec Mon Sep 17 00:00:00 2001 From: Kelvin Nilsen Date: Wed, 24 Sep 2025 22:49:01 +0000 Subject: [PATCH 220/556] 8368015: Shenandoah: fix error in computation of average allocation rate Reviewed-by: wkemper --- .../shenandoahAdaptiveHeuristics.cpp | 33 ++++++++++++++----- .../shenandoahAdaptiveHeuristics.hpp | 27 ++++++++++----- .../heuristics/shenandoahHeuristics.hpp | 5 +++ .../heuristics/shenandoahSpaceInfo.hpp | 4 +++ .../gc/shenandoah/shenandoahGeneration.cpp | 4 +-- .../gc/shenandoah/shenandoahGeneration.hpp | 2 +- .../share/gc/shenandoah/shenandoahHeap.cpp | 25 +++++++++++--- 7 files changed, 75 insertions(+), 25 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp index 144773908ca..6498f0acdb6 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp @@ -240,13 +240,14 @@ bool ShenandoahAdaptiveHeuristics::should_start_gc() { log_debug(gc)("should_start_gc? available: %zu, soft_max_capacity: %zu" ", allocated: %zu", available, capacity, allocated); + // Track allocation rate even if we decide to start a cycle for other reasons. + double rate = _allocation_rate.sample(allocated); + if (_start_gc_is_pending) { log_trigger("GC start is already pending"); return true; } - // Track allocation rate even if we decide to start a cycle for other reasons. - double rate = _allocation_rate.sample(allocated); _last_trigger = OTHER; size_t min_threshold = min_free_threshold(); @@ -360,16 +361,32 @@ ShenandoahAllocationRate::ShenandoahAllocationRate() : _rate_avg(int(ShenandoahAdaptiveSampleSizeSeconds * ShenandoahAdaptiveSampleFrequencyHz), ShenandoahAdaptiveDecayFactor) { } +double ShenandoahAllocationRate::force_sample(size_t allocated, size_t &unaccounted_bytes_allocated) { + const double MinSampleTime = 0.002; // Do not sample if time since last update is less than 2 ms + double now = os::elapsedTime(); + double time_since_last_update = now -_last_sample_time; + if (time_since_last_update < MinSampleTime) { + unaccounted_bytes_allocated = allocated - _last_sample_value; + _last_sample_value = 0; + return 0.0; + } else { + double rate = instantaneous_rate(now, allocated); + _rate.add(rate); + _rate_avg.add(_rate.avg()); + _last_sample_time = now; + _last_sample_value = allocated; + unaccounted_bytes_allocated = 0; + return rate; + } +} + double ShenandoahAllocationRate::sample(size_t allocated) { double now = os::elapsedTime(); double rate = 0.0; if (now - _last_sample_time > _interval_sec) { - if (allocated >= _last_sample_value) { - rate = instantaneous_rate(now, allocated); - _rate.add(rate); - _rate_avg.add(_rate.avg()); - } - + rate = instantaneous_rate(now, allocated); + _rate.add(rate); + _rate_avg.add(_rate.avg()); _last_sample_time = now; _last_sample_value = allocated; } diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp index 68e540960c7..014a4d99131 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp @@ -37,10 +37,12 @@ class ShenandoahAllocationRate : public CHeapObj { explicit ShenandoahAllocationRate(); void allocation_counter_reset(); + double force_sample(size_t allocated, size_t &unaccounted_bytes_allocated); double sample(size_t allocated); double upper_bound(double sds) const; bool is_spiking(double rate, double threshold) const; + private: double instantaneous_rate(double time, size_t allocated) const; @@ -71,18 +73,18 @@ public: virtual void choose_collection_set_from_regiondata(ShenandoahCollectionSet* cset, RegionData* data, size_t size, - size_t actual_free); + size_t actual_free) override; - void record_cycle_start(); - void record_success_concurrent(); - void record_success_degenerated(); - void record_success_full(); + virtual void record_cycle_start() override; + virtual void record_success_concurrent() override; + virtual void record_success_degenerated() override; + virtual void record_success_full() override; - virtual bool should_start_gc(); + virtual bool should_start_gc() override; - virtual const char* name() { return "Adaptive"; } - virtual bool is_diagnostic() { return false; } - virtual bool is_experimental() { return false; } + virtual const char* name() override { return "Adaptive"; } + virtual bool is_diagnostic() override { return false; } + virtual bool is_experimental() override { return false; } private: // These are used to adjust the margin of error and the spike threshold @@ -150,6 +152,13 @@ protected: _last_trigger = trigger_type; ShenandoahHeuristics::accept_trigger(); } + +public: + virtual size_t force_alloc_rate_sample(size_t bytes_allocated) override { + size_t unaccounted_bytes; + _allocation_rate.force_sample(bytes_allocated, unaccounted_bytes); + return unaccounted_bytes; + } }; #endif // SHARE_GC_SHENANDOAH_HEURISTICS_SHENANDOAHADAPTIVEHEURISTICS_HPP diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp index d036a62a32c..6e3062d158f 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp @@ -241,6 +241,11 @@ public: double elapsed_cycle_time() const; + virtual size_t force_alloc_rate_sample(size_t bytes_allocated) { + // do nothing + return 0; + } + // Format prefix and emit log message indicating a GC cycle hs been triggered void log_trigger(const char* fmt, ...) ATTRIBUTE_PRINTF(2, 3); }; diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahSpaceInfo.hpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahSpaceInfo.hpp index 2131f95b413..7fb44c7b71b 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahSpaceInfo.hpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahSpaceInfo.hpp @@ -41,6 +41,10 @@ public: virtual size_t soft_available() const = 0; virtual size_t available() const = 0; virtual size_t used() const = 0; + + // Return an approximation of the bytes allocated since GC start. The value returned is monotonically non-decreasing + // in time within each GC cycle. For certain GC cycles, the value returned may include some bytes allocated before + // the start of the current GC cycle. virtual size_t bytes_allocated_since_gc_start() const = 0; }; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp index fafa3fde437..0c55613efcc 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGeneration.cpp @@ -151,8 +151,8 @@ size_t ShenandoahGeneration::bytes_allocated_since_gc_start() const { return AtomicAccess::load(&_bytes_allocated_since_gc_start); } -void ShenandoahGeneration::reset_bytes_allocated_since_gc_start() { - AtomicAccess::store(&_bytes_allocated_since_gc_start, (size_t)0); +void ShenandoahGeneration::reset_bytes_allocated_since_gc_start(size_t initial_bytes_allocated) { + AtomicAccess::store(&_bytes_allocated_since_gc_start, initial_bytes_allocated); } void ShenandoahGeneration::increase_allocated(size_t bytes) { diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGeneration.hpp b/src/hotspot/share/gc/shenandoah/shenandoahGeneration.hpp index 9d70d4d76f3..e6597b3c1e4 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGeneration.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGeneration.hpp @@ -142,7 +142,7 @@ private: size_t soft_available() const override; size_t bytes_allocated_since_gc_start() const override; - void reset_bytes_allocated_since_gc_start(); + void reset_bytes_allocated_since_gc_start(size_t initial_bytes_allocated); void increase_allocated(size_t bytes); // These methods change the capacity of the generation by adding or subtracting the given number of bytes from the current diff --git a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp index 714c9cb9f5b..10d7e8edcad 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp @@ -2319,12 +2319,27 @@ address ShenandoahHeap::in_cset_fast_test_addr() { } void ShenandoahHeap::reset_bytes_allocated_since_gc_start() { - if (mode()->is_generational()) { - young_generation()->reset_bytes_allocated_since_gc_start(); - old_generation()->reset_bytes_allocated_since_gc_start(); - } + // It is important to force_alloc_rate_sample() before the associated generation's bytes_allocated has been reset. + // Note that there is no lock to prevent additional alloations between sampling bytes_allocated_since_gc_start() and + // reset_bytes_allocated_since_gc_start(). If additional allocations happen, they will be ignored in the average + // allocation rate computations. This effect is considered to be be negligible. - global_generation()->reset_bytes_allocated_since_gc_start(); + // unaccounted_bytes is the bytes not accounted for by our forced sample. If the sample interval is too short, + // the "forced sample" will not happen, and any recently allocated bytes are "unaccounted for". We pretend these + // bytes are allocated after the start of subsequent gc. + size_t unaccounted_bytes; + if (mode()->is_generational()) { + size_t bytes_allocated = young_generation()->bytes_allocated_since_gc_start(); + unaccounted_bytes = young_generation()->heuristics()->force_alloc_rate_sample(bytes_allocated); + young_generation()->reset_bytes_allocated_since_gc_start(unaccounted_bytes); + unaccounted_bytes = 0; + old_generation()->reset_bytes_allocated_since_gc_start(unaccounted_bytes); + } else { + size_t bytes_allocated = global_generation()->bytes_allocated_since_gc_start(); + // Single-gen Shenandoah uses global heuristics. + unaccounted_bytes = heuristics()->force_alloc_rate_sample(bytes_allocated); + } + global_generation()->reset_bytes_allocated_since_gc_start(unaccounted_bytes); } void ShenandoahHeap::set_degenerated_gc_in_progress(bool in_progress) { From 2aafda1968f3fc8902f7d146a1cba72998aeb976 Mon Sep 17 00:00:00 2001 From: Igor Veresov Date: Wed, 24 Sep 2025 23:07:44 +0000 Subject: [PATCH 221/556] 8366948: AOT cache creation crashes when iterating training data Reviewed-by: vlivanov, iklam --- src/hotspot/share/oops/trainingData.hpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/hotspot/share/oops/trainingData.hpp b/src/hotspot/share/oops/trainingData.hpp index 9c645d437dc..d214a03a284 100644 --- a/src/hotspot/share/oops/trainingData.hpp +++ b/src/hotspot/share/oops/trainingData.hpp @@ -152,6 +152,9 @@ public: _lock_mode = need_data() ? +1 : -1; // if -1, we go lock-free #endif } + static void assert_locked_or_snapshotted() { + assert(safely_locked() || _snapshot, "use under TrainingDataLocker or after snapshot"); + } static void assert_locked() { assert(safely_locked(), "use under TrainingDataLocker"); } @@ -338,20 +341,24 @@ private: } int length() const { + TrainingDataLocker::assert_locked_or_snapshotted(); return (_deps_dyn != nullptr ? _deps_dyn->length() : _deps != nullptr ? _deps->length() : 0); } E* adr_at(int i) const { + TrainingDataLocker::assert_locked_or_snapshotted(); return (_deps_dyn != nullptr ? _deps_dyn->adr_at(i) : _deps != nullptr ? _deps->adr_at(i) : nullptr); } E at(int i) const { + TrainingDataLocker::assert_locked_or_snapshotted(); assert(i >= 0 && i < length(), "oob"); return *adr_at(i); } bool append_if_missing(E dep) { + TrainingDataLocker::assert_can_add(); if (_deps_dyn == nullptr) { _deps_dyn = new GrowableArrayCHeap(10); _deps_dyn->append(dep); @@ -361,23 +368,27 @@ private: } } bool remove_if_existing(E dep) { + TrainingDataLocker::assert_can_add(); if (_deps_dyn != nullptr) { return _deps_dyn->remove_if_existing(dep); } return false; } void clear() { + TrainingDataLocker::assert_can_add(); if (_deps_dyn != nullptr) { _deps_dyn->clear(); } } void append(E dep) { + TrainingDataLocker::assert_can_add(); if (_deps_dyn == nullptr) { _deps_dyn = new GrowableArrayCHeap(10); } _deps_dyn->append(dep); } bool contains(E dep) { + TrainingDataLocker::assert_locked(); for (int i = 0; i < length(); i++) { if (dep == at(i)) { return true; // found @@ -591,6 +602,7 @@ public: DepList _data; public: OptionalReturnType find(const Args&... args) { + TrainingDataLocker l; ArgumentsType a(args...); for (int i = 0; i < _data.length(); i++) { if (_data.at(i).arguments() == a) { @@ -599,8 +611,11 @@ public: } return OptionalReturnType(false, ReturnType()); } - bool append_if_missing(const ReturnType& result, const Args&... args) { - return _data.append_if_missing(Record(result, ArgumentsType(args...))); + void append_if_missing(const ReturnType& result, const Args&... args) { + TrainingDataLocker l; + if (l.can_add()) { + _data.append_if_missing(Record(result, ArgumentsType(args...))); + } } #if INCLUDE_CDS void remove_unshareable_info() { _data.remove_unshareable_info(); } From 17244c699ad20fafe7448678a53266ce6bf017e5 Mon Sep 17 00:00:00 2001 From: Serguei Spitsyn Date: Thu, 25 Sep 2025 05:41:32 +0000 Subject: [PATCH 222/556] 8368159: Significant performance overhead when started with jdwp agent and unattached debugger Reviewed-by: lmesnik, cjplummer --- src/hotspot/share/prims/jvmtiExport.cpp | 17 +++++++++++++++++ src/hotspot/share/prims/jvmtiExport.hpp | 3 ++- .../share/runtime/continuationFreezeThaw.cpp | 9 ++++----- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/hotspot/share/prims/jvmtiExport.cpp b/src/hotspot/share/prims/jvmtiExport.cpp index 2da9a074fb6..077b3fec505 100644 --- a/src/hotspot/share/prims/jvmtiExport.cpp +++ b/src/hotspot/share/prims/jvmtiExport.cpp @@ -1690,6 +1690,23 @@ void JvmtiExport::post_vthread_unmount(jobject vthread) { } } +bool JvmtiExport::has_frame_pops(JavaThread* thread) { + if (!can_post_frame_pop()) { + return false; + } + JvmtiThreadState *state = get_jvmti_thread_state(thread); + if (state == nullptr) { + return false; + } + JvmtiEnvThreadStateIterator it(state); + for (JvmtiEnvThreadState* ets = it.first(); ets != nullptr; ets = it.next(ets)) { + if (ets->has_frame_pops()) { + return true; + } + } + return false; +} + void JvmtiExport::continuation_yield_cleanup(JavaThread* thread, jint continuation_frame_count) { if (JvmtiEnv::get_phase() < JVMTI_PHASE_PRIMORDIAL) { return; diff --git a/src/hotspot/share/prims/jvmtiExport.hpp b/src/hotspot/share/prims/jvmtiExport.hpp index 4f8c3016908..062057c70ab 100644 --- a/src/hotspot/share/prims/jvmtiExport.hpp +++ b/src/hotspot/share/prims/jvmtiExport.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -373,6 +373,7 @@ class JvmtiExport : public AllStatic { JVMTI_ONLY(return _should_post_class_file_load_hook); NOT_JVMTI(return false;) } + static bool has_frame_pops(JavaThread* thread) NOT_JVMTI_RETURN_(false); static bool is_early_phase() NOT_JVMTI_RETURN_(false); static bool has_early_class_hook_env() NOT_JVMTI_RETURN_(false); static bool has_early_vmstart_env() NOT_JVMTI_RETURN_(false); diff --git a/src/hotspot/share/runtime/continuationFreezeThaw.cpp b/src/hotspot/share/runtime/continuationFreezeThaw.cpp index 64f9d2b9994..024b69c765f 100644 --- a/src/hotspot/share/runtime/continuationFreezeThaw.cpp +++ b/src/hotspot/share/runtime/continuationFreezeThaw.cpp @@ -1619,15 +1619,14 @@ static int num_java_frames(ContinuationWrapper& cont) { } static void invalidate_jvmti_stack(JavaThread* thread) { - if (thread->is_interp_only_mode()) { - JvmtiThreadState *state = thread->jvmti_thread_state(); - if (state != nullptr) - state->invalidate_cur_stack_depth(); + JvmtiThreadState *state = thread->jvmti_thread_state(); + if (state != nullptr) { + state->invalidate_cur_stack_depth(); } } static void jvmti_yield_cleanup(JavaThread* thread, ContinuationWrapper& cont) { - if (JvmtiExport::can_post_frame_pop()) { + if (JvmtiExport::has_frame_pops(thread)) { int num_frames = num_java_frames(cont); ContinuationWrapper::SafepointOp so(Thread::current(), cont); From e6ddb39635cb8b5a21445a50b28aeeebc9e1d9d3 Mon Sep 17 00:00:00 2001 From: Hamlin Li Date: Thu, 25 Sep 2025 08:11:15 +0000 Subject: [PATCH 223/556] 8368525: nmethod ic cleanup Reviewed-by: chagedorn, mhaessig --- src/hotspot/share/code/nmethod.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/hotspot/share/code/nmethod.cpp b/src/hotspot/share/code/nmethod.cpp index fbd9f5030c8..c0091cd2f65 100644 --- a/src/hotspot/share/code/nmethod.cpp +++ b/src/hotspot/share/code/nmethod.cpp @@ -791,15 +791,9 @@ class CheckClass : public MetadataClosure { }; #endif // ASSERT - -static void clean_ic_if_metadata_is_dead(CompiledIC *ic) { - ic->clean_metadata(); -} - // Clean references to unloaded nmethods at addr from this one, which is not unloaded. template -static void clean_if_nmethod_is_unloaded(CallsiteT* callsite, nmethod* from, - bool clean_all) { +static void clean_if_nmethod_is_unloaded(CallsiteT* callsite, bool clean_all) { CodeBlob* cb = CodeCache::find_blob(callsite->destination()); if (!cb->is_nmethod()) { return; @@ -874,15 +868,15 @@ void nmethod::cleanup_inline_caches_impl(bool unloading_occurred, bool clean_all if (unloading_occurred) { // If class unloading occurred we first clear ICs where the cached metadata // is referring to an unloaded klass or method. - clean_ic_if_metadata_is_dead(CompiledIC_at(&iter)); + CompiledIC_at(&iter)->clean_metadata(); } - clean_if_nmethod_is_unloaded(CompiledIC_at(&iter), this, clean_all); + clean_if_nmethod_is_unloaded(CompiledIC_at(&iter), clean_all); break; case relocInfo::opt_virtual_call_type: case relocInfo::static_call_type: - clean_if_nmethod_is_unloaded(CompiledDirectCall::at(iter.reloc()), this, clean_all); + clean_if_nmethod_is_unloaded(CompiledDirectCall::at(iter.reloc()), clean_all); break; case relocInfo::static_stub_type: { From 847b107df821e0c1d347383f1858d505137eb724 Mon Sep 17 00:00:00 2001 From: Fredrik Bredberg Date: Thu, 25 Sep 2025 08:15:45 +0000 Subject: [PATCH 224/556] 8365191: Cleanup after removing LockingMode related code Reviewed-by: coleenp, dholmes, yzheng, mdoerr, ayang, fyang, amitkumar --- .../cpu/aarch64/c1_CodeStubs_aarch64.cpp | 8 +++---- .../cpu/aarch64/c1_LIRAssembler_aarch64.cpp | 4 +--- .../cpu/aarch64/c1_MacroAssembler_aarch64.cpp | 14 +++++------ .../cpu/aarch64/c1_MacroAssembler_aarch64.hpp | 22 ++++++++--------- .../cpu/aarch64/sharedRuntime_aarch64.cpp | 3 --- src/hotspot/cpu/arm/c1_CodeStubs_arm.cpp | 7 +++--- src/hotspot/cpu/arm/c1_LIRAssembler_arm.cpp | 3 +-- src/hotspot/cpu/arm/c1_MacroAssembler_arm.cpp | 24 +++++++++---------- src/hotspot/cpu/arm/c1_MacroAssembler_arm.hpp | 6 ++--- src/hotspot/cpu/arm/sharedRuntime_arm.cpp | 10 ++++---- src/hotspot/cpu/ppc/c1_CodeStubs_ppc.cpp | 7 +++--- src/hotspot/cpu/ppc/c1_LIRAssembler_ppc.cpp | 4 +--- src/hotspot/cpu/riscv/c1_CodeStubs_riscv.cpp | 8 +++---- .../cpu/riscv/c1_LIRAssembler_riscv.cpp | 4 +--- .../cpu/riscv/c1_MacroAssembler_riscv.cpp | 14 +++++------ .../cpu/riscv/c1_MacroAssembler_riscv.hpp | 22 ++++++++--------- src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp | 2 -- src/hotspot/cpu/s390/c1_CodeStubs_s390.cpp | 10 ++++---- src/hotspot/cpu/s390/c1_LIRAssembler_s390.cpp | 4 +--- src/hotspot/cpu/x86/c1_CodeStubs_x86.cpp | 8 +++---- src/hotspot/cpu/x86/c1_LIRAssembler_x86.cpp | 4 +--- src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp | 18 +++++++------- src/hotspot/cpu/x86/c1_MacroAssembler_x86.hpp | 18 +++++++------- src/hotspot/share/c1/c1_CodeStubs.hpp | 11 +++------ src/hotspot/share/c1/c1_LIRGenerator.cpp | 2 +- src/hotspot/share/jvmci/vmStructs_jvmci.cpp | 2 +- src/hotspot/share/runtime/basicLock.cpp | 2 +- src/hotspot/share/runtime/basicLock.hpp | 16 ++++++------- .../share/runtime/basicLock.inline.hpp | 8 +++---- src/hotspot/share/runtime/deoptimization.cpp | 2 +- .../share/runtime/synchronizer.inline.hpp | 2 +- src/hotspot/share/runtime/vmStructs.cpp | 1 - .../classes/sun/jvm/hotspot/oops/Mark.java | 8 +------ .../sun/jvm/hotspot/runtime/BasicLock.java | 14 +---------- 34 files changed, 125 insertions(+), 167 deletions(-) diff --git a/src/hotspot/cpu/aarch64/c1_CodeStubs_aarch64.cpp b/src/hotspot/cpu/aarch64/c1_CodeStubs_aarch64.cpp index 954e4abee14..9bf46678535 100644 --- a/src/hotspot/cpu/aarch64/c1_CodeStubs_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/c1_CodeStubs_aarch64.cpp @@ -216,10 +216,10 @@ void MonitorEnterStub::emit_code(LIR_Assembler* ce) { void MonitorExitStub::emit_code(LIR_Assembler* ce) { __ bind(_entry); - if (_compute_lock) { - // lock_reg was destroyed by fast unlocking attempt => recompute it - ce->monitor_address(_monitor_ix, _lock_reg); - } + + // lock_reg was destroyed by fast unlocking attempt => recompute it + ce->monitor_address(_monitor_ix, _lock_reg); + ce->store_parameter(_lock_reg->as_register(), 0); // note: non-blocking leaf routine => no call info needed StubId exit_id; diff --git a/src/hotspot/cpu/aarch64/c1_LIRAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/c1_LIRAssembler_aarch64.cpp index d788c0c201a..9ab463125fe 100644 --- a/src/hotspot/cpu/aarch64/c1_LIRAssembler_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/c1_LIRAssembler_aarch64.cpp @@ -409,7 +409,7 @@ int LIR_Assembler::emit_unwind_handler() { MonitorExitStub* stub = nullptr; if (method()->is_synchronized()) { monitor_address(0, FrameMap::r0_opr); - stub = new MonitorExitStub(FrameMap::r0_opr, true, 0); + stub = new MonitorExitStub(FrameMap::r0_opr, 0); __ unlock_object(r5, r4, r0, r6, *stub->entry()); __ bind(*stub->continuation()); } @@ -2481,7 +2481,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { Register lock = op->lock_opr()->as_register(); Register temp = op->scratch_opr()->as_register(); if (op->code() == lir_lock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); // add debug info for NullPointerException only if one is possible int null_check_offset = __ lock_object(hdr, obj, lock, temp, *op->stub()->entry()); if (op->info() != nullptr) { @@ -2489,7 +2488,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { } // done } else if (op->code() == lir_unlock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); __ unlock_object(hdr, obj, lock, temp, *op->stub()->entry()); } else { Unimplemented(); diff --git a/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp index 8a79274b2ff..31c36e749c5 100644 --- a/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp @@ -59,28 +59,28 @@ void C1_MacroAssembler::float_cmp(bool is_float, int unordered_result, } } -int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register disp_hdr, Register temp, Label& slow_case) { - assert_different_registers(hdr, obj, disp_hdr, temp, rscratch2); +int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register basic_lock, Register temp, Label& slow_case) { + assert_different_registers(hdr, obj, basic_lock, temp, rscratch2); int null_check_offset = -1; verify_oop(obj); // save object being locked into the BasicObjectLock - str(obj, Address(disp_hdr, BasicObjectLock::obj_offset())); + str(obj, Address(basic_lock, BasicObjectLock::obj_offset())); null_check_offset = offset(); - lightweight_lock(disp_hdr, obj, hdr, temp, rscratch2, slow_case); + lightweight_lock(basic_lock, obj, hdr, temp, rscratch2, slow_case); return null_check_offset; } -void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Register temp, Label& slow_case) { - assert_different_registers(hdr, obj, disp_hdr, temp, rscratch2); +void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register basic_lock, Register temp, Label& slow_case) { + assert_different_registers(hdr, obj, basic_lock, temp, rscratch2); // load object - ldr(obj, Address(disp_hdr, BasicObjectLock::obj_offset())); + ldr(obj, Address(basic_lock, BasicObjectLock::obj_offset())); verify_oop(obj); lightweight_unlock(obj, hdr, temp, rscratch2, slow_case); diff --git a/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.hpp b/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.hpp index fc8e83d706b..7b181b104c1 100644 --- a/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, 2021, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -55,19 +55,19 @@ using MacroAssembler::null_check; Register result); // locking - // hdr : must be r0, contents destroyed - // obj : must point to the object to lock, contents preserved - // disp_hdr: must point to the displaced header location, contents preserved - // temp : temporary register, must not be rscratch1 or rscratch2 + // hdr : must be r0, contents destroyed + // obj : must point to the object to lock, contents preserved + // basic_lock: must point to the basic lock, contents preserved + // temp : temporary register, must not be rscratch1 or rscratch2 // returns code offset at which to add null check debug information - int lock_object (Register swap, Register obj, Register disp_hdr, Register temp, Label& slow_case); + int lock_object (Register swap, Register obj, Register basic_lock, Register temp, Label& slow_case); // unlocking - // hdr : contents destroyed - // obj : must point to the object to lock, contents preserved - // disp_hdr: must be r0 & must point to the displaced header location, contents destroyed - // temp : temporary register, must not be rscratch1 or rscratch2 - void unlock_object(Register swap, Register obj, Register lock, Register temp, Label& slow_case); + // hdr : contents destroyed + // obj : must point to the object to lock, contents preserved + // basic_lock: must be r0 & must point to the basic lock, contents destroyed + // temp : temporary register, must not be rscratch1 or rscratch2 + void unlock_object(Register swap, Register obj, Register basic_lock, Register temp, Label& slow_case); void initialize_object( Register obj, // result: pointer to object after successful allocation diff --git a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp index 4b3b8e58b9a..70af8dd91d8 100644 --- a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp @@ -1761,9 +1761,6 @@ nmethod* SharedRuntime::generate_native_wrapper(MacroAssembler* masm, Label lock_done; if (method->is_synchronized()) { - Label count; - const int mark_word_offset = BasicLock::displaced_header_offset_in_bytes(); - // Get the handle (the 2nd argument) __ mov(oop_handle_reg, c_rarg1); diff --git a/src/hotspot/cpu/arm/c1_CodeStubs_arm.cpp b/src/hotspot/cpu/arm/c1_CodeStubs_arm.cpp index efdc190f09a..8e49cfcbcaa 100644 --- a/src/hotspot/cpu/arm/c1_CodeStubs_arm.cpp +++ b/src/hotspot/cpu/arm/c1_CodeStubs_arm.cpp @@ -200,9 +200,10 @@ void MonitorEnterStub::emit_code(LIR_Assembler* ce) { void MonitorExitStub::emit_code(LIR_Assembler* ce) { __ bind(_entry); - if (_compute_lock) { - ce->monitor_address(_monitor_ix, _lock_reg); - } + + // lock_reg was destroyed by fast unlocking attempt => recompute it + ce->monitor_address(_monitor_ix, _lock_reg); + const Register lock_reg = _lock_reg->as_pointer_register(); ce->verify_reserved_argument_area_size(1); diff --git a/src/hotspot/cpu/arm/c1_LIRAssembler_arm.cpp b/src/hotspot/cpu/arm/c1_LIRAssembler_arm.cpp index d6ed82dcdb2..219c49d1f14 100644 --- a/src/hotspot/cpu/arm/c1_LIRAssembler_arm.cpp +++ b/src/hotspot/cpu/arm/c1_LIRAssembler_arm.cpp @@ -245,7 +245,7 @@ int LIR_Assembler::emit_unwind_handler() { MonitorExitStub* stub = nullptr; if (method()->is_synchronized()) { monitor_address(0, FrameMap::R0_opr); - stub = new MonitorExitStub(FrameMap::R0_opr, true, 0); + stub = new MonitorExitStub(FrameMap::R0_opr, 0); __ unlock_object(R2, R1, R0, *stub->entry()); __ bind(*stub->continuation()); } @@ -2427,7 +2427,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { Register lock = op->lock_opr()->as_pointer_register(); if (op->code() == lir_lock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); int null_check_offset = __ lock_object(hdr, obj, lock, *op->stub()->entry()); if (op->info() != nullptr) { add_debug_info_for_null_check(null_check_offset, op->info()); diff --git a/src/hotspot/cpu/arm/c1_MacroAssembler_arm.cpp b/src/hotspot/cpu/arm/c1_MacroAssembler_arm.cpp index f2b08269750..ca7711353d2 100644 --- a/src/hotspot/cpu/arm/c1_MacroAssembler_arm.cpp +++ b/src/hotspot/cpu/arm/c1_MacroAssembler_arm.cpp @@ -176,17 +176,17 @@ void C1_MacroAssembler::allocate_array(Register obj, Register len, initialize_object(obj, tmp1, klass, len, tmp2, tmp3, header_size_in_bytes, -1, /* is_tlab_allocated */ UseTLAB); } -int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register disp_hdr, Label& slow_case) { +int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register basic_lock, Label& slow_case) { int null_check_offset = 0; const Register tmp2 = Rtemp; // Rtemp should be free at c1 LIR level - assert_different_registers(hdr, obj, disp_hdr, tmp2); + assert_different_registers(hdr, obj, basic_lock, tmp2); assert(BasicObjectLock::lock_offset() == 0, "adjust this code"); assert(oopDesc::mark_offset_in_bytes() == 0, "Required by atomic instructions"); // save object being locked into the BasicObjectLock - str(obj, Address(disp_hdr, BasicObjectLock::obj_offset())); + str(obj, Address(basic_lock, BasicObjectLock::obj_offset())); null_check_offset = offset(); @@ -197,26 +197,26 @@ int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register disp_hdr b(slow_case, ne); } - Register t1 = disp_hdr; // Needs saving, probably - Register t2 = hdr; // blow - Register t3 = Rtemp; // blow + Register t1 = basic_lock; // Needs saving, probably + Register t2 = hdr; // blow + Register t3 = Rtemp; // blow lightweight_lock(obj, t1, t2, t3, 1 /* savemask - save t1 */, slow_case); // Success: fall through return null_check_offset; } -void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Label& slow_case) { - assert_different_registers(hdr, obj, disp_hdr, Rtemp); +void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register basic_lock, Label& slow_case) { + assert_different_registers(hdr, obj, basic_lock, Rtemp); assert(BasicObjectLock::lock_offset() == 0, "adjust this code"); assert(oopDesc::mark_offset_in_bytes() == 0, "Required by atomic instructions"); - ldr(obj, Address(disp_hdr, BasicObjectLock::obj_offset())); + ldr(obj, Address(basic_lock, BasicObjectLock::obj_offset())); - Register t1 = disp_hdr; // Needs saving, probably - Register t2 = hdr; // blow - Register t3 = Rtemp; // blow + Register t1 = basic_lock; // Needs saving, probably + Register t2 = hdr; // blow + Register t3 = Rtemp; // blow lightweight_unlock(obj, t1, t2, t3, 1 /* savemask - save t1 */, slow_case); // Success: fall through diff --git a/src/hotspot/cpu/arm/c1_MacroAssembler_arm.hpp b/src/hotspot/cpu/arm/c1_MacroAssembler_arm.hpp index 0a626822a9b..fd88b6c4fe9 100644 --- a/src/hotspot/cpu/arm/c1_MacroAssembler_arm.hpp +++ b/src/hotspot/cpu/arm/c1_MacroAssembler_arm.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -59,9 +59,9 @@ max_array_allocation_length = 0x01000000 }; - int lock_object(Register hdr, Register obj, Register disp_hdr, Label& slow_case); + int lock_object(Register hdr, Register obj, Register basic_lock, Label& slow_case); - void unlock_object(Register hdr, Register obj, Register disp_hdr, Label& slow_case); + void unlock_object(Register hdr, Register obj, Register basic_lock, Label& slow_case); // This platform only uses signal-based null checks. The Label is not needed. void null_check(Register r, Label *Lnull = nullptr) { MacroAssembler::null_check(r); } diff --git a/src/hotspot/cpu/arm/sharedRuntime_arm.cpp b/src/hotspot/cpu/arm/sharedRuntime_arm.cpp index 9e90b56f9f0..99d8773368d 100644 --- a/src/hotspot/cpu/arm/sharedRuntime_arm.cpp +++ b/src/hotspot/cpu/arm/sharedRuntime_arm.cpp @@ -1128,7 +1128,7 @@ nmethod* SharedRuntime::generate_native_wrapper(MacroAssembler* masm, const Register sync_handle = R5; const Register sync_obj = R6; - const Register disp_hdr = altFP_7_11; + const Register basic_lock = altFP_7_11; const Register tmp = R8; Label slow_lock, lock_done, fast_lock; @@ -1139,7 +1139,7 @@ nmethod* SharedRuntime::generate_native_wrapper(MacroAssembler* masm, __ mov(sync_handle, R1); log_trace(fastlock)("SharedRuntime lock fast"); - __ lightweight_lock(sync_obj /* object */, disp_hdr /* t1 */, tmp /* t2 */, Rtemp /* t3 */, + __ lightweight_lock(sync_obj /* object */, basic_lock /* t1 */, tmp /* t2 */, Rtemp /* t3 */, 0x7 /* savemask */, slow_lock); // Fall through to lock_done __ bind(lock_done); @@ -1254,7 +1254,7 @@ nmethod* SharedRuntime::generate_native_wrapper(MacroAssembler* masm, // last_Java_frame is already set, so do call_VM manually; no exception can occur __ mov(R0, sync_obj); - __ mov(R1, disp_hdr); + __ mov(R1, basic_lock); __ mov(R2, Rthread); __ call(CAST_FROM_FN_PTR(address, SharedRuntime::complete_monitor_locking_C)); @@ -1269,12 +1269,12 @@ nmethod* SharedRuntime::generate_native_wrapper(MacroAssembler* masm, // Clear pending exception before reentering VM. // Can store the oop in register since it is a leaf call. - assert_different_registers(Rtmp_save1, sync_obj, disp_hdr); + assert_different_registers(Rtmp_save1, sync_obj, basic_lock); __ ldr(Rtmp_save1, Address(Rthread, Thread::pending_exception_offset())); Register zero = __ zero_register(Rtemp); __ str(zero, Address(Rthread, Thread::pending_exception_offset())); __ mov(R0, sync_obj); - __ mov(R1, disp_hdr); + __ mov(R1, basic_lock); __ mov(R2, Rthread); __ call(CAST_FROM_FN_PTR(address, SharedRuntime::complete_monitor_unlocking_C)); __ str(Rtmp_save1, Address(Rthread, Thread::pending_exception_offset())); diff --git a/src/hotspot/cpu/ppc/c1_CodeStubs_ppc.cpp b/src/hotspot/cpu/ppc/c1_CodeStubs_ppc.cpp index 438521b0a9b..61780a73969 100644 --- a/src/hotspot/cpu/ppc/c1_CodeStubs_ppc.cpp +++ b/src/hotspot/cpu/ppc/c1_CodeStubs_ppc.cpp @@ -268,9 +268,10 @@ void MonitorEnterStub::emit_code(LIR_Assembler* ce) { void MonitorExitStub::emit_code(LIR_Assembler* ce) { __ bind(_entry); - if (_compute_lock) { - ce->monitor_address(_monitor_ix, _lock_reg); - } + + // lock_reg was destroyed by fast unlocking attempt => recompute it + ce->monitor_address(_monitor_ix, _lock_reg); + address stub = Runtime1::entry_for(ce->compilation()->has_fpu_code() ? StubId::c1_monitorexit_id : StubId::c1_monitorexit_nofpu_id); //__ load_const_optimized(R0, stub); __ add_const_optimized(R0, R29_TOC, MacroAssembler::offset_to_global_toc(stub)); diff --git a/src/hotspot/cpu/ppc/c1_LIRAssembler_ppc.cpp b/src/hotspot/cpu/ppc/c1_LIRAssembler_ppc.cpp index 3ca75305eca..108da2039f6 100644 --- a/src/hotspot/cpu/ppc/c1_LIRAssembler_ppc.cpp +++ b/src/hotspot/cpu/ppc/c1_LIRAssembler_ppc.cpp @@ -227,7 +227,7 @@ int LIR_Assembler::emit_unwind_handler() { MonitorExitStub* stub = nullptr; if (method()->is_synchronized()) { monitor_address(0, FrameMap::R4_opr); - stub = new MonitorExitStub(FrameMap::R4_opr, true, 0); + stub = new MonitorExitStub(FrameMap::R4_opr, 0); __ unlock_object(R5, R6, R4, *stub->entry()); __ bind(*stub->continuation()); } @@ -2614,7 +2614,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { // Obj may not be an oop. if (op->code() == lir_lock) { MonitorEnterStub* stub = (MonitorEnterStub*)op->stub(); - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); // Add debug info for NullPointerException only if one is possible. if (op->info() != nullptr) { if (!os::zero_page_read_protected() || !ImplicitNullChecks) { @@ -2626,7 +2625,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { __ lock_object(hdr, obj, lock, op->scratch_opr()->as_register(), *op->stub()->entry()); } else { assert (op->code() == lir_unlock, "Invalid code, expected lir_unlock"); - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); __ unlock_object(hdr, obj, lock, *op->stub()->entry()); } __ bind(*op->stub()->continuation()); diff --git a/src/hotspot/cpu/riscv/c1_CodeStubs_riscv.cpp b/src/hotspot/cpu/riscv/c1_CodeStubs_riscv.cpp index 0c16e632e5a..a8a21342248 100644 --- a/src/hotspot/cpu/riscv/c1_CodeStubs_riscv.cpp +++ b/src/hotspot/cpu/riscv/c1_CodeStubs_riscv.cpp @@ -204,10 +204,10 @@ void MonitorEnterStub::emit_code(LIR_Assembler* ce) { void MonitorExitStub::emit_code(LIR_Assembler* ce) { __ bind(_entry); - if (_compute_lock) { - // lock_reg was destroyed by fast unlocking attempt => recompute it - ce->monitor_address(_monitor_ix, _lock_reg); - } + + // lock_reg was destroyed by fast unlocking attempt => recompute it + ce->monitor_address(_monitor_ix, _lock_reg); + ce->store_parameter(_lock_reg->as_register(), 0); // note: non-blocking leaf routine => no call info needed StubId exit_id; diff --git a/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp b/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp index c3fe72870cf..adc79350be6 100644 --- a/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp +++ b/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp @@ -338,7 +338,7 @@ int LIR_Assembler::emit_unwind_handler() { MonitorExitStub* stub = nullptr; if (method()->is_synchronized()) { monitor_address(0, FrameMap::r10_opr); - stub = new MonitorExitStub(FrameMap::r10_opr, true, 0); + stub = new MonitorExitStub(FrameMap::r10_opr, 0); __ unlock_object(x15, x14, x10, x16, *stub->entry()); __ bind(*stub->continuation()); } @@ -1494,14 +1494,12 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { Register lock = op->lock_opr()->as_register(); Register temp = op->scratch_opr()->as_register(); if (op->code() == lir_lock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); // add debug info for NullPointerException only if one is possible int null_check_offset = __ lock_object(hdr, obj, lock, temp, *op->stub()->entry()); if (op->info() != nullptr) { add_debug_info_for_null_check(null_check_offset, op->info()); } } else if (op->code() == lir_unlock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); __ unlock_object(hdr, obj, lock, temp, *op->stub()->entry()); } else { Unimplemented(); diff --git a/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp b/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp index 8198192f506..8e989de2665 100644 --- a/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp +++ b/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp @@ -48,27 +48,27 @@ void C1_MacroAssembler::float_cmp(bool is_float, int unordered_result, } } -int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register disp_hdr, Register temp, Label& slow_case) { - assert_different_registers(hdr, obj, disp_hdr, temp, t0, t1); +int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register basic_lock, Register temp, Label& slow_case) { + assert_different_registers(hdr, obj, basic_lock, temp, t0, t1); int null_check_offset = -1; verify_oop(obj); // save object being locked into the BasicObjectLock - sd(obj, Address(disp_hdr, BasicObjectLock::obj_offset())); + sd(obj, Address(basic_lock, BasicObjectLock::obj_offset())); null_check_offset = offset(); - lightweight_lock(disp_hdr, obj, hdr, temp, t1, slow_case); + lightweight_lock(basic_lock, obj, hdr, temp, t1, slow_case); return null_check_offset; } -void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Register temp, Label& slow_case) { - assert_different_registers(hdr, obj, disp_hdr, temp, t0, t1); +void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register basic_lock, Register temp, Label& slow_case) { + assert_different_registers(hdr, obj, basic_lock, temp, t0, t1); // load object - ld(obj, Address(disp_hdr, BasicObjectLock::obj_offset())); + ld(obj, Address(basic_lock, BasicObjectLock::obj_offset())); verify_oop(obj); lightweight_unlock(obj, hdr, temp, t1, slow_case); diff --git a/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.hpp b/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.hpp index 561053045ec..16e76884049 100644 --- a/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.hpp +++ b/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, 2015, Red Hat Inc. All rights reserved. * Copyright (c) 2020, 2022, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -56,19 +56,19 @@ using MacroAssembler::null_check; Register result); // locking - // hdr : must be x10, contents destroyed - // obj : must point to the object to lock, contents preserved - // disp_hdr: must point to the displaced header location, contents preserved - // temp : temporary register, must not be scratch register t0 or t1 + // hdr : must be x10, contents destroyed + // obj : must point to the object to lock, contents preserved + // basic_lock: must point to the basic_lock, contents preserved + // temp : temporary register, must not be scratch register t0 or t1 // returns code offset at which to add null check debug information - int lock_object(Register swap, Register obj, Register disp_hdr, Register temp, Label& slow_case); + int lock_object(Register swap, Register obj, Register basic_lock, Register temp, Label& slow_case); // unlocking - // hdr : contents destroyed - // obj : must point to the object to lock, contents preserved - // disp_hdr: must be x10 & must point to the displaced header location, contents destroyed - // temp : temporary register, must not be scratch register t0 or t1 - void unlock_object(Register swap, Register obj, Register lock, Register temp, Label& slow_case); + // hdr : contents destroyed + // obj : must point to the object to lock, contents preserved + // basic_lock: must be x10 & must point to the basic lock, contents destroyed + // temp : temporary register, must not be scratch register t0 or t1 + void unlock_object(Register swap, Register obj, Register basic_lock, Register temp, Label& slow_case); void initialize_object( Register obj, // result: pointer to object after successful allocation diff --git a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp index 01970c68090..34f0f63dbc8 100644 --- a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp +++ b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp @@ -1677,8 +1677,6 @@ nmethod* SharedRuntime::generate_native_wrapper(MacroAssembler* masm, Label lock_done; if (method->is_synchronized()) { - const int mark_word_offset = BasicLock::displaced_header_offset_in_bytes(); - // Get the handle (the 2nd argument) __ mv(oop_handle_reg, c_rarg1); diff --git a/src/hotspot/cpu/s390/c1_CodeStubs_s390.cpp b/src/hotspot/cpu/s390/c1_CodeStubs_s390.cpp index 68e7114b3b6..f1272ee1a22 100644 --- a/src/hotspot/cpu/s390/c1_CodeStubs_s390.cpp +++ b/src/hotspot/cpu/s390/c1_CodeStubs_s390.cpp @@ -234,12 +234,10 @@ void MonitorEnterStub::emit_code(LIR_Assembler* ce) { void MonitorExitStub::emit_code(LIR_Assembler* ce) { __ bind(_entry); // Move address of the BasicObjectLock into Z_R1_scratch. - if (_compute_lock) { - // Lock_reg was destroyed by fast unlocking attempt => recompute it. - ce->monitor_address(_monitor_ix, FrameMap::as_opr(Z_R1_scratch)); - } else { - __ lgr_if_needed(Z_R1_scratch, _lock_reg->as_register()); - } + + // Lock_reg was destroyed by fast unlocking attempt => recompute it. + ce->monitor_address(_monitor_ix, FrameMap::as_opr(Z_R1_scratch)); + // Note: non-blocking leaf routine => no call info needed. StubId exit_id; if (ce->compilation()->has_fpu_code()) { diff --git a/src/hotspot/cpu/s390/c1_LIRAssembler_s390.cpp b/src/hotspot/cpu/s390/c1_LIRAssembler_s390.cpp index b875eeca9ad..298234156c3 100644 --- a/src/hotspot/cpu/s390/c1_LIRAssembler_s390.cpp +++ b/src/hotspot/cpu/s390/c1_LIRAssembler_s390.cpp @@ -228,7 +228,7 @@ int LIR_Assembler::emit_unwind_handler() { // StubId::c1_monitorexit_id expects lock address in Z_R1_scratch. LIR_Opr lock = FrameMap::as_opr(Z_R1_scratch); monitor_address(0, lock); - stub = new MonitorExitStub(lock, true, 0); + stub = new MonitorExitStub(lock, 0); __ unlock_object(Rtmp1, Rtmp2, lock->as_register(), *stub->entry()); __ bind(*stub->continuation()); } @@ -2711,7 +2711,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { Register hdr = op->hdr_opr()->as_register(); Register lock = op->lock_opr()->as_register(); if (op->code() == lir_lock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); // Add debug info for NullPointerException only if one is possible. if (op->info() != nullptr) { add_debug_info_for_null_check_here(op->info()); @@ -2719,7 +2718,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { __ lock_object(hdr, obj, lock, *op->stub()->entry()); // done } else if (op->code() == lir_unlock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); __ unlock_object(hdr, obj, lock, *op->stub()->entry()); } else { ShouldNotReachHere(); diff --git a/src/hotspot/cpu/x86/c1_CodeStubs_x86.cpp b/src/hotspot/cpu/x86/c1_CodeStubs_x86.cpp index 2fd067a7749..95ce48f34db 100644 --- a/src/hotspot/cpu/x86/c1_CodeStubs_x86.cpp +++ b/src/hotspot/cpu/x86/c1_CodeStubs_x86.cpp @@ -207,10 +207,10 @@ void MonitorEnterStub::emit_code(LIR_Assembler* ce) { void MonitorExitStub::emit_code(LIR_Assembler* ce) { __ bind(_entry); - if (_compute_lock) { - // lock_reg was destroyed by fast unlocking attempt => recompute it - ce->monitor_address(_monitor_ix, _lock_reg); - } + + // lock_reg was destroyed by fast unlocking attempt => recompute it + ce->monitor_address(_monitor_ix, _lock_reg); + ce->store_parameter(_lock_reg->as_register(), 0); // note: non-blocking leaf routine => no call info needed StubId exit_id; diff --git a/src/hotspot/cpu/x86/c1_LIRAssembler_x86.cpp b/src/hotspot/cpu/x86/c1_LIRAssembler_x86.cpp index 98759295bb1..edeb0baea0e 100644 --- a/src/hotspot/cpu/x86/c1_LIRAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/c1_LIRAssembler_x86.cpp @@ -412,7 +412,7 @@ int LIR_Assembler::emit_unwind_handler() { MonitorExitStub* stub = nullptr; if (method()->is_synchronized()) { monitor_address(0, FrameMap::rax_opr); - stub = new MonitorExitStub(FrameMap::rax_opr, true, 0); + stub = new MonitorExitStub(FrameMap::rax_opr, 0); __ unlock_object(rdi, rsi, rax, *stub->entry()); __ bind(*stub->continuation()); } @@ -2730,7 +2730,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { Register hdr = op->hdr_opr()->as_register(); Register lock = op->lock_opr()->as_register(); if (op->code() == lir_lock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); Register tmp = op->scratch_opr()->as_register(); // add debug info for NullPointerException only if one is possible int null_check_offset = __ lock_object(hdr, obj, lock, tmp, *op->stub()->entry()); @@ -2739,7 +2738,6 @@ void LIR_Assembler::emit_lock(LIR_OpLock* op) { } // done } else if (op->code() == lir_unlock) { - assert(BasicLock::displaced_header_offset_in_bytes() == 0, "lock_reg must point to the displaced header"); __ unlock_object(hdr, obj, lock, *op->stub()->entry()); } else { Unimplemented(); diff --git a/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp b/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp index 36efeafa940..c3d45f9d15d 100644 --- a/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp @@ -41,32 +41,32 @@ #include "utilities/checkedCast.hpp" #include "utilities/globalDefinitions.hpp" -int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register disp_hdr, Register tmp, Label& slow_case) { +int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register basic_lock, Register tmp, Label& slow_case) { assert(hdr == rax, "hdr must be rax, for the cmpxchg instruction"); - assert_different_registers(hdr, obj, disp_hdr, tmp); + assert_different_registers(hdr, obj, basic_lock, tmp); int null_check_offset = -1; verify_oop(obj); // save object being locked into the BasicObjectLock - movptr(Address(disp_hdr, BasicObjectLock::obj_offset()), obj); + movptr(Address(basic_lock, BasicObjectLock::obj_offset()), obj); null_check_offset = offset(); - lightweight_lock(disp_hdr, obj, hdr, tmp, slow_case); + lightweight_lock(basic_lock, obj, hdr, tmp, slow_case); return null_check_offset; } -void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Label& slow_case) { - assert(disp_hdr == rax, "disp_hdr must be rax, for the cmpxchg instruction"); - assert(hdr != obj && hdr != disp_hdr && obj != disp_hdr, "registers must be different"); +void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register basic_lock, Label& slow_case) { + assert(basic_lock == rax, "basic_lock must be rax, for the cmpxchg instruction"); + assert(hdr != obj && hdr != basic_lock && obj != basic_lock, "registers must be different"); // load object - movptr(obj, Address(disp_hdr, BasicObjectLock::obj_offset())); + movptr(obj, Address(basic_lock, BasicObjectLock::obj_offset())); verify_oop(obj); - lightweight_unlock(obj, disp_hdr, hdr, slow_case); + lightweight_unlock(obj, rax, hdr, slow_case); } diff --git a/src/hotspot/cpu/x86/c1_MacroAssembler_x86.hpp b/src/hotspot/cpu/x86/c1_MacroAssembler_x86.hpp index 6344a7b6ef1..f33e47aadb3 100644 --- a/src/hotspot/cpu/x86/c1_MacroAssembler_x86.hpp +++ b/src/hotspot/cpu/x86/c1_MacroAssembler_x86.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -46,17 +46,17 @@ void initialize_body(Register obj, Register len_in_bytes, int hdr_size_in_bytes, Register t1); // locking - // hdr : must be rax, contents destroyed - // obj : must point to the object to lock, contents preserved - // disp_hdr: must point to the displaced header location, contents preserved + // hdr : must be rax, contents destroyed + // obj : must point to the object to lock, contents preserved + // basic_lock: must point to the basic lock, contents preserved // returns code offset at which to add null check debug information - int lock_object (Register swap, Register obj, Register disp_hdr, Register tmp, Label& slow_case); + int lock_object (Register swap, Register obj, Register basic_lock, Register tmp, Label& slow_case); // unlocking - // hdr : contents destroyed - // obj : must point to the object to lock, contents preserved - // disp_hdr: must be eax & must point to the displaced header location, contents destroyed - void unlock_object(Register swap, Register obj, Register lock, Label& slow_case); + // hdr : contents destroyed + // obj : must point to the object to lock, contents preserved + // basic_lock: must be eax & must point to the basic lock, contents destroyed + void unlock_object(Register swap, Register obj, Register basic_lock, Label& slow_case); void initialize_object( Register obj, // result: pointer to object after successful allocation diff --git a/src/hotspot/share/c1/c1_CodeStubs.hpp b/src/hotspot/share/c1/c1_CodeStubs.hpp index 5d1c51bdbbf..a02368487c5 100644 --- a/src/hotspot/share/c1/c1_CodeStubs.hpp +++ b/src/hotspot/share/c1/c1_CodeStubs.hpp @@ -371,21 +371,16 @@ class MonitorEnterStub: public MonitorAccessStub { class MonitorExitStub: public MonitorAccessStub { private: - bool _compute_lock; int _monitor_ix; public: - MonitorExitStub(LIR_Opr lock_reg, bool compute_lock, int monitor_ix) + MonitorExitStub(LIR_Opr lock_reg, int monitor_ix) : MonitorAccessStub(LIR_OprFact::illegalOpr, lock_reg), - _compute_lock(compute_lock), _monitor_ix(monitor_ix) { } + _monitor_ix(monitor_ix) { } virtual void emit_code(LIR_Assembler* e); virtual void visit(LIR_OpVisitState* visitor) { assert(_obj_reg->is_illegal(), "unused"); - if (_compute_lock) { - visitor->do_temp(_lock_reg); - } else { - visitor->do_input(_lock_reg); - } + visitor->do_temp(_lock_reg); } #ifndef PRODUCT virtual void print_name(outputStream* out) const { out->print("MonitorExitStub"); } diff --git a/src/hotspot/share/c1/c1_LIRGenerator.cpp b/src/hotspot/share/c1/c1_LIRGenerator.cpp index b30667dcac3..66adfa5ed66 100644 --- a/src/hotspot/share/c1/c1_LIRGenerator.cpp +++ b/src/hotspot/share/c1/c1_LIRGenerator.cpp @@ -635,7 +635,7 @@ void LIRGenerator::monitor_exit(LIR_Opr object, LIR_Opr lock, LIR_Opr new_hdr, L // setup registers LIR_Opr hdr = lock; lock = new_hdr; - CodeStub* slow_path = new MonitorExitStub(lock, true, monitor_no); + CodeStub* slow_path = new MonitorExitStub(lock, monitor_no); __ load_stack_address_monitor(monitor_no, lock); __ unlock_object(hdr, object, lock, scratch, slow_path); } diff --git a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp index 7ddb9be540a..b91de1c9b35 100644 --- a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp +++ b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp @@ -168,7 +168,7 @@ nonstatic_field(Array, _length, int) \ nonstatic_field(Array, _data[0], Klass*) \ \ - volatile_nonstatic_field(BasicLock, _metadata, uintptr_t) \ + volatile_nonstatic_field(BasicLock, _monitor, ObjectMonitor*) \ \ static_field(CodeCache, _low_bound, address) \ static_field(CodeCache, _high_bound, address) \ diff --git a/src/hotspot/share/runtime/basicLock.cpp b/src/hotspot/share/runtime/basicLock.cpp index 71082e24bb9..4a6e7402dfa 100644 --- a/src/hotspot/share/runtime/basicLock.cpp +++ b/src/hotspot/share/runtime/basicLock.cpp @@ -74,7 +74,7 @@ void BasicLock::move_to(oop obj, BasicLock* dest) { } #ifdef ASSERT else { - dest->set_bad_metadata_deopt(); + dest->set_bad_monitor_deopt(); } #endif } diff --git a/src/hotspot/share/runtime/basicLock.hpp b/src/hotspot/share/runtime/basicLock.hpp index c22416a1c06..479748f51a4 100644 --- a/src/hotspot/share/runtime/basicLock.hpp +++ b/src/hotspot/share/runtime/basicLock.hpp @@ -37,23 +37,21 @@ class BasicLock { private: // Used as a cache of the ObjectMonitor* used when locking. Must either // be nullptr or the ObjectMonitor* used when locking. - volatile uintptr_t _metadata; + ObjectMonitor* volatile _monitor; - uintptr_t get_metadata() const { return AtomicAccess::load(&_metadata); } - void set_metadata(uintptr_t value) { AtomicAccess::store(&_metadata, value); } - static int metadata_offset_in_bytes() { return (int)offset_of(BasicLock, _metadata); } + ObjectMonitor* get_monitor() const { return AtomicAccess::load(&_monitor); } + void set_monitor(ObjectMonitor* mon) { AtomicAccess::store(&_monitor, mon); } + static int monitor_offset_in_bytes() { return (int)offset_of(BasicLock, _monitor); } public: - BasicLock() : _metadata(0) {} + BasicLock() : _monitor(nullptr) {} - void set_bad_metadata_deopt() { set_metadata(badDispHeaderDeopt); } - - static int displaced_header_offset_in_bytes() { return metadata_offset_in_bytes(); } + void set_bad_monitor_deopt() { set_monitor(reinterpret_cast(badDispHeaderDeopt)); } inline ObjectMonitor* object_monitor_cache() const; inline void clear_object_monitor_cache(); inline void set_object_monitor_cache(ObjectMonitor* mon); - static int object_monitor_cache_offset_in_bytes() { return metadata_offset_in_bytes(); } + static int object_monitor_cache_offset_in_bytes() { return monitor_offset_in_bytes(); } void print_on(outputStream* st, oop owner) const; diff --git a/src/hotspot/share/runtime/basicLock.inline.hpp b/src/hotspot/share/runtime/basicLock.inline.hpp index 2ea1fe2371c..9f0f26ee957 100644 --- a/src/hotspot/share/runtime/basicLock.inline.hpp +++ b/src/hotspot/share/runtime/basicLock.inline.hpp @@ -32,23 +32,23 @@ inline ObjectMonitor* BasicLock::object_monitor_cache() const { assert(UseObjectMonitorTable, "must be"); #if !defined(ZERO) && (defined(X86) || defined(AARCH64) || defined(RISCV64) || defined(PPC64) || defined(S390)) - return reinterpret_cast(get_metadata()); + return reinterpret_cast(get_monitor()); #else // Other platforms do not make use of the cache yet, // and are not as careful with maintaining the invariant - // that the metadata either is nullptr or ObjectMonitor*. + // that the monitor either is nullptr or a valid ObjectMonitor*. return nullptr; #endif } inline void BasicLock::clear_object_monitor_cache() { assert(UseObjectMonitorTable, "must be"); - set_metadata(0); + set_monitor(nullptr); } inline void BasicLock::set_object_monitor_cache(ObjectMonitor* mon) { assert(UseObjectMonitorTable, "must be"); - set_metadata(reinterpret_cast(mon)); + set_monitor(mon); } #endif // SHARE_RUNTIME_BASICLOCK_INLINE_HPP diff --git a/src/hotspot/share/runtime/deoptimization.cpp b/src/hotspot/share/runtime/deoptimization.cpp index d14db6ad0ed..a96c18a18aa 100644 --- a/src/hotspot/share/runtime/deoptimization.cpp +++ b/src/hotspot/share/runtime/deoptimization.cpp @@ -1656,7 +1656,7 @@ bool Deoptimization::relock_objects(JavaThread* thread, GrowableArraylock()->set_bad_metadata_deopt(); + mon_info->lock()->set_bad_monitor_deopt(); } #endif JvmtiDeferredUpdates::inc_relock_count_after_wait(deoptee_thread); diff --git a/src/hotspot/share/runtime/synchronizer.inline.hpp b/src/hotspot/share/runtime/synchronizer.inline.hpp index a44fe817276..6a850e5c8ca 100644 --- a/src/hotspot/share/runtime/synchronizer.inline.hpp +++ b/src/hotspot/share/runtime/synchronizer.inline.hpp @@ -45,7 +45,7 @@ inline ObjectMonitor* ObjectSynchronizer::read_monitor(Thread* current, oop obj, inline void ObjectSynchronizer::enter(Handle obj, BasicLock* lock, JavaThread* current) { assert(current == Thread::current(), "must be"); - LightweightSynchronizer::enter(obj, lock, current); + LightweightSynchronizer::enter(obj, lock, current); } inline bool ObjectSynchronizer::quick_enter(oop obj, BasicLock* lock, JavaThread* current) { diff --git a/src/hotspot/share/runtime/vmStructs.cpp b/src/hotspot/share/runtime/vmStructs.cpp index 86874a967e3..ac095be6936 100644 --- a/src/hotspot/share/runtime/vmStructs.cpp +++ b/src/hotspot/share/runtime/vmStructs.cpp @@ -682,7 +682,6 @@ unchecked_nonstatic_field(ObjectMonitor, _object, sizeof(void *)) /* NOTE: no type */ \ volatile_nonstatic_field(ObjectMonitor, _owner, int64_t) \ volatile_nonstatic_field(ObjectMonitor, _next_om, ObjectMonitor*) \ - volatile_nonstatic_field(BasicLock, _metadata, uintptr_t) \ nonstatic_field(ObjectMonitor, _contentions, int) \ volatile_nonstatic_field(ObjectMonitor, _waiters, int) \ volatile_nonstatic_field(ObjectMonitor, _recursions, intx) \ diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/oops/Mark.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/oops/Mark.java index 1ea34fe6cd2..c41372810a3 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/oops/Mark.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/oops/Mark.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -151,12 +151,6 @@ public class Mark extends VMObject { public boolean hasLocker() { return ((value() & lockMaskInPlace) == lockedValue); } - public BasicLock locker() { - if (Assert.ASSERTS_ENABLED) { - Assert.that(hasLocker(), "check"); - } - return new BasicLock(valueAsAddress()); - } public boolean hasMonitor() { return ((value() & monitorValue) != 0); } diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/BasicLock.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/BasicLock.java index 979e08cfb8a..6ca53b4c545 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/BasicLock.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/BasicLock.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,23 +36,11 @@ public class BasicLock extends VMObject { static { VM.registerVMInitializedObserver(new Observer() { public void update(Observable o, Object data) { - initialize(VM.getVM().getTypeDataBase()); } }); } - private static synchronized void initialize(TypeDataBase db) throws WrongTypeException { - Type type = db.lookupType("BasicLock"); - displacedHeaderField = type.getCIntegerField("_metadata"); - } - - private static CIntegerField displacedHeaderField; - public BasicLock(Address addr) { super(addr); } - - public Mark displacedHeader() { - return new Mark(addr.addOffsetTo(displacedHeaderField.getOffset())); - } } From ba44656b97b7103d96718452e300df8a6bd59c87 Mon Sep 17 00:00:00 2001 From: Alice Pellegrini Date: Thu, 25 Sep 2025 08:44:14 +0000 Subject: [PATCH 225/556] 8366454: TLS1.3 server fails with bad_record_mac when receiving encrypted records with empty body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Jeliński Reviewed-by: djelinski --- .../share/classes/sun/security/ssl/Alert.java | 10 ++++++++++ .../share/classes/sun/security/ssl/SSLCipher.java | 13 ++++++------- .../ssl/SSLEngineImpl/SSLEngineEmptyFragments.java | 6 +++--- .../ssl/SSLSocketImpl/SSLSocketEmptyFragments.java | 8 ++++---- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/java.base/share/classes/sun/security/ssl/Alert.java b/src/java.base/share/classes/sun/security/ssl/Alert.java index 960b3f3b37d..ed5e079bf44 100644 --- a/src/java.base/share/classes/sun/security/ssl/Alert.java +++ b/src/java.base/share/classes/sun/security/ssl/Alert.java @@ -181,6 +181,16 @@ public enum Alert { AlertMessage(TransportContext context, ByteBuffer m) throws IOException { + // From RFC 8446 "Implementations + // MUST NOT send Handshake and Alert records that have a zero-length + // TLSInnerPlaintext.content; if such a message is received, the + // receiving implementation MUST terminate the connection with an + // "unexpected_message" alert." + if (m.remaining() == 0) { + throw context.fatal(Alert.UNEXPECTED_MESSAGE, + "Alert fragments must not be zero length."); + } + // struct { // AlertLevel level; // AlertDescription description; diff --git a/src/java.base/share/classes/sun/security/ssl/SSLCipher.java b/src/java.base/share/classes/sun/security/ssl/SSLCipher.java index d11ffc96b47..4a52a2ea583 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLCipher.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLCipher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1923,9 +1923,9 @@ enum SSLCipher { // remove inner plaintext padding int i = pt.limit() - 1; - for (; i > 0 && pt.get(i) == 0; i--); + for (; i >= pos && pt.get(i) == 0; i--); - if (i < (pos + 1)) { + if (i < pos) { throw new BadPaddingException( "Incorrect inner plaintext: no content type"); } @@ -2441,10 +2441,9 @@ enum SSLCipher { // remove inner plaintext padding int i = pt.limit() - 1; - for (; i > 0 && pt.get(i) == 0; i--) { - // blank - } - if (i < (pos + 1)) { + for (; i >= pos && pt.get(i) == 0; i--); + + if (i < pos) { throw new BadPaddingException( "Incorrect inner plaintext: no content type"); } diff --git a/test/jdk/sun/security/ssl/SSLEngineImpl/SSLEngineEmptyFragments.java b/test/jdk/sun/security/ssl/SSLEngineImpl/SSLEngineEmptyFragments.java index f2aa8041b9a..837e607c009 100644 --- a/test/jdk/sun/security/ssl/SSLEngineImpl/SSLEngineEmptyFragments.java +++ b/test/jdk/sun/security/ssl/SSLEngineImpl/SSLEngineEmptyFragments.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -103,7 +103,7 @@ public class SSLEngineEmptyFragments extends SSLContextTemplate { try { unwrap(serverEngine, alert, serverIn); throw new RuntimeException("Expected exception was not thrown."); - } catch (SSLHandshakeException exc) { + } catch (SSLProtocolException exc) { log("Got the exception I wanted."); } } @@ -133,7 +133,7 @@ public class SSLEngineEmptyFragments extends SSLContextTemplate { unwrap(serverEngine, alert, serverIn); log("Server unwrap was successful when it should have failed."); throw new RuntimeException("Expected exception was not thrown."); - } catch (SSLHandshakeException exc) { + } catch (SSLProtocolException exc) { log("Got the exception I wanted."); } } diff --git a/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketEmptyFragments.java b/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketEmptyFragments.java index dec93c1c199..de0ef43924d 100644 --- a/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketEmptyFragments.java +++ b/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketEmptyFragments.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -351,9 +351,9 @@ public class SSLSocketEmptyFragments extends SSLContextTemplate { tests.executeTest( tests::testEmptyHandshakeRecord, SSLProtocolException.class); tests.executeTest( - tests::testEmptyAlertNotHandshaking, SSLHandshakeException.class); + tests::testEmptyAlertNotHandshaking, SSLProtocolException.class); tests.executeTest( - tests::testEmptyAlertDuringHandshake, SSLHandshakeException.class); + tests::testEmptyAlertDuringHandshake, SSLProtocolException.class); tests.executeTest( tests::testEmptyChangeCipherSpecMessage, SSLProtocolException.class); @@ -361,6 +361,6 @@ public class SSLSocketEmptyFragments extends SSLContextTemplate { tests.executeTest( tests::testEmptyHandshakeRecord, SSLProtocolException.class); tests.executeTest( - tests::testEmptyAlertNotHandshaking, SSLHandshakeException.class); + tests::testEmptyAlertNotHandshaking, SSLProtocolException.class); } } From d407ef651032de687e3d4a2a2db211cab1016676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20Sikstr=C3=B6m?= Date: Thu, 25 Sep 2025 09:38:43 +0000 Subject: [PATCH 226/556] 8368251: Parallel: Refactor lgrp_id used in MutableNUMASpace Reviewed-by: lkorinth, ayang, tschatzl --- src/hotspot/os/linux/os_linux.cpp | 11 +- src/hotspot/os/posix/os_posix.inline.hpp | 5 +- src/hotspot/os/windows/os_windows.cpp | 14 --- src/hotspot/os/windows/os_windows.inline.hpp | 4 +- .../share/gc/parallel/mutableNUMASpace.cpp | 100 ++++-------------- .../share/gc/parallel/mutableNUMASpace.hpp | 2 +- src/hotspot/share/gc/parallel/psYoungGen.cpp | 2 +- src/hotspot/share/runtime/os.hpp | 1 - src/hotspot/share/runtime/thread.cpp | 2 +- src/hotspot/share/runtime/thread.hpp | 4 +- 10 files changed, 29 insertions(+), 116 deletions(-) diff --git a/src/hotspot/os/linux/os_linux.cpp b/src/hotspot/os/linux/os_linux.cpp index 3d44d839735..9ec892d4305 100644 --- a/src/hotspot/os/linux/os_linux.cpp +++ b/src/hotspot/os/linux/os_linux.cpp @@ -860,11 +860,9 @@ static void *thread_native_entry(Thread *thread) { osthread->set_thread_id(checked_cast(os::current_thread_id())); if (UseNUMA) { - int lgrp_id = os::numa_get_group_id(); - if (lgrp_id != -1) { - thread->set_lgrp_id(lgrp_id); - } + thread->update_lgrp_id(); } + // initialize signal mask for this thread PosixSignals::hotspot_sigmask(thread); @@ -1191,10 +1189,7 @@ bool os::create_attached_thread(JavaThread* thread) { thread->set_osthread(osthread); if (UseNUMA) { - int lgrp_id = os::numa_get_group_id(); - if (lgrp_id != -1) { - thread->set_lgrp_id(lgrp_id); - } + thread->update_lgrp_id(); } if (os::is_primordial_thread()) { diff --git a/src/hotspot/os/posix/os_posix.inline.hpp b/src/hotspot/os/posix/os_posix.inline.hpp index 67be82e4d63..f1ec9411f2a 100644 --- a/src/hotspot/os/posix/os_posix.inline.hpp +++ b/src/hotspot/os/posix/os_posix.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,9 +34,6 @@ #include #include -// Aix does not have NUMA support but need these for compilation. -inline bool os::numa_has_group_homing() { AIX_ONLY(ShouldNotReachHere();) return false; } - // Platform Mutex/Monitor implementation inline void PlatformMutex::lock() { diff --git a/src/hotspot/os/windows/os_windows.cpp b/src/hotspot/os/windows/os_windows.cpp index 8d4c93f3080..34788a53710 100644 --- a/src/hotspot/os/windows/os_windows.cpp +++ b/src/hotspot/os/windows/os_windows.cpp @@ -534,13 +534,6 @@ static unsigned thread_native_entry(void* t) { OSThread* osthr = thread->osthread(); assert(osthr->get_state() == RUNNABLE, "invalid os thread state"); - if (UseNUMA) { - int lgrp_id = os::numa_get_group_id(); - if (lgrp_id != -1) { - thread->set_lgrp_id(lgrp_id); - } - } - // Diagnostic code to investigate JDK-6573254 int res = 30115; // non-java thread if (thread->is_Java_thread()) { @@ -598,13 +591,6 @@ static OSThread* create_os_thread(Thread* thread, HANDLE thread_handle, osthread->set_thread_handle(thread_handle); osthread->set_thread_id(thread_id); - if (UseNUMA) { - int lgrp_id = os::numa_get_group_id(); - if (lgrp_id != -1) { - thread->set_lgrp_id(lgrp_id); - } - } - // Initial thread state is INITIALIZED, not SUSPENDED osthread->set_state(INITIALIZED); diff --git a/src/hotspot/os/windows/os_windows.inline.hpp b/src/hotspot/os/windows/os_windows.inline.hpp index 41471224b6c..9eebbee2d8b 100644 --- a/src/hotspot/os/windows/os_windows.inline.hpp +++ b/src/hotspot/os/windows/os_windows.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -62,8 +62,6 @@ inline void os::map_stack_shadow_pages(address sp) { state->set_shadow_zone_growth_watermark(original_sp); } -inline bool os::numa_has_group_homing() { return false; } - // Platform Mutex/Monitor implementation inline void PlatformMutex::lock() { diff --git a/src/hotspot/share/gc/parallel/mutableNUMASpace.cpp b/src/hotspot/share/gc/parallel/mutableNUMASpace.cpp index 08569fd5bd1..df4312ebd75 100644 --- a/src/hotspot/share/gc/parallel/mutableNUMASpace.cpp +++ b/src/hotspot/share/gc/parallel/mutableNUMASpace.cpp @@ -118,73 +118,30 @@ size_t MutableNUMASpace::free_in_words() const { return s; } -int MutableNUMASpace::lgrp_space_index(int lgrp_id) const { - return lgrp_spaces()->find_if([&](LGRPSpace* space) { - return space->lgrp_id() == checked_cast(lgrp_id); +MutableNUMASpace::LGRPSpace *MutableNUMASpace::lgrp_space_for_thread(Thread* thr) const { + guarantee(thr != nullptr, "No thread"); + + int lgrp_id = thr->lgrp_id(); + assert(lgrp_id != -1, "lgrp_id must be set during thread creation"); + + int lgrp_spaces_index = lgrp_spaces()->find_if([&](LGRPSpace* space) { + return space->lgrp_id() == (uint)lgrp_id; }); + + assert(lgrp_spaces_index != -1, "must have created spaces for all lgrp_ids"); + return lgrp_spaces()->at(lgrp_spaces_index); } size_t MutableNUMASpace::tlab_capacity(Thread *thr) const { - guarantee(thr != nullptr, "No thread"); - int lgrp_id = thr->lgrp_id(); - if (lgrp_id == -1) { - // This case can occur after the topology of the system has - // changed. Thread can change their location, the new home - // group will be determined during the first allocation - // attempt. For now we can safely assume that all spaces - // have equal size because the whole space will be reinitialized. - if (lgrp_spaces()->length() > 0) { - return capacity_in_bytes() / lgrp_spaces()->length(); - } else { - assert(false, "There should be at least one locality group"); - return 0; - } - } - // That's the normal case, where we know the locality group of the thread. - int i = lgrp_space_index(lgrp_id); - if (i == -1) { - return 0; - } - return lgrp_spaces()->at(i)->space()->capacity_in_bytes(); + return lgrp_space_for_thread(thr)->space()->capacity_in_bytes(); } size_t MutableNUMASpace::tlab_used(Thread *thr) const { - // Please see the comments for tlab_capacity(). - guarantee(thr != nullptr, "No thread"); - int lgrp_id = thr->lgrp_id(); - if (lgrp_id == -1) { - if (lgrp_spaces()->length() > 0) { - return (used_in_bytes()) / lgrp_spaces()->length(); - } else { - assert(false, "There should be at least one locality group"); - return 0; - } - } - int i = lgrp_space_index(lgrp_id); - if (i == -1) { - return 0; - } - return lgrp_spaces()->at(i)->space()->used_in_bytes(); + return lgrp_space_for_thread(thr)->space()->used_in_bytes(); } - size_t MutableNUMASpace::unsafe_max_tlab_alloc(Thread *thr) const { - // Please see the comments for tlab_capacity(). - guarantee(thr != nullptr, "No thread"); - int lgrp_id = thr->lgrp_id(); - if (lgrp_id == -1) { - if (lgrp_spaces()->length() > 0) { - return free_in_bytes() / lgrp_spaces()->length(); - } else { - assert(false, "There should be at least one locality group"); - return 0; - } - } - int i = lgrp_space_index(lgrp_id); - if (i == -1) { - return 0; - } - return lgrp_spaces()->at(i)->space()->free_in_bytes(); + return lgrp_space_for_thread(thr)->space()->free_in_bytes(); } // Bias region towards the first-touching lgrp. Set the right page sizes. @@ -528,32 +485,13 @@ void MutableNUMASpace::clear(bool mangle_space) { } } -/* - Linux supports static memory binding, therefore the most part of the - logic dealing with the possible invalid page allocation is effectively - disabled. Besides there is no notion of the home node in Linux. A - thread is allowed to migrate freely. Although the scheduler is rather - reluctant to move threads between the nodes. We check for the current - node every allocation. And with a high probability a thread stays on - the same node for some time allowing local access to recently allocated - objects. - */ - HeapWord* MutableNUMASpace::cas_allocate(size_t size) { - Thread* thr = Thread::current(); - int lgrp_id = thr->lgrp_id(); - if (lgrp_id == -1 || !os::numa_has_group_homing()) { - lgrp_id = os::numa_get_group_id(); - thr->set_lgrp_id(lgrp_id); - } + Thread *thr = Thread::current(); - int i = lgrp_space_index(lgrp_id); - // It is possible that a new CPU has been hotplugged and - // we haven't reshaped the space accordingly. - if (i == -1) { - i = os::random() % lgrp_spaces()->length(); - } - LGRPSpace *ls = lgrp_spaces()->at(i); + // Update the locality group to match where the thread actually is. + thr->update_lgrp_id(); + + LGRPSpace *ls = lgrp_space_for_thread(thr); MutableSpace *s = ls->space(); HeapWord *p = s->cas_allocate(size); if (p != nullptr) { diff --git a/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp b/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp index abb4c77952a..3699dcde964 100644 --- a/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp +++ b/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp @@ -152,7 +152,7 @@ class MutableNUMASpace : public MutableSpace { void select_tails(MemRegion new_region, MemRegion intersection, MemRegion* bottom_region, MemRegion *top_region); - int lgrp_space_index(int lgrp_id) const; + LGRPSpace *lgrp_space_for_thread(Thread *thr) const; public: GrowableArray* lgrp_spaces() const { return _lgrp_spaces; } diff --git a/src/hotspot/share/gc/parallel/psYoungGen.cpp b/src/hotspot/share/gc/parallel/psYoungGen.cpp index 21d023f8207..4fbe314d14c 100644 --- a/src/hotspot/share/gc/parallel/psYoungGen.cpp +++ b/src/hotspot/share/gc/parallel/psYoungGen.cpp @@ -314,7 +314,7 @@ HeapWord* PSYoungGen::expand_and_allocate(size_t word_size) { } HeapWord* result = eden_space()->cas_allocate(word_size); - assert(result, "inv"); + assert(result || UseNUMA, "inv"); return result; } diff --git a/src/hotspot/share/runtime/os.hpp b/src/hotspot/share/runtime/os.hpp index 626970fa4fb..ff215105e8e 100644 --- a/src/hotspot/share/runtime/os.hpp +++ b/src/hotspot/share/runtime/os.hpp @@ -535,7 +535,6 @@ class os: AllStatic { static void realign_memory(char *addr, size_t bytes, size_t alignment_hint); // NUMA-specific interface - static bool numa_has_group_homing(); static void numa_make_local(char *addr, size_t bytes, int lgrp_hint); static void numa_make_global(char *addr, size_t bytes); static size_t numa_get_groups_num(); diff --git a/src/hotspot/share/runtime/thread.cpp b/src/hotspot/share/runtime/thread.cpp index 361743f83e8..32c99320bdf 100644 --- a/src/hotspot/share/runtime/thread.cpp +++ b/src/hotspot/share/runtime/thread.cpp @@ -65,7 +65,7 @@ Thread::Thread(MemTag mem_tag) { // stack and get_thread set_stack_base(nullptr); set_stack_size(0); - set_lgrp_id(-1); + _lgrp_id = -1; DEBUG_ONLY(clear_suspendible_thread();) DEBUG_ONLY(clear_indirectly_suspendible_thread();) DEBUG_ONLY(clear_indirectly_safepoint_thread();) diff --git a/src/hotspot/share/runtime/thread.hpp b/src/hotspot/share/runtime/thread.hpp index fe2b997f94c..f752ce87644 100644 --- a/src/hotspot/share/runtime/thread.hpp +++ b/src/hotspot/share/runtime/thread.hpp @@ -537,8 +537,8 @@ protected: void register_thread_stack_with_NMT(); void unregister_thread_stack_with_NMT(); - int lgrp_id() const { return _lgrp_id; } - void set_lgrp_id(int value) { _lgrp_id = value; } + int lgrp_id() const { return _lgrp_id; } + void update_lgrp_id() { _lgrp_id = os::numa_get_group_id(); } // Printing void print_on(outputStream* st, bool print_extended_info) const; From d85e410c191bdcc8c20498f1c3c4516193bc79dd Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Thu, 25 Sep 2025 09:42:53 +0000 Subject: [PATCH 227/556] 8368546: java/net/httpclient/RedirectTimeoutTest.java fails intermittently for HTTP/3 in tier7 Reviewed-by: jpai, syan, djelinski, vyazici --- test/jdk/java/net/httpclient/RedirectTimeoutTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/java/net/httpclient/RedirectTimeoutTest.java b/test/jdk/java/net/httpclient/RedirectTimeoutTest.java index be634398ba2..3e921879fe3 100644 --- a/test/jdk/java/net/httpclient/RedirectTimeoutTest.java +++ b/test/jdk/java/net/httpclient/RedirectTimeoutTest.java @@ -133,7 +133,7 @@ public class RedirectTimeoutTest implements HttpServerAdapters { if (version == HTTP_3) { reqBuilder = reqBuilder.version(HTTP_3).setOption(H3_DISCOVERY, HTTP_3_URI_ONLY); } - HttpRequest request = reqBuilder + HttpRequest request = reqBuilder.copy() .GET() .version(version) .timeout(Duration.ofMillis(adjustTimeout(TIMEOUT_MILLIS))) From 4f4030a423d04c8f488d143f0eda4a8de9dbd469 Mon Sep 17 00:00:00 2001 From: Serhiy Sachkov Date: Thu, 25 Sep 2025 09:51:51 +0000 Subject: [PATCH 228/556] 8333526: Restructure java/nio/channels/DatagramChannel/StressNativeSignal.java to a fail fast exception handling policy Reviewed-by: dfuchs --- .../DatagramChannel/StressNativeSignal.java | 87 +++++++------------ 1 file changed, 30 insertions(+), 57 deletions(-) diff --git a/test/jdk/java/nio/channels/DatagramChannel/StressNativeSignal.java b/test/jdk/java/nio/channels/DatagramChannel/StressNativeSignal.java index d6d2f083eca..cc726d19c0d 100644 --- a/test/jdk/java/nio/channels/DatagramChannel/StressNativeSignal.java +++ b/test/jdk/java/nio/channels/DatagramChannel/StressNativeSignal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,42 +36,23 @@ import java.nio.channels.DatagramChannel; import java.util.concurrent.CountDownLatch; public class StressNativeSignal { - private UDPThread udpThread; - private ServerSocketThread serverSocketThread; + private final UDPThread udpThread; + private final ServerSocketThread serverSocketThread; - StressNativeSignal() { + StressNativeSignal() throws IOException { serverSocketThread = initServerSocketThread(); - if (serverSocketThread != null) { - serverSocketThread.start(); - } + serverSocketThread.start(); udpThread = initUDPThread(); - if (udpThread != null) { - udpThread.start(); - } + udpThread.start(); } - private UDPThread initUDPThread() { - UDPThread aUDPThread = null; - try { - aUDPThread = new UDPThread(); - } catch (Exception z) { - System.err.println("failed to create and start a UDPThread"); - z.printStackTrace(); - } - return aUDPThread; + private UDPThread initUDPThread() throws IOException { + return new UDPThread(); } - private ServerSocketThread initServerSocketThread() { - ServerSocketThread aServerSocketThread = null; - try { - aServerSocketThread = new ServerSocketThread(); - - } catch (Exception z) { - System.err.println("failed to create and start a ServerSocketThread"); - z.printStackTrace(); - } - return aServerSocketThread; + private ServerSocketThread initServerSocketThread() throws IOException { + return new ServerSocketThread(); } public static void main(String[] args) throws Throwable { @@ -80,46 +61,39 @@ public class StressNativeSignal { test.shutdown(); } - public void shutdown() { - if ((udpThread != null) && udpThread.isAlive()) { + public void shutdown() throws InterruptedException, IOException { + if (udpThread != null && udpThread.isAlive()) { udpThread.terminate(); - try { - udpThread.join(); - } catch (Exception z) { - z.printStackTrace(System.err); - } + udpThread.join(); + } else { System.out.println("UDPThread test scenario was not run"); } - if ((serverSocketThread != null) && (serverSocketThread.isAlive())) { + if (serverSocketThread != null && serverSocketThread.isAlive()) { serverSocketThread.terminate(); - try { - serverSocketThread.join(); - } catch (Exception z) { - z.printStackTrace(System.err); - } + serverSocketThread.join(); } else { System.out.println("ServerSocketThread test scenario was not run"); } } - public void waitForTestThreadsToStart() { - if ((udpThread != null) && udpThread.isAlive()) { + public void waitForTestThreadsToStart() throws InterruptedException { + if (udpThread != null && udpThread.isAlive()) { udpThread.waitTestThreadStart(); } - if ((serverSocketThread != null) && (serverSocketThread.isAlive())) { + if (serverSocketThread != null && serverSocketThread.isAlive()) { serverSocketThread.waitTestThreadStart(); } } public class ServerSocketThread extends Thread { private volatile boolean shouldTerminate; - private ServerSocket socket; + private final ServerSocket socket; private final CountDownLatch threadStarted = new CountDownLatch(1); - public ServerSocketThread () throws Exception { - socket = new ServerSocket(1122); + public ServerSocketThread() throws IOException { + socket = new ServerSocket(0); } public void run() { @@ -129,7 +103,7 @@ public class StressNativeSignal { Socket client = socket.accept(); client.close(); throw new RuntimeException("Unexpected return from accept call"); - } catch (Exception z) { + } catch (IOException z) { System.err.println("ServerSocketThread: caught exception " + z.getClass().getName()); if (!shouldTerminate) { z.printStackTrace(System.err); @@ -141,7 +115,7 @@ public class StressNativeSignal { shouldTerminate = true; try { socket.close(); - } catch (Exception z) { + } catch (IOException z) { z.printStackTrace(System.err); // ignore } @@ -150,7 +124,7 @@ public class StressNativeSignal { public void waitTestThreadStart() { try { threadStarted.await(); - } catch (Exception z) { + } catch (InterruptedException z) { z.printStackTrace(System.err); // ignore } @@ -158,15 +132,14 @@ public class StressNativeSignal { } public class UDPThread extends Thread { - private DatagramChannel channel; + private final DatagramChannel channel; private volatile boolean shouldTerminate; private final CountDownLatch threadStarted = new CountDownLatch(1); - public UDPThread () throws Exception { - + public UDPThread() throws IOException { channel = DatagramChannel.open(); channel.setOption(StandardSocketOptions.SO_RCVBUF, 6553600); - channel.bind(new InetSocketAddress(19870)); + channel.bind(new InetSocketAddress(0)); } @Override @@ -191,7 +164,7 @@ public class StressNativeSignal { shouldTerminate = true; try { channel.close(); - } catch (Exception z) { + } catch (IOException z) { System.err.println("UDPThread: caught exception " + z.getClass().getName()); z.printStackTrace(System.err); // ignore @@ -201,7 +174,7 @@ public class StressNativeSignal { public void waitTestThreadStart() { try { threadStarted.await(); - } catch (Exception z) { + } catch (InterruptedException z) { z.printStackTrace(System.err); // ignore } From 44cb9cad263b4fe2749fd6c223b657d77dca5119 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 25 Sep 2025 09:59:37 +0000 Subject: [PATCH 229/556] 8368518: [s390x] test failure with failed: wrong size of mach node Reviewed-by: dlong, mdoerr, lucy --- src/hotspot/cpu/s390/c2_globals_s390.hpp | 3 +-- src/hotspot/cpu/s390/s390.ad | 1 + .../share/runtime/flags/jvmFlagConstraintsCompiler.cpp | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/hotspot/cpu/s390/c2_globals_s390.hpp b/src/hotspot/cpu/s390/c2_globals_s390.hpp index 94190c25f5b..f1e757889b0 100644 --- a/src/hotspot/cpu/s390/c2_globals_s390.hpp +++ b/src/hotspot/cpu/s390/c2_globals_s390.hpp @@ -45,8 +45,7 @@ define_pd_global(intx, CompileThreshold, 10000); define_pd_global(intx, OnStackReplacePercentage, 140); define_pd_global(intx, ConditionalMoveLimit, 4); define_pd_global(intx, FreqInlineSize, 175); -// 10 prevents spill-split-recycle sanity check in JVM2008.xml.transform. -define_pd_global(intx, InteriorEntryAlignment, 2); +define_pd_global(intx, InteriorEntryAlignment, 4); define_pd_global(size_t, NewSizeThreadIncrease, ScaleForWordSize(4*K)); define_pd_global(intx, RegisterCostAreaRatio, 12000); define_pd_global(intx, LoopUnrollLimit, 60); diff --git a/src/hotspot/cpu/s390/s390.ad b/src/hotspot/cpu/s390/s390.ad index 4c6b0d96e6f..e9377733d2d 100644 --- a/src/hotspot/cpu/s390/s390.ad +++ b/src/hotspot/cpu/s390/s390.ad @@ -1947,6 +1947,7 @@ bool Matcher::is_spillable_arg(int reg) { uint Matcher::int_pressure_limit() { // Medium size register set, 6 special purpose regs, 3 SOE regs. + // 10 prevents spill-split-recycle sanity check in JVM2008.xml.transform. return (INTPRESSURE == -1) ? 10 : INTPRESSURE; } diff --git a/src/hotspot/share/runtime/flags/jvmFlagConstraintsCompiler.cpp b/src/hotspot/share/runtime/flags/jvmFlagConstraintsCompiler.cpp index 4e28135dccf..d0141c2e6cc 100644 --- a/src/hotspot/share/runtime/flags/jvmFlagConstraintsCompiler.cpp +++ b/src/hotspot/share/runtime/flags/jvmFlagConstraintsCompiler.cpp @@ -355,10 +355,8 @@ JVMFlag::Error InteriorEntryAlignmentConstraintFunc(intx value, bool verbose) { } int minimum_alignment = 16; -#if defined(X86) && !defined(AMD64) +#if (defined(X86) && !defined(AMD64)) || defined(S390) minimum_alignment = 4; -#elif defined(S390) - minimum_alignment = 2; #endif if (InteriorEntryAlignment < minimum_alignment) { From 67cb53d0888adfeb2909296e21d0532bc3643326 Mon Sep 17 00:00:00 2001 From: Dingli Zhang Date: Thu, 25 Sep 2025 10:06:57 +0000 Subject: [PATCH 230/556] 8368206: RISC-V: compiler/vectorapi/VectorMaskCompareNotTest.java fails when running without RVV Reviewed-by: fyang, mhaessig, mli --- .../jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java index 851113ea4de..235093cceed 100644 --- a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java @@ -35,6 +35,7 @@ import jdk.test.lib.Asserts; * @library /test/lib / * @summary test combining vector not operation with compare * @modules jdk.incubator.vector + * @requires (os.arch != "riscv64" | (os.arch == "riscv64" & vm.cpu.features ~= ".*rvv.*")) * * @run driver compiler.vectorapi.VectorMaskCompareNotTest */ From 2407eb0522d192135a6bed52e88be5a59cba8f6c Mon Sep 17 00:00:00 2001 From: Daniel Gredler Date: Thu, 25 Sep 2025 10:08:56 +0000 Subject: [PATCH 231/556] 8367867: [macosx] Ignorable whitespace in text not removed when printing Reviewed-by: prr, serb --- .../sun/lwawt/macosx/CPrinterGraphics.java | 14 +++++++++++++- .../share/classes/sun/print/RasterPrinterJob.java | 15 +++++++++------ .../classes/sun/awt/windows/WPathGraphics.java | 5 +++-- .../classes/sun/awt/windows/WPrinterJob.java | 10 +--------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java index c3ed089472b..45fcdfd2c0c 100644 --- a/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java +++ b/src/java.desktop/macosx/classes/sun/lwawt/macosx/CPrinterGraphics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -75,4 +75,16 @@ public final class CPrinterGraphics extends ProxyGraphics2D { // needToCopyBgColorImage, is private instead of protected!) return getDelegate().drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, bgcolor, observer); } + + @Override + public void drawString(String str, int x, int y) { + str = RasterPrinterJob.removeControlChars(str); + super.drawString(str, x, y); + } + + @Override + public void drawString(String str, float x, float y) { + str = RasterPrinterJob.removeControlChars(str); + super.drawString(str, x, y); + } } diff --git a/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java b/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java index 2a6d45e2ba8..754af87b94e 100644 --- a/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java +++ b/src/java.desktop/share/classes/sun/print/RasterPrinterJob.java @@ -2470,13 +2470,16 @@ public abstract class RasterPrinterJob extends PrinterJob { g.setPaint(Color.black); } - /* On-screen drawString renders most control chars as the missing glyph - * and have the non-zero advance of that glyph. - * Exceptions are \t, \n and \r which are considered zero-width. - * This is a utility method used by subclasses to remove them so we - * don't have to worry about platform or font specific handling of them. + /** + * Removes ignorable whitespace from the specified text, so that there + * is no need for platform-specific or font-specific custom whitespace + * handling, and so that these characters are not treated like control + * characters which are printed as the missing glyph. + * + * @param s the text to process + * @return the input text, with ignorable whitespace (if any) removed */ - protected String removeControlChars(String s) { + public static String removeControlChars(String s) { char[] in_chars = s.toCharArray(); int len = in_chars.length; char[] out_chars = new char[len]; diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java b/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java index 75e8fa42da0..87b1591c0eb 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WPathGraphics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -73,6 +73,7 @@ import sun.font.TrueTypeFont; import sun.print.PathGraphics; import sun.print.ProxyGraphics2D; +import sun.print.RasterPrinterJob; final class WPathGraphics extends PathGraphics { @@ -847,7 +848,7 @@ final class WPathGraphics extends PathGraphics { * removed now so the string and positions are the same length. * For other cases we need to pass glyph codes to GDI. */ - str = wPrinterJob.removeControlChars(str); + str = RasterPrinterJob.removeControlChars(str); char[] chars = str.toCharArray(); int len = chars.length; GlyphVector gv = null; diff --git a/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java b/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java index e50cfcff33b..17d41036bcc 100644 --- a/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java +++ b/src/java.desktop/windows/classes/sun/awt/windows/WPrinterJob.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -1202,14 +1202,6 @@ public final class WPrinterJob extends RasterPrinterJob } } - /** - * Remove control characters. - */ - @Override - protected String removeControlChars(String str) { - return super.removeControlChars(str); - } - /** * Draw the string {@code text} to the printer's * device context at the specified position. From 52e550462798c568a8a5457af2f9554fd784cd8a Mon Sep 17 00:00:00 2001 From: Guanqiang Han Date: Thu, 25 Sep 2025 11:55:18 +0000 Subject: [PATCH 232/556] 8368089: G1: G1PeriodicGCTask::should_start_periodic_gc may use uninitialised value if os::loadavg is unsupported Reviewed-by: ayang, tschatzl, iwalulya --- src/hotspot/share/gc/g1/g1PeriodicGCTask.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1PeriodicGCTask.cpp b/src/hotspot/share/gc/g1/g1PeriodicGCTask.cpp index afbd0f35ce6..0a7367fcab5 100644 --- a/src/hotspot/share/gc/g1/g1PeriodicGCTask.cpp +++ b/src/hotspot/share/gc/g1/g1PeriodicGCTask.cpp @@ -54,11 +54,17 @@ bool G1PeriodicGCTask::should_start_periodic_gc(G1CollectedHeap* g1h, // Check if load is lower than max. double recent_load; - if ((G1PeriodicGCSystemLoadThreshold > 0.0f) && - (os::loadavg(&recent_load, 1) == -1 || recent_load > G1PeriodicGCSystemLoadThreshold)) { - log_debug(gc, periodic)("Load %1.2f is higher than threshold %1.2f. Skipping.", - recent_load, G1PeriodicGCSystemLoadThreshold); - return false; + if (G1PeriodicGCSystemLoadThreshold > 0.0) { + if (os::loadavg(&recent_load, 1) == -1) { + G1PeriodicGCSystemLoadThreshold = 0.0; + log_warning(gc, periodic)("System loadavg() call failed, " + "disabling G1PeriodicGCSystemLoadThreshold check."); + // Fall through and start the periodic GC. + } else if (recent_load > G1PeriodicGCSystemLoadThreshold) { + log_debug(gc, periodic)("Load %1.2f is higher than threshold %1.2f. Skipping.", + recent_load, G1PeriodicGCSystemLoadThreshold); + return false; + } } // Record counters with GC safepoints blocked, to get a consistent snapshot. From 77a71c5b097500ea2cab0c84f87553e833692fd2 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Thu, 25 Sep 2025 12:08:39 +0000 Subject: [PATCH 233/556] 8366896: JFR: Use GarbageCollection.name in gc view Reviewed-by: mgronlun --- .../share/classes/jdk/jfr/internal/query/view.ini | 10 ++++------ test/jdk/jdk/jfr/jcmd/TestJcmdView.java | 2 +- test/jdk/jdk/jfr/tool/TestView.java | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini index 1ab9840b28e..91decb1aa20 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini @@ -290,18 +290,16 @@ table = "SELECT finalizableClass, LAST_BATCH(objects) AS O, LAST_BATCH(totalFina [jvm.gc] label = "Garbage Collections" -table = "COLUMN 'Start', 'GC ID', 'Type', 'Heap Before GC', 'Heap After GC', 'Longest Pause' +table = "COLUMN 'Start', 'GC ID', 'GC Name', 'Heap Before GC', 'Heap After GC', 'Longest Pause' FORMAT none, none, missing:Unknown, none, none, none - SELECT G.startTime, gcId, [Y|O].eventType.label, + SELECT G.startTime, gcId, G.name, B.heapUsed, A.heapUsed, longestPause FROM GarbageCollection AS G, GCHeapSummary AS B, - GCHeapSummary AS A, - OldGarbageCollection AS O, - YoungGarbageCollection AS Y + GCHeapSummary AS A WHERE B.when = 'Before GC' AND A.when = 'After GC' - GROUP BY gcId ORDER BY G.startTime" + GROUP BY gcId ORDER BY gcId" [jvm.gc-concurrent-phases] label = "Concurrent GC Phases" diff --git a/test/jdk/jdk/jfr/jcmd/TestJcmdView.java b/test/jdk/jdk/jfr/jcmd/TestJcmdView.java index 92e5c950f9b..206d6c7bc8f 100644 --- a/test/jdk/jdk/jfr/jcmd/TestJcmdView.java +++ b/test/jdk/jdk/jfr/jcmd/TestJcmdView.java @@ -163,7 +163,7 @@ public class TestJcmdView { // Verify verbose heading output.shouldContain("(longestPause)"); // Verify row contents - output.shouldContain("Old Garbage Collection"); + output.shouldContain("G1"); // Verify verbose query output.shouldContain("SELECT"); } diff --git a/test/jdk/jdk/jfr/tool/TestView.java b/test/jdk/jdk/jfr/tool/TestView.java index 89c45c3e068..38174859b51 100644 --- a/test/jdk/jdk/jfr/tool/TestView.java +++ b/test/jdk/jdk/jfr/tool/TestView.java @@ -84,7 +84,7 @@ public class TestView { // Verify verbose heading output.shouldContain("(longestPause)"); // Verify row contents - output.shouldContain("Old Garbage Collection"); + output.shouldContain("G1"); // Verify verbose query output.shouldContain("SELECT"); } From 26b5708c47150023798a1546ba095c1b0b7807e1 Mon Sep 17 00:00:00 2001 From: Matthew Donovan Date: Thu, 25 Sep 2025 12:15:09 +0000 Subject: [PATCH 234/556] 8360882: Tests throw SkippedException when they should fail Reviewed-by: mullan --- test/jdk/sun/security/pkcs11/PKCS11Test.java | 7 +++- .../test/lib/artifacts/ArtifactResolver.java | 9 ++--- .../lib/security/OpensslArtifactFetcher.java | 37 ++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/test/jdk/sun/security/pkcs11/PKCS11Test.java b/test/jdk/sun/security/pkcs11/PKCS11Test.java index 56127f86792..8454f3ac463 100644 --- a/test/jdk/sun/security/pkcs11/PKCS11Test.java +++ b/test/jdk/sun/security/pkcs11/PKCS11Test.java @@ -779,7 +779,12 @@ public abstract class PKCS11Test { } private static Path fetchNssLib(Class clazz, Path libraryName) throws IOException { - Path p = ArtifactResolver.fetchOne(clazz); + Path p; + try { + p = ArtifactResolver.fetchOne(clazz); + } catch (IOException exc) { + throw new SkippedException("Could not find NSS", exc); + } return findNSSLibrary(p, libraryName); } diff --git a/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java b/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java index 83e381356a0..fb67ce83bbe 100644 --- a/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java +++ b/test/lib/jdk/test/lib/artifacts/ArtifactResolver.java @@ -23,8 +23,7 @@ package jdk.test.lib.artifacts; -import jtreg.SkippedException; - +import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -90,15 +89,15 @@ public class ArtifactResolver { * @return the local path to the artifact. If the artifact is a compressed * file that gets unpacked, this path will point to the root * directory of the uncompressed file(s). - * @throws SkippedException thrown if the artifact cannot be found + * @throws IOException thrown if the artifact cannot be found */ - public static Path fetchOne(Class klass) { + public static Path fetchOne(Class klass) throws IOException { try { return ArtifactResolver.resolve(klass).entrySet().stream() .findAny().get().getValue(); } catch (ArtifactResolverException e) { Artifact artifact = klass.getAnnotation(Artifact.class); - throw new SkippedException("Cannot find the artifact " + artifact.name(), e); + throw new IOException("Cannot find the artifact " + artifact.name(), e); } } diff --git a/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java b/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java index 82252000154..6fa8cb2e408 100644 --- a/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java +++ b/test/lib/jdk/test/lib/security/OpensslArtifactFetcher.java @@ -23,6 +23,7 @@ package jdk.test.lib.security; +import java.io.IOException; import java.nio.file.Path; import jdk.test.lib.Platform; import jdk.test.lib.process.ProcessTools; @@ -49,42 +50,40 @@ public class OpensslArtifactFetcher { * * @return openssl binary path of the current version * @throws SkippedException if a valid version of OpenSSL cannot be found + * or if OpenSSL is not available on the target platform */ public static String getOpensslPath() { String path = getOpensslFromSystemProp(OPENSSL_BUNDLE_VERSION); if (path != null) { + System.out.println("Using OpenSSL from system property."); return path; } + path = getDefaultSystemOpensslPath(OPENSSL_BUNDLE_VERSION); if (path != null) { + System.out.println("Using OpenSSL from system."); return path; } + if (Platform.isX64()) { if (Platform.isLinux()) { - path = fetchOpenssl(LINUX_X64.class); + return fetchOpenssl(LINUX_X64.class); } else if (Platform.isOSX()) { - path = fetchOpenssl(MACOSX_X64.class); + return fetchOpenssl(MACOSX_X64.class); } else if (Platform.isWindows()) { - path = fetchOpenssl(WINDOWS_X64.class); + return fetchOpenssl(WINDOWS_X64.class); } } else if (Platform.isAArch64()) { if (Platform.isLinux()) { - path = fetchOpenssl(LINUX_AARCH64.class); + return fetchOpenssl(LINUX_AARCH64.class); } if (Platform.isOSX()) { - path = fetchOpenssl(MACOSX_AARCH64.class); + return fetchOpenssl(MACOSX_AARCH64.class); } } - if (!verifyOpensslVersion(path, OPENSSL_BUNDLE_VERSION)) { - String exMsg = "Can't find the version: " - + OpensslArtifactFetcher.getTestOpensslBundleVersion() - + " of openssl binary on this machine, please install" - + " and set openssl path with property 'test.openssl.path'"; - throw new SkippedException(exMsg); - } else { - return path; - } + throw new SkippedException(String.format("No OpenSSL %s found for %s/%s", + OPENSSL_BUNDLE_VERSION, Platform.getOsName(), Platform.getOsArch())); } private static String getOpensslFromSystemProp(String version) { @@ -120,9 +119,13 @@ public class OpensslArtifactFetcher { } private static String fetchOpenssl(Class clazz) { - return ArtifactResolver.fetchOne(clazz) - .resolve("openssl", "bin", "openssl") - .toString(); + try { + return ArtifactResolver.fetchOne(clazz) + .resolve("openssl", "bin", "openssl") + .toString(); + } catch (IOException exc) { + throw new SkippedException("Could not find openssl", exc); + } } // retrieve the provider directory path from /bin/openssl From d1ea6ea22d49884bec53f89fad7029400fb1d7f2 Mon Sep 17 00:00:00 2001 From: Hamlin Li Date: Thu, 25 Sep 2025 12:42:18 +0000 Subject: [PATCH 235/556] 8367103: RISC-V: store cpu features in a bitmap Reviewed-by: fyang, luhenry --- src/hotspot/cpu/riscv/vm_version_riscv.cpp | 27 ++- src/hotspot/cpu/riscv/vm_version_riscv.hpp | 243 +++++++++++++++------ 2 files changed, 202 insertions(+), 68 deletions(-) diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.cpp b/src/hotspot/cpu/riscv/vm_version_riscv.cpp index 1bb9509cdde..87366116b3b 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.cpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.cpp @@ -35,15 +35,28 @@ uint32_t VM_Version::_initial_vector_length = 0; -#define DEF_RV_FEATURE(NAME, PRETTY, BIT, FSTRING, FLAGF) \ -VM_Version::NAME##RVFeatureValue VM_Version::NAME(PRETTY, BIT, FSTRING); -RV_FEATURE_FLAGS(DEF_RV_FEATURE) +#define DEF_RV_EXT_FEATURE(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ +VM_Version::NAME##RVExtFeatureValue VM_Version::NAME; +RV_EXT_FEATURE_FLAGS(DEF_RV_EXT_FEATURE) +#undef DEF_RV_EXT_FEATURE -#define ADD_RV_FEATURE_IN_LIST(NAME, PRETTY, BIT, FSTRING, FLAGF) \ - &VM_Version::NAME, -VM_Version::RVFeatureValue* VM_Version::_feature_list[] = { -RV_FEATURE_FLAGS(ADD_RV_FEATURE_IN_LIST) +#define DEF_RV_NON_EXT_FEATURE(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ +VM_Version::NAME##RVNonExtFeatureValue VM_Version::NAME; +RV_NON_EXT_FEATURE_FLAGS(DEF_RV_NON_EXT_FEATURE) +#undef DEF_RV_NON_EXT_FEATURE + +#define ADD_RV_EXT_FEATURE_IN_LIST(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ + &VM_Version::NAME, +#define ADD_RV_NON_EXT_FEATURE_IN_LIST(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ + &VM_Version::NAME, + VM_Version::RVFeatureValue* VM_Version::_feature_list[] = { + RV_EXT_FEATURE_FLAGS(ADD_RV_EXT_FEATURE_IN_LIST) + RV_NON_EXT_FEATURE_FLAGS(ADD_RV_NON_EXT_FEATURE_IN_LIST) nullptr}; +#undef ADD_RV_NON_EXT_FEATURE_IN_LIST +#undef ADD_RV_EXT_FEATURE_IN_LIST + +VM_Version::RVExtFeatures* VM_Version::_rv_ext_features = new VM_Version::RVExtFeatures(); void VM_Version::useRVA20U64Profile() { RV_USE_RVA20U64; diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.hpp b/src/hotspot/cpu/riscv/vm_version_riscv.hpp index d94385cd8ac..aec975d27be 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.hpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.hpp @@ -46,30 +46,29 @@ class VM_Version : public Abstract_VM_Version { RIVOS = 0x6cf, // JEDEC: 0x4f, Bank: 14 }; + class RVExtFeatures; + class RVFeatureValue { const char* const _pretty; const bool _feature_string; - const uint64_t _feature_bit; - bool _enabled; + const uint64_t _linux_feature_bit; int64_t _value; public: - RVFeatureValue(const char* pretty, int bit_num, bool fstring) : - _pretty(pretty), _feature_string(fstring), _feature_bit(nth_bit(bit_num)), - _enabled(false), _value(-1) { + RVFeatureValue(const char* pretty, int linux_bit_num, bool fstring) : + _pretty(pretty), _feature_string(fstring), _linux_feature_bit(nth_bit(linux_bit_num)), + _value(-1) { } - void enable_feature(int64_t value = 0) { - _enabled = true; + virtual void enable_feature(int64_t value = 0) { _value = value; } - void disable_feature() { - _enabled = false; + virtual void disable_feature() { _value = -1; } const char* pretty() { return _pretty; } - uint64_t feature_bit() { return _feature_bit; } + uint64_t feature_bit() { return _linux_feature_bit; } bool feature_string() { return _feature_string; } - bool enabled() { return _enabled; } int64_t value() { return _value; } + virtual bool enabled() = 0; virtual void update_flag() = 0; }; @@ -111,6 +110,45 @@ class VM_Version : public Abstract_VM_Version { #define NO_UPDATE_DEFAULT \ void update_flag() {} \ + + class RVExtFeatureValue : public RVFeatureValue { + const uint32_t _cpu_feature_index; + public: + RVExtFeatureValue(const char* pretty, int linux_bit_num, uint32_t cpu_feature_index, bool fstring) : + RVFeatureValue(pretty, linux_bit_num, fstring), + _cpu_feature_index(cpu_feature_index) { + } + bool enabled() { + return RVExtFeatures::current()->support_feature(_cpu_feature_index); + } + void enable_feature(int64_t value = 0) { + RVFeatureValue::enable_feature(value); + RVExtFeatures::current()->set_feature(_cpu_feature_index); + } + void disable_feature() { + RVFeatureValue::disable_feature(); + RVExtFeatures::current()->clear_feature(_cpu_feature_index); + } + }; + + class RVNonExtFeatureValue : public RVFeatureValue { + bool _enabled; + public: + RVNonExtFeatureValue(const char* pretty, int linux_bit_num, bool fstring) : + RVFeatureValue(pretty, linux_bit_num, fstring), + _enabled(false) { + } + bool enabled() { return _enabled; } + void enable_feature(int64_t value = 0) { + RVFeatureValue::enable_feature(value); + _enabled = true; + } + void disable_feature() { + RVFeatureValue::disable_feature(); + _enabled = false; + } + }; + // Frozen standard extensions // I RV64I // M Integer Multiplication and Division @@ -161,58 +199,140 @@ class VM_Version : public Abstract_VM_Version { #define RV_NO_FLAG_BIT (BitsPerWord+1) // nth_bit will return 0 on values larger than BitsPerWord // Note: the order matters, depender should be after their dependee. E.g. ext_V before ext_Zvbb. - // declaration name , extension name, bit pos ,in str, mapped flag) - #define RV_FEATURE_FLAGS(decl) \ - decl(ext_I , "i" , ('I' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_M , "m" , ('M' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_A , "a" , ('A' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_F , "f" , ('F' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_D , "d" , ('D' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_C , "c" , ('C' - 'A'), true , UPDATE_DEFAULT(UseRVC)) \ - decl(ext_Q , "q" , ('Q' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_H , "h" , ('H' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_V , "v" , ('V' - 'A'), true , UPDATE_DEFAULT(UseRVV)) \ - decl(ext_Zicbom , "Zicbom" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbom)) \ - decl(ext_Zicboz , "Zicboz" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicboz)) \ - decl(ext_Zicbop , "Zicbop" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbop)) \ - decl(ext_Zba , "Zba" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZba)) \ - decl(ext_Zbb , "Zbb" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbb)) \ - decl(ext_Zbc , "Zbc" , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zbs , "Zbs" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbs)) \ - decl(ext_Zbkb , "Zbkb" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbkb)) \ - decl(ext_Zcb , "Zcb" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZcb)) \ - decl(ext_Zfa , "Zfa" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfa)) \ - decl(ext_Zfh , "Zfh" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfh)) \ - decl(ext_Zfhmin , "Zfhmin" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfhmin)) \ - decl(ext_Zicsr , "Zicsr" , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zicntr , "Zicntr" , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zifencei , "Zifencei" , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zic64b , "Zic64b" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZic64b)) \ - decl(ext_Ztso , "Ztso" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZtso)) \ - decl(ext_Zihintpause , "Zihintpause" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZihintpause)) \ - decl(ext_Zacas , "Zacas" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZacas)) \ - decl(ext_Zvbb , "Zvbb" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbb, ext_V)) \ - decl(ext_Zvbc , "Zvbc" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbc, ext_V)) \ - decl(ext_Zvfh , "Zvfh" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvfh, ext_V)) \ - decl(ext_Zvkn , "Zvkn" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvkn, ext_V)) \ - decl(ext_Zicond , "Zicond" , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicond)) \ - decl(mvendorid , "VendorId" , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ - decl(marchid , "ArchId" , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ - decl(mimpid , "ImpId" , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ - decl(unaligned_access , "Unaligned" , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ - decl(satp_mode , "SATP" , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ - decl(zicboz_block_size, "ZicbozBlockSize", RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + // + // Fields description in `decl`: + // declaration name, extension name, bit value from linux, feature string?, mapped flag) + #define RV_EXT_FEATURE_FLAGS(decl) \ + decl(ext_I , i , ('I' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_M , m , ('M' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_A , a , ('A' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_F , f , ('F' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_D , d , ('D' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_C , c , ('C' - 'A'), true , UPDATE_DEFAULT(UseRVC)) \ + decl(ext_Q , q , ('Q' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_H , h , ('H' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_V , v , ('V' - 'A'), true , UPDATE_DEFAULT(UseRVV)) \ + decl(ext_Zicbom , Zicbom , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbom)) \ + decl(ext_Zicboz , Zicboz , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicboz)) \ + decl(ext_Zicbop , Zicbop , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbop)) \ + decl(ext_Zba , Zba , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZba)) \ + decl(ext_Zbb , Zbb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbb)) \ + decl(ext_Zbc , Zbc , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zbs , Zbs , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbs)) \ + decl(ext_Zbkb , Zbkb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbkb)) \ + decl(ext_Zcb , Zcb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZcb)) \ + decl(ext_Zfa , Zfa , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfa)) \ + decl(ext_Zfh , Zfh , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfh)) \ + decl(ext_Zfhmin , Zfhmin , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfhmin)) \ + decl(ext_Zicsr , Zicsr , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zicntr , Zicntr , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zifencei , Zifencei , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zic64b , Zic64b , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZic64b)) \ + decl(ext_Ztso , Ztso , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZtso)) \ + decl(ext_Zihintpause , Zihintpause , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZihintpause)) \ + decl(ext_Zacas , Zacas , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZacas)) \ + decl(ext_Zvbb , Zvbb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbb, ext_V)) \ + decl(ext_Zvbc , Zvbc , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbc, ext_V)) \ + decl(ext_Zvfh , Zvfh , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvfh, ext_V)) \ + decl(ext_Zvkn , Zvkn , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvkn, ext_V)) \ + decl(ext_Zicond , Zicond , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicond)) \ - #define DECLARE_RV_FEATURE(NAME, PRETTY, BIT, FSTRING, FLAGF) \ - struct NAME##RVFeatureValue : public RVFeatureValue { \ - NAME##RVFeatureValue(const char* pretty, int bit, bool fstring) : \ - RVFeatureValue(pretty, bit, fstring) {} \ - FLAGF; \ - }; \ - static NAME##RVFeatureValue NAME; \ + #define DECLARE_RV_EXT_FEATURE(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ + struct NAME##RVExtFeatureValue : public RVExtFeatureValue { \ + NAME##RVExtFeatureValue() : \ + RVExtFeatureValue(#PRETTY, LINUX_BIT, RVExtFeatures::CPU_##NAME, FSTRING) {} \ + FLAGF; \ + }; \ + static NAME##RVExtFeatureValue NAME; \ - RV_FEATURE_FLAGS(DECLARE_RV_FEATURE) - #undef DECLARE_RV_FEATURE + RV_EXT_FEATURE_FLAGS(DECLARE_RV_EXT_FEATURE) + #undef DECLARE_RV_EXT_FEATURE + + // Non-extension features + // + #define RV_NON_EXT_FEATURE_FLAGS(decl) \ + decl(unaligned_access , Unaligned , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + decl(mvendorid , VendorId , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + decl(marchid , ArchId , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + decl(mimpid , ImpId , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + decl(satp_mode , SATP , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + decl(zicboz_block_size, ZicbozBlockSize , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + + #define DECLARE_RV_NON_EXT_FEATURE(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ + struct NAME##RVNonExtFeatureValue : public RVNonExtFeatureValue { \ + NAME##RVNonExtFeatureValue() : \ + RVNonExtFeatureValue(#PRETTY, LINUX_BIT, FSTRING) {} \ + FLAGF; \ + }; \ + static NAME##RVNonExtFeatureValue NAME; \ + + RV_NON_EXT_FEATURE_FLAGS(DECLARE_RV_NON_EXT_FEATURE) + #undef DECLARE_RV_NON_EXT_FEATURE + +private: + // Utility for AOT CPU feature store/check. + class RVExtFeatures : public CHeapObj { + public: + enum RVFeatureIndex { + #define DECLARE_RV_FEATURE_ENUM(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) CPU_##NAME, + + RV_EXT_FEATURE_FLAGS(DECLARE_RV_FEATURE_ENUM) + MAX_CPU_FEATURE_INDEX + #undef DECLARE_RV_FEATURE_ENUM + }; + private: + uint64_t _features_bitmap[(MAX_CPU_FEATURE_INDEX / BitsPerLong) + 1]; + STATIC_ASSERT(sizeof(_features_bitmap) * BitsPerByte >= MAX_CPU_FEATURE_INDEX); + + // Number of 8-byte elements in _features_bitmap. + constexpr static int element_count() { + return sizeof(_features_bitmap) / sizeof(uint64_t); + } + + static int element_index(RVFeatureIndex feature) { + int idx = feature / BitsPerLong; + assert(idx < element_count(), "Features array index out of bounds"); + return idx; + } + + static uint64_t feature_bit(RVFeatureIndex feature) { + return (1ULL << (feature % BitsPerLong)); + } + + static RVFeatureIndex convert(uint32_t index) { + assert(index < MAX_CPU_FEATURE_INDEX, "must"); + return (RVFeatureIndex)index; + } + + public: + static RVExtFeatures* current() { + return _rv_ext_features; + } + + RVExtFeatures() { + for (int i = 0; i < element_count(); i++) { + _features_bitmap[i] = 0; + } + } + + void set_feature(uint32_t feature) { + RVFeatureIndex f = convert(feature); + int idx = element_index(f); + _features_bitmap[idx] |= feature_bit(f); + } + + void clear_feature(uint32_t feature) { + RVFeatureIndex f = convert(feature); + int idx = element_index(f); + _features_bitmap[idx] &= ~feature_bit(f); + } + + bool support_feature(uint32_t feature) { + RVFeatureIndex f = convert(feature); + int idx = element_index(f); + return (_features_bitmap[idx] & feature_bit(f)) != 0; + } + }; // enable extensions based on profile, current supported profiles: // RVA20U64 @@ -286,6 +406,7 @@ class VM_Version : public Abstract_VM_Version { // Null terminated list static RVFeatureValue* _feature_list[]; + static RVExtFeatures* _rv_ext_features; // Enables features in _feature_list static void setup_cpu_available_features(); From 2b451131a57dc7080c4ccb77d6cb5a96ee24d891 Mon Sep 17 00:00:00 2001 From: Boris Ulasevich Date: Thu, 25 Sep 2025 13:35:36 +0000 Subject: [PATCH 236/556] 8359378: aarch64: crash when using -XX:+UseFPUForSpilling Reviewed-by: aph, rcastanedalo --- .../cpu/aarch64/gc/shared/barrierSetAssembler_aarch64.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hotspot/cpu/aarch64/gc/shared/barrierSetAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/gc/shared/barrierSetAssembler_aarch64.cpp index a2b43d80746..021af3e5698 100644 --- a/src/hotspot/cpu/aarch64/gc/shared/barrierSetAssembler_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/gc/shared/barrierSetAssembler_aarch64.cpp @@ -398,14 +398,18 @@ void BarrierSetAssembler::check_oop(MacroAssembler* masm, Register obj, Register OptoReg::Name BarrierSetAssembler::encode_float_vector_register_size(const Node* node, OptoReg::Name opto_reg) { switch (node->ideal_reg()) { case Op_RegF: + case Op_RegI: // RA may place scalar values (Op_RegI/N/L/P) in FP registers when UseFPUForSpilling is enabled + case Op_RegN: // No need to refine. The original encoding is already fine to distinguish. - assert(opto_reg % 4 == 0, "Float register should only occupy a single slot"); + assert(opto_reg % 4 == 0, "32-bit register should only occupy a single slot"); break; // Use different encoding values of the same fp/vector register to help distinguish different sizes. // Such as V16. The OptoReg::name and its corresponding slot value are // "V16": 64, "V16_H": 65, "V16_J": 66, "V16_K": 67. case Op_RegD: case Op_VecD: + case Op_RegL: + case Op_RegP: opto_reg &= ~3; opto_reg |= 1; break; From 043aeaf02a50a7413e1956a99341d04ea3f9ac92 Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Thu, 25 Sep 2025 14:30:47 +0000 Subject: [PATCH 237/556] 8368552: H3ErrorHandlingTest.testCloseControlStream intermittent timed out Reviewed-by: dfuchs --- .../httpclient/http3/H3ErrorHandlingTest.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java b/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java index 10a3a3af8f2..4315fd36de5 100644 --- a/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java +++ b/test/jdk/java/net/httpclient/http3/H3ErrorHandlingTest.java @@ -34,6 +34,7 @@ import jdk.internal.net.quic.QuicTransportException; import jdk.internal.net.quic.QuicVersion; import jdk.test.lib.net.SimpleSSLContext; import jdk.test.lib.net.URIBuilder; +import jdk.test.lib.Utils; import org.testng.IRetryAnalyzer; import org.testng.ITestResult; import org.testng.annotations.AfterClass; @@ -802,7 +803,7 @@ public class H3ErrorHandlingTest implements HttpServerAdapters { final HttpResponse response = client.sendAsync( request, BodyHandlers.discarding()) - .get(10, TimeUnit.SECONDS); + .get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (Exception e) { final String expectedMsg = "stateless reset from peer"; @@ -943,7 +944,7 @@ public class H3ErrorHandlingTest implements HttpServerAdapters { final HttpResponse response = client.sendAsync( request, BodyHandlers.discarding()) - .get(10, TimeUnit.SECONDS); + .get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); @@ -966,13 +967,13 @@ public class H3ErrorHandlingTest implements HttpServerAdapters { final HttpResponse response = client.sendAsync( request, BodyHandlers.discarding()) - .get(10, TimeUnit.SECONDS); + .get(Utils.adjustTimeout(20), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); var cause = e.getCause(); assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); - TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + TerminationCause terminationCause = errorCF.get(Utils.adjustTimeout(10), TimeUnit.SECONDS); System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); final long actual = terminationCause.getCloseCode(); // expected @@ -989,13 +990,13 @@ public class H3ErrorHandlingTest implements HttpServerAdapters { final HttpResponse response = client.sendAsync( request, BodyHandlers.discarding()) - .get(10, TimeUnit.SECONDS); + .get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); var cause = e.getCause(); assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); - TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + TerminationCause terminationCause = errorCF.get(Utils.adjustTimeout(10), TimeUnit.SECONDS); System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); final long actual = terminationCause.getCloseCode(); // expected @@ -1019,13 +1020,13 @@ public class H3ErrorHandlingTest implements HttpServerAdapters { BodyHandlers.discarding(), (initiatingRequest, pushPromiseRequest, acceptor) -> acceptor.apply(BodyHandlers.discarding()) - ).get(10, TimeUnit.SECONDS); + ).get(Utils.adjustTimeout(10), TimeUnit.SECONDS); fail("Expected the request to fail, got " + response); } catch (ExecutionException e) { System.out.println("Client exception [expected]: " + e); var cause = e.getCause(); assertTrue(cause instanceof ProtocolException, "Expected ProtocolException"); - TerminationCause terminationCause = errorCF.get(10, TimeUnit.SECONDS); + TerminationCause terminationCause = errorCF.get(Utils.adjustTimeout(10), TimeUnit.SECONDS); System.out.println("Server reason: \"" + terminationCause.getPeerVisibleReason()+'"'); final long actual = terminationCause.getCloseCode(); // expected From 569e78080b3c25c95d85e9e194626f95f86b9b10 Mon Sep 17 00:00:00 2001 From: Artur Barashev Date: Thu, 25 Sep 2025 14:44:06 +0000 Subject: [PATCH 238/556] 8365820: Apply certificate scope constraints to algorithms in "signature_algorithms" extension when "signature_algorithms_cert" extension is not being sent Reviewed-by: hchao --- .../security/ssl/CertSignAlgsExtension.java | 5 + .../ssl/SignatureAlgorithmsExtension.java | 78 +++-- .../DisableCertSignAlgsExtForClientTLS12.java | 87 ++++++ .../DisableCertSignAlgsExtForClientTLS13.java | 89 ++++++ .../DisableCertSignAlgsExtForServerTLS13.java | 281 ++++++++++++++++++ .../DisableSignatureSchemePerScopeTLS12.java | 17 +- .../DisableSignatureSchemePerScopeTLS13.java | 32 +- 7 files changed, 536 insertions(+), 53 deletions(-) create mode 100644 test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS12.java create mode 100644 test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS13.java create mode 100644 test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForServerTLS13.java diff --git a/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java b/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java index b975290d09d..2125a148162 100644 --- a/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/CertSignAlgsExtension.java @@ -36,6 +36,11 @@ import sun.security.ssl.SignatureAlgorithmsExtension.SignatureSchemesSpec; /** * Pack of the "signature_algorithms_cert" extensions. + *

        + * Note: Per RFC 8446, if no "signature_algorithms_cert" extension is + * present, then the "signature_algorithms" extension also applies to + * signatures appearing in certificates. + * See {@code SignatureAlgorithmsExtension} for details. */ final class CertSignAlgsExtension { static final HandshakeProducer chNetworkProducer = diff --git a/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java b/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java index a95b31583bb..b298da05e9a 100644 --- a/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SignatureAlgorithmsExtension.java @@ -31,6 +31,7 @@ import static sun.security.ssl.SignatureScheme.HANDSHAKE_SCOPE; import java.io.IOException; import java.nio.ByteBuffer; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -191,21 +192,9 @@ final class SignatureAlgorithmsExtension { // Produce the extension. SignatureScheme.updateHandshakeLocalSupportedAlgs(chc); - int vectorLen = SignatureScheme.sizeInRecord() * - chc.localSupportedSignAlgs.size(); - byte[] extData = new byte[vectorLen + 2]; - ByteBuffer m = ByteBuffer.wrap(extData); - Record.putInt16(m, vectorLen); - for (SignatureScheme ss : chc.localSupportedSignAlgs) { - Record.putInt16(m, ss.id); - } - - // Update the context. - chc.handshakeExtensions.put( + return produceNetworkLoad(chc, SSLExtension.CH_SIGNATURE_ALGORITHMS, - new SignatureSchemesSpec(chc.localSupportedSignAlgs)); - - return extData; + SSLExtension.CH_SIGNATURE_ALGORITHMS_CERT); } } @@ -391,23 +380,11 @@ final class SignatureAlgorithmsExtension { } // Produce the extension. - // localSupportedSignAlgs has been already updated when we - // set the negotiated protocol. - int vectorLen = SignatureScheme.sizeInRecord() - * shc.localSupportedSignAlgs.size(); - byte[] extData = new byte[vectorLen + 2]; - ByteBuffer m = ByteBuffer.wrap(extData); - Record.putInt16(m, vectorLen); - for (SignatureScheme ss : shc.localSupportedSignAlgs) { - Record.putInt16(m, ss.id); - } - - // Update the context. - shc.handshakeExtensions.put( + // localSupportedSignAlgs and localSupportedCertSignAlgs have been + // already updated when we set the negotiated protocol. + return produceNetworkLoad(shc, SSLExtension.CR_SIGNATURE_ALGORITHMS, - new SignatureSchemesSpec(shc.localSupportedSignAlgs)); - - return extData; + SSLExtension.CR_SIGNATURE_ALGORITHMS_CERT); } } @@ -546,4 +523,45 @@ final class SignatureAlgorithmsExtension { hc.handshakeSession.setPeerSupportedSignatureAlgorithms(certSS); } } + + /** + * Produce network load and update context. + * + * @param hc HandshakeContext + * @param signatureAlgorithmsExt "signature_algorithms" extension + * @param signatureAlgorithmsCertExt "signature_algorithms_cert" + * extension + * @return network load as byte array + */ + private static byte[] produceNetworkLoad( + HandshakeContext hc, SSLExtension signatureAlgorithmsExt, + SSLExtension signatureAlgorithmsCertExt) throws IOException { + + List sigAlgs; + + // If we don't produce "signature_algorithms_cert" extension, then + // the "signature_algorithms" extension should contain signatures + // supported for both: handshake signatures and certificate signatures. + if (hc.sslConfig.isAvailable(signatureAlgorithmsCertExt)) { + sigAlgs = hc.localSupportedSignAlgs; + } else { + sigAlgs = new ArrayList<>(hc.localSupportedSignAlgs); + sigAlgs.retainAll(hc.localSupportedCertSignAlgs); + } + + int vectorLen = SignatureScheme.sizeInRecord() * sigAlgs.size(); + byte[] extData = new byte[vectorLen + 2]; + ByteBuffer m = ByteBuffer.wrap(extData); + Record.putInt16(m, vectorLen); + + for (SignatureScheme ss : sigAlgs) { + Record.putInt16(m, ss.id); + } + + // Update the context. + hc.handshakeExtensions.put( + signatureAlgorithmsExt, new SignatureSchemesSpec(sigAlgs)); + + return extData; + } } diff --git a/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS12.java b/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS12.java new file mode 100644 index 00000000000..7d16e07e7b1 --- /dev/null +++ b/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS12.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8365820 + * @summary Apply certificate scope constraints to algorithms in + * "signature_algorithms" extension when + * "signature_algorithms_cert" extension is not being sent. + * This test covers the client side for TLSv1.2. + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm DisableCertSignAlgsExtForClientTLS12 + */ + +import static jdk.test.lib.Asserts.assertEquals; +import static jdk.test.lib.Asserts.assertFalse; + +import java.security.Security; +import java.util.List; + +// Test disabled signature_algorithms_cert extension on the client side +// for TLSv1.2. +public class DisableCertSignAlgsExtForClientTLS12 extends + DisableSignatureSchemePerScopeTLS12 { + + protected DisableCertSignAlgsExtForClientTLS12() + throws Exception { + super(); + } + + public static void main(String[] args) throws Exception { + Security.setProperty( + "jdk.tls.disabledAlgorithms", DISABLED_CONSTRAINTS); + // Disable signature_algorithms_cert extension for the client. + System.setProperty("jdk.tls.client.disableExtensions", + "signature_algorithms_cert"); + new DisableCertSignAlgsExtForClientTLS12().run(); + } + + @Override + protected void checkClientHello() throws Exception { + // --- Check signature_algorithms extension --- + + // Get signature_algorithms extension signature schemes. + List sigAlgsSS = getSigSchemesCliHello( + extractHandshakeMsg(cTOs, TLS_HS_CLI_HELLO), + SIG_ALGS_EXT); + + // signature_algorithms extension MUST NOT contain disabled + // handshake signature scheme. + assertFalse(sigAlgsSS.contains(HANDSHAKE_DISABLED_SIG), + "Signature Scheme " + HANDSHAKE_DISABLED_SIG + + " present in ClientHello's signature_algorithms extension"); + + // signature_algorithms extension MUST NOT contain disabled + // certificate signature scheme. + assertFalse(sigAlgsSS.contains(CERTIFICATE_DISABLED_SIG), + "Signature Scheme " + CERTIFICATE_DISABLED_SIG + + " present in ClientHello's signature_algorithms extension"); + + // signature_algorithms_cert extension MUST NOT be present. + assertEquals(getSigSchemesCliHello(extractHandshakeMsg( + cTOs, TLS_HS_CLI_HELLO), SIG_ALGS_CERT_EXT).size(), 0, + "signature_algorithms_cert extension present in ClientHello"); + } +} diff --git a/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS13.java b/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS13.java new file mode 100644 index 00000000000..5f594cd2cdf --- /dev/null +++ b/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForClientTLS13.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8365820 + * @summary Apply certificate scope constraints to algorithms in + * "signature_algorithms" extension when + * "signature_algorithms_cert" extension is not being sent. + * This test covers the client side for TLSv1.3. + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm DisableCertSignAlgsExtForClientTLS13 + */ + +import static jdk.test.lib.Asserts.assertFalse; + +import java.security.Security; +import java.util.List; + +// Test disabled signature_algorithms_cert extension on the client side +// for TLSv1.3. +public class DisableCertSignAlgsExtForClientTLS13 extends + DisableCertSignAlgsExtForClientTLS12 { + + protected DisableCertSignAlgsExtForClientTLS13() + throws Exception { + super(); + } + + public static void main(String[] args) throws Exception { + Security.setProperty( + "jdk.tls.disabledAlgorithms", DISABLED_CONSTRAINTS); + // Disable signature_algorithms_cert extension for the client. + System.setProperty("jdk.tls.client.disableExtensions", + "signature_algorithms_cert"); + new DisableCertSignAlgsExtForClientTLS13().run(); + } + + @Override + protected String getProtocol() { + return "TLSv1.3"; + } + + @Override + protected void checkClientHello() throws Exception { + super.checkClientHello(); + + // Get signature_algorithms extension signature schemes. + List sigAlgsSS = getSigSchemesCliHello( + extractHandshakeMsg(cTOs, TLS_HS_CLI_HELLO), + SIG_ALGS_EXT); + + // These signature schemes MOST NOT be present in signature_algorithms + // extension. + TLS13_CERT_ONLY.forEach(ss -> + assertFalse(sigAlgsSS.contains(ss), "Signature Scheme " + ss + + " present in ClientHello's" + + " signature_algorithms extension")); + } + + // TLSv1.3 sends CertificateRequest signature schemes in + // signature_algorithms and signature_algorithms_cert extensions. Same as + // ClientHello, but they are encrypted. So we skip CertificateRequest + // signature schemes verification for TLSv1.3. + @Override + protected void checkCertificateRequest() { + } +} diff --git a/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForServerTLS13.java b/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForServerTLS13.java new file mode 100644 index 00000000000..4c93e4fc240 --- /dev/null +++ b/test/jdk/sun/security/ssl/SignatureScheme/DisableCertSignAlgsExtForServerTLS13.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import static jdk.test.lib.Asserts.assertTrue; +import static jdk.test.lib.Utils.runAndCheckException; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.SocketException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.TrustManagerFactory; +import jdk.test.lib.security.CertificateBuilder; +import sun.security.x509.AuthorityKeyIdentifierExtension; +import sun.security.x509.GeneralName; +import sun.security.x509.GeneralNames; +import sun.security.x509.KeyIdentifier; +import sun.security.x509.SerialNumber; +import sun.security.x509.X500Name; + +/* + * @test + * @bug 8365820 + * @summary Apply certificate scope constraints to algorithms in + * "signature_algorithms" extension when + * "signature_algorithms_cert" extension is not being sent. + * This test covers the server side for TLSv1.3. + * @modules java.base/sun.security.x509 + * java.base/sun.security.util + * @library /javax/net/ssl/templates + * /test/lib + * @run main/othervm DisableCertSignAlgsExtForServerTLS13 true + * @run main/othervm DisableCertSignAlgsExtForServerTLS13 false + */ + +/* + * Test disabled signature_algorithms_cert extension on the server side. + * + * CertificateRequest's extensions are encrypted in TLSv1.3. So we can't verify + * the content of the CertificateRequest's signature_algorithms extension + * directly like we do it for extensions in ClientHello message. + * Instead, we run a TLS handshake and check that certificate scope + * constraints are being applied to algorithms in "signature_algorithms" + * extension when "signature_algorithms_cert" extension is not being sent. + * + * Note that for TLSv1.2 disabling "signature_algorithms_cert" extension + * doesn't change anything for the signatures schemes list contained in + * CertificateRequest message. The TLSv1.2 CertificateRequest message + * doesn't contain extensions and includes the signatures schemes list + * directly (which is also an intersection of signature schemes allowed + * for handshake signatures and for the certificate signatures). + * This functionality is being tested by "DisableSignatureSchemePerScopeTLS12". + */ + +public class DisableCertSignAlgsExtForServerTLS13 extends SSLSocketTemplate { + + private static final String KEY_ALGORITHM = "RSA"; + private static final String SERVER_CERT_SIG_ALG = "RSASSA-PSS"; + // SHA256withRSA signature algorithm is not allowed for handshake + // signatures in TLSv1.3, but it's allowed for certificate + // signatures. This is regardless of jdk.tls.disabledAlgorithms + // configuration. We use this difference to construct our test. + private static final String CLIENT_CERT_SIG_ALG = "SHA256withRSA"; + private static final String TRUSTED_CERT_SIG_ALG = "RSASSA-PSS"; + + private final String protocol; + private X509Certificate trustedCert; + private X509Certificate serverCert; + private X509Certificate clientCert; + private KeyPair serverKeys; + private KeyPair clientKeys; + + protected DisableCertSignAlgsExtForServerTLS13( + String protocol) throws Exception { + super(); + this.protocol = protocol; + setupCertificates(); + } + + public static void main(String[] args) throws Exception { + if (args.length != 1) { + throw new RuntimeException("Wrong number of arguments"); + } + + boolean disabled = Boolean.parseBoolean(args[0]); + + // Disable signature_algorithms_cert extension on the server side. + if (disabled) { + System.setProperty("jdk.tls.server.disableExtensions", + "signature_algorithms_cert"); + } + + // Should always run fine on TLSv1.2 because SHA256withRSA signature + // algorithm is allowed for both handshake and certificates signatures + // in TLSv1.2. + new DisableCertSignAlgsExtForServerTLS13("TLSv1.2").run(); + + var tls13Test = new DisableCertSignAlgsExtForServerTLS13("TLSv1.3"); + + if (disabled) { + // Fails with "signature_algorithms_cert" extension disabled + // because in such case we use an intersection of signature + // schemes allowed for handshake signatures and for the certificate + // signatures for "signature_algorithms" extension. + runAndCheckException( + tls13Test::run, + localEx -> { + Throwable remoteEx = localEx.getSuppressed()[0]; + + for (Throwable ex : + new Throwable[]{localEx, remoteEx}) { + assertTrue((ex instanceof SSLHandshakeException + && ex.getMessage() + .contains("(certificate_required)") + // Sometimes we can get SocketException + // instead, depends on network setup. + || ex instanceof SocketException)); + } + }); + } else { + // Runs fine with "signature_algorithms_cert" extension present. + tls13Test.run(); + } + } + + @Override + protected void configureServerSocket(SSLServerSocket sslServerSocket) { + // Require a conforming certificate for the client. + SSLParameters sslParameters = sslServerSocket.getSSLParameters(); + sslParameters.setNeedClientAuth(true); + sslServerSocket.setSSLParameters(sslParameters); + } + + @Override + public SSLContext createServerSSLContext() throws Exception { + return getSSLContext( + trustedCert, serverCert, serverKeys.getPrivate(), protocol); + } + + @Override + public SSLContext createClientSSLContext() throws Exception { + return getSSLContext( + trustedCert, clientCert, clientKeys.getPrivate(), protocol); + } + + private static SSLContext getSSLContext( + X509Certificate trustedCertificate, X509Certificate keyCertificate, + PrivateKey privateKey, String protocol) + throws Exception { + + // create a key store + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + + // import the trusted cert + ks.setCertificateEntry("TLS Signer", trustedCertificate); + + // generate certificate chain + Certificate[] chain = new Certificate[2]; + chain[0] = keyCertificate; + chain[1] = trustedCertificate; + + // import the key entry. + final char[] passphrase = "passphrase".toCharArray(); + ks.setKeyEntry("Whatever", privateKey, passphrase, chain); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(ks); + + // create SSL context + SSLContext ctx = SSLContext.getInstance(protocol); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + + return ctx; + } + + // Certificate-building helper methods. + + private void setupCertificates() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance( + KEY_ALGORITHM); + KeyPair caKeys = kpg.generateKeyPair(); + this.serverKeys = kpg.generateKeyPair(); + this.clientKeys = kpg.generateKeyPair(); + + this.trustedCert = createTrustedCert(caKeys); + + this.serverCert = customCertificateBuilder( + "O=Some-Org, L=Some-City, ST=Some-State, C=US", + serverKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), SERVER_CERT_SIG_ALG); + + this.clientCert = customCertificateBuilder( + "CN=localhost, OU=SSL-Client, O=Some-Org, L=Some-City, ST=Some-State, C=US", + clientKeys.getPublic(), caKeys.getPublic()) + .addBasicConstraintsExt(false, false, -1) + .build(trustedCert, caKeys.getPrivate(), CLIENT_CERT_SIG_ALG); + } + + private static X509Certificate createTrustedCert(KeyPair caKeys) + throws Exception { + SecureRandom random = new SecureRandom(); + + KeyIdentifier kid = new KeyIdentifier(caKeys.getPublic()); + GeneralNames gns = new GeneralNames(); + GeneralName name = new GeneralName(new X500Name( + "O=Some-Org, L=Some-City, ST=Some-State, C=US")); + gns.add(name); + BigInteger serialNumber = BigInteger.valueOf( + random.nextLong(1000000) + 1); + return customCertificateBuilder( + "O=Some-Org, L=Some-City, ST=Some-State, C=US", + caKeys.getPublic(), caKeys.getPublic()) + .setSerialNumber(serialNumber) + .addExtension(new AuthorityKeyIdentifierExtension(kid, gns, + new SerialNumber(serialNumber))) + .addBasicConstraintsExt(true, true, -1) + .build(null, caKeys.getPrivate(), TRUSTED_CERT_SIG_ALG); + } + + private static CertificateBuilder customCertificateBuilder( + String subjectName, PublicKey publicKey, PublicKey caKey) + throws CertificateException, IOException { + SecureRandom random = new SecureRandom(); + + CertificateBuilder builder = new CertificateBuilder() + .setSubjectName(subjectName) + .setPublicKey(publicKey) + .setNotBefore( + Date.from(Instant.now().minus(1, ChronoUnit.HOURS))) + .setNotAfter(Date.from(Instant.now().plus(1, ChronoUnit.HOURS))) + .setSerialNumber( + BigInteger.valueOf(random.nextLong(1000000) + 1)) + .addSubjectKeyIdExt(publicKey) + .addAuthorityKeyIdExt(caKey); + builder.addKeyUsageExt( + new boolean[]{true, true, true, true, true, true}); + + return builder; + } +} diff --git a/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS12.java b/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS12.java index e2ee9f0889a..29a50dccefe 100644 --- a/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS12.java +++ b/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS12.java @@ -23,7 +23,7 @@ /* * @test - * @bug 8349583 + * @bug 8349583 8365820 * @summary Add mechanism to disable signature schemes based on their TLS scope. * This test only covers TLS 1.2. * @library /javax/net/ssl/templates @@ -53,6 +53,17 @@ public class DisableSignatureSchemePerScopeTLS12 extends HANDSHAKE_DISABLED_SIG + " usage HandShakesignature, " + CERTIFICATE_DISABLED_SIG + " usage certificateSignature"; + // Signature schemes not supported in TLSv1.3 for the handshake + // regardless of jdk.tls.disabledAlgorithms configuration. + // In TLSv1.2 these are supported for both: handshake and certificate. + protected static final List TLS13_CERT_ONLY = List.of( + "ecdsa_sha1", + "rsa_pkcs1_sha1", + "rsa_pkcs1_sha256", + "rsa_pkcs1_sha384", + "rsa_pkcs1_sha512" + ); + protected DisableSignatureSchemePerScopeTLS12() throws Exception { super(); } @@ -120,13 +131,13 @@ public class DisableSignatureSchemePerScopeTLS12 extends assertTrue(sigAlgsCertSS.contains(HANDSHAKE_DISABLED_SIG), "Signature Scheme " + HANDSHAKE_DISABLED_SIG + " isn't present in ClientHello's" - + " signature_algorithms extension"); + + " signature_algorithms_cert extension"); // signature_algorithms_cert extension MUST NOT contain disabled // certificate signature scheme. assertFalse(sigAlgsCertSS.contains(CERTIFICATE_DISABLED_SIG), "Signature Scheme " + CERTIFICATE_DISABLED_SIG - + " present in ClientHello's signature_algorithms extension"); + + " present in ClientHello's signature_algorithms_cert extension"); } protected void checkCertificateRequest() throws Exception { diff --git a/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS13.java b/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS13.java index 5d5d445cb45..6f5d88052ff 100644 --- a/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS13.java +++ b/test/jdk/sun/security/ssl/SignatureScheme/DisableSignatureSchemePerScopeTLS13.java @@ -23,7 +23,7 @@ /* * @test - * @bug 8349583 + * @bug 8349583 8365820 * @summary Add mechanism to disable signature schemes based on their TLS scope. * This test only covers TLS 1.3. * @library /javax/net/ssl/templates @@ -41,15 +41,6 @@ import java.util.List; public class DisableSignatureSchemePerScopeTLS13 extends DisableSignatureSchemePerScopeTLS12 { - // Signature schemes not supported in TLSv1.3 only for the handshake. - // This is regardless of jdk.tls.disabledAlgorithms configuration. - List NOT_SUPPORTED_FOR_HANDSHAKE = List.of( - "rsa_pkcs1_sha1", - "rsa_pkcs1_sha256", - "rsa_pkcs1_sha384", - "rsa_pkcs1_sha512" - ); - protected DisableSignatureSchemePerScopeTLS13() throws Exception { super(); } @@ -74,23 +65,24 @@ public class DisableSignatureSchemePerScopeTLS13 extractHandshakeMsg(cTOs, TLS_HS_CLI_HELLO), SIG_ALGS_EXT); - // Should not be present in signature_algorithms extension. - NOT_SUPPORTED_FOR_HANDSHAKE.forEach(ss -> - assertFalse(sigAlgsSS.contains(ss), - "Signature Scheme " + ss - + " present in ClientHello's signature_algorithms extension")); + // These signature schemes MOST NOT be present in signature_algorithms + // extension. + TLS13_CERT_ONLY.forEach(ss -> + assertFalse(sigAlgsSS.contains(ss), "Signature Scheme " + ss + + " present in ClientHello's" + + " signature_algorithms extension")); // Get signature_algorithms_cert extension signature schemes. List sigAlgsCertSS = getSigSchemesCliHello( extractHandshakeMsg(cTOs, TLS_HS_CLI_HELLO), SIG_ALGS_CERT_EXT); - // Should be present in signature_algorithms_cert extension. - NOT_SUPPORTED_FOR_HANDSHAKE.forEach(ss -> - assertTrue(sigAlgsCertSS.contains(ss), - "Signature Scheme " + ss + // These signature schemes MUST be present in + // signature_algorithms_cert extension. + TLS13_CERT_ONLY.forEach(ss -> + assertTrue(sigAlgsCertSS.contains(ss), "Signature Scheme " + ss + " isn't present in ClientHello's" - + " signature_algorithms extension")); + + " signature_algorithms_cert extension")); } // TLSv1.3 sends CertificateRequest signature schemes in From 8ca1feaf7e29c1370853b9b95c2ee7a62c6b84b7 Mon Sep 17 00:00:00 2001 From: William Kemper Date: Thu, 25 Sep 2025 15:37:02 +0000 Subject: [PATCH 239/556] 8368499: GenShen: Do not collect age census during evac when adaptive tenuring is disabled Reviewed-by: kdnilsen, ysr --- .../gc/shenandoah/shenandoahAgeCensus.cpp | 100 +++++++++--------- .../gc/shenandoah/shenandoahAgeCensus.hpp | 12 +-- .../gc/shenandoah/shenandoahControlThread.cpp | 22 +--- .../gc/shenandoah/shenandoahEvacTracker.cpp | 75 ++----------- .../gc/shenandoah/shenandoahEvacTracker.hpp | 9 -- .../shenandoahGenerationalControlThread.cpp | 49 +++++---- .../shenandoahGenerationalControlThread.hpp | 6 +- .../shenandoah/shenandoahGenerationalHeap.cpp | 5 - .../share/gc/shenandoah/shenandoahHeap.cpp | 21 ++++ .../share/gc/shenandoah/shenandoahHeap.hpp | 5 +- .../shenandoah/shenandoahThreadLocalData.hpp | 4 - 11 files changed, 113 insertions(+), 195 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.cpp b/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.cpp index c3b99af8a80..86ff6f22c72 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.cpp @@ -43,7 +43,7 @@ ShenandoahAgeCensus::ShenandoahAgeCensus(uint max_workers) ShenandoahGenerationalMinTenuringAge, ShenandoahGenerationalMaxTenuringAge)); } - _global_age_table = NEW_C_HEAP_ARRAY(AgeTable*, MAX_SNAPSHOTS, mtGC); + _global_age_tables = NEW_C_HEAP_ARRAY(AgeTable*, MAX_SNAPSHOTS, mtGC); CENSUS_NOISE(_global_noise = NEW_C_HEAP_ARRAY(ShenandoahNoiseStats, MAX_SNAPSHOTS, mtGC);) _tenuring_threshold = NEW_C_HEAP_ARRAY(uint, MAX_SNAPSHOTS, mtGC); CENSUS_NOISE(_skipped = 0); @@ -52,36 +52,40 @@ ShenandoahAgeCensus::ShenandoahAgeCensus(uint max_workers) for (int i = 0; i < MAX_SNAPSHOTS; i++) { // Note that we don't now get perfdata from age_table - _global_age_table[i] = new AgeTable(false); + _global_age_tables[i] = new AgeTable(false); CENSUS_NOISE(_global_noise[i].clear();) // Sentinel value _tenuring_threshold[i] = MAX_COHORTS; } if (ShenandoahGenerationalAdaptiveTenuring) { - _local_age_table = NEW_C_HEAP_ARRAY(AgeTable*, _max_workers, mtGC); + _local_age_tables = NEW_C_HEAP_ARRAY(AgeTable*, _max_workers, mtGC); CENSUS_NOISE(_local_noise = NEW_C_HEAP_ARRAY(ShenandoahNoiseStats, max_workers, mtGC);) for (uint i = 0; i < _max_workers; i++) { - _local_age_table[i] = new AgeTable(false); + _local_age_tables[i] = new AgeTable(false); CENSUS_NOISE(_local_noise[i].clear();) } } else { - _local_age_table = nullptr; + _local_age_tables = nullptr; + } + _epoch = MAX_SNAPSHOTS - 1; // see prepare_for_census_update() + + if (!ShenandoahGenerationalAdaptiveTenuring) { + _tenuring_threshold[_epoch] = InitialTenuringThreshold; } - _epoch = MAX_SNAPSHOTS - 1; // see update_epoch() } ShenandoahAgeCensus::~ShenandoahAgeCensus() { for (uint i = 0; i < MAX_SNAPSHOTS; i++) { - delete _global_age_table[i]; + delete _global_age_tables[i]; } - FREE_C_HEAP_ARRAY(AgeTable*, _global_age_table); + FREE_C_HEAP_ARRAY(AgeTable*, _global_age_tables); FREE_C_HEAP_ARRAY(uint, _tenuring_threshold); CENSUS_NOISE(FREE_C_HEAP_ARRAY(ShenandoahNoiseStats, _global_noise)); - if (_local_age_table) { + if (_local_age_tables) { for (uint i = 0; i < _max_workers; i++) { - delete _local_age_table[i]; + delete _local_age_tables[i]; } - FREE_C_HEAP_ARRAY(AgeTable*, _local_age_table); + FREE_C_HEAP_ARRAY(AgeTable*, _local_age_tables); CENSUS_NOISE(FREE_C_HEAP_ARRAY(ShenandoahNoiseStats, _local_noise)); } } @@ -142,37 +146,31 @@ void ShenandoahAgeCensus::prepare_for_census_update() { if (++_epoch >= MAX_SNAPSHOTS) { _epoch=0; } - _global_age_table[_epoch]->clear(); + _global_age_tables[_epoch]->clear(); CENSUS_NOISE(_global_noise[_epoch].clear();) } // Update the census data from appropriate sources, // and compute the new tenuring threshold. -void ShenandoahAgeCensus::update_census(size_t age0_pop, AgeTable* pv1, AgeTable* pv2) { +void ShenandoahAgeCensus::update_census(size_t age0_pop) { prepare_for_census_update(); - assert(_global_age_table[_epoch]->is_clear(), "Dirty decks"); + assert(ShenandoahGenerationalAdaptiveTenuring, "Only update census when adaptive tenuring is enabled"); + assert(_global_age_tables[_epoch]->is_clear(), "Dirty decks"); CENSUS_NOISE(assert(_global_noise[_epoch].is_clear(), "Dirty decks");) - if (ShenandoahGenerationalAdaptiveTenuring) { - assert(pv1 == nullptr && pv2 == nullptr, "Error, check caller"); - // Seed cohort 0 with population that may have been missed during - // regular census. - _global_age_table[_epoch]->add(0u, age0_pop); - // Merge data from local age tables into the global age table for the epoch, - // clearing the local tables. - for (uint i = 0; i < _max_workers; i++) { - // age stats - _global_age_table[_epoch]->merge(_local_age_table[i]); - _local_age_table[i]->clear(); // clear for next census - // Merge noise stats - CENSUS_NOISE(_global_noise[_epoch].merge(_local_noise[i]);) - CENSUS_NOISE(_local_noise[i].clear();) - } - } else { - // census during evac - assert(pv1 != nullptr && pv2 != nullptr, "Error, check caller"); - _global_age_table[_epoch]->merge(pv1); - _global_age_table[_epoch]->merge(pv2); + // Seed cohort 0 with population that may have been missed during + // regular census. + _global_age_tables[_epoch]->add(0u, age0_pop); + + // Merge data from local age tables into the global age table for the epoch, + // clearing the local tables. + for (uint i = 0; i < _max_workers; i++) { + // age stats + _global_age_tables[_epoch]->merge(_local_age_tables[i]); + _local_age_tables[i]->clear(); // clear for next census + // Merge noise stats + CENSUS_NOISE(_global_noise[_epoch].merge(_local_noise[i]);) + CENSUS_NOISE(_local_noise[i].clear();) } update_tenuring_threshold(); @@ -188,7 +186,7 @@ void ShenandoahAgeCensus::update_census(size_t age0_pop, AgeTable* pv1, AgeTable void ShenandoahAgeCensus::reset_global() { assert(_epoch < MAX_SNAPSHOTS, "Out of bounds"); for (uint i = 0; i < MAX_SNAPSHOTS; i++) { - _global_age_table[i]->clear(); + _global_age_tables[i]->clear(); CENSUS_NOISE(_global_noise[i].clear();) } _epoch = MAX_SNAPSHOTS; @@ -198,11 +196,11 @@ void ShenandoahAgeCensus::reset_global() { // Reset the local age tables, clearing any partial census. void ShenandoahAgeCensus::reset_local() { if (!ShenandoahGenerationalAdaptiveTenuring) { - assert(_local_age_table == nullptr, "Error"); + assert(_local_age_tables == nullptr, "Error"); return; } for (uint i = 0; i < _max_workers; i++) { - _local_age_table[i]->clear(); + _local_age_tables[i]->clear(); CENSUS_NOISE(_local_noise[i].clear();) } } @@ -212,7 +210,7 @@ void ShenandoahAgeCensus::reset_local() { bool ShenandoahAgeCensus::is_clear_global() { assert(_epoch < MAX_SNAPSHOTS, "Out of bounds"); for (uint i = 0; i < MAX_SNAPSHOTS; i++) { - bool clear = _global_age_table[i]->is_clear(); + bool clear = _global_age_tables[i]->is_clear(); CENSUS_NOISE(clear |= _global_noise[i].is_clear();) if (!clear) { return false; @@ -224,11 +222,11 @@ bool ShenandoahAgeCensus::is_clear_global() { // Is local census information clear? bool ShenandoahAgeCensus::is_clear_local() { if (!ShenandoahGenerationalAdaptiveTenuring) { - assert(_local_age_table == nullptr, "Error"); + assert(_local_age_tables == nullptr, "Error"); return true; } for (uint i = 0; i < _max_workers; i++) { - bool clear = _local_age_table[i]->is_clear(); + bool clear = _local_age_tables[i]->is_clear(); CENSUS_NOISE(clear |= _local_noise[i].is_clear();) if (!clear) { return false; @@ -240,7 +238,7 @@ bool ShenandoahAgeCensus::is_clear_local() { size_t ShenandoahAgeCensus::get_all_ages(uint snap) { assert(snap < MAX_SNAPSHOTS, "Out of bounds"); size_t pop = 0; - const AgeTable* pv = _global_age_table[snap]; + const AgeTable* pv = _global_age_tables[snap]; for (uint i = 0; i < MAX_COHORTS; i++) { pop += pv->sizes[i]; } @@ -260,13 +258,11 @@ void ShenandoahAgeCensus::update_total() { #endif // !PRODUCT void ShenandoahAgeCensus::update_tenuring_threshold() { - if (!ShenandoahGenerationalAdaptiveTenuring) { - _tenuring_threshold[_epoch] = InitialTenuringThreshold; - } else { - uint tt = compute_tenuring_threshold(); - assert(tt <= MAX_COHORTS, "Out of bounds"); - _tenuring_threshold[_epoch] = tt; - } + assert(ShenandoahGenerationalAdaptiveTenuring, "Only update when adaptive tenuring is enabled"); + uint tt = compute_tenuring_threshold(); + assert(tt <= MAX_COHORTS, "Out of bounds"); + _tenuring_threshold[_epoch] = tt; + print(); log_info(gc, age)("New tenuring threshold %zu (min %zu, max %zu)", (uintx) _tenuring_threshold[_epoch], ShenandoahGenerationalMinTenuringAge, ShenandoahGenerationalMaxTenuringAge); @@ -296,8 +292,8 @@ uint ShenandoahAgeCensus::compute_tenuring_threshold() { const uint prev_epoch = cur_epoch > 0 ? cur_epoch - 1 : markWord::max_age; // Current and previous population vectors in ring - const AgeTable* cur_pv = _global_age_table[cur_epoch]; - const AgeTable* prev_pv = _global_age_table[prev_epoch]; + const AgeTable* cur_pv = _global_age_tables[cur_epoch]; + const AgeTable* prev_pv = _global_age_tables[prev_epoch]; uint upper_bound = ShenandoahGenerationalMaxTenuringAge; const uint prev_tt = previous_tenuring_threshold(); if (ShenandoahGenerationalCensusIgnoreOlderCohorts && prev_tt > 0) { @@ -372,8 +368,8 @@ void ShenandoahAgeCensus::print() { const uint cur_epoch = _epoch; const uint prev_epoch = cur_epoch > 0 ? cur_epoch - 1: markWord::max_age; - const AgeTable* cur_pv = _global_age_table[cur_epoch]; - const AgeTable* prev_pv = _global_age_table[prev_epoch]; + const AgeTable* cur_pv = _global_age_tables[cur_epoch]; + const AgeTable* prev_pv = _global_age_tables[prev_epoch]; const uint tt = tenuring_threshold(); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.hpp b/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.hpp index d862498c69d..39ea4ee9002 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahAgeCensus.hpp @@ -97,8 +97,8 @@ struct ShenandoahNoiseStats { // once the per-worker data is consolidated into the appropriate population vector // per minor collection. The _local_age_table is thus C x N, for N GC workers. class ShenandoahAgeCensus: public CHeapObj { - AgeTable** _global_age_table; // Global age table used for adapting tenuring threshold, one per snapshot - AgeTable** _local_age_table; // Local scratch age tables to track object ages, one per worker + AgeTable** _global_age_tables; // Global age tables used for adapting tenuring threshold, one per snapshot + AgeTable** _local_age_tables; // Local scratch age tables to track object ages, one per worker #ifdef SHENANDOAH_CENSUS_NOISE ShenandoahNoiseStats* _global_noise; // Noise stats, one per snapshot @@ -175,7 +175,7 @@ class ShenandoahAgeCensus: public CHeapObj { // Return the local age table (population vector) for worker_id. // Only used in the case of ShenandoahGenerationalAdaptiveTenuring AgeTable* get_local_age_table(uint worker_id) const { - return _local_age_table[worker_id]; + return _local_age_tables[worker_id]; } // Return the most recently computed tenuring threshold. @@ -209,11 +209,7 @@ class ShenandoahAgeCensus: public CHeapObj { // age0_pop is the population of Cohort 0 that may have been missed in // the regular census during the marking cycle, corresponding to objects // allocated when the concurrent marking was in progress. - // Optional parameters, pv1 and pv2 are population vectors that together - // provide object census data (only) for the case when - // ShenandoahGenerationalCensusAtEvac. In this case, the age0_pop - // is 0, because the evacuated objects have all had their ages incremented. - void update_census(size_t age0_pop, AgeTable* pv1 = nullptr, AgeTable* pv2 = nullptr); + void update_census(size_t age0_pop); // Reset the epoch, clearing accumulated census history // Note: this isn't currently used, but reserved for planned diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp index f4005e45f39..b960255891f 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.cpp @@ -201,26 +201,8 @@ void ShenandoahControlThread::run_service() { heuristics->clear_metaspace_oom(); } - // Commit worker statistics to cycle data - heap->phase_timings()->flush_par_workers_to_cycle(); - - // Print GC stats for current cycle - { - LogTarget(Info, gc, stats) lt; - if (lt.is_enabled()) { - ResourceMark rm; - LogStream ls(lt); - heap->phase_timings()->print_cycle_on(&ls); - if (ShenandoahEvacTracking) { - ShenandoahEvacuationTracker* evac_tracker = heap->evac_tracker(); - ShenandoahCycleStats evac_stats = evac_tracker->flush_cycle_to_global(); - evac_tracker->print_evacuations_on(&ls, &evac_stats.workers, &evac_stats.mutators); - } - } - } - - // Commit statistics to globals - heap->phase_timings()->flush_cycle_to_global(); + // Manage and print gc stats + heap->process_gc_stats(); // Print Metaspace change following GC (if logging is enabled). MetaspaceUtils::print_metaspace_change(meta_sizes); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp b/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp index bc8fad713af..72d0773eccd 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.cpp @@ -23,7 +23,6 @@ * */ -#include "gc/shenandoah/shenandoahAgeCensus.hpp" #include "gc/shenandoah/shenandoahEvacTracker.hpp" #include "gc/shenandoah/shenandoahHeap.inline.hpp" #include "gc/shenandoah/shenandoahThreadLocalData.hpp" @@ -44,19 +43,6 @@ ShenandoahEvacuationStats::ShenandoahEvacuations* ShenandoahEvacuationStats::get return &_old; } -ShenandoahEvacuationStats::ShenandoahEvacuationStats() - : _use_age_table(!ShenandoahGenerationalAdaptiveTenuring), - _age_table(nullptr) { - if (_use_age_table) { - _age_table = new AgeTable(false); - } -} - -AgeTable* ShenandoahEvacuationStats::age_table() const { - assert(_use_age_table, "Don't call"); - return _age_table; -} - void ShenandoahEvacuationStats::begin_evacuation(size_t bytes, ShenandoahAffiliation from, ShenandoahAffiliation to) { ShenandoahEvacuations* category = get_category(from, to); category->_evacuations_attempted++; @@ -70,31 +56,16 @@ void ShenandoahEvacuationStats::end_evacuation(size_t bytes, ShenandoahAffiliati category->_bytes_completed += bytes; } -void ShenandoahEvacuationStats::record_age(size_t bytes, uint age) { - assert(_use_age_table, "Don't call!"); - if (age <= markWord::max_age) { // Filter age sentinel. - _age_table->add(age, bytes >> LogBytesPerWord); - } -} - void ShenandoahEvacuationStats::accumulate(const ShenandoahEvacuationStats* other) { _young.accumulate(other->_young); _old.accumulate(other->_old); _promotion.accumulate(other->_promotion); - - if (_use_age_table) { - _age_table->merge(other->age_table()); - } } void ShenandoahEvacuationStats::reset() { _young.reset(); _old.reset(); _promotion.reset(); - - if (_use_age_table) { - _age_table->clear(); - } } void ShenandoahEvacuationStats::ShenandoahEvacuations::print_on(outputStream* st) const { @@ -112,10 +83,6 @@ void ShenandoahEvacuationStats::print_on(outputStream* st) const { st->print("Promotion: "); _promotion.print_on(st); st->print("Old: "); _old.print_on(st); } - - if (_use_age_table) { - _age_table->print_on(st); - } } void ShenandoahEvacuationTracker::print_global_on(outputStream* st) { @@ -125,28 +92,13 @@ void ShenandoahEvacuationTracker::print_global_on(outputStream* st) { void ShenandoahEvacuationTracker::print_evacuations_on(outputStream* st, ShenandoahEvacuationStats* workers, ShenandoahEvacuationStats* mutators) { - if (ShenandoahEvacTracking) { - st->print_cr("Workers: "); - workers->print_on(st); - st->cr(); - st->print_cr("Mutators: "); - mutators->print_on(st); - st->cr(); - } - - ShenandoahHeap* heap = ShenandoahHeap::heap(); - if (heap->mode()->is_generational()) { - AgeTable young_region_ages(false); - for (uint i = 0; i < heap->num_regions(); ++i) { - ShenandoahHeapRegion* r = heap->get_region(i); - if (r->is_young()) { - young_region_ages.add(r->age(), r->get_live_data_words()); - } - } - st->print("Young regions: "); - young_region_ages.print_on(st); - st->cr(); - } + assert(ShenandoahEvacTracking, "Only when evac tracking is enabled"); + st->print_cr("Workers: "); + workers->print_on(st); + st->cr(); + st->print_cr("Mutators: "); + mutators->print_on(st); + st->cr(); } class ShenandoahStatAggregator : public ThreadClosure { @@ -173,15 +125,6 @@ ShenandoahCycleStats ShenandoahEvacuationTracker::flush_cycle_to_global() { _mutators_global.accumulate(&mutators); _workers_global.accumulate(&workers); - if (!ShenandoahGenerationalAdaptiveTenuring) { - // Ingest mutator & worker collected population vectors into the heap's - // global census data, and use it to compute an appropriate tenuring threshold - // for use in the next cycle. - // The first argument is used for any age 0 cohort population that we may otherwise have - // missed during the census. This is non-zero only when census happens at marking. - ShenandoahGenerationalHeap::heap()->age_census()->update_census(0, mutators.age_table(), workers.age_table()); - } - return {workers, mutators}; } @@ -192,7 +135,3 @@ void ShenandoahEvacuationTracker::begin_evacuation(Thread* thread, size_t bytes, void ShenandoahEvacuationTracker::end_evacuation(Thread* thread, size_t bytes, ShenandoahAffiliation from, ShenandoahAffiliation to) { ShenandoahThreadLocalData::end_evacuation(thread, bytes, from, to); } - -void ShenandoahEvacuationTracker::record_age(Thread* thread, size_t bytes, uint age) { - ShenandoahThreadLocalData::record_age(thread, bytes, age); -} diff --git a/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.hpp b/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.hpp index e5d7a7fec94..6e1182680a5 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahEvacTracker.hpp @@ -66,20 +66,12 @@ private: ShenandoahEvacuations _old; ShenandoahEvacuations _promotion; - bool _use_age_table; - AgeTable* _age_table; - public: - ShenandoahEvacuationStats(); - - AgeTable* age_table() const; - // Record that the current thread is attempting to copy this many bytes. void begin_evacuation(size_t bytes, ShenandoahAffiliation from, ShenandoahAffiliation to); // Record that the current thread has completed copying this many bytes. void end_evacuation(size_t bytes, ShenandoahAffiliation from, ShenandoahAffiliation to); - void record_age(size_t bytes, uint age); void print_on(outputStream* st) const; void accumulate(const ShenandoahEvacuationStats* other); @@ -106,7 +98,6 @@ public: // Multiple threads may attempt to evacuate the same object, but only the successful thread will end the evacuation. // Evacuations that were begun, but not ended are considered 'abandoned'. void end_evacuation(Thread* thread, size_t bytes, ShenandoahAffiliation from, ShenandoahAffiliation to); - void record_age(Thread* thread, size_t bytes, uint age); void print_global_on(outputStream* st); void print_evacuations_on(outputStream* st, diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp index 761ba02d569..c6d1cf02ee2 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.cpp @@ -201,6 +201,24 @@ ShenandoahGenerationalControlThread::GCMode ShenandoahGenerationalControlThread: return request.generation->is_old() ? servicing_old : concurrent_normal; } +void ShenandoahGenerationalControlThread::maybe_print_young_region_ages() const { + LogTarget(Debug, gc, age) lt; + if (lt.is_enabled()) { + LogStream ls(lt); + AgeTable young_region_ages(false); + for (uint i = 0; i < _heap->num_regions(); ++i) { + const ShenandoahHeapRegion* r = _heap->get_region(i); + if (r->is_young()) { + young_region_ages.add(r->age(), r->get_live_data_words()); + } + } + + ls.print("Young regions: "); + young_region_ages.print_on(&ls); + ls.cr(); + } +} + void ShenandoahGenerationalControlThread::maybe_set_aging_cycle() { if (_age_period-- == 0) { _heap->set_aging_cycle(true); @@ -298,7 +316,11 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest _heap->global_generation()->heuristics()->clear_metaspace_oom(); } - process_phase_timings(); + // Manage and print gc stats + _heap->process_gc_stats(); + + // Print table for young region ages if log is enabled + maybe_print_young_region_ages(); // Print Metaspace change following GC (if logging is enabled). MetaspaceUtils::print_metaspace_change(meta_sizes); @@ -317,29 +339,6 @@ void ShenandoahGenerationalControlThread::run_gc_cycle(const ShenandoahGCRequest gc_mode_name(gc_mode()), GCCause::to_string(request.cause), request.generation->name(), GCCause::to_string(_heap->cancelled_cause())); } -void ShenandoahGenerationalControlThread::process_phase_timings() const { - // Commit worker statistics to cycle data - _heap->phase_timings()->flush_par_workers_to_cycle(); - - ShenandoahEvacuationTracker* evac_tracker = _heap->evac_tracker(); - ShenandoahCycleStats evac_stats = evac_tracker->flush_cycle_to_global(); - - // Print GC stats for current cycle - { - LogTarget(Info, gc, stats) lt; - if (lt.is_enabled()) { - ResourceMark rm; - LogStream ls(lt); - _heap->phase_timings()->print_cycle_on(&ls); - evac_tracker->print_evacuations_on(&ls, &evac_stats.workers, - &evac_stats.mutators); - } - } - - // Commit statistics to globals - _heap->phase_timings()->flush_cycle_to_global(); -} - // Young and old concurrent cycles are initiated by the regulator. Implicit // and explicit GC requests are handled by the controller thread and always // run a global cycle (which is concurrent by default, but may be overridden @@ -417,7 +416,7 @@ void ShenandoahGenerationalControlThread::service_concurrent_old_cycle(const She set_gc_mode(bootstrapping_old); young_generation->set_old_gen_task_queues(old_generation->task_queues()); service_concurrent_cycle(young_generation, request.cause, true); - process_phase_timings(); + _heap->process_gc_stats(); if (_heap->cancelled_gc()) { // Young generation bootstrap cycle has failed. Concurrent mark for old generation // is going to resume after degenerated bootstrap cycle completes. diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp index 1586205742a..6a4f5bde578 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp @@ -129,9 +129,6 @@ private: // Returns true if the old generation marking was interrupted to allow a young cycle. bool preempt_old_marking(ShenandoahGeneration* generation); - // Flushes cycle timings to global timings and prints the phase timings for the last completed cycle. - void process_phase_timings() const; - // Set the gc mode and post a notification if it has changed. The overloaded variant should be used // when the _control_lock is already held. void set_gc_mode(GCMode new_mode); @@ -160,6 +157,9 @@ private: GCMode prepare_for_allocation_failure_gc(ShenandoahGCRequest &request); GCMode prepare_for_explicit_gc(ShenandoahGCRequest &request) const; GCMode prepare_for_concurrent_gc(const ShenandoahGCRequest &request) const; + + // Print table for young region ages if log is enabled + void maybe_print_young_region_ages() const; }; #endif // SHARE_GC_SHENANDOAH_SHENANDOAHGENERATIONALCONTROLTHREAD_HPP diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp index 8a21ae376e1..34f217ada25 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalHeap.cpp @@ -360,11 +360,6 @@ oop ShenandoahGenerationalHeap::try_evacuate_object(oop p, Thread* thread, Shena // When copying to the old generation above, we don't care // about recording object age in the census stats. assert(target_gen == YOUNG_GENERATION, "Error"); - // We record this census only when simulating pre-adaptive tenuring behavior, or - // when we have been asked to record the census at evacuation rather than at mark - if (!ShenandoahGenerationalAdaptiveTenuring) { - evac_tracker()->record_age(thread, size * HeapWordSize, ShenandoahHeap::get_object_age(copy_val)); - } } shenandoah_assert_correct(nullptr, copy_val); return copy_val; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp index 10d7e8edcad..e6d41237c98 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp @@ -1441,6 +1441,27 @@ void ShenandoahHeap::print_heap_regions_on(outputStream* st) const { } } +void ShenandoahHeap::process_gc_stats() const { + // Commit worker statistics to cycle data + phase_timings()->flush_par_workers_to_cycle(); + + // Print GC stats for current cycle + LogTarget(Info, gc, stats) lt; + if (lt.is_enabled()) { + ResourceMark rm; + LogStream ls(lt); + phase_timings()->print_cycle_on(&ls); + if (ShenandoahEvacTracking) { + ShenandoahCycleStats evac_stats = evac_tracker()->flush_cycle_to_global(); + evac_tracker()->print_evacuations_on(&ls, &evac_stats.workers, + &evac_stats.mutators); + } + } + + // Commit statistics to globals + phase_timings()->flush_cycle_to_global(); +} + size_t ShenandoahHeap::trash_humongous_region_at(ShenandoahHeapRegion* start) const { assert(start->is_humongous_start(), "reclaim regions starting with the first one"); assert(!start->has_live(), "liveness must be zero"); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp b/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp index 322ac26e254..11a20d4a2f9 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahHeap.hpp @@ -206,9 +206,12 @@ public: void initialize_serviceability() override; void print_heap_on(outputStream* st) const override; - void print_gc_on(outputStream *st) const override; + void print_gc_on(outputStream* st) const override; void print_heap_regions_on(outputStream* st) const; + // Flushes cycle timings to global timings and prints the phase timings for the last completed cycle. + void process_gc_stats() const; + void prepare_for_verify() override; void verify(VerifyOption vo) override; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahThreadLocalData.hpp b/src/hotspot/share/gc/shenandoah/shenandoahThreadLocalData.hpp index 37d935d1f78..f54a65b0785 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahThreadLocalData.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahThreadLocalData.hpp @@ -166,10 +166,6 @@ public: data(thread)->_evacuation_stats->end_evacuation(bytes, from, to); } - static void record_age(Thread* thread, size_t bytes, uint age) { - data(thread)->_evacuation_stats->record_age(bytes, age); - } - static ShenandoahEvacuationStats* evacuation_stats(Thread* thread) { return data(thread)->_evacuation_stats; } From 741221988e03d1710d3a73ab9c7764991f216fae Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Thu, 25 Sep 2025 16:47:22 +0000 Subject: [PATCH 240/556] 8368261: Serial: Use more precise nmethod scope during Full GC marking Reviewed-by: stefank, fandreuzzi --- src/hotspot/share/code/nmethod.hpp | 9 +++++++++ src/hotspot/share/gc/serial/serialFullGC.cpp | 14 +++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/hotspot/share/code/nmethod.hpp b/src/hotspot/share/code/nmethod.hpp index 3abda6010b7..020370c504b 100644 --- a/src/hotspot/share/code/nmethod.hpp +++ b/src/hotspot/share/code/nmethod.hpp @@ -1080,4 +1080,13 @@ public: static const Vptr _vpntr; }; +struct NMethodMarkingScope : StackObj { + NMethodMarkingScope() { + nmethod::oops_do_marking_prologue(); + } + ~NMethodMarkingScope() { + nmethod::oops_do_marking_epilogue(); + } +}; + #endif // SHARE_CODE_NMETHOD_HPP diff --git a/src/hotspot/share/gc/serial/serialFullGC.cpp b/src/hotspot/share/gc/serial/serialFullGC.cpp index d45454a768f..271b4b4ebdb 100644 --- a/src/hotspot/share/gc/serial/serialFullGC.cpp +++ b/src/hotspot/share/gc/serial/serialFullGC.cpp @@ -54,7 +54,6 @@ #include "gc/shared/referencePolicy.hpp" #include "gc/shared/referenceProcessorPhaseTimes.hpp" #include "gc/shared/space.hpp" -#include "gc/shared/strongRootsScope.hpp" #include "gc/shared/weakProcessor.hpp" #include "memory/iterator.inline.hpp" #include "memory/universe.hpp" @@ -483,10 +482,6 @@ void SerialFullGC::phase1_mark(bool clear_all_softrefs) { ref_processor()->start_discovery(clear_all_softrefs); { - StrongRootsScope srs(0); - - MarkingNMethodClosure mark_code_closure(&follow_root_closure); - // Start tracing from roots, there are 3 kinds of roots in full-gc. // // 1. CLD. This method internally takes care of whether class loading is @@ -494,8 +489,13 @@ void SerialFullGC::phase1_mark(bool clear_all_softrefs) { // strong CLDs. ClassLoaderDataGraph::always_strong_cld_do(&follow_cld_closure); - // 2. Threads stack frames and active nmethods in them. - Threads::oops_do(&follow_root_closure, &mark_code_closure); + { + // 2. Threads stack frames and active nmethods in them. + NMethodMarkingScope nmethod_marking_scope; + MarkingNMethodClosure mark_code_closure(&follow_root_closure); + + Threads::oops_do(&follow_root_closure, &mark_code_closure); + } // 3. VM internal roots. OopStorageSet::strong_oops_do(&follow_root_closure); From de1f5a3c437ab4c6009f8be6f9f109ed36fb0b53 Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Thu, 25 Sep 2025 17:42:46 +0000 Subject: [PATCH 241/556] 8368468: Split out everything but configure results from spec.gmk Reviewed-by: erikj --- make/RunTestsPrebuilt.gmk | 5 +- make/RunTestsPrebuiltSpec.gmk | 58 +-- make/autoconf/boot-jdk.m4 | 8 +- make/autoconf/bootcycle-spec.gmk.template | 21 +- make/autoconf/build-performance.m4 | 20 +- make/autoconf/buildjdk-spec.gmk.template | 79 +--- make/autoconf/help.m4 | 4 +- make/autoconf/hotspot.m4 | 2 +- make/autoconf/jdk-options.m4 | 6 +- make/autoconf/spec.gmk.template | 343 +++-------------- make/common/CommonVars.gmk | 442 ++++++++++++++++++++++ make/common/MakeBase.gmk | 1 + 12 files changed, 524 insertions(+), 465 deletions(-) create mode 100644 make/common/CommonVars.gmk diff --git a/make/RunTestsPrebuilt.gmk b/make/RunTestsPrebuilt.gmk index f5fe1d33830..3ea0d330d58 100644 --- a/make/RunTestsPrebuilt.gmk +++ b/make/RunTestsPrebuilt.gmk @@ -131,7 +131,7 @@ $(eval $(call SetupVariable,MAKE,make,NO_CHECK)) $(eval $(call SetupVariable,BASH,bash,NO_CHECK)) # Check optional variables -$(eval $(call SetupVariable,JIB_JAR,OPTIONAL)) +$(eval $(call SetupVariable,JIB_HOME,OPTIONAL)) # Now that we have verified that we have the required variables available, we # can include the prebuilt spec file ourselves, without an ephemeral spec @@ -265,7 +265,7 @@ $(call CreateNewSpec, $(NEW_SPEC), \ SYMBOLS_IMAGE_DIR := $(SYMBOLS_IMAGE_DIR), \ MAKE := $(MAKE), \ BASH := $(BASH), \ - JIB_JAR := $(JIB_JAR), \ + JIB_HOME := $(JIB_HOME), \ FIXPATH_BASE := $(FIXPATH_BASE), \ FIXPATH := $(FIXPATH), \ OPENJDK_TARGET_OS := $(OPENJDK_TARGET_OS), \ @@ -295,6 +295,7 @@ test-prebuilt: # ExecuteWithLog is called in RunTests.gmk. The PrepareFailureLogs macro # is unfortunately not available at this point. $(call MakeDir, $(MAKESUPPORT_OUTPUTDIR)/failure-logs) + $(call MakeDir, $(JAVA_TMP_DIR)) @$(RM) -f $(MAKESUPPORT_OUTPUTDIR)/exit-with-error # We need to fill the FindTest cache before entering RunTests.gmk. @cd $(TOPDIR)/make && $(MAKE) $(MAKE_ARGS) SPEC=$(SPEC) \ diff --git a/make/RunTestsPrebuiltSpec.gmk b/make/RunTestsPrebuiltSpec.gmk index e9fd901c9e1..132532b3b38 100644 --- a/make/RunTestsPrebuiltSpec.gmk +++ b/make/RunTestsPrebuiltSpec.gmk @@ -27,9 +27,6 @@ # Fake minimalistic spec file for RunTestsPrebuilt.gmk. ################################################################################ -# Make sure all shell commands are executed with the C locale -export LC_ALL := C - define VerifyVariable ifeq ($$($1), ) $$(info Error: Variable $1 is missing, needed by RunTestPrebuiltSpec.gmk) @@ -57,26 +54,18 @@ $(eval $(call VerifyVariable,BASH)) # The "human readable" name of this configuration CONF_NAME := run-test-prebuilt +LOCALE_USED := C + # Number of parallel jobs to use for compilation -JOBS ?= $(NUM_CORES) -TEST_JOBS ?= 0 +CONF_JOBS := $(NUM_CORES) +CONF_TEST_JOBS := 0 # Use hard-coded values for java flags (one size, fits all!) JAVA_FLAGS := -Duser.language=en -Duser.country=US JAVA_FLAGS_BIG := -Xms64M -Xmx2048M JAVA_FLAGS_SMALL := -XX:+UseSerialGC -Xms32M -Xmx512M -XX:TieredStopAtLevel=1 -BUILDJDK_JAVA_FLAGS_SMALL := -Xms32M -Xmx512M -XX:TieredStopAtLevel=1 -BUILD_JAVA_FLAGS := $(JAVA_FLAGS_BIG) - -################################################################################ -# Hard-coded values copied from spec.gmk.in. -X := -SPACE := $(X) $(X) -COMMA := , -MAKE_ARGS = $(MAKE_LOG_FLAGS) -r -R -I $(TOPDIR)/make/common SPEC=$(SPEC) \ - MAKE_LOG_FLAGS="$(MAKE_LOG_FLAGS)" LOG_LEVEL=$(LOG_LEVEL) -BASH_ARGS := -o pipefail -e -SHELL := $(BASH) $(BASH_ARGS) +BUILD_JAVA_FLAGS_SMALL := -Xms32M -Xmx512M -XX:TieredStopAtLevel=1 +BOOTCYCLE_JVM_ARGS_BIG := $(JAVA_FLAGS_BIG) ################################################################################ # Set some reasonable defaults for features @@ -84,20 +73,6 @@ DEBUG_LEVEL := release HOTSPOT_DEBUG_LEVEL := release BUILD_FAILURE_HANDLER := true -################################################################################ -# Alias some paths (that should not really be used) to our JDK image under test. -SUPPORT_OUTPUTDIR := $(OUTPUTDIR)/support -BUILDTOOLS_OUTPUTDIR := $(OUTPUTDIR)/buildtools -HOTSPOT_OUTPUTDIR := $(OUTPUTDIR)/hotspot -JDK_OUTPUTDIR := $(OUTPUTDIR)/jdk -IMAGES_OUTPUTDIR := $(OUTPUTDIR)/images -BUNDLES_OUTPUTDIR := $(OUTPUTDIR)/bundles -TESTMAKE_OUTPUTDIR := $(OUTPUTDIR)/test-make -MAKESUPPORT_OUTPUTDIR := $(OUTPUTDIR)/make-support -BUILDJDK_OUTPUTDIR := $(OUTPUTDIR)/buildjdk - -JRE_IMAGE_DIR := $(JDK_IMAGE_DIR) - ################################################################################ # Assume build platform is same as target platform OPENJDK_BUILD_OS := $(OPENJDK_TARGET_OS) @@ -109,30 +84,19 @@ OPENJDK_BUILD_CPU_ARCH := $(OPENJDK_TARGET_CPU_ARCH) OPENJDK_BUILD_CPU_BITS := $(OPENJDK_TARGET_CPU_BITS) OPENJDK_BUILD_CPU_ENDIAN := $(OPENJDK_TARGET_CPU_ENDIAN) +EXTERNAL_BUILDJDK_PATH := + ################################################################################ # Java executable definitions -JAVA_CMD := $(BOOT_JDK)/bin/java -JAVAC_CMD := $(BOOT_JDK)/bin/javac -JAR_CMD := $(BOOT_JDK)/bin/jar -JLINK_CMD := $(JDK_OUTPUTDIR)/bin/jlink -JMOD_CMD := $(JDK_OUTPUTDIR)/bin/jmod +JAVA_CMD := $(FIXPATH) $(BOOT_JDK)/bin/java +JAVAC_CMD := $(FIXPATH) $(BOOT_JDK)/bin/javac +JAR_CMD := $(FIXPATH) $(BOOT_JDK)/bin/jar -JAVA := $(FIXPATH) $(JAVA_CMD) $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) -JAVA_SMALL := $(FIXPATH) $(JAVA_CMD) $(JAVA_FLAGS_SMALL) $(JAVA_FLAGS) -JAVAC := $(FIXPATH) $(JAVAC_CMD) -JAR := $(FIXPATH) $(JAR_CMD) -JLINK := $(FIXPATH) $(JLINK_CMD) -JMOD := $(FIXPATH) $(JMOD_CMD) - -JTREG_JAVA := $(FIXPATH) $(JTREG_JDK)/bin/java $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) - -BUILD_JAVA := $(JDK_IMAGE_DIR)/bin/JAVA ################################################################################ # Some common tools. Assume most common name and no path. AWK := awk BASENAME := basename CAT := cat -CD := cd CHMOD := chmod CP := cp CUT := cut diff --git a/make/autoconf/boot-jdk.m4 b/make/autoconf/boot-jdk.m4 index 1dd768b2ae1..49a3842573c 100644 --- a/make/autoconf/boot-jdk.m4 +++ b/make/autoconf/boot-jdk.m4 @@ -376,10 +376,10 @@ AC_DEFUN_ONCE([BOOTJDK_SETUP_BOOT_JDK], AC_SUBST(BOOT_JDK) # Setup tools from the Boot JDK. - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVA, java) - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVAC, javac) - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVADOC, javadoc) - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAR, jar) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVA_CMD, java) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVAC_CMD, javac) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVADOC_CMD, javadoc) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAR_CMD, jar) # Finally, set some other options... diff --git a/make/autoconf/bootcycle-spec.gmk.template b/make/autoconf/bootcycle-spec.gmk.template index 8b6035606a5..572ea8f221f 100644 --- a/make/autoconf/bootcycle-spec.gmk.template +++ b/make/autoconf/bootcycle-spec.gmk.template @@ -28,23 +28,4 @@ # First include the real base spec.gmk file include @SPEC@ -# Override specific values to do a boot cycle build - -# Use a different Boot JDK -BOOT_JDK := $(JDK_IMAGE_DIR) - -# The bootcycle build has a different output directory -OLD_OUTPUTDIR := @OUTPUTDIR@ -OUTPUTDIR := $(OLD_OUTPUTDIR)/bootcycle-build -# No spaces in patsubst to avoid leading space in variable -JAVAC_SERVER_DIR := $(patsubst $(OLD_OUTPUTDIR)%,$(OUTPUTDIR)%,$(JAVAC_SERVER_DIR)) - -JAVA_CMD := $(FIXPATH) $(BOOT_JDK)/bin/java -JAVAC_CMD := $(FIXPATH) $(BOOT_JDK)/bin/javac -JAR_CMD := $(FIXPATH) $(BOOT_JDK)/bin/jar -# The bootcycle JVM arguments may differ from the original boot jdk. -JAVA_FLAGS_BIG := @BOOTCYCLE_JVM_ARGS_BIG@ -# Any CDS settings generated for the bootjdk are invalid in the bootcycle build. -# By filtering out those JVM args, the bootcycle JVM will use its default -# settings for CDS. -JAVA_FLAGS := $(filter-out -XX:SharedArchiveFile% -Xshare%, $(JAVA_FLAGS)) +IS_BOOTCYCLE_JDK_SPEC := true diff --git a/make/autoconf/build-performance.m4 b/make/autoconf/build-performance.m4 index dfc9e979d2f..49ae9b39cf2 100644 --- a/make/autoconf/build-performance.m4 +++ b/make/autoconf/build-performance.m4 @@ -130,18 +130,18 @@ AC_DEFUN_ONCE([BPERF_SETUP_BUILD_JOBS], memory_gb=`expr $MEMORY_SIZE / 1024` # Pick the lowest of memory in gb and number of cores. if test "$memory_gb" -lt "$NUM_CORES"; then - JOBS="$memory_gb" + CONF_JOBS="$memory_gb" else - JOBS="$NUM_CORES" + CONF_JOBS="$NUM_CORES" fi - if test "$JOBS" -eq "0"; then - JOBS=1 + if test "$CONF_JOBS" -eq "0"; then + CONF_JOBS=1 fi - AC_MSG_RESULT([$JOBS]) + AC_MSG_RESULT([$CONF_JOBS]) else - JOBS=$with_jobs + CONF_JOBS=$with_jobs fi - AC_SUBST(JOBS) + AC_SUBST(CONF_JOBS) ]) AC_DEFUN_ONCE([BPERF_SETUP_TEST_JOBS], @@ -150,11 +150,11 @@ AC_DEFUN_ONCE([BPERF_SETUP_TEST_JOBS], AC_ARG_WITH(test-jobs, [AS_HELP_STRING([--with-test-jobs], [number of parallel tests jobs to run @<:@based on build jobs@:>@])]) if test "x$with_test_jobs" = x; then - TEST_JOBS=0 + CONF_TEST_JOBS=0 else - TEST_JOBS=$with_test_jobs + CONF_TEST_JOBS=$with_test_jobs fi - AC_SUBST(TEST_JOBS) + AC_SUBST(CONF_TEST_JOBS) ]) AC_DEFUN([BPERF_SETUP_CCACHE], diff --git a/make/autoconf/buildjdk-spec.gmk.template b/make/autoconf/buildjdk-spec.gmk.template index 924389b94e8..02f2a6396ce 100644 --- a/make/autoconf/buildjdk-spec.gmk.template +++ b/make/autoconf/buildjdk-spec.gmk.template @@ -30,81 +30,4 @@ # First include the real base spec.gmk file include @SPEC@ -CC := @BUILD_CC@ -CXX := @BUILD_CXX@ -# Ideally this should be probed by configure but that is tricky to implement, -# and this should work in most cases. -CPP := @BUILD_CC@ -E -LD := @BUILD_LD@ -LDCXX := @BUILD_LDCXX@ -AS := @BUILD_AS@ -NM := @BUILD_NM@ -AR := @BUILD_AR@ -LIB := @BUILD_LIB@ -OBJCOPY := @BUILD_OBJCOPY@ -STRIP := @BUILD_STRIP@ -SYSROOT_CFLAGS := @BUILD_SYSROOT_CFLAGS@ -SYSROOT_LDFLAGS := @BUILD_SYSROOT_LDFLAGS@ - -# These directories should not be moved to BUILDJDK_OUTPUTDIR -HOTSPOT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(HOTSPOT_OUTPUTDIR)) -BUILDTOOLS_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(BUILDTOOLS_OUTPUTDIR)) -SUPPORT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(SUPPORT_OUTPUTDIR)) -JDK_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(JDK_OUTPUTDIR)) -IMAGES_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(IMAGES_OUTPUTDIR)) - -OPENJDK_BUILD_CPU_LEGACY := @OPENJDK_BUILD_CPU_LEGACY@ -OPENJDK_BUILD_CPU_LEGACY_LIB := @OPENJDK_BUILD_CPU_LEGACY_LIB@ -OPENJDK_BUILD_LIBC := @OPENJDK_BUILD_LIBC@ -OPENJDK_TARGET_CPU := @OPENJDK_BUILD_CPU@ -OPENJDK_TARGET_CPU_ARCH := @OPENJDK_BUILD_CPU_ARCH@ -OPENJDK_TARGET_CPU_BITS := @OPENJDK_BUILD_CPU_BITS@ -OPENJDK_TARGET_CPU_ENDIAN := @OPENJDK_BUILD_CPU_ENDIAN@ -OPENJDK_TARGET_CPU_LEGACY := @OPENJDK_BUILD_CPU_LEGACY@ -OPENJDK_TARGET_LIBC := @OPENJDK_BUILD_LIBC@ -OPENJDK_TARGET_OS_INCLUDE_SUBDIR := @OPENJDK_BUILD_OS_INCLUDE_SUBDIR@ - -HOTSPOT_TARGET_OS := @HOTSPOT_BUILD_OS@ -HOTSPOT_TARGET_OS_TYPE := @HOTSPOT_BUILD_OS_TYPE@ -HOTSPOT_TARGET_CPU := @HOTSPOT_BUILD_CPU@ -HOTSPOT_TARGET_CPU_ARCH := @HOTSPOT_BUILD_CPU_ARCH@ -HOTSPOT_TARGET_CPU_DEFINE := @HOTSPOT_BUILD_CPU_DEFINE@ -HOTSPOT_TARGET_LIBC := @HOTSPOT_BUILD_LIBC@ - -CFLAGS_JDKLIB := @OPENJDK_BUILD_CFLAGS_JDKLIB@ -CXXFLAGS_JDKLIB := @OPENJDK_BUILD_CXXFLAGS_JDKLIB@ -LDFLAGS_JDKLIB := @OPENJDK_BUILD_LDFLAGS_JDKLIB@ -CFLAGS_JDKEXE := @OPENJDK_BUILD_CFLAGS_JDKEXE@ -CXXFLAGS_JDKEXE := @OPENJDK_BUILD_CXXFLAGS_JDKEXE@ -LDFLAGS_JDKEXE := @OPENJDK_BUILD_LDFLAGS_JDKEXE@ - -JVM_CFLAGS := @OPENJDK_BUILD_JVM_CFLAGS@ -JVM_LDFLAGS := @OPENJDK_BUILD_JVM_LDFLAGS@ -JVM_ASFLAGS := @OPENJDK_BUILD_JVM_ASFLAGS@ -JVM_LIBS := @OPENJDK_BUILD_JVM_LIBS@ - -FDLIBM_CFLAGS := @OPENJDK_BUILD_FDLIBM_CFLAGS@ - -INTERIM_LANGTOOLS_ARGS := $(subst $(OUTPUTDIR),$(BUILDJDK_OUTPUTDIR),$(INTERIM_LANGTOOLS_ARGS)) - -# The compiler for the build platform is likely not warning compatible with the official -# compiler. -WARNINGS_AS_ERRORS := false -DISABLE_WARNING_PREFIX := @BUILD_CC_DISABLE_WARNING_PREFIX@ - -# Save speed and disk space by not enabling debug symbols for the buildjdk -ENABLE_DEBUG_SYMBOLS := false - -JVM_VARIANTS := server -JVM_VARIANT_MAIN := server -JVM_FEATURES_server := cds compiler1 compiler2 g1gc serialgc - -# Some users still set EXTRA_*FLAGS on the make command line. Must -# make sure to override that when building buildjdk. -override EXTRA_CFLAGS := -override EXTRA_CXXFLAGS := -override EXTRA_LDFLAGS := - -# hsdis is not needed -HSDIS_BACKEND := none -ENABLE_HSDIS_BUNDLING := false +IS_BUILD_JDK_SPEC := true diff --git a/make/autoconf/help.m4 b/make/autoconf/help.m4 index d8c0b2ffaef..4dc3cb5c267 100644 --- a/make/autoconf/help.m4 +++ b/make/autoconf/help.m4 @@ -305,7 +305,7 @@ AC_DEFUN_ONCE([HELP_PRINT_SUMMARY_AND_WARNINGS], $ECHO "* Version string: $VERSION_STRING ($VERSION_SHORT)" if test "x$SOURCE_DATE" != xupdated; then - source_date_info="$SOURCE_DATE ($SOURCE_DATE_ISO_8601)" + source_date_info="$SOURCE_DATE ($SOURCE_DATE_ISO_8601_FIXED)" else source_date_info="Determined at build time" fi @@ -330,7 +330,7 @@ AC_DEFUN_ONCE([HELP_PRINT_SUMMARY_AND_WARNINGS], $ECHO "" $ECHO "Build performance summary:" - $ECHO "* Build jobs: $JOBS" + $ECHO "* Build jobs: $CONF_JOBS" $ECHO "* Memory limit: $MEMORY_SIZE MB" if test "x$CCACHE_STATUS" != "x"; then $ECHO "* ccache status: $CCACHE_STATUS" diff --git a/make/autoconf/hotspot.m4 b/make/autoconf/hotspot.m4 index 6dc46d17aa3..aba5f1ef1d2 100644 --- a/make/autoconf/hotspot.m4 +++ b/make/autoconf/hotspot.m4 @@ -129,7 +129,7 @@ AC_DEFUN_ONCE([HOTSPOT_SETUP_MISC], AC_MSG_RESULT([determined at build time (default)]) else # If we have a fixed value for SOURCE_DATE, use it as default - HOTSPOT_BUILD_TIME="$SOURCE_DATE_ISO_8601" + HOTSPOT_BUILD_TIME="$SOURCE_DATE_ISO_8601_FIXED" AC_MSG_RESULT([$HOTSPOT_BUILD_TIME (from --with-source-date)]) fi fi diff --git a/make/autoconf/jdk-options.m4 b/make/autoconf/jdk-options.m4 index d4299078abf..6de43d0a169 100644 --- a/make/autoconf/jdk-options.m4 +++ b/make/autoconf/jdk-options.m4 @@ -897,15 +897,15 @@ AC_DEFUN_ONCE([JDKOPT_SETUP_REPRODUCIBLE_BUILD], # for the rest of configure. SOURCE_DATE_EPOCH="$SOURCE_DATE" if test "x$IS_GNU_DATE" = xyes; then - SOURCE_DATE_ISO_8601=`$DATE --utc --date="@$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` + SOURCE_DATE_ISO_8601_FIXED=`$DATE --utc --date="@$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` else - SOURCE_DATE_ISO_8601=`$DATE -u -j -f "%s" "$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` + SOURCE_DATE_ISO_8601_FIXED=`$DATE -u -j -f "%s" "$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` fi fi AC_SUBST(SOURCE_DATE) AC_SUBST(ISO_8601_FORMAT_STRING) - AC_SUBST(SOURCE_DATE_ISO_8601) + AC_SUBST(SOURCE_DATE_ISO_8601_FIXED) ]) ################################################################################ diff --git a/make/autoconf/spec.gmk.template b/make/autoconf/spec.gmk.template index 0b336721d65..3258e87e2be 100644 --- a/make/autoconf/spec.gmk.template +++ b/make/autoconf/spec.gmk.template @@ -46,33 +46,15 @@ SPEC := @SPEC@ # Path to autoconf if overridden by the user, to be used by "make reconfigure" AUTOCONF := @AUTOCONF@ -# SPACE and COMMA are defined in MakeBase.gmk, but they are also used in -# some definitions here, and are needed if MakeBase.gmk is not included before -# this file. -X := -SPACE := $(X) $(X) -COMMA := , - # What make to use for main processing, after bootstrapping top-level Makefile. MAKE := @MAKE@ -# Make sure all shell commands are executed with a proper locale -export LC_ALL := @LOCALE_USED@ - -# Make sure we override any local CLASSPATH variable -export CLASSPATH := @CLASSPATH@ - -# The default make arguments -MAKE_ARGS = $(MAKE_LOG_FLAGS) -r -R -I $(TOPDIR)/make/common SPEC=$(SPEC) \ - MAKE_LOG_FLAGS="$(MAKE_LOG_FLAGS)" $(MAKE_LOG_VARS) - OUTPUT_SYNC_SUPPORTED := @OUTPUT_SYNC_SUPPORTED@ OUTPUT_SYNC := @OUTPUT_SYNC@ # Override the shell with bash BASH := @BASH@ BASH_ARGS := @BASH_ARGS@ -SHELL := $(BASH) $(BASH_ARGS) # The "human readable" name of this configuration CONF_NAME := @CONF_NAME@ @@ -121,9 +103,33 @@ OPENJDK_BUILD_CPU_ARCH := @OPENJDK_BUILD_CPU_ARCH@ OPENJDK_BUILD_CPU_BITS := @OPENJDK_BUILD_CPU_BITS@ OPENJDK_BUILD_CPU_ENDIAN := @OPENJDK_BUILD_CPU_ENDIAN@ +OPENJDK_BUILD_CPU_LEGACY := @OPENJDK_BUILD_CPU_LEGACY@ +OPENJDK_BUILD_CPU_LEGACY_LIB := @OPENJDK_BUILD_CPU_LEGACY_LIB@ OPENJDK_BUILD_LIBC := @OPENJDK_BUILD_LIBC@ -OPENJDK_BUILD_OS_INCLUDE_SUBDIR := @OPENJDK_TARGET_OS_INCLUDE_SUBDIR@ +OPENJDK_BUILD_OS_INCLUDE_SUBDIR := @OPENJDK_BUILD_OS_INCLUDE_SUBDIR@ + +HOTSPOT_BUILD_OS := @HOTSPOT_BUILD_OS@ +HOTSPOT_BUILD_OS_TYPE := @HOTSPOT_BUILD_OS_TYPE@ +HOTSPOT_BUILD_CPU := @HOTSPOT_BUILD_CPU@ +HOTSPOT_BUILD_CPU_ARCH := @HOTSPOT_BUILD_CPU_ARCH@ +HOTSPOT_BUILD_CPU_DEFINE := @HOTSPOT_BUILD_CPU_DEFINE@ +HOTSPOT_BUILD_LIBC := @HOTSPOT_BUILD_LIBC@ + +OPENJDK_BUILD_CFLAGS_JDKLIB := @OPENJDK_BUILD_CFLAGS_JDKLIB@ +OPENJDK_BUILD_CXXFLAGS_JDKLIB := @OPENJDK_BUILD_CXXFLAGS_JDKLIB@ +OPENJDK_BUILD_LDFLAGS_JDKLIB := @OPENJDK_BUILD_LDFLAGS_JDKLIB@ +OPENJDK_BUILD_CFLAGS_JDKEXE := @OPENJDK_BUILD_CFLAGS_JDKEXE@ +OPENJDK_BUILD_CXXFLAGS_JDKEXE := @OPENJDK_BUILD_CXXFLAGS_JDKEXE@ +OPENJDK_BUILD_LDFLAGS_JDKEXE := @OPENJDK_BUILD_LDFLAGS_JDKEXE@ + +OPENJDK_BUILD_JVM_CFLAGS := @OPENJDK_BUILD_JVM_CFLAGS@ +OPENJDK_BUILD_JVM_LDFLAGS := @OPENJDK_BUILD_JVM_LDFLAGS@ +OPENJDK_BUILD_JVM_ASFLAGS := @OPENJDK_BUILD_JVM_ASFLAGS@ +OPENJDK_BUILD_JVM_LIBS := @OPENJDK_BUILD_JVM_LIBS@ + +OPENJDK_BUILD_FDLIBM_CFLAGS := @OPENJDK_BUILD_FDLIBM_CFLAGS@ +BUILD_CC_DISABLE_WARNING_PREFIX := @BUILD_CC_DISABLE_WARNING_PREFIX@ # Target platform value in ModuleTarget class file attribute. OPENJDK_MODULE_TARGET_PLATFORM := @OPENJDK_MODULE_TARGET_PLATFORM@ @@ -135,12 +141,7 @@ RELEASE_FILE_LIBC := @RELEASE_FILE_LIBC@ SOURCE_DATE := @SOURCE_DATE@ ISO_8601_FORMAT_STRING := @ISO_8601_FORMAT_STRING@ - -ifneq ($(SOURCE_DATE), updated) - # For "updated" source date value, these are set in InitSupport.gmk - export SOURCE_DATE_EPOCH := $(SOURCE_DATE) - SOURCE_DATE_ISO_8601 := @SOURCE_DATE_ISO_8601@ -endif +SOURCE_DATE_ISO_8601_FIXED := @SOURCE_DATE_ISO_8601_FIXED@ LIBM := @LIBM@ LIBDL := @LIBDL@ @@ -149,23 +150,9 @@ LIBPTHREAD := @LIBPTHREAD@ WINENV_ROOT := @WINENV_ROOT@ WINENV_PREFIX := @WINENV_PREFIX@ -ifneq ($(findstring windows.wsl, @OPENJDK_BUILD_OS_ENV@), ) - # Tell WSL to convert PATH between linux and windows - export WSLENV := PATH/l -else ifeq (@OPENJDK_BUILD_OS_ENV@, windows.msys2) - # Prohibit msys2 from attempting any path wrangling - export MSYS2_ARG_CONV_EXCL := "*" -endif - # Save the original path before replacing it with the Visual Studio tools ORIGINAL_PATH := @ORIGINAL_PATH@ -ifeq (@TOOLCHAIN_TYPE@, microsoft) - # The Visual Studio toolchain needs the PATH to be adjusted to include - # Visual Studio tools. - export PATH := @TOOLCHAIN_PATH@:$(PATH) -endif - SYSROOT_CFLAGS := @SYSROOT_CFLAGS@ SYSROOT_LDFLAGS := @SYSROOT_LDFLAGS@ @@ -230,8 +217,6 @@ VERSION_NUMBER_FOUR_POSITIONS := @VERSION_NUMBER_FOUR_POSITIONS@ VERSION_STRING := @VERSION_STRING@ # The short version string, without trailing zeroes and just PRE, if present. VERSION_SHORT := @VERSION_SHORT@ -# The Java specification version. It usually equals the feature version number. -VERSION_SPECIFICATION := @VERSION_FEATURE@ # A GA version is defined by the PRE string being empty. Rather than testing for # that, this variable defines it with true/false. VERSION_IS_GA := @VERSION_IS_GA@ @@ -251,57 +236,6 @@ VERSION_DOCS_API_SINCE := @VERSION_DOCS_API_SINCE@ JDK_SOURCE_TARGET_VERSION := @JDK_SOURCE_TARGET_VERSION@ -# Convenience CFLAGS settings for passing version information into native programs. -VERSION_CFLAGS = \ - -DVERSION_FEATURE=$(VERSION_FEATURE) \ - -DVERSION_INTERIM=$(VERSION_INTERIM) \ - -DVERSION_UPDATE=$(VERSION_UPDATE) \ - -DVERSION_PATCH=$(VERSION_PATCH) \ - -DVERSION_EXTRA1=$(VERSION_EXTRA1) \ - -DVERSION_EXTRA2=$(VERSION_EXTRA2) \ - -DVERSION_EXTRA3=$(VERSION_EXTRA3) \ - -DVERSION_PRE='"$(VERSION_PRE)"' \ - -DVERSION_BUILD=$(VERSION_BUILD) \ - -DVERSION_OPT='"$(VERSION_OPT)"' \ - -DVERSION_NUMBER='"$(VERSION_NUMBER)"' \ - -DVERSION_STRING='"$(VERSION_STRING)"' \ - -DVERSION_SHORT='"$(VERSION_SHORT)"' \ - -DVERSION_SPECIFICATION='"$(VERSION_SPECIFICATION)"' \ - -DVERSION_DATE='"$(VERSION_DATE)"' \ - -DVENDOR_VERSION_STRING='"$(VENDOR_VERSION_STRING)"' \ - -DVERSION_CLASSFILE_MAJOR=$(VERSION_CLASSFILE_MAJOR) \ - -DVERSION_CLASSFILE_MINOR=$(VERSION_CLASSFILE_MINOR) \ - # - -ifneq ($(COMPANY_NAME), ) - # COMPANY_NAME is set to "N/A" in make/conf/branding.conf by default, - # but can be customized with the '--with-vendor-name' configure option. - # Only export "VENDOR" to the build if COMPANY_NAME contains a real value. - # Otherwise the default value for VENDOR, which is used to set the "java.vendor" - # and "java.vm.vendor" properties is hard-coded into the source code (i.e. in - # VersionProps.java.template in the jdk for "java.vendor" and - # vm_version.cpp in the VM for "java.vm.vendor") - ifneq ($(COMPANY_NAME), N/A) - VERSION_CFLAGS += -DVENDOR='"$(COMPANY_NAME)"' - endif -endif - -# Only export VENDOR_URL, VENDOR_URL_BUG and VENDOR_VM_URL_BUG to the build if -# they are not empty. Otherwise, default values which are defined in the sources -# will be used. -ifneq ($(VENDOR_URL), ) - VERSION_CFLAGS += -DVENDOR_URL='"$(VENDOR_URL)"' -endif -ifneq ($(VENDOR_URL_BUG), ) - VERSION_CFLAGS += -DVENDOR_URL_BUG='"$(VENDOR_URL_BUG)"' -endif -ifneq ($(VENDOR_URL_VM_BUG), ) - VERSION_CFLAGS += -DVENDOR_URL_VM_BUG='"$(VENDOR_URL_VM_BUG)"' -endif - -# Different naming strings generated from the above information. -RUNTIME_NAME = $(PRODUCT_NAME) $(PRODUCT_SUFFIX) - # How to compile the code: release, fastdebug or slowdebug DEBUG_LEVEL := @DEBUG_LEVEL@ HOTSPOT_DEBUG_LEVEL := @HOTSPOT_DEBUG_LEVEL@ @@ -343,22 +277,8 @@ ENABLE_FULL_DOCS := @ENABLE_FULL_DOCS@ # You can run $(JDK_OUTPUTDIR)/bin/java OUTPUTDIR := @OUTPUTDIR@ -# Colon left out to be able to override IMAGES_OUTPUTDIR for bootcycle-images -SUPPORT_OUTPUTDIR = $(OUTPUTDIR)/support -BUILDTOOLS_OUTPUTDIR = $(OUTPUTDIR)/buildtools -HOTSPOT_OUTPUTDIR = $(OUTPUTDIR)/hotspot -JDK_OUTPUTDIR = $(OUTPUTDIR)/jdk -IMAGES_OUTPUTDIR = $(OUTPUTDIR)/images -BUNDLES_OUTPUTDIR = $(OUTPUTDIR)/bundles -TESTMAKE_OUTPUTDIR = $(OUTPUTDIR)/test-make -MAKESUPPORT_OUTPUTDIR = $(OUTPUTDIR)/make-support - -JAVA_TMP_DIR = $(SUPPORT_OUTPUTDIR)/javatmp - -# This does not get overridden in a bootcycle build CONFIGURESUPPORT_OUTPUTDIR := @CONFIGURESUPPORT_OUTPUTDIR@ -BUILDJDK_OUTPUTDIR = $(OUTPUTDIR)/buildjdk BUILD_FAILURE_HANDLER := @BUILD_FAILURE_HANDLER@ @@ -388,21 +308,6 @@ BOOT_JDK := @BOOT_JDK@ EXTERNAL_BUILDJDK_PATH := @EXTERNAL_BUILDJDK_PATH@ -ifneq ($(EXTERNAL_BUILDJDK_PATH), ) - EXTERNAL_BUILDJDK := true - CREATE_BUILDJDK := false - BUILD_JDK := $(EXTERNAL_BUILDJDK_PATH) -else - EXTERNAL_BUILDJDK := false - ifeq ($(COMPILE_TYPE), cross) - CREATE_BUILDJDK := true - BUILD_JDK := $(BUILDJDK_OUTPUTDIR)/jdk - else - CREATE_BUILDJDK := false - BUILD_JDK := $(JDK_OUTPUTDIR) - endif -endif - # Whether the boot jdk jar supports --date=TIMESTAMP BOOT_JDK_JAR_SUPPORTS_DATE := @BOOT_JDK_JAR_SUPPORTS_DATE@ @@ -413,13 +318,10 @@ OLDEST_BOOT_JDK_VERSION := @OLDEST_BOOT_JDK_VERSION@ NUM_CORES := @NUM_CORES@ MEMORY_SIZE := @MEMORY_SIZE@ ENABLE_JAVAC_SERVER := @ENABLE_JAVAC_SERVER@ -# Store javac server synchronization files here, and -# the javac server log files. -JAVAC_SERVER_DIR = $(MAKESUPPORT_OUTPUTDIR)/javacservers # Number of parallel jobs to use for compilation -JOBS ?= @JOBS@ -TEST_JOBS ?= @TEST_JOBS@ +CONF_JOBS := @CONF_JOBS@ +CONF_TEST_JOBS := @CONF_TEST_JOBS@ # Default make target DEFAULT_MAKE_TARGET := @DEFAULT_MAKE_TARGET@ @@ -537,7 +439,7 @@ ADLC_LANGSTD_CXXFLAGS := @ADLC_LANGSTD_CXXFLAGS@ ADLC_LDFLAGS := @ADLC_LDFLAGS@ # Tools that potentially need to be cross compilation aware. -CC := @CCACHE@ @ICECC@ @CC@ +CC := @CC@ # CFLAGS used to compile the jdk native libraries (C-code) CFLAGS_JDKLIB := @CFLAGS_JDKLIB@ @@ -563,7 +465,7 @@ EXTRA_CXXFLAGS := @EXTRA_CXXFLAGS@ EXTRA_LDFLAGS := @EXTRA_LDFLAGS@ EXTRA_ASFLAGS := @EXTRA_ASFLAGS@ -CXX := @CCACHE@ @ICECC@ @CXX@ +CXX := @CXX@ CPP := @CPP@ @@ -594,8 +496,8 @@ LIBCXX := @LIBCXX@ # BUILD_CC/BUILD_LD is a compiler/linker that generates code that is runnable on the # build platform. -BUILD_CC := @BUILD_ICECC@ @BUILD_CC@ -BUILD_CXX := @BUILD_ICECC@ @BUILD_CXX@ +BUILD_CC := @BUILD_CC@ +BUILD_CXX := @BUILD_CXX@ BUILD_LD := @BUILD_LD@ BUILD_LDCXX := @BUILD_LDCXX@ BUILD_AS := @BUILD_AS@ @@ -646,77 +548,24 @@ OBJ_SUFFIX := @OBJ_SUFFIX@ STRIPFLAGS := @STRIPFLAGS@ -JAVA_FLAGS_TMPDIR := -Djava.io.tmpdir=$(JAVA_TMP_DIR) -JAVA_FLAGS := @JAVA_FLAGS@ $(JAVA_FLAGS_TMPDIR) +JAVA_FLAGS := @JAVA_FLAGS@ JAVA_FLAGS_BIG := @JAVA_FLAGS_BIG@ JAVA_FLAGS_SMALL := @JAVA_FLAGS_SMALL@ BUILD_JAVA_FLAGS_SMALL := @BUILD_JAVA_FLAGS_SMALL@ JAVA_TOOL_FLAGS_SMALL := @JAVA_TOOL_FLAGS_SMALL@ -# The *_CMD variables are defined separately to be easily overridden in bootcycle-spec.gmk -# for bootcycle-images build. Make sure to keep them in sync. Do not use the *_CMD -# versions of the variables directly. -JAVA_CMD := @JAVA@ -JAVAC_CMD := @JAVAC@ -JAVADOC_CMD := @JAVADOC@ -JAR_CMD := @JAR@ -JLINK_CMD := @FIXPATH@ $(BUILD_JDK)/bin/jlink -JMOD_CMD := @FIXPATH@ $(BUILD_JDK)/bin/jmod -# These variables are meant to be used. They are defined with = instead of := to make -# it possible to override only the *_CMD variables. -JAVA = $(JAVA_CMD) $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) -JAVA_SMALL = $(JAVA_CMD) $(JAVA_FLAGS_SMALL) $(JAVA_FLAGS) -JAVAC = $(JAVAC_CMD) -JAVADOC = $(JAVADOC_CMD) -JAR = $(JAR_CMD) -JLINK = $(JLINK_CMD) -JMOD = $(JMOD_CMD) +# Do not use the *_CMD versions of the variables directly. +JAVA_CMD := @JAVA_CMD@ +JAVAC_CMD := @JAVAC_CMD@ +JAVADOC_CMD := @JAVADOC_CMD@ +JAR_CMD := @JAR_CMD@ JTREG_JDK := @JTREG_JDK@ -JTREG_JAVA = @FIXPATH@ $(JTREG_JDK)/bin/java $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) -BUILD_JAVA_FLAGS := @BOOTCYCLE_JVM_ARGS_BIG@ -BUILD_JAVA = @FIXPATH@ $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS) -BUILD_JAVA_SMALL = @FIXPATH@ $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS_SMALL) -BUILD_JAVAC = @FIXPATH@ $(BUILD_JDK)/bin/javac -BUILD_JAR = @FIXPATH@ $(BUILD_JDK)/bin/jar +BOOTCYCLE_JVM_ARGS_BIG := @BOOTCYCLE_JVM_ARGS_BIG@ DOCS_REFERENCE_JAVADOC := @DOCS_REFERENCE_JAVADOC@ -# A file containing a way to uniquely identify the source code revision that -# the build was created from -SOURCE_REVISION_TRACKER := $(SUPPORT_OUTPUTDIR)/src-rev/source-revision-tracker - -# Interim langtools modules and arguments -INTERIM_LANGTOOLS_BASE_MODULES := java.compiler jdk.compiler jdk.internal.md jdk.javadoc -INTERIM_LANGTOOLS_MODULES := $(addsuffix .interim, $(INTERIM_LANGTOOLS_BASE_MODULES)) -INTERIM_LANGTOOLS_ADD_EXPORTS := \ - --add-exports java.base/sun.reflect.annotation=jdk.compiler.interim \ - --add-exports java.base/jdk.internal.jmod=jdk.compiler.interim \ - --add-exports java.base/jdk.internal.misc=jdk.compiler.interim \ - --add-exports java.base/sun.invoke.util=jdk.compiler.interim \ - --add-exports java.base/jdk.internal.javac=java.compiler.interim \ - --add-exports java.base/jdk.internal.javac=jdk.compiler.interim \ - --add-exports jdk.internal.opt/jdk.internal.opt=jdk.compiler.interim \ - --add-exports jdk.internal.opt/jdk.internal.opt=jdk.javadoc.interim \ - # -INTERIM_LANGTOOLS_MODULES_COMMA := $(strip $(subst $(SPACE),$(COMMA),$(strip \ - $(INTERIM_LANGTOOLS_MODULES)))) -INTERIM_LANGTOOLS_ARGS := \ - --limit-modules java.base,jdk.zipfs,$(INTERIM_LANGTOOLS_MODULES_COMMA) \ - --add-modules $(INTERIM_LANGTOOLS_MODULES_COMMA) \ - --module-path $(BUILDTOOLS_OUTPUTDIR)/interim_langtools_modules \ - --patch-module java.base=$(BUILDTOOLS_OUTPUTDIR)/gensrc/java.base.interim \ - $(INTERIM_LANGTOOLS_ADD_EXPORTS) \ - # -JAVAC_MAIN_CLASS := -m jdk.compiler.interim/com.sun.tools.javac.Main -JAVADOC_MAIN_CLASS := -m jdk.javadoc.interim/jdk.javadoc.internal.tool.Main - -# You run the new javac using the boot jdk with $(BOOT_JDK)/bin/java $(NEW_JAVAC) ... -# Use = assignment to be able to override in bootcycle-spec.gmk -NEW_JAVAC = $(INTERIM_LANGTOOLS_ARGS) $(JAVAC_MAIN_CLASS) -NEW_JAVADOC = $(INTERIM_LANGTOOLS_ARGS) $(JAVADOC_MAIN_CLASS) - JMOD_COMPRESS := @JMOD_COMPRESS@ JLINK_KEEP_PACKAGED_MODULES := @JLINK_KEEP_PACKAGED_MODULES@ JLINK_PRODUCE_LINKABLE_RUNTIME := @JLINK_PRODUCE_LINKABLE_RUNTIME@ @@ -729,8 +578,6 @@ AWK := @AWK@ BASENAME := @BASENAME@ CAT := @CAT@ CCACHE := @CCACHE@ -# CD is going away, but remains to cater for legacy makefiles. -CD := cd CHMOD := @CHMOD@ CMAKE := @CMAKE@ CODESIGN := @CODESIGN@ @@ -781,7 +628,6 @@ MT := @MT@ RC := @RC@ DUMPBIN := @DUMPBIN@ PATHTOOL := @PATHTOOL@ -WSLPATH := @WSLPATH@ LDD := @LDD@ OTOOL := @OTOOL@ READELF := @READELF@ @@ -846,110 +692,11 @@ OS_VERSION_MICRO := @OS_VERSION_MICRO@ # Arm SVE SVE_CFLAGS := @SVE_CFLAGS@ -# Images directory definitions -JDK_IMAGE_SUBDIR := jdk -JRE_IMAGE_SUBDIR := jre -JCOV_IMAGE_SUBDIR := jdk-jcov -STATIC_JDK_IMAGE_SUBDIR := static-jdk - -# Colon left out to be able to override output dir for bootcycle-images -JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_IMAGE_SUBDIR) -JRE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_IMAGE_SUBDIR) -STATIC_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(STATIC_JDK_IMAGE_SUBDIR) -JCOV_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JCOV_IMAGE_SUBDIR) - -# Test image, as above -TEST_IMAGE_SUBDIR := test -TEST_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(TEST_IMAGE_SUBDIR) - -# Symbols image -SYMBOLS_IMAGE_SUBDIR := symbols -SYMBOLS_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(SYMBOLS_IMAGE_SUBDIR) - -# Interim image -INTERIM_JMODS_DIR := $(SUPPORT_OUTPUTDIR)/interim-jmods -INTERIM_IMAGE_DIR := $(SUPPORT_OUTPUTDIR)/interim-image - -# Docs image -DOCS_JDK_IMAGE_SUBDIR := docs -DOCS_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JDK_IMAGE_SUBDIR) -DOCS_JAVASE_IMAGE_SUBDIR := docs-javase -DOCS_JAVASE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JAVASE_IMAGE_SUBDIR) -DOCS_REFERENCE_IMAGE_SUBDIR := docs-reference -DOCS_REFERENCE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_REFERENCE_IMAGE_SUBDIR) -# Output docs directly into image -DOCS_OUTPUTDIR := $(DOCS_JDK_IMAGE_DIR) - -# Static libs image -STATIC_LIBS_IMAGE_SUBDIR := static-libs -STATIC_LIBS_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_IMAGE_SUBDIR) - -# Graal static libs image -STATIC_LIBS_GRAAL_IMAGE_SUBDIR := static-libs-graal -STATIC_LIBS_GRAAL_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_GRAAL_IMAGE_SUBDIR) - -# Graal builder image -GRAAL_BUILDER_IMAGE_SUBDIR := graal-builder-jdk -GRAAL_BUILDER_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(GRAAL_BUILDER_IMAGE_SUBDIR) - -# Macosx bundles directory definitions -JDK_MACOSX_BUNDLE_SUBDIR := jdk-bundle -JRE_MACOSX_BUNDLE_SUBDIR := jre-bundle -JDK_MACOSX_BUNDLE_SUBDIR_SIGNED := jdk-bundle-signed -JRE_MACOSX_BUNDLE_SUBDIR_SIGNED := jre-bundle-signed -JDK_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR) -JRE_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR) -JDK_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR_SIGNED) -JRE_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR_SIGNED) -JDK_MACOSX_BUNDLE_TOP_SUBDIR = jdk-$(VERSION_NUMBER).jdk -JRE_MACOSX_BUNDLE_TOP_SUBDIR = jre-$(VERSION_NUMBER).jre -JDK_MACOSX_CONTENTS_SUBDIR = $(JDK_MACOSX_BUNDLE_TOP_SUBDIR)/Contents -JRE_MACOSX_CONTENTS_SUBDIR = $(JRE_MACOSX_BUNDLE_TOP_SUBDIR)/Contents -JDK_MACOSX_CONTENTS_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_CONTENTS_SUBDIR) -JRE_MACOSX_CONTENTS_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_CONTENTS_SUBDIR) -JDK_MACOSX_BUNDLE_TOP_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_BUNDLE_TOP_SUBDIR) -JRE_MACOSX_BUNDLE_TOP_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_BUNDLE_TOP_SUBDIR) - -# Bundle names -ifneq ($(VERSION_BUILD), ) - BASE_NAME := $(VERSION_SHORT)+$(VERSION_BUILD)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) -else - BASE_NAME := $(VERSION_SHORT)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) -endif - -ifeq ($(DEBUG_LEVEL), fastdebug) - DEBUG_PART := -debug -else ifneq ($(DEBUG_LEVEL), release) - DEBUG_PART := -$(DEBUG_LEVEL) -endif -ifeq ($(OPENJDK_TARGET_OS), windows) - JDK_BUNDLE_EXTENSION := zip -else - JDK_BUNDLE_EXTENSION := tar.gz -endif -JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) -JRE_BUNDLE_NAME := jre-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) -JDK_SYMBOLS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART)-symbols.tar.gz -TEST_DEMOS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests-demos$(DEBUG_PART).tar.gz -TEST_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests$(DEBUG_PART).tar.gz -DOCS_JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz -DOCS_JAVASE_BUNDLE_NAME := javase-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz -DOCS_REFERENCE_BUNDLE_NAME := jdk-reference-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz -STATIC_LIBS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs$(DEBUG_PART).tar.gz -STATIC_LIBS_GRAAL_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs-graal$(DEBUG_PART).tar.gz -STATIC_JDK_BUNDLE_NAME := static-jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) -JCOV_BUNDLE_NAME := jdk-jcov-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) - -JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_BUNDLE_NAME) -JRE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JRE_BUNDLE_NAME) -JDK_SYMBOLS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_SYMBOLS_BUNDLE_NAME) -TEST_DEMOS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_DEMOS_BUNDLE_NAME) -TEST_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_BUNDLE_NAME) -DOCS_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JDK_BUNDLE_NAME) -DOCS_JAVASE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JAVASE_BUNDLE_NAME) -DOCS_REFERENCE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_REFERENCE_BUNDLE_NAME) -STATIC_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(STATIC_JDK_BUNDLE_NAME) -JCOV_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JCOV_BUNDLE_NAME) +BUILD_ICECC := @BUILD_ICECC@ +ICECC := @ICECC@ +TOOLCHAIN_PATH := @TOOLCHAIN_PATH@ +LOCALE_USED := @LOCALE_USED@ +CLASSPATH := @CLASSPATH@ # This macro is called to allow inclusion of closed source counterparts. # Unless overridden in closed sources, it expands to nothing. @@ -960,4 +707,4 @@ define IncludeCustomExtension endef # Include the custom-spec.gmk file if it exists --include $(dir @SPEC@)/custom-spec.gmk +-include $(dir $(SPEC))/custom-spec.gmk diff --git a/make/common/CommonVars.gmk b/make/common/CommonVars.gmk new file mode 100644 index 00000000000..1ee6cc27481 --- /dev/null +++ b/make/common/CommonVars.gmk @@ -0,0 +1,442 @@ +# +# Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +include MakeIncludeStart.gmk +ifeq ($(INCLUDE), true) + +################################################################################ +# CommonVars include common variables and definitions used in multiple +# makefiles. +################################################################################ + +# Make sure all shell commands are executed with a proper locale +export LC_ALL := $(LOCALE_USED) + +# Make sure we override any local CLASSPATH variable +export CLASSPATH := $(CLASSPATH) + +# The default make arguments +MAKE_ARGS = $(MAKE_LOG_FLAGS) -r -R -I $(TOPDIR)/make/common SPEC=$(SPEC) \ + MAKE_LOG_FLAGS="$(MAKE_LOG_FLAGS)" $(MAKE_LOG_VARS) + +SHELL := $(BASH) $(BASH_ARGS) + +ifneq ($(SOURCE_DATE), updated) + # For "updated" source date value, these are set in InitSupport.gmk + export SOURCE_DATE_EPOCH := $(SOURCE_DATE) + SOURCE_DATE_ISO_8601 := $(SOURCE_DATE_ISO_8601_FIXED) +endif + +ifneq ($(findstring windows.wsl, $(OPENJDK_BUILD_OS_ENV)), ) + # Tell WSL to convert PATH between linux and windows + export WSLENV := PATH/l +else ifeq ($(OPENJDK_BUILD_OS_ENV), windows.msys2) + # Prohibit msys2 from attempting any path wrangling + export MSYS2_ARG_CONV_EXCL := "*" +endif + +ifeq ($(TOOLCHAIN_TYPE), microsoft) + # The Visual Studio toolchain needs the PATH to be adjusted to include + # Visual Studio tools. + export PATH := $(TOOLCHAIN_PATH):$(PATH) +endif + +# The Java specification version. It usually equals the feature version number. +VERSION_SPECIFICATION := $(VERSION_FEATURE) + +# Convenience CFLAGS settings for passing version information into native programs. +VERSION_CFLAGS = \ + -DVERSION_FEATURE=$(VERSION_FEATURE) \ + -DVERSION_INTERIM=$(VERSION_INTERIM) \ + -DVERSION_UPDATE=$(VERSION_UPDATE) \ + -DVERSION_PATCH=$(VERSION_PATCH) \ + -DVERSION_EXTRA1=$(VERSION_EXTRA1) \ + -DVERSION_EXTRA2=$(VERSION_EXTRA2) \ + -DVERSION_EXTRA3=$(VERSION_EXTRA3) \ + -DVERSION_PRE='"$(VERSION_PRE)"' \ + -DVERSION_BUILD=$(VERSION_BUILD) \ + -DVERSION_OPT='"$(VERSION_OPT)"' \ + -DVERSION_NUMBER='"$(VERSION_NUMBER)"' \ + -DVERSION_STRING='"$(VERSION_STRING)"' \ + -DVERSION_SHORT='"$(VERSION_SHORT)"' \ + -DVERSION_SPECIFICATION='"$(VERSION_SPECIFICATION)"' \ + -DVERSION_DATE='"$(VERSION_DATE)"' \ + -DVENDOR_VERSION_STRING='"$(VENDOR_VERSION_STRING)"' \ + -DVERSION_CLASSFILE_MAJOR=$(VERSION_CLASSFILE_MAJOR) \ + -DVERSION_CLASSFILE_MINOR=$(VERSION_CLASSFILE_MINOR) \ + # + +ifneq ($(COMPANY_NAME), ) + # COMPANY_NAME is set to "N/A" in make/conf/branding.conf by default, + # but can be customized with the '--with-vendor-name' configure option. + # Only export "VENDOR" to the build if COMPANY_NAME contains a real value. + # Otherwise the default value for VENDOR, which is used to set the "java.vendor" + # and "java.vm.vendor" properties is hard-coded into the source code (i.e. in + # VersionProps.java.template in the jdk for "java.vendor" and + # vm_version.cpp in the VM for "java.vm.vendor") + ifneq ($(COMPANY_NAME), N/A) + VERSION_CFLAGS += -DVENDOR='"$(COMPANY_NAME)"' + endif +endif + +# Only export VENDOR_URL, VENDOR_URL_BUG and VENDOR_VM_URL_BUG to the build if +# they are not empty. Otherwise, default values which are defined in the sources +# will be used. +ifneq ($(VENDOR_URL), ) + VERSION_CFLAGS += -DVENDOR_URL='"$(VENDOR_URL)"' +endif +ifneq ($(VENDOR_URL_BUG), ) + VERSION_CFLAGS += -DVENDOR_URL_BUG='"$(VENDOR_URL_BUG)"' +endif +ifneq ($(VENDOR_URL_VM_BUG), ) + VERSION_CFLAGS += -DVENDOR_URL_VM_BUG='"$(VENDOR_URL_VM_BUG)"' +endif + +# Different naming strings generated from the above information. +RUNTIME_NAME = $(PRODUCT_NAME) $(PRODUCT_SUFFIX) + +# Colon left out to be able to override IMAGES_OUTPUTDIR for bootcycle-images +SUPPORT_OUTPUTDIR = $(OUTPUTDIR)/support +BUILDTOOLS_OUTPUTDIR = $(OUTPUTDIR)/buildtools + +HOTSPOT_OUTPUTDIR = $(OUTPUTDIR)/hotspot +JDK_OUTPUTDIR = $(OUTPUTDIR)/jdk +IMAGES_OUTPUTDIR = $(OUTPUTDIR)/images +BUNDLES_OUTPUTDIR = $(OUTPUTDIR)/bundles +TESTMAKE_OUTPUTDIR = $(OUTPUTDIR)/test-make +MAKESUPPORT_OUTPUTDIR = $(OUTPUTDIR)/make-support + +JAVA_TMP_DIR = $(SUPPORT_OUTPUTDIR)/javatmp + +BUILDJDK_OUTPUTDIR = $(OUTPUTDIR)/buildjdk + +ifneq ($(EXTERNAL_BUILDJDK_PATH), ) + EXTERNAL_BUILDJDK := true + CREATE_BUILDJDK := false + BUILD_JDK := $(EXTERNAL_BUILDJDK_PATH) +else + EXTERNAL_BUILDJDK := false + ifeq ($(COMPILE_TYPE), cross) + CREATE_BUILDJDK := true + BUILD_JDK := $(BUILDJDK_OUTPUTDIR)/jdk + else + CREATE_BUILDJDK := false + BUILD_JDK := $(JDK_OUTPUTDIR) + endif +endif + +# Store javac server synchronization files here, and +# the javac server log files. +JAVAC_SERVER_DIR = $(MAKESUPPORT_OUTPUTDIR)/javacservers + +# Number of parallel jobs to use for compilation +JOBS ?= $(CONF_JOBS) +TEST_JOBS ?= $(CONF_TEST_JOBS) + +# Tools that potentially need to be cross compilation aware. +CC := $(CCACHE) $(ICECC) $(CC) + +CXX := $(CCACHE) $(ICECC) $(CXX) + +# BUILD_CC/BUILD_LD is a compiler/linker that generates code that is runnable on the +# build platform. +BUILD_CC := $(BUILD_ICECC) $(BUILD_CC) +BUILD_CXX := $(BUILD_ICECC) $(BUILD_CXX) + +JAVA_FLAGS_TMPDIR := -Djava.io.tmpdir=$(JAVA_TMP_DIR) +JAVA_FLAGS := $(JAVA_FLAGS) $(JAVA_FLAGS_TMPDIR) + +JLINK_CMD := $(FIXPATH) $(BUILD_JDK)/bin/jlink +JMOD_CMD := $(FIXPATH) $(BUILD_JDK)/bin/jmod + +# These variables are meant to be used. They are defined with = instead of := to make +# it possible to override only the *_CMD variables. +JAVA = $(JAVA_CMD) $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) +JAVA_SMALL = $(JAVA_CMD) $(JAVA_FLAGS_SMALL) $(JAVA_FLAGS) +JAVAC = $(JAVAC_CMD) +JAVADOC = $(JAVADOC_CMD) +JAR = $(JAR_CMD) +JLINK = $(JLINK_CMD) +JMOD = $(JMOD_CMD) + +JTREG_JAVA = $(FIXPATH) $(JTREG_JDK)/bin/java $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) + +BUILD_JAVA_FLAGS := $(BOOTCYCLE_JVM_ARGS_BIG) +BUILD_JAVA = $(FIXPATH) $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS) +BUILD_JAVA_SMALL = $(FIXPATH) $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS_SMALL) +BUILD_JAVAC = $(FIXPATH) $(BUILD_JDK)/bin/javac +BUILD_JAR = $(FIXPATH) $(BUILD_JDK)/bin/jar + +# A file containing a way to uniquely identify the source code revision that +# the build was created from +SOURCE_REVISION_TRACKER := $(SUPPORT_OUTPUTDIR)/src-rev/source-revision-tracker + +# Interim langtools modules and arguments +INTERIM_LANGTOOLS_BASE_MODULES := java.compiler jdk.compiler jdk.internal.md jdk.javadoc +INTERIM_LANGTOOLS_MODULES := $(addsuffix .interim, $(INTERIM_LANGTOOLS_BASE_MODULES)) +INTERIM_LANGTOOLS_ADD_EXPORTS := \ + --add-exports java.base/sun.reflect.annotation=jdk.compiler.interim \ + --add-exports java.base/jdk.internal.jmod=jdk.compiler.interim \ + --add-exports java.base/jdk.internal.misc=jdk.compiler.interim \ + --add-exports java.base/sun.invoke.util=jdk.compiler.interim \ + --add-exports java.base/jdk.internal.javac=java.compiler.interim \ + --add-exports java.base/jdk.internal.javac=jdk.compiler.interim \ + --add-exports jdk.internal.opt/jdk.internal.opt=jdk.compiler.interim \ + --add-exports jdk.internal.opt/jdk.internal.opt=jdk.javadoc.interim \ + # +INTERIM_LANGTOOLS_MODULES_COMMA := $(strip $(subst $(SPACE),$(COMMA),$(strip \ + $(INTERIM_LANGTOOLS_MODULES)))) +INTERIM_LANGTOOLS_ARGS := \ + --limit-modules java.base,jdk.zipfs,$(INTERIM_LANGTOOLS_MODULES_COMMA) \ + --add-modules $(INTERIM_LANGTOOLS_MODULES_COMMA) \ + --module-path $(BUILDTOOLS_OUTPUTDIR)/interim_langtools_modules \ + --patch-module java.base=$(BUILDTOOLS_OUTPUTDIR)/gensrc/java.base.interim \ + $(INTERIM_LANGTOOLS_ADD_EXPORTS) \ + # + +JAVADOC_MAIN_CLASS := -m jdk.javadoc.interim/jdk.javadoc.internal.tool.Main +# Use = assignment to pick up overridden INTERIM_LANGTOOLS_ARGS in bootcycle builds +NEW_JAVADOC = $(INTERIM_LANGTOOLS_ARGS) $(JAVADOC_MAIN_CLASS) + +# CD is going away, but remains to cater for legacy makefiles. +CD := cd + +# Images directory definitions +JDK_IMAGE_SUBDIR := jdk +JRE_IMAGE_SUBDIR := jre +JCOV_IMAGE_SUBDIR := jdk-jcov +STATIC_JDK_IMAGE_SUBDIR := static-jdk + +# Colon left out to be able to override output dir for bootcycle-images +ifeq ($(JDK_IMAGE_DIR), ) + JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_IMAGE_SUBDIR) +endif +JRE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_IMAGE_SUBDIR) +STATIC_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(STATIC_JDK_IMAGE_SUBDIR) +ifeq ($(JCOV_IMAGE_DIR), ) + JCOV_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JCOV_IMAGE_SUBDIR) +endif +# Test image, as above +TEST_IMAGE_SUBDIR := test +ifeq ($(TEST_IMAGE_DIR), ) + TEST_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(TEST_IMAGE_SUBDIR) +endif + +# Symbols image +SYMBOLS_IMAGE_SUBDIR := symbols +ifeq ($(SYMBOLS_IMAGE_DIR), ) + SYMBOLS_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(SYMBOLS_IMAGE_SUBDIR) +endif + +# Interim image +INTERIM_JMODS_DIR := $(SUPPORT_OUTPUTDIR)/interim-jmods +INTERIM_IMAGE_DIR := $(SUPPORT_OUTPUTDIR)/interim-image + +# Docs image +DOCS_JDK_IMAGE_SUBDIR := docs +DOCS_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JDK_IMAGE_SUBDIR) +DOCS_JAVASE_IMAGE_SUBDIR := docs-javase +DOCS_JAVASE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JAVASE_IMAGE_SUBDIR) +DOCS_REFERENCE_IMAGE_SUBDIR := docs-reference +DOCS_REFERENCE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_REFERENCE_IMAGE_SUBDIR) +# Output docs directly into image +DOCS_OUTPUTDIR := $(DOCS_JDK_IMAGE_DIR) + +# Static libs image +STATIC_LIBS_IMAGE_SUBDIR := static-libs +STATIC_LIBS_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_IMAGE_SUBDIR) + +# Graal static libs image +STATIC_LIBS_GRAAL_IMAGE_SUBDIR := static-libs-graal +STATIC_LIBS_GRAAL_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_GRAAL_IMAGE_SUBDIR) + +# Graal builder image +GRAAL_BUILDER_IMAGE_SUBDIR := graal-builder-jdk +GRAAL_BUILDER_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(GRAAL_BUILDER_IMAGE_SUBDIR) + +# Macosx bundles directory definitions +JDK_MACOSX_BUNDLE_SUBDIR := jdk-bundle +JRE_MACOSX_BUNDLE_SUBDIR := jre-bundle +JDK_MACOSX_BUNDLE_SUBDIR_SIGNED := jdk-bundle-signed +JRE_MACOSX_BUNDLE_SUBDIR_SIGNED := jre-bundle-signed +JDK_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR) +JRE_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR) +JDK_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR_SIGNED) +JRE_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR_SIGNED) +JDK_MACOSX_BUNDLE_TOP_SUBDIR = jdk-$(VERSION_NUMBER).jdk +JRE_MACOSX_BUNDLE_TOP_SUBDIR = jre-$(VERSION_NUMBER).jre +JDK_MACOSX_CONTENTS_SUBDIR = $(JDK_MACOSX_BUNDLE_TOP_SUBDIR)/Contents +JRE_MACOSX_CONTENTS_SUBDIR = $(JRE_MACOSX_BUNDLE_TOP_SUBDIR)/Contents +JDK_MACOSX_CONTENTS_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_CONTENTS_SUBDIR) +JRE_MACOSX_CONTENTS_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_CONTENTS_SUBDIR) +JDK_MACOSX_BUNDLE_TOP_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_BUNDLE_TOP_SUBDIR) +JRE_MACOSX_BUNDLE_TOP_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_BUNDLE_TOP_SUBDIR) + +# Bundle names +ifneq ($(VERSION_BUILD), ) + BASE_NAME := $(VERSION_SHORT)+$(VERSION_BUILD)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) +else + BASE_NAME := $(VERSION_SHORT)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) +endif + +ifeq ($(DEBUG_LEVEL), fastdebug) + DEBUG_PART := -debug +else ifneq ($(DEBUG_LEVEL), release) + DEBUG_PART := -$(DEBUG_LEVEL) +endif +ifeq ($(OPENJDK_TARGET_OS), windows) + JDK_BUNDLE_EXTENSION := zip +else + JDK_BUNDLE_EXTENSION := tar.gz +endif +JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) +JRE_BUNDLE_NAME := jre-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) +JDK_SYMBOLS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART)-symbols.tar.gz +TEST_DEMOS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests-demos$(DEBUG_PART).tar.gz +TEST_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests$(DEBUG_PART).tar.gz +DOCS_JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz +DOCS_JAVASE_BUNDLE_NAME := javase-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz +DOCS_REFERENCE_BUNDLE_NAME := jdk-reference-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz +STATIC_LIBS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs$(DEBUG_PART).tar.gz +STATIC_LIBS_GRAAL_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs-graal$(DEBUG_PART).tar.gz +STATIC_JDK_BUNDLE_NAME := static-jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) +JCOV_BUNDLE_NAME := jdk-jcov-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) + +JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_BUNDLE_NAME) +JRE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JRE_BUNDLE_NAME) +JDK_SYMBOLS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_SYMBOLS_BUNDLE_NAME) +TEST_DEMOS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_DEMOS_BUNDLE_NAME) +TEST_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_BUNDLE_NAME) +DOCS_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JDK_BUNDLE_NAME) +DOCS_JAVASE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JAVASE_BUNDLE_NAME) +DOCS_REFERENCE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_REFERENCE_BUNDLE_NAME) +STATIC_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(STATIC_JDK_BUNDLE_NAME) +JCOV_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JCOV_BUNDLE_NAME) + +ifeq ($(IS_BUILD_JDK_SPEC), true) + CC := $(BUILD_CC) + CXX := $(BUILD_CXX) + # Ideally this should be probed by configure but that is tricky to implement, + # and this should work in most cases. + CPP := $(BUILD_CC) -E + LD := $(BUILD_LD) + LDCXX := $(BUILD_LDCXX) + AS := $(BUILD_AS) + NM := $(BUILD_NM) + AR := $(BUILD_AR) + LIB := $(BUILD_LIB) + OBJCOPY := $(BUILD_OBJCOPY) + STRIP := $(BUILD_STRIP) + SYSROOT_CFLAGS := $(BUILD_SYSROOT_CFLAGS) + SYSROOT_LDFLAGS := $(BUILD_SYSROOT_LDFLAGS) + + # These directories should not be moved to BUILDJDK_OUTPUTDIR + HOTSPOT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(HOTSPOT_OUTPUTDIR)) + BUILDTOOLS_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(BUILDTOOLS_OUTPUTDIR)) + SUPPORT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(SUPPORT_OUTPUTDIR)) + JDK_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(JDK_OUTPUTDIR)) + IMAGES_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(IMAGES_OUTPUTDIR)) + + OPENJDK_TARGET_CPU := $(OPENJDK_BUILD_CPU) + OPENJDK_TARGET_CPU_ARCH := $(OPENJDK_BUILD_CPU_ARCH) + OPENJDK_TARGET_CPU_BITS := $(OPENJDK_BUILD_CPU_BITS) + OPENJDK_TARGET_CPU_ENDIAN := $(OPENJDK_BUILD_CPU_ENDIAN) + OPENJDK_TARGET_CPU_LEGACY := $(OPENJDK_BUILD_CPU_LEGACY) + OPENJDK_TARGET_LIBC := $(OPENJDK_BUILD_LIBC) + OPENJDK_TARGET_OS_INCLUDE_SUBDIR := $(OPENJDK_BUILD_OS_INCLUDE_SUBDIR) + + HOTSPOT_TARGET_OS := $(HOTSPOT_BUILD_OS) + HOTSPOT_TARGET_OS_TYPE := $(HOTSPOT_BUILD_OS_TYPE) + HOTSPOT_TARGET_CPU := $(HOTSPOT_BUILD_CPU) + HOTSPOT_TARGET_CPU_ARCH := $(HOTSPOT_BUILD_CPU_ARCH) + HOTSPOT_TARGET_CPU_DEFINE := $(HOTSPOT_BUILD_CPU_DEFINE) + HOTSPOT_TARGET_LIBC := $(HOTSPOT_BUILD_LIBC) + + CFLAGS_JDKLIB := $(OPENJDK_BUILD_CFLAGS_JDKLIB) + CXXFLAGS_JDKLIB := $(OPENJDK_BUILD_CXXFLAGS_JDKLIB) + LDFLAGS_JDKLIB := $(OPENJDK_BUILD_LDFLAGS_JDKLIB) + CFLAGS_JDKEXE := $(OPENJDK_BUILD_CFLAGS_JDKEXE) + CXXFLAGS_JDKEXE := $(OPENJDK_BUILD_CXXFLAGS_JDKEXE) + LDFLAGS_JDKEXE := $(OPENJDK_BUILD_LDFLAGS_JDKEXE) + + JVM_CFLAGS := $(OPENJDK_BUILD_JVM_CFLAGS) + JVM_LDFLAGS := $(OPENJDK_BUILD_JVM_LDFLAGS) + JVM_ASFLAGS := $(OPENJDK_BUILD_JVM_ASFLAGS) + JVM_LIBS := $(OPENJDK_BUILD_JVM_LIBS) + + FDLIBM_CFLAGS := $(OPENJDK_BUILD_FDLIBM_CFLAGS) + + INTERIM_LANGTOOLS_ARGS := $(subst $(OUTPUTDIR),$(BUILDJDK_OUTPUTDIR),$(INTERIM_LANGTOOLS_ARGS)) + + # The compiler for the build platform is likely not warning compatible with the official + # compiler. + WARNINGS_AS_ERRORS := false + DISABLE_WARNING_PREFIX := $(BUILD_CC_DISABLE_WARNING_PREFIX) + + # Save speed and disk space by not enabling debug symbols for the buildjdk + ENABLE_DEBUG_SYMBOLS := false + + JVM_VARIANTS := server + JVM_VARIANT_MAIN := server + JVM_FEATURES_server := cds compiler1 compiler2 g1gc serialgc + + # Some users still set EXTRA_*FLAGS on the make command line. Must + # make sure to override that when building buildjdk. + override EXTRA_CFLAGS := + override EXTRA_CXXFLAGS := + override EXTRA_LDFLAGS := + + # hsdis is not needed + HSDIS_BACKEND := none + ENABLE_HSDIS_BUNDLING := false +endif + +ifeq ($(IS_BOOTCYCLE_JDK_SPEC), true) + # Override specific values to do a boot cycle build + + # Use a different Boot JDK + BOOT_JDK := $(JDK_IMAGE_DIR) + + # The bootcycle build has a different output directory + OLD_OUTPUTDIR := $(OUTPUTDIR) + OUTPUTDIR := $(OLD_OUTPUTDIR)/bootcycle-build + # No spaces in patsubst to avoid leading space in variable + JAVAC_SERVER_DIR := $(patsubst $(OLD_OUTPUTDIR)%,$(OUTPUTDIR)%,$(JAVAC_SERVER_DIR)) + + JAVA_CMD := $(FIXPATH) $(BOOT_JDK)/bin/java + JAVAC_CMD := $(FIXPATH) $(BOOT_JDK)/bin/javac + JAR_CMD := $(FIXPATH) $(BOOT_JDK)/bin/jar + # The bootcycle JVM arguments may differ from the original boot jdk. + JAVA_FLAGS_BIG := $(BOOTCYCLE_JVM_ARGS_BIG) + # Any CDS settings generated for the bootjdk are invalid in the bootcycle build. + # By filtering out those JVM args, the bootcycle JVM will use its default + # settings for CDS. + JAVA_FLAGS := $(filter-out -XX:SharedArchiveFile% -Xshare%, $(JAVA_FLAGS)) +endif + +################################################################################ + +include MakeIncludeEnd.gmk +endif # include guard diff --git a/make/common/MakeBase.gmk b/make/common/MakeBase.gmk index 97ef88932cb..2bc5d562924 100644 --- a/make/common/MakeBase.gmk +++ b/make/common/MakeBase.gmk @@ -75,6 +75,7 @@ endif # least for now. # Utils.gmk must be included before FileUtils.gmk, since it uses some of the # basic utility functions there. +include $(TOPDIR)/make/common/CommonVars.gmk include $(TOPDIR)/make/common/Utils.gmk include $(TOPDIR)/make/common/FileUtils.gmk From 32ab0dbc0b8170ecd168dbb7c3f1be69dfa5299b Mon Sep 17 00:00:00 2001 From: Magnus Ihse Bursie Date: Thu, 25 Sep 2025 17:44:55 +0000 Subject: [PATCH 242/556] 8368674: Incremental builds keep rebuilding interim jmod Reviewed-by: cstein, erikj --- make/common/Execute.gmk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make/common/Execute.gmk b/make/common/Execute.gmk index 0311c4ecba1..c195510151f 100644 --- a/make/common/Execute.gmk +++ b/make/common/Execute.gmk @@ -149,7 +149,7 @@ define SetupExecuteBody endif $1_VARDEPS := $$($1_COMMAND) $$($1_PRE_COMMAND) $$($1_POST_COMMAND) - $1_VARDEPS_FILE := $$(call DependOnVariable, $1_VARDEPS) + $1_VARDEPS_FILE := $$(call DependOnVariable, $1_VARDEPS, $$($1_BASE)_exec.vardeps) ifneq ($$($1_PRE_COMMAND), ) From 5c596e2a9599e1e0eb9ec845f1b6e0e7b59f186a Mon Sep 17 00:00:00 2001 From: Valerie Peng Date: Thu, 25 Sep 2025 18:10:58 +0000 Subject: [PATCH 243/556] 8360463: Ambiguity in Cipher.getInstance() specification between NoSuchAlgorithmException and NoSuchPaddingException Reviewed-by: mullan --- .../share/classes/javax/crypto/Cipher.java | 48 +++++++++++-------- .../unittest/ChaCha20CipherUnitTest.java | 47 ++++++++++-------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/java.base/share/classes/javax/crypto/Cipher.java b/src/java.base/share/classes/javax/crypto/Cipher.java index 902c3998fbe..b22b8cf5483 100644 --- a/src/java.base/share/classes/javax/crypto/Cipher.java +++ b/src/java.base/share/classes/javax/crypto/Cipher.java @@ -515,12 +515,12 @@ public class Cipher { * transformation * * @throws NoSuchAlgorithmException if {@code transformation} - * is {@code null}, empty, in an invalid format, - * or if no provider supports a {@code CipherSpi} - * implementation for the specified algorithm + * is {@code null}, empty or in an invalid format; + * or if a {@code CipherSpi} implementation is not found or + * is found but does not support the mode * - * @throws NoSuchPaddingException if {@code transformation} - * contains a padding scheme that is not available + * @throws NoSuchPaddingException if a {@code CipherSpi} implementation + * is found but does not support the padding scheme * * @see java.security.Provider */ @@ -573,8 +573,12 @@ public class Cipher { failure = e; } } + if (failure instanceof NoSuchPaddingException nspe) { + throw nspe; + } throw new NoSuchAlgorithmException - ("Cannot find any provider supporting " + transformation, failure); + ("Cannot find any provider supporting " + transformation, + failure); } /** @@ -582,8 +586,8 @@ public class Cipher { * transformation. * *

        A new {@code Cipher} object encapsulating the - * {@code CipherSpi} implementation from the specified provider - * is returned. The specified provider must be registered + * {@code CipherSpi} implementation from the specified {@code provider} + * is returned. The specified {@code provider} must be registered * in the security provider list. * *

        Note that the list of registered providers may be retrieved via @@ -625,15 +629,16 @@ public class Cipher { * is {@code null} or empty * * @throws NoSuchAlgorithmException if {@code transformation} - * is {@code null}, empty, in an invalid format, - * or if a {@code CipherSpi} implementation for the - * specified algorithm is not available from the specified - * provider + * is {@code null}, empty or in an invalid format; + * or if a {@code CipherSpi} implementation from the specified + * {@code provider} is not found or is found but does not support + * the mode * - * @throws NoSuchPaddingException if {@code transformation} - * contains a padding scheme that is not available + * @throws NoSuchPaddingException if a {@code CipherSpi} implementation + * from the specified {@code provider} is found but does not + * support the padding scheme * - * @throws NoSuchProviderException if the specified provider is not + * @throws NoSuchProviderException if the specified {@code provider} is not * registered in the security provider list * * @see java.security.Provider @@ -706,13 +711,14 @@ public class Cipher { * is {@code null} * * @throws NoSuchAlgorithmException if {@code transformation} - * is {@code null}, empty, in an invalid format, - * or if a {@code CipherSpi} implementation for the - * specified algorithm is not available from the specified - * {@code provider} object + * is {@code null}, empty or in an invalid format; + * or if a {@code CipherSpi} implementation from the specified + * {@code provider} is not found or is found but does not support + * the mode * - * @throws NoSuchPaddingException if {@code transformation} - * contains a padding scheme that is not available + * @throws NoSuchPaddingException if a {@code CipherSpi} implementation + * from the specified {@code provider} is found but does not + * support the padding scheme * * @see java.security.Provider */ diff --git a/test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/unittest/ChaCha20CipherUnitTest.java b/test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/unittest/ChaCha20CipherUnitTest.java index 5216f48d5c3..892f3300d65 100644 --- a/test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/unittest/ChaCha20CipherUnitTest.java +++ b/test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/unittest/ChaCha20CipherUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,7 +23,7 @@ /* * @test - * @bug 8153029 + * @bug 8153029 8360463 * @library /test/lib * @run main ChaCha20CipherUnitTest * @summary Unit test for com.sun.crypto.provider.ChaCha20Cipher. @@ -38,6 +38,7 @@ import java.util.Arrays; import java.util.HexFormat; import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.ChaCha20ParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; @@ -66,32 +67,36 @@ public class ChaCha20CipherUnitTest { private static void testTransformations() throws Exception { System.out.println("== transformations =="); - checkTransformation("ChaCha20", true); - checkTransformation("ChaCha20/None/NoPadding", true); - checkTransformation("ChaCha20-Poly1305", true); - checkTransformation("ChaCha20-Poly1305/None/NoPadding", true); + Class NSAE = NoSuchAlgorithmException.class; + Class NSPE = NoSuchPaddingException.class; - checkTransformation("ChaCha20/ECB/NoPadding", false); - checkTransformation("ChaCha20/None/PKCS5Padding", false); - checkTransformation("ChaCha20-Poly1305/ECB/NoPadding", false); - checkTransformation("ChaCha20-Poly1305/None/PKCS5Padding", false); + checkTransformation("ChaCha20", null); + checkTransformation("ChaCha20/None/NoPadding", null); + checkTransformation("ChaCha20-Poly1305", null); + checkTransformation("ChaCha20-Poly1305/None/NoPadding", null); + checkTransformation("ChaCha20/ECB/NoPadding", NSAE); + checkTransformation("ChaCha20/None/PKCS5Padding", NSPE); + checkTransformation("ChaCha20-Poly1305/ECB/NoPadding", NSAE); + checkTransformation("ChaCha20-Poly1305/None/PKCS5Padding", NSPE); } - private static void checkTransformation(String transformation, - boolean expected) throws Exception { + private static void checkTransformation(String transformation, Class exCls) + throws Exception { try { - Cipher.getInstance(transformation); - if (!expected) { - throw new RuntimeException( - "Unexpected transformation: " + transformation); + Cipher.getInstance(transformation, + System.getProperty("test.provider.name", "SunJCE")); + if (exCls != null) { + throw new RuntimeException("Expected Exception not thrown: " + + exCls); } else { - System.out.println("Expected transformation: " + transformation); + System.out.println(transformation + ": pass"); } - } catch (NoSuchAlgorithmException e) { - if (!expected) { - System.out.println("Unexpected transformation: " + transformation); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + if (e.getClass() != exCls) { + throw new RuntimeException("Unexpected Exception", e); } else { - throw new RuntimeException("Unexpected fail: " + transformation, e); + System.out.println(transformation + ": got expected " + + exCls.getName()); } } } From 80cb0ead502ae439660f2a3bbab42df4da39d9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jeli=C5=84ski?= Date: Thu, 25 Sep 2025 18:17:19 +0000 Subject: [PATCH 244/556] 8367133: DTLS: fragmentation of Finished message results in handshake failure Reviewed-by: jnimeh --- .../sun/security/ssl/DTLSInputRecord.java | 9 ++- .../net/ssl/DTLS/FragmentedFinished.java | 72 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 test/jdk/javax/net/ssl/DTLS/FragmentedFinished.java diff --git a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java index e0196f3009c..101a42a5407 100644 --- a/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java +++ b/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -801,8 +801,11 @@ final class DTLSInputRecord extends InputRecord implements DTLSRecord { // buffer this fragment if (hsf.handshakeType == SSLHandshake.FINISHED.id) { - // Need no status update. - bufferedFragments.add(hsf); + // Make sure it's not a retransmitted message + if (hsf.recordEpoch > handshakeEpoch) { + bufferedFragments.add(hsf); + flightIsReady = holes.isEmpty(); + } } else { bufferFragment(hsf); } diff --git a/test/jdk/javax/net/ssl/DTLS/FragmentedFinished.java b/test/jdk/javax/net/ssl/DTLS/FragmentedFinished.java new file mode 100644 index 00000000000..2b6fd16005c --- /dev/null +++ b/test/jdk/javax/net/ssl/DTLS/FragmentedFinished.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +// SunJSSE does not support dynamic system properties, no way to re-use +// system properties in samevm/agentvm mode. + +/* + * @test + * @bug 8367133 + * @summary Verify that handshake succeeds when Finished message is fragmented + * @modules java.base/sun.security.util + * @library /test/lib + * @build DTLSOverDatagram + * @run main/othervm FragmentedFinished + */ + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import java.net.DatagramPacket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; + +public class FragmentedFinished extends DTLSOverDatagram { + private SSLEngine serverSSLEngine; + public static void main(String[] args) throws Exception { + FragmentedFinished testCase = new FragmentedFinished(); + testCase.runTest(testCase); + } + + @Override + SSLEngine createSSLEngine(boolean isClient) throws Exception { + SSLEngine sslEngine = super.createSSLEngine(isClient); + if (!isClient) { + serverSSLEngine = sslEngine; + } + return sslEngine; + } + + @Override + DatagramPacket createHandshakePacket(byte[] ba, SocketAddress socketAddr) { + if (ba.length < 30) { // detect ChangeCipherSpec + // Reduce the maximumPacketSize to force fragmentation + // of the Finished message + SSLParameters params = serverSSLEngine.getSSLParameters(); + params.setMaximumPacketSize(53); + serverSSLEngine.setSSLParameters(params); + } + + return super.createHandshakePacket(ba, socketAddr); + } +} From a48538dd6379d606b75b849dd899413af76a068c Mon Sep 17 00:00:00 2001 From: Johannes Graham Date: Thu, 25 Sep 2025 18:23:13 +0000 Subject: [PATCH 245/556] 8367324: Avoid redundant parsing when formatting with DigitList Reviewed-by: jlu, rgiulietti --- .../share/classes/java/math/BigInteger.java | 10 +- .../share/classes/java/text/DigitList.java | 102 ++++-------------- .../jdk/internal/math/FloatingDecimal.java | 6 +- .../text/Format/DecimalFormat/CloneTest.java | 8 +- .../bench/java/text/DefFormatterBench.java | 40 ++++++- 5 files changed, 72 insertions(+), 94 deletions(-) diff --git a/src/java.base/share/classes/java/math/BigInteger.java b/src/java.base/share/classes/java/math/BigInteger.java index 21f8598266f..6253adffb2b 100644 --- a/src/java.base/share/classes/java/math/BigInteger.java +++ b/src/java.base/share/classes/java/math/BigInteger.java @@ -4180,6 +4180,10 @@ public class BigInteger extends Number implements Comparable { if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) radix = 10; + if (fitsIntoLong()) { + return Long.toString(longValue(), radix); + } + BigInteger abs = this.abs(); // Ensure buffer capacity sufficient to contain string representation @@ -5121,12 +5125,16 @@ public class BigInteger extends Number implements Comparable { * @since 1.8 */ public long longValueExact() { - if (mag.length <= 2 && bitLength() < Long.SIZE) + if (fitsIntoLong()) return longValue(); throw new ArithmeticException("BigInteger out of long range"); } + private boolean fitsIntoLong() { + return mag.length <= 2 && bitLength() < Long.SIZE; + } + /** * Converts this {@code BigInteger} to an {@code int}, checking * for lost information. If the value of this {@code BigInteger} diff --git a/src/java.base/share/classes/java/text/DigitList.java b/src/java.base/share/classes/java/text/DigitList.java index 2895126f93b..ce098bd8800 100644 --- a/src/java.base/share/classes/java/text/DigitList.java +++ b/src/java.base/share/classes/java/text/DigitList.java @@ -46,6 +46,7 @@ import java.nio.charset.StandardCharsets; import jdk.internal.access.SharedSecrets; import jdk.internal.math.FloatingDecimal; import jdk.internal.util.ArraysSupport; +import jdk.internal.vm.annotation.Stable; /** * Digit List. Private to DecimalFormat. @@ -108,7 +109,6 @@ final class DigitList implements Cloneable { public int count = 0; public byte[] digits = new byte[MAX_COUNT]; - private byte[] data; private RoundingMode roundingMode = RoundingMode.HALF_EVEN; private boolean isNegative = false; @@ -320,15 +320,18 @@ final class DigitList implements Cloneable { * fractional digits to be converted. If false, total digits. */ void set(boolean isNegative, double source, int maximumDigits, boolean fixedPoint) { + assert Double.isFinite(source); + FloatingDecimal.BinaryToASCIIConverter fdConverter = FloatingDecimal.getBinaryToASCIIConverter(source, COMPAT); boolean hasBeenRoundedUp = fdConverter.digitsRoundedUp(); boolean valueExactAsDecimal = fdConverter.decimalDigitsExact(); - assert !fdConverter.isExceptional(); - byte[] chars = getDataChars(26); - int len = fdConverter.getChars(chars); - set(isNegative, chars, len, + count = fdConverter.getDigits(digits); + + int exp = fdConverter.getDecimalExponent() - count; + + set(isNegative, exp, hasBeenRoundedUp, valueExactAsDecimal, maximumDigits, fixedPoint); } @@ -340,44 +343,18 @@ final class DigitList implements Cloneable { * @param valueExactAsDecimal whether or not collected digits provide * an exact decimal representation of the value. */ - private void set(boolean isNegative, byte[] source, int len, + private void set(boolean isNegative, int exp, boolean roundedUp, boolean valueExactAsDecimal, int maximumDigits, boolean fixedPoint) { this.isNegative = isNegative; - decimalAt = -1; - count = 0; - int exponent = 0; - // Number of zeros between decimal point and first non-zero digit after - // decimal point, for numbers < 1. - int leadingZerosAfterDecimal = 0; - boolean nonZeroDigitSeen = false; - - for (int i = 0; i < len; ) { - byte c = source[i++]; - if (c == '.') { - decimalAt = count; - } else if (c == 'e' || c == 'E') { - exponent = parseInt(source, i, len); - break; - } else { - if (!nonZeroDigitSeen) { - nonZeroDigitSeen = (c != '0'); - if (!nonZeroDigitSeen && decimalAt != -1) - ++leadingZerosAfterDecimal; - } - if (nonZeroDigitSeen) { - digits[count++] = c; - } - } - } - if (decimalAt == -1) { - decimalAt = count; - } - if (nonZeroDigitSeen) { - decimalAt += exponent - leadingZerosAfterDecimal; + if (!nonZeroAfterIndex(0)) { + count = 0; + decimalAt = 0; + return; } + decimalAt = count + exp; if (fixedPoint) { // The negative of the exponent represents the number of leading @@ -669,13 +646,13 @@ final class DigitList implements Cloneable { */ @SuppressWarnings("deprecation") void set(boolean isNegative, BigDecimal source, int maximumDigits, boolean fixedPoint) { - String s = source.toString(); - extendDigits(s.length()); - + String s = source.unscaledValue().toString(); int len = s.length(); - byte[] chars = getDataChars(len); - s.getBytes(0, len, chars, 0); - set(isNegative, chars, len, + + extendDigits(len); + s.getBytes(0, len, digits, 0); + count = len; + set(isNegative, -source.scale(), false, true, maximumDigits, fixedPoint); } @@ -745,14 +722,7 @@ final class DigitList implements Cloneable { public Object clone() { try { DigitList other = (DigitList) super.clone(); - byte[] newDigits = new byte[digits.length]; - System.arraycopy(digits, 0, newDigits, 0, digits.length); - other.digits = newDigits; - - // Data does not need to be copied because it does - // not carry significant information. It will be recreated on demand. - // Setting it to null is needed to avoid sharing across clones. - other.data = null; + other.digits = digits.clone(); return other; } catch (CloneNotSupportedException e) { @@ -760,29 +730,8 @@ final class DigitList implements Cloneable { } } - private static int parseInt(byte[] str, int offset, int strLen) { - byte c; - boolean positive = true; - if ((c = str[offset]) == '-') { - positive = false; - offset++; - } else if (c == '+') { - offset++; - } - - int value = 0; - while (offset < strLen) { - c = str[offset++]; - if (c >= '0' && c <= '9') { - value = value * 10 + (c - '0'); - } else { - break; - } - } - return positive ? value : -value; - } - // The digit part of -9223372036854775808L + @Stable private static final byte[] LONG_MIN_REP = "9223372036854775808".getBytes(StandardCharsets.ISO_8859_1); public String toString() { @@ -798,11 +747,4 @@ final class DigitList implements Cloneable { digits = new byte[len]; } } - - private byte[] getDataChars(int length) { - if (data == null || data.length < length) { - data = new byte[length]; - } - return data; - } } diff --git a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java index 32399310bd3..e4354b9b958 100644 --- a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java +++ b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java @@ -115,7 +115,7 @@ public class FloatingDecimal{ * @param digits The digit array. * @return The number of valid digits copied into the array. */ - int getDigits(char[] digits); + int getDigits(byte[] digits); /** * Indicates the sign of the value. @@ -173,7 +173,7 @@ public class FloatingDecimal{ } @Override - public int getDigits(char[] digits) { + public int getDigits(byte[] digits) { throw new IllegalArgumentException("Exceptional value does not have digits"); } @@ -255,7 +255,7 @@ public class FloatingDecimal{ } @Override - public int getDigits(char[] digits) { + public int getDigits(byte[] digits) { System.arraycopy(this.digits, firstDigitIndex, digits, 0, this.nDigits); return this.nDigits; } diff --git a/test/jdk/java/text/Format/DecimalFormat/CloneTest.java b/test/jdk/java/text/Format/DecimalFormat/CloneTest.java index f11d576a39a..dabdd137f5f 100644 --- a/test/jdk/java/text/Format/DecimalFormat/CloneTest.java +++ b/test/jdk/java/text/Format/DecimalFormat/CloneTest.java @@ -23,7 +23,7 @@ /* * @test - * @bug 8354522 8358880 + * @bug 8354522 8358880 8367324 * @summary Check for cloning interference * @library /test/lib * @run junit/othervm --add-opens=java.base/java.text=ALL-UNNAMED CloneTest @@ -89,12 +89,6 @@ public class CloneTest { Object digits = valFromDigitList(original, "digits"); assertNotSame(digits, valFromDigitList(dfClone, "digits")); - - Object data = valFromDigitList(original, "data"); - if (data != null) { - assertNotSame(data, valFromDigitList(dfClone, "data")); - } - assertEquals(digitListField.get(original), digitListField.get(dfClone)); } catch (ReflectiveOperationException e) { throw new SkippedException("reflective access in white-box test failed", e); diff --git a/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java b/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java index 1da18cc97a0..cd49469c15d 100644 --- a/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java +++ b/test/micro/org/openjdk/bench/java/text/DefFormatterBench.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,13 +22,16 @@ */ package org.openjdk.bench.java.text; +import java.math.BigDecimal; import java.text.NumberFormat; import java.util.Locale; import java.util.concurrent.TimeUnit; +import java.util.stream.DoubleStream; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OperationsPerInvocation; @@ -50,25 +53,52 @@ import org.openjdk.jmh.runner.options.OptionsBuilder; @State(Scope.Benchmark) public class DefFormatterBench { + public static final int VALUES_SIZE = 13; public double[] values; + public BigDecimal[] bdLargeValues; + public BigDecimal[] bdSmallValues; - @Setup + @Setup(Level.Invocation) public void setup() { values = new double[] { 1.23, 1.49, 1.80, 1.7, 0.0, -1.49, -1.50, 9999.9123, 1.494, 1.495, 1.03, 25.996, -25.996 }; + + bdLargeValues = DoubleStream.of(values) + .mapToObj(BigDecimal::new) + .toArray(BigDecimal[]::new); + + bdSmallValues = DoubleStream.of(values) + .mapToObj(BigDecimal::valueOf) + .toArray(BigDecimal[]::new); } private DefNumberFormat dnf = new DefNumberFormat(); @Benchmark - @OperationsPerInvocation(13) + @OperationsPerInvocation(VALUES_SIZE) public void testDefNumberFormatter(final Blackhole blackhole) { for (double value : values) { blackhole.consume(this.dnf.format(value)); } } + @Benchmark + @OperationsPerInvocation(VALUES_SIZE) + public void testSmallBigDecDefNumberFormatter(final Blackhole blackhole) { + for (BigDecimal value : bdSmallValues) { + blackhole.consume(this.dnf.format(value)); + } + } + + @Benchmark + @OperationsPerInvocation(VALUES_SIZE) + public void testLargeBigDecDefNumberFormatter(final Blackhole blackhole) { + for (BigDecimal value : bdLargeValues) { + blackhole.consume(this.dnf.format(value)); + } + } + public static void main(String... args) throws Exception { Options opts = new OptionsBuilder().include(DefFormatterBench.class.getSimpleName()).shouldDoGC(true).build(); new Runner(opts).run(); @@ -88,5 +118,9 @@ public class DefFormatterBench { public String format(final double d) { return this.n.format(d); } + + public String format(final BigDecimal bd) { + return this.n.format(bd); + } } } From 3c9fd7688f4d73067db9b128c329ca7603a60578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jeli=C5=84ski?= Date: Thu, 25 Sep 2025 18:47:32 +0000 Subject: [PATCH 246/556] 8368514: TLS stateless session ticket decryption fails on some providers Reviewed-by: valeriep, abarashev --- .../security/ssl/SessionTicketExtension.java | 6 ++- .../FipsModeTLS.java} | 46 +++++++++++------- .../pkcs11/tls/{tls12 => fips}/cert8.db | Bin .../pkcs11/tls/{tls12 => fips}/cert9.db | Bin .../pkcs11/tls/{tls12 => fips}/key3.db | Bin .../pkcs11/tls/{tls12 => fips}/key4.db | Bin .../pkcs11/tls/{tls12 => fips}/keystore | Bin .../pkcs11/tls/{tls12 => fips}/nss.cfg | 0 .../pkcs11/tls/{tls12 => fips}/pkcs11.txt | 0 .../pkcs11/tls/{tls12 => fips}/secmod.db | Bin 10 files changed, 32 insertions(+), 20 deletions(-) rename test/jdk/sun/security/pkcs11/tls/{tls12/FipsModeTLS12.java => fips/FipsModeTLS.java} (95%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/cert8.db (100%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/cert9.db (100%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/key3.db (100%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/key4.db (100%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/keystore (100%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/nss.cfg (100%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/pkcs11.txt (100%) rename test/jdk/sun/security/pkcs11/tls/{tls12 => fips}/secmod.db (100%) diff --git a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java index c0d2bea77ca..444af5d6dae 100644 --- a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java +++ b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java @@ -278,8 +278,10 @@ final class SessionTicketExtension { aad.putInt(keyID).put(compressed); c.updateAAD(aad); + // use getOutputSize to avoid a ShortBufferException + // from providers that require oversized buffers. See JDK-8368514. ByteBuffer out = ByteBuffer.allocate( - data.remaining() - GCM_TAG_LEN / 8); + c.getOutputSize(data.remaining())); c.doFinal(data, out); out.flip(); @@ -291,7 +293,7 @@ final class SessionTicketExtension { return out; } catch (Exception e) { if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { - SSLLogger.fine("Decryption failed." + e.getMessage()); + SSLLogger.fine("Decryption failed." + e); } } diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/FipsModeTLS12.java b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java similarity index 95% rename from test/jdk/sun/security/pkcs11/tls/tls12/FipsModeTLS12.java rename to test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java index c0af4d71498..a883239281c 100644 --- a/test/jdk/sun/security/pkcs11/tls/tls12/FipsModeTLS12.java +++ b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java @@ -24,16 +24,18 @@ /* * @test - * @bug 8029661 8325164 8368073 + * @bug 8029661 8325164 8368073 8368514 * @summary Test TLS 1.2 and TLS 1.3 * @modules java.base/sun.security.internal.spec * java.base/sun.security.util * java.base/com.sun.crypto.provider * @library /test/lib ../.. * @run main/othervm/timeout=120 -Djdk.tls.client.protocols=TLSv1.2 - * -Djdk.tls.useExtendedMasterSecret=false FipsModeTLS12 - * @comment SunPKCS11 does not support (TLS1.2) SunTlsExtendedMasterSecret yet - * @run main/othervm/timeout=120 -Djdk.tls.client.protocols=TLSv1.3 FipsModeTLS12 + * -Djdk.tls.useExtendedMasterSecret=false + * -Djdk.tls.client.enableSessionTicketExtension=false FipsModeTLS + * @comment SunPKCS11 does not support (TLS1.2) SunTlsExtendedMasterSecret yet. + * Stateless resumption doesn't currently work with NSS-FIPS, see JDK-8368669 + * @run main/othervm/timeout=120 -Djdk.tls.client.protocols=TLSv1.3 FipsModeTLS */ import java.io.File; @@ -73,7 +75,7 @@ import sun.security.internal.spec.TlsMasterSecretParameterSpec; import sun.security.internal.spec.TlsPrfParameterSpec; import sun.security.internal.spec.TlsRsaPremasterSecretParameterSpec; -public final class FipsModeTLS12 extends SecmodTest { +public final class FipsModeTLS extends SecmodTest { private static final boolean enableDebug = true; @@ -101,8 +103,9 @@ public final class FipsModeTLS12 extends SecmodTest { // Test against JCE testTlsAuthenticationCodeGeneration(); - // Self-integrity test (complete TLS 1.2 communication) - new testTLS12SunPKCS11Communication().run(); + // Self-integrity test (complete TLS communication) + testTLSSunPKCS11Communication.initSslContext(); + testTLSSunPKCS11Communication.run(); System.out.println("Test PASS - OK"); } else { @@ -269,15 +272,18 @@ public final class FipsModeTLS12 extends SecmodTest { } } - private static class testTLS12SunPKCS11Communication { + private static class testTLSSunPKCS11Communication { public static void run() throws Exception { SSLEngine[][] enginesToTest = getSSLEnginesToTest(); - + boolean firstSession = true; for (SSLEngine[] engineToTest : enginesToTest) { SSLEngine clientSSLEngine = engineToTest[0]; SSLEngine serverSSLEngine = engineToTest[1]; - + // The first connection needs to do a full handshake. + // Verify that subsequent handshakes use resumption. + clientSSLEngine.setEnableSessionCreation(firstSession); + firstSession = false; // SSLEngine code based on RedhandshakeFinished.java boolean dataDone = false; @@ -400,14 +406,6 @@ public final class FipsModeTLS12 extends SecmodTest { static private SSLEngine createSSLEngine(boolean client) throws Exception { SSLEngine ssle; - KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX", "SunJSSE"); - kmf.init(ks, passphrase); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "SunJSSE"); - tmf.init(ts); - - SSLContext sslCtx = SSLContext.getInstance("TLS", "SunJSSE"); - sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); ssle = sslCtx.createSSLEngine("localhost", 443); ssle.setUseClientMode(client); SSLParameters sslParameters = ssle.getSSLParameters(); @@ -431,6 +429,18 @@ public final class FipsModeTLS12 extends SecmodTest { return ssle; } + + private static SSLContext sslCtx; + private static void initSslContext() throws Exception { + KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX", "SunJSSE"); + kmf.init(ks, passphrase); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "SunJSSE"); + tmf.init(ts); + + sslCtx = SSLContext.getInstance("TLS", "SunJSSE"); + sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + } } private static void initialize() throws Exception { diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/cert8.db b/test/jdk/sun/security/pkcs11/tls/fips/cert8.db similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/cert8.db rename to test/jdk/sun/security/pkcs11/tls/fips/cert8.db diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/cert9.db b/test/jdk/sun/security/pkcs11/tls/fips/cert9.db similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/cert9.db rename to test/jdk/sun/security/pkcs11/tls/fips/cert9.db diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/key3.db b/test/jdk/sun/security/pkcs11/tls/fips/key3.db similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/key3.db rename to test/jdk/sun/security/pkcs11/tls/fips/key3.db diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/key4.db b/test/jdk/sun/security/pkcs11/tls/fips/key4.db similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/key4.db rename to test/jdk/sun/security/pkcs11/tls/fips/key4.db diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/keystore b/test/jdk/sun/security/pkcs11/tls/fips/keystore similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/keystore rename to test/jdk/sun/security/pkcs11/tls/fips/keystore diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/nss.cfg b/test/jdk/sun/security/pkcs11/tls/fips/nss.cfg similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/nss.cfg rename to test/jdk/sun/security/pkcs11/tls/fips/nss.cfg diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/pkcs11.txt b/test/jdk/sun/security/pkcs11/tls/fips/pkcs11.txt similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/pkcs11.txt rename to test/jdk/sun/security/pkcs11/tls/fips/pkcs11.txt diff --git a/test/jdk/sun/security/pkcs11/tls/tls12/secmod.db b/test/jdk/sun/security/pkcs11/tls/fips/secmod.db similarity index 100% rename from test/jdk/sun/security/pkcs11/tls/tls12/secmod.db rename to test/jdk/sun/security/pkcs11/tls/fips/secmod.db From 52e777845f0a09b4c285131f1eff02dfbffa0d1f Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Thu, 25 Sep 2025 19:59:52 +0000 Subject: [PATCH 247/556] 8367910: Reduce warnings about unsupported classes in AOT cache creation Reviewed-by: dholmes, kvn, shade --- .../share/cds/dumpTimeClassInfo.inline.hpp | 2 +- .../classfile/systemDictionaryShared.cpp | 63 +++++++++++-------- .../classfile/systemDictionaryShared.hpp | 3 +- src/hotspot/share/classfile/verifier.cpp | 2 +- src/hotspot/share/oops/trainingData.cpp | 2 +- .../cds/appcds/aotCache/OldClassSupport.java | 1 + .../cds/appcds/aotCache/VerifierFailOver.java | 2 +- .../aotClassLinking/BulkLoaderTest.java | 2 +- 8 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp b/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp index 90f35c61de1..658d3d31e50 100644 --- a/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp +++ b/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp @@ -53,7 +53,7 @@ void DumpTimeSharedClassTable::iterate_all_live_classes(Function function) const assert(k->is_loader_alive(), "must not change"); } else { if (!SystemDictionaryShared::is_excluded_class(k)) { - SystemDictionaryShared::warn_excluded(k, "Class loader not alive"); + SystemDictionaryShared::log_exclusion(k, "Class loader not alive"); SystemDictionaryShared::set_excluded_locked(k); } } diff --git a/src/hotspot/share/classfile/systemDictionaryShared.cpp b/src/hotspot/share/classfile/systemDictionaryShared.cpp index 04c2d7ffb84..da022436292 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.cpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.cpp @@ -354,11 +354,13 @@ void SystemDictionaryShared::check_exclusion_for_self_and_dependencies(InstanceK }); } -// Returns true so the caller can do: return warn_excluded("....."); -bool SystemDictionaryShared::warn_excluded(InstanceKlass* k, const char* reason) { +void SystemDictionaryShared::log_exclusion(InstanceKlass* k, const char* reason, bool is_warning) { ResourceMark rm; - aot_log_warning(aot)("Skipping %s: %s", k->name()->as_C_string(), reason); - return true; + if (is_warning) { + aot_log_warning(aot)("Skipping %s: %s", k->name()->as_C_string(), reason); + } else { + aot_log_info(aot)("Skipping %s: %s", k->name()->as_C_string(), reason); + } } bool SystemDictionaryShared::is_jfr_event_class(InstanceKlass *k) { @@ -377,23 +379,35 @@ bool SystemDictionaryShared::is_early_klass(InstanceKlass* ik) { } bool SystemDictionaryShared::check_self_exclusion(InstanceKlass* k) { + bool log_warning = false; + const char* error = check_self_exclusion_helper(k, log_warning); + if (error != nullptr) { + log_exclusion(k, error, log_warning); + return true; // Should be excluded + } else { + return false; // Should not be excluded + } +} + +const char* SystemDictionaryShared::check_self_exclusion_helper(InstanceKlass* k, bool& log_warning) { assert_lock_strong(DumpTimeTable_lock); if (CDSConfig::is_dumping_final_static_archive() && k->defined_by_other_loaders() && k->in_aot_cache()) { - return false; // Do not exclude: unregistered classes are passed from preimage to final image. + return nullptr; // Do not exclude: unregistered classes are passed from preimage to final image. } if (k->is_in_error_state()) { - return warn_excluded(k, "In error state"); + log_warning = true; + return "In error state"; } if (k->is_scratch_class()) { - return warn_excluded(k, "A scratch class"); + return "A scratch class"; } if (!k->is_loaded()) { - return warn_excluded(k, "Not in loaded state"); + return "Not in loaded state"; } if (has_been_redefined(k)) { - return warn_excluded(k, "Has been redefined"); + return "Has been redefined"; } if (!k->is_hidden() && k->shared_classpath_index() < 0 && is_builtin(k)) { if (k->name()->starts_with("java/lang/invoke/BoundMethodHandle$Species_")) { @@ -401,43 +415,42 @@ bool SystemDictionaryShared::check_self_exclusion(InstanceKlass* k) { if (CDSConfig::is_dumping_method_handles()) { k->set_shared_classpath_index(0); } else { - ResourceMark rm; - aot_log_info(aot)("Skipping %s because it is dynamically generated", k->name()->as_C_string()); - return true; // exclude without warning + return "dynamically generated"; } } else { // These are classes loaded from unsupported locations (such as those loaded by JVMTI native // agent during dump time). - return warn_excluded(k, "Unsupported location"); + return "Unsupported location"; } } if (k->signers() != nullptr) { // We cannot include signed classes in the archive because the certificates // used during dump time may be different than those used during // runtime (due to expiration, etc). - return warn_excluded(k, "Signed JAR"); + return "Signed JAR"; } if (is_jfr_event_class(k)) { // We cannot include JFR event classes because they need runtime-specific // instrumentation in order to work with -XX:FlightRecorderOptions:retransform=false. // There are only a small number of these classes, so it's not worthwhile to // support them and make CDS more complicated. - return warn_excluded(k, "JFR event class"); + return "JFR event class"; } if (!k->is_linked()) { if (has_class_failed_verification(k)) { - return warn_excluded(k, "Failed verification"); + log_warning = true; + return "Failed verification"; } else if (CDSConfig::is_dumping_aot_linked_classes()) { // Most loaded classes should have been speculatively linked by AOTMetaspace::link_class_for_cds(). // Old classes may not be linked if CDSConfig::is_preserving_verification_constraints()==false. // An unlinked class may fail to verify in AOTLinkedClassBulkLoader::init_required_classes_for_loader(), // causing the JVM to fail at bootstrap. - return warn_excluded(k, "Unlinked class not supported by AOTClassLinking"); + return "Unlinked class not supported by AOTClassLinking"; } else if (CDSConfig::is_dumping_preimage_static_archive()) { // When dumping the final static archive, we will unconditionally load and link all // classes from the preimage. We don't want to get a VerifyError when linking this class. - return warn_excluded(k, "Unlinked class not supported by AOTConfiguration"); + return "Unlinked class not supported by AOTConfiguration"; } } else { if (!k->can_be_verified_at_dumptime()) { @@ -447,17 +460,15 @@ bool SystemDictionaryShared::check_self_exclusion(InstanceKlass* k) { // won't work at runtime. // As a result, we cannot store this class. It must be loaded and fully verified // at runtime. - return warn_excluded(k, "Old class has been linked"); + return "Old class has been linked"; } } if (UnregisteredClasses::check_for_exclusion(k)) { - ResourceMark rm; - aot_log_info(aot)("Skipping %s: used only when dumping CDS archive", k->name()->as_C_string()); - return true; + return "used only when dumping CDS archive"; } - return false; + return nullptr; } // Returns true if DumpTimeClassInfo::is_excluded() is true for at least one of k's exclusion dependencies. @@ -511,7 +522,7 @@ bool SystemDictionaryShared::is_dependency_excluded(InstanceKlass* k, InstanceKl DumpTimeClassInfo* dependency_info = get_info_locked(dependency); if (dependency_info->is_excluded()) { ResourceMark rm; - aot_log_warning(aot)("Skipping %s: %s %s is excluded", k->name()->as_C_string(), type, dependency->name()->as_C_string()); + aot_log_info(aot)("Skipping %s: %s %s is excluded", k->name()->as_C_string(), type, dependency->name()->as_C_string()); return true; } return false; @@ -838,7 +849,7 @@ public: InstanceKlass* k = _list.at(i); bool i_am_first = SystemDictionaryShared::add_unregistered_class(_thread, k); if (!i_am_first) { - SystemDictionaryShared::warn_excluded(k, "Duplicated unregistered class"); + SystemDictionaryShared::log_exclusion(k, "Duplicated unregistered class"); SystemDictionaryShared::set_excluded_locked(k); } } @@ -967,7 +978,7 @@ bool SystemDictionaryShared::has_class_failed_verification(InstanceKlass* ik) { } void SystemDictionaryShared::set_from_class_file_load_hook(InstanceKlass* ik) { - warn_excluded(ik, "From ClassFileLoadHook"); + log_exclusion(ik, "From ClassFileLoadHook"); set_excluded(ik); } diff --git a/src/hotspot/share/classfile/systemDictionaryShared.hpp b/src/hotspot/share/classfile/systemDictionaryShared.hpp index baad020cb61..5ff57653dd0 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.hpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.hpp @@ -180,6 +180,7 @@ private: // exclusion checks static void check_exclusion_for_self_and_dependencies(InstanceKlass *k); static bool check_self_exclusion(InstanceKlass* k); + static const char* check_self_exclusion_helper(InstanceKlass* k, bool& log_warning); static bool check_dependencies_exclusion(InstanceKlass* k, DumpTimeClassInfo* info); static bool check_verification_constraint_exclusion(InstanceKlass* k, Symbol* constraint_class_name); static bool is_dependency_excluded(InstanceKlass* k, InstanceKlass* dependency, const char* type); @@ -277,7 +278,7 @@ public: static void set_excluded(InstanceKlass* k); static void set_excluded_locked(InstanceKlass* k); static void set_from_class_file_load_hook(InstanceKlass* k) NOT_CDS_RETURN; - static bool warn_excluded(InstanceKlass* k, const char* reason); + static void log_exclusion(InstanceKlass* k, const char* reason, bool is_warning = false); static void dumptime_classes_do(class MetaspaceClosure* it); static void write_to_archive(bool is_static_archive = true); static void serialize_dictionary_headers(class SerializeClosure* soc, diff --git a/src/hotspot/share/classfile/verifier.cpp b/src/hotspot/share/classfile/verifier.cpp index f6f5aa70fbd..bc278f3d158 100644 --- a/src/hotspot/share/classfile/verifier.cpp +++ b/src/hotspot/share/classfile/verifier.cpp @@ -237,7 +237,7 @@ bool Verifier::verify(InstanceKlass* klass, bool should_verify_class, TRAPS) { // Exclude any classes that are verified with the old verifier, as the old verifier // doesn't call SystemDictionaryShared::add_verification_constraint() if (CDSConfig::is_dumping_archive()) { - SystemDictionaryShared::warn_excluded(klass, "Verified with old verifier"); + SystemDictionaryShared::log_exclusion(klass, "Verified with old verifier"); SystemDictionaryShared::set_excluded(klass); } #endif diff --git a/src/hotspot/share/oops/trainingData.cpp b/src/hotspot/share/oops/trainingData.cpp index c768d13fe59..41fbc6de994 100644 --- a/src/hotspot/share/oops/trainingData.cpp +++ b/src/hotspot/share/oops/trainingData.cpp @@ -618,7 +618,7 @@ void CompileTrainingData::verify(bool verify_dep_counter) { for (int i = 0; i < init_dep_count(); i++) { KlassTrainingData* ktd = init_dep(i); if (ktd->has_holder() && ktd->holder()->defined_by_other_loaders()) { - LogStreamHandle(Warning, training) log; + LogStreamHandle(Info, training) log; if (log.is_enabled()) { ResourceMark rm; log.print("CTD "); print_value_on(&log); diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/OldClassSupport.java b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/OldClassSupport.java index 42161b469bf..732ef4f2810 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/OldClassSupport.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/OldClassSupport.java @@ -65,6 +65,7 @@ public class OldClassSupport { @Override public String[] vmArgs(RunMode runMode) { return new String[] { + "-Xlog:aot", "-Xlog:aot+class=debug", "-Xlog:aot+resolve=trace", }; diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/VerifierFailOver.java b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/VerifierFailOver.java index 50b47e4424d..8107e3fe0a3 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/VerifierFailOver.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/VerifierFailOver.java @@ -39,7 +39,7 @@ import jdk.test.lib.process.OutputAnalyzer; public class VerifierFailOver { public static void main(String... args) throws Exception { SimpleCDSAppTester.of("VerifierFailOver") - .addVmArgs("-Xlog:aot+class=debug") + .addVmArgs("-Xlog:aot,aot+class=debug") .classpath("app.jar") .appCommandLine("VerifierFailOverApp") .setTrainingChecker((OutputAnalyzer out) -> { diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java index 9e43d00e872..482f8fb4ad0 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/BulkLoaderTest.java @@ -118,7 +118,7 @@ public class BulkLoaderTest { @Override public String[] vmArgs(RunMode runMode) { return new String[] { - "-Xlog:cds,aot+load,cds+class=debug,aot+class=debug", + "-Xlog:cds,aot,aot+load,cds+class=debug,aot+class=debug", "-XX:+AOTClassLinking", }; } From 648582ab781d98556906c274067f26f856fc0449 Mon Sep 17 00:00:00 2001 From: "Daniel D. Daugherty" Date: Thu, 25 Sep 2025 20:55:05 +0000 Subject: [PATCH 248/556] 8368714: [BACKOUT] JDK-8368468 Split out everything but configure results from spec.gmk Reviewed-by: ihse --- make/RunTestsPrebuilt.gmk | 5 +- make/RunTestsPrebuiltSpec.gmk | 58 ++- make/autoconf/boot-jdk.m4 | 8 +- make/autoconf/bootcycle-spec.gmk.template | 21 +- make/autoconf/build-performance.m4 | 20 +- make/autoconf/buildjdk-spec.gmk.template | 79 +++- make/autoconf/help.m4 | 4 +- make/autoconf/hotspot.m4 | 2 +- make/autoconf/jdk-options.m4 | 6 +- make/autoconf/spec.gmk.template | 343 ++++++++++++++--- make/common/CommonVars.gmk | 442 ---------------------- make/common/MakeBase.gmk | 1 - 12 files changed, 465 insertions(+), 524 deletions(-) delete mode 100644 make/common/CommonVars.gmk diff --git a/make/RunTestsPrebuilt.gmk b/make/RunTestsPrebuilt.gmk index 3ea0d330d58..f5fe1d33830 100644 --- a/make/RunTestsPrebuilt.gmk +++ b/make/RunTestsPrebuilt.gmk @@ -131,7 +131,7 @@ $(eval $(call SetupVariable,MAKE,make,NO_CHECK)) $(eval $(call SetupVariable,BASH,bash,NO_CHECK)) # Check optional variables -$(eval $(call SetupVariable,JIB_HOME,OPTIONAL)) +$(eval $(call SetupVariable,JIB_JAR,OPTIONAL)) # Now that we have verified that we have the required variables available, we # can include the prebuilt spec file ourselves, without an ephemeral spec @@ -265,7 +265,7 @@ $(call CreateNewSpec, $(NEW_SPEC), \ SYMBOLS_IMAGE_DIR := $(SYMBOLS_IMAGE_DIR), \ MAKE := $(MAKE), \ BASH := $(BASH), \ - JIB_HOME := $(JIB_HOME), \ + JIB_JAR := $(JIB_JAR), \ FIXPATH_BASE := $(FIXPATH_BASE), \ FIXPATH := $(FIXPATH), \ OPENJDK_TARGET_OS := $(OPENJDK_TARGET_OS), \ @@ -295,7 +295,6 @@ test-prebuilt: # ExecuteWithLog is called in RunTests.gmk. The PrepareFailureLogs macro # is unfortunately not available at this point. $(call MakeDir, $(MAKESUPPORT_OUTPUTDIR)/failure-logs) - $(call MakeDir, $(JAVA_TMP_DIR)) @$(RM) -f $(MAKESUPPORT_OUTPUTDIR)/exit-with-error # We need to fill the FindTest cache before entering RunTests.gmk. @cd $(TOPDIR)/make && $(MAKE) $(MAKE_ARGS) SPEC=$(SPEC) \ diff --git a/make/RunTestsPrebuiltSpec.gmk b/make/RunTestsPrebuiltSpec.gmk index 132532b3b38..e9fd901c9e1 100644 --- a/make/RunTestsPrebuiltSpec.gmk +++ b/make/RunTestsPrebuiltSpec.gmk @@ -27,6 +27,9 @@ # Fake minimalistic spec file for RunTestsPrebuilt.gmk. ################################################################################ +# Make sure all shell commands are executed with the C locale +export LC_ALL := C + define VerifyVariable ifeq ($$($1), ) $$(info Error: Variable $1 is missing, needed by RunTestPrebuiltSpec.gmk) @@ -54,18 +57,26 @@ $(eval $(call VerifyVariable,BASH)) # The "human readable" name of this configuration CONF_NAME := run-test-prebuilt -LOCALE_USED := C - # Number of parallel jobs to use for compilation -CONF_JOBS := $(NUM_CORES) -CONF_TEST_JOBS := 0 +JOBS ?= $(NUM_CORES) +TEST_JOBS ?= 0 # Use hard-coded values for java flags (one size, fits all!) JAVA_FLAGS := -Duser.language=en -Duser.country=US JAVA_FLAGS_BIG := -Xms64M -Xmx2048M JAVA_FLAGS_SMALL := -XX:+UseSerialGC -Xms32M -Xmx512M -XX:TieredStopAtLevel=1 -BUILD_JAVA_FLAGS_SMALL := -Xms32M -Xmx512M -XX:TieredStopAtLevel=1 -BOOTCYCLE_JVM_ARGS_BIG := $(JAVA_FLAGS_BIG) +BUILDJDK_JAVA_FLAGS_SMALL := -Xms32M -Xmx512M -XX:TieredStopAtLevel=1 +BUILD_JAVA_FLAGS := $(JAVA_FLAGS_BIG) + +################################################################################ +# Hard-coded values copied from spec.gmk.in. +X := +SPACE := $(X) $(X) +COMMA := , +MAKE_ARGS = $(MAKE_LOG_FLAGS) -r -R -I $(TOPDIR)/make/common SPEC=$(SPEC) \ + MAKE_LOG_FLAGS="$(MAKE_LOG_FLAGS)" LOG_LEVEL=$(LOG_LEVEL) +BASH_ARGS := -o pipefail -e +SHELL := $(BASH) $(BASH_ARGS) ################################################################################ # Set some reasonable defaults for features @@ -73,6 +84,20 @@ DEBUG_LEVEL := release HOTSPOT_DEBUG_LEVEL := release BUILD_FAILURE_HANDLER := true +################################################################################ +# Alias some paths (that should not really be used) to our JDK image under test. +SUPPORT_OUTPUTDIR := $(OUTPUTDIR)/support +BUILDTOOLS_OUTPUTDIR := $(OUTPUTDIR)/buildtools +HOTSPOT_OUTPUTDIR := $(OUTPUTDIR)/hotspot +JDK_OUTPUTDIR := $(OUTPUTDIR)/jdk +IMAGES_OUTPUTDIR := $(OUTPUTDIR)/images +BUNDLES_OUTPUTDIR := $(OUTPUTDIR)/bundles +TESTMAKE_OUTPUTDIR := $(OUTPUTDIR)/test-make +MAKESUPPORT_OUTPUTDIR := $(OUTPUTDIR)/make-support +BUILDJDK_OUTPUTDIR := $(OUTPUTDIR)/buildjdk + +JRE_IMAGE_DIR := $(JDK_IMAGE_DIR) + ################################################################################ # Assume build platform is same as target platform OPENJDK_BUILD_OS := $(OPENJDK_TARGET_OS) @@ -84,19 +109,30 @@ OPENJDK_BUILD_CPU_ARCH := $(OPENJDK_TARGET_CPU_ARCH) OPENJDK_BUILD_CPU_BITS := $(OPENJDK_TARGET_CPU_BITS) OPENJDK_BUILD_CPU_ENDIAN := $(OPENJDK_TARGET_CPU_ENDIAN) -EXTERNAL_BUILDJDK_PATH := - ################################################################################ # Java executable definitions -JAVA_CMD := $(FIXPATH) $(BOOT_JDK)/bin/java -JAVAC_CMD := $(FIXPATH) $(BOOT_JDK)/bin/javac -JAR_CMD := $(FIXPATH) $(BOOT_JDK)/bin/jar +JAVA_CMD := $(BOOT_JDK)/bin/java +JAVAC_CMD := $(BOOT_JDK)/bin/javac +JAR_CMD := $(BOOT_JDK)/bin/jar +JLINK_CMD := $(JDK_OUTPUTDIR)/bin/jlink +JMOD_CMD := $(JDK_OUTPUTDIR)/bin/jmod +JAVA := $(FIXPATH) $(JAVA_CMD) $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) +JAVA_SMALL := $(FIXPATH) $(JAVA_CMD) $(JAVA_FLAGS_SMALL) $(JAVA_FLAGS) +JAVAC := $(FIXPATH) $(JAVAC_CMD) +JAR := $(FIXPATH) $(JAR_CMD) +JLINK := $(FIXPATH) $(JLINK_CMD) +JMOD := $(FIXPATH) $(JMOD_CMD) + +JTREG_JAVA := $(FIXPATH) $(JTREG_JDK)/bin/java $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) + +BUILD_JAVA := $(JDK_IMAGE_DIR)/bin/JAVA ################################################################################ # Some common tools. Assume most common name and no path. AWK := awk BASENAME := basename CAT := cat +CD := cd CHMOD := chmod CP := cp CUT := cut diff --git a/make/autoconf/boot-jdk.m4 b/make/autoconf/boot-jdk.m4 index 49a3842573c..1dd768b2ae1 100644 --- a/make/autoconf/boot-jdk.m4 +++ b/make/autoconf/boot-jdk.m4 @@ -376,10 +376,10 @@ AC_DEFUN_ONCE([BOOTJDK_SETUP_BOOT_JDK], AC_SUBST(BOOT_JDK) # Setup tools from the Boot JDK. - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVA_CMD, java) - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVAC_CMD, javac) - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVADOC_CMD, javadoc) - BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAR_CMD, jar) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVA, java) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVAC, javac) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAVADOC, javadoc) + BOOTJDK_CHECK_TOOL_IN_BOOTJDK(JAR, jar) # Finally, set some other options... diff --git a/make/autoconf/bootcycle-spec.gmk.template b/make/autoconf/bootcycle-spec.gmk.template index 572ea8f221f..8b6035606a5 100644 --- a/make/autoconf/bootcycle-spec.gmk.template +++ b/make/autoconf/bootcycle-spec.gmk.template @@ -28,4 +28,23 @@ # First include the real base spec.gmk file include @SPEC@ -IS_BOOTCYCLE_JDK_SPEC := true +# Override specific values to do a boot cycle build + +# Use a different Boot JDK +BOOT_JDK := $(JDK_IMAGE_DIR) + +# The bootcycle build has a different output directory +OLD_OUTPUTDIR := @OUTPUTDIR@ +OUTPUTDIR := $(OLD_OUTPUTDIR)/bootcycle-build +# No spaces in patsubst to avoid leading space in variable +JAVAC_SERVER_DIR := $(patsubst $(OLD_OUTPUTDIR)%,$(OUTPUTDIR)%,$(JAVAC_SERVER_DIR)) + +JAVA_CMD := $(FIXPATH) $(BOOT_JDK)/bin/java +JAVAC_CMD := $(FIXPATH) $(BOOT_JDK)/bin/javac +JAR_CMD := $(FIXPATH) $(BOOT_JDK)/bin/jar +# The bootcycle JVM arguments may differ from the original boot jdk. +JAVA_FLAGS_BIG := @BOOTCYCLE_JVM_ARGS_BIG@ +# Any CDS settings generated for the bootjdk are invalid in the bootcycle build. +# By filtering out those JVM args, the bootcycle JVM will use its default +# settings for CDS. +JAVA_FLAGS := $(filter-out -XX:SharedArchiveFile% -Xshare%, $(JAVA_FLAGS)) diff --git a/make/autoconf/build-performance.m4 b/make/autoconf/build-performance.m4 index 49ae9b39cf2..dfc9e979d2f 100644 --- a/make/autoconf/build-performance.m4 +++ b/make/autoconf/build-performance.m4 @@ -130,18 +130,18 @@ AC_DEFUN_ONCE([BPERF_SETUP_BUILD_JOBS], memory_gb=`expr $MEMORY_SIZE / 1024` # Pick the lowest of memory in gb and number of cores. if test "$memory_gb" -lt "$NUM_CORES"; then - CONF_JOBS="$memory_gb" + JOBS="$memory_gb" else - CONF_JOBS="$NUM_CORES" + JOBS="$NUM_CORES" fi - if test "$CONF_JOBS" -eq "0"; then - CONF_JOBS=1 + if test "$JOBS" -eq "0"; then + JOBS=1 fi - AC_MSG_RESULT([$CONF_JOBS]) + AC_MSG_RESULT([$JOBS]) else - CONF_JOBS=$with_jobs + JOBS=$with_jobs fi - AC_SUBST(CONF_JOBS) + AC_SUBST(JOBS) ]) AC_DEFUN_ONCE([BPERF_SETUP_TEST_JOBS], @@ -150,11 +150,11 @@ AC_DEFUN_ONCE([BPERF_SETUP_TEST_JOBS], AC_ARG_WITH(test-jobs, [AS_HELP_STRING([--with-test-jobs], [number of parallel tests jobs to run @<:@based on build jobs@:>@])]) if test "x$with_test_jobs" = x; then - CONF_TEST_JOBS=0 + TEST_JOBS=0 else - CONF_TEST_JOBS=$with_test_jobs + TEST_JOBS=$with_test_jobs fi - AC_SUBST(CONF_TEST_JOBS) + AC_SUBST(TEST_JOBS) ]) AC_DEFUN([BPERF_SETUP_CCACHE], diff --git a/make/autoconf/buildjdk-spec.gmk.template b/make/autoconf/buildjdk-spec.gmk.template index 02f2a6396ce..924389b94e8 100644 --- a/make/autoconf/buildjdk-spec.gmk.template +++ b/make/autoconf/buildjdk-spec.gmk.template @@ -30,4 +30,81 @@ # First include the real base spec.gmk file include @SPEC@ -IS_BUILD_JDK_SPEC := true +CC := @BUILD_CC@ +CXX := @BUILD_CXX@ +# Ideally this should be probed by configure but that is tricky to implement, +# and this should work in most cases. +CPP := @BUILD_CC@ -E +LD := @BUILD_LD@ +LDCXX := @BUILD_LDCXX@ +AS := @BUILD_AS@ +NM := @BUILD_NM@ +AR := @BUILD_AR@ +LIB := @BUILD_LIB@ +OBJCOPY := @BUILD_OBJCOPY@ +STRIP := @BUILD_STRIP@ +SYSROOT_CFLAGS := @BUILD_SYSROOT_CFLAGS@ +SYSROOT_LDFLAGS := @BUILD_SYSROOT_LDFLAGS@ + +# These directories should not be moved to BUILDJDK_OUTPUTDIR +HOTSPOT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(HOTSPOT_OUTPUTDIR)) +BUILDTOOLS_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(BUILDTOOLS_OUTPUTDIR)) +SUPPORT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(SUPPORT_OUTPUTDIR)) +JDK_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(JDK_OUTPUTDIR)) +IMAGES_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(IMAGES_OUTPUTDIR)) + +OPENJDK_BUILD_CPU_LEGACY := @OPENJDK_BUILD_CPU_LEGACY@ +OPENJDK_BUILD_CPU_LEGACY_LIB := @OPENJDK_BUILD_CPU_LEGACY_LIB@ +OPENJDK_BUILD_LIBC := @OPENJDK_BUILD_LIBC@ +OPENJDK_TARGET_CPU := @OPENJDK_BUILD_CPU@ +OPENJDK_TARGET_CPU_ARCH := @OPENJDK_BUILD_CPU_ARCH@ +OPENJDK_TARGET_CPU_BITS := @OPENJDK_BUILD_CPU_BITS@ +OPENJDK_TARGET_CPU_ENDIAN := @OPENJDK_BUILD_CPU_ENDIAN@ +OPENJDK_TARGET_CPU_LEGACY := @OPENJDK_BUILD_CPU_LEGACY@ +OPENJDK_TARGET_LIBC := @OPENJDK_BUILD_LIBC@ +OPENJDK_TARGET_OS_INCLUDE_SUBDIR := @OPENJDK_BUILD_OS_INCLUDE_SUBDIR@ + +HOTSPOT_TARGET_OS := @HOTSPOT_BUILD_OS@ +HOTSPOT_TARGET_OS_TYPE := @HOTSPOT_BUILD_OS_TYPE@ +HOTSPOT_TARGET_CPU := @HOTSPOT_BUILD_CPU@ +HOTSPOT_TARGET_CPU_ARCH := @HOTSPOT_BUILD_CPU_ARCH@ +HOTSPOT_TARGET_CPU_DEFINE := @HOTSPOT_BUILD_CPU_DEFINE@ +HOTSPOT_TARGET_LIBC := @HOTSPOT_BUILD_LIBC@ + +CFLAGS_JDKLIB := @OPENJDK_BUILD_CFLAGS_JDKLIB@ +CXXFLAGS_JDKLIB := @OPENJDK_BUILD_CXXFLAGS_JDKLIB@ +LDFLAGS_JDKLIB := @OPENJDK_BUILD_LDFLAGS_JDKLIB@ +CFLAGS_JDKEXE := @OPENJDK_BUILD_CFLAGS_JDKEXE@ +CXXFLAGS_JDKEXE := @OPENJDK_BUILD_CXXFLAGS_JDKEXE@ +LDFLAGS_JDKEXE := @OPENJDK_BUILD_LDFLAGS_JDKEXE@ + +JVM_CFLAGS := @OPENJDK_BUILD_JVM_CFLAGS@ +JVM_LDFLAGS := @OPENJDK_BUILD_JVM_LDFLAGS@ +JVM_ASFLAGS := @OPENJDK_BUILD_JVM_ASFLAGS@ +JVM_LIBS := @OPENJDK_BUILD_JVM_LIBS@ + +FDLIBM_CFLAGS := @OPENJDK_BUILD_FDLIBM_CFLAGS@ + +INTERIM_LANGTOOLS_ARGS := $(subst $(OUTPUTDIR),$(BUILDJDK_OUTPUTDIR),$(INTERIM_LANGTOOLS_ARGS)) + +# The compiler for the build platform is likely not warning compatible with the official +# compiler. +WARNINGS_AS_ERRORS := false +DISABLE_WARNING_PREFIX := @BUILD_CC_DISABLE_WARNING_PREFIX@ + +# Save speed and disk space by not enabling debug symbols for the buildjdk +ENABLE_DEBUG_SYMBOLS := false + +JVM_VARIANTS := server +JVM_VARIANT_MAIN := server +JVM_FEATURES_server := cds compiler1 compiler2 g1gc serialgc + +# Some users still set EXTRA_*FLAGS on the make command line. Must +# make sure to override that when building buildjdk. +override EXTRA_CFLAGS := +override EXTRA_CXXFLAGS := +override EXTRA_LDFLAGS := + +# hsdis is not needed +HSDIS_BACKEND := none +ENABLE_HSDIS_BUNDLING := false diff --git a/make/autoconf/help.m4 b/make/autoconf/help.m4 index 4dc3cb5c267..d8c0b2ffaef 100644 --- a/make/autoconf/help.m4 +++ b/make/autoconf/help.m4 @@ -305,7 +305,7 @@ AC_DEFUN_ONCE([HELP_PRINT_SUMMARY_AND_WARNINGS], $ECHO "* Version string: $VERSION_STRING ($VERSION_SHORT)" if test "x$SOURCE_DATE" != xupdated; then - source_date_info="$SOURCE_DATE ($SOURCE_DATE_ISO_8601_FIXED)" + source_date_info="$SOURCE_DATE ($SOURCE_DATE_ISO_8601)" else source_date_info="Determined at build time" fi @@ -330,7 +330,7 @@ AC_DEFUN_ONCE([HELP_PRINT_SUMMARY_AND_WARNINGS], $ECHO "" $ECHO "Build performance summary:" - $ECHO "* Build jobs: $CONF_JOBS" + $ECHO "* Build jobs: $JOBS" $ECHO "* Memory limit: $MEMORY_SIZE MB" if test "x$CCACHE_STATUS" != "x"; then $ECHO "* ccache status: $CCACHE_STATUS" diff --git a/make/autoconf/hotspot.m4 b/make/autoconf/hotspot.m4 index aba5f1ef1d2..6dc46d17aa3 100644 --- a/make/autoconf/hotspot.m4 +++ b/make/autoconf/hotspot.m4 @@ -129,7 +129,7 @@ AC_DEFUN_ONCE([HOTSPOT_SETUP_MISC], AC_MSG_RESULT([determined at build time (default)]) else # If we have a fixed value for SOURCE_DATE, use it as default - HOTSPOT_BUILD_TIME="$SOURCE_DATE_ISO_8601_FIXED" + HOTSPOT_BUILD_TIME="$SOURCE_DATE_ISO_8601" AC_MSG_RESULT([$HOTSPOT_BUILD_TIME (from --with-source-date)]) fi fi diff --git a/make/autoconf/jdk-options.m4 b/make/autoconf/jdk-options.m4 index 6de43d0a169..d4299078abf 100644 --- a/make/autoconf/jdk-options.m4 +++ b/make/autoconf/jdk-options.m4 @@ -897,15 +897,15 @@ AC_DEFUN_ONCE([JDKOPT_SETUP_REPRODUCIBLE_BUILD], # for the rest of configure. SOURCE_DATE_EPOCH="$SOURCE_DATE" if test "x$IS_GNU_DATE" = xyes; then - SOURCE_DATE_ISO_8601_FIXED=`$DATE --utc --date="@$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` + SOURCE_DATE_ISO_8601=`$DATE --utc --date="@$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` else - SOURCE_DATE_ISO_8601_FIXED=`$DATE -u -j -f "%s" "$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` + SOURCE_DATE_ISO_8601=`$DATE -u -j -f "%s" "$SOURCE_DATE" +"$ISO_8601_FORMAT_STRING" 2> /dev/null` fi fi AC_SUBST(SOURCE_DATE) AC_SUBST(ISO_8601_FORMAT_STRING) - AC_SUBST(SOURCE_DATE_ISO_8601_FIXED) + AC_SUBST(SOURCE_DATE_ISO_8601) ]) ################################################################################ diff --git a/make/autoconf/spec.gmk.template b/make/autoconf/spec.gmk.template index 3258e87e2be..0b336721d65 100644 --- a/make/autoconf/spec.gmk.template +++ b/make/autoconf/spec.gmk.template @@ -46,15 +46,33 @@ SPEC := @SPEC@ # Path to autoconf if overridden by the user, to be used by "make reconfigure" AUTOCONF := @AUTOCONF@ +# SPACE and COMMA are defined in MakeBase.gmk, but they are also used in +# some definitions here, and are needed if MakeBase.gmk is not included before +# this file. +X := +SPACE := $(X) $(X) +COMMA := , + # What make to use for main processing, after bootstrapping top-level Makefile. MAKE := @MAKE@ +# Make sure all shell commands are executed with a proper locale +export LC_ALL := @LOCALE_USED@ + +# Make sure we override any local CLASSPATH variable +export CLASSPATH := @CLASSPATH@ + +# The default make arguments +MAKE_ARGS = $(MAKE_LOG_FLAGS) -r -R -I $(TOPDIR)/make/common SPEC=$(SPEC) \ + MAKE_LOG_FLAGS="$(MAKE_LOG_FLAGS)" $(MAKE_LOG_VARS) + OUTPUT_SYNC_SUPPORTED := @OUTPUT_SYNC_SUPPORTED@ OUTPUT_SYNC := @OUTPUT_SYNC@ # Override the shell with bash BASH := @BASH@ BASH_ARGS := @BASH_ARGS@ +SHELL := $(BASH) $(BASH_ARGS) # The "human readable" name of this configuration CONF_NAME := @CONF_NAME@ @@ -103,33 +121,9 @@ OPENJDK_BUILD_CPU_ARCH := @OPENJDK_BUILD_CPU_ARCH@ OPENJDK_BUILD_CPU_BITS := @OPENJDK_BUILD_CPU_BITS@ OPENJDK_BUILD_CPU_ENDIAN := @OPENJDK_BUILD_CPU_ENDIAN@ -OPENJDK_BUILD_CPU_LEGACY := @OPENJDK_BUILD_CPU_LEGACY@ -OPENJDK_BUILD_CPU_LEGACY_LIB := @OPENJDK_BUILD_CPU_LEGACY_LIB@ OPENJDK_BUILD_LIBC := @OPENJDK_BUILD_LIBC@ -OPENJDK_BUILD_OS_INCLUDE_SUBDIR := @OPENJDK_BUILD_OS_INCLUDE_SUBDIR@ - -HOTSPOT_BUILD_OS := @HOTSPOT_BUILD_OS@ -HOTSPOT_BUILD_OS_TYPE := @HOTSPOT_BUILD_OS_TYPE@ -HOTSPOT_BUILD_CPU := @HOTSPOT_BUILD_CPU@ -HOTSPOT_BUILD_CPU_ARCH := @HOTSPOT_BUILD_CPU_ARCH@ -HOTSPOT_BUILD_CPU_DEFINE := @HOTSPOT_BUILD_CPU_DEFINE@ -HOTSPOT_BUILD_LIBC := @HOTSPOT_BUILD_LIBC@ - -OPENJDK_BUILD_CFLAGS_JDKLIB := @OPENJDK_BUILD_CFLAGS_JDKLIB@ -OPENJDK_BUILD_CXXFLAGS_JDKLIB := @OPENJDK_BUILD_CXXFLAGS_JDKLIB@ -OPENJDK_BUILD_LDFLAGS_JDKLIB := @OPENJDK_BUILD_LDFLAGS_JDKLIB@ -OPENJDK_BUILD_CFLAGS_JDKEXE := @OPENJDK_BUILD_CFLAGS_JDKEXE@ -OPENJDK_BUILD_CXXFLAGS_JDKEXE := @OPENJDK_BUILD_CXXFLAGS_JDKEXE@ -OPENJDK_BUILD_LDFLAGS_JDKEXE := @OPENJDK_BUILD_LDFLAGS_JDKEXE@ - -OPENJDK_BUILD_JVM_CFLAGS := @OPENJDK_BUILD_JVM_CFLAGS@ -OPENJDK_BUILD_JVM_LDFLAGS := @OPENJDK_BUILD_JVM_LDFLAGS@ -OPENJDK_BUILD_JVM_ASFLAGS := @OPENJDK_BUILD_JVM_ASFLAGS@ -OPENJDK_BUILD_JVM_LIBS := @OPENJDK_BUILD_JVM_LIBS@ - -OPENJDK_BUILD_FDLIBM_CFLAGS := @OPENJDK_BUILD_FDLIBM_CFLAGS@ -BUILD_CC_DISABLE_WARNING_PREFIX := @BUILD_CC_DISABLE_WARNING_PREFIX@ +OPENJDK_BUILD_OS_INCLUDE_SUBDIR := @OPENJDK_TARGET_OS_INCLUDE_SUBDIR@ # Target platform value in ModuleTarget class file attribute. OPENJDK_MODULE_TARGET_PLATFORM := @OPENJDK_MODULE_TARGET_PLATFORM@ @@ -141,7 +135,12 @@ RELEASE_FILE_LIBC := @RELEASE_FILE_LIBC@ SOURCE_DATE := @SOURCE_DATE@ ISO_8601_FORMAT_STRING := @ISO_8601_FORMAT_STRING@ -SOURCE_DATE_ISO_8601_FIXED := @SOURCE_DATE_ISO_8601_FIXED@ + +ifneq ($(SOURCE_DATE), updated) + # For "updated" source date value, these are set in InitSupport.gmk + export SOURCE_DATE_EPOCH := $(SOURCE_DATE) + SOURCE_DATE_ISO_8601 := @SOURCE_DATE_ISO_8601@ +endif LIBM := @LIBM@ LIBDL := @LIBDL@ @@ -150,9 +149,23 @@ LIBPTHREAD := @LIBPTHREAD@ WINENV_ROOT := @WINENV_ROOT@ WINENV_PREFIX := @WINENV_PREFIX@ +ifneq ($(findstring windows.wsl, @OPENJDK_BUILD_OS_ENV@), ) + # Tell WSL to convert PATH between linux and windows + export WSLENV := PATH/l +else ifeq (@OPENJDK_BUILD_OS_ENV@, windows.msys2) + # Prohibit msys2 from attempting any path wrangling + export MSYS2_ARG_CONV_EXCL := "*" +endif + # Save the original path before replacing it with the Visual Studio tools ORIGINAL_PATH := @ORIGINAL_PATH@ +ifeq (@TOOLCHAIN_TYPE@, microsoft) + # The Visual Studio toolchain needs the PATH to be adjusted to include + # Visual Studio tools. + export PATH := @TOOLCHAIN_PATH@:$(PATH) +endif + SYSROOT_CFLAGS := @SYSROOT_CFLAGS@ SYSROOT_LDFLAGS := @SYSROOT_LDFLAGS@ @@ -217,6 +230,8 @@ VERSION_NUMBER_FOUR_POSITIONS := @VERSION_NUMBER_FOUR_POSITIONS@ VERSION_STRING := @VERSION_STRING@ # The short version string, without trailing zeroes and just PRE, if present. VERSION_SHORT := @VERSION_SHORT@ +# The Java specification version. It usually equals the feature version number. +VERSION_SPECIFICATION := @VERSION_FEATURE@ # A GA version is defined by the PRE string being empty. Rather than testing for # that, this variable defines it with true/false. VERSION_IS_GA := @VERSION_IS_GA@ @@ -236,6 +251,57 @@ VERSION_DOCS_API_SINCE := @VERSION_DOCS_API_SINCE@ JDK_SOURCE_TARGET_VERSION := @JDK_SOURCE_TARGET_VERSION@ +# Convenience CFLAGS settings for passing version information into native programs. +VERSION_CFLAGS = \ + -DVERSION_FEATURE=$(VERSION_FEATURE) \ + -DVERSION_INTERIM=$(VERSION_INTERIM) \ + -DVERSION_UPDATE=$(VERSION_UPDATE) \ + -DVERSION_PATCH=$(VERSION_PATCH) \ + -DVERSION_EXTRA1=$(VERSION_EXTRA1) \ + -DVERSION_EXTRA2=$(VERSION_EXTRA2) \ + -DVERSION_EXTRA3=$(VERSION_EXTRA3) \ + -DVERSION_PRE='"$(VERSION_PRE)"' \ + -DVERSION_BUILD=$(VERSION_BUILD) \ + -DVERSION_OPT='"$(VERSION_OPT)"' \ + -DVERSION_NUMBER='"$(VERSION_NUMBER)"' \ + -DVERSION_STRING='"$(VERSION_STRING)"' \ + -DVERSION_SHORT='"$(VERSION_SHORT)"' \ + -DVERSION_SPECIFICATION='"$(VERSION_SPECIFICATION)"' \ + -DVERSION_DATE='"$(VERSION_DATE)"' \ + -DVENDOR_VERSION_STRING='"$(VENDOR_VERSION_STRING)"' \ + -DVERSION_CLASSFILE_MAJOR=$(VERSION_CLASSFILE_MAJOR) \ + -DVERSION_CLASSFILE_MINOR=$(VERSION_CLASSFILE_MINOR) \ + # + +ifneq ($(COMPANY_NAME), ) + # COMPANY_NAME is set to "N/A" in make/conf/branding.conf by default, + # but can be customized with the '--with-vendor-name' configure option. + # Only export "VENDOR" to the build if COMPANY_NAME contains a real value. + # Otherwise the default value for VENDOR, which is used to set the "java.vendor" + # and "java.vm.vendor" properties is hard-coded into the source code (i.e. in + # VersionProps.java.template in the jdk for "java.vendor" and + # vm_version.cpp in the VM for "java.vm.vendor") + ifneq ($(COMPANY_NAME), N/A) + VERSION_CFLAGS += -DVENDOR='"$(COMPANY_NAME)"' + endif +endif + +# Only export VENDOR_URL, VENDOR_URL_BUG and VENDOR_VM_URL_BUG to the build if +# they are not empty. Otherwise, default values which are defined in the sources +# will be used. +ifneq ($(VENDOR_URL), ) + VERSION_CFLAGS += -DVENDOR_URL='"$(VENDOR_URL)"' +endif +ifneq ($(VENDOR_URL_BUG), ) + VERSION_CFLAGS += -DVENDOR_URL_BUG='"$(VENDOR_URL_BUG)"' +endif +ifneq ($(VENDOR_URL_VM_BUG), ) + VERSION_CFLAGS += -DVENDOR_URL_VM_BUG='"$(VENDOR_URL_VM_BUG)"' +endif + +# Different naming strings generated from the above information. +RUNTIME_NAME = $(PRODUCT_NAME) $(PRODUCT_SUFFIX) + # How to compile the code: release, fastdebug or slowdebug DEBUG_LEVEL := @DEBUG_LEVEL@ HOTSPOT_DEBUG_LEVEL := @HOTSPOT_DEBUG_LEVEL@ @@ -277,8 +343,22 @@ ENABLE_FULL_DOCS := @ENABLE_FULL_DOCS@ # You can run $(JDK_OUTPUTDIR)/bin/java OUTPUTDIR := @OUTPUTDIR@ +# Colon left out to be able to override IMAGES_OUTPUTDIR for bootcycle-images +SUPPORT_OUTPUTDIR = $(OUTPUTDIR)/support +BUILDTOOLS_OUTPUTDIR = $(OUTPUTDIR)/buildtools +HOTSPOT_OUTPUTDIR = $(OUTPUTDIR)/hotspot +JDK_OUTPUTDIR = $(OUTPUTDIR)/jdk +IMAGES_OUTPUTDIR = $(OUTPUTDIR)/images +BUNDLES_OUTPUTDIR = $(OUTPUTDIR)/bundles +TESTMAKE_OUTPUTDIR = $(OUTPUTDIR)/test-make +MAKESUPPORT_OUTPUTDIR = $(OUTPUTDIR)/make-support + +JAVA_TMP_DIR = $(SUPPORT_OUTPUTDIR)/javatmp + +# This does not get overridden in a bootcycle build CONFIGURESUPPORT_OUTPUTDIR := @CONFIGURESUPPORT_OUTPUTDIR@ +BUILDJDK_OUTPUTDIR = $(OUTPUTDIR)/buildjdk BUILD_FAILURE_HANDLER := @BUILD_FAILURE_HANDLER@ @@ -308,6 +388,21 @@ BOOT_JDK := @BOOT_JDK@ EXTERNAL_BUILDJDK_PATH := @EXTERNAL_BUILDJDK_PATH@ +ifneq ($(EXTERNAL_BUILDJDK_PATH), ) + EXTERNAL_BUILDJDK := true + CREATE_BUILDJDK := false + BUILD_JDK := $(EXTERNAL_BUILDJDK_PATH) +else + EXTERNAL_BUILDJDK := false + ifeq ($(COMPILE_TYPE), cross) + CREATE_BUILDJDK := true + BUILD_JDK := $(BUILDJDK_OUTPUTDIR)/jdk + else + CREATE_BUILDJDK := false + BUILD_JDK := $(JDK_OUTPUTDIR) + endif +endif + # Whether the boot jdk jar supports --date=TIMESTAMP BOOT_JDK_JAR_SUPPORTS_DATE := @BOOT_JDK_JAR_SUPPORTS_DATE@ @@ -318,10 +413,13 @@ OLDEST_BOOT_JDK_VERSION := @OLDEST_BOOT_JDK_VERSION@ NUM_CORES := @NUM_CORES@ MEMORY_SIZE := @MEMORY_SIZE@ ENABLE_JAVAC_SERVER := @ENABLE_JAVAC_SERVER@ +# Store javac server synchronization files here, and +# the javac server log files. +JAVAC_SERVER_DIR = $(MAKESUPPORT_OUTPUTDIR)/javacservers # Number of parallel jobs to use for compilation -CONF_JOBS := @CONF_JOBS@ -CONF_TEST_JOBS := @CONF_TEST_JOBS@ +JOBS ?= @JOBS@ +TEST_JOBS ?= @TEST_JOBS@ # Default make target DEFAULT_MAKE_TARGET := @DEFAULT_MAKE_TARGET@ @@ -439,7 +537,7 @@ ADLC_LANGSTD_CXXFLAGS := @ADLC_LANGSTD_CXXFLAGS@ ADLC_LDFLAGS := @ADLC_LDFLAGS@ # Tools that potentially need to be cross compilation aware. -CC := @CC@ +CC := @CCACHE@ @ICECC@ @CC@ # CFLAGS used to compile the jdk native libraries (C-code) CFLAGS_JDKLIB := @CFLAGS_JDKLIB@ @@ -465,7 +563,7 @@ EXTRA_CXXFLAGS := @EXTRA_CXXFLAGS@ EXTRA_LDFLAGS := @EXTRA_LDFLAGS@ EXTRA_ASFLAGS := @EXTRA_ASFLAGS@ -CXX := @CXX@ +CXX := @CCACHE@ @ICECC@ @CXX@ CPP := @CPP@ @@ -496,8 +594,8 @@ LIBCXX := @LIBCXX@ # BUILD_CC/BUILD_LD is a compiler/linker that generates code that is runnable on the # build platform. -BUILD_CC := @BUILD_CC@ -BUILD_CXX := @BUILD_CXX@ +BUILD_CC := @BUILD_ICECC@ @BUILD_CC@ +BUILD_CXX := @BUILD_ICECC@ @BUILD_CXX@ BUILD_LD := @BUILD_LD@ BUILD_LDCXX := @BUILD_LDCXX@ BUILD_AS := @BUILD_AS@ @@ -548,24 +646,77 @@ OBJ_SUFFIX := @OBJ_SUFFIX@ STRIPFLAGS := @STRIPFLAGS@ -JAVA_FLAGS := @JAVA_FLAGS@ +JAVA_FLAGS_TMPDIR := -Djava.io.tmpdir=$(JAVA_TMP_DIR) +JAVA_FLAGS := @JAVA_FLAGS@ $(JAVA_FLAGS_TMPDIR) JAVA_FLAGS_BIG := @JAVA_FLAGS_BIG@ JAVA_FLAGS_SMALL := @JAVA_FLAGS_SMALL@ BUILD_JAVA_FLAGS_SMALL := @BUILD_JAVA_FLAGS_SMALL@ JAVA_TOOL_FLAGS_SMALL := @JAVA_TOOL_FLAGS_SMALL@ -# Do not use the *_CMD versions of the variables directly. -JAVA_CMD := @JAVA_CMD@ -JAVAC_CMD := @JAVAC_CMD@ -JAVADOC_CMD := @JAVADOC_CMD@ -JAR_CMD := @JAR_CMD@ +# The *_CMD variables are defined separately to be easily overridden in bootcycle-spec.gmk +# for bootcycle-images build. Make sure to keep them in sync. Do not use the *_CMD +# versions of the variables directly. +JAVA_CMD := @JAVA@ +JAVAC_CMD := @JAVAC@ +JAVADOC_CMD := @JAVADOC@ +JAR_CMD := @JAR@ +JLINK_CMD := @FIXPATH@ $(BUILD_JDK)/bin/jlink +JMOD_CMD := @FIXPATH@ $(BUILD_JDK)/bin/jmod +# These variables are meant to be used. They are defined with = instead of := to make +# it possible to override only the *_CMD variables. +JAVA = $(JAVA_CMD) $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) +JAVA_SMALL = $(JAVA_CMD) $(JAVA_FLAGS_SMALL) $(JAVA_FLAGS) +JAVAC = $(JAVAC_CMD) +JAVADOC = $(JAVADOC_CMD) +JAR = $(JAR_CMD) +JLINK = $(JLINK_CMD) +JMOD = $(JMOD_CMD) JTREG_JDK := @JTREG_JDK@ +JTREG_JAVA = @FIXPATH@ $(JTREG_JDK)/bin/java $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) -BOOTCYCLE_JVM_ARGS_BIG := @BOOTCYCLE_JVM_ARGS_BIG@ +BUILD_JAVA_FLAGS := @BOOTCYCLE_JVM_ARGS_BIG@ +BUILD_JAVA = @FIXPATH@ $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS) +BUILD_JAVA_SMALL = @FIXPATH@ $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS_SMALL) +BUILD_JAVAC = @FIXPATH@ $(BUILD_JDK)/bin/javac +BUILD_JAR = @FIXPATH@ $(BUILD_JDK)/bin/jar DOCS_REFERENCE_JAVADOC := @DOCS_REFERENCE_JAVADOC@ +# A file containing a way to uniquely identify the source code revision that +# the build was created from +SOURCE_REVISION_TRACKER := $(SUPPORT_OUTPUTDIR)/src-rev/source-revision-tracker + +# Interim langtools modules and arguments +INTERIM_LANGTOOLS_BASE_MODULES := java.compiler jdk.compiler jdk.internal.md jdk.javadoc +INTERIM_LANGTOOLS_MODULES := $(addsuffix .interim, $(INTERIM_LANGTOOLS_BASE_MODULES)) +INTERIM_LANGTOOLS_ADD_EXPORTS := \ + --add-exports java.base/sun.reflect.annotation=jdk.compiler.interim \ + --add-exports java.base/jdk.internal.jmod=jdk.compiler.interim \ + --add-exports java.base/jdk.internal.misc=jdk.compiler.interim \ + --add-exports java.base/sun.invoke.util=jdk.compiler.interim \ + --add-exports java.base/jdk.internal.javac=java.compiler.interim \ + --add-exports java.base/jdk.internal.javac=jdk.compiler.interim \ + --add-exports jdk.internal.opt/jdk.internal.opt=jdk.compiler.interim \ + --add-exports jdk.internal.opt/jdk.internal.opt=jdk.javadoc.interim \ + # +INTERIM_LANGTOOLS_MODULES_COMMA := $(strip $(subst $(SPACE),$(COMMA),$(strip \ + $(INTERIM_LANGTOOLS_MODULES)))) +INTERIM_LANGTOOLS_ARGS := \ + --limit-modules java.base,jdk.zipfs,$(INTERIM_LANGTOOLS_MODULES_COMMA) \ + --add-modules $(INTERIM_LANGTOOLS_MODULES_COMMA) \ + --module-path $(BUILDTOOLS_OUTPUTDIR)/interim_langtools_modules \ + --patch-module java.base=$(BUILDTOOLS_OUTPUTDIR)/gensrc/java.base.interim \ + $(INTERIM_LANGTOOLS_ADD_EXPORTS) \ + # +JAVAC_MAIN_CLASS := -m jdk.compiler.interim/com.sun.tools.javac.Main +JAVADOC_MAIN_CLASS := -m jdk.javadoc.interim/jdk.javadoc.internal.tool.Main + +# You run the new javac using the boot jdk with $(BOOT_JDK)/bin/java $(NEW_JAVAC) ... +# Use = assignment to be able to override in bootcycle-spec.gmk +NEW_JAVAC = $(INTERIM_LANGTOOLS_ARGS) $(JAVAC_MAIN_CLASS) +NEW_JAVADOC = $(INTERIM_LANGTOOLS_ARGS) $(JAVADOC_MAIN_CLASS) + JMOD_COMPRESS := @JMOD_COMPRESS@ JLINK_KEEP_PACKAGED_MODULES := @JLINK_KEEP_PACKAGED_MODULES@ JLINK_PRODUCE_LINKABLE_RUNTIME := @JLINK_PRODUCE_LINKABLE_RUNTIME@ @@ -578,6 +729,8 @@ AWK := @AWK@ BASENAME := @BASENAME@ CAT := @CAT@ CCACHE := @CCACHE@ +# CD is going away, but remains to cater for legacy makefiles. +CD := cd CHMOD := @CHMOD@ CMAKE := @CMAKE@ CODESIGN := @CODESIGN@ @@ -628,6 +781,7 @@ MT := @MT@ RC := @RC@ DUMPBIN := @DUMPBIN@ PATHTOOL := @PATHTOOL@ +WSLPATH := @WSLPATH@ LDD := @LDD@ OTOOL := @OTOOL@ READELF := @READELF@ @@ -692,11 +846,110 @@ OS_VERSION_MICRO := @OS_VERSION_MICRO@ # Arm SVE SVE_CFLAGS := @SVE_CFLAGS@ -BUILD_ICECC := @BUILD_ICECC@ -ICECC := @ICECC@ -TOOLCHAIN_PATH := @TOOLCHAIN_PATH@ -LOCALE_USED := @LOCALE_USED@ -CLASSPATH := @CLASSPATH@ +# Images directory definitions +JDK_IMAGE_SUBDIR := jdk +JRE_IMAGE_SUBDIR := jre +JCOV_IMAGE_SUBDIR := jdk-jcov +STATIC_JDK_IMAGE_SUBDIR := static-jdk + +# Colon left out to be able to override output dir for bootcycle-images +JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_IMAGE_SUBDIR) +JRE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_IMAGE_SUBDIR) +STATIC_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(STATIC_JDK_IMAGE_SUBDIR) +JCOV_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JCOV_IMAGE_SUBDIR) + +# Test image, as above +TEST_IMAGE_SUBDIR := test +TEST_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(TEST_IMAGE_SUBDIR) + +# Symbols image +SYMBOLS_IMAGE_SUBDIR := symbols +SYMBOLS_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(SYMBOLS_IMAGE_SUBDIR) + +# Interim image +INTERIM_JMODS_DIR := $(SUPPORT_OUTPUTDIR)/interim-jmods +INTERIM_IMAGE_DIR := $(SUPPORT_OUTPUTDIR)/interim-image + +# Docs image +DOCS_JDK_IMAGE_SUBDIR := docs +DOCS_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JDK_IMAGE_SUBDIR) +DOCS_JAVASE_IMAGE_SUBDIR := docs-javase +DOCS_JAVASE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JAVASE_IMAGE_SUBDIR) +DOCS_REFERENCE_IMAGE_SUBDIR := docs-reference +DOCS_REFERENCE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_REFERENCE_IMAGE_SUBDIR) +# Output docs directly into image +DOCS_OUTPUTDIR := $(DOCS_JDK_IMAGE_DIR) + +# Static libs image +STATIC_LIBS_IMAGE_SUBDIR := static-libs +STATIC_LIBS_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_IMAGE_SUBDIR) + +# Graal static libs image +STATIC_LIBS_GRAAL_IMAGE_SUBDIR := static-libs-graal +STATIC_LIBS_GRAAL_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_GRAAL_IMAGE_SUBDIR) + +# Graal builder image +GRAAL_BUILDER_IMAGE_SUBDIR := graal-builder-jdk +GRAAL_BUILDER_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(GRAAL_BUILDER_IMAGE_SUBDIR) + +# Macosx bundles directory definitions +JDK_MACOSX_BUNDLE_SUBDIR := jdk-bundle +JRE_MACOSX_BUNDLE_SUBDIR := jre-bundle +JDK_MACOSX_BUNDLE_SUBDIR_SIGNED := jdk-bundle-signed +JRE_MACOSX_BUNDLE_SUBDIR_SIGNED := jre-bundle-signed +JDK_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR) +JRE_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR) +JDK_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR_SIGNED) +JRE_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR_SIGNED) +JDK_MACOSX_BUNDLE_TOP_SUBDIR = jdk-$(VERSION_NUMBER).jdk +JRE_MACOSX_BUNDLE_TOP_SUBDIR = jre-$(VERSION_NUMBER).jre +JDK_MACOSX_CONTENTS_SUBDIR = $(JDK_MACOSX_BUNDLE_TOP_SUBDIR)/Contents +JRE_MACOSX_CONTENTS_SUBDIR = $(JRE_MACOSX_BUNDLE_TOP_SUBDIR)/Contents +JDK_MACOSX_CONTENTS_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_CONTENTS_SUBDIR) +JRE_MACOSX_CONTENTS_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_CONTENTS_SUBDIR) +JDK_MACOSX_BUNDLE_TOP_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_BUNDLE_TOP_SUBDIR) +JRE_MACOSX_BUNDLE_TOP_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_BUNDLE_TOP_SUBDIR) + +# Bundle names +ifneq ($(VERSION_BUILD), ) + BASE_NAME := $(VERSION_SHORT)+$(VERSION_BUILD)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) +else + BASE_NAME := $(VERSION_SHORT)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) +endif + +ifeq ($(DEBUG_LEVEL), fastdebug) + DEBUG_PART := -debug +else ifneq ($(DEBUG_LEVEL), release) + DEBUG_PART := -$(DEBUG_LEVEL) +endif +ifeq ($(OPENJDK_TARGET_OS), windows) + JDK_BUNDLE_EXTENSION := zip +else + JDK_BUNDLE_EXTENSION := tar.gz +endif +JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) +JRE_BUNDLE_NAME := jre-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) +JDK_SYMBOLS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART)-symbols.tar.gz +TEST_DEMOS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests-demos$(DEBUG_PART).tar.gz +TEST_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests$(DEBUG_PART).tar.gz +DOCS_JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz +DOCS_JAVASE_BUNDLE_NAME := javase-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz +DOCS_REFERENCE_BUNDLE_NAME := jdk-reference-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz +STATIC_LIBS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs$(DEBUG_PART).tar.gz +STATIC_LIBS_GRAAL_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs-graal$(DEBUG_PART).tar.gz +STATIC_JDK_BUNDLE_NAME := static-jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) +JCOV_BUNDLE_NAME := jdk-jcov-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) + +JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_BUNDLE_NAME) +JRE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JRE_BUNDLE_NAME) +JDK_SYMBOLS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_SYMBOLS_BUNDLE_NAME) +TEST_DEMOS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_DEMOS_BUNDLE_NAME) +TEST_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_BUNDLE_NAME) +DOCS_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JDK_BUNDLE_NAME) +DOCS_JAVASE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JAVASE_BUNDLE_NAME) +DOCS_REFERENCE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_REFERENCE_BUNDLE_NAME) +STATIC_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(STATIC_JDK_BUNDLE_NAME) +JCOV_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JCOV_BUNDLE_NAME) # This macro is called to allow inclusion of closed source counterparts. # Unless overridden in closed sources, it expands to nothing. @@ -707,4 +960,4 @@ define IncludeCustomExtension endef # Include the custom-spec.gmk file if it exists --include $(dir $(SPEC))/custom-spec.gmk +-include $(dir @SPEC@)/custom-spec.gmk diff --git a/make/common/CommonVars.gmk b/make/common/CommonVars.gmk deleted file mode 100644 index 1ee6cc27481..00000000000 --- a/make/common/CommonVars.gmk +++ /dev/null @@ -1,442 +0,0 @@ -# -# Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -include MakeIncludeStart.gmk -ifeq ($(INCLUDE), true) - -################################################################################ -# CommonVars include common variables and definitions used in multiple -# makefiles. -################################################################################ - -# Make sure all shell commands are executed with a proper locale -export LC_ALL := $(LOCALE_USED) - -# Make sure we override any local CLASSPATH variable -export CLASSPATH := $(CLASSPATH) - -# The default make arguments -MAKE_ARGS = $(MAKE_LOG_FLAGS) -r -R -I $(TOPDIR)/make/common SPEC=$(SPEC) \ - MAKE_LOG_FLAGS="$(MAKE_LOG_FLAGS)" $(MAKE_LOG_VARS) - -SHELL := $(BASH) $(BASH_ARGS) - -ifneq ($(SOURCE_DATE), updated) - # For "updated" source date value, these are set in InitSupport.gmk - export SOURCE_DATE_EPOCH := $(SOURCE_DATE) - SOURCE_DATE_ISO_8601 := $(SOURCE_DATE_ISO_8601_FIXED) -endif - -ifneq ($(findstring windows.wsl, $(OPENJDK_BUILD_OS_ENV)), ) - # Tell WSL to convert PATH between linux and windows - export WSLENV := PATH/l -else ifeq ($(OPENJDK_BUILD_OS_ENV), windows.msys2) - # Prohibit msys2 from attempting any path wrangling - export MSYS2_ARG_CONV_EXCL := "*" -endif - -ifeq ($(TOOLCHAIN_TYPE), microsoft) - # The Visual Studio toolchain needs the PATH to be adjusted to include - # Visual Studio tools. - export PATH := $(TOOLCHAIN_PATH):$(PATH) -endif - -# The Java specification version. It usually equals the feature version number. -VERSION_SPECIFICATION := $(VERSION_FEATURE) - -# Convenience CFLAGS settings for passing version information into native programs. -VERSION_CFLAGS = \ - -DVERSION_FEATURE=$(VERSION_FEATURE) \ - -DVERSION_INTERIM=$(VERSION_INTERIM) \ - -DVERSION_UPDATE=$(VERSION_UPDATE) \ - -DVERSION_PATCH=$(VERSION_PATCH) \ - -DVERSION_EXTRA1=$(VERSION_EXTRA1) \ - -DVERSION_EXTRA2=$(VERSION_EXTRA2) \ - -DVERSION_EXTRA3=$(VERSION_EXTRA3) \ - -DVERSION_PRE='"$(VERSION_PRE)"' \ - -DVERSION_BUILD=$(VERSION_BUILD) \ - -DVERSION_OPT='"$(VERSION_OPT)"' \ - -DVERSION_NUMBER='"$(VERSION_NUMBER)"' \ - -DVERSION_STRING='"$(VERSION_STRING)"' \ - -DVERSION_SHORT='"$(VERSION_SHORT)"' \ - -DVERSION_SPECIFICATION='"$(VERSION_SPECIFICATION)"' \ - -DVERSION_DATE='"$(VERSION_DATE)"' \ - -DVENDOR_VERSION_STRING='"$(VENDOR_VERSION_STRING)"' \ - -DVERSION_CLASSFILE_MAJOR=$(VERSION_CLASSFILE_MAJOR) \ - -DVERSION_CLASSFILE_MINOR=$(VERSION_CLASSFILE_MINOR) \ - # - -ifneq ($(COMPANY_NAME), ) - # COMPANY_NAME is set to "N/A" in make/conf/branding.conf by default, - # but can be customized with the '--with-vendor-name' configure option. - # Only export "VENDOR" to the build if COMPANY_NAME contains a real value. - # Otherwise the default value for VENDOR, which is used to set the "java.vendor" - # and "java.vm.vendor" properties is hard-coded into the source code (i.e. in - # VersionProps.java.template in the jdk for "java.vendor" and - # vm_version.cpp in the VM for "java.vm.vendor") - ifneq ($(COMPANY_NAME), N/A) - VERSION_CFLAGS += -DVENDOR='"$(COMPANY_NAME)"' - endif -endif - -# Only export VENDOR_URL, VENDOR_URL_BUG and VENDOR_VM_URL_BUG to the build if -# they are not empty. Otherwise, default values which are defined in the sources -# will be used. -ifneq ($(VENDOR_URL), ) - VERSION_CFLAGS += -DVENDOR_URL='"$(VENDOR_URL)"' -endif -ifneq ($(VENDOR_URL_BUG), ) - VERSION_CFLAGS += -DVENDOR_URL_BUG='"$(VENDOR_URL_BUG)"' -endif -ifneq ($(VENDOR_URL_VM_BUG), ) - VERSION_CFLAGS += -DVENDOR_URL_VM_BUG='"$(VENDOR_URL_VM_BUG)"' -endif - -# Different naming strings generated from the above information. -RUNTIME_NAME = $(PRODUCT_NAME) $(PRODUCT_SUFFIX) - -# Colon left out to be able to override IMAGES_OUTPUTDIR for bootcycle-images -SUPPORT_OUTPUTDIR = $(OUTPUTDIR)/support -BUILDTOOLS_OUTPUTDIR = $(OUTPUTDIR)/buildtools - -HOTSPOT_OUTPUTDIR = $(OUTPUTDIR)/hotspot -JDK_OUTPUTDIR = $(OUTPUTDIR)/jdk -IMAGES_OUTPUTDIR = $(OUTPUTDIR)/images -BUNDLES_OUTPUTDIR = $(OUTPUTDIR)/bundles -TESTMAKE_OUTPUTDIR = $(OUTPUTDIR)/test-make -MAKESUPPORT_OUTPUTDIR = $(OUTPUTDIR)/make-support - -JAVA_TMP_DIR = $(SUPPORT_OUTPUTDIR)/javatmp - -BUILDJDK_OUTPUTDIR = $(OUTPUTDIR)/buildjdk - -ifneq ($(EXTERNAL_BUILDJDK_PATH), ) - EXTERNAL_BUILDJDK := true - CREATE_BUILDJDK := false - BUILD_JDK := $(EXTERNAL_BUILDJDK_PATH) -else - EXTERNAL_BUILDJDK := false - ifeq ($(COMPILE_TYPE), cross) - CREATE_BUILDJDK := true - BUILD_JDK := $(BUILDJDK_OUTPUTDIR)/jdk - else - CREATE_BUILDJDK := false - BUILD_JDK := $(JDK_OUTPUTDIR) - endif -endif - -# Store javac server synchronization files here, and -# the javac server log files. -JAVAC_SERVER_DIR = $(MAKESUPPORT_OUTPUTDIR)/javacservers - -# Number of parallel jobs to use for compilation -JOBS ?= $(CONF_JOBS) -TEST_JOBS ?= $(CONF_TEST_JOBS) - -# Tools that potentially need to be cross compilation aware. -CC := $(CCACHE) $(ICECC) $(CC) - -CXX := $(CCACHE) $(ICECC) $(CXX) - -# BUILD_CC/BUILD_LD is a compiler/linker that generates code that is runnable on the -# build platform. -BUILD_CC := $(BUILD_ICECC) $(BUILD_CC) -BUILD_CXX := $(BUILD_ICECC) $(BUILD_CXX) - -JAVA_FLAGS_TMPDIR := -Djava.io.tmpdir=$(JAVA_TMP_DIR) -JAVA_FLAGS := $(JAVA_FLAGS) $(JAVA_FLAGS_TMPDIR) - -JLINK_CMD := $(FIXPATH) $(BUILD_JDK)/bin/jlink -JMOD_CMD := $(FIXPATH) $(BUILD_JDK)/bin/jmod - -# These variables are meant to be used. They are defined with = instead of := to make -# it possible to override only the *_CMD variables. -JAVA = $(JAVA_CMD) $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) -JAVA_SMALL = $(JAVA_CMD) $(JAVA_FLAGS_SMALL) $(JAVA_FLAGS) -JAVAC = $(JAVAC_CMD) -JAVADOC = $(JAVADOC_CMD) -JAR = $(JAR_CMD) -JLINK = $(JLINK_CMD) -JMOD = $(JMOD_CMD) - -JTREG_JAVA = $(FIXPATH) $(JTREG_JDK)/bin/java $(JAVA_FLAGS_BIG) $(JAVA_FLAGS) - -BUILD_JAVA_FLAGS := $(BOOTCYCLE_JVM_ARGS_BIG) -BUILD_JAVA = $(FIXPATH) $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS) -BUILD_JAVA_SMALL = $(FIXPATH) $(BUILD_JDK)/bin/java $(BUILD_JAVA_FLAGS_SMALL) -BUILD_JAVAC = $(FIXPATH) $(BUILD_JDK)/bin/javac -BUILD_JAR = $(FIXPATH) $(BUILD_JDK)/bin/jar - -# A file containing a way to uniquely identify the source code revision that -# the build was created from -SOURCE_REVISION_TRACKER := $(SUPPORT_OUTPUTDIR)/src-rev/source-revision-tracker - -# Interim langtools modules and arguments -INTERIM_LANGTOOLS_BASE_MODULES := java.compiler jdk.compiler jdk.internal.md jdk.javadoc -INTERIM_LANGTOOLS_MODULES := $(addsuffix .interim, $(INTERIM_LANGTOOLS_BASE_MODULES)) -INTERIM_LANGTOOLS_ADD_EXPORTS := \ - --add-exports java.base/sun.reflect.annotation=jdk.compiler.interim \ - --add-exports java.base/jdk.internal.jmod=jdk.compiler.interim \ - --add-exports java.base/jdk.internal.misc=jdk.compiler.interim \ - --add-exports java.base/sun.invoke.util=jdk.compiler.interim \ - --add-exports java.base/jdk.internal.javac=java.compiler.interim \ - --add-exports java.base/jdk.internal.javac=jdk.compiler.interim \ - --add-exports jdk.internal.opt/jdk.internal.opt=jdk.compiler.interim \ - --add-exports jdk.internal.opt/jdk.internal.opt=jdk.javadoc.interim \ - # -INTERIM_LANGTOOLS_MODULES_COMMA := $(strip $(subst $(SPACE),$(COMMA),$(strip \ - $(INTERIM_LANGTOOLS_MODULES)))) -INTERIM_LANGTOOLS_ARGS := \ - --limit-modules java.base,jdk.zipfs,$(INTERIM_LANGTOOLS_MODULES_COMMA) \ - --add-modules $(INTERIM_LANGTOOLS_MODULES_COMMA) \ - --module-path $(BUILDTOOLS_OUTPUTDIR)/interim_langtools_modules \ - --patch-module java.base=$(BUILDTOOLS_OUTPUTDIR)/gensrc/java.base.interim \ - $(INTERIM_LANGTOOLS_ADD_EXPORTS) \ - # - -JAVADOC_MAIN_CLASS := -m jdk.javadoc.interim/jdk.javadoc.internal.tool.Main -# Use = assignment to pick up overridden INTERIM_LANGTOOLS_ARGS in bootcycle builds -NEW_JAVADOC = $(INTERIM_LANGTOOLS_ARGS) $(JAVADOC_MAIN_CLASS) - -# CD is going away, but remains to cater for legacy makefiles. -CD := cd - -# Images directory definitions -JDK_IMAGE_SUBDIR := jdk -JRE_IMAGE_SUBDIR := jre -JCOV_IMAGE_SUBDIR := jdk-jcov -STATIC_JDK_IMAGE_SUBDIR := static-jdk - -# Colon left out to be able to override output dir for bootcycle-images -ifeq ($(JDK_IMAGE_DIR), ) - JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_IMAGE_SUBDIR) -endif -JRE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_IMAGE_SUBDIR) -STATIC_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(STATIC_JDK_IMAGE_SUBDIR) -ifeq ($(JCOV_IMAGE_DIR), ) - JCOV_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(JCOV_IMAGE_SUBDIR) -endif -# Test image, as above -TEST_IMAGE_SUBDIR := test -ifeq ($(TEST_IMAGE_DIR), ) - TEST_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(TEST_IMAGE_SUBDIR) -endif - -# Symbols image -SYMBOLS_IMAGE_SUBDIR := symbols -ifeq ($(SYMBOLS_IMAGE_DIR), ) - SYMBOLS_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(SYMBOLS_IMAGE_SUBDIR) -endif - -# Interim image -INTERIM_JMODS_DIR := $(SUPPORT_OUTPUTDIR)/interim-jmods -INTERIM_IMAGE_DIR := $(SUPPORT_OUTPUTDIR)/interim-image - -# Docs image -DOCS_JDK_IMAGE_SUBDIR := docs -DOCS_JDK_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JDK_IMAGE_SUBDIR) -DOCS_JAVASE_IMAGE_SUBDIR := docs-javase -DOCS_JAVASE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_JAVASE_IMAGE_SUBDIR) -DOCS_REFERENCE_IMAGE_SUBDIR := docs-reference -DOCS_REFERENCE_IMAGE_DIR = $(IMAGES_OUTPUTDIR)/$(DOCS_REFERENCE_IMAGE_SUBDIR) -# Output docs directly into image -DOCS_OUTPUTDIR := $(DOCS_JDK_IMAGE_DIR) - -# Static libs image -STATIC_LIBS_IMAGE_SUBDIR := static-libs -STATIC_LIBS_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_IMAGE_SUBDIR) - -# Graal static libs image -STATIC_LIBS_GRAAL_IMAGE_SUBDIR := static-libs-graal -STATIC_LIBS_GRAAL_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_GRAAL_IMAGE_SUBDIR) - -# Graal builder image -GRAAL_BUILDER_IMAGE_SUBDIR := graal-builder-jdk -GRAAL_BUILDER_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(GRAAL_BUILDER_IMAGE_SUBDIR) - -# Macosx bundles directory definitions -JDK_MACOSX_BUNDLE_SUBDIR := jdk-bundle -JRE_MACOSX_BUNDLE_SUBDIR := jre-bundle -JDK_MACOSX_BUNDLE_SUBDIR_SIGNED := jdk-bundle-signed -JRE_MACOSX_BUNDLE_SUBDIR_SIGNED := jre-bundle-signed -JDK_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR) -JRE_MACOSX_BUNDLE_DIR = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR) -JDK_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JDK_MACOSX_BUNDLE_SUBDIR_SIGNED) -JRE_MACOSX_BUNDLE_DIR_SIGNED = $(IMAGES_OUTPUTDIR)/$(JRE_MACOSX_BUNDLE_SUBDIR_SIGNED) -JDK_MACOSX_BUNDLE_TOP_SUBDIR = jdk-$(VERSION_NUMBER).jdk -JRE_MACOSX_BUNDLE_TOP_SUBDIR = jre-$(VERSION_NUMBER).jre -JDK_MACOSX_CONTENTS_SUBDIR = $(JDK_MACOSX_BUNDLE_TOP_SUBDIR)/Contents -JRE_MACOSX_CONTENTS_SUBDIR = $(JRE_MACOSX_BUNDLE_TOP_SUBDIR)/Contents -JDK_MACOSX_CONTENTS_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_CONTENTS_SUBDIR) -JRE_MACOSX_CONTENTS_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_CONTENTS_SUBDIR) -JDK_MACOSX_BUNDLE_TOP_DIR = $(JDK_MACOSX_BUNDLE_DIR)/$(JDK_MACOSX_BUNDLE_TOP_SUBDIR) -JRE_MACOSX_BUNDLE_TOP_DIR = $(JRE_MACOSX_BUNDLE_DIR)/$(JRE_MACOSX_BUNDLE_TOP_SUBDIR) - -# Bundle names -ifneq ($(VERSION_BUILD), ) - BASE_NAME := $(VERSION_SHORT)+$(VERSION_BUILD)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) -else - BASE_NAME := $(VERSION_SHORT)_$(OPENJDK_TARGET_BUNDLE_PLATFORM) -endif - -ifeq ($(DEBUG_LEVEL), fastdebug) - DEBUG_PART := -debug -else ifneq ($(DEBUG_LEVEL), release) - DEBUG_PART := -$(DEBUG_LEVEL) -endif -ifeq ($(OPENJDK_TARGET_OS), windows) - JDK_BUNDLE_EXTENSION := zip -else - JDK_BUNDLE_EXTENSION := tar.gz -endif -JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) -JRE_BUNDLE_NAME := jre-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) -JDK_SYMBOLS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin$(DEBUG_PART)-symbols.tar.gz -TEST_DEMOS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests-demos$(DEBUG_PART).tar.gz -TEST_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-tests$(DEBUG_PART).tar.gz -DOCS_JDK_BUNDLE_NAME := jdk-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz -DOCS_JAVASE_BUNDLE_NAME := javase-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz -DOCS_REFERENCE_BUNDLE_NAME := jdk-reference-$(BASE_NAME)_doc-api-spec$(DEBUG_PART).tar.gz -STATIC_LIBS_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs$(DEBUG_PART).tar.gz -STATIC_LIBS_GRAAL_BUNDLE_NAME := jdk-$(BASE_NAME)_bin-static-libs-graal$(DEBUG_PART).tar.gz -STATIC_JDK_BUNDLE_NAME := static-jdk-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) -JCOV_BUNDLE_NAME := jdk-jcov-$(BASE_NAME)_bin$(DEBUG_PART).$(JDK_BUNDLE_EXTENSION) - -JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_BUNDLE_NAME) -JRE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JRE_BUNDLE_NAME) -JDK_SYMBOLS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JDK_SYMBOLS_BUNDLE_NAME) -TEST_DEMOS_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_DEMOS_BUNDLE_NAME) -TEST_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(TEST_BUNDLE_NAME) -DOCS_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JDK_BUNDLE_NAME) -DOCS_JAVASE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_JAVASE_BUNDLE_NAME) -DOCS_REFERENCE_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(DOCS_REFERENCE_BUNDLE_NAME) -STATIC_JDK_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(STATIC_JDK_BUNDLE_NAME) -JCOV_BUNDLE := $(BUNDLES_OUTPUTDIR)/$(JCOV_BUNDLE_NAME) - -ifeq ($(IS_BUILD_JDK_SPEC), true) - CC := $(BUILD_CC) - CXX := $(BUILD_CXX) - # Ideally this should be probed by configure but that is tricky to implement, - # and this should work in most cases. - CPP := $(BUILD_CC) -E - LD := $(BUILD_LD) - LDCXX := $(BUILD_LDCXX) - AS := $(BUILD_AS) - NM := $(BUILD_NM) - AR := $(BUILD_AR) - LIB := $(BUILD_LIB) - OBJCOPY := $(BUILD_OBJCOPY) - STRIP := $(BUILD_STRIP) - SYSROOT_CFLAGS := $(BUILD_SYSROOT_CFLAGS) - SYSROOT_LDFLAGS := $(BUILD_SYSROOT_LDFLAGS) - - # These directories should not be moved to BUILDJDK_OUTPUTDIR - HOTSPOT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(HOTSPOT_OUTPUTDIR)) - BUILDTOOLS_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(BUILDTOOLS_OUTPUTDIR)) - SUPPORT_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(SUPPORT_OUTPUTDIR)) - JDK_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(JDK_OUTPUTDIR)) - IMAGES_OUTPUTDIR := $(patsubst $(OUTPUTDIR)%,$(BUILDJDK_OUTPUTDIR)%,$(IMAGES_OUTPUTDIR)) - - OPENJDK_TARGET_CPU := $(OPENJDK_BUILD_CPU) - OPENJDK_TARGET_CPU_ARCH := $(OPENJDK_BUILD_CPU_ARCH) - OPENJDK_TARGET_CPU_BITS := $(OPENJDK_BUILD_CPU_BITS) - OPENJDK_TARGET_CPU_ENDIAN := $(OPENJDK_BUILD_CPU_ENDIAN) - OPENJDK_TARGET_CPU_LEGACY := $(OPENJDK_BUILD_CPU_LEGACY) - OPENJDK_TARGET_LIBC := $(OPENJDK_BUILD_LIBC) - OPENJDK_TARGET_OS_INCLUDE_SUBDIR := $(OPENJDK_BUILD_OS_INCLUDE_SUBDIR) - - HOTSPOT_TARGET_OS := $(HOTSPOT_BUILD_OS) - HOTSPOT_TARGET_OS_TYPE := $(HOTSPOT_BUILD_OS_TYPE) - HOTSPOT_TARGET_CPU := $(HOTSPOT_BUILD_CPU) - HOTSPOT_TARGET_CPU_ARCH := $(HOTSPOT_BUILD_CPU_ARCH) - HOTSPOT_TARGET_CPU_DEFINE := $(HOTSPOT_BUILD_CPU_DEFINE) - HOTSPOT_TARGET_LIBC := $(HOTSPOT_BUILD_LIBC) - - CFLAGS_JDKLIB := $(OPENJDK_BUILD_CFLAGS_JDKLIB) - CXXFLAGS_JDKLIB := $(OPENJDK_BUILD_CXXFLAGS_JDKLIB) - LDFLAGS_JDKLIB := $(OPENJDK_BUILD_LDFLAGS_JDKLIB) - CFLAGS_JDKEXE := $(OPENJDK_BUILD_CFLAGS_JDKEXE) - CXXFLAGS_JDKEXE := $(OPENJDK_BUILD_CXXFLAGS_JDKEXE) - LDFLAGS_JDKEXE := $(OPENJDK_BUILD_LDFLAGS_JDKEXE) - - JVM_CFLAGS := $(OPENJDK_BUILD_JVM_CFLAGS) - JVM_LDFLAGS := $(OPENJDK_BUILD_JVM_LDFLAGS) - JVM_ASFLAGS := $(OPENJDK_BUILD_JVM_ASFLAGS) - JVM_LIBS := $(OPENJDK_BUILD_JVM_LIBS) - - FDLIBM_CFLAGS := $(OPENJDK_BUILD_FDLIBM_CFLAGS) - - INTERIM_LANGTOOLS_ARGS := $(subst $(OUTPUTDIR),$(BUILDJDK_OUTPUTDIR),$(INTERIM_LANGTOOLS_ARGS)) - - # The compiler for the build platform is likely not warning compatible with the official - # compiler. - WARNINGS_AS_ERRORS := false - DISABLE_WARNING_PREFIX := $(BUILD_CC_DISABLE_WARNING_PREFIX) - - # Save speed and disk space by not enabling debug symbols for the buildjdk - ENABLE_DEBUG_SYMBOLS := false - - JVM_VARIANTS := server - JVM_VARIANT_MAIN := server - JVM_FEATURES_server := cds compiler1 compiler2 g1gc serialgc - - # Some users still set EXTRA_*FLAGS on the make command line. Must - # make sure to override that when building buildjdk. - override EXTRA_CFLAGS := - override EXTRA_CXXFLAGS := - override EXTRA_LDFLAGS := - - # hsdis is not needed - HSDIS_BACKEND := none - ENABLE_HSDIS_BUNDLING := false -endif - -ifeq ($(IS_BOOTCYCLE_JDK_SPEC), true) - # Override specific values to do a boot cycle build - - # Use a different Boot JDK - BOOT_JDK := $(JDK_IMAGE_DIR) - - # The bootcycle build has a different output directory - OLD_OUTPUTDIR := $(OUTPUTDIR) - OUTPUTDIR := $(OLD_OUTPUTDIR)/bootcycle-build - # No spaces in patsubst to avoid leading space in variable - JAVAC_SERVER_DIR := $(patsubst $(OLD_OUTPUTDIR)%,$(OUTPUTDIR)%,$(JAVAC_SERVER_DIR)) - - JAVA_CMD := $(FIXPATH) $(BOOT_JDK)/bin/java - JAVAC_CMD := $(FIXPATH) $(BOOT_JDK)/bin/javac - JAR_CMD := $(FIXPATH) $(BOOT_JDK)/bin/jar - # The bootcycle JVM arguments may differ from the original boot jdk. - JAVA_FLAGS_BIG := $(BOOTCYCLE_JVM_ARGS_BIG) - # Any CDS settings generated for the bootjdk are invalid in the bootcycle build. - # By filtering out those JVM args, the bootcycle JVM will use its default - # settings for CDS. - JAVA_FLAGS := $(filter-out -XX:SharedArchiveFile% -Xshare%, $(JAVA_FLAGS)) -endif - -################################################################################ - -include MakeIncludeEnd.gmk -endif # include guard diff --git a/make/common/MakeBase.gmk b/make/common/MakeBase.gmk index 2bc5d562924..97ef88932cb 100644 --- a/make/common/MakeBase.gmk +++ b/make/common/MakeBase.gmk @@ -75,7 +75,6 @@ endif # least for now. # Utils.gmk must be included before FileUtils.gmk, since it uses some of the # basic utility functions there. -include $(TOPDIR)/make/common/CommonVars.gmk include $(TOPDIR)/make/common/Utils.gmk include $(TOPDIR)/make/common/FileUtils.gmk From ca03080c9f3857e88f71a5803f55877edbc7da18 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 26 Sep 2025 00:10:21 +0000 Subject: [PATCH 249/556] 8368030: Make package bundlers stateless Reviewed-by: almatvee --- .../jpackage/internal/DesktopIntegration.java | 21 +- .../jpackage/internal/LinuxDebBundler.java | 373 +------------- .../jpackage/internal/LinuxDebPackager.java | 344 +++++++++++++ .../internal/LinuxDebSystemEnvironment.java | 36 ++ .../LinuxDebSystemEnvironmentMixin.java | 59 +++ .../jpackage/internal/LinuxFromParams.java | 20 +- .../internal/LinuxLaunchersAsServices.java | 4 +- .../internal/LinuxPackageBundler.java | 207 +------- .../jdk/jpackage/internal/LinuxPackager.java | 184 +++++++ .../jpackage/internal/LinuxRpmBundler.java | 195 +------ .../jpackage/internal/LinuxRpmPackager.java | 167 ++++++ .../internal/LinuxRpmSystemEnvironment.java | 36 ++ .../LinuxRpmSystemEnvironmentMixin.java | 74 +++ .../internal/LinuxSystemEnvironment.java | 111 ++++ .../jdk/jpackage/internal/MacDmgBundler.java | 38 +- .../jdk/jpackage/internal/MacDmgPackager.java | 121 +---- .../internal/MacDmgSystemEnvironment.java | 94 ++++ .../jdk/jpackage/internal/MacPkgBundler.java | 16 +- .../jdk/jpackage/internal/MacPkgPackager.java | 59 +-- .../{PackagerBuilder.java => Packager.java} | 64 ++- .../jpackage/internal/SystemEnvironment.java | 28 + .../jdk/jpackage/internal/ToolValidator.java | 123 +++-- .../resources/MainResources.properties | 11 +- .../jdk/jpackage/internal/util/Result.java | 125 +++++ .../jdk/jpackage/internal/WinExeBundler.java | 72 +-- .../jdk/jpackage/internal/WinExePackager.java | 108 ++++ .../jdk/jpackage/internal/WinFromParams.java | 16 + .../jdk/jpackage/internal/WinMsiBundler.java | 471 +---------------- .../jdk/jpackage/internal/WinMsiPackager.java | 486 ++++++++++++++++++ .../internal/WinSystemEnvironment.java | 41 ++ .../jdk/jpackage/internal/WixTool.java | 13 +- .../jpackage/internal/ToolValidatorTest.java | 191 ++++++- 32 files changed, 2412 insertions(+), 1496 deletions(-) create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java create mode 100644 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java create mode 100644 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java rename src/jdk.jpackage/share/classes/jdk/jpackage/internal/{PackagerBuilder.java => Packager.java} (57%) create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java create mode 100644 src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Result.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java create mode 100644 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinSystemEnvironment.java diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java index 2d0c78c2474..790d5d877aa 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java @@ -31,6 +31,7 @@ import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -67,7 +68,7 @@ final class DesktopIntegration extends ShellCustomAction { private static final List REPLACEMENT_STRING_IDS = List.of( COMMANDS_INSTALL, COMMANDS_UNINSTALL, SCRIPTS, COMMON_SCRIPTS); - private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launcher) throws IOException { + private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launcher) { associations = launcher.fileAssociations().stream().map( LinuxFileAssociation::create).toList(); @@ -88,10 +89,14 @@ final class DesktopIntegration extends ShellCustomAction { // This is additional launcher with explicit `no icon` configuration. withDesktopFile = false; } else { - final Path nullPath = null; - if (curIconResource.get().saveToFile(nullPath) != OverridableResource.Source.DefaultResource) { - // This launcher has custom icon configured. - withDesktopFile = true; + try { + if (curIconResource.get().saveToFile((Path)null) != OverridableResource.Source.DefaultResource) { + // This launcher has custom icon configured. + withDesktopFile = true; + } + } catch (IOException ex) { + // Should never happen as `saveToFile((Path)null)` should not perform any actual I/O operations. + throw new UncheckedIOException(ex); } } @@ -135,13 +140,13 @@ final class DesktopIntegration extends ShellCustomAction { return (LinuxLauncher)v; }).filter(l -> { return toRequest(l.shortcut()).orElse(true); - }).map(toFunction(l -> { + }).map(l -> { return new DesktopIntegration(env, pkg, l); - })).toList(); + }).toList(); } } - static ShellCustomAction create(BuildEnv env, Package pkg) throws IOException { + static ShellCustomAction create(BuildEnv env, Package pkg) { if (pkg.isRuntimeInstaller()) { return ShellCustomAction.nop(REPLACEMENT_STRING_IDS); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java index d5b369ba23a..9e2ea63cc32 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java @@ -25,362 +25,19 @@ package jdk.jpackage.internal; -import jdk.jpackage.internal.model.LinuxPackage; -import jdk.jpackage.internal.model.PackagerException; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.LinuxDebPackage; -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; - -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.model.AppImageLayout; -import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; -import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB; +import jdk.jpackage.internal.model.LinuxDebPackage; +import jdk.jpackage.internal.model.PackagerException; +import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.internal.util.Result; public class LinuxDebBundler extends LinuxPackageBundler { - private static final String TOOL_DPKG_DEB = "dpkg-deb"; - private static final String TOOL_DPKG = "dpkg"; - private static final String TOOL_FAKEROOT = "fakeroot"; - public LinuxDebBundler() { super(LinuxFromParams.DEB_PACKAGE); } - @Override - protected void doValidate(BuildEnv env, LinuxPackage pkg) throws ConfigException { - - // Show warning if license file is missing - if (pkg.licenseFile().isEmpty()) { - Log.verbose(I18N.getString("message.debs-like-licenses")); - } - } - - @Override - protected List getToolValidators() { - return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map( - ToolValidator::new).toList(); - } - - @Override - protected void createConfigFiles(Map replacementData, - BuildEnv env, LinuxPackage pkg) throws IOException { - prepareProjectConfig(replacementData, env, pkg); - adjustPermissionsRecursive(env.appImageDir()); - } - - @Override - protected Path buildPackageBundle(BuildEnv env, LinuxPackage pkg, - Path outputParentDir) throws PackagerException, IOException { - return buildDeb(env, pkg, outputParentDir); - } - - private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):"); - - @Override - protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) { - - libProvidersLookup.setPackageLookup(file -> { - Path realPath = file.toRealPath(); - - try { - // Try the real path first as it works better on newer Ubuntu versions - return findProvidingPackages(realPath); - } catch (IOException ex) { - // Try the default path if differ - if (!realPath.toString().equals(file.toString())) { - return findProvidingPackages(file); - } else { - throw ex; - } - } - }); - } - - private static Stream findProvidingPackages(Path file) throws IOException { - // - // `dpkg -S` command does glob pattern lookup. If not the absolute path - // to the file is specified it might return mltiple package names. - // Even for full paths multiple package names can be returned as - // it is OK for multiple packages to provide the same file. `/opt` - // directory is such an example. So we have to deal with multiple - // packages per file situation. - // - // E.g.: `dpkg -S libc.so.6` command reports three packages: - // libc6-x32: /libx32/libc.so.6 - // libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6 - // libc6-i386: /lib32/libc.so.6 - // `:amd64` is architecture suffix and can (should) be dropped. - // Still need to decide what package to choose from three. - // libc6-x32 and libc6-i386 both depend on libc6: - // $ dpkg -s libc6-x32 - // Package: libc6-x32 - // Status: install ok installed - // Priority: optional - // Section: libs - // Installed-Size: 10840 - // Maintainer: Ubuntu Developers - // Architecture: amd64 - // Source: glibc - // Version: 2.23-0ubuntu10 - // Depends: libc6 (= 2.23-0ubuntu10) - // - // We can dive into tracking dependencies, but this would be overly - // complicated. - // - // For simplicity lets consider the following rules: - // 1. If there is one item in `dpkg -S` output, accept it. - // 2. If there are multiple items in `dpkg -S` output and there is at - // least one item with the default arch suffix (DEB_ARCH), - // accept only these items. - // 3. If there are multiple items in `dpkg -S` output and there are - // no with the default arch suffix (DEB_ARCH), accept all items. - // So lets use this heuristics: don't accept packages for whom - // `dpkg -p` command fails. - // 4. Arch suffix should be stripped from accepted package names. - // - - Set archPackages = new HashSet<>(); - Set otherPackages = new HashSet<>(); - - var debArch = LinuxPackageArch.getValue(LINUX_DEB); - - Executor.of(TOOL_DPKG, "-S", file.toString()) - .saveOutput(true).executeExpectSuccess() - .getOutput().forEach(line -> { - Matcher matcher = PACKAGE_NAME_REGEX.matcher(line); - if (matcher.find()) { - String name = matcher.group(1); - if (name.endsWith(":" + debArch)) { - // Strip arch suffix - name = name.substring(0, - name.length() - (debArch.length() + 1)); - archPackages.add(name); - } else { - otherPackages.add(name); - } - } - }); - - if (!archPackages.isEmpty()) { - return archPackages.stream(); - } - return otherPackages.stream(); - } - - @Override - protected List verifyOutputBundle(BuildEnv env, LinuxPackage pkg, - Path packageBundle) { - List errors = new ArrayList<>(); - - String controlFileName = "control"; - - List properties = List.of( - new PackageProperty("Package", pkg.packageName(), - "APPLICATION_PACKAGE", controlFileName), - new PackageProperty("Version", ((LinuxDebPackage)pkg).versionWithRelease(), - "APPLICATION_VERSION_WITH_RELEASE", - controlFileName), - new PackageProperty("Architecture", pkg.arch(), "APPLICATION_ARCH", controlFileName)); - - List cmdline = new ArrayList<>(List.of(TOOL_DPKG_DEB, "-f", - packageBundle.toString())); - properties.forEach(property -> cmdline.add(property.name)); - try { - Map actualValues = Executor.of(cmdline.toArray(String[]::new)) - .saveOutput(true) - .executeExpectSuccess() - .getOutput().stream() - .map(line -> line.split(":\\s+", 2)) - .collect(Collectors.toMap( - components -> components[0], - components -> components[1])); - properties.forEach(property -> errors.add(property.verifyValue( - actualValues.get(property.name)))); - } catch (IOException ex) { - // Ignore error as it is not critical. Just report it. - Log.verbose(ex); - } - - return errors; - } - - /* - * set permissions with a string like "rwxr-xr-x" - * - * This cannot be directly backport to 22u which is built with 1.6 - */ - private void setPermissions(Path file, String permissions) { - Set filePermissions = - PosixFilePermissions.fromString(permissions); - try { - if (Files.exists(file)) { - Files.setPosixFilePermissions(file, filePermissions); - } - } catch (IOException ex) { - Log.error(ex.getMessage()); - Log.verbose(ex); - } - - } - - public static boolean isDebian() { - // we are just going to run "dpkg -s coreutils" and assume Debian - // or deritive if no error is returned. - try { - Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess(); - return true; - } catch (IOException e) { - // just fall thru - } - return false; - } - - private void adjustPermissionsRecursive(Path dir) throws IOException { - Files.walkFileTree(dir, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, - BasicFileAttributes attrs) - throws IOException { - if (file.endsWith(".so") || !Files.isExecutable(file)) { - setPermissions(file, "rw-r--r--"); - } else if (Files.isExecutable(file)) { - setPermissions(file, "rwxr-xr-x"); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) - throws IOException { - if (e == null) { - setPermissions(dir, "rwxr-xr-x"); - return FileVisitResult.CONTINUE; - } else { - // directory iteration failed - throw e; - } - } - }); - } - - private class DebianFile { - - DebianFile(Path dstFilePath, String comment) { - this.dstFilePath = dstFilePath; - this.comment = comment; - } - - DebianFile setExecutable() { - permissions = "rwxr-xr-x"; - return this; - } - - void create(Map data, Function resourceFactory) - throws IOException { - resourceFactory.apply("template." + dstFilePath.getFileName().toString()) - .setCategory(I18N.getString(comment)) - .setSubstitutionData(data) - .saveToFile(dstFilePath); - if (permissions != null) { - setPermissions(dstFilePath, permissions); - } - } - - private final Path dstFilePath; - private final String comment; - private String permissions; - } - - private void prepareProjectConfig(Map data, BuildEnv env, LinuxPackage pkg) throws IOException { - - Path configDir = env.appImageDir().resolve("DEBIAN"); - List debianFiles = new ArrayList<>(); - debianFiles.add(new DebianFile( - configDir.resolve("control"), - "resource.deb-control-file")); - debianFiles.add(new DebianFile( - configDir.resolve("preinst"), - "resource.deb-preinstall-script").setExecutable()); - debianFiles.add(new DebianFile( - configDir.resolve("prerm"), - "resource.deb-prerm-script").setExecutable()); - debianFiles.add(new DebianFile( - configDir.resolve("postinst"), - "resource.deb-postinstall-script").setExecutable()); - debianFiles.add(new DebianFile( - configDir.resolve("postrm"), - "resource.deb-postrm-script").setExecutable()); - - ((LinuxDebPackage)pkg).relativeCopyrightFilePath().ifPresent(copyrightFile -> { - debianFiles.add(new DebianFile(env.appImageDir().resolve(copyrightFile), - "resource.copyright-file")); - }); - - for (DebianFile debianFile : debianFiles) { - debianFile.create(data, env::createResource); - } - } - - @Override - protected Map createReplacementData(BuildEnv env, LinuxPackage pkg) throws IOException { - Map data = new HashMap<>(); - - String licenseText = pkg.licenseFile().map(toFunction(Files::readString)).orElse("Unknown"); - - data.put("APPLICATION_MAINTAINER", ((LinuxDebPackage) pkg).maintainer()); - data.put("APPLICATION_SECTION", pkg.category().orElseThrow()); - data.put("APPLICATION_COPYRIGHT", pkg.app().copyright()); - data.put("APPLICATION_LICENSE_TEXT", licenseText); - data.put("APPLICATION_ARCH", pkg.arch()); - data.put("APPLICATION_INSTALLED_SIZE", Long.toString( - AppImageLayout.toPathGroup(env.appImageLayout()).sizeInBytes() >> 10)); - data.put("APPLICATION_HOMEPAGE", pkg.aboutURL().map( - value -> "Homepage: " + value).orElse("")); - data.put("APPLICATION_VERSION_WITH_RELEASE", ((LinuxDebPackage) pkg).versionWithRelease()); - - return data; - } - - private Path buildDeb(BuildEnv env, LinuxPackage pkg, Path outdir) throws IOException { - Path outFile = outdir.resolve(pkg.packageFileNameWithSuffix()); - Log.verbose(I18N.format("message.outputting-to-location", outFile.toAbsolutePath())); - - List cmdline = new ArrayList<>(); - cmdline.addAll(List.of(TOOL_FAKEROOT, TOOL_DPKG_DEB)); - if (Log.isVerbose()) { - cmdline.add("--verbose"); - } - cmdline.addAll(List.of("-b", env.appImageDir().toString(), - outFile.toAbsolutePath().toString())); - - // run dpkg - RetryExecutor.retryOnKnownErrorMessage( - "semop(1): encountered an error: Invalid argument").execute( - cmdline.toArray(String[]::new)); - - Log.verbose(I18N.format("message.output-to-location", outFile.toAbsolutePath())); - - return outFile; - } - @Override public String getName() { return I18N.getString("deb.bundler.name"); @@ -392,12 +49,28 @@ public class LinuxDebBundler extends LinuxPackageBundler { } @Override - public boolean supported(boolean runtimeInstaller) { - return OperatingSystem.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null); + public Path execute(Map params, Path outputParentDir) throws PackagerException { + + return Packager.build().outputDir(outputParentDir) + .pkg(LinuxFromParams.DEB_PACKAGE.fetchFrom(params)) + .env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params)) + .pipelineBuilderMutatorFactory((env, pkg, outputDir) -> { + return new LinuxDebPackager(env, pkg, outputDir, sysEnv.orElseThrow()); + }).execute(LinuxPackagingPipeline.build()); + } + + @Override + protected Result sysEnv() { + return sysEnv; } @Override public boolean isDefault() { - return isDebian(); + return sysEnv.value() + .map(LinuxSystemEnvironment::nativePackageType) + .map(StandardPackageType.LINUX_DEB::equals) + .orElse(false); } + + private final Result sysEnv = LinuxDebSystemEnvironment.create(SYS_ENV); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java new file mode 100644 index 00000000000..64a0368e9a0 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB; +import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; +import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; +import jdk.jpackage.internal.model.AppImageLayout; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.LinuxDebPackage; + +final class LinuxDebPackager extends LinuxPackager { + + LinuxDebPackager(BuildEnv env, LinuxDebPackage pkg, Path outputDir, LinuxDebSystemEnvironment sysEnv) { + super(env, pkg, outputDir, sysEnv); + this.sysEnv = Objects.requireNonNull(sysEnv); + } + + @Override + protected void createConfigFiles(Map replacementData) throws IOException { + prepareProjectConfig(replacementData); + adjustPermissionsRecursive(); + } + + @Override + protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) { + + libProvidersLookup.setPackageLookup(file -> { + Path realPath = file.toRealPath(); + + try { + // Try the real path first as it works better on newer Ubuntu versions + return findProvidingPackages(realPath, sysEnv.dpkg()); + } catch (IOException ex) { + // Try the default path if differ + if (!realPath.equals(file)) { + return findProvidingPackages(file, sysEnv.dpkg()); + } else { + throw ex; + } + } + }); + } + + @Override + protected List findErrorsInOutputPackage() throws IOException { + List errors = new ArrayList<>(); + + var controlFileName = "control"; + + List properties = List.of( + new PackageProperty("Package", pkg.packageName(), + "APPLICATION_PACKAGE", controlFileName), + new PackageProperty("Version", pkg.versionWithRelease(), + "APPLICATION_VERSION_WITH_RELEASE", + controlFileName), + new PackageProperty("Architecture", pkg.arch(), "APPLICATION_ARCH", controlFileName)); + + List cmdline = new ArrayList<>(List.of( + sysEnv.dpkgdeb().toString(), "-f", outputPackageFile().toString())); + + properties.forEach(property -> cmdline.add(property.name)); + + Map actualValues = Executor.of(cmdline.toArray(String[]::new)) + .saveOutput(true) + .executeExpectSuccess() + .getOutput().stream() + .map(line -> line.split(":\\s+", 2)) + .collect(Collectors.toMap( + components -> components[0], + components -> components[1])); + + for (var property : properties) { + Optional.ofNullable(property.verifyValue(actualValues.get(property.name))).ifPresent(errors::add); + } + + return errors; + } + + @Override + protected Map createReplacementData() throws IOException { + Map data = new HashMap<>(); + + String licenseText = pkg.licenseFile().map(toFunction(Files::readString)).orElse("Unknown"); + + data.put("APPLICATION_MAINTAINER", pkg.maintainer()); + data.put("APPLICATION_SECTION", pkg.category().orElseThrow()); + data.put("APPLICATION_COPYRIGHT", pkg.app().copyright()); + data.put("APPLICATION_LICENSE_TEXT", licenseText); + data.put("APPLICATION_ARCH", pkg.arch()); + data.put("APPLICATION_INSTALLED_SIZE", Long.toString( + AppImageLayout.toPathGroup(env.appImageLayout()).sizeInBytes() >> 10)); + data.put("APPLICATION_HOMEPAGE", pkg.aboutURL().map( + value -> "Homepage: " + value).orElse("")); + data.put("APPLICATION_VERSION_WITH_RELEASE", pkg.versionWithRelease()); + + return data; + } + + @Override + protected void buildPackage() throws IOException { + + Path debFile = outputPackageFile(); + + Log.verbose(I18N.format("message.outputting-to-location", debFile.toAbsolutePath())); + + List cmdline = new ArrayList<>(); + Stream.of(sysEnv.fakeroot(), sysEnv.dpkgdeb()).map(Path::toString).forEach(cmdline::add); + if (Log.isVerbose()) { + cmdline.add("--verbose"); + } + cmdline.addAll(List.of("-b", env.appImageDir().toString(), debFile.toAbsolutePath().toString())); + + // run dpkg + RetryExecutor.retryOnKnownErrorMessage( + "semop(1): encountered an error: Invalid argument").execute( + cmdline.toArray(String[]::new)); + + Log.verbose(I18N.format("message.output-to-location", debFile.toAbsolutePath())); + } + + @Override + public void accept(PackagingPipeline.Builder pipelineBuilder) { + super.accept(pipelineBuilder); + + // Build deb config files after app image contents are ready because + // it calculates the size of the image and saves the value in one of the config files. + pipelineBuilder.configuredTasks().filter(task -> { + return PackageTaskID.CREATE_CONFIG_FILES.equals(task.task()); + }).findFirst().orElseThrow() + .addDependencies(PrimaryTaskID.BUILD_APPLICATION_IMAGE, PrimaryTaskID.COPY_APP_IMAGE) + .add(); + } + + private void adjustPermissionsRecursive() throws IOException { + Files.walkFileTree(env.appImageDir(), new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (file.endsWith(".so") || !Files.isExecutable(file)) { + Files.setPosixFilePermissions(file, SO_PERMISSIONS); + } else if (Files.isExecutable(file)) { + Files.setPosixFilePermissions(file, EXECUTABLE_PERMISSIONS); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { + if (e == null) { + Files.setPosixFilePermissions(dir, FOLDER_PERMISSIONS); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + } + + private void prepareProjectConfig(Map data) throws IOException { + + Path configDir = env.appImageDir().resolve("DEBIAN"); + List debianFiles = new ArrayList<>(); + debianFiles.add(new DebianFile( + configDir.resolve("control"), + "resource.deb-control-file")); + debianFiles.add(new DebianFile( + configDir.resolve("preinst"), + "resource.deb-preinstall-script").setExecutable()); + debianFiles.add(new DebianFile( + configDir.resolve("prerm"), + "resource.deb-prerm-script").setExecutable()); + debianFiles.add(new DebianFile( + configDir.resolve("postinst"), + "resource.deb-postinstall-script").setExecutable()); + debianFiles.add(new DebianFile( + configDir.resolve("postrm"), + "resource.deb-postrm-script").setExecutable()); + + pkg.relativeCopyrightFilePath().ifPresent(copyrightFile -> { + debianFiles.add(new DebianFile(env.appImageDir().resolve(copyrightFile), + "resource.copyright-file")); + }); + + for (DebianFile debianFile : debianFiles) { + debianFile.create(data, env::createResource); + } + } + + private static Stream findProvidingPackages(Path file, Path dpkg) throws IOException { + // + // `dpkg -S` command does glob pattern lookup. If not the absolute path + // to the file is specified it might return mltiple package names. + // Even for full paths multiple package names can be returned as + // it is OK for multiple packages to provide the same file. `/opt` + // directory is such an example. So we have to deal with multiple + // packages per file situation. + // + // E.g.: `dpkg -S libc.so.6` command reports three packages: + // libc6-x32: /libx32/libc.so.6 + // libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6 + // libc6-i386: /lib32/libc.so.6 + // `:amd64` is architecture suffix and can (should) be dropped. + // Still need to decide what package to choose from three. + // libc6-x32 and libc6-i386 both depend on libc6: + // $ dpkg -s libc6-x32 + // Package: libc6-x32 + // Status: install ok installed + // Priority: optional + // Section: libs + // Installed-Size: 10840 + // Maintainer: Ubuntu Developers + // Architecture: amd64 + // Source: glibc + // Version: 2.23-0ubuntu10 + // Depends: libc6 (= 2.23-0ubuntu10) + // + // We can dive into tracking dependencies, but this would be overly + // complicated. + // + // For simplicity lets consider the following rules: + // 1. If there is one item in `dpkg -S` output, accept it. + // 2. If there are multiple items in `dpkg -S` output and there is at + // least one item with the default arch suffix (DEB_ARCH), + // accept only these items. + // 3. If there are multiple items in `dpkg -S` output and there are + // no with the default arch suffix (DEB_ARCH), accept all items. + // So lets use this heuristics: don't accept packages for whom + // `dpkg -p` command fails. + // 4. Arch suffix should be stripped from accepted package names. + // + + Set archPackages = new HashSet<>(); + Set otherPackages = new HashSet<>(); + + var debArch = LinuxPackageArch.getValue(LINUX_DEB); + + Executor.of(dpkg.toString(), "-S", file.toString()) + .saveOutput(true).executeExpectSuccess() + .getOutput().forEach(line -> { + Matcher matcher = PACKAGE_NAME_REGEX.matcher(line); + if (matcher.find()) { + String name = matcher.group(1); + if (name.endsWith(":" + debArch)) { + // Strip arch suffix + name = name.substring(0, + name.length() - (debArch.length() + 1)); + archPackages.add(name); + } else { + otherPackages.add(name); + } + } + }); + + if (!archPackages.isEmpty()) { + return archPackages.stream(); + } + return otherPackages.stream(); + } + + + private static final class DebianFile { + + DebianFile(Path dstFilePath, String comment) { + this.dstFilePath = Objects.requireNonNull(dstFilePath); + this.comment = Objects.requireNonNull(comment); + } + + DebianFile setExecutable() { + permissions = EXECUTABLE_PERMISSIONS; + return this; + } + + void create(Map data, Function resourceFactory) + throws IOException { + resourceFactory.apply("template." + dstFilePath.getFileName().toString()) + .setCategory(I18N.getString(comment)) + .setSubstitutionData(data) + .saveToFile(dstFilePath); + if (permissions != null) { + Files.setPosixFilePermissions(dstFilePath, permissions); + } + } + + private final Path dstFilePath; + private final String comment; + private Set permissions; + } + + + private final LinuxDebSystemEnvironment sysEnv; + + private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):"); + + private static final Set EXECUTABLE_PERMISSIONS = PosixFilePermissions.fromString("rwxr-xr-x"); + private static final Set FOLDER_PERMISSIONS = PosixFilePermissions.fromString("rwxr-xr-x"); + private static final Set SO_PERMISSIONS = PosixFilePermissions.fromString("rw-r--r--"); +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java new file mode 100644 index 00000000000..5b5decb7a67 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.LinuxSystemEnvironment.mixin; + +import jdk.jpackage.internal.util.Result; + +public interface LinuxDebSystemEnvironment extends LinuxSystemEnvironment, LinuxDebSystemEnvironmentMixin { + + static Result create(Result base) { + return mixin(LinuxDebSystemEnvironment.class, base, LinuxDebSystemEnvironmentMixin::create); + } +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java new file mode 100644 index 00000000000..8688327b353 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.Result; + +public interface LinuxDebSystemEnvironmentMixin { + Path dpkg(); + Path dpkgdeb(); + Path fakeroot(); + + record Stub(Path dpkg, Path dpkgdeb, Path fakeroot) implements LinuxDebSystemEnvironmentMixin { + } + + static Result create() { + final var errors = Stream.of(Internal.TOOL_DPKG_DEB, Internal.TOOL_DPKG, Internal.TOOL_FAKEROOT) + .map(ToolValidator::new) + .map(ToolValidator::validate) + .filter(Objects::nonNull) + .toList(); + if (errors.isEmpty()) { + return Result.ofValue(new Stub(Internal.TOOL_DPKG, Internal.TOOL_DPKG_DEB, Internal.TOOL_FAKEROOT)); + } else { + return Result.ofErrors(errors); + } + } + + static final class Internal { + + private static final Path TOOL_DPKG_DEB = Path.of("dpkg-deb"); + private static final Path TOOL_DPKG = Path.of("dpkg"); + private static final Path TOOL_FAKEROOT = Path.of("fakeroot"); + } +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java index ced77b1aa68..a8d109220dc 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromParams.java @@ -39,9 +39,10 @@ import java.io.IOException; import java.util.Map; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.LinuxApplication; +import jdk.jpackage.internal.model.LinuxDebPackage; import jdk.jpackage.internal.model.LinuxLauncher; import jdk.jpackage.internal.model.LinuxLauncherMixin; -import jdk.jpackage.internal.model.LinuxPackage; +import jdk.jpackage.internal.model.LinuxRpmPackage; import jdk.jpackage.internal.model.StandardPackageType; final class LinuxFromParams { @@ -76,7 +77,7 @@ final class LinuxFromParams { return pkgBuilder; } - private static LinuxPackage createLinuxRpmPackage( + private static LinuxRpmPackage createLinuxRpmPackage( Map params) throws ConfigException, IOException { final var superPkgBuilder = createLinuxPackageBuilder(params, LINUX_RPM); @@ -88,7 +89,7 @@ final class LinuxFromParams { return pkgBuilder.create(); } - private static LinuxPackage createLinuxDebPackage( + private static LinuxDebPackage createLinuxDebPackage( Map params) throws ConfigException, IOException { final var superPkgBuilder = createLinuxPackageBuilder(params, LINUX_DEB); @@ -97,16 +98,23 @@ final class LinuxFromParams { MAINTAINER_EMAIL.copyInto(params, pkgBuilder::maintainerEmail); - return pkgBuilder.create(); + final var pkg = pkgBuilder.create(); + + // Show warning if license file is missing + if (pkg.licenseFile().isEmpty()) { + Log.verbose(I18N.getString("message.debs-like-licenses")); + } + + return pkg; } static final BundlerParamInfo APPLICATION = createApplicationBundlerParam( LinuxFromParams::createLinuxApplication); - static final BundlerParamInfo RPM_PACKAGE = createPackageBundlerParam( + static final BundlerParamInfo RPM_PACKAGE = createPackageBundlerParam( LinuxFromParams::createLinuxRpmPackage); - static final BundlerParamInfo DEB_PACKAGE = createPackageBundlerParam( + static final BundlerParamInfo DEB_PACKAGE = createPackageBundlerParam( LinuxFromParams::createLinuxDebPackage); private static final BundlerParamInfo LINUX_SHORTCUT_HINT = createStringBundlerParam( diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java index 742f87b3a9f..b14404d67b1 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java @@ -38,7 +38,7 @@ import jdk.jpackage.internal.model.Package; */ public final class LinuxLaunchersAsServices extends UnixLaunchersAsServices { - private LinuxLaunchersAsServices(BuildEnv env, Package pkg) throws IOException { + private LinuxLaunchersAsServices(BuildEnv env, Package pkg) { super(env.appImageDir(), pkg.app(), REQUIRED_PACKAGES, launcher -> { return new LauncherImpl(env, pkg, launcher); }); @@ -58,7 +58,7 @@ public final class LinuxLaunchersAsServices extends UnixLaunchersAsServices { return data; } - static ShellCustomAction create(BuildEnv env, Package pkg) throws IOException { + static ShellCustomAction create(BuildEnv env, Package pkg) { if (pkg.isRuntimeInstaller()) { return ShellCustomAction.nop(LINUX_REPLACEMENT_STRING_IDS); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java index 2f3e6da1e69..1f674c0be11 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java @@ -24,33 +24,16 @@ */ package jdk.jpackage.internal; -import java.io.IOException; -import java.nio.file.Path; -import java.text.MessageFormat; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.function.Predicate; -import java.util.stream.Stream; -import jdk.jpackage.internal.PackagingPipeline.PackageBuildEnv; -import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; -import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; -import jdk.jpackage.internal.model.AppImageLayout; import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.LinuxDebPackage; import jdk.jpackage.internal.model.LinuxPackage; -import jdk.jpackage.internal.model.Package; -import jdk.jpackage.internal.model.PackagerException; +import jdk.jpackage.internal.util.Result; abstract class LinuxPackageBundler extends AbstractBundler { LinuxPackageBundler(BundlerParamInfo pkgParam) { - this.pkgParam = pkgParam; - customActions = List.of(new CustomActionInstance( - DesktopIntegration::create), new CustomActionInstance( - LinuxLaunchersAsServices::create)); + this.pkgParam = Objects.requireNonNull(pkgParam); } @Override @@ -58,39 +41,32 @@ abstract class LinuxPackageBundler extends AbstractBundler { throws ConfigException { // Order is important! - LinuxPackage pkg = pkgParam.fetchFrom(params); - var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params); + pkgParam.fetchFrom(params); + BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - for (var validator: getToolValidators()) { - ConfigException ex = validator.validate(); - if (ex != null) { - throw ex; - } + LinuxSystemEnvironment sysEnv; + try { + sysEnv = sysEnv().orElseThrow(); + } catch (RuntimeException ex) { + throw ConfigException.rethrowConfigException(ex); } if (!isDefault()) { - withFindNeededPackages = false; - Log.verbose(MessageFormat.format(I18N.getString( - "message.not-default-bundler-no-dependencies-lookup"), + Log.verbose(I18N.format( + "message.not-default-bundler-no-dependencies-lookup", getName())); - } else { - withFindNeededPackages = LibProvidersLookup.supported(); - if (!withFindNeededPackages) { - final String advice; - if ("deb".equals(getID())) { - advice = "message.deb-ldd-not-available.advice"; - } else { - advice = "message.rpm-ldd-not-available.advice"; - } - // Let user know package dependencies will not be generated. - Log.error(String.format("%s\n%s", I18N.getString( - "message.ldd-not-available"), I18N.getString(advice))); + } else if (!sysEnv.soLookupAvailable()) { + final String advice; + if ("deb".equals(getID())) { + advice = "message.deb-ldd-not-available.advice"; + } else { + advice = "message.rpm-ldd-not-available.advice"; } + // Let user know package dependencies will not be generated. + Log.error(String.format("%s\n%s", I18N.getString( + "message.ldd-not-available"), I18N.getString(advice))); } - // Packaging specific validation - doValidate(env, pkg); - return true; } @@ -100,148 +76,13 @@ abstract class LinuxPackageBundler extends AbstractBundler { } @Override - public final Path execute(Map params, - Path outputParentDir) throws PackagerException { - IOUtils.writableOutputDir(outputParentDir); - - // Order is important! - final LinuxPackage pkg = pkgParam.fetchFrom(params); - final var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - - final var pipelineBuilder = LinuxPackagingPipeline.build() - .excludeDirFromCopying(outputParentDir) - .task(PackageTaskID.CREATE_PACKAGE_FILE) - .packageAction(this::buildPackage) - .add(); - - final var createConfigFilesTaskBuilder = pipelineBuilder - .task(PackageTaskID.CREATE_CONFIG_FILES) - .packageAction(this::buildConfigFiles); - - if (pkg instanceof LinuxDebPackage) { - // Build deb config files after app image contents are ready because - // it calculates the size of the image and saves the value in one of the config files. - createConfigFilesTaskBuilder.addDependencies(PrimaryTaskID.BUILD_APPLICATION_IMAGE, PrimaryTaskID.COPY_APP_IMAGE); - } - - createConfigFilesTaskBuilder.add(); - - pipelineBuilder.create().execute(env, pkg, outputParentDir); - - return outputParentDir.resolve(pkg.packageFileNameWithSuffix()).toAbsolutePath(); + public boolean supported(boolean runtimeInstaller) { + return sysEnv().hasValue(); } - private void buildConfigFiles(PackageBuildEnv env) throws PackagerException, IOException { - for (var ca : customActions) { - ca.init(env.env(), env.pkg()); - } - - Map data = createDefaultReplacementData(env.env(), env.pkg()); - - for (var ca : customActions) { - ShellCustomAction.mergeReplacementData(data, ca.instance.create()); - } - - data.putAll(createReplacementData(env.env(), env.pkg())); - - createConfigFiles(Collections.unmodifiableMap(data), env.env(), env.pkg()); - } - - private void buildPackage(PackageBuildEnv env) throws PackagerException, IOException { - Path packageBundle = buildPackageBundle(env.env(), env.pkg(), env.outputDir()); - - verifyOutputBundle(env.env(), env.pkg(), packageBundle).stream() - .filter(Objects::nonNull) - .forEachOrdered(ex -> { - Log.verbose(ex.getLocalizedMessage()); - Log.verbose(ex.getAdvice()); - }); - } - - private List getListOfNeededPackages(BuildEnv env) throws IOException { - - final List caPackages = customActions.stream() - .map(ca -> ca.instance) - .map(ShellCustomAction::requiredPackages) - .flatMap(List::stream).toList(); - - final List neededLibPackages; - if (withFindNeededPackages) { - LibProvidersLookup lookup = new LibProvidersLookup(); - initLibProvidersLookup(lookup); - - neededLibPackages = lookup.execute(env.appImageDir()); - } else { - neededLibPackages = Collections.emptyList(); - Log.info(I18N.getString("warning.foreign-app-image")); - } - - // Merge all package lists together. - // Filter out empty names, sort and remove duplicates. - List result = Stream.of(caPackages, neededLibPackages).flatMap( - List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().toList(); - - Log.verbose(String.format("Required packages: %s", result)); - - return result; - } - - private Map createDefaultReplacementData(BuildEnv env, LinuxPackage pkg) throws IOException { - Map data = new HashMap<>(); - - data.put("APPLICATION_PACKAGE", pkg.packageName()); - data.put("APPLICATION_VENDOR", pkg.app().vendor()); - data.put("APPLICATION_VERSION", pkg.version()); - data.put("APPLICATION_DESCRIPTION", pkg.description()); - - String defaultDeps = String.join(", ", getListOfNeededPackages(env)); - String customDeps = pkg.additionalDependencies().orElse(""); - if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) { - customDeps = ", " + customDeps; - } - data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps); - data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps); - - return data; - } - - protected abstract List verifyOutputBundle( - BuildEnv env, LinuxPackage pkg, Path packageBundle); - - protected abstract void initLibProvidersLookup(LibProvidersLookup libProvidersLookup); - - protected abstract List getToolValidators(); - - protected abstract void doValidate(BuildEnv env, LinuxPackage pkg) - throws ConfigException; - - protected abstract Map createReplacementData( - BuildEnv env, LinuxPackage pkg) throws IOException; - - protected abstract void createConfigFiles( - Map replacementData, - BuildEnv env, LinuxPackage pkg) throws IOException; - - protected abstract Path buildPackageBundle( - BuildEnv env, LinuxPackage pkg, Path outputParentDir) throws - PackagerException, IOException; + protected abstract Result sysEnv(); private final BundlerParamInfo pkgParam; - private boolean withFindNeededPackages; - private final List customActions; - private static final class CustomActionInstance { - - CustomActionInstance(ShellCustomActionFactory factory) { - this.factory = factory; - } - - void init(BuildEnv env, Package pkg) throws IOException { - instance = factory.create(env, pkg); - Objects.requireNonNull(instance); - } - - private final ShellCustomActionFactory factory; - ShellCustomAction instance; - } + static final Result SYS_ENV = LinuxSystemEnvironment.create(); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java new file mode 100644 index 00000000000..806592904d1 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackager.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; +import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; +import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; +import jdk.jpackage.internal.PackagingPipeline.TaskID; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.LinuxPackage; +import jdk.jpackage.internal.model.PackagerException; + +abstract class LinuxPackager implements Consumer { + + LinuxPackager(BuildEnv env, T pkg, Path outputDir, LinuxSystemEnvironment sysEnv) { + this.env = Objects.requireNonNull(env); + this.pkg = Objects.requireNonNull(pkg); + this.outputDir = Objects.requireNonNull(outputDir); + this.withRequiredPackagesLookup = sysEnv.soLookupAvailable() && sysEnv.nativePackageType().equals(pkg.type()); + + customActions = List.of( + DesktopIntegration.create(env, pkg), + LinuxLaunchersAsServices.create(env, pkg)); + } + + enum LinuxPackageTaskID implements TaskID { + INIT_REQUIRED_PACKAGES, + VERIFY_PACKAGE + } + + @Override + public void accept(PackagingPipeline.Builder pipelineBuilder) { + pipelineBuilder.excludeDirFromCopying(outputDir) + .task(PackageTaskID.CREATE_CONFIG_FILES) + .action(this::buildConfigFiles) + .add() + .task(LinuxPackageTaskID.INIT_REQUIRED_PACKAGES) + .addDependencies(PrimaryTaskID.BUILD_APPLICATION_IMAGE, PrimaryTaskID.COPY_APP_IMAGE) + .addDependent(PackageTaskID.CREATE_CONFIG_FILES) + .action(this::initRequiredPackages) + .add() + .task(LinuxPackageTaskID.VERIFY_PACKAGE) + .addDependencies(PackageTaskID.CREATE_PACKAGE_FILE) + .addDependent(PrimaryTaskID.PACKAGE) + .action(this::verifyOutputPackage) + .add() + .task(PackageTaskID.CREATE_PACKAGE_FILE) + .action(this::buildPackage) + .add(); + } + + protected final Path outputPackageFile() { + return outputDir.resolve(pkg.packageFileNameWithSuffix()); + } + + protected abstract void buildPackage() throws IOException; + + protected abstract List findErrorsInOutputPackage() throws IOException; + + protected abstract void createConfigFiles(Map replacementData) throws IOException; + + protected abstract Map createReplacementData() throws IOException; + + protected abstract void initLibProvidersLookup(LibProvidersLookup libProvidersLookup); + + private void buildConfigFiles() throws PackagerException, IOException { + + final var data = createDefaultReplacementData(); + + for (var ca : customActions) { + ShellCustomAction.mergeReplacementData(data, ca.create()); + } + + data.putAll(createReplacementData()); + + createConfigFiles(Collections.unmodifiableMap(data)); + } + + private Map createDefaultReplacementData() { + Map data = new HashMap<>(); + + data.put("APPLICATION_PACKAGE", pkg.packageName()); + data.put("APPLICATION_VENDOR", pkg.app().vendor()); + data.put("APPLICATION_VERSION", pkg.version()); + data.put("APPLICATION_DESCRIPTION", pkg.description()); + + String defaultDeps = String.join(", ", requiredPackages); + String customDeps = pkg.additionalDependencies().orElse(""); + if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) { + customDeps = ", " + customDeps; + } + data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps); + data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps); + + return data; + } + + private void initRequiredPackages() throws IOException { + + final List caPackages = customActions.stream() + .map(ShellCustomAction::requiredPackages) + .flatMap(List::stream).toList(); + + final List neededLibPackages; + if (withRequiredPackagesLookup) { + neededLibPackages = findRequiredPackages(); + } else { + neededLibPackages = Collections.emptyList(); + Log.info(I18N.getString("warning.foreign-app-image")); + } + + // Merge all package lists together. + // Filter out empty names, sort and remove duplicates. + Stream.of(caPackages, neededLibPackages) + .flatMap(List::stream) + .filter(Predicate.not(String::isEmpty)) + .sorted().distinct().forEach(requiredPackages::add); + + Log.verbose(String.format("Required packages: %s", requiredPackages)); + } + + private List findRequiredPackages() throws IOException { + var lookup = new LibProvidersLookup(); + initLibProvidersLookup(lookup); + return lookup.execute(env.appImageDir()); + } + + private void verifyOutputPackage() { + final List errors; + try { + errors = findErrorsInOutputPackage(); + } catch (Exception ex) { + // Ignore error as it is not critical. Just report it. + Log.verbose(ex); + return; + } + + for (var ex : errors) { + Log.verbose(ex.getLocalizedMessage()); + if (ex instanceof ConfigException cfgEx) { + Log.verbose(cfgEx.getAdvice()); + } + } + } + + protected final BuildEnv env; + protected final T pkg; + protected final Path outputDir; + private final boolean withRequiredPackagesLookup; + private final List requiredPackages = new ArrayList<>(); + private final List customActions; +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java index 350f70f11bc..2337a286011 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java @@ -25,189 +25,20 @@ package jdk.jpackage.internal; -import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.DottedVersion; -import jdk.jpackage.internal.model.LinuxPackage; import jdk.jpackage.internal.model.LinuxRpmPackage; -import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.model.PackagerException; +import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.internal.util.Result; -/** - * There are two command line options to configure license information for RPM - * packaging: --linux-rpm-license-type and --license-file. Value of - * --linux-rpm-license-type command line option configures "License:" section - * of RPM spec. Value of --license-file command line option specifies a license - * file to be added to the package. License file is a sort of documentation file - * but it will be installed even if user selects an option to install the - * package without documentation. --linux-rpm-license-type is the primary option - * to set license information. --license-file makes little sense in case of RPM - * packaging. - */ public class LinuxRpmBundler extends LinuxPackageBundler { - private static final String DEFAULT_SPEC_TEMPLATE = "template.spec"; - - public static final String TOOL_RPM = "rpm"; - public static final String TOOL_RPMBUILD = "rpmbuild"; - public static final DottedVersion TOOL_RPMBUILD_MIN_VERSION = DottedVersion.lazy( - "4.10"); - public LinuxRpmBundler() { super(LinuxFromParams.RPM_PACKAGE); } - @Override - protected void doValidate(BuildEnv env, LinuxPackage pkg) throws ConfigException { - } - - private static ToolValidator createRpmbuildToolValidator() { - Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)"); - return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion( - TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> { - String versionString = lines.limit(1).collect( - Collectors.toList()).get(0); - Matcher matcher = pattern.matcher(versionString); - if (matcher.find()) { - return matcher.group(1); - } - return null; - }); - } - - @Override - protected List getToolValidators() { - return List.of(createRpmbuildToolValidator()); - } - - protected void createConfigFiles(Map replacementData, - BuildEnv env, LinuxPackage pkg) throws IOException { - Path specFile = specFile(env, pkg); - - // prepare spec file - env.createResource(DEFAULT_SPEC_TEMPLATE) - .setCategory(I18N.getString("resource.rpm-spec-file")) - .setSubstitutionData(replacementData) - .saveToFile(specFile); - } - - @Override - protected Path buildPackageBundle(BuildEnv env, LinuxPackage pkg, - Path outputParentDir) throws PackagerException, IOException { - return buildRPM(env, pkg, outputParentDir); - } - - private static Path installPrefix(LinuxPackage pkg) { - Path path = pkg.relativeInstallDir(); - if (!pkg.isInstallDirInUsrTree()) { - path = path.getParent(); - } - return Path.of("/").resolve(path); - } - - @Override - protected Map createReplacementData(BuildEnv env, LinuxPackage pkg) throws IOException { - Map data = new HashMap<>(); - - data.put("APPLICATION_RELEASE", pkg.release().orElseThrow()); - data.put("APPLICATION_PREFIX", installPrefix(pkg).toString()); - data.put("APPLICATION_DIRECTORY", Path.of("/").resolve(pkg.relativeInstallDir()).toString()); - data.put("APPLICATION_SUMMARY", pkg.app().name()); - data.put("APPLICATION_LICENSE_TYPE", ((LinuxRpmPackage)pkg).licenseType()); - - String licenseFile = pkg.licenseFile().map(v -> { - return v.toAbsolutePath().normalize().toString(); - }).orElse(null); - data.put("APPLICATION_LICENSE_FILE", licenseFile); - data.put("APPLICATION_GROUP", pkg.category().orElse("")); - - data.put("APPLICATION_URL", pkg.aboutURL().orElse("")); - - return data; - } - - @Override - protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) { - libProvidersLookup.setPackageLookup(file -> { - return Executor.of(TOOL_RPM, - "-q", "--queryformat", "%{name}\\n", - "-q", "--whatprovides", file.toString()) - .saveOutput(true).executeExpectSuccess().getOutput().stream(); - }); - } - - @Override - protected List verifyOutputBundle(BuildEnv env, LinuxPackage pkg, - Path packageBundle) { - List errors = new ArrayList<>(); - - String specFileName = specFile(env, pkg).getFileName().toString(); - - try { - List properties = List.of( - new PackageProperty("Name", pkg.packageName(), - "APPLICATION_PACKAGE", specFileName), - new PackageProperty("Version", pkg.version(), - "APPLICATION_VERSION", specFileName), - new PackageProperty("Release", pkg.release().orElseThrow(), - "APPLICATION_RELEASE", specFileName), - new PackageProperty("Arch", pkg.arch(), null, specFileName)); - - List actualValues = Executor.of(TOOL_RPM, "-qp", "--queryformat", - properties.stream().map(entry -> String.format("%%{%s}", - entry.name)).collect(Collectors.joining("\\n")), - packageBundle.toString()).saveOutput(true).executeExpectSuccess().getOutput(); - - Iterator actualValuesIt = actualValues.iterator(); - properties.forEach(property -> errors.add(property.verifyValue( - actualValuesIt.next()))); - } catch (IOException ex) { - // Ignore error as it is not critical. Just report it. - Log.verbose(ex); - } - - return errors; - } - - private Path specFile(BuildEnv env, Package pkg) { - return env.buildRoot().resolve(Path.of("SPECS", pkg.packageName() + ".spec")); - } - - private Path buildRPM(BuildEnv env, Package pkg, Path outdir) throws IOException { - - Path rpmFile = outdir.toAbsolutePath().resolve(pkg.packageFileNameWithSuffix()); - - Log.verbose(I18N.format("message.outputting-bundle-location", rpmFile.getParent())); - - //run rpmbuild - Executor.of(TOOL_RPMBUILD, - "-bb", specFile(env, pkg).toAbsolutePath().toString(), - "--define", String.format("%%_sourcedir %s", - env.appImageDir().toAbsolutePath()), - // save result to output dir - "--define", String.format("%%_rpmdir %s", rpmFile.getParent()), - // do not use other system directories to build as current user - "--define", String.format("%%_topdir %s", - env.buildRoot().toAbsolutePath()), - "--define", String.format("%%_rpmfilename %s", rpmFile.getFileName()) - ).executeExpectSuccess(); - - Log.verbose(I18N.format("message.output-bundle-location", rpmFile.getParent())); - - return rpmFile; - } - @Override public String getName() { return I18N.getString("rpm.bundler.name"); @@ -219,12 +50,28 @@ public class LinuxRpmBundler extends LinuxPackageBundler { } @Override - public boolean supported(boolean runtimeInstaller) { - return OperatingSystem.isLinux() && (createRpmbuildToolValidator().validate() == null); + public Path execute(Map params, Path outputParentDir) throws PackagerException { + + return Packager.build().outputDir(outputParentDir) + .pkg(LinuxFromParams.RPM_PACKAGE.fetchFrom(params)) + .env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params)) + .pipelineBuilderMutatorFactory((env, pkg, outputDir) -> { + return new LinuxRpmPackager(env, pkg, outputDir, sysEnv.orElseThrow()); + }).execute(LinuxPackagingPipeline.build()); + } + + @Override + protected Result sysEnv() { + return sysEnv; } @Override public boolean isDefault() { - return !LinuxDebBundler.isDebian(); + return sysEnv.value() + .map(LinuxSystemEnvironment::nativePackageType) + .map(StandardPackageType.LINUX_RPM::equals) + .orElse(false); } + + private final Result sysEnv = LinuxRpmSystemEnvironment.create(SYS_ENV); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java new file mode 100644 index 00000000000..60355d0d1a2 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmPackager.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal; + +import static java.util.stream.Collectors.joining; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.LinuxRpmPackage; + + +/** + * There are two command line options to configure license information for RPM + * packaging: --linux-rpm-license-type and --license-file. Value of + * --linux-rpm-license-type command line option configures "License:" section + * of RPM spec. Value of --license-file command line option specifies a license + * file to be added to the package. License file is a sort of documentation file + * but it will be installed even if user selects an option to install the + * package without documentation. --linux-rpm-license-type is the primary option + * to set license information. --license-file makes little sense in case of RPM + * packaging. + */ +final class LinuxRpmPackager extends LinuxPackager { + + LinuxRpmPackager(BuildEnv env, LinuxRpmPackage pkg, Path outputDir, LinuxRpmSystemEnvironment sysEnv) { + super(env, pkg, outputDir, sysEnv); + this.sysEnv = Objects.requireNonNull(sysEnv); + } + + @Override + protected void createConfigFiles(Map replacementData) throws IOException { + Path specFile = specFile(); + + // prepare spec file + env.createResource("template.spec") + .setCategory(I18N.getString("resource.rpm-spec-file")) + .setSubstitutionData(replacementData) + .saveToFile(specFile); + } + + @Override + protected Map createReplacementData() { + Map data = new HashMap<>(); + + data.put("APPLICATION_RELEASE", pkg.release().orElseThrow()); + data.put("APPLICATION_PREFIX", installPrefix().toString()); + data.put("APPLICATION_DIRECTORY", Path.of("/").resolve(pkg.relativeInstallDir()).toString()); + data.put("APPLICATION_SUMMARY", pkg.app().name()); + data.put("APPLICATION_LICENSE_TYPE", pkg.licenseType()); + + String licenseFile = pkg.licenseFile().map(v -> { + return v.toAbsolutePath().normalize().toString(); + }).orElse(null); + data.put("APPLICATION_LICENSE_FILE", licenseFile); + data.put("APPLICATION_GROUP", pkg.category().orElse("")); + + data.put("APPLICATION_URL", pkg.aboutURL().orElse("")); + + return data; + } + + @Override + protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) { + libProvidersLookup.setPackageLookup(file -> { + return Executor.of(sysEnv.rpm().toString(), + "-q", "--queryformat", "%{name}\\n", + "-q", "--whatprovides", file.toString() + ).saveOutput(true).executeExpectSuccess().getOutput().stream(); + }); + } + + @Override + protected List findErrorsInOutputPackage() throws IOException { + List errors = new ArrayList<>(); + + var specFileName = specFile().getFileName().toString(); + + var properties = List.of( + new PackageProperty("Name", pkg.packageName(), + "APPLICATION_PACKAGE", specFileName), + new PackageProperty("Version", pkg.version(), + "APPLICATION_VERSION", specFileName), + new PackageProperty("Release", pkg.release().orElseThrow(), + "APPLICATION_RELEASE", specFileName), + new PackageProperty("Arch", pkg.arch(), null, specFileName)); + + var actualValues = Executor.of( + sysEnv.rpm().toString(), + "-qp", + "--queryformat", properties.stream().map(e -> String.format("%%{%s}", e.name)).collect(joining("\\n")), + outputPackageFile().toString() + ).saveOutput(true).executeExpectSuccess().getOutput(); + + for (int i = 0; i != properties.size(); i++) { + Optional.ofNullable(properties.get(i).verifyValue(actualValues.get(i))).ifPresent(errors::add); + } + + return errors; + } + + @Override + protected void buildPackage() throws IOException { + + Path rpmFile = outputPackageFile(); + + Log.verbose(I18N.format("message.outputting-bundle-location", rpmFile.getParent())); + + //run rpmbuild + Executor.of(sysEnv.rpmbuild().toString(), + "-bb", specFile().toAbsolutePath().toString(), + "--define", String.format("%%_sourcedir %s", + env.appImageDir().toAbsolutePath()), + // save result to output dir + "--define", String.format("%%_rpmdir %s", rpmFile.getParent()), + // do not use other system directories to build as current user + "--define", String.format("%%_topdir %s", + env.buildRoot().toAbsolutePath()), + "--define", String.format("%%_rpmfilename %s", rpmFile.getFileName()) + ).executeExpectSuccess(); + + Log.verbose(I18N.format("message.output-bundle-location", rpmFile.getParent())); + } + + private Path installPrefix() { + Path path = pkg.relativeInstallDir(); + if (!pkg.isInstallDirInUsrTree()) { + path = path.getParent(); + } + return Path.of("/").resolve(path); + } + + private Path specFile() { + return env.buildRoot().resolve(Path.of("SPECS", pkg.packageName() + ".spec")); + } + + private final LinuxRpmSystemEnvironment sysEnv; +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java new file mode 100644 index 00000000000..58c10668227 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.LinuxSystemEnvironment.mixin; + +import jdk.jpackage.internal.util.Result; + +public interface LinuxRpmSystemEnvironment extends LinuxSystemEnvironment, LinuxRpmSystemEnvironmentMixin { + + static Result create(Result base) { + return mixin(LinuxRpmSystemEnvironment.class, base, LinuxRpmSystemEnvironmentMixin::create); + } +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java new file mode 100644 index 00000000000..b741495f5ed --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import jdk.jpackage.internal.model.DottedVersion; +import jdk.jpackage.internal.util.Result; + +public interface LinuxRpmSystemEnvironmentMixin { + Path rpm(); + Path rpmbuild(); + + record Stub(Path rpm, Path rpmbuild) implements LinuxRpmSystemEnvironmentMixin { + } + + static Result create() { + + final var errors = Stream.of( + Internal.createRpmbuildToolValidator(), + new ToolValidator(Internal.TOOL_RPM) + ).map(ToolValidator::validate).filter(Objects::nonNull).toList(); + + if (errors.isEmpty()) { + return Result.ofValue(new Stub(Internal.TOOL_RPM, Internal.TOOL_RPMBUILD)); + } else { + return Result.ofErrors(errors); + } + } + + static final class Internal { + private static ToolValidator createRpmbuildToolValidator() { + Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)"); + return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion( + TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> { + String versionString = lines.limit(1).findFirst().orElseThrow(); + Matcher matcher = pattern.matcher(versionString); + if (matcher.find()) { + return matcher.group(1); + } + return null; + }); + } + + private static final Path TOOL_RPM = Path.of("rpm"); + private static final Path TOOL_RPMBUILD = Path.of("rpmbuild"); + private static final DottedVersion TOOL_RPMBUILD_MIN_VERSION = DottedVersion.lazy("4.10"); + } +} diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java new file mode 100644 index 00000000000..1a70cc938b8 --- /dev/null +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.internal.util.CompositeProxy; +import jdk.jpackage.internal.util.Result; + +public interface LinuxSystemEnvironment extends SystemEnvironment { + boolean soLookupAvailable(); + PackageType nativePackageType(); + + static Result create() { + return detectNativePackageType().map(LinuxSystemEnvironment::create).orElseGet(() -> { + return Result.ofError(new RuntimeException("Unknown native package type")); + }); + } + + static Optional detectNativePackageType() { + if (Internal.isDebian()) { + return Optional.of(StandardPackageType.LINUX_DEB); + } else if (Internal.isRpm()) { + return Optional.of(StandardPackageType.LINUX_RPM); + } else { + return Optional.empty(); + } + } + + static Result create(PackageType nativePackageType) { + return Result.ofValue(new Stub(LibProvidersLookup.supported(), + Objects.requireNonNull(nativePackageType))); + } + + static U createWithMixin(Class type, LinuxSystemEnvironment base, T mixin) { + return CompositeProxy.create(type, base, mixin); + } + + static Result mixin(Class type, + Result base, Supplier> mixinResultSupplier) { + final var mixin = mixinResultSupplier.get(); + + final List errors = new ArrayList<>(); + errors.addAll(base.errors()); + errors.addAll(mixin.errors()); + + if (errors.isEmpty()) { + return Result.ofValue(createWithMixin(type, base.orElseThrow(), mixin.orElseThrow())); + } else { + return Result.ofErrors(errors); + } + } + + record Stub(boolean soLookupAvailable, PackageType nativePackageType) implements LinuxSystemEnvironment { + } + + static final class Internal { + + private static boolean isDebian() { + // we are just going to run "dpkg -s coreutils" and assume Debian + // or derivative if no error is returned. + try { + Executor.of("dpkg", "-s", "coreutils").executeExpectSuccess(); + return true; + } catch (IOException e) { + // just fall thru + return false; + } + } + + private static boolean isRpm() { + // we are just going to run "rpm -q rpm" and assume RPM + // or derivative if no error is returned. + try { + Executor.of("rpm", "-q", "rpm").executeExpectSuccess(); + return true; + } catch (IOException e) { + // just fall thru + return false; + } + } + } +} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java index d2c72765d7a..0ddb987dbee 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java @@ -25,12 +25,14 @@ package jdk.jpackage.internal; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Objects; +import java.util.Optional; import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.MacDmgPackage; import jdk.jpackage.internal.model.PackagerException; +import jdk.jpackage.internal.util.Result; public class MacDmgBundler extends MacBaseInstallerBundler { @@ -70,39 +72,27 @@ public class MacDmgBundler extends MacBaseInstallerBundler { public Path execute(Map params, Path outputParentDir) throws PackagerException { - final var pkg = MacFromParams.DMG_PACKAGE.fetchFrom(params); - var env = MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params); + var pkg = MacFromParams.DMG_PACKAGE.fetchFrom(params); - final var packager = MacDmgPackager.build().outputDir(outputParentDir).pkg(pkg).env(env); + Log.verbose(I18N.format("message.building-dmg", pkg.app().name())); - MacDmgPackager.findSetFileUtility().ifPresent(packager::setFileUtility); - - return packager.execute(); + return Packager.build().outputDir(outputParentDir) + .pkg(pkg) + .env(MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params)) + .pipelineBuilderMutatorFactory((env, _, outputDir) -> { + return new MacDmgPackager(env, pkg, outputDir, sysEnv.orElseThrow()); + }).execute(MacPackagingPipeline.build(Optional.of(pkg))); } @Override public boolean supported(boolean runtimeInstaller) { - return isSupported(); - } - - public static final String[] required = - {"/usr/bin/hdiutil", "/usr/bin/osascript"}; - public static boolean isSupported() { - try { - for (String s : required) { - Path f = Path.of(s); - if (!Files.exists(f) || !Files.isExecutable(f)) { - return false; - } - } - return true; - } catch (Exception e) { - return false; - } + return sysEnv.hasValue(); } @Override public boolean isDefault() { return true; } + + private final Result sysEnv = MacDmgSystemEnvironment.create(); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java index eb3ad3a76c8..6e13a2ff0c1 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java @@ -36,106 +36,25 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.Optional; -import java.util.stream.Stream; +import java.util.function.Consumer; import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; -import jdk.jpackage.internal.PackagingPipeline.StartupParameters; import jdk.jpackage.internal.PackagingPipeline.TaskID; import jdk.jpackage.internal.model.MacDmgPackage; -import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.util.FileUtils; import jdk.jpackage.internal.util.PathGroup; -record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path outputDir, Optional setFileUtility) { +record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, + MacDmgSystemEnvironment sysEnv) implements Consumer { MacDmgPackager { - Objects.requireNonNull(pkg); Objects.requireNonNull(env); - Objects.requireNonNull(hdiutil); + Objects.requireNonNull(pkg); Objects.requireNonNull(outputDir); - Objects.requireNonNull(setFileUtility); + Objects.requireNonNull(sysEnv); } - static Builder build() { - return new Builder(); - } - - static final class Builder extends PackagerBuilder { - - Builder hdiutil(Path v) { - hdiutil = v; - return this; - } - - Builder setFileUtility(Path v) { - setFileUtility = v; - return this; - } - - Path execute() throws PackagerException { - Log.verbose(MessageFormat.format(I18N.getString("message.building-dmg"), - pkg.app().name())); - - IOUtils.writableOutputDir(outputDir); - - return execute(MacPackagingPipeline.build(Optional.of(pkg))); - } - - @Override - protected void configurePackagingPipeline(PackagingPipeline.Builder pipelineBuilder, - StartupParameters startupParameters) { - final var packager = new MacDmgPackager(pkg, startupParameters.packagingEnv(), - validatedHdiutil(), outputDir, Optional.ofNullable(setFileUtility)); - packager.applyToPipeline(pipelineBuilder); - } - - private Path validatedHdiutil() { - return Optional.ofNullable(hdiutil).orElse(HDIUTIL); - } - - private Path hdiutil; - private Path setFileUtility; - } - - // Location of SetFile utility may be different depending on MacOS version - // We look for several known places and if none of them work will - // try to find it - static Optional findSetFileUtility() { - String typicalPaths[] = {"/Developer/Tools/SetFile", - "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"}; - - final var setFilePath = Stream.of(typicalPaths).map(Path::of).filter(Files::isExecutable).findFirst(); - if (setFilePath.isPresent()) { - // Validate SetFile, if Xcode is not installed it will run, but exit with error - // code - try { - if (Executor.of(setFilePath.orElseThrow().toString(), "-h").setQuiet(true).execute() == 0) { - return setFilePath; - } - } catch (Exception ignored) { - // No need for generic find attempt. We found it, but it does not work. - // Probably due to missing xcode. - return Optional.empty(); - } - } - - // generic find attempt - try { - final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile"); - final var code = executor.setQuiet(true).saveOutput(true).execute(); - if (code == 0 && executor.getOutput().isEmpty()) { - final var firstLine = executor.getOutput().getFirst(); - Path f = Path.of(firstLine); - if (Files.exists(f) && Files.isExecutable(f)) { - return Optional.of(f.toAbsolutePath()); - } - } - } catch (IOException ignored) {} - - return Optional.empty(); - } - - private void applyToPipeline(PackagingPipeline.Builder pipelineBuilder) { + @Override + public void accept(PackagingPipeline.Builder pipelineBuilder) { pipelineBuilder .excludeDirFromCopying(outputDir) .task(DmgPackageTaskID.COPY_DMG_CONTENT) @@ -318,7 +237,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // create temp image ProcessBuilder pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "create", hdiUtilVerbosityFlag, "-srcfolder", normalizedAbsolutePathString(srcFolder), @@ -341,7 +260,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // We need extra room for icons and background image. When we providing // actual files to hdiutil, it will create DMG with ~50 megabytes extra room. pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "create", hdiUtilVerbosityFlag, "-size", String.valueOf(size), @@ -357,7 +276,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // mount temp image pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "attach", normalizedAbsolutePathString(protoDMG), hdiUtilVerbosityFlag, @@ -382,7 +301,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // to install-dir in DMG as critical error, since it can fail in // headless environment. try { - pb = new ProcessBuilder("/usr/bin/osascript", + pb = new ProcessBuilder(sysEnv.osascript().toString(), normalizedAbsolutePathString(volumeScript())); IOUtils.exec(pb, 180); // Wait 3 minutes. See JDK-8248248. } catch (IOException ex) { @@ -397,7 +316,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // NB: attributes of the root directory are ignored // when creating the volume // Therefore we have to do this after we mount image - if (setFileUtility.isPresent()) { + if (sysEnv.setFileUtility().isPresent()) { //can not find utility => keep going without icon try { volumeIconFile.toFile().setWritable(true); @@ -406,14 +325,14 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // "icnC" for the volume icon // (might not work on Mac 10.13 with old XCode) pb = new ProcessBuilder( - setFileUtility.orElseThrow().toString(), + sysEnv.setFileUtility().orElseThrow().toString(), "-c", "icnC", normalizedAbsolutePathString(volumeIconFile)); IOUtils.exec(pb); volumeIconFile.toFile().setReadOnly(); pb = new ProcessBuilder( - setFileUtility.orElseThrow().toString(), + sysEnv.setFileUtility().orElseThrow().toString(), "-a", "C", normalizedAbsolutePathString(mountedVolume)); IOUtils.exec(pb); @@ -428,7 +347,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output } finally { // Detach the temporary image pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "detach", hdiUtilVerbosityFlag, normalizedAbsolutePathString(mountedVolume)); @@ -451,7 +370,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // Now force to detach if it still attached if (Files.exists(mountedVolume)) { pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "detach", "-force", hdiUtilVerbosityFlag, @@ -464,7 +383,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output // Compress it to a new image pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "convert", normalizedAbsolutePathString(protoDMG), hdiUtilVerbosityFlag, @@ -481,7 +400,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output Files.copy(protoDMG, protoCopyDMG); try { pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "convert", normalizedAbsolutePathString(protoCopyDMG), hdiUtilVerbosityFlag, @@ -496,7 +415,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output //add license if needed if (pkg.licenseFile().isPresent()) { pb = new ProcessBuilder( - hdiutil.toString(), + sysEnv.hdiutil().toString(), "udifrez", normalizedAbsolutePathString(finalDMG), "-xml", @@ -527,6 +446,4 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output private static final String TEMPLATE_BUNDLE_ICON = "JavaApp.icns"; private static final String DEFAULT_LICENSE_PLIST="lic_template.plist"; - - private static final Path HDIUTIL = Path.of("/usr/bin/hdiutil"); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java new file mode 100644 index 00000000000..54eb0c6f4fe --- /dev/null +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.Result; + +record MacDmgSystemEnvironment(Path hdiutil, Path osascript, Optional setFileUtility) implements SystemEnvironment { + + MacDmgSystemEnvironment { + } + + static Result create() { + final var errors = Stream.of(HDIUTIL, OSASCRIPT) + .map(ToolValidator::new) + .map(ToolValidator::checkExistsOnly) + .map(ToolValidator::validate) + .filter(Objects::nonNull) + .toList(); + if (errors.isEmpty()) { + return Result.ofValue(new MacDmgSystemEnvironment(HDIUTIL, OSASCRIPT, findSetFileUtility())); + } else { + return Result.ofErrors(errors); + } + } + + // Location of SetFile utility may be different depending on MacOS version + // We look for several known places and if none of them work will + // try to find it + private static Optional findSetFileUtility() { + String typicalPaths[] = {"/Developer/Tools/SetFile", + "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"}; + + final var setFilePath = Stream.of(typicalPaths).map(Path::of).filter(Files::isExecutable).findFirst(); + if (setFilePath.isPresent()) { + // Validate SetFile, if Xcode is not installed it will run, but exit with error + // code + try { + if (Executor.of(setFilePath.orElseThrow().toString(), "-h").setQuiet(true).execute() == 0) { + return setFilePath; + } + } catch (Exception ignored) { + // No need for generic find attempt. We found it, but it does not work. + // Probably due to missing xcode. + return Optional.empty(); + } + } + + // generic find attempt + try { + final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile"); + final var code = executor.setQuiet(true).saveOutput(true).execute(); + if (code == 0 && !executor.getOutput().isEmpty()) { + final var firstLine = executor.getOutput().getFirst(); + Path f = Path.of(firstLine); + if (new ToolValidator(f).checkExistsOnly().validate() == null) { + return Optional.of(f.toAbsolutePath()); + } + } + } catch (IOException ignored) {} + + return Optional.empty(); + } + + private static final Path HDIUTIL = Path.of("/usr/bin/hdiutil"); + private static final Path OSASCRIPT = Path.of("/usr/bin/osascript"); +} diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java index bc19e7c4a1a..e827f238db3 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java @@ -28,7 +28,9 @@ package jdk.jpackage.internal; import java.nio.file.Path; import java.util.Map; import java.util.Objects; +import java.util.Optional; import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.MacPkgPackage; import jdk.jpackage.internal.model.PackagerException; public class MacPkgBundler extends MacBaseInstallerBundler { @@ -49,7 +51,7 @@ public class MacPkgBundler extends MacBaseInstallerBundler { try { Objects.requireNonNull(params); - final var pkgPkg = MacFromParams.PKG_PACKAGE.fetchFrom(params); + final var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params); // run basic validation to ensure requirements are met // we are not interested in return code, only possible exception @@ -72,12 +74,16 @@ public class MacPkgBundler extends MacBaseInstallerBundler { public Path execute(Map params, Path outputParentDir) throws PackagerException { - final var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params); - var env = MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params); + var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params); - final var packager = MacPkgPackager.build().outputDir(outputParentDir).pkg(pkg).env(env); + Log.verbose(I18N.format("message.building-pkg", pkg.app().name())); - return packager.execute(); + return Packager.build().outputDir(outputParentDir) + .pkg(pkg) + .env(MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params)) + .pipelineBuilderMutatorFactory((env, _, outputDir) -> { + return new MacPkgPackager(env, pkg, outputDir); + }).execute(MacPackagingPipeline.build(Optional.of(pkg))); } @Override diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java index 1fb0b9bc160..891c5b120f5 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Stream; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; @@ -52,15 +53,25 @@ import javax.xml.transform.stream.StreamSource; import jdk.internal.util.Architecture; import jdk.internal.util.OSVersion; import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; -import jdk.jpackage.internal.PackagingPipeline.StartupParameters; import jdk.jpackage.internal.PackagingPipeline.TaskID; import jdk.jpackage.internal.model.MacPkgPackage; -import jdk.jpackage.internal.model.PackagerException; import jdk.jpackage.internal.resources.ResourceLocator; import jdk.jpackage.internal.util.XmlUtils; import org.xml.sax.SAXException; -record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional services, Path outputDir) { +record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional services, + Path outputDir) implements Consumer { + + MacPkgPackager { + Objects.requireNonNull(env); + Objects.requireNonNull(pkg); + Objects.requireNonNull(services); + Objects.requireNonNull(outputDir); + } + + MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Path outputDir) { + this(env, pkg, createServices(env, pkg), outputDir); + } enum PkgPackageTaskID implements TaskID { PREPARE_MAIN_SCRIPTS, @@ -69,37 +80,6 @@ record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional servic PREPARE_SERVICES } - static Builder build() { - return new Builder(); - } - - static final class Builder extends PackagerBuilder { - - Path execute() throws PackagerException { - Log.verbose(MessageFormat.format(I18N.getString("message.building-pkg"), - pkg.app().name())); - - IOUtils.writableOutputDir(outputDir); - - return execute(MacPackagingPipeline.build(Optional.of(pkg))); - } - - @Override - protected void configurePackagingPipeline(PackagingPipeline.Builder pipelineBuilder, - StartupParameters startupParameters) { - final var packager = new MacPkgPackager(pkg, startupParameters.packagingEnv(), createServices(), outputDir); - packager.applyToPipeline(pipelineBuilder); - } - - private Optional createServices() { - if (pkg.app().isService()) { - return Optional.of(Services.create(pkg, env)); - } else { - return Optional.empty(); - } - } - } - record InternalPackage(Path srcRoot, String identifier, Path path, List otherPkgbuildArgs) { InternalPackage { @@ -230,7 +210,8 @@ record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional servic private final Optional nameSuffix; } - private void applyToPipeline(PackagingPipeline.Builder pipelineBuilder) { + @Override + public void accept(PackagingPipeline.Builder pipelineBuilder) { pipelineBuilder .excludeDirFromCopying(outputDir) .task(PkgPackageTaskID.PREPARE_MAIN_SCRIPTS) @@ -559,6 +540,14 @@ record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional servic IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT); } + private static Optional createServices(BuildEnv env, MacPkgPackage pkg) { + if (pkg.app().isService()) { + return Optional.of(Services.create(pkg, env)); + } else { + return Optional.empty(); + } + } + private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png"; private static final String DEFAULT_PDF = "product-def.plist"; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagerBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Packager.java similarity index 57% rename from src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagerBuilder.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/Packager.java index 4ba2f0fbda6..501fd64bdca 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagerBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Packager.java @@ -26,50 +26,80 @@ package jdk.jpackage.internal; import java.nio.file.Path; import java.util.Objects; -import jdk.jpackage.internal.PackagingPipeline.StartupParameters; +import java.util.Optional; +import java.util.function.Consumer; import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.model.PackagerException; -abstract class PackagerBuilder> { +final class Packager { - U pkg(T v) { + static Packager build() { + return new Packager<>(); + } + + Packager pkg(T v) { pkg = v; - return thiz(); + return this; } - U env(BuildEnv v) { + Packager env(BuildEnv v) { env = v; - return thiz(); + return this; } - U outputDir(Path v) { + Packager outputDir(Path v) { outputDir = v; - return thiz(); + return this; } - @SuppressWarnings("unchecked") - private U thiz() { - return (U)this; + Packager pipelineBuilderMutatorFactory(PipelineBuilderMutatorFactory v) { + pipelineBuilderMutatorFactory = v; + return this; } - protected abstract void configurePackagingPipeline(PackagingPipeline.Builder pipelineBuilder, - StartupParameters startupParameters); + T pkg() { + return Objects.requireNonNull(pkg); + } + + Path outputDir() { + return Objects.requireNonNull(outputDir); + } + + BuildEnv env() { + return Objects.requireNonNull(env); + } Path execute(PackagingPipeline.Builder pipelineBuilder) throws PackagerException { Objects.requireNonNull(pkg); Objects.requireNonNull(env); Objects.requireNonNull(outputDir); + IOUtils.writableOutputDir(outputDir); + final var startupParameters = pipelineBuilder.createStartupParameters(env, pkg, outputDir); - configurePackagingPipeline(pipelineBuilder, startupParameters); + pipelineBuilderMutatorFactory().ifPresent(factory -> { + factory.create(startupParameters.packagingEnv(), pkg, outputDir).accept(pipelineBuilder); + }); pipelineBuilder.create().execute(startupParameters); return outputDir.resolve(pkg.packageFileNameWithSuffix()); } - protected T pkg; - protected BuildEnv env; - protected Path outputDir; + + @FunctionalInterface + interface PipelineBuilderMutatorFactory { + Consumer create(BuildEnv env, T pkg, Path outputDir); + } + + + private Optional> pipelineBuilderMutatorFactory() { + return Optional.ofNullable(pipelineBuilderMutatorFactory); + } + + private T pkg; + private BuildEnv env; + private Path outputDir; + private PipelineBuilderMutatorFactory pipelineBuilderMutatorFactory; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java new file mode 100644 index 00000000000..de98e97c922 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +public interface SystemEnvironment { +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java index f673ac7e33e..13e87d5cfa6 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java @@ -24,36 +24,31 @@ */ package jdk.jpackage.internal; -import jdk.internal.util.OperatingSystem; -import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.DottedVersion; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.DottedVersion; -public final class ToolValidator { +final class ToolValidator { ToolValidator(String tool) { this(Path.of(tool)); } ToolValidator(Path toolPath) { - this.toolPath = toolPath; - args = new ArrayList<>(); - + this.toolPath = Objects.requireNonNull(toolPath); if (OperatingSystem.isLinux()) { setCommandLine("--version"); } - - setToolNotFoundErrorHandler(null); - setToolOldVersionErrorHandler(null); } ToolValidator setCommandLine(String... args) { @@ -67,7 +62,17 @@ public final class ToolValidator { } ToolValidator setMinimalVersion(DottedVersion v) { - return setMinimalVersion(t -> DottedVersion.compareComponents(v, DottedVersion.lazy(t))); + return setMinimalVersion(new Comparable() { + @Override + public int compareTo(String o) { + return DottedVersion.compareComponents(v, DottedVersion.lazy(o)); + } + + @Override + public String toString() { + return v.toString(); + } + }); } ToolValidator setVersionParser(Function, String> v) { @@ -75,69 +80,85 @@ public final class ToolValidator { return this; } - ToolValidator setToolNotFoundErrorHandler( - BiFunction v) { + ToolValidator setToolNotFoundErrorHandler(Function v) { toolNotFoundErrorHandler = v; return this; } - ToolValidator setToolOldVersionErrorHandler(BiFunction v) { + ToolValidator setToolOldVersionErrorHandler(BiFunction v) { toolOldVersionErrorHandler = v; return this; } + ToolValidator checkExistsOnly(boolean v) { + checkExistsOnly = v; + return this; + } + + ToolValidator checkExistsOnly() { + return checkExistsOnly(true); + } + ConfigException validate() { + if (checkExistsOnly) { + if (Files.isExecutable(toolPath) && !Files.isDirectory(toolPath)) { + return null; + } else if (Files.exists(toolPath)) { + return new ConfigException( + I18N.format("error.tool-not-executable", toolPath), (String)null); + } else if (toolNotFoundErrorHandler != null) { + return toolNotFoundErrorHandler.apply(toolPath); + } else { + return new ConfigException( + I18N.format("error.tool-not-found", toolPath), + I18N.format("error.tool-not-found.advice", toolPath)); + } + } + List cmdline = new ArrayList<>(); cmdline.add(toolPath.toString()); - cmdline.addAll(args); + if (args != null) { + cmdline.addAll(args); + } + + boolean canUseTool[] = new boolean[1]; + if (minimalVersion == null) { + // No version check. + canUseTool[0] = true; + } + + String[] version = new String[1]; - String name = IOUtils.getFileName(toolPath).toString(); try { - ProcessBuilder pb = new ProcessBuilder(cmdline); - AtomicBoolean canUseTool = new AtomicBoolean(); - if (minimalVersion == null) { - // No version check. - canUseTool.setPlain(true); - } - - String[] version = new String[1]; - Executor.of(pb).setQuiet(true).setOutputConsumer(lines -> { + Executor.of(cmdline.toArray(String[]::new)).setQuiet(true).setOutputConsumer(lines -> { if (versionParser != null && minimalVersion != null) { version[0] = versionParser.apply(lines); - if (minimalVersion.compareTo(version[0]) < 0) { - canUseTool.setPlain(true); + if (version[0] != null && minimalVersion.compareTo(version[0]) <= 0) { + canUseTool[0] = true; } } }).execute(); - - if (!canUseTool.getPlain()) { - if (toolOldVersionErrorHandler != null) { - return toolOldVersionErrorHandler.apply(name, version[0]); - } - return new ConfigException(MessageFormat.format(I18N.getString( - "error.tool-old-version"), name, minimalVersion), - MessageFormat.format(I18N.getString( - "error.tool-old-version.advice"), name, - minimalVersion)); - } } catch (IOException e) { - if (toolNotFoundErrorHandler != null) { - return toolNotFoundErrorHandler.apply(name, e); - } - return new ConfigException(MessageFormat.format(I18N.getString( - "error.tool-not-found"), name, e.getMessage()), - MessageFormat.format(I18N.getString( - "error.tool-not-found.advice"), name), e); + return new ConfigException(I18N.format("error.tool-error", toolPath, e.getMessage()), null, e); } - // All good. Tool can be used. - return null; + if (canUseTool[0]) { + // All good. Tool can be used. + return null; + } else if (toolOldVersionErrorHandler != null) { + return toolOldVersionErrorHandler.apply(toolPath, version[0]); + } else { + return new ConfigException( + I18N.format("error.tool-old-version", toolPath, minimalVersion), + I18N.format("error.tool-old-version.advice", toolPath, minimalVersion)); + } } private final Path toolPath; private List args; private Comparable minimalVersion; private Function, String> versionParser; - private BiFunction toolNotFoundErrorHandler; - private BiFunction toolOldVersionErrorHandler; + private Function toolNotFoundErrorHandler; + private BiFunction toolOldVersionErrorHandler; + private boolean checkExistsOnly; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index 684a97bc1bd..967549f6855 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -66,10 +66,13 @@ error.no-content-types-for-file-association.advice=Specify MIME type for File As error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0} error.too-many-content-types-for-file-association.advice=Specify only one MIME type for File Association number {0} -error.tool-not-found=Can not find {0}. Reason: {1} -error.tool-not-found.advice=Please install {0} -error.tool-old-version=Can not find {0} {1} or newer -error.tool-old-version.advice=Please install {0} {1} or newer +error.tool-error=Can not validate "{0}". Reason: {1} +error.tool-not-executable="{0}" is not executable +error.tool-not-found=Can not find "{0}" +error.tool-not-found.advice=Please install "{0}" +error.tool-old-version=Can not find "{0}" {1} or newer +error.tool-old-version.advice=Please install "{0}" {1} or newer + error.jlink.failed=jlink failed with: {0} error.blocked.option=jlink option [{0}] is not permitted in --jlink-options error.no.name=Name not specified with --name and cannot infer one from app-image diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Result.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Result.java new file mode 100644 index 00000000000..7bd6408183a --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Result.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal.util; + +import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.StreamSupport; + + +public record Result(Optional value, Collection errors) { + public Result { + if (value.isEmpty() == errors.isEmpty()) { + throw new IllegalArgumentException(); + } + + if (value.isEmpty() && errors.isEmpty()) { + throw new IllegalArgumentException("Error collection must be non-empty"); + } + + } + + public T orElseThrow() { + firstError().ifPresent(ex -> { + rethrowUnchecked(ex); + }); + return value.orElseThrow(); + } + + public boolean hasValue() { + return value.isPresent(); + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } + + public Result map(Function conv) { + return new Result<>(value.map(conv), errors); + } + + public Result flatMap(Function> conv) { + return value.map(conv).orElseGet(() -> { + return new Result<>(Optional.empty(), errors); + }); + } + + public Result mapErrors(UnaryOperator> errorsMapper) { + return new Result<>(value, errorsMapper.apply(errors)); + } + + public Result mapErrors() { + return new Result<>(Optional.empty(), errors); + } + + public Result peekErrors(Consumer> consumer) { + if (hasErrors()) { + consumer.accept(errors); + } + return this; + } + + public Result peekValue(Consumer consumer) { + value.ifPresent(consumer); + return this; + } + + public Optional firstError() { + return errors.stream().findFirst(); + } + + public static Result create(Supplier supplier) { + try { + return ofValue(supplier.get()); + } catch (Exception ex) { + return ofError(ex); + } + } + + public static Result ofValue(T value) { + return new Result<>(Optional.of(value), List.of()); + } + + public static Result ofErrors(Collection errors) { + return new Result<>(Optional.empty(), List.copyOf(errors)); + } + + public static Result ofError(Exception error) { + return ofErrors(List.of(error)); + } + + public static boolean allHaveValues(Iterable> results) { + return StreamSupport.stream(results.spliterator(), false).allMatch(Result::hasValue); + } + + public static boolean allHaveValues(Result... results) { + return allHaveValues(List.of(results)); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java index 54f43f05715..9ce758eb3c7 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java @@ -24,17 +24,11 @@ */ package jdk.jpackage.internal; -import static jdk.jpackage.internal.StandardBundlerParam.ICON; -import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable; - -import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.Map; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.PackagerException; -import jdk.jpackage.internal.model.WinExePackage; +import jdk.jpackage.internal.model.WinMsiPackage; @SuppressWarnings("restricted") public class WinExeBundler extends AbstractBundler { @@ -79,63 +73,23 @@ public class WinExeBundler extends AbstractBundler { throws PackagerException { // Order is important! - var pkg = WinFromParams.MSI_PACKAGE.fetchFrom(params); + var pkg = WinFromParams.EXE_PACKAGE.fetchFrom(params); var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - IOUtils.writableOutputDir(outdir); + var msiOutputDir = env.buildRoot().resolve("msi"); - Path msiDir = env.buildRoot().resolve("msi"); - toRunnable(() -> Files.createDirectories(msiDir)).run(); - - // Write msi to temporary directory. - Path msi = msiBundler.execute(params, msiDir); - - try { - new ScriptRunner() - .setDirectory(msi.getParent()) - .setResourceCategoryId("resource.post-msi-script") - .setScriptNameSuffix("post-msi") - .setEnvironmentVariable("JpMsiFile", msi.toAbsolutePath().toString()) - .run(env, pkg.packageName()); - - var exePkg = new WinExePackageBuilder(pkg).icon(ICON.fetchFrom(params)).create(); - return buildEXE(env, exePkg, msi, outdir); - } catch (IOException|ConfigException ex) { - Log.verbose(ex); - throw new PackagerException(ex); - } + return Packager.build().outputDir(msiOutputDir) + .pkg(pkg.msiPackage()) + .env(env) + .pipelineBuilderMutatorFactory((packagingEnv, msiPackage, _) -> { + var msiPackager = new WinMsiPackager(packagingEnv, msiPackage, + msiOutputDir, msiBundler.sysEnv.orElseThrow()); + var exePackager = new WinExePackager(packagingEnv, pkg, outdir, msiOutputDir); + return msiPackager.andThen(exePackager); + }).execute(WinPackagingPipeline.build()); } - private Path buildEXE(BuildEnv env, WinExePackage pkg, Path msi, - Path outdir) throws IOException { - - Log.verbose(I18N.format("message.outputting-to-location", outdir.toAbsolutePath())); - - // Copy template msi wrapper next to msi file - final Path exePath = msi.getParent().resolve(pkg.packageFileNameWithSuffix()); - - env.createResource("msiwrapper.exe") - .setCategory(I18N.getString("resource.installer-exe")) - .setPublicName("installer.exe") - .saveToFile(exePath); - - new ExecutableRebrander(pkg, env::createResource, resourceLock -> { - // Embed msi in msi wrapper exe. - embedMSI(resourceLock, msi.toAbsolutePath().toString()); - }).execute(env, exePath, pkg.icon()); - - Path dstExePath = outdir.resolve(exePath.getFileName()); - - Files.copy(exePath, dstExePath, StandardCopyOption.REPLACE_EXISTING); - - dstExePath.toFile().setExecutable(true); - - Log.verbose(I18N.format("message.output-location", outdir.toAbsolutePath())); - - return dstExePath; - } + static native int embedMSI(long resourceLock, String msiPath); private final WinMsiBundler msiBundler = new WinMsiBundler(); - - private static native int embedMSI(long resourceLock, String msiPath); } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.java new file mode 100644 index 00000000000..9a13a0f954d --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExePackager.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Objects; +import java.util.function.Consumer; +import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; +import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; +import jdk.jpackage.internal.PackagingPipeline.TaskID; +import jdk.jpackage.internal.model.WinExePackage; + +final record WinExePackager(BuildEnv env, WinExePackage pkg, Path outputDir, Path msiOutputDir) implements Consumer { + + WinExePackager { + Objects.requireNonNull(env); + Objects.requireNonNull(pkg); + Objects.requireNonNull(outputDir); + Objects.requireNonNull(msiOutputDir); + } + + enum ExePackageTaskID implements TaskID { + RUN_POST_MSI_USER_SCRIPT, + WRAP_MSI_IN_EXE + } + + @Override + public void accept(PackagingPipeline.Builder pipelineBuilder) { + pipelineBuilder.excludeDirFromCopying(outputDir) + .task(ExePackageTaskID.RUN_POST_MSI_USER_SCRIPT) + .action(this::runPostMsiScript) + .addDependency(PackageTaskID.CREATE_PACKAGE_FILE) + .add() + .task(ExePackageTaskID.WRAP_MSI_IN_EXE) + .action(this::wrapMsiInExe) + .addDependency(ExePackageTaskID.RUN_POST_MSI_USER_SCRIPT) + .addDependent(PrimaryTaskID.PACKAGE) + .add(); + } + + private Path msi() { + return msiOutputDir.resolve(pkg.msiPackage().packageFileNameWithSuffix()); + } + + private void runPostMsiScript() throws IOException { + new ScriptRunner() + .setDirectory(msiOutputDir) + .setResourceCategoryId("resource.post-msi-script") + .setScriptNameSuffix("post-msi") + .setEnvironmentVariable("JpMsiFile", msi().toAbsolutePath().toString()) + .run(env, pkg.msiPackage().packageName()); + } + + private void wrapMsiInExe() throws IOException { + + Log.verbose(I18N.format("message.outputting-to-location", outputDir.toAbsolutePath())); + + final var msi = msi(); + + // Copy template msi wrapper next to msi file + final Path exePath = msi.getParent().resolve(pkg.packageFileNameWithSuffix()); + + env.createResource("msiwrapper.exe") + .setCategory(I18N.getString("resource.installer-exe")) + .setPublicName("installer.exe") + .saveToFile(exePath); + + new ExecutableRebrander(pkg, env::createResource, resourceLock -> { + // Embed msi in msi wrapper exe. + WinExeBundler.embedMSI(resourceLock, msi.toAbsolutePath().toString()); + }).execute(env, exePath, pkg.icon()); + + Path dstExePath = outputDir.resolve(exePath.getFileName()); + + Files.createDirectories(dstExePath.getParent()); + Files.copy(exePath, dstExePath, StandardCopyOption.REPLACE_EXISTING); + + dstExePath.toFile().setExecutable(true); + + Log.verbose(I18N.format("message.output-location", outputDir.toAbsolutePath())); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java index e2259535058..29c1b665f86 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinFromParams.java @@ -31,6 +31,7 @@ import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam; import static jdk.jpackage.internal.FromParams.createPackageBuilder; import static jdk.jpackage.internal.FromParams.createPackageBundlerParam; import static jdk.jpackage.internal.FromParams.findLauncherShortcut; +import static jdk.jpackage.internal.StandardBundlerParam.ICON; import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; import static jdk.jpackage.internal.WinPackagingPipeline.APPLICATION_LAYOUT; import static jdk.jpackage.internal.model.StandardPackageType.WIN_MSI; @@ -41,6 +42,7 @@ import java.util.Map; import java.util.UUID; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.WinApplication; +import jdk.jpackage.internal.model.WinExePackage; import jdk.jpackage.internal.model.WinLauncher; import jdk.jpackage.internal.model.WinLauncherMixin; import jdk.jpackage.internal.model.WinMsiPackage; @@ -99,12 +101,26 @@ final class WinFromParams { return pkgBuilder.create(); } + private static WinExePackage createWinExePackage(Map params) throws ConfigException, IOException { + + final var msiPkg = MSI_PACKAGE.fetchFrom(params); + + final var pkgBuilder = new WinExePackageBuilder(msiPkg); + + ICON.copyInto(params, pkgBuilder::icon); + + return pkgBuilder.create(); + } + static final BundlerParamInfo APPLICATION = createApplicationBundlerParam( WinFromParams::createWinApplication); static final BundlerParamInfo MSI_PACKAGE = createPackageBundlerParam( WinFromParams::createWinMsiPackage); + static final BundlerParamInfo EXE_PACKAGE = createPackageBundlerParam( + WinFromParams::createWinExePackage); + private static final BundlerParamInfo WIN_MENU_HINT = createStringBundlerParam( Arguments.CLIOptions.WIN_MENU_HINT.getId()); diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java index 9bd0c679146..3ca26f38f82 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java @@ -27,115 +27,16 @@ package jdk.jpackage.internal; import static jdk.jpackage.internal.model.ConfigException.rethrowConfigException; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.PathMatcher; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import jdk.jpackage.internal.PackagingPipeline.PackageBuildEnv; -import jdk.jpackage.internal.model.AppImageLayout; -import jdk.jpackage.internal.model.ApplicationLayout; import jdk.jpackage.internal.model.ConfigException; -import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.model.PackagerException; -import jdk.jpackage.internal.model.RuntimeLayout; import jdk.jpackage.internal.model.WinMsiPackage; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; +import jdk.jpackage.internal.util.Result; -/** - * WinMsiBundler - * - * Produces .msi installer from application image. Uses WiX Toolkit to build - * .msi installer. - *

        - * {@link #execute} method creates a number of source files with the description - * of installer to be processed by WiX tools. Generated source files are stored - * in "config" subdirectory next to "app" subdirectory in the root work - * directory. The following WiX source files are generated: - *

          - *
        • main.wxs. Main source file with the installer description - *
        • bundle.wxf. Source file with application and Java run-time directory tree - * description. - *
        • ui.wxf. Source file with UI description of the installer. - *
        - * - *

        - * main.wxs file is a copy of main.wxs resource from - * jdk.jpackage.internal.resources package. It is parametrized with the - * following WiX variables: - *

          - *
        • JpAppName. Name of the application. Set to the value of --name command - * line option - *
        • JpAppVersion. Version of the application. Set to the value of - * --app-version command line option - *
        • JpAppVendor. Vendor of the application. Set to the value of --vendor - * command line option - *
        • JpAppDescription. Description of the application. Set to the value of - * --description command line option - *
        • JpProductCode. Set to product code UUID of the application. Random value - * generated by jpackage every time {@link #execute} method is called - *
        • JpProductUpgradeCode. Set to upgrade code UUID of the application. Random - * value generated by jpackage every time {@link #execute} method is called if - * --win-upgrade-uuid command line option is not specified. Otherwise this - * variable is set to the value of --win-upgrade-uuid command line option - *
        • JpAllowUpgrades. Set to "yes", but all that matters is it is defined. - *
        • JpAllowDowngrades. Defined for application installers, and undefined for - * Java runtime installers. - *
        • JpConfigDir. Absolute path to the directory with generated WiX source - * files. - *
        • JpIsSystemWide. Set to "yes" if --win-per-user-install command line - * option was not specified. Undefined otherwise - *
        • JpAppSizeKb. Set to estimated size of the application in kilobytes - *
        • JpHelpURL. Set to value of --win-help-url command line option if it - * was specified. Undefined otherwise - *
        • JpAboutURL. Set to value of --about-url command line option if it - * was specified. Undefined otherwise - *
        • JpUpdateURL. Set to value of --win-update-url command line option if it - * was specified. Undefined otherwise - *
        - * - *

        - * ui.wxf file is generated based on --license-file, --win-shortcut-prompt, - * --win-dir-chooser command line options. It is parametrized with the following - * WiX variables: - *

          - *
        • JpLicenseRtf. Set to the value of --license-file command line option. - * Undefined if --license-file command line option was not specified - *
        - */ public class WinMsiBundler extends AbstractBundler { public WinMsiBundler() { - wixFragments = Stream.of( - Map.entry("bundle.wxf", new WixAppImageFragmentBuilder()), - Map.entry("ui.wxf", new WixUiFragmentBuilder()), - Map.entry("os-condition.wxf", OSVersionCondition.createWixFragmentBuilder()) - ).map(e -> { - e.getValue().setOutputFileName(e.getKey()); - return e.getValue(); - }).toList(); } @Override @@ -156,10 +57,12 @@ public class WinMsiBundler extends AbstractBundler { @Override public boolean supported(boolean platformInstaller) { try { - if (wixToolset == null) { - wixToolset = WixTool.createToolset(); + try { + sysEnv.orElseThrow(); + return true; + } catch (RuntimeException ex) { + ConfigException.rethrowConfigException(ex); } - return true; } catch (ConfigException ce) { Log.error(ce.getMessage()); if (ce.getAdvice() != null) { @@ -184,9 +87,7 @@ public class WinMsiBundler extends AbstractBundler { WinFromParams.APPLICATION.fetchFrom(params); BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - if (wixToolset == null) { - wixToolset = WixTool.createToolset(); - } + final var wixToolset = sysEnv.orElseThrow().wixToolset(); for (var tool : wixToolset.getType().getTools()) { Log.verbose(I18N.format("message.tool-version", @@ -194,367 +95,23 @@ public class WinMsiBundler extends AbstractBundler { wixToolset.getVersion())); } - wixFragments.forEach(wixFragment -> wixFragment.setWixVersion(wixToolset.getVersion(), - wixToolset.getType())); - - wixFragments.stream().map(WixFragmentBuilder::getLoggableWixFeatures).flatMap( - List::stream).distinct().toList().forEach(Log::verbose); - return true; } catch (RuntimeException re) { throw rethrowConfigException(re); } } - private void prepareProto(Package pkg, BuildEnv env, AppImageLayout appImageLayout) throws - PackagerException, IOException { - - // Configure installer icon - if (appImageLayout instanceof RuntimeLayout runtimeLayout) { - // Use icon from java launcher. - // Assume java.exe exists in Java Runtime being packed. - // Ignore custom icon if any as we don't want to copy anything in - // Java Runtime image. - installerIcon = runtimeLayout.runtimeDirectory().resolve(Path.of("bin", "java.exe")); - } else if (appImageLayout instanceof ApplicationLayout appLayout) { - installerIcon = appLayout.launchersDirectory().resolve( - pkg.app().mainLauncher().orElseThrow().executableNameWithSuffix()); - } - installerIcon = installerIcon.toAbsolutePath(); - - pkg.licenseFile().ifPresent(licenseFile -> { - // need to copy license file to the working directory - // and convert to rtf if needed - Path destFile = env.configDir().resolve(licenseFile.getFileName()); - - try { - IOUtils.copyFile(licenseFile, destFile); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - destFile.toFile().setWritable(true); - ensureByMutationFileIsRTF(destFile); - }); - } - @Override public Path execute(Map params, Path outputParentDir) throws PackagerException { - IOUtils.writableOutputDir(outputParentDir); - - // Order is important! - var pkg = WinFromParams.MSI_PACKAGE.fetchFrom(params); - var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params); - - WinPackagingPipeline.build() - .excludeDirFromCopying(outputParentDir) - .task(PackagingPipeline.PackageTaskID.CREATE_CONFIG_FILES) - .packageAction(this::prepareConfigFiles) - .add() - .task(PackagingPipeline.PackageTaskID.CREATE_PACKAGE_FILE) - .packageAction(this::buildPackage) - .add() - .create().execute(env, pkg, outputParentDir); - - return outputParentDir.resolve(pkg.packageFileNameWithSuffix()).toAbsolutePath(); + return Packager.build().outputDir(outputParentDir) + .pkg(WinFromParams.MSI_PACKAGE.fetchFrom(params)) + .env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params)) + .pipelineBuilderMutatorFactory((env, pkg, outputDir) -> { + return new WinMsiPackager(env, pkg, outputDir, sysEnv.orElseThrow()); + }).execute(WinPackagingPipeline.build()); } - private void prepareConfigFiles(PackageBuildEnv env) throws PackagerException, IOException { - prepareProto(env.pkg(), env.env(), env.resolvedLayout()); - for (var wixFragment : wixFragments) { - wixFragment.initFromParams(env.env(), env.pkg()); - wixFragment.addFilesToConfigRoot(); - } - - final var msiOut = env.outputDir().resolve(env.pkg().packageFileNameWithSuffix()); - - Log.verbose(I18N.format("message.preparing-msi-config", msiOut.toAbsolutePath())); - - final var wixVars = createWixVars(env); - - final var wixObjDir = env.env().buildRoot().resolve("wixobj"); - - final var configDir = env.env().configDir(); - - final var wixPipelineBuilder = WixPipeline.build() - .setWixObjDir(wixObjDir) - .setWorkDir(env.env().appImageDir()) - .addSource(configDir.resolve("main.wxs"), wixVars); - - for (var wixFragment : wixFragments) { - wixFragment.configureWixPipeline(wixPipelineBuilder); - } - - switch (wixToolset.getType()) { - case Wix3 -> { - wixPipelineBuilder.addLightOptions("-sice:ICE27"); - - if (!env.pkg().isSystemWideInstall()) { - wixPipelineBuilder.addLightOptions("-sice:ICE91"); - } - } - case Wix4 -> { - } - default -> { - throw new IllegalArgumentException(); - } - } - - var primaryWxlFiles = Stream.of("de", "en", "ja", "zh_CN").map(loc -> { - return configDir.resolve("MsiInstallerStrings_" + loc + ".wxl"); - }).toList(); - - var wixResources = new WixSourceConverter.ResourceGroup(wixToolset.getType()); - - // Copy standard l10n files. - for (var path : primaryWxlFiles) { - var name = path.getFileName().toString(); - wixResources.addResource(env.env().createResource(name).setPublicName(name).setCategory( - I18N.getString("resource.wxl-file")), path); - } - - wixResources.addResource(env.env().createResource("main.wxs").setPublicName("main.wxs"). - setCategory(I18N.getString("resource.main-wix-file")), configDir.resolve("main.wxs")); - - wixResources.addResource(env.env().createResource("overrides.wxi").setPublicName( - "overrides.wxi").setCategory(I18N.getString("resource.overrides-wix-file")), - configDir.resolve("overrides.wxi")); - - // Filter out custom l10n files that were already used to - // override primary l10n files. Ignore case filename comparison, - // both lists are expected to be short. - List customWxlFiles = env.env().resourceDir() - .map(WinMsiBundler::getWxlFilesFromDir) - .orElseGet(Collections::emptyList) - .stream() - .filter(custom -> primaryWxlFiles.stream().noneMatch(primary -> - primary.getFileName().toString().equalsIgnoreCase( - custom.getFileName().toString()))) - .peek(custom -> Log.verbose(I18N.format( - "message.using-custom-resource", String.format("[%s]", - I18N.getString("resource.wxl-file")), - custom.getFileName()))).toList(); - - // Copy custom l10n files. - for (var path : customWxlFiles) { - var name = path.getFileName().toString(); - wixResources.addResource(env.env().createResource(name).setPublicName(name). - setSourceOrder(OverridableResource.Source.ResourceDir).setCategory(I18N. - getString("resource.wxl-file")), configDir.resolve(name)); - } - - // Save all WiX resources into config dir. - wixResources.saveResources(); - - // All l10n files are supplied to WiX with "-loc", but only - // Cultures from custom files and a single primary Culture are - // included into "-cultures" list - for (var wxl : primaryWxlFiles) { - wixPipelineBuilder.addLightOptions("-loc", wxl.toString()); - } - - List cultures = new ArrayList<>(); - for (var wxl : customWxlFiles) { - wxl = configDir.resolve(wxl.getFileName()); - wixPipelineBuilder.addLightOptions("-loc", wxl.toString()); - cultures.add(getCultureFromWxlFile(wxl)); - } - - // Append a primary culture bases on runtime locale. - final Path primaryWxlFile = configDir.resolve( - I18N.getString("resource.wxl-file-name")); - cultures.add(getCultureFromWxlFile(primaryWxlFile)); - - // Build ordered list of unique cultures. - Set uniqueCultures = new LinkedHashSet<>(); - uniqueCultures.addAll(cultures); - switch (wixToolset.getType()) { - case Wix3 -> { - wixPipelineBuilder.addLightOptions(uniqueCultures.stream().collect(Collectors.joining(";", - "-cultures:", ""))); - } - case Wix4 -> { - uniqueCultures.forEach(culture -> { - wixPipelineBuilder.addLightOptions("-culture", culture); - }); - } - default -> { - throw new IllegalArgumentException(); - } - } - - Files.createDirectories(wixObjDir); - wixPipeline = wixPipelineBuilder.create(wixToolset); - } - - private void buildPackage(PackageBuildEnv env) throws PackagerException, IOException { - final var msiOut = env.outputDir().resolve(env.pkg().packageFileNameWithSuffix()); - Log.verbose(I18N.format("message.generating-msi", msiOut.toAbsolutePath())); - wixPipeline.buildMsi(msiOut.toAbsolutePath()); - } - - private Map createWixVars(PackageBuildEnv env) throws IOException { - Map data = new HashMap<>(); - - final var pkg = env.pkg(); - - data.put("JpProductCode", pkg.productCode().toString()); - data.put("JpProductUpgradeCode", pkg.upgradeCode().toString()); - - Log.verbose(I18N.format("message.product-code", pkg.productCode())); - Log.verbose(I18N.format("message.upgrade-code", pkg.upgradeCode())); - - data.put("JpAllowUpgrades", "yes"); - if (!pkg.isRuntimeInstaller()) { - data.put("JpAllowDowngrades", "yes"); - } - - data.put("JpAppName", pkg.packageName()); - data.put("JpAppDescription", pkg.description()); - data.put("JpAppVendor", pkg.app().vendor()); - data.put("JpAppVersion", pkg.version()); - if (Files.exists(installerIcon)) { - data.put("JpIcon", installerIcon.toString()); - } - - pkg.helpURL().ifPresent(value -> { - data.put("JpHelpURL", value); - }); - - pkg.updateURL().ifPresent(value -> { - data.put("JpUpdateURL", value); - }); - - pkg.aboutURL().ifPresent(value -> { - data.put("JpAboutURL", value); - }); - - data.put("JpAppSizeKb", Long.toString(AppImageLayout.toPathGroup( - env.resolvedLayout()).sizeInBytes() >> 10)); - - data.put("JpConfigDir", env.env().configDir().toAbsolutePath().toString()); - - if (pkg.isSystemWideInstall()) { - data.put("JpIsSystemWide", "yes"); - } - - return data; - } - - private static List getWxlFilesFromDir(Path dir) { - final String glob = "glob:**/*.wxl"; - final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher( - glob); - - try (var walk = Files.walk(dir, 1)) { - return walk - .filter(Files::isReadable) - .filter(pathMatcher::matches) - .sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString())) - .toList(); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - - private static String getCultureFromWxlFile(Path wxlPath) { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - - Document doc = builder.parse(wxlPath.toFile()); - - XPath xPath = XPathFactory.newInstance().newXPath(); - NodeList nodes = (NodeList) xPath.evaluate( - "//WixLocalization/@Culture", doc, XPathConstants.NODESET); - if (nodes.getLength() != 1) { - throw new IOException(I18N.format( - "error.extract-culture-from-wix-l10n-file", - wxlPath.toAbsolutePath().normalize())); - } - - return nodes.item(0).getNodeValue(); - } catch (XPathExpressionException | ParserConfigurationException - | SAXException ex) { - throw new UncheckedIOException(new IOException( - I18N.format("error.read-wix-l10n-file", wxlPath.toAbsolutePath().normalize()), ex)); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - - private static void ensureByMutationFileIsRTF(Path f) { - try { - boolean existingLicenseIsRTF = false; - - try (InputStream fin = Files.newInputStream(f)) { - byte[] firstBits = new byte[7]; - - if (fin.read(firstBits) == firstBits.length) { - String header = new String(firstBits); - existingLicenseIsRTF = "{\\rtf1\\".equals(header); - } - } - - if (!existingLicenseIsRTF) { - List oldLicense = Files.readAllLines(f); - try (Writer w = Files.newBufferedWriter( - f, Charset.forName("Windows-1252"))) { - w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033" - + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" - + "\\viewkind4\\uc1\\pard\\sa200\\sl276" - + "\\slmult1\\lang9\\fs20 "); - oldLicense.forEach(l -> { - try { - for (char c : l.toCharArray()) { - // 0x00 <= ch < 0x20 Escaped (\'hh) - // 0x20 <= ch < 0x80 Raw(non - escaped) char - // 0x80 <= ch <= 0xFF Escaped(\ 'hh) - // 0x5C, 0x7B, 0x7D (special RTF characters - // \,{,})Escaped(\'hh) - // ch > 0xff Escaped (\\ud###?) - if (c < 0x10) { - w.write("\\'0"); - w.write(Integer.toHexString(c)); - } else if (c > 0xff) { - w.write("\\ud"); - w.write(Integer.toString(c)); - // \\uc1 is in the header and in effect - // so we trail with a replacement char if - // the font lacks that character - '?' - w.write("?"); - } else if ((c < 0x20) || (c >= 0x80) || - (c == 0x5C) || (c == 0x7B) || - (c == 0x7D)) { - w.write("\\'"); - w.write(Integer.toHexString(c)); - } else { - w.write(c); - } - } - // blank lines are interpreted as paragraph breaks - if (l.length() < 1) { - w.write("\\par"); - } else { - w.write(" "); - } - w.write("\r\n"); - } catch (IOException e) { - Log.verbose(e); - } - }); - w.write("}\r\n"); - } - } - } catch (IOException e) { - Log.verbose(e); - } - } - - private Path installerIcon; - private WixToolset wixToolset; - private WixPipeline wixPipeline; - private final List wixFragments; + final Result sysEnv = WinSystemEnvironment.create(); } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java new file mode 100644 index 00000000000..99b6c367fec --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiPackager.java @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import jdk.jpackage.internal.model.AppImageLayout; +import jdk.jpackage.internal.model.PackagerException; +import jdk.jpackage.internal.model.RuntimeLayout; +import jdk.jpackage.internal.model.WinMsiPackage; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * WinMsiPackager + * + * Produces .msi installer from application image. Uses WiX Toolkit to build + * .msi installer. + *

        + * Creates a number of source files with the description + * of installer to be processed by WiX tools. Generated source files are stored + * in "config" subdirectory next to "app" subdirectory in the root work + * directory. The following WiX source files are generated: + *

          + *
        • main.wxs. Main source file with the installer description + *
        • bundle.wxf. Source file with application and Java run-time directory tree + * description. + *
        • ui.wxf. Source file with UI description of the installer. + *
        + * + *

        + * main.wxs file is a copy of main.wxs resource from + * jdk.jpackage.internal.resources package. It is parametrized with the + * following WiX variables: + *

          + *
        • JpAppName. Name of the application. Set to the value of --name command + * line option + *
        • JpAppVersion. Version of the application. Set to the value of + * --app-version command line option + *
        • JpAppVendor. Vendor of the application. Set to the value of --vendor + * command line option + *
        • JpAppDescription. Description of the application. Set to the value of + * --description command line option + *
        • JpProductCode. Set to product code UUID of the application. Random value + * generated by jpackage every time {@link #execute} method is called + *
        • JpProductUpgradeCode. Set to upgrade code UUID of the application. Random + * value generated by jpackage every time {@link #execute} method is called if + * --win-upgrade-uuid command line option is not specified. Otherwise this + * variable is set to the value of --win-upgrade-uuid command line option + *
        • JpAllowUpgrades. Set to "yes", but all that matters is it is defined. + *
        • JpAllowDowngrades. Defined for application installers, and undefined for + * Java runtime installers. + *
        • JpConfigDir. Absolute path to the directory with generated WiX source + * files. + *
        • JpIsSystemWide. Set to "yes" if --win-per-user-install command line + * option was not specified. Undefined otherwise + *
        • JpAppSizeKb. Set to estimated size of the application in kilobytes + *
        • JpHelpURL. Set to value of --win-help-url command line option if it + * was specified. Undefined otherwise + *
        • JpAboutURL. Set to value of --about-url command line option if it + * was specified. Undefined otherwise + *
        • JpUpdateURL. Set to value of --win-update-url command line option if it + * was specified. Undefined otherwise + *
        + * + *

        + * ui.wxf file is generated based on --license-file, --win-shortcut-prompt, + * --win-dir-chooser command line options. It is parametrized with the following + * WiX variables: + *

          + *
        • JpLicenseRtf. Set to the value of --license-file command line option. + * Undefined if --license-file command line option was not specified + *
        + */ +final class WinMsiPackager implements Consumer { + + WinMsiPackager(BuildEnv env, WinMsiPackage pkg, Path outputDir, WixToolset wixToolset) { + this.pkg = Objects.requireNonNull(pkg); + this.env = Objects.requireNonNull(env); + this.outputDir = Objects.requireNonNull(outputDir); + this.wixToolset = Objects.requireNonNull(wixToolset); + + wixFragments = Stream.of( + Map.entry("bundle.wxf", new WixAppImageFragmentBuilder()), + Map.entry("ui.wxf", new WixUiFragmentBuilder()), + Map.entry("os-condition.wxf", OSVersionCondition.createWixFragmentBuilder()) + ).map(e -> { + e.getValue().setOutputFileName(e.getKey()); + return e.getValue(); + }).toList(); + + // Configure installer icon + if (env.appImageLayout() instanceof RuntimeLayout runtimeLayout) { + // Use icon from java launcher. + // Assume java.exe exists in Java Runtime being packed. + // Ignore custom icon if any as we don't want to copy anything in + // Java Runtime image. + installerIcon = runtimeLayout.runtimeDirectory().resolve(Path.of("bin", "java.exe")).toAbsolutePath(); + } else { + installerIcon = env.asApplicationLayout().orElseThrow().launchersDirectory().resolve( + pkg.app().mainLauncher().orElseThrow().executableNameWithSuffix()).toAbsolutePath(); + } + + wixFragments.forEach(wixFragment -> wixFragment.setWixVersion(wixToolset.getVersion(), + wixToolset.getType())); + + wixFragments.stream().map(WixFragmentBuilder::getLoggableWixFeatures).flatMap( + List::stream).distinct().toList().forEach(Log::verbose); + } + + WinMsiPackager(BuildEnv env, WinMsiPackage pkg, Path outputDir, WinSystemEnvironment sysEnv) { + this(env, pkg, outputDir, sysEnv.wixToolset()); + } + + @Override + public void accept(PackagingPipeline.Builder pipelineBuilder) { + pipelineBuilder.excludeDirFromCopying(outputDir) + .task(PackagingPipeline.PackageTaskID.CREATE_CONFIG_FILES) + .action(this::prepareConfigFiles) + .add() + .task(PackagingPipeline.PackageTaskID.CREATE_PACKAGE_FILE) + .action(this::buildPackage) + .add(); + } + + private void prepareConfigFiles() throws PackagerException, IOException { + + pkg.licenseFile().ifPresent(licenseFile -> { + // need to copy license file to the working directory + // and convert to rtf if needed + Path destFile = env.configDir().resolve(licenseFile.getFileName()); + + try { + IOUtils.copyFile(licenseFile, destFile); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + destFile.toFile().setWritable(true); + ensureByMutationFileIsRTF(destFile); + }); + + for (var wixFragment : wixFragments) { + wixFragment.initFromParams(env, pkg); + wixFragment.addFilesToConfigRoot(); + } + + final var msiOut = outputDir.resolve(pkg.packageFileNameWithSuffix()); + + Log.verbose(I18N.format("message.preparing-msi-config", msiOut.toAbsolutePath())); + + final var wixVars = createWixVars(); + + final var wixObjDir = env.buildRoot().resolve("wixobj"); + + final var configDir = env.configDir(); + + final var wixPipelineBuilder = WixPipeline.build() + .setWixObjDir(wixObjDir) + .setWorkDir(env.appImageDir()) + .addSource(configDir.resolve("main.wxs"), wixVars); + + for (var wixFragment : wixFragments) { + wixFragment.configureWixPipeline(wixPipelineBuilder); + } + + switch (wixToolset.getType()) { + case Wix3 -> { + wixPipelineBuilder.addLightOptions("-sice:ICE27"); + + if (!pkg.isSystemWideInstall()) { + wixPipelineBuilder.addLightOptions("-sice:ICE91"); + } + } + case Wix4 -> { + } + default -> { + throw new IllegalArgumentException(); + } + } + + var primaryWxlFiles = Stream.of("de", "en", "ja", "zh_CN").map(loc -> { + return configDir.resolve("MsiInstallerStrings_" + loc + ".wxl"); + }).toList(); + + var wixResources = new WixSourceConverter.ResourceGroup(wixToolset.getType()); + + // Copy standard l10n files. + for (var path : primaryWxlFiles) { + var name = path.getFileName().toString(); + wixResources.addResource(env.createResource(name).setPublicName(name).setCategory( + I18N.getString("resource.wxl-file")), path); + } + + wixResources.addResource(env.createResource("main.wxs").setPublicName("main.wxs"). + setCategory(I18N.getString("resource.main-wix-file")), configDir.resolve("main.wxs")); + + wixResources.addResource(env.createResource("overrides.wxi").setPublicName( + "overrides.wxi").setCategory(I18N.getString("resource.overrides-wix-file")), + configDir.resolve("overrides.wxi")); + + // Filter out custom l10n files that were already used to + // override primary l10n files. Ignore case filename comparison, + // both lists are expected to be short. + List customWxlFiles = env.resourceDir() + .map(WinMsiPackager::getWxlFilesFromDir) + .orElseGet(Collections::emptyList) + .stream() + .filter(custom -> primaryWxlFiles.stream().noneMatch(primary -> + primary.getFileName().toString().equalsIgnoreCase( + custom.getFileName().toString()))) + .peek(custom -> Log.verbose(I18N.format( + "message.using-custom-resource", String.format("[%s]", + I18N.getString("resource.wxl-file")), + custom.getFileName()))).toList(); + + // Copy custom l10n files. + for (var path : customWxlFiles) { + var name = path.getFileName().toString(); + wixResources.addResource(env.createResource(name).setPublicName(name). + setSourceOrder(OverridableResource.Source.ResourceDir).setCategory(I18N. + getString("resource.wxl-file")), configDir.resolve(name)); + } + + // Save all WiX resources into config dir. + wixResources.saveResources(); + + // All l10n files are supplied to WiX with "-loc", but only + // Cultures from custom files and a single primary Culture are + // included into "-cultures" list + for (var wxl : primaryWxlFiles) { + wixPipelineBuilder.addLightOptions("-loc", wxl.toString()); + } + + List cultures = new ArrayList<>(); + for (var wxl : customWxlFiles) { + wxl = configDir.resolve(wxl.getFileName()); + wixPipelineBuilder.addLightOptions("-loc", wxl.toString()); + cultures.add(getCultureFromWxlFile(wxl)); + } + + // Append a primary culture bases on runtime locale. + final Path primaryWxlFile = configDir.resolve( + I18N.getString("resource.wxl-file-name")); + cultures.add(getCultureFromWxlFile(primaryWxlFile)); + + // Build ordered list of unique cultures. + Set uniqueCultures = new LinkedHashSet<>(); + uniqueCultures.addAll(cultures); + switch (wixToolset.getType()) { + case Wix3 -> { + wixPipelineBuilder.addLightOptions(uniqueCultures.stream().collect(Collectors.joining(";", + "-cultures:", ""))); + } + case Wix4 -> { + uniqueCultures.forEach(culture -> { + wixPipelineBuilder.addLightOptions("-culture", culture); + }); + } + default -> { + throw new IllegalArgumentException(); + } + } + + Files.createDirectories(wixObjDir); + wixPipeline = wixPipelineBuilder.create(wixToolset); + } + + private void buildPackage() throws PackagerException, IOException { + final var msiOut = outputDir.resolve(pkg.packageFileNameWithSuffix()); + Log.verbose(I18N.format("message.generating-msi", msiOut.toAbsolutePath())); + wixPipeline.buildMsi(msiOut.toAbsolutePath()); + } + + private Map createWixVars() throws IOException { + Map data = new HashMap<>(); + + data.put("JpProductCode", pkg.productCode().toString()); + data.put("JpProductUpgradeCode", pkg.upgradeCode().toString()); + + Log.verbose(I18N.format("message.product-code", pkg.productCode())); + Log.verbose(I18N.format("message.upgrade-code", pkg.upgradeCode())); + + data.put("JpAllowUpgrades", "yes"); + if (!pkg.isRuntimeInstaller()) { + data.put("JpAllowDowngrades", "yes"); + } + + data.put("JpAppName", pkg.packageName()); + data.put("JpAppDescription", pkg.description()); + data.put("JpAppVendor", pkg.app().vendor()); + data.put("JpAppVersion", pkg.version()); + if (Files.exists(installerIcon)) { + data.put("JpIcon", installerIcon.toString()); + } + + pkg.helpURL().ifPresent(value -> { + data.put("JpHelpURL", value); + }); + + pkg.updateURL().ifPresent(value -> { + data.put("JpUpdateURL", value); + }); + + pkg.aboutURL().ifPresent(value -> { + data.put("JpAboutURL", value); + }); + + data.put("JpAppSizeKb", Long.toString(AppImageLayout.toPathGroup( + env.appImageLayout()).sizeInBytes() >> 10)); + + data.put("JpConfigDir", env.configDir().toAbsolutePath().toString()); + + if (pkg.isSystemWideInstall()) { + data.put("JpIsSystemWide", "yes"); + } + + return data; + } + + private static List getWxlFilesFromDir(Path dir) { + final String glob = "glob:**/*.wxl"; + final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher( + glob); + + try (var walk = Files.walk(dir, 1)) { + return walk + .filter(Files::isReadable) + .filter(pathMatcher::matches) + .sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString())) + .toList(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static String getCultureFromWxlFile(Path wxlPath) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + Document doc = builder.parse(wxlPath.toFile()); + + XPath xPath = XPathFactory.newInstance().newXPath(); + NodeList nodes = (NodeList) xPath.evaluate( + "//WixLocalization/@Culture", doc, XPathConstants.NODESET); + if (nodes.getLength() != 1) { + throw new RuntimeException(I18N.format( + "error.extract-culture-from-wix-l10n-file", + wxlPath.toAbsolutePath().normalize())); + } + + return nodes.item(0).getNodeValue(); + } catch (XPathExpressionException | ParserConfigurationException | SAXException ex) { + throw new RuntimeException(I18N.format( + "error.read-wix-l10n-file", wxlPath.toAbsolutePath().normalize()), ex); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static void ensureByMutationFileIsRTF(Path f) { + try { + boolean existingLicenseIsRTF = false; + + try (InputStream fin = Files.newInputStream(f)) { + byte[] firstBits = new byte[7]; + + if (fin.read(firstBits) == firstBits.length) { + String header = new String(firstBits); + existingLicenseIsRTF = "{\\rtf1\\".equals(header); + } + } + + if (!existingLicenseIsRTF) { + List oldLicense = Files.readAllLines(f); + try (Writer w = Files.newBufferedWriter( + f, Charset.forName("Windows-1252"))) { + w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033" + + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n" + + "\\viewkind4\\uc1\\pard\\sa200\\sl276" + + "\\slmult1\\lang9\\fs20 "); + oldLicense.forEach(l -> { + try { + for (char c : l.toCharArray()) { + // 0x00 <= ch < 0x20 Escaped (\'hh) + // 0x20 <= ch < 0x80 Raw(non - escaped) char + // 0x80 <= ch <= 0xFF Escaped(\ 'hh) + // 0x5C, 0x7B, 0x7D (special RTF characters + // \,{,})Escaped(\'hh) + // ch > 0xff Escaped (\\ud###?) + if (c < 0x10) { + w.write("\\'0"); + w.write(Integer.toHexString(c)); + } else if (c > 0xff) { + w.write("\\ud"); + w.write(Integer.toString(c)); + // \\uc1 is in the header and in effect + // so we trail with a replacement char if + // the font lacks that character - '?' + w.write("?"); + } else if ((c < 0x20) || (c >= 0x80) || + (c == 0x5C) || (c == 0x7B) || + (c == 0x7D)) { + w.write("\\'"); + w.write(Integer.toHexString(c)); + } else { + w.write(c); + } + } + // blank lines are interpreted as paragraph breaks + if (l.length() < 1) { + w.write("\\par"); + } else { + w.write(" "); + } + w.write("\r\n"); + } catch (IOException e) { + Log.verbose(e); + } + }); + w.write("}\r\n"); + } + } + } catch (IOException e) { + Log.verbose(e); + } + } + + private final WinMsiPackage pkg; + private final BuildEnv env; + private final Path outputDir; + private final WixToolset wixToolset; + private final List wixFragments; + private final Path installerIcon; + private WixPipeline wixPipeline; +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinSystemEnvironment.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinSystemEnvironment.java new file mode 100644 index 00000000000..33b5a09a31b --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinSystemEnvironment.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + +import java.util.Objects; +import jdk.jpackage.internal.util.Result; + +record WinSystemEnvironment(WixToolset wixToolset) implements SystemEnvironment { + + WinSystemEnvironment { + Objects.requireNonNull(wixToolset); + } + + static Result create() { + return Result.create(toSupplier(WixTool::createToolset)).map(WinSystemEnvironment::new); + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java index 7e4353876b4..6e72511e080 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java @@ -183,13 +183,12 @@ public enum WixTool { final boolean[] tooOld = new boolean[1]; final String[] parsedVersion = new String[1]; - final var validator = new ToolValidator(toolPath).setMinimalVersion(tool.minimalVersion). - setToolNotFoundErrorHandler((name, ex) -> { - return new ConfigException("", ""); - }).setToolOldVersionErrorHandler((name, version) -> { - tooOld[0] = true; - return null; - }); + final var validator = new ToolValidator(toolPath) + .setMinimalVersion(tool.minimalVersion) + .setToolOldVersionErrorHandler((name, version) -> { + tooOld[0] = true; + return null; + }); final Function, String> versionParser; diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/ToolValidatorTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/ToolValidatorTest.java index c0acea5e3e7..b3660383e47 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/ToolValidatorTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/ToolValidatorTest.java @@ -23,33 +23,141 @@ package jdk.jpackage.internal; -import jdk.jpackage.internal.model.DottedVersion; -import jdk.jpackage.internal.model.ConfigException; -import java.nio.file.Path; -import jdk.internal.util.OperatingSystem; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.model.ConfigException; +import jdk.jpackage.internal.model.DottedVersion; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; public class ToolValidatorTest { - @Test - public void testAvailable() { - assertNull(new ToolValidator(TOOL_JAVA).validate()); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testAvailable(boolean checkExistsOnly) { + assertNull(new ToolValidator(TOOL_JAVA).checkExistsOnly(checkExistsOnly).validate()); } @Test - public void testNotAvailable() { - assertValidationFailure(new ToolValidator(TOOL_UNKNOWN).validate(), true); + public void testAvailable_setCommandLine() { + // java doesn't recognize "--foo" command line option, but the validation will + // still pass as there is no minimal version specified and the validator ignores + // the exit code + assertNull(new ToolValidator(TOOL_JAVA).setCommandLine("--foo").validate()); + } + + enum TestAvailableMode { + NO_VERSION(null), + TOO_OLD("0.9"), + EQUALS("1.0"), + NEWER("1.1"); + + TestAvailableMode(String parsedVersion) { + this.parsedVersion = parsedVersion; + } + + final String parsedVersion; + } + + @ParameterizedTest + @EnumSource(TestAvailableMode.class) + public void testAvailable(TestAvailableMode mode) { + var minVer = TestAvailableMode.EQUALS.parsedVersion; + var err = new ToolValidator(TOOL_JAVA).setVersionParser(lines -> { + return mode.parsedVersion; + }).setMinimalVersion(DottedVersion.greedy(minVer)).validate(); + + if (Set.of(TestAvailableMode.NO_VERSION, TestAvailableMode.TOO_OLD).contains(mode)) { + var expectedMessage = I18N.format("error.tool-old-version", TOOL_JAVA, minVer); + var expectedAdvice = I18N.format("error.tool-old-version.advice", TOOL_JAVA, minVer); + + assertEquals(expectedMessage, err.getMessage()); + assertEquals(expectedAdvice, err.getAdvice()); + } else { + assertNull(err); + } + } + + @ParameterizedTest + @EnumSource(TestAvailableMode.class) + public void testAvailable_setToolOldVersionErrorHandler(TestAvailableMode mode) { + var handler = new ToolOldVersionErrorHandler(); + var minVer = TestAvailableMode.EQUALS.parsedVersion; + var err = new ToolValidator(TOOL_JAVA).setVersionParser(lines -> { + return mode.parsedVersion; + }).setMinimalVersion(DottedVersion.greedy(minVer)).setToolOldVersionErrorHandler(handler).validate(); + + if (Set.of(TestAvailableMode.NO_VERSION, TestAvailableMode.TOO_OLD).contains(mode)) { + assertSame(ToolOldVersionErrorHandler.ERR, err); + handler.verifyCalled(Path.of(TOOL_JAVA), mode.parsedVersion); + } else { + assertNull(err); + handler.verifyNotCalled(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testNotAvailable(boolean checkExistsOnly, @TempDir Path dir) { + var err = new ToolValidator(dir.resolve("foo")).checkExistsOnly(checkExistsOnly).validate(); + if (checkExistsOnly) { + assertValidationFailure(err, false); + } else { + assertValidationFailureNoAdvice(err, !checkExistsOnly); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testToolIsDirectory(boolean checkExistsOnly, @TempDir Path dir) { + var err = new ToolValidator(dir).checkExistsOnly(checkExistsOnly).validate(); + assertValidationFailureNoAdvice(err, !checkExistsOnly); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testNotAvailable_setToolNotFoundErrorHandler(boolean checkExistsOnly, @TempDir Path dir) { + var handler = new ToolNotFoundErrorHandler(); + var err = new ToolValidator(dir.resolve("foo")).checkExistsOnly(checkExistsOnly) + .setToolNotFoundErrorHandler(handler) + .validate(); + if (checkExistsOnly) { + handler.verifyCalled(dir.resolve("foo")); + assertSame(ToolNotFoundErrorHandler.ERR, err); + } else { + handler.verifyNotCalled(); + assertValidationFailureNoAdvice(err, !checkExistsOnly); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testToolIsDirectory_setToolNotFoundErrorHandler(boolean checkExistsOnly, @TempDir Path dir) { + var handler = new ToolNotFoundErrorHandler(); + var err = new ToolValidator(dir).checkExistsOnly(checkExistsOnly).validate(); + handler.verifyNotCalled(); + assertValidationFailureNoAdvice(err, !checkExistsOnly); } @Test public void testVersionParserUsage() { // Without minimal version configured, version parser should not be used new ToolValidator(TOOL_JAVA).setVersionParser(unused -> { - throw new RuntimeException(); + throw new AssertionError(); }).validate(); // Minimal version is 1, actual is 10. Should be OK. @@ -81,9 +189,68 @@ public class ToolValidatorTest { } } + private static void assertValidationFailureNoAdvice(ConfigException v, boolean withCause) { + assertNotNull(v); + assertNotEquals("", v.getMessage().strip()); + assertNull(v.getAdvice()); + if (withCause) { + assertNotNull(v.getCause()); + } else { + assertNull(v.getCause()); + } + } + + + private static final class ToolNotFoundErrorHandler implements Function { + + @Override + public ConfigException apply(Path tool) { + assertNotNull(tool); + this.tool = tool; + return ERR; + } + + void verifyCalled(Path expectedTool) { + assertEquals(Objects.requireNonNull(expectedTool), tool); + } + + void verifyNotCalled() { + assertNull(tool); + } + + private Path tool; + + static final ConfigException ERR = new ConfigException("no tool", "install the tool"); + } + + + private static final class ToolOldVersionErrorHandler implements BiFunction { + + @Override + public ConfigException apply(Path tool, String parsedVersion) { + assertNotNull(tool); + this.tool = tool; + this.parsedVersion = parsedVersion; + return ERR; + } + + void verifyCalled(Path expectedTool, String expectedParsedVersion) { + assertEquals(Objects.requireNonNull(expectedTool), tool); + assertEquals(expectedParsedVersion, parsedVersion); + } + + void verifyNotCalled() { + assertNull(tool); + } + + private Path tool; + private String parsedVersion; + + static final ConfigException ERR = new ConfigException("tool too old", "install the newer version"); + } + + private static final String TOOL_JAVA; - private static final String TOOL_UNKNOWN = Path.of(System.getProperty( - "java.home"), "bin").toString(); static { String fname = "java"; From a6638121211afd688a9e25b5cbadf2f1441b1e65 Mon Sep 17 00:00:00 2001 From: Thomas Stuefe Date: Fri, 26 Sep 2025 06:14:28 +0000 Subject: [PATCH 250/556] 8368124: Show useful thread names in ASAN reports Reviewed-by: dholmes, mbaesken --- src/hotspot/os/linux/os_linux.cpp | 29 ++++++++++++-------- src/hotspot/os/linux/os_linux.hpp | 1 - src/hotspot/share/utilities/stringUtils.cpp | 1 + src/hotspot/share/utilities/stringUtils.hpp | 2 +- test/hotspot/gtest/runtime/test_os_linux.cpp | 24 ++++++++++++++++ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/hotspot/os/linux/os_linux.cpp b/src/hotspot/os/linux/os_linux.cpp index 9ec892d4305..04f361fe53b 100644 --- a/src/hotspot/os/linux/os_linux.cpp +++ b/src/hotspot/os/linux/os_linux.cpp @@ -160,7 +160,6 @@ address os::Linux::_initial_thread_stack_bottom = nullptr; uintptr_t os::Linux::_initial_thread_stack_size = 0; int (*os::Linux::_pthread_getcpuclockid)(pthread_t, clockid_t *) = nullptr; -int (*os::Linux::_pthread_setname_np)(pthread_t, const char*) = nullptr; pthread_t os::Linux::_main_thread; bool os::Linux::_supports_fast_thread_cpu_time = false; const char * os::Linux::_libc_version = nullptr; @@ -4371,10 +4370,6 @@ void os::init(void) { // _main_thread points to the thread that created/loaded the JVM. Linux::_main_thread = pthread_self(); - // retrieve entry point for pthread_setname_np - Linux::_pthread_setname_np = - (int(*)(pthread_t, const char*))dlsym(RTLD_DEFAULT, "pthread_setname_np"); - check_pax(); // Check the availability of MADV_POPULATE_WRITE. @@ -4851,14 +4846,24 @@ uint os::processor_id() { } void os::set_native_thread_name(const char *name) { - if (Linux::_pthread_setname_np) { - char buf [16]; // according to glibc manpage, 16 chars incl. '/0' - (void) os::snprintf(buf, sizeof(buf), "%s", name); - buf[sizeof(buf) - 1] = '\0'; - const int rc = Linux::_pthread_setname_np(pthread_self(), buf); - // ERANGE should not happen; all other errors should just be ignored. - assert(rc != ERANGE, "pthread_setname_np failed"); + char buf[16]; // according to glibc manpage, 16 chars incl. '/0' + // We may need to truncate the thread name. Since a common pattern + // for thread names is to be both longer than 15 chars and have a + // trailing number ("DispatcherWorkerThread21", "C2 CompilerThread#54" etc), + // we preserve the end of the thread name by truncating the middle + // (e.g. "Dispatc..read21"). + const size_t len = strlen(name); + if (len < sizeof(buf)) { + strcpy(buf, name); + } else { + (void) os::snprintf(buf, sizeof(buf), "%.7s..%.6s", name, name + len - 6); } + // Note: we use the system call here instead of calling pthread_setname_np + // since this is the only way to make ASAN aware of our thread names. Even + // though ASAN intercepts both prctl and pthread_setname_np, it only processes + // the thread name given to the former. + int rc = prctl(PR_SET_NAME, buf); + assert(rc == 0, "prctl(PR_SET_NAME) failed"); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/hotspot/os/linux/os_linux.hpp b/src/hotspot/os/linux/os_linux.hpp index 497d383200d..8f40e51dfb0 100644 --- a/src/hotspot/os/linux/os_linux.hpp +++ b/src/hotspot/os/linux/os_linux.hpp @@ -33,7 +33,6 @@ class os::Linux { friend class os; static int (*_pthread_getcpuclockid)(pthread_t, clockid_t *); - static int (*_pthread_setname_np)(pthread_t, const char*); static address _initial_thread_stack_bottom; static uintptr_t _initial_thread_stack_size; diff --git a/src/hotspot/share/utilities/stringUtils.cpp b/src/hotspot/share/utilities/stringUtils.cpp index cd4c2d6c33b..0872ce43d4b 100644 --- a/src/hotspot/share/utilities/stringUtils.cpp +++ b/src/hotspot/share/utilities/stringUtils.cpp @@ -24,6 +24,7 @@ #include "jvm_io.h" #include "memory/allocation.hpp" +#include "runtime/os.hpp" #include "utilities/debug.hpp" #include "utilities/stringUtils.hpp" diff --git a/src/hotspot/share/utilities/stringUtils.hpp b/src/hotspot/share/utilities/stringUtils.hpp index f30c9c3ea78..c3d21233808 100644 --- a/src/hotspot/share/utilities/stringUtils.hpp +++ b/src/hotspot/share/utilities/stringUtils.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it diff --git a/test/hotspot/gtest/runtime/test_os_linux.cpp b/test/hotspot/gtest/runtime/test_os_linux.cpp index 21133788a36..c8467784b0a 100644 --- a/test/hotspot/gtest/runtime/test_os_linux.cpp +++ b/test/hotspot/gtest/runtime/test_os_linux.cpp @@ -35,6 +35,7 @@ #include "unittest.hpp" #include +#include static bool using_explicit_hugepages() { return UseLargePages && !UseTransparentHugePages; } @@ -468,4 +469,27 @@ TEST_VM(os_linux, glibc_mallinfo_wrapper) { #endif // ADDRESS_SANITIZER #endif // __GLIBC__ +static void test_set_thread_name(const char* name, const char* expected) { + os::set_native_thread_name(name); + char buf[16]; + int rc = prctl(PR_GET_NAME, buf); + ASSERT_EQ(0, rc); + ASSERT_STREQ(buf, expected); +} + +TEST_VM(os_linux, set_thread_name) { + char buf[16]; + // retrieve current name + int rc = prctl(PR_GET_NAME, buf); + ASSERT_EQ(0, rc); + + test_set_thread_name("shortname", "shortname"); + test_set_thread_name("012345678901234", "012345678901234"); + test_set_thread_name("0123456789012345", "0123456..012345"); + test_set_thread_name("MyAllocationWorkerThread22", "MyAlloc..read22"); + + // restore current name + test_set_thread_name(buf, buf); +} + #endif // LINUX From 873078028b7cac1df94cd5a09e403c8537f14ba9 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Fri, 26 Sep 2025 07:27:51 +0000 Subject: [PATCH 251/556] 8368006: Parallel: Skip full regions in dense prefix during Full GC Reviewed-by: gli, fandreuzzi --- .../share/gc/parallel/psParallelCompact.cpp | 125 ++++++++++-------- .../share/gc/parallel/psParallelCompact.hpp | 2 +- 2 files changed, 71 insertions(+), 56 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.cpp b/src/hotspot/share/gc/parallel/psParallelCompact.cpp index f4383e573af..484533619a4 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.cpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.cpp @@ -1660,21 +1660,19 @@ static void compaction_with_stealing_work(TaskTerminator* terminator, uint worke } class FillDensePrefixAndCompactionTask: public WorkerTask { - uint _num_workers; TaskTerminator _terminator; public: FillDensePrefixAndCompactionTask(uint active_workers) : WorkerTask("FillDensePrefixAndCompactionTask"), - _num_workers(active_workers), _terminator(active_workers, ParCompactionManager::region_task_queues()) { } virtual void work(uint worker_id) { - { + if (worker_id == 0) { auto start = Ticks::now(); - PSParallelCompact::fill_dead_objs_in_dense_prefix(worker_id, _num_workers); - log_trace(gc, phases)("Fill dense prefix by worker %u: %.3f ms", worker_id, (Ticks::now() - start).seconds() * 1000); + PSParallelCompact::fill_dead_objs_in_dense_prefix(); + log_trace(gc, phases)("Fill dense prefix by worker 0: %.3f ms", (Ticks::now() - start).seconds() * 1000); } compaction_with_stealing_work(&_terminator, worker_id); } @@ -1687,9 +1685,10 @@ void PSParallelCompact::fill_range_in_dense_prefix(HeapWord* start, HeapWord* en assert(mark_bitmap()->find_obj_beg(start, end) == end, "precondition"); HeapWord* bottom = _space_info[old_space_id].space()->bottom(); if (start != bottom) { + // The preceding live obj. HeapWord* obj_start = mark_bitmap()->find_obj_beg_reverse(bottom, start); - HeapWord* after_obj = obj_start + cast_to_oop(obj_start)->size(); - assert(after_obj == start, "precondition"); + HeapWord* obj_end = obj_start + cast_to_oop(obj_start)->size(); + assert(obj_end == start, "precondition"); } } #endif @@ -1703,58 +1702,51 @@ void PSParallelCompact::fill_range_in_dense_prefix(HeapWord* start, HeapWord* en } while (addr < end); } -void PSParallelCompact::fill_dead_objs_in_dense_prefix(uint worker_id, uint num_workers) { +void PSParallelCompact::fill_dead_objs_in_dense_prefix() { ParMarkBitMap* bitmap = mark_bitmap(); HeapWord* const bottom = _space_info[old_space_id].space()->bottom(); HeapWord* const prefix_end = dense_prefix(old_space_id); - if (bottom == prefix_end) { - return; - } + const size_t region_size = ParallelCompactData::RegionSize; - size_t bottom_region = _summary_data.addr_to_region_idx(bottom); - size_t prefix_end_region = _summary_data.addr_to_region_idx(prefix_end); + // Fill dead space in [start_addr, end_addr) + HeapWord* const start_addr = bottom; + HeapWord* const end_addr = prefix_end; - size_t start_region; - size_t end_region; - split_regions_for_worker(bottom_region, prefix_end_region, - worker_id, num_workers, - &start_region, &end_region); + for (HeapWord* cur_addr = start_addr; cur_addr < end_addr; /* empty */) { + RegionData* cur_region_ptr = _summary_data.addr_to_region_ptr(cur_addr); + if (cur_region_ptr->data_size() == region_size) { + // Full; no dead space. Next region. + if (_summary_data.is_region_aligned(cur_addr)) { + cur_addr += region_size; + } else { + cur_addr = _summary_data.region_align_up(cur_addr); + } + continue; + } - if (start_region == end_region) { - return; - } + // Fill dead space inside cur_region. + if (_summary_data.is_region_aligned(cur_addr)) { + cur_addr += cur_region_ptr->partial_obj_size(); + } - HeapWord* const start_addr = _summary_data.region_to_addr(start_region); - HeapWord* const end_addr = _summary_data.region_to_addr(end_region); - - // Skip live partial obj (if any) from previous region. - HeapWord* cur_addr; - RegionData* start_region_ptr = _summary_data.region(start_region); - if (start_region_ptr->partial_obj_size() != 0) { - HeapWord* partial_obj_start = start_region_ptr->partial_obj_addr(); - assert(bitmap->is_marked(partial_obj_start), "inv"); - cur_addr = partial_obj_start + cast_to_oop(partial_obj_start)->size(); - } else { - cur_addr = start_addr; - } - - // end_addr is inclusive to handle regions starting with dead space. - while (cur_addr <= end_addr) { - // Use prefix_end to handle trailing obj in each worker region-chunk. - HeapWord* live_start = bitmap->find_obj_beg(cur_addr, prefix_end); - if (cur_addr != live_start) { - // Only worker 0 handles proceeding dead space. - if (cur_addr != start_addr || worker_id == 0) { + HeapWord* region_end_addr = _summary_data.region_align_up(cur_addr + 1); + assert(region_end_addr <= end_addr, "inv"); + while (cur_addr < region_end_addr) { + // Use end_addr to allow filler-obj to cross region boundary. + HeapWord* live_start = bitmap->find_obj_beg(cur_addr, end_addr); + if (cur_addr != live_start) { + // Found dead space [cur_addr, live_start). fill_range_in_dense_prefix(cur_addr, live_start); } + if (live_start >= region_end_addr) { + cur_addr = live_start; + break; + } + assert(bitmap->is_marked(live_start), "inv"); + cur_addr = live_start + cast_to_oop(live_start)->size(); } - if (live_start >= end_addr) { - break; - } - assert(bitmap->is_marked(live_start), "inv"); - cur_addr = live_start + cast_to_oop(live_start)->size(); } } @@ -1787,15 +1779,38 @@ void PSParallelCompact::compact() { void PSParallelCompact::verify_filler_in_dense_prefix() { HeapWord* bottom = _space_info[old_space_id].space()->bottom(); HeapWord* dense_prefix_end = dense_prefix(old_space_id); - HeapWord* cur_addr = bottom; - while (cur_addr < dense_prefix_end) { - oop obj = cast_to_oop(cur_addr); - oopDesc::verify(obj); - if (!mark_bitmap()->is_marked(cur_addr)) { - Klass* k = cast_to_oop(cur_addr)->klass(); - assert(k == Universe::fillerArrayKlass() || k == vmClasses::FillerObject_klass(), "inv"); + + const size_t region_size = ParallelCompactData::RegionSize; + + for (HeapWord* cur_addr = bottom; cur_addr < dense_prefix_end; /* empty */) { + RegionData* cur_region_ptr = _summary_data.addr_to_region_ptr(cur_addr); + if (cur_region_ptr->data_size() == region_size) { + // Full; no dead space. Next region. + if (_summary_data.is_region_aligned(cur_addr)) { + cur_addr += region_size; + } else { + cur_addr = _summary_data.region_align_up(cur_addr); + } + continue; + } + + // This region contains filler objs. + if (_summary_data.is_region_aligned(cur_addr)) { + cur_addr += cur_region_ptr->partial_obj_size(); + } + + HeapWord* region_end_addr = _summary_data.region_align_up(cur_addr + 1); + assert(region_end_addr <= dense_prefix_end, "inv"); + + while (cur_addr < region_end_addr) { + oop obj = cast_to_oop(cur_addr); + oopDesc::verify(obj); + if (!mark_bitmap()->is_marked(cur_addr)) { + Klass* k = cast_to_oop(cur_addr)->klass(); + assert(k == Universe::fillerArrayKlass() || k == vmClasses::FillerObject_klass(), "inv"); + } + cur_addr += obj->size(); } - cur_addr += obj->size(); } } diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.hpp b/src/hotspot/share/gc/parallel/psParallelCompact.hpp index 4d212499b4c..2297d720b35 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.hpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.hpp @@ -755,7 +755,7 @@ private: static void fill_range_in_dense_prefix(HeapWord* start, HeapWord* end); public: - static void fill_dead_objs_in_dense_prefix(uint worker_id, uint num_workers); + static void fill_dead_objs_in_dense_prefix(); // This method invokes a full collection. // clear_all_soft_refs controls whether soft-refs should be cleared or not. From b90799c0e92468b341235989f731bb93e2741a77 Mon Sep 17 00:00:00 2001 From: Arno Zeller Date: Fri, 26 Sep 2025 07:47:26 +0000 Subject: [PATCH 252/556] 8368616: runtime/cds/appcds/aotCache/JavaAgent.java#dynamic fails on non CDS platforms/builds after JDK-8362561 Reviewed-by: mbaesken, shade, iklam --- test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java index 491a5bf1fa7..758d252f18d 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/JavaAgent.java @@ -38,6 +38,7 @@ * @test id=dynamic * @bug 8362561 * @summary -javaagent is not allowed when creating dynamic CDS archive + * @requires vm.cds.supports.aot.class.linking * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds/test-classes * @build JavaAgent JavaAgentTransformer Util * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar JavaAgentApp JavaAgentApp$ShouldBeTransformed From a80ba6260effdca7a7703c6903f273401b861793 Mon Sep 17 00:00:00 2001 From: Matthias Baesken Date: Fri, 26 Sep 2025 07:56:40 +0000 Subject: [PATCH 253/556] 8357691: File blocked.certs contains bad content when boot jdk 25 is used, sun/security/lib/CheckBlockedCerts.java failing Reviewed-by: erikj, iklam --- make/ToolsJdk.gmk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make/ToolsJdk.gmk b/make/ToolsJdk.gmk index 629cadbf83a..ae0dd069c80 100644 --- a/make/ToolsJdk.gmk +++ b/make/ToolsJdk.gmk @@ -63,7 +63,7 @@ TOOL_GENERATECURRENCYDATA = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_ TOOL_TZDB = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ build.tools.tzdb.TzdbZoneRulesCompiler -TOOL_BLOCKED_CERTS = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ +TOOL_BLOCKED_CERTS = $(JAVA_SMALL) -Xlog:disable -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ --add-exports java.base/sun.security.util=ALL-UNNAMED \ build.tools.blockedcertsconverter.BlockedCertsConverter From 7bfdb0120752d01da96c19e8037a6e909847d63c Mon Sep 17 00:00:00 2001 From: Matthias Baesken Date: Fri, 26 Sep 2025 09:00:59 +0000 Subject: [PATCH 254/556] 8368565: Adjust comment regarding dependency of libjvm.so to librt Reviewed-by: dholmes --- make/autoconf/libraries.m4 | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/make/autoconf/libraries.m4 b/make/autoconf/libraries.m4 index bf697928f1b..8dc3d55ed0c 100644 --- a/make/autoconf/libraries.m4 +++ b/make/autoconf/libraries.m4 @@ -136,12 +136,8 @@ AC_DEFUN_ONCE([LIB_SETUP_LIBRARIES], BASIC_JVM_LIBS="$BASIC_JVM_LIBS $LIBPTHREAD" fi - # librt for legacy clock_gettime + # librt - for timers (timer_* functions) if test "x$OPENJDK_TARGET_OS" = xlinux; then - # Hotspot needs to link librt to get the clock_* functions. - # But once our supported minimum build and runtime platform - # has glibc 2.17, this can be removed as the functions are - # in libc. BASIC_JVM_LIBS="$BASIC_JVM_LIBS -lrt" fi From f0e1078c7175b3f930502a6079feff86aa53b669 Mon Sep 17 00:00:00 2001 From: Joachim Kern Date: Fri, 26 Sep 2025 12:14:58 +0000 Subject: [PATCH 255/556] 8368250: [AIX] now ubsan vptr check is also possible (follow up of JDK-8354686) Reviewed-by: erikj, stuefe, mbaesken --- make/autoconf/flags-ldflags.m4 | 2 +- make/autoconf/jdk-options.m4 | 9 +++++++-- src/hotspot/share/oops/compressedKlass.cpp | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/make/autoconf/flags-ldflags.m4 b/make/autoconf/flags-ldflags.m4 index 651be3a1913..66f8904db89 100644 --- a/make/autoconf/flags-ldflags.m4 +++ b/make/autoconf/flags-ldflags.m4 @@ -79,7 +79,7 @@ AC_DEFUN([FLAGS_SETUP_LDFLAGS_HELPER], fi if test "x$OPENJDK_TARGET_OS" = xaix; then BASIC_LDFLAGS="-Wl,-b64 -Wl,-brtl -Wl,-bnorwexec -Wl,-blibpath:/usr/lib:lib -Wl,-bnoexpall \ - -Wl,-bernotok -Wl,-bdatapsize:64k -Wl,-btextpsize:64k -Wl,-bstackpsize:64k" + -Wl,-bernotok -Wl,-bcdtors:mbr::s -Wl,-bdatapsize:64k -Wl,-btextpsize:64k -Wl,-bstackpsize:64k" BASIC_LDFLAGS_JVM_ONLY="$BASIC_LDFLAGS_JVM_ONLY -Wl,-lC_r -Wl,-bbigtoc" fi diff --git a/make/autoconf/jdk-options.m4 b/make/autoconf/jdk-options.m4 index d4299078abf..bb188778001 100644 --- a/make/autoconf/jdk-options.m4 +++ b/make/autoconf/jdk-options.m4 @@ -565,9 +565,14 @@ AC_DEFUN_ONCE([JDKOPT_SETUP_UNDEFINED_BEHAVIOR_SANITIZER], # with an additional define LLVM_SYMBOLIZER, which we set here. # To calculate the correct llvm_symbolizer path we can use the location of the compiler, because # their relation is fixed. + # In the ubsan case we have to link every binary with the C++-compiler as linker, because inherently + # the C-Compiler and the C++-compiler used as linker provide a different set of ubsan exports. + # Linking an executable with the C-compiler and one of its shared libraries with the C++-compiler + # leeds to unresolved symbols. if test "x$TOOLCHAIN_TYPE" = "xclang" && test "x$OPENJDK_TARGET_OS" = "xaix"; then - UBSAN_CFLAGS="$UBSAN_CFLAGS -fno-sanitize=function,vptr -DLLVM_SYMBOLIZER=$(dirname $(dirname $CC))/tools/ibm-llvm-symbolizer" - UBSAN_LDFLAGS="$UBSAN_LDFLAGS -fno-sanitize=function,vptr -Wl,-bbigtoc" + UBSAN_CFLAGS="$UBSAN_CFLAGS -DLLVM_SYMBOLIZER=$(dirname $(dirname $CC))/tools/ibm-llvm-symbolizer" + UBSAN_LDFLAGS="$UBSAN_LDFLAGS -Wl,-bbigtoc" + LD="$LDCXX" fi UTIL_ARG_ENABLE(NAME: ubsan, DEFAULT: false, RESULT: UBSAN_ENABLED, DESC: [enable UndefinedBehaviorSanitizer], diff --git a/src/hotspot/share/oops/compressedKlass.cpp b/src/hotspot/share/oops/compressedKlass.cpp index d7c97d3c8d5..b32d10c74d2 100644 --- a/src/hotspot/share/oops/compressedKlass.cpp +++ b/src/hotspot/share/oops/compressedKlass.cpp @@ -110,7 +110,7 @@ void CompressedKlassPointers::sanity_check_after_initialization() { // Check that Klass range is fully engulfed in the encoding range const address encoding_start = _base; const address encoding_end = (address) - LP64_ONLY(p2u(_base) + (uintptr_t)nth_bit(narrow_klass_pointer_bits() + _shift)) + LP64_ONLY((p2u(_base) + (uintptr_t)nth_bit(narrow_klass_pointer_bits() + _shift))) NOT_LP64(max_klass_range_size()); ASSERT_HERE_2(_klass_range_start >= _base && _klass_range_end <= encoding_end, "Resulting encoding range does not fully cover the class range"); From bdb7d25ac11ca60a357b371c75544b346e523940 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Fri, 26 Sep 2025 13:59:39 +0000 Subject: [PATCH 256/556] 8358723: jpackage signing issues: the main launcher doesn't have entitlements Reviewed-by: almatvee --- .../jdk/jpackage/internal/AppImageSigner.java | 2 +- .../jpackage/internal/util/PListReader.java | 273 ++++++++++++++++- test/jdk/tools/jpackage/TEST.properties | 1 + .../jdk/jpackage/test/JPackageCommand.java | 4 +- .../test/LauncherAsServiceVerifier.java | 2 +- .../jdk/jpackage/test/LauncherVerifier.java | 44 ++- .../helpers/jdk/jpackage/test/MacHelper.java | 3 +- .../jdk/jpackage/test/MacSignVerify.java | 11 + .../internal/util/PListReaderTest.java | 290 ++++++++++++++++-- .../macosx/MacFileAssociationsTest.java | 35 ++- 10 files changed, 602 insertions(+), 63 deletions(-) diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java index 7563dfa0ed3..571f163f682 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java @@ -237,7 +237,7 @@ final class AppImageSigner { final var codesignExecutableFile = Codesign.build(signingCfg::toCodesignArgs).quiet(true).create().asConsumer(); final var codesignFile = Codesign.build(signingCfgWithoutEntitlements::toCodesignArgs).quiet(true).create().asConsumer(); - final var codesignDir = Codesign.build(signingCfgWithoutEntitlements::toCodesignArgs).force(true).create().asConsumer(); + final var codesignDir = Codesign.build(signingCfg::toCodesignArgs).force(true).create().asConsumer(); return new Codesigners(codesignFile, codesignExecutableFile, codesignDir); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java index 211a55897d0..6d7532bbd74 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java @@ -24,21 +24,141 @@ */ package jdk.jpackage.internal.util; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; -import jdk.jpackage.internal.util.function.ThrowingSupplier; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.SAXException; +/** + * Property list (plist) file reader. + */ public final class PListReader { + public record Raw(String value, Type type) { + + public enum Type { + STRING, + BOOLEAN, + REAL, + INTEGER, + DATE, + DATA; + + private static Optional fromElementName(String name) { + switch (name) { + case "string" -> { + return Optional.of(STRING); + } + case "true" -> { + return Optional.of(BOOLEAN); + } + case "false" -> { + return Optional.of(BOOLEAN); + } + case "real" -> { + return Optional.of(REAL); + } + case "integer" -> { + return Optional.of(INTEGER); + } + case "date" -> { + return Optional.of(DATE); + } + case "data" -> { + return Optional.of(DATA); + } + default -> { + return Optional.empty(); + } + } + } + } + + public Raw { + Objects.requireNonNull(value); + Objects.requireNonNull(type); + } + + private static Optional tryCreate(Element e) { + return Type.fromElementName(e.getNodeName()).map(type -> { + if (type == Type.BOOLEAN) { + if ("true".equals(e.getNodeName())) { + return new Raw(Boolean.TRUE.toString(), type); + } else { + return new Raw(Boolean.FALSE.toString(), type); + } + } else { + return new Raw(e.getTextContent(), type); + } + }); + } + } + + /** + * Returns the contents of the the underlying "dict" element as a Map. + *

        + * The keys in the returned map are names of the properties. + *

        + * Values of nested "dict" properties are stored as {@code Map} + * or {@code PListReader} objects depending on the value of the + * {@code fetchDictionaries} parameter. + *

        + * Values of "array" properties are stored as {@code List} objects. + *

        + * Values of other properties are stored as {@code Raw} objects. + * + * @param fetchDictionaries controls the type of objects of nested "dict" + * elements. If the value is {@code true}, + * {@code Map} type is used, and + * {@code PListReader} type otherwise. + * @return the contents of the the underlying "dict" element as a Map + */ + public Map toMap(boolean fetchDictionaries) { + Map reply = new HashMap<>(); + var nodes = root.getChildNodes(); + for (int i = 0; i != nodes.getLength(); i++) { + if (nodes.item(i) instanceof Element e) { + tryCreateValue(e, fetchDictionaries).ifPresent(value -> { + final var query = "preceding-sibling::*[1]"; + Optional.ofNullable(toSupplier(() -> { + return (Node) XPathSingleton.INSTANCE.evaluate(query, e, XPathConstants.NODE); + }).get()).ifPresent(n -> { + if ("key".equals(n.getNodeName())) { + var keyName = n.getTextContent(); + reply.putIfAbsent(keyName, value); + } + }); + }); + } + } + + return reply; + } + + /** + * Returns the value of the given string property in the underlying "dict" + * element. + * + * @param keyName the name of a string property whose value to query + * @return the value of the string property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no string property with the given + * name in the underlying "dict" element + */ public String queryValue(String keyName) { final var node = getNode(keyName); switch (node.getNodeName()) { @@ -51,6 +171,38 @@ public final class PListReader { } } + /** + * Returns the value of the given "dict" property in the underlying "dict" + * element. + * + * @param keyName the name of a "dict" property whose value to query + * @return the value of the "dict" property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no "dict" property with the given + * name in the underlying "dict" element + */ + public PListReader queryDictValue(String keyName) { + final var node = getNode(keyName); + switch (node.getNodeName()) { + case "dict" -> { + return new PListReader(node); + } + default -> { + throw new NoSuchElementException(); + } + } + } + + /** + * Returns the value of the given boolean property in the underlying "dict" + * element. + * + * @param keyName the name of a boolean property whose value to query + * @return the value of the boolean property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no string property with the given + * name in the underlying "dict" element + */ public boolean queryBoolValue(String keyName) { final var node = getNode(keyName); switch (node.getNodeName()) { @@ -66,13 +218,58 @@ public final class PListReader { } } - public List queryArrayValue(String keyName) { + /** + * Returns the value of the given array property in the underlying "dict" + * element as a list of strings. + *

        + * Processes the result of calling {@link #queryArrayValue(String)} on the + * specified property name by filtering {@link Raw} instances of type + * {@link Raw.Type#STRING}. + * + * @param keyName the name of an array property whose value to query + * @return the value of the array property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no array property with the given + * name in the underlying "dict" element + */ + public List queryStringArrayValue(String keyName) { + return queryArrayValue(keyName, false).map(v -> { + if (v instanceof Raw r) { + if (r.type() == Raw.Type.STRING) { + return r.value(); + } + } + return (String)null; + }).filter(Objects::nonNull).toList(); + } + + /** + * Returns the value of the given array property in the underlying "dict" + * element as a stream of {@link Object}-s. + *

        + * Values of "dict" array items are stored as {@code Map} or + * {@code PListReader} objects depending on the value of the + * {@code fetchDictionaries} parameter. + *

        + * Values of "array" array items are stored as {@code List} objects. + *

        + * Values of other types are stored as {@code Raw} objects. + * + * @param keyName the name of an array property whose value to query + * @param fetchDictionaries controls the type of objects of "dict" elements. If + * the value is {@code true}, + * {@code Map} type is used, and + * {@code PListReader} type otherwise. + * @return the value of the array property with the specified name in the + * underlying "dict" element + * @throws NoSuchElementException if there is no array key with the given name + * in the underlying "dict" element + */ + public Stream queryArrayValue(String keyName, boolean fetchDictionaries) { final var node = getNode(keyName); switch (node.getNodeName()) { case "array" -> { - return XmlUtils.toStream(node.getChildNodes()).filter(n -> { - return n.getNodeName().equals("string"); - }).map(Node::getTextContent).toList(); + return readArray(node, fetchDictionaries); } default -> { throw new NoSuchElementException(); @@ -80,21 +277,75 @@ public final class PListReader { } } - public PListReader(Node doc) { - this.root = Objects.requireNonNull(doc); + /** + * Creates plist reader from the given node. + *

        + * If the specified node is an element with the name "dict", the reader is bound + * to the specified node; otherwise, it is bound to the {@code /plist/dict} + * element in the document. + * + * @param node the node + * @throws NoSuchElementException if the specified node is not an element with + * name "dict" and there is no + * {@code /plist/dict} node in the document + */ + public PListReader(Node node) { + Objects.requireNonNull(node); + if (node.getNodeName().equals("dict")) { + this.root = node; + } else { + this.root = Optional.ofNullable(toSupplier(() -> { + return (Node) XPathSingleton.INSTANCE.evaluate("/plist[1]/dict[1]", node, XPathConstants.NODE); + }).get()).orElseThrow(NoSuchElementException::new); + } } public PListReader(byte[] xmlData) throws ParserConfigurationException, SAXException, IOException { this(XmlUtils.initDocumentBuilder().parse(new ByteArrayInputStream(xmlData))); } + private Optional tryCreateValue(Element e, boolean fetchDictionaries) { + switch (e.getNodeName()) { + case "dict" -> { + var plistReader = new PListReader(e); + if (fetchDictionaries) { + return Optional.of(plistReader.toMap(fetchDictionaries)); + } else { + return Optional.of(plistReader); + } + } + case "array" -> { + return Optional.of(readArray(e, fetchDictionaries).toList()); + } + default -> { + return Raw.tryCreate(e); + } + } + } + + private Stream readArray(Node node, boolean fetchDictionaries) { + return XmlUtils.toStream(node.getChildNodes()).map(n -> { + if (n instanceof Element e) { + return tryCreateValue(e, fetchDictionaries); + } else { + return Optional.empty(); + } + }).filter(Optional::isPresent).map(Optional::get); + } + private Node getNode(String keyName) { - final var xPath = XPathFactory.newInstance().newXPath(); - final var query = String.format("//*[preceding-sibling::key = \"%s\"][1]", keyName); - return Optional.ofNullable(ThrowingSupplier.toSupplier(() -> { - return (Node) xPath.evaluate(query, root, XPathConstants.NODE); + Objects.requireNonNull(keyName); + final var query = String.format("*[preceding-sibling::key = \"%s\"][1]", keyName); + return Optional.ofNullable(toSupplier(() -> { + return (Node) XPathSingleton.INSTANCE.evaluate(query, root, XPathConstants.NODE); }).get()).orElseThrow(NoSuchElementException::new); } + + private static final class XPathSingleton { + private static final XPath INSTANCE = XPathFactory.newInstance().newXPath(); + } + + private final Node root; } diff --git a/test/jdk/tools/jpackage/TEST.properties b/test/jdk/tools/jpackage/TEST.properties index 58a82a9444b..7c799e4d320 100644 --- a/test/jdk/tools/jpackage/TEST.properties +++ b/test/jdk/tools/jpackage/TEST.properties @@ -22,5 +22,6 @@ modules = \ jdk.jpackage/jdk.jpackage.internal:+open \ jdk.jpackage/jdk.jpackage.internal.util \ jdk.jpackage/jdk.jpackage.internal.util.function \ + jdk.jpackage/jdk.jpackage.internal.resources \ java.base/jdk.internal.util \ jdk.jlink/jdk.tools.jlink.internal diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 49f565e27e9..6aacc261fb6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1085,7 +1085,9 @@ public class JPackageCommand extends CommandArguments { }), MAIN_LAUNCHER_FILES(cmd -> { if (!cmd.isRuntime()) { - new LauncherVerifier(cmd).verify(cmd, LauncherVerifier.Action.VERIFY_INSTALLED); + new LauncherVerifier(cmd).verify(cmd, + LauncherVerifier.Action.VERIFY_INSTALLED, + LauncherVerifier.Action.VERIFY_MAC_ENTITLEMENTS); } }), MAIN_JAR_FILE(cmd -> { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java index 42821822894..e1cd37fe8b4 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherAsServiceVerifier.java @@ -311,7 +311,7 @@ public final class LauncherAsServiceVerifier { var servicePlist = MacHelper.readPList(servicePlistFile); - var args = servicePlist.queryArrayValue("ProgramArguments"); + var args = servicePlist.queryStringArrayValue("ProgramArguments"); TKit.assertEquals(1, args.size(), "Check number of array elements in 'ProgramArguments' property in the property file"); TKit.assertEquals(installedLauncherPath.toString(), args.get(0), diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java index b3ed030c69d..7bf01fdde05 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -37,9 +37,14 @@ import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; +import javax.xml.parsers.ParserConfigurationException; +import jdk.jpackage.internal.resources.ResourceLocator; +import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.internal.util.function.ThrowingSupplier; import jdk.jpackage.test.AdditionalLauncher.PropertyFile; import jdk.jpackage.test.LauncherShortcut.StartupDirectory; +import org.xml.sax.SAXException; public final class LauncherVerifier { @@ -82,6 +87,11 @@ public final class LauncherVerifier { verifier.verifyInAppImageFile(cmd); } }), + VERIFY_MAC_ENTITLEMENTS((verifier, cmd) -> { + if (TKit.isOSX() && MacHelper.appImageSigned(cmd)) { + verifier.verifyMacEntitlements(cmd); + } + }), EXECUTE_LAUNCHER(LauncherVerifier::executeLauncher), ; @@ -96,7 +106,7 @@ public final class LauncherVerifier { private final BiConsumer action; static final List VERIFY_APP_IMAGE = List.of( - VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED, VERIFY_APP_IMAGE_FILE + VERIFY_ICON, VERIFY_DESCRIPTION, VERIFY_INSTALLED, VERIFY_APP_IMAGE_FILE, VERIFY_MAC_ENTITLEMENTS ); static final List VERIFY_DEFAULTS = Stream.concat( @@ -323,6 +333,24 @@ public final class LauncherVerifier { } } + private void verifyMacEntitlements(JPackageCommand cmd) throws ParserConfigurationException, SAXException, IOException { + Path launcherPath = cmd.appLauncherPath(name); + var entitlements = MacSignVerify.findEntitlements(launcherPath); + + TKit.assertTrue(entitlements.isPresent(), String.format("Check [%s] launcher is signed with entitlements", name)); + + Map expected; + if (cmd.hasArgument("--mac-entitlements")) { + expected = new PListReader(Files.readAllBytes(Path.of(cmd.getArgumentValue("--mac-entitlements")))).toMap(true); + } else if (cmd.hasArgument("--mac-app-store")) { + expected = DefaultEntitlements.APP_STORE; + } else { + expected = DefaultEntitlements.STANDARD; + } + + TKit.assertEquals(expected, entitlements.orElseThrow().toMap(true), String.format("Check [%s] launcher is signed with expected entitlements", name)); + } + private void executeLauncher(JPackageCommand cmd) throws IOException { Path launcherPath = cmd.appLauncherPath(name); @@ -362,6 +390,20 @@ public final class LauncherVerifier { }); } + + private static final class DefaultEntitlements { + private static Map loadFromResources(String resourceName) { + return ThrowingSupplier.toSupplier(() -> { + var bytes = ResourceLocator.class.getResourceAsStream("entitlements.plist").readAllBytes(); + return new PListReader(bytes).toMap(true); + }).get(); + } + + static final Map STANDARD = loadFromResources("entitlements.plist"); + static final Map APP_STORE = loadFromResources("sandbox.plist"); + } + + private final String name; private final Optional> javaOptions; private final Optional> arguments; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index f4feb3e2fde..88e2819ce15 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -74,7 +74,8 @@ public final class MacHelper { Path mountPoint = null; try { - var plist = readPList(attachExecutor.getOutput()); + // The first "dict" item of "system-entities" array property contains "mount-point" string property. + var plist = readPList(attachExecutor.getOutput()).queryArrayValue("system-entities", false).findFirst().map(PListReader.class::cast).orElseThrow(); mountPoint = Path.of(plist.queryValue("mount-point")); // code here used to copy just or .app diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java index 432433b4fd0..ae27e292bf6 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java @@ -56,6 +56,17 @@ public final class MacSignVerify { String.format("Check [%s] signed with adhoc signature", path)); } + public static Optional findEntitlements(Path path) { + final var exec = Executor.of("/usr/bin/codesign", "-d", "--entitlements", "-", "--xml", path.toString()).saveOutput().dumpOutput(); + final var result = exec.execute(); + var xml = result.stdout().getOutput(); + if (xml.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(MacHelper.readPList(xml)); + } + } + public static void assertUnsigned(Path path) { TKit.assertTrue(findSpctlSignOrigin(SpctlType.EXEC, path).isEmpty(), String.format("Check [%s] unsigned", path)); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java index d766505cf14..fe7d0c63870 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java @@ -32,14 +32,18 @@ import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.function.BiFunction; +import java.util.stream.Stream; import javax.xml.parsers.ParserConfigurationException; +import jdk.jpackage.internal.util.PListReader.Raw; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; import org.junit.jupiter.params.provider.MethodSource; import org.w3c.dom.Node; import org.xml.sax.InputSource; @@ -51,7 +55,23 @@ public class PListReaderTest { enum QueryType { STRING(PListReader::queryValue), BOOLEAN(PListReader::queryBoolValue), - STRING_ARRAY(PListReader::queryArrayValue); + DICT((plistReader, keyName) -> { + return plistReader.queryDictValue(keyName).toMap(true); + }), + STRING_ARRAY(PListReader::queryStringArrayValue), + RAW_ARRAY((plistReader, keyName) -> { + return plistReader.queryArrayValue(keyName, false).toList(); + }), + RAW_ARRAY_RECURSIVE((plistReader, keyName) -> { + return plistReader.queryArrayValue(keyName, true).toList(); + }), + TO_MAP((plistReader, _) -> { + return plistReader.toMap(false); + }), + TO_MAP_RECURSIVE((plistReader, _) -> { + return plistReader.toMap(true); + }), + ; QueryType(BiFunction queryMethod) { this.queryMethod = Objects.requireNonNull(queryMethod); @@ -65,10 +85,10 @@ public class PListReaderTest { private final BiFunction queryMethod; } - public record QueryValueTestSpec(QueryType queryType, String keyName, Optional expectedValue, + public record TestSpec(QueryType queryType, Optional keyName, Optional expectedValue, Optional> expectedException, String... xml) { - public QueryValueTestSpec { + public TestSpec { Objects.requireNonNull(queryType); Objects.requireNonNull(keyName); Objects.requireNonNull(expectedValue); @@ -77,6 +97,14 @@ public class PListReaderTest { if (expectedValue.isEmpty() == expectedException.isEmpty()) { throw new IllegalArgumentException(); } + if (keyName.isEmpty()) { + switch (queryType) { + case TO_MAP, TO_MAP_RECURSIVE -> {} + default -> { + throw new IllegalArgumentException(); + } + } + } } static final class Builder { @@ -91,7 +119,7 @@ public class PListReaderTest { return this; } - Builder expectedValue(Object v) { + Builder expect(Object v) { expectedValue = v; if (v instanceof String) { queryType(QueryType.STRING); @@ -99,11 +127,13 @@ public class PListReaderTest { queryType(QueryType.BOOLEAN); } else if (v instanceof List) { queryType(QueryType.STRING_ARRAY); + } else if (v instanceof Map) { + queryType(QueryType.DICT); } return this; } - Builder expectedException(Class v) { + Builder expectException(Class v) { expectedException = v; return this; } @@ -113,8 +143,11 @@ public class PListReaderTest { return this; } - QueryValueTestSpec create() { - return new QueryValueTestSpec(queryType, keyName, Optional.ofNullable(expectedValue), + TestSpec create() { + return new TestSpec( + queryType, + Optional.ofNullable(keyName), + Optional.ofNullable(expectedValue), validatedExpectedException(), xml); } @@ -137,12 +170,12 @@ public class PListReaderTest { final var plistReader = new PListReader(createXml(xml)); expectedValue.ifPresent(v -> { - final var actualValue = queryType.queryValue(plistReader, keyName); + final var actualValue = queryType.queryValue(plistReader, keyName.orElse(null)); assertEquals(v, actualValue); }); expectedException.ifPresent(v -> { - assertThrows(v, () -> queryType.queryValue(plistReader, keyName)); + assertThrows(v, () -> queryType.queryValue(plistReader, keyName.orElse(null))); }); } @@ -150,7 +183,9 @@ public class PListReaderTest { public String toString() { final var sb = new StringBuilder(); sb.append(queryType); - sb.append("; key=").append(keyName); + if (keyName != null) { + sb.append("; key=").append(keyName); + } expectedValue.ifPresent(v -> { sb.append("; expected="); sb.append(v); @@ -166,13 +201,13 @@ public class PListReaderTest { } @ParameterizedTest - @EnumSource(QueryType.class) + @EnumSource(mode = Mode.MATCH_NONE, names = {"TO_MAP.*"}) public void testNoSuchElement(QueryType queryType) { testSpec(queryType).create().test(); } @ParameterizedTest - @EnumSource(QueryType.class) + @EnumSource(mode = Mode.MATCH_NONE, names = {"TO_MAP.*"}) public void testWrongValueType(QueryType queryType) { final var builder = testSpec(queryType).xml( "string-key", @@ -182,33 +217,56 @@ public class PListReaderTest { "boolean-false-key", "", "array-key", - "b"); + "b", + "dict-key", + "nested-dict-key345"); - List testSpecs = new ArrayList<>(); + List testSpecs = new ArrayList<>(); switch (queryType) { case STRING -> { testSpecs.add(builder.keyName("boolean-true-key").create()); testSpecs.add(builder.keyName("boolean-false-key").create()); testSpecs.add(builder.keyName("array-key").create()); + testSpecs.add(builder.keyName("dict-key").create()); } case BOOLEAN -> { testSpecs.add(builder.keyName("string-key").create()); testSpecs.add(builder.keyName("array-key").create()); + testSpecs.add(builder.keyName("dict-key").create()); } - case STRING_ARRAY -> { + case STRING_ARRAY, RAW_ARRAY, RAW_ARRAY_RECURSIVE -> { testSpecs.add(builder.keyName("string-key").create()); testSpecs.add(builder.keyName("boolean-true-key").create()); testSpecs.add(builder.keyName("boolean-false-key").create()); + testSpecs.add(builder.keyName("dict-key").create()); + } + case DICT -> { + testSpecs.add(builder.keyName("string-key").create()); + testSpecs.add(builder.keyName("boolean-true-key").create()); + testSpecs.add(builder.keyName("boolean-false-key").create()); + testSpecs.add(builder.keyName("array-key").create()); + } + case TO_MAP, TO_MAP_RECURSIVE -> { + throw new UnsupportedOperationException(); } } - testSpecs.forEach(QueryValueTestSpec::test); + testSpecs.forEach(TestSpec::test); + + builder.keyName(null).expect(Map.of( + "string-key", new Raw("a", Raw.Type.STRING), + "boolean-true-key", new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN), + "boolean-false-key", new Raw(Boolean.FALSE.toString(), Raw.Type.BOOLEAN), + "array-key", List.of(new Raw("b", Raw.Type.STRING)), + "dict-key", Map.of("nested-dict-key", new Raw("345", Raw.Type.INTEGER)) + )).queryType(QueryType.TO_MAP_RECURSIVE).create().test(); + } @ParameterizedTest @MethodSource - public void testQueryValue(QueryValueTestSpec testSpec) { + public void test(TestSpec testSpec) { testSpec.test(); } @@ -219,28 +277,190 @@ public class PListReaderTest { assertEquals("A", actualValue); } - private static List testQueryValue() { - return List.of( - testSpec().expectedValue("A").xml("fooA").create(), - testSpec().expectedValue("").xml("foo").create(), - testSpec().xml("foo").create(), - testSpec().expectedValue(Boolean.TRUE).xml("foo").create(), - testSpec().expectedValue(Boolean.FALSE).xml("foo").create(), - testSpec(QueryType.BOOLEAN).xml("foo").create(), - testSpec(QueryType.BOOLEAN).xml("foo").create(), - testSpec().expectedValue(List.of("foo", "bar")).xml("foofoobar").create(), - testSpec().expectedValue(List.of()).xml("foo").create(), - testSpec(QueryType.STRING_ARRAY).xml("foo").create(), - testSpec().expectedValue("A").xml("fooAB").create(), - testSpec().expectedValue("A").xml("fooAfooB").create() + @Test + public void test_toMap() { + + var builder = testSpec(); + + builder.xml( + "AppName", + "Hello", + "", + "AppVersion", + "1.0", + "Release", + "", + "Debug", + "", + "ReleaseDate", + "2025-09-24T09:23:00Z", + "UserData", + "", + "", + " Foo", + " ", + " Str", + " ", + " Another Str", + " ", + " ", + " ", + " ", + "", + "Checksum", + "7841ff0076cdde93bdca02cfd332748c40620ce4", + "Plugins", + "", + " ", + " PluginName", + " Foo", + " Priority", + " 13", + " History", + " ", + " New File", + " Another New File", + " ", + " ", + " ", + " PluginName", + " Bar", + " Priority", + " 23", + " History", + " ", + " ", + "" ); + + var expected = Map.of( + "AppName", new Raw("Hello", Raw.Type.STRING), + "AppVersion", new Raw("1.0", Raw.Type.REAL), + "Release", new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN), + "Debug", new Raw(Boolean.FALSE.toString(), Raw.Type.BOOLEAN), + "ReleaseDate", new Raw("2025-09-24T09:23:00Z", Raw.Type.DATE), + "Checksum", new Raw("7841ff0076cdde93bdca02cfd332748c40620ce4", Raw.Type.DATA), + "UserData", Map.of( + "Foo", List.of( + new Raw("Str", Raw.Type.STRING), + List.of( + new Raw("Another Str", Raw.Type.STRING), + new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN), + new Raw(Boolean.FALSE.toString(), Raw.Type.BOOLEAN) + ) + ) + ), + "Plugins", List.of( + Map.of( + "PluginName", new Raw("Foo", Raw.Type.STRING), + "Priority", new Raw("13", Raw.Type.INTEGER), + "History", List.of( + new Raw("New File", Raw.Type.STRING), + new Raw("Another New File", Raw.Type.STRING) + ) + ), + Map.of( + "PluginName", new Raw("Bar", Raw.Type.STRING), + "Priority", new Raw("23", Raw.Type.REAL), + "History", List.of() + ) + ) + ); + + builder.expect(expected).queryType(QueryType.TO_MAP_RECURSIVE).create().test(); } - private static QueryValueTestSpec.Builder testSpec() { - return new QueryValueTestSpec.Builder(); + private static List test() { + + List data = new ArrayList<>(); + + Stream.of( + testSpec().expect("A").xml("fooA"), + testSpec().expect("A").xml("BfooA"), + testSpec().expect("").xml("foo some text "), + testSpec().xml("foo"), + testSpec().xml("foo"), + testSpec().xml("fooA"), + testSpec().expect(Boolean.TRUE).xml("foo"), + testSpec().expect(Boolean.FALSE).xml("foo"), + testSpec(QueryType.BOOLEAN).xml("foo"), + testSpec(QueryType.BOOLEAN).xml("foo"), + testSpec().expect(List.of("foo", "bar")).xml("foofoobar"), + testSpec().expect(List.of()).xml("foo"), + testSpec(QueryType.STRING_ARRAY).xml("foo"), + testSpec().expect("A").xml("fooAB"), + testSpec().expect("A").xml("fooAfooB"), + + testSpec().expect(Map.of()).xml("foo"), + + // + // Test that if there are multiple keys with the same name, all but the first are ignored. + // + testSpec().expect("A").xml("fooAfooBfooC"), + testSpec().expect("A").xml("fooAfooB"), + testSpec(QueryType.STRING).xml("fooBfooA"), + testSpec().expect(Boolean.TRUE).xml("foofoo"), + testSpec().expect(Boolean.TRUE).xml("foofoo"), + testSpec(QueryType.BOOLEAN).xml("foofoo"), + + // + // Test that it doesn't look up keys in nested "dict" or "array" elements. + // + testSpec().xml("foofooA"), + testSpec().expect("B").xml("barfooAfooB"), + testSpec().xml("foofooA"), + testSpec().expect("B").xml("barfooAfooB"), + + // + // Test empty arrays. + // + testSpec().expect(List.of()).queryType(QueryType.RAW_ARRAY_RECURSIVE).xml("foo"), + testSpec().expect(List.of()).queryType(QueryType.RAW_ARRAY).xml("foo") + + ).map(TestSpec.Builder::create).forEach(data::add); + + // + // Test toMap() method. + // + Stream.of( + testSpec().expect(Map.of()).xml(), + testSpec().expect(Map.of()).xml("foobar"), + testSpec().expect(Map.of()).xml("Abar"), + testSpec().expect(Map.of()).xml("A"), + testSpec().expect(Map.of()).xml("fooA"), + testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING))).xml("fooAB"), + testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING))).xml("fooA hello foo bye B"), + testSpec().expect(Map.of("foo", new Raw("A", Raw.Type.STRING), "Foo", new Raw("B", Raw.Type.STRING))).xml("fooAFooB") + ).map(builder -> { + return builder.queryType(QueryType.TO_MAP_RECURSIVE); + }).map(TestSpec.Builder::create).forEach(data::add); + + var arrayTestSpec = testSpec().expect(List.of( + new Raw("Hello", Raw.Type.STRING), + Map.of("foo", new Raw("Bye", Raw.Type.STRING)), + new Raw("integer", Raw.Type.INTEGER), + Map.of(), + new Raw(Boolean.TRUE.toString(), Raw.Type.BOOLEAN) + )).queryType(QueryType.RAW_ARRAY_RECURSIVE); + + Stream.of( + "HellofooByeinteger", + "HelloBingofooByeinteger", + "BHellofooByeByeeeinteger", + "HellobarfooByeinteger", + "HellofooByefooByeByeinteger" + ).map(xml -> { + return "foo" + xml + ""; + }).map(arrayTestSpec::xml).map(TestSpec.Builder::create).forEach(data::add); + + return data; } - private static QueryValueTestSpec.Builder testSpec(QueryType queryType) { + private static TestSpec.Builder testSpec() { + return new TestSpec.Builder(); + } + + private static TestSpec.Builder testSpec(QueryType queryType) { return testSpec().queryType(queryType); } @@ -248,7 +468,9 @@ public class PListReaderTest { final List content = new ArrayList<>(); content.add(""); content.add(""); + content.add(""); content.addAll(List.of(xml)); + content.add(""); content.add(""); return String.join("", content.toArray(String[]::new)); } diff --git a/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java b/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java index 81831f59da3..e6d8b41f05f 100644 --- a/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java +++ b/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java @@ -21,15 +21,16 @@ * questions. */ +import static java.util.Map.entry; + import java.nio.file.Path; import java.util.List; import java.util.Map; -import static java.util.Map.entry; -import jdk.jpackage.test.JPackageCommand; -import jdk.jpackage.test.TKit; -import jdk.jpackage.test.MacHelper; import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.TKit; /** * Tests generation of app image with --file-associations and mac additional file @@ -80,21 +81,26 @@ public class MacFileAssociationsTest { "Check value of %s plist key", key)); } - private static void checkBoolValue(PListReader plist, String key, Boolean value) { - Boolean result = plist.queryBoolValue(key); - TKit.assertEquals(value.toString(), result.toString(), String.format( + private static void checkBoolValue(PListReader plist, String key, boolean value) { + boolean result = plist.queryBoolValue(key); + TKit.assertEquals(value, result, String.format( "Check value of %s plist key", key)); } private static void checkArrayValue(PListReader plist, String key, List values) { - List result = plist.queryArrayValue(key); + List result = plist.queryStringArrayValue(key); TKit.assertStringListEquals(values, result, String.format( "Check value of %s plist key", key)); } private static void verifyPList(Path appImage) throws Exception { - var plist = MacHelper.readPListFromAppImage(appImage); + final var rootPlist = MacHelper.readPListFromAppImage(appImage); + + TKit.traceFileContents(appImage.resolve("Contents/Info.plist"), "Info.plist"); + + var plist = rootPlist.queryArrayValue("CFBundleDocumentTypes", false).findFirst().map(PListReader.class::cast).orElseThrow(); + checkStringValue(plist, "CFBundleTypeRole", "Viewer"); checkStringValue(plist, "LSHandlerRank", "Default"); checkStringValue(plist, "NSDocumentClass", "SomeClass"); @@ -103,10 +109,13 @@ public class MacFileAssociationsTest { checkBoolValue(plist, "LSSupportsOpeningDocumentsInPlace", false); checkBoolValue(plist, "UISupportsDocumentBrowser", false); - checkArrayValue(plist, "NSExportableTypes", List.of("public.png", - "public.jpg")); + plist = rootPlist.queryArrayValue("UTExportedTypeDeclarations", false).findFirst().map(PListReader.class::cast).orElseThrow(); + + checkArrayValue(plist, "UTTypeConformsTo", List.of("public.image", "public.data")); + + plist = plist.queryDictValue("UTTypeTagSpecification"); + + checkArrayValue(plist, "NSExportableTypes", List.of("public.png", "public.jpg")); - checkArrayValue(plist, "UTTypeConformsTo", List.of("public.image", - "public.data")); } } From 2360542e89067e5c5d5a7bf403c18c9f371efd9a Mon Sep 17 00:00:00 2001 From: Roger Riggs Date: Fri, 26 Sep 2025 14:19:12 +0000 Subject: [PATCH 257/556] 8368683: [process] Increase jtreg debug output maxOutputSize for TreeTest Reviewed-by: msheppar --- test/jdk/java/lang/ProcessHandle/TEST.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/jdk/java/lang/ProcessHandle/TEST.properties diff --git a/test/jdk/java/lang/ProcessHandle/TEST.properties b/test/jdk/java/lang/ProcessHandle/TEST.properties new file mode 100644 index 00000000000..c7e8a6850ca --- /dev/null +++ b/test/jdk/java/lang/ProcessHandle/TEST.properties @@ -0,0 +1 @@ +maxOutputSize=6000000 From 501b2b3ebc50d9bb1c32267ef8e56561ea1e71eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20H=C3=A4ssig?= Date: Fri, 26 Sep 2025 14:28:35 +0000 Subject: [PATCH 258/556] 8368615: VSCode IDE: Oracle Java extension routinely runs out of memory Reviewed-by: erikj --- make/ide/vscode/hotspot/template-workspace.jsonc | 1 + 1 file changed, 1 insertion(+) diff --git a/make/ide/vscode/hotspot/template-workspace.jsonc b/make/ide/vscode/hotspot/template-workspace.jsonc index c582c48047d..8a349b232ce 100644 --- a/make/ide/vscode/hotspot/template-workspace.jsonc +++ b/make/ide/vscode/hotspot/template-workspace.jsonc @@ -22,6 +22,7 @@ // Java extension "jdk.project.jdkhome": "{{OUTPUTDIR}}/jdk", "jdk.java.onSave.organizeImports": false, // prevents unnecessary changes + "jdk.serverVmOptions": ["-Xmx2G"], // prevent out of memory // Additional conventions "files.associations": { From 25abdd85c41f7aef41915cabd8596c0ce573acd6 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Fri, 26 Sep 2025 14:48:26 +0000 Subject: [PATCH 259/556] 8368752: Serial: Remove unused arg of DefNewGeneration::gc_epilogue Reviewed-by: tschatzl, fandreuzzi --- src/hotspot/share/gc/serial/defNewGeneration.cpp | 2 +- src/hotspot/share/gc/serial/defNewGeneration.hpp | 2 +- src/hotspot/share/gc/serial/serialHeap.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hotspot/share/gc/serial/defNewGeneration.cpp b/src/hotspot/share/gc/serial/defNewGeneration.cpp index bcd131a5fa2..42f8b191f5e 100644 --- a/src/hotspot/share/gc/serial/defNewGeneration.cpp +++ b/src/hotspot/share/gc/serial/defNewGeneration.cpp @@ -807,7 +807,7 @@ void DefNewGeneration::reset_scratch() { } } -void DefNewGeneration::gc_epilogue(bool full) { +void DefNewGeneration::gc_epilogue() { assert(!GCLocker::is_active(), "We should not be executing here"); // update the generation and space performance counters update_counters(); diff --git a/src/hotspot/share/gc/serial/defNewGeneration.hpp b/src/hotspot/share/gc/serial/defNewGeneration.hpp index 7f4077873b2..32b6b32f42f 100644 --- a/src/hotspot/share/gc/serial/defNewGeneration.hpp +++ b/src/hotspot/share/gc/serial/defNewGeneration.hpp @@ -187,7 +187,7 @@ class DefNewGeneration: public Generation { HeapWord* allocate(size_t word_size); HeapWord* par_allocate(size_t word_size); - void gc_epilogue(bool full); + void gc_epilogue(); // For Old collection (part of running Full GC), the DefNewGeneration can // contribute the free part of "to-space" as the scratch space. diff --git a/src/hotspot/share/gc/serial/serialHeap.cpp b/src/hotspot/share/gc/serial/serialHeap.cpp index 89218648fe0..dbd54c302ea 100644 --- a/src/hotspot/share/gc/serial/serialHeap.cpp +++ b/src/hotspot/share/gc/serial/serialHeap.cpp @@ -779,7 +779,7 @@ void SerialHeap::gc_epilogue(bool full) { resize_all_tlabs(); - _young_gen->gc_epilogue(full); + _young_gen->gc_epilogue(); _old_gen->gc_epilogue(); if (_is_heap_almost_full) { From aa6ff45052516f5383fb7e62cfb469cbade0c42e Mon Sep 17 00:00:00 2001 From: Ashutosh Mehra Date: Fri, 26 Sep 2025 14:56:03 +0000 Subject: [PATCH 260/556] 8368693: Duplicate methods in vmClasses Reviewed-by: liach, coleenp, dholmes --- src/hotspot/share/classfile/classFileParser.cpp | 4 ++-- src/hotspot/share/classfile/javaClasses.cpp | 4 ++-- src/hotspot/share/classfile/vmClasses.cpp | 2 +- src/hotspot/share/classfile/vmClasses.hpp | 6 ------ src/hotspot/share/memory/universe.cpp | 4 ++-- src/hotspot/share/oops/constantPool.cpp | 2 +- src/hotspot/share/oops/instanceKlass.cpp | 2 +- src/hotspot/share/oops/objArrayKlass.cpp | 2 +- 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/hotspot/share/classfile/classFileParser.cpp b/src/hotspot/share/classfile/classFileParser.cpp index 52bbab01d61..fddd9df726b 100644 --- a/src/hotspot/share/classfile/classFileParser.cpp +++ b/src/hotspot/share/classfile/classFileParser.cpp @@ -2445,7 +2445,7 @@ Method* ClassFileParser::parse_method(const ClassFileStream* const cfs, cfs->skip_u2_fast(method_parameters_length); cfs->skip_u2_fast(method_parameters_length); // ignore this attribute if it cannot be reflected - if (!vmClasses::Parameter_klass_loaded()) + if (!vmClasses::reflect_Parameter_klass_is_loaded()) method_parameters_length = -1; } else if (method_attribute_name == vmSymbols::tag_synthetic()) { if (method_attribute_length != 0) { @@ -3979,7 +3979,7 @@ void ClassFileParser::set_precomputed_flags(InstanceKlass* ik) { #endif // Check if this klass supports the java.lang.Cloneable interface - if (vmClasses::Cloneable_klass_loaded()) { + if (vmClasses::Cloneable_klass_is_loaded()) { if (ik->is_subtype_of(vmClasses::Cloneable_klass())) { ik->set_is_cloneable(); } diff --git a/src/hotspot/share/classfile/javaClasses.cpp b/src/hotspot/share/classfile/javaClasses.cpp index 75f54bfa549..cfe55bf7f76 100644 --- a/src/hotspot/share/classfile/javaClasses.cpp +++ b/src/hotspot/share/classfile/javaClasses.cpp @@ -1150,7 +1150,7 @@ void java_lang_Class::create_mirror(Klass* k, Handle class_loader, // Class_klass has to be loaded because it is used to allocate // the mirror. - if (vmClasses::Class_klass_loaded()) { + if (vmClasses::Class_klass_is_loaded()) { Handle mirror; Handle comp_mirror; @@ -1223,7 +1223,7 @@ bool java_lang_Class::restore_archived_mirror(Klass *k, Handle protection_domain, TRAPS) { // Postpone restoring archived mirror until java.lang.Class is loaded. Please // see more details in vmClasses::resolve_all(). - if (!vmClasses::Class_klass_loaded() && !CDSConfig::is_using_aot_linked_classes()) { + if (!vmClasses::Class_klass_is_loaded() && !CDSConfig::is_using_aot_linked_classes()) { assert(fixup_mirror_list() != nullptr, "fixup_mirror_list not initialized"); fixup_mirror_list()->push(k); return true; diff --git a/src/hotspot/share/classfile/vmClasses.cpp b/src/hotspot/share/classfile/vmClasses.cpp index e337d5569bc..478f40ff05b 100644 --- a/src/hotspot/share/classfile/vmClasses.cpp +++ b/src/hotspot/share/classfile/vmClasses.cpp @@ -113,7 +113,7 @@ void vmClasses::resolve_until(vmClassID limit_id, vmClassID &start_id, TRAPS) { } void vmClasses::resolve_all(TRAPS) { - assert(!Object_klass_loaded(), "well-known classes should only be initialized once"); + assert(!Object_klass_is_loaded(), "well-known classes should only be initialized once"); // Create the ModuleEntry for java.base. This call needs to be done here, // after vmSymbols::initialize() is called but before any classes are pre-loaded. diff --git a/src/hotspot/share/classfile/vmClasses.hpp b/src/hotspot/share/classfile/vmClasses.hpp index 4fa078c50cd..caa54590eb2 100644 --- a/src/hotspot/share/classfile/vmClasses.hpp +++ b/src/hotspot/share/classfile/vmClasses.hpp @@ -102,12 +102,6 @@ public: assert((uint)t < T_VOID+1, "range check"); return check_klass(_box_klasses[t]); } - - static bool Object_klass_loaded() { return is_loaded(VM_CLASS_AT(Object_klass)); } - static bool Class_klass_loaded() { return is_loaded(VM_CLASS_AT(Class_klass)); } - static bool Cloneable_klass_loaded() { return is_loaded(VM_CLASS_AT(Cloneable_klass)); } - static bool Parameter_klass_loaded() { return is_loaded(VM_CLASS_AT(reflect_Parameter_klass)); } - static bool ClassLoader_klass_loaded() { return is_loaded(VM_CLASS_AT(ClassLoader_klass)); } }; #endif // SHARE_CLASSFILE_VMCLASSES_HPP diff --git a/src/hotspot/share/memory/universe.cpp b/src/hotspot/share/memory/universe.cpp index a3b841a400b..424e43c5e83 100644 --- a/src/hotspot/share/memory/universe.cpp +++ b/src/hotspot/share/memory/universe.cpp @@ -504,7 +504,7 @@ void Universe::genesis(TRAPS) { // Since some of the old system object arrays have been converted to // ordinary object arrays, _objectArrayKlass will be loaded when // SystemDictionary::initialize(CHECK); is run. See the extra check - // for Object_klass_loaded in objArrayKlassKlass::allocate_objArray_klass_impl. + // for Object_klass_is_loaded in ObjArrayKlass::allocate_objArray_klass. { Klass* oak = vmClasses::Object_klass()->array_klass(CHECK); _objectArrayKlass = ObjArrayKlass::cast(oak); @@ -593,7 +593,7 @@ void Universe::fixup_mirrors(TRAPS) { // but we cannot do that for classes created before java.lang.Class is loaded. Here we simply // walk over permanent objects created so far (mostly classes) and fixup their mirrors. Note // that the number of objects allocated at this point is very small. - assert(vmClasses::Class_klass_loaded(), "java.lang.Class should be loaded"); + assert(vmClasses::Class_klass_is_loaded(), "java.lang.Class should be loaded"); HandleMark hm(THREAD); if (!CDSConfig::is_using_archive()) { diff --git a/src/hotspot/share/oops/constantPool.cpp b/src/hotspot/share/oops/constantPool.cpp index ddb24302d69..5d5c0548215 100644 --- a/src/hotspot/share/oops/constantPool.cpp +++ b/src/hotspot/share/oops/constantPool.cpp @@ -395,7 +395,7 @@ void ConstantPool::restore_unshareable_info(TRAPS) { // Only create the new resolved references array if it hasn't been attempted before if (resolved_references() != nullptr) return; - if (vmClasses::Object_klass_loaded()) { + if (vmClasses::Object_klass_is_loaded()) { ClassLoaderData* loader_data = pool_holder()->class_loader_data(); #if INCLUDE_CDS_JAVA_HEAP if (ArchiveHeapLoader::is_in_use() && diff --git a/src/hotspot/share/oops/instanceKlass.cpp b/src/hotspot/share/oops/instanceKlass.cpp index 9d6cb951aeb..e13f4849454 100644 --- a/src/hotspot/share/oops/instanceKlass.cpp +++ b/src/hotspot/share/oops/instanceKlass.cpp @@ -159,7 +159,7 @@ static inline bool is_class_loader(const Symbol* class_name, return true; } - if (vmClasses::ClassLoader_klass_loaded()) { + if (vmClasses::ClassLoader_klass_is_loaded()) { const Klass* const super_klass = parser.super_klass(); if (super_klass != nullptr) { if (super_klass->is_subtype_of(vmClasses::ClassLoader_klass())) { diff --git a/src/hotspot/share/oops/objArrayKlass.cpp b/src/hotspot/share/oops/objArrayKlass.cpp index 48d56ddc540..ba2bc7f889f 100644 --- a/src/hotspot/share/oops/objArrayKlass.cpp +++ b/src/hotspot/share/oops/objArrayKlass.cpp @@ -78,7 +78,7 @@ ObjArrayKlass* ObjArrayKlass::allocate_objArray_klass(ClassLoaderData* loader_da // Eagerly allocate the direct array supertype. Klass* super_klass = nullptr; - if (!Universe::is_bootstrapping() || vmClasses::Object_klass_loaded()) { + if (!Universe::is_bootstrapping() || vmClasses::Object_klass_is_loaded()) { assert(MultiArray_lock->holds_lock(THREAD), "must hold lock after bootstrapping"); Klass* element_super = element_klass->super(); if (element_super != nullptr) { From bdf6853cfdd24176bdddb59b6d7bb85036b94c57 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Fri, 26 Sep 2025 16:50:05 +0000 Subject: [PATCH 261/556] 8368328: CompactNumberFormat.clone does not produce independent instances Reviewed-by: rgiulietti, jlu --- .../java/text/CompactNumberFormat.java | 27 ++++-- .../Format/CompactNumberFormat/TestClone.java | 95 +++++++++++++++++++ 2 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 test/jdk/java/text/Format/CompactNumberFormat/TestClone.java diff --git a/src/java.base/share/classes/java/text/CompactNumberFormat.java b/src/java.base/share/classes/java/text/CompactNumberFormat.java index 7163b2dd63b..74c1eabe970 100644 --- a/src/java.base/share/classes/java/text/CompactNumberFormat.java +++ b/src/java.base/share/classes/java/text/CompactNumberFormat.java @@ -250,38 +250,43 @@ public final class CompactNumberFormat extends NumberFormat { /** * List of positive prefix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List positivePrefixPatterns; /** * List of negative prefix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List negativePrefixPatterns; /** * List of positive suffix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List positiveSuffixPatterns; /** * List of negative suffix patterns of this formatter's - * compact number patterns. + * compact number patterns. This field is a read-only + * constant once initialized. */ private transient List negativeSuffixPatterns; /** * List of divisors of this formatter's compact number patterns. * Divisor can be either Long or BigInteger (if the divisor value goes - * beyond long boundary) + * beyond long boundary). This field is a read-only constant + * once initialized. */ private transient List divisors; /** * List of place holders that represent minimum integer digits at each index - * for each count. + * for each count. This field is a read-only constant once initialized. */ private transient List placeHolderPatterns; @@ -374,7 +379,7 @@ public final class CompactNumberFormat extends NumberFormat { /** * The map for plural rules that maps LDML defined tags (e.g. "one") to - * its rule. + * its rule. This field is a read-only constant once initialized. */ private transient Map rulesMap; @@ -1515,7 +1520,7 @@ public final class CompactNumberFormat extends NumberFormat { } } - private final transient DigitList digitList = new DigitList(); + private transient DigitList digitList = new DigitList(); private static final int STATUS_INFINITE = 0; private static final int STATUS_POSITIVE = 1; private static final int STATUS_LENGTH = 2; @@ -2506,8 +2511,14 @@ public final class CompactNumberFormat extends NumberFormat { @Override public CompactNumberFormat clone() { CompactNumberFormat other = (CompactNumberFormat) super.clone(); + + // Cloning reference fields. Other fields (e.g., "positivePrefixPatterns") + // are not cloned since they are read-only constants after initialization. other.compactPatterns = compactPatterns.clone(); other.symbols = (DecimalFormatSymbols) symbols.clone(); + other.decimalFormat = (DecimalFormat) decimalFormat.clone(); + other.defaultDecimalFormat = (DecimalFormat) defaultDecimalFormat.clone(); + other.digitList = (DigitList) digitList.clone(); return other; } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestClone.java b/test/jdk/java/text/Format/CompactNumberFormat/TestClone.java new file mode 100644 index 00000000000..42abc1be2ba --- /dev/null +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestClone.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +/* + * @test + * @bug 8368328 + * @summary Tests if CompactNumberFormat.clone() creates an independent object + * @run junit/othervm --add-opens java.base/java.text=ALL-UNNAMED TestClone + */ + +import java.lang.invoke.MethodHandles; +import java.text.CompactNumberFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Locale; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; + +public class TestClone { + // Concurrently parse numbers using cloned instances as originally + // reported in the bug. This test could produce false negative results, + // depending on the testing environment + @Test + void randomAccessTest() { + var original = + NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); + var threads = IntStream.range(0, 10) + .mapToObj(num -> new Thread(() -> { + var clone = (NumberFormat) original.clone(); + for (int i = 0; i < 1000; i++) { + assertDoesNotThrow(() -> + assertEquals(num, clone.parse(String.valueOf(num)).intValue())); + } + })).toList(); + threads.forEach(Thread::start); + threads.forEach(t -> { + try { + t.join(); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + }); + } + + private static Stream referenceFields() throws ClassNotFoundException { + return Stream.of( + Arguments.of("compactPatterns", String[].class), + Arguments.of("symbols", DecimalFormatSymbols.class), + Arguments.of("decimalFormat", DecimalFormat.class), + Arguments.of("defaultDecimalFormat", DecimalFormat.class), + Arguments.of("digitList", Class.forName("java.text.DigitList")) + ); + } + // Explicitly checks if the cloned object has its own references for + // "compactPatterns", "symbols", "decimalFormat", "defaultDecimalFormat", + // and "digitList" + @ParameterizedTest + @MethodSource("referenceFields") + void whiteBoxTest(String fieldName, Class type) throws Throwable { + var original = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); + var clone = original.clone(); + var lookup = MethodHandles.privateLookupIn(CompactNumberFormat.class, MethodHandles.lookup()); + + assertNotSame(lookup.findGetter(CompactNumberFormat.class, fieldName, type).invoke(original), + lookup.findGetter(CompactNumberFormat.class, fieldName, type).invoke(clone)); + } +} From 556dfddac82f69b8a3d3730d05fcd00e49b84f2e Mon Sep 17 00:00:00 2001 From: Leonid Mesnik Date: Fri, 26 Sep 2025 19:34:04 +0000 Subject: [PATCH 262/556] 8308027: GetThreadListStackTraces/OneGetThreadListStackTraces.java should be skipped when thread factory is used Reviewed-by: dholmes, sspitsyn --- test/hotspot/jtreg/ProblemList-Virtual.txt | 1 - .../GetThreadListStackTraces/OneGetThreadListStackTraces.java | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/ProblemList-Virtual.txt b/test/hotspot/jtreg/ProblemList-Virtual.txt index 4216eddb885..3c74d7bf816 100644 --- a/test/hotspot/jtreg/ProblemList-Virtual.txt +++ b/test/hotspot/jtreg/ProblemList-Virtual.txt @@ -25,7 +25,6 @@ # Bugs serviceability/AsyncGetCallTrace/MyPackage/ASGCTBaseTest.java 8308026 generic-all -serviceability/jvmti/GetThreadListStackTraces/OneGetThreadListStackTraces.java 8308027 generic-all serviceability/jvmti/Heap/IterateHeapWithEscapeAnalysisEnabled.java 8264699 generic-all vmTestbase/vm/mlvm/indy/func/jvmti/mergeCP_indy2manyDiff_a/TestDescription.java 8308367 generic-all diff --git a/test/hotspot/jtreg/serviceability/jvmti/GetThreadListStackTraces/OneGetThreadListStackTraces.java b/test/hotspot/jtreg/serviceability/jvmti/GetThreadListStackTraces/OneGetThreadListStackTraces.java index 349824af9ff..20012b70186 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/GetThreadListStackTraces/OneGetThreadListStackTraces.java +++ b/test/hotspot/jtreg/serviceability/jvmti/GetThreadListStackTraces/OneGetThreadListStackTraces.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, NTT DATA. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -27,6 +27,7 @@ * @bug 8242428 * @summary Verifies JVMTI GetThreadListStackTraces API with thread_count = 1 * @requires vm.jvmti + * @requires test.thread.factory == null * @library /test/lib * @run main/othervm/native -agentlib:OneGetThreadListStackTraces OneGetThreadListStackTraces * From 62cc347242ddbc8b51f023c288d78785b128e421 Mon Sep 17 00:00:00 2001 From: Leonid Mesnik Date: Fri, 26 Sep 2025 19:36:00 +0000 Subject: [PATCH 263/556] 8368699: nsk/jvmti/scenarios/events/EM04/em04t001/em04t001.cpp destroys jvmti monitor when VM is dead Reviewed-by: sspitsyn --- .../scenarios/events/EM04/em04t001/em04t001.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/events/EM04/em04t001/em04t001.cpp b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/events/EM04/em04t001/em04t001.cpp index a7244db3441..a8d16a1ecae 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/events/EM04/em04t001/em04t001.cpp +++ b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/events/EM04/em04t001/em04t001.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -118,6 +118,13 @@ cbDynamicCodeGenerated2(jvmtiEnv *jvmti_env, const char *name, } +void JNICALL +cbVMDeath(jvmtiEnv* jvmti, JNIEnv* jni) { + if (!NSK_JVMTI_VERIFY(jvmti->DestroyRawMonitor(syncLock))) { + nsk_jvmti_setFailStatus(); + } +} + /* ============================================================================= */ static int @@ -138,6 +145,7 @@ int setCallBacks(int stage) { eventCallbacks.DynamicCodeGenerated = (stage == 1) ? cbDynamicCodeGenerated1 : cbDynamicCodeGenerated2; + eventCallbacks.VMDeath = &cbVMDeath; if (!NSK_JVMTI_VERIFY(jvmti->SetEventCallbacks(&eventCallbacks, sizeof(eventCallbacks)))) return NSK_FALSE; @@ -256,9 +264,6 @@ Agent_OnUnload(JavaVM *jvm) nsk_jvmti_setFailStatus(); } - if (!NSK_JVMTI_VERIFY(jvmti->DestroyRawMonitor(syncLock))) { - nsk_jvmti_setFailStatus(); - } } } From c6cecc581f331dc61af0df2dfd5d7e0d523f6b61 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Fri, 26 Sep 2025 19:51:04 +0000 Subject: [PATCH 264/556] 8283198: Remove src/jdk.hotspot.agent/test Reviewed-by: amenkov, ayang, sspitsyn --- .../test/libproc/LibprocClient.java | 164 ------------------ .../test/libproc/LibprocTest.java | 41 ----- src/jdk.hotspot.agent/test/libproc/Makefile | 30 ---- src/jdk.hotspot.agent/test/libproc/README | 42 ----- .../test/libproc/libproctest.sh | 66 ------- .../test/libproc/libproctest64.sh | 65 ------- 6 files changed, 408 deletions(-) delete mode 100644 src/jdk.hotspot.agent/test/libproc/LibprocClient.java delete mode 100644 src/jdk.hotspot.agent/test/libproc/LibprocTest.java delete mode 100644 src/jdk.hotspot.agent/test/libproc/Makefile delete mode 100644 src/jdk.hotspot.agent/test/libproc/README delete mode 100644 src/jdk.hotspot.agent/test/libproc/libproctest.sh delete mode 100644 src/jdk.hotspot.agent/test/libproc/libproctest64.sh diff --git a/src/jdk.hotspot.agent/test/libproc/LibprocClient.java b/src/jdk.hotspot.agent/test/libproc/LibprocClient.java deleted file mode 100644 index 751c23ee92f..00000000000 --- a/src/jdk.hotspot.agent/test/libproc/LibprocClient.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -import sun.jvm.hotspot.oops.*; -import sun.jvm.hotspot.runtime.*; -import sun.jvm.hotspot.tools.*; -import sun.jvm.hotspot.utilities.*; - -/** - We don't run any of the "standard" SA command line tools for sanity - check. This is because the standard tools print addresses in hex - which could change legally. Also, textual comparison of output may - not match because of other reasons as well. This tool checks - validity of threads and frames logically. This class has reference - frame names from "known" threads. The debuggee is assumed to run - "LibprocTest.java". -*/ - -public class LibprocClient extends Tool { - - public void run() { - // try to get VM version and check - String version = VM.getVM().getVMRelease(); - Assert.that(version.startsWith("1.5"), "1.5 expected"); - - // try getting threads - Threads threads = VM.getVM().getThreads(); - boolean mainTested = false; - - // check frames of each thread - for (JavaThread cur = threads.first(); cur != null; cur = cur.next()) { - if (cur.isJavaThread()) { - String name = cur.getThreadName(); - // testing of basic frame walking for all threads - for (JavaVFrame vf = getLastJavaVFrame(cur); vf != null; vf = vf.javaSender()) { - checkFrame(vf); - } - - // special testing for "known" threads. For now, only "main" thread. - if (name.equals("main")) { - checkMainThread(cur); - mainTested = true; - } - } - } - Assert.that(mainTested, "main thread missing"); - } - - public static void main(String[] args) { - try { - LibprocClient lc = new LibprocClient(); - lc.start(args); - lc.getAgent().detach(); - System.out.println("\nPASSED\n"); - } catch (Exception exp) { - System.out.println("\nFAILED\n"); - exp.printStackTrace(); - } - } - - // -- Internals only below this point - private static JavaVFrame getLastJavaVFrame(JavaThread cur) { - RegisterMap regMap = cur.newRegisterMap(true); - Frame f = cur.getCurrentFrameGuess(); - if (f == null) { - System.err.println(" (Unable to get a top most frame)"); - return null; - } - VFrame vf = VFrame.newVFrame(f, regMap, cur, true, true); - if (vf == null) { - System.err.println(" (Unable to create vframe for topmost frame guess)"); - return null; - } - if (vf.isJavaFrame()) { - return (JavaVFrame) vf; - } - return (JavaVFrame) vf.javaSender(); - } - - private void checkMethodSignature(Symbol sig) { - SignatureIterator itr = new SignatureIterator(sig) { - public void doBool () {} - public void doChar () {} - public void doFloat () {} - public void doDouble() {} - public void doByte () {} - public void doShort () {} - public void doInt () {} - public void doLong () {} - public void doVoid () {} - public void doObject(int begin, int end) {} - public void doArray (int begin, int end) {} - }; - // this will throw RuntimeException for any invalid item in signature. - itr.iterate(); - } - - private void checkBCI(Method m, int bci) { - if (! m.isNative()) { - byte[] buf = m.getByteCode(); - Assert.that(bci >= 0 && bci < buf.length, "invalid bci, not in code range"); - if (m.hasLineNumberTable()) { - int lineNum = m.getLineNumberFromBCI(bci); - Assert.that(lineNum >= 0, "expecting non-negative line number"); - } - } - } - - private void checkMethodHolder(Method method) { - Klass klass = method.getMethodHolder(); - Assert.that(klass != null, "expecting non-null instance klass"); - } - - private void checkFrame(JavaVFrame vf) { - Method method = vf.getMethod(); - Assert.that(method != null, "expecting a non-null method here"); - Assert.that(method.getName() != null, "expecting non-null method name"); - checkMethodHolder(method); - checkMethodSignature(method.getSignature()); - checkBCI(method, vf.getBCI()); - } - - // from the test case LibprocTest.java - in the main thread we - // should see frames as below - private static String[] mainThreadMethods = new String[] { - "java.lang.Object.wait(long)", - "java.lang.Object.wait()", - "LibprocTest.main(java.lang.String[])" - }; - - private void checkMainThread(JavaThread thread) { - checkFrames(thread, mainThreadMethods); - } - - private void checkFrames(JavaThread thread, String[] expectedMethodNames) { - int i = 0; - for (JavaVFrame vf = getLastJavaVFrame(thread); vf != null; vf = vf.javaSender(), i++) { - Method m = vf.getMethod(); - Assert.that(m.externalNameAndSignature().equals(expectedMethodNames[i]), - "expected frame missing"); - } - } -} diff --git a/src/jdk.hotspot.agent/test/libproc/LibprocTest.java b/src/jdk.hotspot.agent/test/libproc/LibprocTest.java deleted file mode 100644 index a3e27a94ec0..00000000000 --- a/src/jdk.hotspot.agent/test/libproc/LibprocTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -/** - This is test case run by debuggee for running LibprocClient.java. -*/ - -public class LibprocTest { - public static void main(String[] args) throws Exception { - String myStr = ""; - System.out.println("main start"); - synchronized(myStr) { - try { - myStr.wait(); - } catch (InterruptedException ee) { - } - } - System.out.println("main end"); - } -} diff --git a/src/jdk.hotspot.agent/test/libproc/Makefile b/src/jdk.hotspot.agent/test/libproc/Makefile deleted file mode 100644 index c7b171bc633..00000000000 --- a/src/jdk.hotspot.agent/test/libproc/Makefile +++ /dev/null @@ -1,30 +0,0 @@ -# -# Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# -# - -all: - javac LibprocTest.java - javac -classpath ../../build/classes LibprocClient.java - -clean: - rm -rf *.class diff --git a/src/jdk.hotspot.agent/test/libproc/README b/src/jdk.hotspot.agent/test/libproc/README deleted file mode 100644 index 928c43d3fe7..00000000000 --- a/src/jdk.hotspot.agent/test/libproc/README +++ /dev/null @@ -1,42 +0,0 @@ -# -# Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# -# - -After making any changes to libproc.so, the shell scripts described -below can be run to verify that it does not break Java Serviceability -Agent. - -Setup: - -You need to have jdk 1.5 installed to run this test. Set environment -variable SA_JAVA to point to the java executable of jdk -1.5. Otherwise, the script picks-up 'java' from PATH. - -Running the tests: - -run libproctest.sh (32-bit debuggee) and libproctest64.sh (64-bit -debuggee) - -Interpreting result: - -"PASSED" or "FAILED" is printed in standard output. diff --git a/src/jdk.hotspot.agent/test/libproc/libproctest.sh b/src/jdk.hotspot.agent/test/libproc/libproctest.sh deleted file mode 100644 index 0ed1d1a4cf4..00000000000 --- a/src/jdk.hotspot.agent/test/libproc/libproctest.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/ksh - -# -# Copyright (c) 2003, 2020, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# -# - -# This script is used to run consistency check of Serviceabilty Agent -# after making any libproc.so changes. Prints "PASSED" or "FAILED" in -# standard output. - -usage() { - echo "usage: $0" - echo " set SA_JAVA to be java executable from JDK 1.5" - exit 1 -} - -STARTDIR=`dirname $0` - -if [ "$1" == "-help" ]; then - usage -fi - -if [ "x$SA_JAVA" = "x" ]; then - SA_JAVA=java -fi - -# create java process with test case -tmp=/tmp/libproctest -rm -f $tmp -$SA_JAVA -classpath $STARTDIR LibprocTest > $tmp & -pid=$! -while [ ! -s $tmp ] ; do - # Kludge alert! - sleep 2 -done - -# dump core -gcore $pid -kill -9 $pid - - -# run libproc client -$SA_JAVA -showversion -cp $STARTDIR/../../build/classes::$STARTDIR/../sa.jar:$STARTDIR LibprocClient x core.$pid - -# delete core -rm -f core.$pid diff --git a/src/jdk.hotspot.agent/test/libproc/libproctest64.sh b/src/jdk.hotspot.agent/test/libproc/libproctest64.sh deleted file mode 100644 index 7791190743b..00000000000 --- a/src/jdk.hotspot.agent/test/libproc/libproctest64.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/ksh - -# -# Copyright (c) 2003, 2020, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. -# -# This code is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# -# - -# This script is used to run consistency check of Serviceabilty Agent -# after making any libproc.so changes. Prints "PASSED" or "FAILED" in -# standard output. - -usage() { - echo "usage: $0" - echo " set SA_JAVA to be the java executable from JDK 1.5" - exit 1 -} - -if [ "$1" == "-help" ]; then - usage -fi - -if [ "x$SA_JAVA" = "x" ]; then - SA_JAVA=java -fi - -STARTDIR=`dirname $0` - -# create java process with test case -tmp=/tmp/libproctest -rm -f $tmp -$SA_JAVA -d64 -classpath $STARTDIR LibprocTest > $tmp & -pid=$! -while [ ! -s $tmp ] ; do - # Kludge alert! - sleep 2 -done - -# dump core -gcore $pid -kill -9 $pid - -# run libproc client -$SA_JAVA -d64 -showversion -cp $STARTDIR/../../build/classes::$STARTDIR/../sa.jar:$STARTDIR LibprocClient x core.$pid - -# delete core -rm -f core.$pid From 12c0f29b97f0ccd03dee6850a3a9a7117124016e Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Fri, 26 Sep 2025 20:12:48 +0000 Subject: [PATCH 265/556] 8368498: Use JUnit instead of TestNG for jdk_text tests Reviewed-by: naoto --- .../text/Collator/RuleBasedCollatorTest.java | 95 +++++++------- .../CompactFormatAndParseHelper.java | 11 +- .../CompactNumberFormat/TestCNFRounding.java | 55 ++++---- .../TestCompactNumber.java | 88 +++++++------ .../TestCompactPatternsValidity.java | 50 ++++---- .../CompactNumberFormat/TestEquality.java | 15 ++- .../TestFormatToCharacterIterator.java | 34 +++-- .../TestMutatingInstance.java | 31 +++-- .../TestParseBigDecimal.java | 23 ++-- .../CompactNumberFormat/TestPlurals.java | 55 ++++---- .../TestSpecialValues.java | 25 ++-- .../TestUExtensionOverride.java | 25 ++-- .../serialization/TestDeserializeCNF.java | 23 ++-- .../serialization/TestSerialization.java | 24 ++-- .../text/Format/DateFormat/Bug8193444.java | 22 ++-- .../DateFormat/CaseInsensitiveParseTest.java | 32 +++-- .../Format/DateFormat/LocaleDateFormats.java | 24 ++-- .../SimpleDateFormatPatternTest.java | 120 ++++++++++-------- .../DecimalFormat/SetGroupingSizeTest.java | 38 +++--- .../NumberFormat/DFSMinusPerCentMill.java | 53 ++++---- .../Normalizer/SquareEraCharacterTest.java | 24 ++-- 21 files changed, 490 insertions(+), 377 deletions(-) diff --git a/test/jdk/java/text/Collator/RuleBasedCollatorTest.java b/test/jdk/java/text/Collator/RuleBasedCollatorTest.java index 429768b6bf1..cd1e8ca69c0 100644 --- a/test/jdk/java/text/Collator/RuleBasedCollatorTest.java +++ b/test/jdk/java/text/Collator/RuleBasedCollatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,40 +26,45 @@ * @bug 4406815 8222969 8266784 * @summary RuleBasedCollatorTest uses very limited but selected test data * to test basic functionalities provided by RuleBasedCollator. - * @run testng/othervm RuleBasedCollatorTest + * @run junit/othervm RuleBasedCollatorTest */ +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.CollationElementIterator; import java.text.CollationKey; -import java.text.RuleBasedCollator; import java.text.Collator; import java.text.ParseException; +import java.text.RuleBasedCollator; import java.util.Arrays; import java.util.Locale; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; -import org.testng.SkipException; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class RuleBasedCollatorTest { - static RuleBasedCollator USC; - static String US_RULES; + private static RuleBasedCollator USC; + private static String US_RULES; - @BeforeClass - public void setup() { + @BeforeAll + void setup() { Collator c = Collator.getInstance(Locale.US); - if (!(c instanceof RuleBasedCollator)) { - throw new SkipException("skip tests."); - } + assumeFalse(!(c instanceof RuleBasedCollator), "skip tests."); USC = (RuleBasedCollator) c; US_RULES = USC.getRules(); } - @DataProvider(name = "rulesData") Object[][] rulesData() { //Basic Tailor String BASIC_TAILOR_RULES = "< b=c<\u00e6;A,a"; @@ -91,15 +96,15 @@ public class RuleBasedCollatorTest { }; } - @Test(dataProvider = "rulesData") - public void testRules(String rules, String[] testData, String[] expected) + @ParameterizedTest + @MethodSource("rulesData") + void testRules(String rules, String[] testData, String[] expected) throws ParseException { Arrays.sort(testData, new RuleBasedCollator(rules)); - assertEquals(testData, expected); + assertArrayEquals(expected, testData); } - @DataProvider(name = "FrenchSecondarySort") Object[][] FrenchSecondarySort() { return new Object[][] { { "\u0061\u00e1\u0061", "\u00e1\u0061\u0061", 1 }, @@ -111,8 +116,9 @@ public class RuleBasedCollatorTest { { "a", "\u1ea1", -1 } }; } - @Test(dataProvider = "FrenchSecondarySort") - public void testFrenchSecondarySort(String sData, String tData, + @ParameterizedTest + @MethodSource("FrenchSecondarySort") + void testFrenchSecondarySort(String sData, String tData, int expected) throws ParseException { String french_rule = "@"; String rules = US_RULES + french_rule; @@ -121,7 +127,6 @@ public class RuleBasedCollatorTest { assertEquals(expected, result); } - @DataProvider(name = "ThaiLaoVowelConsonantSwapping") Object[][] ThaiLaoVowelConsonantSwapping() { return new Object[][] {{"\u0e44\u0e01", "\u0e40\u0e2e", -1},//swap {"\u0e2e\u0e40", "\u0e01\u0e44", 1},//no swap @@ -129,8 +134,9 @@ public class RuleBasedCollatorTest { }; } - @Test(dataProvider = "ThaiLaoVowelConsonantSwapping") - public void testThaiLaoVowelConsonantSwapping(String sData, String tData, + @ParameterizedTest + @MethodSource("ThaiLaoVowelConsonantSwapping") + void testThaiLaoVowelConsonantSwapping(String sData, String tData, int expected) throws ParseException { String thai_rule = "& Z < \u0e01 < \u0e2e <\u0e40 < \u0e44!"; String rules = US_RULES + thai_rule; @@ -140,16 +146,15 @@ public class RuleBasedCollatorTest { } @Test - public void testIgnorableCharacter() throws ParseException { + void testIgnorableCharacter() throws ParseException { String rule = "=f new RuleBasedCollator(rule)); } - @Test(expectedExceptions = NullPointerException.class) - public void testNullParseException() throws ParseException{ - new RuleBasedCollator(null); + @Test + void testNullParseException() { + assertThrows(NullPointerException.class, () -> new RuleBasedCollator(null)); } } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/CompactFormatAndParseHelper.java b/test/jdk/java/text/Format/CompactNumberFormat/CompactFormatAndParseHelper.java index 3f3f1932e51..4d410f97b5d 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/CompactFormatAndParseHelper.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/CompactFormatAndParseHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,14 +24,15 @@ import java.text.NumberFormat; import java.text.ParseException; import java.text.ParsePosition; -import static org.testng.Assert.assertEquals; + +import static org.junit.jupiter.api.Assertions.assertEquals; class CompactFormatAndParseHelper { static void testFormat(NumberFormat cnf, Object number, String expected) { String result = cnf.format(number); - assertEquals(result, expected, "Incorrect formatting of the number '" + assertEquals(expected, result, "Incorrect formatting of the number '" + number + "'"); } @@ -46,11 +47,11 @@ class CompactFormatAndParseHelper { } if (returnType != null) { - assertEquals(number.getClass(), returnType, + assertEquals(returnType, number.getClass(), "Incorrect return type for string '" + parseString + "'"); } - assertEquals(number, expected, "Incorrect parsing of the string '" + assertEquals(expected, number, "Incorrect parsing of the string '" + parseString + "'"); } } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestCNFRounding.java b/test/jdk/java/text/Format/CompactNumberFormat/TestCNFRounding.java index b4dddfa1df7..0ba9587a344 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestCNFRounding.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestCNFRounding.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,21 +20,28 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 * @summary Checks the rounding of formatted number in compact number formatting - * @run testng/othervm TestCNFRounding + * @run junit/othervm TestCNFRounding */ +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.math.RoundingMode; import java.text.NumberFormat; import java.util.List; import java.util.Locale; -import static org.testng.Assert.*; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestCNFRounding { private static final List MODES = List.of( @@ -46,7 +53,6 @@ public class TestCNFRounding { RoundingMode.CEILING, RoundingMode.FLOOR); - @DataProvider(name = "roundingData") Object[][] roundingData() { return new Object[][]{ // Number, half_even, half_up, half_down, up, down, ceiling, floor @@ -70,7 +76,6 @@ public class TestCNFRounding { {-4500, new String[]{"-4K", "-5K", "-4K", "-5K", "-4K", "-4K", "-5K"}},}; } - @DataProvider(name = "roundingFract") Object[][] roundingFract() { return new Object[][]{ // Number, half_even, half_up, half_down, up, down, ceiling, floor @@ -94,7 +99,6 @@ public class TestCNFRounding { {-4500, new String[]{"-4.5K", "-4.5K", "-4.5K", "-4.5K", "-4.5K", "-4.5K", "-4.5K"}},}; } - @DataProvider(name = "rounding2Fract") Object[][] rounding2Fract() { return new Object[][]{ // Number, half_even, half_up, half_down @@ -118,37 +122,42 @@ public class TestCNFRounding { {4686, new String[]{"4.69K", "4.69K", "4.69K"}},}; } - @Test(expectedExceptions = NullPointerException.class) - public void testNullMode() { - NumberFormat fmt = NumberFormat - .getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); - fmt.setRoundingMode(null); + @Test + void testNullMode() { + assertThrows(NullPointerException.class, () -> { + NumberFormat fmt = NumberFormat + .getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); + fmt.setRoundingMode(null); + }); } @Test - public void testDefaultRoundingMode() { + void testDefaultRoundingMode() { NumberFormat fmt = NumberFormat .getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); - assertEquals(fmt.getRoundingMode(), RoundingMode.HALF_EVEN, + assertEquals(RoundingMode.HALF_EVEN, fmt.getRoundingMode(), "Default RoundingMode should be " + RoundingMode.HALF_EVEN); } - @Test(dataProvider = "roundingData") - public void testRounding(Object number, String[] expected) { + @ParameterizedTest + @MethodSource("roundingData") + void testRounding(Object number, String[] expected) { for (int index = 0; index < MODES.size(); index++) { testRoundingMode(number, expected[index], 0, MODES.get(index)); } } - @Test(dataProvider = "roundingFract") - public void testRoundingFract(Object number, String[] expected) { + @ParameterizedTest + @MethodSource("roundingFract") + void testRoundingFract(Object number, String[] expected) { for (int index = 0; index < MODES.size(); index++) { testRoundingMode(number, expected[index], 1, MODES.get(index)); } } - @Test(dataProvider = "rounding2Fract") - public void testRounding2Fract(Object number, String[] expected) { + @ParameterizedTest + @MethodSource("rounding2Fract") + void testRounding2Fract(Object number, String[] expected) { List rModes = List.of(RoundingMode.HALF_EVEN, RoundingMode.HALF_UP, RoundingMode.HALF_DOWN); for (int index = 0; index < rModes.size(); index++) { @@ -161,12 +170,12 @@ public class TestCNFRounding { NumberFormat fmt = NumberFormat .getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); fmt.setRoundingMode(rounding); - assertEquals(fmt.getRoundingMode(), rounding, + assertEquals(rounding, fmt.getRoundingMode(), "RoundingMode set is not returned by getRoundingMode"); fmt.setMinimumFractionDigits(fraction); String result = fmt.format(number); - assertEquals(result, expected, "Incorrect formatting of number " + assertEquals(expected, result, "Incorrect formatting of number " + number + " using rounding mode: " + rounding); } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java b/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java index e9972f62f3e..b81226c00db 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestCompactNumber.java @@ -25,8 +25,14 @@ * @bug 8177552 8217721 8222756 8295372 8306116 8319990 8338690 8363972 * @summary Checks the functioning of compact number format * @modules jdk.localedata - * @run testng/othervm TestCompactNumber + * @run junit/othervm TestCompactNumber */ + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.math.BigDecimal; import java.math.BigInteger; import java.text.FieldPosition; @@ -36,10 +42,11 @@ import java.text.ParseException; import java.text.ParsePosition; import java.util.Locale; import java.util.stream.Stream; -import static org.testng.Assert.*; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestCompactNumber { private static final NumberFormat FORMAT_DZ_LONG = NumberFormat @@ -106,7 +113,6 @@ public class TestCompactNumber { FORMAT_PT_LONG_FD4.setMaximumFractionDigits(4); } - @DataProvider(name = "format") Object[][] compactFormatData() { return new Object[][]{ // compact number format instance, number to format, formatted output @@ -378,7 +384,6 @@ public class TestCompactNumber { }; } - @DataProvider(name = "parse") Object[][] compactParseData() { return new Object[][]{ // compact number format instance, string to parse, parsed number, return type @@ -491,7 +496,6 @@ public class TestCompactNumber { }; } - @DataProvider(name = "exceptionParse") Object[][] exceptionParseData() { return new Object[][]{ // compact number instance, string to parse, null (no o/p; must throw exception) @@ -508,7 +512,6 @@ public class TestCompactNumber { }; } - @DataProvider(name = "invalidParse") Object[][] invalidParseData() { return new Object[][]{ // compact number instance, string to parse, parsed number @@ -542,7 +545,6 @@ public class TestCompactNumber { }; } - @DataProvider(name = "fieldPosition") Object[][] formatFieldPositionData() { return new Object[][]{ //compact number instance, number to format, field, start position, end position, formatted string @@ -588,7 +590,6 @@ public class TestCompactNumber { {FORMAT_SE_SHORT, new BigDecimal("-48982865901234567890.98"), NumberFormat.Field.INTEGER, 1, 9, "\u221248982866\u00a0bn"},}; } - @DataProvider(name = "varParsePosition") Object[][] varParsePosition() { return new Object[][]{ // compact number instance, parse string, parsed number, @@ -616,73 +617,82 @@ public class TestCompactNumber { } @Test - public void testInstanceCreation() { + void testInstanceCreation() { Stream.of(NumberFormat.getAvailableLocales()).forEach(l -> NumberFormat .getCompactNumberInstance(l, NumberFormat.Style.SHORT).format(10000)); Stream.of(NumberFormat.getAvailableLocales()).forEach(l -> NumberFormat .getCompactNumberInstance(l, NumberFormat.Style.LONG).format(10000)); } - @Test(expectedExceptions = IllegalArgumentException.class) - public void testFormatWithNullParam() { - FORMAT_EN_US_SHORT.format(null); + @Test + void testFormatWithNullParam() { + assertThrows(IllegalArgumentException.class, () -> { + FORMAT_EN_US_SHORT.format(null); + }); } - @Test(dataProvider = "format") - public void testFormat(NumberFormat cnf, Object number, + @ParameterizedTest + @MethodSource("compactFormatData") + void testFormat(NumberFormat cnf, Object number, String expected) { CompactFormatAndParseHelper.testFormat(cnf, number, expected); } - @Test(dataProvider = "parse") - public void testParse(NumberFormat cnf, String parseString, + @ParameterizedTest + @MethodSource("compactParseData") + void testParse(NumberFormat cnf, String parseString, Number expected, Class returnType) throws ParseException { CompactFormatAndParseHelper.testParse(cnf, parseString, expected, null, returnType); } - @Test(dataProvider = "parse") - public void testParsePosition(NumberFormat cnf, String parseString, + @ParameterizedTest + @MethodSource("compactParseData") + void testParsePosition(NumberFormat cnf, String parseString, Number expected, Class returnType) throws ParseException { ParsePosition pos = new ParsePosition(0); CompactFormatAndParseHelper.testParse(cnf, parseString, expected, pos, returnType); - assertEquals(pos.getIndex(), parseString.length()); - assertEquals(pos.getErrorIndex(), -1); + assertEquals(parseString.length(), pos.getIndex()); + assertEquals(-1, pos.getErrorIndex()); } - @Test(dataProvider = "varParsePosition") - public void testVarParsePosition(NumberFormat cnf, String parseString, + @ParameterizedTest + @MethodSource("varParsePosition") + void testVarParsePosition(NumberFormat cnf, String parseString, Number expected, int startPosition, int indexPosition, int errPosition) throws ParseException { ParsePosition pos = new ParsePosition(startPosition); CompactFormatAndParseHelper.testParse(cnf, parseString, expected, pos, null); - assertEquals(pos.getIndex(), indexPosition); - assertEquals(pos.getErrorIndex(), errPosition); + assertEquals(indexPosition, pos.getIndex()); + assertEquals(errPosition, pos.getErrorIndex()); } - @Test(dataProvider = "exceptionParse", expectedExceptions = ParseException.class) - public void throwsParseException(NumberFormat cnf, String parseString, + @ParameterizedTest + @MethodSource("exceptionParseData") + void throwsParseException(NumberFormat cnf, String parseString, + Number expected) { + assertThrows(ParseException.class, () -> CompactFormatAndParseHelper.testParse(cnf, parseString, expected, null, null)); + } + + @ParameterizedTest + @MethodSource("invalidParseData") + void testInvalidParse(NumberFormat cnf, String parseString, Number expected) throws ParseException { CompactFormatAndParseHelper.testParse(cnf, parseString, expected, null, null); } - @Test(dataProvider = "invalidParse") - public void testInvalidParse(NumberFormat cnf, String parseString, - Number expected) throws ParseException { - CompactFormatAndParseHelper.testParse(cnf, parseString, expected, null, null); - } - - @Test(dataProvider = "fieldPosition") - public void testFormatWithFieldPosition(NumberFormat nf, + @ParameterizedTest + @MethodSource("formatFieldPositionData") + void testFormatWithFieldPosition(NumberFormat nf, Object number, Format.Field field, int posStartExpected, int posEndExpected, String expected) { FieldPosition pos = new FieldPosition(field); StringBuffer buf = new StringBuffer(); StringBuffer result = nf.format(number, buf, pos); - assertEquals(result.toString(), expected, "Incorrect formatting of the number '" + assertEquals(expected, result.toString(), "Incorrect formatting of the number '" + number + "'"); - assertEquals(pos.getBeginIndex(), posStartExpected, "Incorrect start position" + assertEquals(posStartExpected, pos.getBeginIndex(), "Incorrect start position" + " while formatting the number '" + number + "', for the field " + field); - assertEquals(pos.getEndIndex(), posEndExpected, "Incorrect end position" + assertEquals(posEndExpected, pos.getEndIndex(), "Incorrect end position" + " while formatting the number '" + number + "', for the field " + field); } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestCompactPatternsValidity.java b/test/jdk/java/text/Format/CompactNumberFormat/TestCompactPatternsValidity.java index 8476b568ba0..1f8f1041e9e 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestCompactPatternsValidity.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestCompactPatternsValidity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,9 +25,13 @@ * @bug 8177552 8217254 8251499 8281317 * @summary Checks the validity of compact number patterns specified through * CompactNumberFormat constructor - * @run testng/othervm TestCompactPatternsValidity + * @run junit/othervm TestCompactPatternsValidity */ +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.math.BigDecimal; import java.math.BigInteger; import java.text.CompactNumberFormat; @@ -35,9 +39,10 @@ import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.util.List; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestCompactPatternsValidity { // Max range 10^4 @@ -74,7 +79,6 @@ public class TestCompactPatternsValidity { private static final String[] COMPACT_PATTERN14 = new String[]{"", "", "", "{one:Kun other:0' 'Kun}"}; // from Somali in CLDR 38 - @DataProvider(name = "invalidPatterns") Object[][] invalidCompactPatterns() { return new Object[][] { // compact patterns @@ -90,7 +94,6 @@ public class TestCompactPatternsValidity { }; } - @DataProvider(name = "validPatternsFormat") Object[][] validPatternsFormat() { return new Object[][] { // compact patterns, numbers, expected output @@ -116,7 +119,6 @@ public class TestCompactPatternsValidity { }; } - @DataProvider(name = "validPatternsParse") Object[][] validPatternsParse() { return new Object[][] { // compact patterns, parse string, expected output @@ -136,7 +138,6 @@ public class TestCompactPatternsValidity { }; } - @DataProvider(name = "validPatternsFormatWithPluralRules") Object[][] validPatternsFormatWithPluralRules() { return new Object[][] { // compact patterns, plural rules, numbers, expected output @@ -144,7 +145,6 @@ public class TestCompactPatternsValidity { }; } - @DataProvider(name = "validPatternsParseWithPluralRules") Object[][] validPatternsParseWithPluralRules() { return new Object[][] { // compact patterns, plural rules, parse string, expected output @@ -152,15 +152,18 @@ public class TestCompactPatternsValidity { }; } - @Test(dataProvider = "invalidPatterns", - expectedExceptions = IllegalArgumentException.class) - public void testInvalidCompactPatterns(String[] compactPatterns) { - new CompactNumberFormat("#,##0.0#", DecimalFormatSymbols - .getInstance(Locale.US), compactPatterns); + @ParameterizedTest + @MethodSource("invalidCompactPatterns") + void testInvalidCompactPatterns(String[] compactPatterns) { + assertThrows(IllegalArgumentException.class, () -> { + new CompactNumberFormat("#,##0.0#", DecimalFormatSymbols + .getInstance(Locale.US), compactPatterns); + }); } - @Test(dataProvider = "validPatternsFormat") - public void testValidPatternsFormat(String[] compactPatterns, + @ParameterizedTest + @MethodSource("validPatternsFormat") + void testValidPatternsFormat(String[] compactPatterns, List numbers, List expected) { CompactNumberFormat fmt = new CompactNumberFormat("#,##0.0#", DecimalFormatSymbols.getInstance(Locale.US), compactPatterns); @@ -170,8 +173,9 @@ public class TestCompactPatternsValidity { } } - @Test(dataProvider = "validPatternsParse") - public void testValidPatternsParse(String[] compactPatterns, + @ParameterizedTest + @MethodSource("validPatternsParse") + void testValidPatternsParse(String[] compactPatterns, List parseString, List numbers) throws ParseException { CompactNumberFormat fmt = new CompactNumberFormat("#,##0.0#", DecimalFormatSymbols.getInstance(Locale.US), compactPatterns); @@ -181,8 +185,9 @@ public class TestCompactPatternsValidity { } } - @Test(dataProvider = "validPatternsFormatWithPluralRules") - public void testValidPatternsFormatWithPluralRules(String[] compactPatterns, String pluralRules, + @ParameterizedTest + @MethodSource("validPatternsFormatWithPluralRules") + void testValidPatternsFormatWithPluralRules(String[] compactPatterns, String pluralRules, List numbers, List expected) { CompactNumberFormat fmt = new CompactNumberFormat("#,##0.0#", DecimalFormatSymbols.getInstance(Locale.US), compactPatterns, pluralRules); @@ -192,8 +197,9 @@ public class TestCompactPatternsValidity { } } - @Test(dataProvider = "validPatternsParseWithPluralRules") - public void testValidPatternsParsewithPluralRules(String[] compactPatterns, String pluralRules, + @ParameterizedTest + @MethodSource("validPatternsParseWithPluralRules") + void testValidPatternsParsewithPluralRules(String[] compactPatterns, String pluralRules, List parseString, List numbers) throws ParseException { CompactNumberFormat fmt = new CompactNumberFormat("#,##0.0#", DecimalFormatSymbols.getInstance(Locale.US), compactPatterns, pluralRules); diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestEquality.java b/test/jdk/java/text/Format/CompactNumberFormat/TestEquality.java index 584f8c2c9bd..2ced18be171 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestEquality.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestEquality.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,25 +20,26 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 8222756 8327640 * @summary Checks the equals and hashCode method of CompactNumberFormat * @modules jdk.localedata - * @run testng/othervm TestEquality - * + * @run junit/othervm TestEquality */ +import org.junit.jupiter.api.Test; + import java.text.CompactNumberFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.Locale; -import org.testng.annotations.Test; public class TestEquality { @Test - public void testEquality() { + void testEquality() { CompactNumberFormat cnf1 = (CompactNumberFormat) NumberFormat .getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); @@ -160,7 +161,7 @@ public class TestEquality { } @Test - public void testHashCode() { + void testHashCode() { NumberFormat cnf1 = NumberFormat .getCompactNumberInstance(Locale.JAPAN, NumberFormat.Style.SHORT); NumberFormat cnf2 = NumberFormat @@ -175,7 +176,7 @@ public class TestEquality { // Test the property of equals and hashCode i.e. two equal object must // always have the same hashCode @Test - public void testEqualsAndHashCode() { + void testEqualsAndHashCode() { NumberFormat cnf1 = NumberFormat .getCompactNumberInstance(Locale.of("hi", "IN"), NumberFormat.Style.SHORT); cnf1.setMinimumIntegerDigits(5); diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestFormatToCharacterIterator.java b/test/jdk/java/text/Format/CompactNumberFormat/TestFormatToCharacterIterator.java index fb18fa256ab..beb6c38aca8 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestFormatToCharacterIterator.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestFormatToCharacterIterator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,14 +20,20 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 * @summary Checks the functioning of * CompactNumberFormat.formatToCharacterIterator method * @modules jdk.localedata - * @run testng/othervm TestFormatToCharacterIterator + * @run junit/othervm TestFormatToCharacterIterator */ + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.math.BigDecimal; import java.math.BigInteger; import java.text.AttributedCharacterIterator; @@ -36,10 +42,10 @@ import java.text.Format; import java.text.NumberFormat; import java.util.Locale; import java.util.Set; -import static org.testng.Assert.assertEquals; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestFormatToCharacterIterator { private static final NumberFormat FORMAT_DZ = NumberFormat @@ -54,7 +60,6 @@ public class TestFormatToCharacterIterator { .getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG); - @DataProvider(name = "fieldPositions") Object[][] compactFieldPositionData() { return new Object[][]{ // compact format instance, number, resulted string, attributes/fields, attribute positions @@ -149,22 +154,23 @@ public class TestFormatToCharacterIterator { }; } - @Test(dataProvider = "fieldPositions") - public void testFormatToCharacterIterator(NumberFormat fmt, Object number, + @ParameterizedTest + @MethodSource("compactFieldPositionData") + void testFormatToCharacterIterator(NumberFormat fmt, Object number, String expected, Format.Field[] expectedFields, int[] positions) { AttributedCharacterIterator iterator = fmt.formatToCharacterIterator(number); - assertEquals(getText(iterator), expected, "Incorrect formatting of the number '" + assertEquals(expected, getText(iterator), "Incorrect formatting of the number '" + number + "'"); iterator.first(); // Check start and end index of the formatted string - assertEquals(iterator.getBeginIndex(), 0, "Incorrect start index: " + assertEquals(0, iterator.getBeginIndex(), "Incorrect start index: " + iterator.getBeginIndex() + " of the formatted string: " + expected); - assertEquals(iterator.getEndIndex(), expected.length(), "Incorrect end index: " + assertEquals(expected.length(), iterator.getEndIndex(), "Incorrect end index: " + iterator.getEndIndex() + " of the formatted string: " + expected); // Check the attributes returned by the formatToCharacterIterator - assertEquals(iterator.getAllAttributeKeys(), Set.of(expectedFields), + assertEquals(Set.of(expectedFields), iterator.getAllAttributeKeys(), "Attributes do not match while formatting number: " + number); // Check the begin and end index for attributes @@ -173,10 +179,10 @@ public class TestFormatToCharacterIterator { do { int start = iterator.getRunStart(); int end = iterator.getRunLimit(); - assertEquals(start, positions[currentPosition], + assertEquals(positions[currentPosition], start, "Incorrect start position for the attribute(s): " + iterator.getAttributes().keySet()); - assertEquals(end, positions[currentPosition + 1], + assertEquals(positions[currentPosition + 1], end, "Incorrect end position for the attribute(s): " + iterator.getAttributes().keySet()); currentPosition = currentPosition + 2; diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestMutatingInstance.java b/test/jdk/java/text/Format/CompactNumberFormat/TestMutatingInstance.java index 47b176692fd..4cf5ce12f16 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestMutatingInstance.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestMutatingInstance.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,6 +20,7 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 @@ -27,8 +28,14 @@ * formatting parameters. For example, min fraction digits, grouping * size etc. * @modules jdk.localedata - * @run testng/othervm TestMutatingInstance + * @run junit/othervm TestMutatingInstance */ + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.math.BigDecimal; import java.math.BigInteger; import java.text.CompactNumberFormat; @@ -36,10 +43,8 @@ import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestMutatingInstance { private static final NumberFormat FORMAT_FRACTION = NumberFormat @@ -61,8 +66,8 @@ public class TestMutatingInstance { "#,##0.0#", DecimalFormatSymbols.getInstance(Locale.US), new String[]{"", "", "", "", "00K", "", "", "", "", "", "", "", "", "", ""}); - @BeforeTest - public void mutateInstances() { + @BeforeAll + void mutateInstances() { FORMAT_FRACTION.setMinimumFractionDigits(2); FORMAT_GROUPING.setGroupingSize(3); FORMAT_GROUPING.setGroupingUsed(true); @@ -75,7 +80,6 @@ public class TestMutatingInstance { FORMAT_NO_PATTERNS.setMinimumFractionDigits(2); } - @DataProvider(name = "format") Object[][] compactFormatData() { return new Object[][]{ {FORMAT_FRACTION, 1900, "1.90 thousand"}, @@ -95,7 +99,6 @@ public class TestMutatingInstance { {FORMAT_NO_PATTERNS, new BigDecimal(12346567890987654.32), "12,346,567,890,987,654"},}; } - @DataProvider(name = "parse") Object[][] compactParseData() { return new Object[][]{ {FORMAT_FRACTION, "190 thousand", 190000L}, @@ -106,14 +109,16 @@ public class TestMutatingInstance { {FORMAT_PARSEINTONLY, "12.345 thousand", 12000L},}; } - @Test(dataProvider = "format") - public void formatCompactNumber(NumberFormat nf, + @ParameterizedTest + @MethodSource("compactFormatData") + void formatCompactNumber(NumberFormat nf, Object number, String expected) { CompactFormatAndParseHelper.testFormat(nf, number, expected); } - @Test(dataProvider = "parse") - public void parseCompactNumber(NumberFormat nf, + @ParameterizedTest + @MethodSource("compactParseData") + void parseCompactNumber(NumberFormat nf, String parseString, Number expected) throws ParseException { CompactFormatAndParseHelper.testParse(nf, parseString, expected, null, null); } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestParseBigDecimal.java b/test/jdk/java/text/Format/CompactNumberFormat/TestParseBigDecimal.java index dc88ce4de4a..422ced9d999 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestParseBigDecimal.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestParseBigDecimal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,17 +20,19 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 8306116 8319990 * @summary Checks CNF.parse() when parseBigDecimal is set to true * @modules jdk.localedata - * @run testng/othervm TestParseBigDecimal + * @run junit/othervm TestParseBigDecimal */ -import org.testng.annotations.BeforeTest; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.math.BigDecimal; import java.text.CompactNumberFormat; @@ -38,6 +40,7 @@ import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestParseBigDecimal { private static final CompactNumberFormat FORMAT_DZ_LONG = (CompactNumberFormat) NumberFormat @@ -64,8 +67,8 @@ public class TestParseBigDecimal { private static final CompactNumberFormat FORMAT_SE_SHORT = (CompactNumberFormat) NumberFormat .getCompactNumberInstance(Locale.of("se"), NumberFormat.Style.SHORT); - @BeforeTest - public void mutateInstances() { + @BeforeAll + void mutateInstances() { FORMAT_DZ_LONG.setParseBigDecimal(true); FORMAT_EN_US_SHORT.setParseBigDecimal(true); FORMAT_EN_LONG.setParseBigDecimal(true); @@ -76,7 +79,6 @@ public class TestParseBigDecimal { FORMAT_SE_SHORT.setParseBigDecimal(true); } - @DataProvider(name = "parse") Object[][] compactParseData() { return new Object[][]{ // compact number format instance, string to parse, parsed number @@ -165,8 +167,9 @@ public class TestParseBigDecimal { {FORMAT_SE_SHORT, "\u221212345679,89\u00a0bn", new BigDecimal("-12345679890000000000.00")},}; } - @Test(dataProvider = "parse") - public void testParse(NumberFormat cnf, String parseString, + @ParameterizedTest + @MethodSource("compactParseData") + void testParse(NumberFormat cnf, String parseString, Number expected) throws ParseException { CompactFormatAndParseHelper.testParse(cnf, parseString, expected, null, BigDecimal.class); } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java b/test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java index bbaaa701d23..b8beb6135dd 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestPlurals.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,21 +20,27 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8222756 * @summary Tests plurals support in CompactNumberFormat - * @run testng/othervm TestPlurals + * @run junit/othervm TestPlurals */ +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.CompactNumberFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; -import static org.testng.Assert.*; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestPlurals { private final static DecimalFormatSymbols DFS = DecimalFormatSymbols.getInstance(Locale.ROOT); @@ -45,7 +51,6 @@ public class TestPlurals { private final static String RULE_3 = "one:n%2=0andn/3=2;"; - @DataProvider Object[][] pluralRules() { return new Object[][]{ // rules, number, expected @@ -78,7 +83,6 @@ public class TestPlurals { }; } - @DataProvider Object[][] invalidRules() { return new Object [][] { {"one:a = 1"}, @@ -92,27 +96,34 @@ public class TestPlurals { }; } - @Test(expectedExceptions = NullPointerException.class) - public void testNullPluralRules() { - String[] pattern = {""}; - new CompactNumberFormat("#", DFS, PATTERN, null); + @Test + void testNullPluralRules() { + assertThrows(NullPointerException.class, () -> { + String[] pattern = {""}; + new CompactNumberFormat("#", DFS, PATTERN, null); + }); } - @Test(dataProvider = "pluralRules") - public void testPluralRules(String rules, Number n, String expected) { + @ParameterizedTest + @MethodSource("pluralRules") + void testPluralRules(String rules, Number n, String expected) { var cnp = new CompactNumberFormat("#", DFS, PATTERN, rules); - assertEquals(cnp.format(n), expected); + assertEquals(expected, cnp.format(n)); } - @Test(dataProvider = "invalidRules", expectedExceptions = IllegalArgumentException.class) - public void testInvalidRules(String rules) { - new CompactNumberFormat("#", DFS, PATTERN, rules); + @ParameterizedTest + @MethodSource("invalidRules") + void testInvalidRules(String rules) { + assertThrows(IllegalArgumentException.class, + () -> new CompactNumberFormat("#", DFS, PATTERN, rules)); } - @Test(expectedExceptions = IllegalArgumentException.class) - public void testLimitExceedingRules() { - String andCond = " and n = 1"; - String invalid = "one: n = 1" + andCond.repeat(2_048 / andCond.length()); - new CompactNumberFormat("#", DFS, PATTERN, invalid); + @Test + void testLimitExceedingRules() { + assertThrows(IllegalArgumentException.class, () -> { + String andCond = " and n = 1"; + String invalid = "one: n = 1" + andCond.repeat(2_048 / andCond.length()); + new CompactNumberFormat("#", DFS, PATTERN, invalid); + }); } } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestSpecialValues.java b/test/jdk/java/text/Format/CompactNumberFormat/TestSpecialValues.java index e8ac2489faf..9bd0ef73830 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestSpecialValues.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestSpecialValues.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,25 +20,29 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 * @summary Checks the formatting and parsing of special values * @modules jdk.localedata - * @run testng/othervm TestSpecialValues + * @run junit/othervm TestSpecialValues */ + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestSpecialValues { private static final NumberFormat FORMAT = NumberFormat .getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); - @DataProvider(name = "formatSpecialValues") Object[][] formatSpecialValues() { return new Object[][]{ // number , formatted ouput @@ -53,7 +57,6 @@ public class TestSpecialValues { {Long.MAX_VALUE, "9223372T"},}; } - @DataProvider(name = "parseSpecialValues") Object[][] parseSpecialValues() { return new Object[][]{ // parse string, parsed number @@ -65,13 +68,15 @@ public class TestSpecialValues { {"-\u221E", Double.NEGATIVE_INFINITY},}; } - @Test(dataProvider = "formatSpecialValues") - public void testFormatSpecialValues(Object number, String expected) { + @ParameterizedTest + @MethodSource("formatSpecialValues") + void testFormatSpecialValues(Object number, String expected) { CompactFormatAndParseHelper.testFormat(FORMAT, number, expected); } - @Test(dataProvider = "parseSpecialValues") - public void testParseSpecialValues(String parseString, Number expected) + @ParameterizedTest + @MethodSource("parseSpecialValues") + void testParseSpecialValues(String parseString, Number expected) throws ParseException { CompactFormatAndParseHelper.testParse(FORMAT, parseString, expected, null, null); } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/TestUExtensionOverride.java b/test/jdk/java/text/Format/CompactNumberFormat/TestUExtensionOverride.java index 099e6978b0b..f77908d84c4 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/TestUExtensionOverride.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/TestUExtensionOverride.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,23 +20,27 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 8221432 * @summary Checks the behaviour of Unicode BCP 47 U Extension with * compact number format * @modules jdk.localedata - * @run testng/othervm TestUExtensionOverride + * @run junit/othervm TestUExtensionOverride */ + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestUExtensionOverride { - @DataProvider(name = "compactFormatData") Object[][] compactFormatData() { return new Object[][]{ // locale, number, formatted string @@ -61,7 +65,6 @@ public class TestUExtensionOverride { "\u0967\u0968\u00a0k"},}; } - @DataProvider(name = "compactParseData") Object[][] compactParseData() { return new Object[][]{ // locale, parse string, parsed number @@ -87,16 +90,18 @@ public class TestUExtensionOverride { "\u0967\u0968\u00a0k", 12000L},}; } - @Test(dataProvider = "compactFormatData") - public void testFormat(Locale locale, double num, + @ParameterizedTest + @MethodSource("compactFormatData") + void testFormat(Locale locale, double num, String expected) { NumberFormat cnf = NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.SHORT); CompactFormatAndParseHelper.testFormat(cnf, num, expected); } - @Test(dataProvider = "compactParseData") - public void testParse(Locale locale, String parseString, + @ParameterizedTest + @MethodSource("compactParseData") + void testParse(Locale locale, String parseString, Number expected) throws ParseException { NumberFormat cnf = NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.SHORT); diff --git a/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestDeserializeCNF.java b/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestDeserializeCNF.java index 0b4710f0086..563ae307762 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestDeserializeCNF.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestDeserializeCNF.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,11 +28,12 @@ * @summary Checks deserialization of compact number format * @library /java/text/testlib * @build TestDeserializeCNF HexDumpReader - * @run testng/othervm TestDeserializeCNF + * @run junit/othervm TestDeserializeCNF */ -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import java.io.IOException; import java.io.InputStream; @@ -41,8 +42,10 @@ import java.math.RoundingMode; import java.text.CompactNumberFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestDeserializeCNF { // This object is serialized in cnf1.ser.txt with HALF_UP @@ -60,8 +63,8 @@ public class TestDeserializeCNF { private static final String FILE_COMPACT_FORMAT1 = "cnf1.ser.txt"; private static final String FILE_COMPACT_FORMAT2 = "cnf2.ser.txt"; - @BeforeTest - public void mutateInstances() { + @BeforeAll + void mutateInstances() { COMPACT_FORMAT1.setRoundingMode(RoundingMode.HALF_UP); COMPACT_FORMAT1.setGroupingSize(3); COMPACT_FORMAT1.setParseBigDecimal(true); @@ -71,18 +74,18 @@ public class TestDeserializeCNF { } @Test - public void testDeserialization() throws IOException, ClassNotFoundException { + void testDeserialization() throws IOException, ClassNotFoundException { try (InputStream istream1 = HexDumpReader.getStreamFromHexDump(FILE_COMPACT_FORMAT1); ObjectInputStream ois1 = new ObjectInputStream(istream1); InputStream istream2 = HexDumpReader.getStreamFromHexDump(FILE_COMPACT_FORMAT2); ObjectInputStream ois2 = new ObjectInputStream(istream2);) { CompactNumberFormat obj1 = (CompactNumberFormat) ois1.readObject(); - assertEquals(obj1, COMPACT_FORMAT1, "Deserialized instance is not" + assertEquals(COMPACT_FORMAT1, obj1, "Deserialized instance is not" + " equal to the instance serialized in " + FILE_COMPACT_FORMAT1); CompactNumberFormat obj2 = (CompactNumberFormat) ois2.readObject(); - assertEquals(obj2, COMPACT_FORMAT2, "Deserialized instance is not" + assertEquals(COMPACT_FORMAT2, obj2, "Deserialized instance is not" + " equal to the instance serialized in " + FILE_COMPACT_FORMAT2); } } diff --git a/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestSerialization.java b/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestSerialization.java index 574e6dc7905..b3a8cbadc76 100644 --- a/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestSerialization.java +++ b/test/jdk/java/text/Format/CompactNumberFormat/serialization/TestSerialization.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,16 +20,18 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8177552 8327640 * @modules jdk.localedata * @summary Checks the serialization feature of CompactNumberFormat - * @run testng/othervm TestSerialization + * @run junit/othervm TestSerialization */ -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -40,8 +42,10 @@ import java.math.RoundingMode; import java.text.CompactNumberFormat; import java.text.NumberFormat; import java.util.Locale; -import static org.testng.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TestSerialization { private static final NumberFormat FORMAT_HI = NumberFormat.getCompactNumberInstance( @@ -57,8 +61,8 @@ public class TestSerialization { private static final NumberFormat FORMAT_KO_KR = NumberFormat.getCompactNumberInstance( Locale.KOREA, NumberFormat.Style.SHORT); - @BeforeTest - public void mutateInstances() { + @BeforeAll + void mutateInstances() { FORMAT_HI.setMinimumFractionDigits(2); FORMAT_HI.setMinimumIntegerDigits(5); @@ -81,7 +85,7 @@ public class TestSerialization { } @Test - public void testSerialization() throws IOException, ClassNotFoundException { + void testSerialization() throws IOException, ClassNotFoundException { // Serialize serialize("cdf.ser", FORMAT_HI, FORMAT_EN_US, FORMAT_JA_JP, FORMAT_FR_FR, FORMAT_DE_DE, FORMAT_KO_KR); // Deserialize @@ -104,13 +108,13 @@ public class TestSerialization { new FileInputStream(fileName))) { for (NumberFormat fmt : formats) { NumberFormat obj = (NumberFormat) os.readObject(); - assertEquals(fmt, obj, "Serialized and deserialized" + assertEquals(obj, fmt, "Serialized and deserialized" + " objects do not match"); long number = 123456789789L; String expected = fmt.format(number); String actual = obj.format(number); - assertEquals(actual, expected, "Serialized and deserialized" + assertEquals(expected, actual, "Serialized and deserialized" + " objects are expected to return same formatted" + " output for number: " + number); } diff --git a/test/jdk/java/text/Format/DateFormat/Bug8193444.java b/test/jdk/java/text/Format/DateFormat/Bug8193444.java index 6c5007bc851..b2fac85dd8a 100644 --- a/test/jdk/java/text/Format/DateFormat/Bug8193444.java +++ b/test/jdk/java/text/Format/DateFormat/Bug8193444.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -20,29 +20,32 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + /* * @test * @bug 8193444 * @summary Checks SimpleDateFormat.format/parse for the AIOOB exception when * formatting/parsing dates through a pattern string that contains a * sequence of 256 or more non-ASCII unicode characters. - * @run testng/othervm Bug8193444 + * @run junit/othervm Bug8193444 */ -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class Bug8193444 { private static final String NON_ASCII_CHAR = "\u263A"; - @DataProvider(name = "dateFormat") Object[][] dateFormatData() { return new Object[][]{ // short_length (between 0 and 254) @@ -53,8 +56,9 @@ public class Bug8193444 { {257},}; } - @Test(dataProvider = "dateFormat") - public void testDateFormatAndParse(int length) + @ParameterizedTest + @MethodSource("dateFormatData") + void testDateFormatAndParse(int length) throws ParseException { String pattern = NON_ASCII_CHAR.repeat(length); @@ -66,7 +70,7 @@ public class Bug8193444 { // Since the tested format patterns do not contain any character // representing date/time field, those characters are not interpreted, // they are simply copied into the output string during formatting - assertEquals(result, pattern, "Failed to format the date using" + assertEquals(pattern, result, "Failed to format the date using" + " pattern of length: " + length); // The format pattern used by this SimpleDateFormat diff --git a/test/jdk/java/text/Format/DateFormat/CaseInsensitiveParseTest.java b/test/jdk/java/text/Format/DateFormat/CaseInsensitiveParseTest.java index 2331e36b2f9..51473710dbb 100644 --- a/test/jdk/java/text/Format/DateFormat/CaseInsensitiveParseTest.java +++ b/test/jdk/java/text/Format/DateFormat/CaseInsensitiveParseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,14 +21,18 @@ * questions. */ -/** +/* * @test * @bug 8248434 * @modules jdk.localedata - * @run testng/othervm CaseInsensitiveParseTest * @summary Checks format/parse round trip in case-insensitive manner. + * @run junit/othervm CaseInsensitiveParseTest */ +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -37,36 +41,36 @@ import java.util.Date; import java.util.Locale; import java.util.stream.Stream; -import static org.testng.Assert.assertEquals; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class CaseInsensitiveParseTest { private final static String PATTERN = "GGGG/yyyy/MMMM/dddd/hhhh/mmmm/ss/aaaa"; private final static Date EPOCH = new Date(0L); - @DataProvider - private Object[][] locales() { + Object[][] locales() { return (Object[][])Arrays.stream(DateFormat.getAvailableLocales()) .map(Stream::of) .map(Stream::toArray) .toArray(Object[][]::new); } - @Test(dataProvider = "locales") - public void testUpperCase(Locale loc) throws ParseException { + @ParameterizedTest + @MethodSource("locales") + void testUpperCase(Locale loc) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(PATTERN, loc); String formatted = sdf.format(EPOCH); - assertEquals(sdf.parse(formatted.toUpperCase(Locale.ROOT)), EPOCH, + assertEquals(EPOCH, sdf.parse(formatted.toUpperCase(Locale.ROOT)), "roundtrip failed for string '" + formatted + "', locale: " + loc); } - @Test(dataProvider = "locales") - public void testLowerCase(Locale loc) throws ParseException { + @ParameterizedTest + @MethodSource("locales") + void testLowerCase(Locale loc) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(PATTERN, loc); String formatted = sdf.format(EPOCH); - assertEquals(sdf.parse(formatted.toLowerCase(Locale.ROOT)), EPOCH, + assertEquals(EPOCH, sdf.parse(formatted.toLowerCase(Locale.ROOT)), "roundtrip failed for string '" + formatted + "', locale: " + loc); } } diff --git a/test/jdk/java/text/Format/DateFormat/LocaleDateFormats.java b/test/jdk/java/text/Format/DateFormat/LocaleDateFormats.java index f5ebdccb91f..6ddddb2058d 100644 --- a/test/jdk/java/text/Format/DateFormat/LocaleDateFormats.java +++ b/test/jdk/java/text/Format/DateFormat/LocaleDateFormats.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,34 +21,38 @@ * questions. */ -/** +/* * @test * @bug 8080774 8174269 * @modules jdk.localedata - * @run testng LocaleDateFormats * @summary This file contains tests for JRE locales date formats + * @run junit LocaleDateFormats */ +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.DateFormat; import java.util.Calendar; import java.util.Locale; -import static org.testng.Assert.assertEquals; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class LocaleDateFormats { - @Test(dataProvider = "dateFormats") - public void testDateFormat(Locale loc, int style, int year, int month, int date, String expectedString) { + @ParameterizedTest + @MethodSource("dateFormats") + void testDateFormat(Locale loc, int style, int year, int month, int date, String expectedString) { Calendar cal = Calendar.getInstance(loc); cal.set(year, month-1, date); // Create date formatter based on requested style and test locale DateFormat df = DateFormat.getDateInstance(style, loc); // Test the date format - assertEquals(df.format(cal.getTime()), expectedString); + assertEquals(expectedString, df.format(cal.getTime())); } - @DataProvider(name = "dateFormats" ) private Object[][] dateFormats() { return new Object[][] { //8080774 diff --git a/test/jdk/java/text/Format/DateFormat/SimpleDateFormatPatternTest.java b/test/jdk/java/text/Format/DateFormat/SimpleDateFormatPatternTest.java index bcf0022b092..e8ff262e0ab 100644 --- a/test/jdk/java/text/Format/DateFormat/SimpleDateFormatPatternTest.java +++ b/test/jdk/java/text/Format/DateFormat/SimpleDateFormatPatternTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,21 +21,25 @@ * questions. */ -/** +/* * @test * @bug 4326988 6990146 8231213 * @summary test SimpleDateFormat, check its pattern in the constructor - * @run testng/othervm SimpleDateFormatPatternTest + * @run junit/othervm SimpleDateFormatPatternTest */ -import java.lang.IllegalArgumentException; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; import java.util.Locale; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SimpleDateFormatPatternTest { private static String[] validPat = { "yyyy-MM-dd h.mm.ss.a z", @@ -136,90 +140,104 @@ public class SimpleDateFormatPatternTest { return objArray; } - @DataProvider(name = "dfAllLocalesObj") Object[][] dfAllLocalesObj() { return dfAllLocalesObj; } - @DataProvider(name = "invalidPatternObj") Object[][] invalidPatternObj() { return invalidPatObj; } - @DataProvider(name = "validPatternObj") Object[][] validPatternObj() { return validPatObj; } //check Constructors for invalid pattern - @Test(dataProvider = "invalidPatternObj", - expectedExceptions = IllegalArgumentException.class) - public void testIllegalArgumentException1(String pattern, Locale loc) + @ParameterizedTest + @MethodSource("invalidPatternObj") + void testIllegalArgumentException1(String pattern, Locale loc) throws IllegalArgumentException { - Locale.setDefault(loc); - new SimpleDateFormat(pattern); + assertThrows(IllegalArgumentException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat(pattern); + }); } - @Test(dataProvider = "invalidPatternObj", - expectedExceptions = IllegalArgumentException.class) - public void testIllegalArgumentException2(String pattern, Locale loc) + @ParameterizedTest + @MethodSource("invalidPatternObj") + void testIllegalArgumentException2(String pattern, Locale loc) throws IllegalArgumentException { - Locale.setDefault(loc); - new SimpleDateFormat(pattern, new DateFormatSymbols()); + assertThrows(IllegalArgumentException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat(pattern, new DateFormatSymbols()); + }); } - @Test(dataProvider = "invalidPatternObj", - expectedExceptions = IllegalArgumentException.class) - public void testIllegalArgumentException3 (String pattern, Locale loc) + @ParameterizedTest + @MethodSource("invalidPatternObj") + void testIllegalArgumentException3 (String pattern, Locale loc) throws IllegalArgumentException { - Locale.setDefault(loc); - new SimpleDateFormat(pattern, Locale.getDefault()); + assertThrows(IllegalArgumentException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat(pattern, Locale.getDefault()); + }); } - @Test(dataProvider = "invalidPatternObj", - expectedExceptions = IllegalArgumentException.class) - public void testIllegalArgumentException4(String pattern, Locale loc) + @ParameterizedTest + @MethodSource("invalidPatternObj") + void testIllegalArgumentException4(String pattern, Locale loc) throws IllegalArgumentException { - Locale.setDefault(loc); - new SimpleDateFormat().applyPattern(pattern); + assertThrows(IllegalArgumentException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat().applyPattern(pattern); + }); } //check Constructors for null pattern - @Test(dataProvider = "dfAllLocalesObj", - expectedExceptions = NullPointerException.class) - public void testNullPointerException1(Locale loc) + @ParameterizedTest + @MethodSource("dfAllLocalesObj") + void testNullPointerException1(Locale loc) throws NullPointerException { - Locale.setDefault(loc); - new SimpleDateFormat(null); + assertThrows(NullPointerException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat(null); + }); } - @Test(dataProvider = "dfAllLocalesObj", - expectedExceptions = NullPointerException.class) - public void testNullPointerException2(Locale loc) + @ParameterizedTest + @MethodSource("dfAllLocalesObj") + void testNullPointerException2(Locale loc) throws NullPointerException { - Locale.setDefault(loc); - new SimpleDateFormat(null, new DateFormatSymbols()); + assertThrows(NullPointerException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat(null, new DateFormatSymbols()); + }); } - @Test(dataProvider = "dfAllLocalesObj", - expectedExceptions = NullPointerException.class) - public void testNullPointerException3(Locale loc) + @ParameterizedTest + @MethodSource("dfAllLocalesObj") + void testNullPointerException3(Locale loc) throws NullPointerException { - Locale.setDefault(loc); - new SimpleDateFormat(null, Locale.getDefault()); + assertThrows(NullPointerException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat(null, Locale.getDefault()); + }); } - @Test(dataProvider = "dfAllLocalesObj", - expectedExceptions = NullPointerException.class) - public void testNullPointerException4(Locale loc) + @ParameterizedTest + @MethodSource("dfAllLocalesObj") + void testNullPointerException4(Locale loc) throws NullPointerException { - Locale.setDefault(loc); - new SimpleDateFormat().applyPattern(null); + assertThrows(NullPointerException.class, () -> { + Locale.setDefault(loc); + new SimpleDateFormat().applyPattern(null); + }); } - @Test(dataProvider = "validPatternObj") + @ParameterizedTest //check Constructors for valid pattern - public void testValidPattern(String pattern, Locale loc) { + @MethodSource("validPatternObj") + void testValidPattern(String pattern, Locale loc) { Locale.setDefault(loc); new SimpleDateFormat(pattern); new SimpleDateFormat(pattern, new DateFormatSymbols()); diff --git a/test/jdk/java/text/Format/DecimalFormat/SetGroupingSizeTest.java b/test/jdk/java/text/Format/DecimalFormat/SetGroupingSizeTest.java index 07982b453c9..95044341290 100644 --- a/test/jdk/java/text/Format/DecimalFormat/SetGroupingSizeTest.java +++ b/test/jdk/java/text/Format/DecimalFormat/SetGroupingSizeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,28 +26,27 @@ * @bug 8212749 * @summary test whether input value check for * DecimalFormat.setGroupingSize(int) works correctly. - * @run testng/othervm SetGroupingSizeTest + * @run junit/othervm SetGroupingSizeTest */ +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + import java.text.DecimalFormat; -import static org.testng.Assert.assertEquals; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; -@Test public class SetGroupingSizeTest { - @DataProvider - public static Object[][] validGroupingSizes() { + static Object[][] validGroupingSizes() { return new Object[][] { { 0 }, { Byte.MAX_VALUE }, }; } - @DataProvider - public static Object[][] invalidGroupingSizes() { + static Object[][] invalidGroupingSizes() { return new Object[][] { { Byte.MIN_VALUE - 1 }, { Byte.MIN_VALUE }, @@ -58,17 +57,20 @@ public class SetGroupingSizeTest { }; } - @Test(dataProvider = "validGroupingSizes") - public void test_validGroupingSize(int newVal) { + @ParameterizedTest + @MethodSource("validGroupingSizes") + void test_validGroupingSize(int newVal) { DecimalFormat df = new DecimalFormat(); df.setGroupingSize(newVal); - assertEquals(df.getGroupingSize(), newVal); + assertEquals(newVal, df.getGroupingSize()); } - @Test(dataProvider = "invalidGroupingSizes", - expectedExceptions = IllegalArgumentException.class) - public void test_invalidGroupingSize(int newVal) { - DecimalFormat df = new DecimalFormat(); - df.setGroupingSize(newVal); + @ParameterizedTest + @MethodSource("invalidGroupingSizes") + void test_invalidGroupingSize(int newVal) { + assertThrows(IllegalArgumentException.class, () -> { + DecimalFormat df = new DecimalFormat(); + df.setGroupingSize(newVal); + }); } } diff --git a/test/jdk/java/text/Format/NumberFormat/DFSMinusPerCentMill.java b/test/jdk/java/text/Format/NumberFormat/DFSMinusPerCentMill.java index a36eaf5f14f..418802261ff 100644 --- a/test/jdk/java/text/Format/NumberFormat/DFSMinusPerCentMill.java +++ b/test/jdk/java/text/Format/NumberFormat/DFSMinusPerCentMill.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,7 +21,7 @@ * questions. */ -/** +/* * @test * @bug 8220309 8230284 * @library /java/text/testlib @@ -29,17 +29,26 @@ * This test assumes CLDR has numbering systems for "arab" and * "arabext", and their minus/percent representations include * BiDi formatting control characters. - * @run testng/othervm DFSMinusPerCentMill + * @run junit/othervm DFSMinusPerCentMill */ -import java.io.*; -import java.util.*; -import java.text.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -import static org.testng.Assert.*; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Locale; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class DFSMinusPerCentMill { private enum Type { NUMBER, PERCENT, CURRENCY, INTEGER, COMPACT, PERMILL @@ -49,7 +58,6 @@ public class DFSMinusPerCentMill { private static final Locale US_ARABEXT = Locale.forLanguageTag("en-US-u-nu-arabext"); private static final double SRC_NUM = -1234.56; - @DataProvider Object[][] formatData() { return new Object[][] { // Locale, FormatStyle, expected format, expected single char symbol @@ -69,7 +77,6 @@ public class DFSMinusPerCentMill { }; } - @DataProvider Object[][] charSymbols() { return new Object[][]{ // Locale, percent, per mille, minus sign @@ -78,8 +85,9 @@ public class DFSMinusPerCentMill { }; } - @Test(dataProvider="formatData") - public void testFormatData(Locale l, Type style, String expected) { + @ParameterizedTest + @MethodSource("formatData") + void testFormatData(Locale l, Type style, String expected) { NumberFormat nf = null; switch (style) { case NUMBER: @@ -102,19 +110,20 @@ public class DFSMinusPerCentMill { break; } - assertEquals(nf.format(SRC_NUM), expected); + assertEquals(expected, nf.format(SRC_NUM)); } - @Test(dataProvider="charSymbols") - public void testCharSymbols(Locale l, char percent, char permill, char minus) { + @ParameterizedTest + @MethodSource("charSymbols") + void testCharSymbols(Locale l, char percent, char permill, char minus) { DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l); - assertEquals(dfs.getPercent(), percent); - assertEquals(dfs.getPerMill(), permill); - assertEquals(dfs.getMinusSign(), minus); + assertEquals(percent, dfs.getPercent()); + assertEquals(permill, dfs.getPerMill()); + assertEquals(minus, dfs.getMinusSign()); } @Test - public void testSerialization() throws Exception { + void testSerialization() throws Exception { DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); new ObjectOutputStream(bos).writeObject(dfs); @@ -122,7 +131,7 @@ public class DFSMinusPerCentMill { new ByteArrayInputStream(bos.toByteArray()) ).readObject(); - assertEquals(dfs, dfsSerialized); + assertEquals(dfsSerialized, dfs); // set minus/percent/permille dfs.setMinusSign('a'); @@ -134,6 +143,6 @@ public class DFSMinusPerCentMill { new ByteArrayInputStream(bos.toByteArray()) ).readObject(); - assertEquals(dfs, dfsSerialized); + assertEquals(dfsSerialized, dfs); } } diff --git a/test/jdk/java/text/Normalizer/SquareEraCharacterTest.java b/test/jdk/java/text/Normalizer/SquareEraCharacterTest.java index 7367badd811..9b669880fe9 100644 --- a/test/jdk/java/text/Normalizer/SquareEraCharacterTest.java +++ b/test/jdk/java/text/Normalizer/SquareEraCharacterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,20 +25,20 @@ * @test * @bug 8221431 * @summary Tests decomposition of Japanese square era characters. - * @run testng/othervm SquareEraCharacterTest + * @run junit/othervm SquareEraCharacterTest */ -import static org.testng.Assert.assertEquals; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.text.Normalizer; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Test +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SquareEraCharacterTest { - @DataProvider Object[][] squareEras() { return new Object[][] { @@ -51,12 +51,10 @@ public class SquareEraCharacterTest { }; } - @Test(dataProvider="squareEras") - public void test_normalize(char squareChar, String expected) { - - assertEquals( - Normalizer.normalize(Character.toString(squareChar), Normalizer.Form.NFKD), - expected, + @ParameterizedTest + @MethodSource("squareEras") + void test_normalize(char squareChar, String expected) { + assertEquals(expected, Normalizer.normalize(Character.toString(squareChar), Normalizer.Form.NFKD), "decomposing " + Character.getName(squareChar) + "."); } } From cedc0117ac36243cc240e8ab6adb3c78af4055fc Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Fri, 26 Sep 2025 20:49:36 +0000 Subject: [PATCH 266/556] 8365057: Add support for java.util.concurrent lock information to Thread.dump_to_file Co-authored-by: Alex Menkov Co-authored-by: Alan Bateman Reviewed-by: sspitsyn, alanb --- src/hotspot/share/services/threadService.cpp | 23 +++++++++++++++---- .../classes/jdk/internal/vm/ThreadDumper.java | 8 ++++++- .../jdk/internal/vm/ThreadSnapshot.java | 20 ++++++++++++---- .../doc-files/threadDump.schema.json | 8 +++++-- .../HotSpotDiagnosticMXBean/DumpThreads.java | 22 ++++++++++++++++-- .../jdk/test/lib/threaddump/ThreadDump.java | 10 ++++++++ 6 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/hotspot/share/services/threadService.cpp b/src/hotspot/share/services/threadService.cpp index 04d39bf9cf2..547ca4e51d5 100644 --- a/src/hotspot/share/services/threadService.cpp +++ b/src/hotspot/share/services/threadService.cpp @@ -1161,9 +1161,11 @@ public: Type _type; // park blocker or an object the thread waiting on/trying to lock OopHandle _obj; + // thread that owns park blocker object when park blocker is AbstractOwnableSynchronizer + OopHandle _owner; - Blocker(Type type, OopHandle obj): _type(type), _obj(obj) {} - Blocker(): _type(NOTHING), _obj(nullptr) {} + Blocker(Type type, OopHandle obj): _type(type), _obj(obj), _owner() {} + Blocker(): _type(NOTHING), _obj(), _owner() {} bool is_empty() const { return _type == NOTHING; @@ -1198,6 +1200,7 @@ public: delete _locks; } _blocker._obj.release(oop_storage()); + _blocker._owner.release(oop_storage()); } private: @@ -1300,6 +1303,13 @@ public: oop park_blocker = java_lang_Thread::park_blocker(_thread_h()); if (park_blocker != nullptr) { _blocker = Blocker(Blocker::PARK_BLOCKER, OopHandle(oop_storage(), park_blocker)); + if (park_blocker->is_a(vmClasses::java_util_concurrent_locks_AbstractOwnableSynchronizer_klass())) { + // could be stale (unlikely in practice), but it's good enough to see deadlocks + oop ownerObj = java_util_concurrent_locks_AbstractOwnableSynchronizer::get_owner_threadObj(park_blocker); + if (ownerObj != nullptr) { + _blocker._owner = OopHandle(oop_storage(), ownerObj); + } + } } ResourceMark rm(current); @@ -1381,6 +1391,7 @@ class jdk_internal_vm_ThreadSnapshot: AllStatic { static int _locks_offset; static int _blockerTypeOrdinal_offset; static int _blockerObject_offset; + static int _parkBlockerOwner_offset; static void compute_offsets(InstanceKlass* klass, TRAPS) { JavaClasses::compute_offset(_name_offset, klass, "name", vmSymbols::string_signature(), false); @@ -1390,6 +1401,7 @@ class jdk_internal_vm_ThreadSnapshot: AllStatic { JavaClasses::compute_offset(_locks_offset, klass, "locks", vmSymbols::jdk_internal_vm_ThreadLock_array(), false); JavaClasses::compute_offset(_blockerTypeOrdinal_offset, klass, "blockerTypeOrdinal", vmSymbols::int_signature(), false); JavaClasses::compute_offset(_blockerObject_offset, klass, "blockerObject", vmSymbols::object_signature(), false); + JavaClasses::compute_offset(_parkBlockerOwner_offset, klass, "parkBlockerOwner", vmSymbols::thread_signature(), false); } public: static void init(InstanceKlass* klass, TRAPS) { @@ -1420,9 +1432,10 @@ public: static void set_locks(oop snapshot, oop locks) { snapshot->obj_field_put(_locks_offset, locks); } - static void set_blocker(oop snapshot, int type_ordinal, oop lock) { + static void set_blocker(oop snapshot, int type_ordinal, oop lock, oop owner) { snapshot->int_field_put(_blockerTypeOrdinal_offset, type_ordinal); snapshot->obj_field_put(_blockerObject_offset, lock); + snapshot->obj_field_put(_parkBlockerOwner_offset, owner); } }; @@ -1434,6 +1447,7 @@ int jdk_internal_vm_ThreadSnapshot::_stackTrace_offset; int jdk_internal_vm_ThreadSnapshot::_locks_offset; int jdk_internal_vm_ThreadSnapshot::_blockerTypeOrdinal_offset; int jdk_internal_vm_ThreadSnapshot::_blockerObject_offset; +int jdk_internal_vm_ThreadSnapshot::_parkBlockerOwner_offset; oop ThreadSnapshotFactory::get_thread_snapshot(jobject jthread, TRAPS) { ThreadsListHandle tlh(THREAD); @@ -1559,7 +1573,8 @@ oop ThreadSnapshotFactory::get_thread_snapshot(jobject jthread, TRAPS) { jdk_internal_vm_ThreadSnapshot::set_stack_trace(snapshot(), trace()); jdk_internal_vm_ThreadSnapshot::set_locks(snapshot(), locks()); if (!cl._blocker.is_empty()) { - jdk_internal_vm_ThreadSnapshot::set_blocker(snapshot(), cl._blocker._type, cl._blocker._obj.resolve()); + jdk_internal_vm_ThreadSnapshot::set_blocker(snapshot(), + cl._blocker._type, cl._blocker._obj.resolve(), cl._blocker._owner.resolve()); } return snapshot(); } diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java index a26003a3afb..276c379a564 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java @@ -205,7 +205,10 @@ public class ThreadDumper { // park blocker Object parkBlocker = snapshot.parkBlocker(); if (parkBlocker != null) { - writer.println(" - parking to wait for " + decorateObject(parkBlocker)); + String suffix = (snapshot.parkBlockerOwner() instanceof Thread owner) + ? ", owner #" + owner.threadId() + : ""; + writer.println(" - parking to wait for " + decorateObject(parkBlocker) + suffix); } // blocked on monitor enter or Object.wait @@ -335,6 +338,9 @@ public class ThreadDumper { // parkBlocker is an object to allow for exclusiveOwnerThread in the future jsonWriter.startObject("parkBlocker"); jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker)); + if (snapshot.parkBlockerOwner() instanceof Thread owner) { + jsonWriter.writeProperty("owner", owner.threadId()); + } jsonWriter.endObject(); } diff --git a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java index 4fcbaf24d2e..357d38008d1 100644 --- a/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java +++ b/src/java.base/share/classes/jdk/internal/vm/ThreadSnapshot.java @@ -44,6 +44,8 @@ class ThreadSnapshot { // an object the thread is blocked/waiting on, converted to ThreadBlocker by ThreadSnapshot.of() private int blockerTypeOrdinal; private Object blockerObject; + // the owner of the blockerObject when the object is park blocker and is AbstractOwnableSynchronizer + private Thread parkBlockerOwner; // set by ThreadSnapshot.of() private ThreadBlocker blocker; @@ -70,8 +72,11 @@ class ThreadSnapshot { snapshot.locks = EMPTY_LOCKS; } if (snapshot.blockerObject != null) { - snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal, snapshot.blockerObject); + snapshot.blocker = new ThreadBlocker(snapshot.blockerTypeOrdinal, + snapshot.blockerObject, + snapshot.parkBlockerOwner); snapshot.blockerObject = null; // release + snapshot.parkBlockerOwner = null; } return snapshot; } @@ -104,6 +109,13 @@ class ThreadSnapshot { return getBlocker(BlockerLockType.PARK_BLOCKER); } + /** + * Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer. + */ + Thread parkBlockerOwner() { + return (blocker != null && blocker.type == BlockerLockType.PARK_BLOCKER) ? blocker.owner : null; + } + /** * Returns the object that the thread is blocked on. * @throws IllegalStateException if not in the blocked state @@ -211,11 +223,11 @@ class ThreadSnapshot { } } - private record ThreadBlocker(BlockerLockType type, Object obj) { + private record ThreadBlocker(BlockerLockType type, Object obj, Thread owner) { private static final BlockerLockType[] lockTypeValues = BlockerLockType.values(); // cache - ThreadBlocker(int typeOrdinal, Object obj) { - this(lockTypeValues[typeOrdinal], obj); + ThreadBlocker(int typeOrdinal, Object obj, Thread owner) { + this(lockTypeValues[typeOrdinal], obj, owner); } } diff --git a/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json index 57ef5c8b859..bf52bb3915d 100644 --- a/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json +++ b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json @@ -78,6 +78,10 @@ "description": "The blocker object responsible for the thread parking." } }, + "owner": { + "type": "string", + "description": "The thread identifier of the owner when the parkBlocker is an AbstractOwnableSynchronizer." + } "required": [ "object" ] @@ -115,9 +119,9 @@ "items": { "type": [ "string", - null + "null" ], - "description": "The object for which the monitor is owned by the thread, null if eliminated" + "description": "The object for which the monitor is owned by the thread, null if eliminated." } } }, diff --git a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java index adf643749c7..77020491c29 100644 --- a/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java +++ b/test/jdk/com/sun/management/HotSpotDiagnosticMXBean/DumpThreads.java @@ -23,7 +23,7 @@ /* * @test - * @bug 8284161 8287008 8309406 8356870 + * @bug 8284161 8287008 8309406 8356870 8365057 * @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads * @requires vm.continuations * @modules java.base/jdk.internal.vm jdk.management @@ -425,7 +425,9 @@ class DumpThreads { ThreadFields fields = findThread(tid, lines); assertNotNull(fields, "thread not found"); assertEquals("WAITING", fields.state()); - assertTrue(contains(lines, "- parking to wait for l.contains(text)); } + /** + * Finds the line of a plain text thread dump containing the given text. + */ + private String find(List lines, String text) { + return lines.stream().map(String::trim) + .filter(l -> l.contains(text)) + .findAny() + .orElse(null); + } + /** * Dump threads to a file in plain text format, return the lines in the file. */ diff --git a/test/lib/jdk/test/lib/threaddump/ThreadDump.java b/test/lib/jdk/test/lib/threaddump/ThreadDump.java index f4964a9521f..ca728e625fc 100644 --- a/test/lib/jdk/test/lib/threaddump/ThreadDump.java +++ b/test/lib/jdk/test/lib/threaddump/ThreadDump.java @@ -296,6 +296,16 @@ public final class ThreadDump { return getStringProperty("parkBlocker", "object"); } + /** + * Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer. + */ + public OptionalLong parkBlockerOwner() { + String owner = getStringProperty("parkBlocker", "owner"); + return (owner != null) + ? OptionalLong.of(Long.parseLong(owner)) + : OptionalLong.empty(); + } + /** * Returns the object that the thread is blocked entering its monitor. */ From 37f0e74d328d909810b54f7889cca991426d7488 Mon Sep 17 00:00:00 2001 From: Mohamed Issa Date: Fri, 26 Sep 2025 21:10:30 +0000 Subject: [PATCH 267/556] 8364305: Support AVX10 saturating floating point conversion instructions Reviewed-by: sviswanathan, sparasa, jbhateja --- src/hotspot/cpu/x86/assembler_x86.cpp | 152 +++++++++++++ src/hotspot/cpu/x86/assembler_x86.hpp | 16 ++ src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp | 90 +++++++- src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp | 7 + src/hotspot/cpu/x86/x86.ad | 71 +++++- src/hotspot/cpu/x86/x86_64.ad | 92 ++++++++ .../floatingpoint/ScalarFPtoIntCastTest.java | 207 ++++++++++++++++++ .../compiler/lib/ir_framework/IRNode.java | 80 +++++++ .../ir_framework/test/IREncodingPrinter.java | 1 + .../vectorapi/VectorFPtoIntCastTest.java | 70 ++++-- .../runner/ArrayTypeConvertTest.java | 78 +++++-- 11 files changed, 816 insertions(+), 48 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java diff --git a/src/hotspot/cpu/x86/assembler_x86.cpp b/src/hotspot/cpu/x86/assembler_x86.cpp index 3f1140c937b..fd62e9358bf 100644 --- a/src/hotspot/cpu/x86/assembler_x86.cpp +++ b/src/hotspot/cpu/x86/assembler_x86.cpp @@ -2225,6 +2225,44 @@ void Assembler::cvttss2sil(Register dst, XMMRegister src) { emit_int16(0x2C, (0xC0 | encode)); } +void Assembler::evcvttss2sisl(Register dst, XMMRegister src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_F3, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttss2sisl(Register dst, Address src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_address_attributes(/* tuple_type */ EVEX_T1S, /* input_size_in_bits */ EVEX_32bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_F3, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + +void Assembler::evcvttss2sisq(Register dst, XMMRegister src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_F3, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttss2sisq(Register dst, Address src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_address_attributes(/* tuple_type */ EVEX_T1S, /* input_size_in_bits */ EVEX_32bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_F3, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + void Assembler::cvttpd2dq(XMMRegister dst, XMMRegister src) { int vector_len = VM_Version::supports_avx512novl() ? AVX_512bit : AVX_128bit; InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); @@ -2310,6 +2348,25 @@ void Assembler::vcvttps2dq(XMMRegister dst, XMMRegister src, int vector_len) { emit_int16(0x5B, (0xC0 | encode)); } +void Assembler::evcvttps2dqs(XMMRegister dst, XMMRegister src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(vector_len, /* rex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_NONE, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttps2dqs(XMMRegister dst, Address src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(vector_len, /* rex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_address_attributes(/* tuple_type */ EVEX_FV, /* input_size_in_bits */ EVEX_32bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_NONE, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + void Assembler::vcvttpd2dq(XMMRegister dst, XMMRegister src, int vector_len) { assert(vector_len <= AVX_256bit ? VM_Version::supports_avx() : VM_Version::supports_evex(), ""); InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); @@ -2317,6 +2374,25 @@ void Assembler::vcvttpd2dq(XMMRegister dst, XMMRegister src, int vector_len) { emit_int16((unsigned char)0xE6, (0xC0 | encode)); } +void Assembler::evcvttpd2dqs(XMMRegister dst, XMMRegister src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_NONE, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttpd2dqs(XMMRegister dst, Address src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_address_attributes(/* tuple_type */ EVEX_FV, /* input_size_in_bits */ EVEX_64bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_NONE, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + void Assembler::vcvtps2dq(XMMRegister dst, XMMRegister src, int vector_len) { assert(vector_len <= AVX_256bit ? VM_Version::supports_avx() : VM_Version::supports_evex(), ""); InstructionAttr attributes(vector_len, /* rex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); @@ -2332,6 +2408,25 @@ void Assembler::evcvttps2qq(XMMRegister dst, XMMRegister src, int vector_len) { emit_int16(0x7A, (0xC0 | encode)); } +void Assembler::evcvttps2qqs(XMMRegister dst, XMMRegister src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(vector_len, /* rex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_66, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttps2qqs(XMMRegister dst, Address src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(vector_len, /* rex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_address_attributes(/* tuple_type */ EVEX_HV, /* input_size_in_bits */ EVEX_32bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_66, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + void Assembler::evcvtpd2qq(XMMRegister dst, XMMRegister src, int vector_len) { assert(VM_Version::supports_avx512dq(), ""); InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); @@ -2356,6 +2451,25 @@ void Assembler::evcvttpd2qq(XMMRegister dst, XMMRegister src, int vector_len) { emit_int16(0x7A, (0xC0 | encode)); } +void Assembler::evcvttpd2qqs(XMMRegister dst, XMMRegister src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_66, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttpd2qqs(XMMRegister dst, Address src, int vector_len) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); + attributes.set_address_attributes(/* tuple_type */ EVEX_FV, /* input_size_in_bits */ EVEX_64bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_66, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + void Assembler::evcvtqq2pd(XMMRegister dst, XMMRegister src, int vector_len) { assert(VM_Version::supports_avx512dq(), ""); InstructionAttr attributes(vector_len, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ true); @@ -14988,6 +15102,44 @@ void Assembler::cvttsd2siq(Register dst, Address src) { emit_operand(dst, src, 0); } +void Assembler::evcvttsd2sisl(Register dst, XMMRegister src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_F2, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttsd2sisl(Register dst, Address src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(AVX_128bit, /* vex_w */ false, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_address_attributes(/* tuple_type */ EVEX_T1S, /* input_size_in_bits */ EVEX_64bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_F2, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + +void Assembler::evcvttsd2sisq(Register dst, XMMRegister src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_is_evex_instruction(); + int encode = vex_prefix_and_encode(dst->encoding(), 0, src->encoding(), VEX_SIMD_F2, VEX_OPCODE_MAP5, &attributes); + emit_int16(0x6D, (0xC0 | encode)); +} + +void Assembler::evcvttsd2sisq(Register dst, Address src) { + assert(VM_Version::supports_avx10_2(), ""); + InstructionMark im(this); + InstructionAttr attributes(AVX_128bit, /* vex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); + attributes.set_address_attributes(/* tuple_type */ EVEX_T1S, /* input_size_in_bits */ EVEX_64bit); + attributes.set_is_evex_instruction(); + vex_prefix(src, 0, dst->encoding(), VEX_SIMD_F2, VEX_OPCODE_MAP5, &attributes); + emit_int8((unsigned char)0x6D); + emit_operand(dst, src, 0); +} + void Assembler::cvttsd2siq(Register dst, XMMRegister src) { InstructionAttr attributes(AVX_128bit, /* rex_w */ true, /* legacy_mode */ false, /* no_mask_reg */ true, /* uses_vl */ false); int encode = simd_prefix_and_encode(as_XMMRegister(dst->encoding()), xnoreg, src, VEX_SIMD_F2, VEX_OPCODE_0F, &attributes); diff --git a/src/hotspot/cpu/x86/assembler_x86.hpp b/src/hotspot/cpu/x86/assembler_x86.hpp index 99dade412b2..c863191df4c 100644 --- a/src/hotspot/cpu/x86/assembler_x86.hpp +++ b/src/hotspot/cpu/x86/assembler_x86.hpp @@ -1316,11 +1316,19 @@ private: void cvttsd2sil(Register dst, XMMRegister src); void cvttsd2siq(Register dst, Address src); void cvttsd2siq(Register dst, XMMRegister src); + void evcvttsd2sisl(Register dst, XMMRegister src); + void evcvttsd2sisl(Register dst, Address src); + void evcvttsd2sisq(Register dst, XMMRegister src); + void evcvttsd2sisq(Register dst, Address src); // Convert with Truncation Scalar Single-Precision Floating-Point Value to Doubleword Integer void cvttss2sil(Register dst, XMMRegister src); void cvttss2siq(Register dst, XMMRegister src); void cvtss2sil(Register dst, XMMRegister src); + void evcvttss2sisl(Register dst, XMMRegister src); + void evcvttss2sisl(Register dst, Address src); + void evcvttss2sisq(Register dst, XMMRegister src); + void evcvttss2sisq(Register dst, Address src); // Convert vector double to int void cvttpd2dq(XMMRegister dst, XMMRegister src); @@ -1332,7 +1340,11 @@ private: // Convert vector float to int/long void vcvtps2dq(XMMRegister dst, XMMRegister src, int vector_len); void vcvttps2dq(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttps2dqs(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttps2dqs(XMMRegister dst, Address src, int vector_len); void evcvttps2qq(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttps2qqs(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttps2qqs(XMMRegister dst, Address src, int vector_len); // Convert vector long to vector FP void evcvtqq2ps(XMMRegister dst, XMMRegister src, int vector_len); @@ -1341,9 +1353,13 @@ private: // Convert vector double to long void evcvtpd2qq(XMMRegister dst, XMMRegister src, int vector_len); void evcvttpd2qq(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttpd2qqs(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttpd2qqs(XMMRegister dst, Address src, int vector_len); // Convert vector double to int void vcvttpd2dq(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttpd2dqs(XMMRegister dst, XMMRegister src, int vector_len); + void evcvttpd2dqs(XMMRegister dst, Address src, int vector_len); // Evex casts with truncation void evpmovwb(XMMRegister dst, XMMRegister src, int vector_len); diff --git a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp index 8c3f33e0aca..8386e57c389 100644 --- a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp @@ -5053,12 +5053,12 @@ void C2_MacroAssembler::vector_cast_int_to_subword(BasicType to_elem_bt, XMMRegi } vpackuswb(dst, dst, zero, vec_enc); break; - default: assert(false, "%s", type2name(to_elem_bt)); + default: assert(false, "Unexpected basic type for target of vector cast int to subword: %s", type2name(to_elem_bt)); } } /* - * Algorithm for vector D2L and F2I conversions:- + * Algorithm for vector D2L and F2I conversions (AVX 10.2 unsupported):- * a) Perform vector D2L/F2I cast. * b) Choose fast path if none of the result vector lane contains 0x80000000 value. * It signifies that source value could be any of the special floating point @@ -5096,7 +5096,7 @@ void C2_MacroAssembler::vector_castF2X_evex(BasicType to_elem_bt, XMMRegister ds case T_BYTE: evpmovdb(dst, dst, vec_enc); break; - default: assert(false, "%s", type2name(to_elem_bt)); + default: assert(false, "Unexpected basic type for target of vector castF2X EVEX: %s", type2name(to_elem_bt)); } } @@ -5143,7 +5143,7 @@ void C2_MacroAssembler::vector_castD2X_evex(BasicType to_elem_bt, XMMRegister ds evpmovsqd(dst, dst, vec_enc); evpmovdb(dst, dst, vec_enc); break; - default: assert(false, "%s", type2name(to_elem_bt)); + default: assert(false, "Unexpected basic type for target of vector castD2X AVX512DQ EVEX: %s", type2name(to_elem_bt)); } } else { assert(type2aelembytes(to_elem_bt) <= 4, ""); @@ -5158,11 +5158,91 @@ void C2_MacroAssembler::vector_castD2X_evex(BasicType to_elem_bt, XMMRegister ds case T_BYTE: evpmovdb(dst, dst, vec_enc); break; - default: assert(false, "%s", type2name(to_elem_bt)); + default: assert(false, "Unexpected basic type for target of vector castD2X EVEX: %s", type2name(to_elem_bt)); } } } +void C2_MacroAssembler::vector_castF2X_avx10(BasicType to_elem_bt, XMMRegister dst, XMMRegister src, int vec_enc) { + switch(to_elem_bt) { + case T_LONG: + evcvttps2qqs(dst, src, vec_enc); + break; + case T_INT: + evcvttps2dqs(dst, src, vec_enc); + break; + case T_SHORT: + evcvttps2dqs(dst, src, vec_enc); + evpmovdw(dst, dst, vec_enc); + break; + case T_BYTE: + evcvttps2dqs(dst, src, vec_enc); + evpmovdb(dst, dst, vec_enc); + break; + default: assert(false, "Unexpected basic type for target of vector castF2X AVX10 (reg src): %s", type2name(to_elem_bt)); + } +} + +void C2_MacroAssembler::vector_castF2X_avx10(BasicType to_elem_bt, XMMRegister dst, Address src, int vec_enc) { + switch(to_elem_bt) { + case T_LONG: + evcvttps2qqs(dst, src, vec_enc); + break; + case T_INT: + evcvttps2dqs(dst, src, vec_enc); + break; + case T_SHORT: + evcvttps2dqs(dst, src, vec_enc); + evpmovdw(dst, dst, vec_enc); + break; + case T_BYTE: + evcvttps2dqs(dst, src, vec_enc); + evpmovdb(dst, dst, vec_enc); + break; + default: assert(false, "Unexpected basic type for target of vector castF2X AVX10 (mem src): %s", type2name(to_elem_bt)); + } +} + +void C2_MacroAssembler::vector_castD2X_avx10(BasicType to_elem_bt, XMMRegister dst, XMMRegister src, int vec_enc) { + switch(to_elem_bt) { + case T_LONG: + evcvttpd2qqs(dst, src, vec_enc); + break; + case T_INT: + evcvttpd2dqs(dst, src, vec_enc); + break; + case T_SHORT: + evcvttpd2dqs(dst, src, vec_enc); + evpmovdw(dst, dst, vec_enc); + break; + case T_BYTE: + evcvttpd2dqs(dst, src, vec_enc); + evpmovdb(dst, dst, vec_enc); + break; + default: assert(false, "Unexpected basic type for target of vector castD2X AVX10 (reg src): %s", type2name(to_elem_bt)); + } +} + +void C2_MacroAssembler::vector_castD2X_avx10(BasicType to_elem_bt, XMMRegister dst, Address src, int vec_enc) { + switch(to_elem_bt) { + case T_LONG: + evcvttpd2qqs(dst, src, vec_enc); + break; + case T_INT: + evcvttpd2dqs(dst, src, vec_enc); + break; + case T_SHORT: + evcvttpd2dqs(dst, src, vec_enc); + evpmovdw(dst, dst, vec_enc); + break; + case T_BYTE: + evcvttpd2dqs(dst, src, vec_enc); + evpmovdb(dst, dst, vec_enc); + break; + default: assert(false, "Unexpected basic type for target of vector castD2X AVX10 (mem src): %s", type2name(to_elem_bt)); + } +} + void C2_MacroAssembler::vector_round_double_evex(XMMRegister dst, XMMRegister src, AddressLiteral double_sign_flip, AddressLiteral new_mxcsr, int vec_enc, Register tmp, XMMRegister xtmp1, XMMRegister xtmp2, KRegister ktmp1, KRegister ktmp2) { diff --git a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp index 950fcb75290..aaee25f440a 100644 --- a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp +++ b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp @@ -347,6 +347,13 @@ public: XMMRegister xtmp2, XMMRegister xtmp3, XMMRegister xtmp4, XMMRegister xtmp5, AddressLiteral float_sign_flip, Register rscratch, int vec_enc); + void vector_castF2X_avx10(BasicType to_elem_bt, XMMRegister dst, XMMRegister src, int vec_enc); + + void vector_castF2X_avx10(BasicType to_elem_bt, XMMRegister dst, Address src, int vec_enc); + + void vector_castD2X_avx10(BasicType to_elem_bt, XMMRegister dst, XMMRegister src, int vec_enc); + + void vector_castD2X_avx10(BasicType to_elem_bt, XMMRegister dst, Address src, int vec_enc); void vector_cast_double_to_int_special_cases_avx(XMMRegister dst, XMMRegister src, XMMRegister xtmp1, XMMRegister xtmp2, XMMRegister xtmp3, XMMRegister xtmp4, XMMRegister xtmp5, Register rscratch, diff --git a/src/hotspot/cpu/x86/x86.ad b/src/hotspot/cpu/x86/x86.ad index 2eb748e350c..efe0482e095 100644 --- a/src/hotspot/cpu/x86/x86.ad +++ b/src/hotspot/cpu/x86/x86.ad @@ -7664,8 +7664,11 @@ instruct vcastFtoD_reg(vec dst, vec src) %{ instruct castFtoX_reg_avx(vec dst, vec src, vec xtmp1, vec xtmp2, vec xtmp3, vec xtmp4, rFlagsReg cr) %{ - predicate(!VM_Version::supports_avx512vl() && Matcher::vector_length_in_bytes(n->in(1)) < 64 && - type2aelembytes(Matcher::vector_element_basic_type(n)) <= 4); + predicate(!VM_Version::supports_avx10_2() && + !VM_Version::supports_avx512vl() && + Matcher::vector_length_in_bytes(n->in(1)) < 64 && + type2aelembytes(Matcher::vector_element_basic_type(n)) <= 4 && + is_integral_type(Matcher::vector_element_basic_type(n))); match(Set dst (VectorCastF2X src)); effect(TEMP dst, TEMP xtmp1, TEMP xtmp2, TEMP xtmp3, TEMP xtmp4, KILL cr); format %{ "vector_cast_f2x $dst,$src\t! using $xtmp1, $xtmp2, $xtmp3 and $xtmp4 as TEMP" %} @@ -7687,7 +7690,8 @@ instruct castFtoX_reg_avx(vec dst, vec src, vec xtmp1, vec xtmp2, vec xtmp3, vec %} instruct castFtoX_reg_evex(vec dst, vec src, vec xtmp1, vec xtmp2, kReg ktmp1, kReg ktmp2, rFlagsReg cr) %{ - predicate((VM_Version::supports_avx512vl() || Matcher::vector_length_in_bytes(n->in(1)) == 64) && + predicate(!VM_Version::supports_avx10_2() && + (VM_Version::supports_avx512vl() || Matcher::vector_length_in_bytes(n->in(1)) == 64) && is_integral_type(Matcher::vector_element_basic_type(n))); match(Set dst (VectorCastF2X src)); effect(TEMP dst, TEMP xtmp1, TEMP xtmp2, TEMP ktmp1, TEMP ktmp2, KILL cr); @@ -7709,6 +7713,33 @@ instruct castFtoX_reg_evex(vec dst, vec src, vec xtmp1, vec xtmp2, kReg ktmp1, k ins_pipe( pipe_slow ); %} +instruct castFtoX_reg_avx10(vec dst, vec src) %{ + predicate(VM_Version::supports_avx10_2() && + is_integral_type(Matcher::vector_element_basic_type(n))); + match(Set dst (VectorCastF2X src)); + format %{ "vector_cast_f2x_avx10 $dst, $src\t!" %} + ins_encode %{ + BasicType to_elem_bt = Matcher::vector_element_basic_type(this); + int vlen_enc = (to_elem_bt == T_LONG) ? vector_length_encoding(this) : vector_length_encoding(this, $src); + __ vector_castF2X_avx10(to_elem_bt, $dst$$XMMRegister, $src$$XMMRegister, vlen_enc); + %} + ins_pipe( pipe_slow ); +%} + +instruct castFtoX_mem_avx10(vec dst, memory src) %{ + predicate(VM_Version::supports_avx10_2() && + is_integral_type(Matcher::vector_element_basic_type(n))); + match(Set dst (VectorCastF2X (LoadVector src))); + format %{ "vector_cast_f2x_avx10 $dst, $src\t!" %} + ins_encode %{ + int vlen = Matcher::vector_length(this); + BasicType to_elem_bt = Matcher::vector_element_basic_type(this); + int vlen_enc = (to_elem_bt == T_LONG) ? vector_length_encoding(this) : vector_length_encoding(vlen * sizeof(jfloat)); + __ vector_castF2X_avx10(to_elem_bt, $dst$$XMMRegister, $src$$Address, vlen_enc); + %} + ins_pipe( pipe_slow ); +%} + instruct vcastDtoF_reg(vec dst, vec src) %{ predicate(Matcher::vector_element_basic_type(n) == T_FLOAT); match(Set dst (VectorCastD2X src)); @@ -7721,7 +7752,9 @@ instruct vcastDtoF_reg(vec dst, vec src) %{ %} instruct castDtoX_reg_avx(vec dst, vec src, vec xtmp1, vec xtmp2, vec xtmp3, vec xtmp4, vec xtmp5, rFlagsReg cr) %{ - predicate(!VM_Version::supports_avx512vl() && Matcher::vector_length_in_bytes(n->in(1)) < 64 && + predicate(!VM_Version::supports_avx10_2() && + !VM_Version::supports_avx512vl() && + Matcher::vector_length_in_bytes(n->in(1)) < 64 && is_integral_type(Matcher::vector_element_basic_type(n))); match(Set dst (VectorCastD2X src)); effect(TEMP dst, TEMP xtmp1, TEMP xtmp2, TEMP xtmp3, TEMP xtmp4, TEMP xtmp5, KILL cr); @@ -7737,7 +7770,8 @@ instruct castDtoX_reg_avx(vec dst, vec src, vec xtmp1, vec xtmp2, vec xtmp3, vec %} instruct castDtoX_reg_evex(vec dst, vec src, vec xtmp1, vec xtmp2, kReg ktmp1, kReg ktmp2, rFlagsReg cr) %{ - predicate((VM_Version::supports_avx512vl() || Matcher::vector_length_in_bytes(n->in(1)) == 64) && + predicate(!VM_Version::supports_avx10_2() && + (VM_Version::supports_avx512vl() || Matcher::vector_length_in_bytes(n->in(1)) == 64) && is_integral_type(Matcher::vector_element_basic_type(n))); match(Set dst (VectorCastD2X src)); effect(TEMP dst, TEMP xtmp1, TEMP xtmp2, TEMP ktmp1, TEMP ktmp2, KILL cr); @@ -7753,6 +7787,33 @@ instruct castDtoX_reg_evex(vec dst, vec src, vec xtmp1, vec xtmp2, kReg ktmp1, k ins_pipe( pipe_slow ); %} +instruct castDtoX_reg_avx10(vec dst, vec src) %{ + predicate(VM_Version::supports_avx10_2() && + is_integral_type(Matcher::vector_element_basic_type(n))); + match(Set dst (VectorCastD2X src)); + format %{ "vector_cast_d2x_avx10 $dst, $src\t!" %} + ins_encode %{ + int vlen_enc = vector_length_encoding(this, $src); + BasicType to_elem_bt = Matcher::vector_element_basic_type(this); + __ vector_castD2X_avx10(to_elem_bt, $dst$$XMMRegister, $src$$XMMRegister, vlen_enc); + %} + ins_pipe( pipe_slow ); +%} + +instruct castDtoX_mem_avx10(vec dst, memory src) %{ + predicate(VM_Version::supports_avx10_2() && + is_integral_type(Matcher::vector_element_basic_type(n))); + match(Set dst (VectorCastD2X (LoadVector src))); + format %{ "vector_cast_d2x_avx10 $dst, $src\t!" %} + ins_encode %{ + int vlen = Matcher::vector_length(this); + int vlen_enc = vector_length_encoding(vlen * sizeof(jdouble)); + BasicType to_elem_bt = Matcher::vector_element_basic_type(this); + __ vector_castD2X_avx10(to_elem_bt, $dst$$XMMRegister, $src$$Address, vlen_enc); + %} + ins_pipe( pipe_slow ); +%} + instruct vucast(vec dst, vec src) %{ match(Set dst (VectorUCastB2X src)); match(Set dst (VectorUCastS2X src)); diff --git a/src/hotspot/cpu/x86/x86_64.ad b/src/hotspot/cpu/x86/x86_64.ad index 0914bea82a1..0b254966db6 100644 --- a/src/hotspot/cpu/x86/x86_64.ad +++ b/src/hotspot/cpu/x86/x86_64.ad @@ -11712,6 +11712,7 @@ instruct convD2F_reg_mem(regF dst, memory src) // XXX do mem variants instruct convF2I_reg_reg(rRegI dst, regF src, rFlagsReg cr) %{ + predicate(!VM_Version::supports_avx10_2()); match(Set dst (ConvF2I src)); effect(KILL cr); format %{ "convert_f2i $dst, $src" %} @@ -11721,8 +11722,31 @@ instruct convF2I_reg_reg(rRegI dst, regF src, rFlagsReg cr) ins_pipe(pipe_slow); %} +instruct convF2I_reg_reg_avx10(rRegI dst, regF src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvF2I src)); + format %{ "evcvttss2sisl $dst, $src" %} + ins_encode %{ + __ evcvttss2sisl($dst$$Register, $src$$XMMRegister); + %} + ins_pipe(pipe_slow); +%} + +instruct convF2I_reg_mem_avx10(rRegI dst, memory src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvF2I (LoadF src))); + format %{ "evcvttss2sisl $dst, $src" %} + ins_encode %{ + __ evcvttss2sisl($dst$$Register, $src$$Address); + %} + ins_pipe(pipe_slow); +%} + instruct convF2L_reg_reg(rRegL dst, regF src, rFlagsReg cr) %{ + predicate(!VM_Version::supports_avx10_2()); match(Set dst (ConvF2L src)); effect(KILL cr); format %{ "convert_f2l $dst, $src"%} @@ -11732,8 +11756,31 @@ instruct convF2L_reg_reg(rRegL dst, regF src, rFlagsReg cr) ins_pipe(pipe_slow); %} +instruct convF2L_reg_reg_avx10(rRegL dst, regF src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvF2L src)); + format %{ "evcvttss2sisq $dst, $src" %} + ins_encode %{ + __ evcvttss2sisq($dst$$Register, $src$$XMMRegister); + %} + ins_pipe(pipe_slow); +%} + +instruct convF2L_reg_mem_avx10(rRegL dst, memory src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvF2L (LoadF src))); + format %{ "evcvttss2sisq $dst, $src" %} + ins_encode %{ + __ evcvttss2sisq($dst$$Register, $src$$Address); + %} + ins_pipe(pipe_slow); +%} + instruct convD2I_reg_reg(rRegI dst, regD src, rFlagsReg cr) %{ + predicate(!VM_Version::supports_avx10_2()); match(Set dst (ConvD2I src)); effect(KILL cr); format %{ "convert_d2i $dst, $src"%} @@ -11743,8 +11790,31 @@ instruct convD2I_reg_reg(rRegI dst, regD src, rFlagsReg cr) ins_pipe(pipe_slow); %} +instruct convD2I_reg_reg_avx10(rRegI dst, regD src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvD2I src)); + format %{ "evcvttsd2sisl $dst, $src" %} + ins_encode %{ + __ evcvttsd2sisl($dst$$Register, $src$$XMMRegister); + %} + ins_pipe(pipe_slow); +%} + +instruct convD2I_reg_mem_avx10(rRegI dst, memory src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvD2I (LoadD src))); + format %{ "evcvttsd2sisl $dst, $src" %} + ins_encode %{ + __ evcvttsd2sisl($dst$$Register, $src$$Address); + %} + ins_pipe(pipe_slow); +%} + instruct convD2L_reg_reg(rRegL dst, regD src, rFlagsReg cr) %{ + predicate(!VM_Version::supports_avx10_2()); match(Set dst (ConvD2L src)); effect(KILL cr); format %{ "convert_d2l $dst, $src"%} @@ -11754,6 +11824,28 @@ instruct convD2L_reg_reg(rRegL dst, regD src, rFlagsReg cr) ins_pipe(pipe_slow); %} +instruct convD2L_reg_reg_avx10(rRegL dst, regD src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvD2L src)); + format %{ "evcvttsd2sisq $dst, $src" %} + ins_encode %{ + __ evcvttsd2sisq($dst$$Register, $src$$XMMRegister); + %} + ins_pipe(pipe_slow); +%} + +instruct convD2L_reg_mem_avx10(rRegL dst, memory src) +%{ + predicate(VM_Version::supports_avx10_2()); + match(Set dst (ConvD2L (LoadD src))); + format %{ "evcvttsd2sisq $dst, $src" %} + ins_encode %{ + __ evcvttsd2sisq($dst$$Register, $src$$Address); + %} + ins_pipe(pipe_slow); +%} + instruct round_double_reg(rRegL dst, regD src, rRegL rtmp, rcx_RegL rcx, rFlagsReg cr) %{ match(Set dst (RoundD src)); diff --git a/test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java b/test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java new file mode 100644 index 00000000000..e6d1c875250 --- /dev/null +++ b/test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** +* @test +* @bug 8364305 +* @summary Test scalar float/double to integral cast +* @requires vm.compiler2.enabled +* @library /test/lib / +* @run main/othervm/native compiler.floatingpoint.ScalarFPtoIntCastTest +*/ + +package compiler.floatingpoint; + +import compiler.lib.ir_framework.*; +import compiler.lib.generators.Generator; +import static compiler.lib.generators.Generators.G; +import compiler.lib.verify.Verify; + +public class ScalarFPtoIntCastTest { + private static final int COUNT = 16; + + private float[] float_arr; + private double[] double_arr; + private long[] long_float_arr; + private long[] long_double_arr; + private int[] int_float_arr; + private int[] int_double_arr; + private short[] short_float_arr; + private short[] short_double_arr; + private byte[] byte_float_arr; + private byte[] byte_double_arr; + + private static final Generator genF = G.floats(); + private static final Generator genD = G.doubles(); + + public static void main(String[] args) { + TestFramework testFramework = new TestFramework(); + testFramework.start(); + } + + public ScalarFPtoIntCastTest() { + long_float_arr = new long[COUNT]; + long_double_arr = new long[COUNT]; + int_float_arr = new int[COUNT]; + int_double_arr = new int[COUNT]; + short_float_arr = new short[COUNT]; + short_double_arr = new short[COUNT]; + byte_float_arr = new byte[COUNT]; + byte_double_arr = new byte[COUNT]; + float_arr = new float[COUNT]; + double_arr = new double[COUNT]; + + G.fill(genF, float_arr); + G.fill(genD, double_arr); + for (int i = 0; i < COUNT; i++) { + long_float_arr[i] = (long) float_arr[i]; + long_double_arr[i] = (long) double_arr[i]; + int_float_arr[i] = (int) float_arr[i]; + int_double_arr[i] = (int) double_arr[i]; + short_float_arr[i] = (short) float_arr[i]; + short_double_arr[i] = (short) double_arr[i]; + byte_float_arr[i] = (byte) float_arr[i]; + byte_double_arr[i] = (byte) double_arr[i]; + } + } + + @Test + @IR(counts = {IRNode.CONV_F2I, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_F2I, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_F2I_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void float2int() { + for (int i = 0; i < COUNT; i++) { + float float_val = float_arr[i]; + int computed = (int) float_val; + int expected = int_float_arr[i]; + Verify.checkEQ(computed, expected); + } + } + + @Test + @IR(counts = {IRNode.CONV_F2L, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_F2L, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_F2L_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void float2long() { + for (int i = 0; i < COUNT; i++) { + float float_val = float_arr[i]; + long computed = (long) float_val; + long expected = long_float_arr[i]; + Verify.checkEQ(computed, expected); + } + } + + @Test + @IR(counts = {IRNode.CONV_F2I, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_F2I, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_F2I_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void float2short() { + for (int i = 0; i < COUNT; i++) { + float float_val = float_arr[i]; + short computed = (short) float_val; + short expected = short_float_arr[i]; + Verify.checkEQ(computed, expected); + } + } + + @Test + @IR(counts = {IRNode.CONV_F2I, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_F2I, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_F2I_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void float2byte() { + for (int i = 0; i < COUNT; i++) { + float float_val = float_arr[i]; + byte computed = (byte) float_val; + byte expected = byte_float_arr[i]; + Verify.checkEQ(computed, expected); + } + } + + @Test + @IR(counts = {IRNode.CONV_D2I, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_D2I, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_D2I_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void double2int() { + for (int i = 0; i < COUNT; i++) { + double double_val = double_arr[i]; + int computed = (int) double_val; + int expected = int_double_arr[i]; + Verify.checkEQ(computed, expected); + } + } + + @Test + @IR(counts = {IRNode.CONV_D2L, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_D2L, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_D2L_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void double2long() { + for (int i = 0; i < COUNT; i++) { + double double_val = double_arr[i]; + long computed = (long) double_val; + long expected = long_double_arr[i]; + Verify.checkEQ(computed, expected); + } + } + + @Test + @IR(counts = {IRNode.CONV_D2I, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_D2I, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_D2I_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void double2short() { + for (int i = 0; i < COUNT; i++) { + double double_val = double_arr[i]; + short computed = (short) double_val; + short expected = short_double_arr[i]; + Verify.checkEQ(computed, expected); + } + } + + @Test + @IR(counts = {IRNode.CONV_D2I, "> 0"}) + @IR(counts = {IRNode.X86_SCONV_D2I, "> 0"}, + applyIfCPUFeature = {"avx10_2", "false"}) + @IR(counts = {IRNode.X86_SCONV_D2I_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) + public void double2byte() { + for (int i = 0; i < COUNT; i++) { + double double_val = double_arr[i]; + byte computed = (byte) double_val; + byte expected = byte_double_arr[i]; + Verify.checkEQ(computed, expected); + } + } +} diff --git a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java index 88b34841e57..15378feb841 100644 --- a/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java +++ b/test/hotspot/jtreg/compiler/lib/ir_framework/IRNode.java @@ -610,11 +610,31 @@ public class IRNode { beforeMatchingNameRegex(CONV, "Conv"); } + public static final String CONV_D2I = PREFIX + "CONV_D2I" + POSTFIX; + static { + beforeMatchingNameRegex(CONV_D2I, "ConvD2I"); + } + + public static final String CONV_D2L = PREFIX + "CONV_D2L" + POSTFIX; + static { + beforeMatchingNameRegex(CONV_D2L, "ConvD2L"); + } + public static final String CONV_F2HF = PREFIX + "CONV_F2HF" + POSTFIX; static { beforeMatchingNameRegex(CONV_F2HF, "ConvF2HF"); } + public static final String CONV_F2I = PREFIX + "CONV_F2I" + POSTFIX; + static { + beforeMatchingNameRegex(CONV_F2I, "ConvF2I"); + } + + public static final String CONV_F2L = PREFIX + "CONV_F2L" + POSTFIX; + static { + beforeMatchingNameRegex(CONV_F2L, "ConvF2L"); + } + public static final String CONV_I2L = PREFIX + "CONV_I2L" + POSTFIX; static { beforeMatchingNameRegex(CONV_I2L, "ConvI2L"); @@ -2675,6 +2695,66 @@ public class IRNode { machOnlyNameRegex(VSTOREMASK_TRUECOUNT, "vstoremask_truecount_neon"); } + public static final String X86_SCONV_D2I = PREFIX + "X86_SCONV_D2I" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_D2I, "convD2I_reg_reg"); + } + + public static final String X86_SCONV_D2L = PREFIX + "X86_SCONV_D2L" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_D2L, "convD2L_reg_reg"); + } + + public static final String X86_SCONV_F2I = PREFIX + "X86_SCONV_F2I" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_F2I, "convF2I_reg_reg"); + } + + public static final String X86_SCONV_F2L = PREFIX + "X86_SCONV_F2L" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_F2L, "convF2L_reg_reg"); + } + + public static final String X86_SCONV_D2I_AVX10 = PREFIX + "X86_SCONV2_D2I_AVX10" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_D2I_AVX10, "convD2I_(reg_reg|reg_mem)_avx10"); + } + + public static final String X86_SCONV_D2L_AVX10 = PREFIX + "X86_SCONV_D2L_AVX10" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_D2L_AVX10, "convD2L_(reg_reg|reg_mem)_avx10"); + } + + public static final String X86_SCONV_F2I_AVX10 = PREFIX + "X86_SCONV_F2I_AVX10" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_F2I_AVX10, "convF2I_(reg_reg|reg_mem)_avx10"); + } + + public static final String X86_SCONV_F2L_AVX10 = PREFIX + "X86_SCONV_F2L_AVX10" + POSTFIX; + static { + machOnlyNameRegex(X86_SCONV_F2L_AVX10, "convF2L_(reg_reg|reg_mem)_avx10"); + } + + public static final String X86_VCAST_F2X = PREFIX + "X86_VCAST_F2X" + POSTFIX; + static { + machOnlyNameRegex(X86_VCAST_F2X, "castFtoX_reg_(av|eve)x"); + } + + public static final String X86_VCAST_D2X = PREFIX + "X86_VCAST_D2X" + POSTFIX; + static { + machOnlyNameRegex(X86_VCAST_D2X, "castDtoX_reg_(av|eve)x"); + } + + public static final String X86_VCAST_F2X_AVX10 = PREFIX + "X86_VCAST_F2X_AVX10" + POSTFIX; + static { + machOnlyNameRegex(X86_VCAST_F2X_AVX10, "castFtoX_(reg|mem)_avx10"); + } + + public static final String X86_VCAST_D2X_AVX10 = PREFIX + "X86_VCAST_D2X_AVX10" + POSTFIX; + static { + machOnlyNameRegex(X86_VCAST_D2X_AVX10, "castDtoX_(reg|mem)_avx10"); + } + public static final String XOR = PREFIX + "XOR" + POSTFIX; static { beforeMatchingNameRegex(XOR, "Xor(I|L)"); diff --git a/test/hotspot/jtreg/compiler/lib/ir_framework/test/IREncodingPrinter.java b/test/hotspot/jtreg/compiler/lib/ir_framework/test/IREncodingPrinter.java index 16b4654013a..c05124edcd7 100644 --- a/test/hotspot/jtreg/compiler/lib/ir_framework/test/IREncodingPrinter.java +++ b/test/hotspot/jtreg/compiler/lib/ir_framework/test/IREncodingPrinter.java @@ -106,6 +106,7 @@ public class IREncodingPrinter { "avx512_fp16", "avx512_vnni", "avx512_vbmi", + "avx10_2", "bmi2", // AArch64 "sha3", diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorFPtoIntCastTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorFPtoIntCastTest.java index 8d5d872375f..1037a2989f9 100644 --- a/test/hotspot/jtreg/compiler/vectorapi/VectorFPtoIntCastTest.java +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorFPtoIntCastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,8 +23,8 @@ /** * @test -* @bug 8287835 -* @summary Test float/double to integral cast +* @bug 8287835 8364305 +* @summary Test vector float/double to integral cast * @modules jdk.incubator.vector * @requires vm.compiler2.enabled * @library /test/lib / @@ -87,7 +87,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_F2I, IRNode.VECTOR_SIZE_16, "> 0"}, - applyIfCPUFeature = {"avx512f", "true"}) + applyIfCPUFeatureOr = {"avx512f", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512f", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void float2int() { var cvec = (IntVector)fvec512.convertShape(VectorOperators.F2I, ispec512, 0); cvec.intoArray(int_arr, 0); @@ -96,7 +100,7 @@ public class VectorFPtoIntCastTest { public void checkf2int(int len) { for (int i = 0; i < len; i++) { - int expected = (int)float_arr[i]; + int expected = (int) float_arr[i]; if (int_arr[i] != expected) { throw new RuntimeException("Invalid result: int_arr[" + i + "] = " + int_arr[i] + " != " + expected); } @@ -105,7 +109,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_F2L, IRNode.VECTOR_SIZE_8, "> 0"}, - applyIfCPUFeature = {"avx512dq", "true"}) + applyIfCPUFeatureOr = {"avx512dq", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512dq", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void float2long() { var cvec = (LongVector)fvec512.convertShape(VectorOperators.F2L, lspec512, 0); cvec.intoArray(long_arr, 0); @@ -114,7 +122,7 @@ public class VectorFPtoIntCastTest { public void checkf2long(int len) { for (int i = 0; i < len; i++) { - long expected = (long)float_arr[i]; + long expected = (long) float_arr[i]; if (long_arr[i] != expected) { throw new RuntimeException("Invalid result: long_arr[" + i + "] = " + long_arr[i] + " != " + expected); } @@ -123,7 +131,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_F2S, IRNode.VECTOR_SIZE_16, "> 0"}, - applyIfCPUFeature = {"avx512f", "true"}) + applyIfCPUFeatureOr = {"avx512f", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512f", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void float2short() { var cvec = (ShortVector)fvec512.convertShape(VectorOperators.F2S, sspec256, 0); cvec.intoArray(short_arr, 0); @@ -132,7 +144,7 @@ public class VectorFPtoIntCastTest { public void checkf2short(int len) { for (int i = 0; i < len; i++) { - short expected = (short)float_arr[i]; + short expected = (short) float_arr[i]; if (short_arr[i] != expected) { throw new RuntimeException("Invalid result: short_arr[" + i + "] = " + short_arr[i] + " != " + expected); } @@ -141,7 +153,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_F2B, IRNode.VECTOR_SIZE_16, "> 0"}, - applyIfCPUFeature = {"avx512f", "true"}) + applyIfCPUFeatureOr = {"avx512f", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512f", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void float2byte() { var cvec = (ByteVector)fvec512.convertShape(VectorOperators.F2B, bspec128, 0); cvec.intoArray(byte_arr, 0); @@ -150,7 +166,7 @@ public class VectorFPtoIntCastTest { public void checkf2byte(int len) { for (int i = 0; i < len; i++) { - byte expected = (byte)float_arr[i]; + byte expected = (byte) float_arr[i]; if (byte_arr[i] != expected) { throw new RuntimeException("Invalid result: byte_arr[" + i + "] = " + byte_arr[i] + " != " + expected); } @@ -159,7 +175,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_D2I, IRNode.VECTOR_SIZE_8, "> 0"}, - applyIfCPUFeature = {"avx512f", "true"}) + applyIfCPUFeatureOr = {"avx512f", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512f", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void double2int() { var cvec = (IntVector)dvec512.convertShape(VectorOperators.D2I, ispec256, 0); cvec.intoArray(int_arr, 0); @@ -168,7 +188,7 @@ public class VectorFPtoIntCastTest { public void checkd2int(int len) { for (int i = 0; i < len; i++) { - int expected = (int)double_arr[i]; + int expected = (int) double_arr[i]; if (int_arr[i] != expected) { throw new RuntimeException("Invalid result: int_arr[" + i + "] = " + int_arr[i] + " != " + expected); } @@ -177,7 +197,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_D2L, IRNode.VECTOR_SIZE_8, "> 0"}, - applyIfCPUFeature = {"avx512dq", "true"}) + applyIfCPUFeatureOr = {"avx512dq", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512dq", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void double2long() { var cvec = (LongVector)dvec512.convertShape(VectorOperators.D2L, lspec512, 0); cvec.intoArray(long_arr, 0); @@ -186,7 +210,7 @@ public class VectorFPtoIntCastTest { public void checkd2long(int len) { for (int i = 0; i < len; i++) { - long expected = (long)double_arr[i]; + long expected = (long) double_arr[i]; if (long_arr[i] != expected) { throw new RuntimeException("Invalid result: long_arr[" + i + "] = " + long_arr[i] + " != " + expected); } @@ -195,7 +219,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE_8, "> 0"}, - applyIfCPUFeature = {"avx512f", "true"}) + applyIfCPUFeatureOr = {"avx512f", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512f", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void double2short() { var cvec = (ShortVector)dvec512.convertShape(VectorOperators.D2S, sspec128, 0); cvec.intoArray(short_arr, 0); @@ -204,7 +232,7 @@ public class VectorFPtoIntCastTest { public void checkd2short(int len) { for (int i = 0; i < len; i++) { - short expected = (short)double_arr[i]; + short expected = (short) double_arr[i]; if (short_arr[i] != expected) { throw new RuntimeException("Invalid result: short_arr[" + i + "] = " + short_arr[i] + " != " + expected); } @@ -213,7 +241,11 @@ public class VectorFPtoIntCastTest { @Test @IR(counts = {IRNode.VECTOR_CAST_D2B, IRNode.VECTOR_SIZE_8, "> 0"}, - applyIfCPUFeature = {"avx512f", "true"}) + applyIfCPUFeatureOr = {"avx512f", "true", "avx10_2", "true"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512f", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public void double2byte() { var cvec = (ByteVector)dvec512.convertShape(VectorOperators.D2B, bspec64, 0); cvec.intoArray(byte_arr, 0); @@ -222,7 +254,7 @@ public class VectorFPtoIntCastTest { public void checkd2byte(int len) { for (int i = 0; i < len; i++) { - byte expected = (byte)double_arr[i]; + byte expected = (byte) double_arr[i]; if (byte_arr[i] != expected) { throw new RuntimeException("Invalid result: byte_arr[" + i + "] = " + byte_arr[i] + " != " + expected); } diff --git a/test/hotspot/jtreg/compiler/vectorization/runner/ArrayTypeConvertTest.java b/test/hotspot/jtreg/compiler/vectorization/runner/ArrayTypeConvertTest.java index 3fa636b42f7..4db973ff728 100644 --- a/test/hotspot/jtreg/compiler/vectorization/runner/ArrayTypeConvertTest.java +++ b/test/hotspot/jtreg/compiler/vectorization/runner/ArrayTypeConvertTest.java @@ -294,8 +294,12 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { // ---------------- Convert F/D to I/L ---------------- @Test - @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx", "true", "rvv", "true"}, - counts = {IRNode.VECTOR_CAST_F2I, IRNode.VECTOR_SIZE + "min(max_float, max_int)", ">0"}) + @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx", "true", "avx10_2", "true", "rvv", "true"}, + counts = {IRNode.VECTOR_CAST_F2I, IRNode.VECTOR_SIZE + "min(max_float, max_int)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public int[] convertFloatToInt() { int[] res = new int[SIZE]; for (int i = 0; i < SIZE; i++) { @@ -305,8 +309,12 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { } @Test - @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx512dq", "true", "rvv", "true"}, - counts = {IRNode.VECTOR_CAST_F2L, IRNode.VECTOR_SIZE + "min(max_float, max_long)", ">0"}) + @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx512dq", "true", "avx10_2", "true", "rvv", "true"}, + counts = {IRNode.VECTOR_CAST_F2L, IRNode.VECTOR_SIZE + "min(max_float, max_long)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512dq", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public long[] convertFloatToLong() { long[] res = new long[SIZE]; for (int i = 0; i < SIZE; i++) { @@ -316,8 +324,12 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { } @Test - @IR(applyIfCPUFeatureOr = {"sve", "true", "avx", "true", "rvv", "true"}, - counts = {IRNode.VECTOR_CAST_D2I, IRNode.VECTOR_SIZE + "min(max_double, max_int)", ">0"}) + @IR(applyIfCPUFeatureOr = {"sve", "true", "avx", "true", "avx10_2", "true", "rvv", "true"}, + counts = {IRNode.VECTOR_CAST_D2I, IRNode.VECTOR_SIZE + "min(max_double, max_int)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public int[] convertDoubleToInt() { int[] res = new int[SIZE]; for (int i = 0; i < SIZE; i++) { @@ -327,8 +339,12 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { } @Test - @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx512dq", "true", "rvv", "true"}, - counts = {IRNode.VECTOR_CAST_D2L, IRNode.VECTOR_SIZE + "min(max_double, max_long)", ">0"}) + @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx512dq", "true", "avx10_2", "true", "rvv", "true"}, + counts = {IRNode.VECTOR_CAST_D2L, IRNode.VECTOR_SIZE + "min(max_double, max_long)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, + applyIfCPUFeatureAnd = {"avx512dq", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIfCPUFeature = {"avx10_2", "true"}) public long[] convertDoubleToLong() { long[] res = new long[SIZE]; for (int i = 0; i < SIZE; i++) { @@ -339,9 +355,15 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { // ---------------- Convert F/D to Subword-I ---------------- @Test - @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx2", "true", "rvv", "true"}, + @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx2", "true", "avx10_2", "true", "rvv", "true"}, applyIfOr = {"AlignVector", "false", "UseCompactObjectHeaders", "false"}, - counts = {IRNode.VECTOR_CAST_F2S, IRNode.VECTOR_SIZE + "min(max_float, max_short)", ">0"}) + counts = {IRNode.VECTOR_CAST_F2S, IRNode.VECTOR_SIZE + "min(max_float, max_short)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfOr = {"AlignVector", "false", "UseCompactObjectHeaders", "false"}, + applyIfCPUFeatureAnd = {"avx2", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfOr = {"AlignVector", "false", "UseCompactObjectHeaders", "false"}, + applyIfCPUFeature = {"avx10_2", "true"}) public short[] convertFloatToShort() { short[] res = new short[SIZE]; for (int i = 0; i < SIZE; i++) { @@ -358,9 +380,15 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { } @Test - @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx2", "true", "rvv", "true"}, + @IR(applyIfCPUFeatureOr = {"asimd", "true", "avx2", "true", "avx10_2", "true", "rvv", "true"}, applyIfOr = {"AlignVector", "false", "UseCompactObjectHeaders", "false"}, - counts = {IRNode.VECTOR_CAST_F2S, IRNode.VECTOR_SIZE + "min(max_float, max_char)", ">0"}) + counts = {IRNode.VECTOR_CAST_F2S, IRNode.VECTOR_SIZE + "min(max_float, max_char)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_F2X, "> 0"}, + applyIfOr = {"AlignVector", "false", "UseCompactObjectHeaders", "false"}, + applyIfCPUFeatureAnd = {"avx2", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_F2X_AVX10, "> 0"}, + applyIfOr = {"AlignVector", "false", "UseCompactObjectHeaders", "false"}, + applyIfCPUFeature = {"avx10_2", "true"}) public char[] convertFloatToChar() { char[] res = new char[SIZE]; for (int i = 0; i < SIZE; i++) { @@ -379,10 +407,16 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { @Test @IR(applyIfCPUFeature = {"rvv", "true"}, applyIf = {"MaxVectorSize", ">=32"}, - counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_short)", ">0"}) - @IR(applyIfCPUFeatureOr = {"sve", "true", "avx", "true"}, + counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_short)", "> 0"}) + @IR(applyIfCPUFeatureOr = {"sve", "true", "avx", "true", "avx10_2", "true"}, applyIf = {"MaxVectorSize", ">=16"}, - counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_short)", ">0"}) + counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_short)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, + applyIf = {"MaxVectorSize", ">=16"}, + applyIfCPUFeatureAnd = {"avx", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIf = {"MaxVectorSize", ">=16"}, + applyIfCPUFeature = {"avx10_2", "true"}) public short[] convertDoubleToShort() { short[] res = new short[SIZE]; for (int i = 0; i < SIZE; i++) { @@ -393,11 +427,17 @@ public class ArrayTypeConvertTest extends VectorizationTestRunner { @Test @IR(applyIfCPUFeature = {"rvv", "true"}, - applyIf = {"MaxVectorSize", ">=32"}, - counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_char)", ">0"}) - @IR(applyIfCPUFeatureOr = {"sve", "true", "avx", "true"}, + applyIf = {"MaxVectorSize", ">= 32"}, + counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_char)", "> 0"}) + @IR(applyIfCPUFeatureOr = {"sve", "true", "avx", "true", "avx10_2", "true"}, + applyIf = {"MaxVectorSize", ">= 16"}, + counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_char)", "> 0"}) + @IR(counts = {IRNode.X86_VCAST_D2X, "> 0"}, applyIf = {"MaxVectorSize", ">=16"}, - counts = {IRNode.VECTOR_CAST_D2S, IRNode.VECTOR_SIZE + "min(max_double, max_char)", ">0"}) + applyIfCPUFeatureAnd = {"avx", "true", "avx10_2", "false"}) + @IR(counts = {IRNode.X86_VCAST_D2X_AVX10, "> 0"}, + applyIf = {"MaxVectorSize", ">=16"}, + applyIfCPUFeature = {"avx10_2", "true"}) public char[] convertDoubleToChar() { char[] res = new char[SIZE]; for (int i = 0; i < SIZE; i++) { From 5b1ebbb2713e54511cb695d1d6f7f6b7f827b2a7 Mon Sep 17 00:00:00 2001 From: Archie Cobbs Date: Sat, 27 Sep 2025 02:34:27 +0000 Subject: [PATCH 268/556] 8366561: Improve documentation for how the -Xlint flag works Reviewed-by: vromero --- .../com/sun/tools/javac/code/Lint.java | 78 +++++++++++-------- .../com/sun/tools/javac/main/Option.java | 42 ++++++++-- .../tools/javac/resources/javac.properties | 33 ++++---- .../share/classes/module-info.java | 35 ++++++--- src/jdk.compiler/share/man/javac.md | 27 ++++--- 5 files changed, 140 insertions(+), 75 deletions(-) diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java index 8eab238f82a..88c9da5d9e8 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java @@ -161,24 +161,7 @@ public class Lint { values = LintCategory.newEmptySet(); } else { // otherwise, enable on-by-default categories - values = LintCategory.newEmptySet(); - - Source source = Source.instance(context); - if (source.compareTo(Source.JDK9) >= 0) { - values.add(LintCategory.DEP_ANN); - } - if (Source.Feature.REDUNDANT_STRICTFP.allowedInSource(source)) { - values.add(LintCategory.STRICTFP); - } - values.add(LintCategory.REQUIRES_TRANSITIVE_AUTOMATIC); - values.add(LintCategory.OPENS); - values.add(LintCategory.MODULE); - values.add(LintCategory.REMOVAL); - if (!options.isSet(Option.PREVIEW)) { - values.add(LintCategory.PREVIEW); - } - values.add(LintCategory.IDENTITY); - values.add(LintCategory.INCUBATING); + values = getDefaults(); } // Look for specific overrides @@ -193,6 +176,23 @@ public class Lint { suppressedValues = LintCategory.newEmptySet(); } + // Obtain the set of on-by-default categories. Note that for a few categories, + // whether the category is on-by-default depends on other compiler options. + private EnumSet getDefaults() { + EnumSet defaults = LintCategory.newEmptySet(); + Source source = Source.instance(context); + Stream.of(LintCategory.values()) + .filter(lc -> + switch (lc) { + case DEP_ANN -> source.compareTo(Source.JDK9) >= 0; + case STRICTFP -> Source.Feature.REDUNDANT_STRICTFP.allowedInSource(source); + case PREVIEW -> !options.isSet(Option.PREVIEW); + default -> lc.enabledByDefault; + }) + .forEach(defaults::add); + return defaults; + } + @Override public String toString() { initializeRootIfNeeded(); @@ -221,7 +221,7 @@ public class Lint { *

        * This category is not supported by {@code @SuppressWarnings}. */ - CLASSFILE("classfile", false), + CLASSFILE("classfile", false, false), /** * Warn about "dangling" documentation comments, @@ -238,7 +238,7 @@ public class Lint { * Warn about items which are documented with an {@code @deprecated} JavaDoc * comment, but which do not have {@code @Deprecated} annotation. */ - DEP_ANN("dep-ann"), + DEP_ANN("dep-ann", true, true), /** * Warn about division by constant integer 0. @@ -268,7 +268,7 @@ public class Lint { /** * Warn about uses of @ValueBased classes where an identity class is expected. */ - IDENTITY("identity", true, "synchronization"), + IDENTITY("identity", true, true, "synchronization"), /** * Warn about use of incubating modules. @@ -276,7 +276,7 @@ public class Lint { *

        * This category is not supported by {@code @SuppressWarnings}. */ - INCUBATING("incubating", false), + INCUBATING("incubating", false, true), /** * Warn about compiler possible lossy conversions. @@ -291,12 +291,12 @@ public class Lint { /** * Warn about module system related issues. */ - MODULE("module"), + MODULE("module", true, true), /** * Warn about issues regarding module opens. */ - OPENS("opens"), + OPENS("opens", true, true), /** * Warn about issues relating to use of command line options. @@ -304,7 +304,7 @@ public class Lint { *

        * This category is not supported by {@code @SuppressWarnings}. */ - OPTIONS("options", false), + OPTIONS("options", false, false), /** * Warn when any output file is written to more than once. @@ -312,7 +312,7 @@ public class Lint { *

        * This category is not supported by {@code @SuppressWarnings}. */ - OUTPUT_FILE_CLASH("output-file-clash", false), + OUTPUT_FILE_CLASH("output-file-clash", false, false), /** * Warn about issues regarding method overloads. @@ -330,12 +330,15 @@ public class Lint { *

        * This category is not supported by {@code @SuppressWarnings}. */ - PATH("path", false), + PATH("path", false, false), /** * Warn about issues regarding annotation processing. + * + *

        + * This category is not supported by {@code @SuppressWarnings}. */ - PROCESSING("processing"), + PROCESSING("processing", false, false), /** * Warn about unchecked operations on raw types. @@ -345,7 +348,7 @@ public class Lint { /** * Warn about use of deprecated-for-removal items. */ - REMOVAL("removal"), + REMOVAL("removal", true, true), /** * Warn about use of automatic modules in the requires clauses. @@ -355,7 +358,7 @@ public class Lint { /** * Warn about automatic modules in requires transitive. */ - REQUIRES_TRANSITIVE_AUTOMATIC("requires-transitive-automatic"), + REQUIRES_TRANSITIVE_AUTOMATIC("requires-transitive-automatic", true, true), /** * Warn about Serializable classes that do not provide a serial version ID. @@ -370,7 +373,7 @@ public class Lint { /** * Warn about unnecessary uses of the strictfp modifier */ - STRICTFP("strictfp"), + STRICTFP("strictfp", true, true), /** * Warn about issues relating to use of text blocks @@ -400,7 +403,7 @@ public class Lint { /** * Warn about use of preview features. */ - PREVIEW("preview"), + PREVIEW("preview", true, true), /** * Warn about use of restricted methods. @@ -408,12 +411,13 @@ public class Lint { RESTRICTED("restricted"); LintCategory(String option) { - this(option, true); + this(option, true, false); } - LintCategory(String option, boolean annotationSuppression, String... aliases) { + LintCategory(String option, boolean annotationSuppression, boolean enabledByDefault, String... aliases) { this.option = option; this.annotationSuppression = annotationSuppression; + this.enabledByDefault = enabledByDefault; ArrayList optionList = new ArrayList<>(1 + aliases.length); optionList.add(option); Collections.addAll(optionList, aliases); @@ -450,6 +454,12 @@ public class Lint { /** Does this category support being suppressed by the {@code @SuppressWarnings} annotation? */ public final boolean annotationSuppression; + + /** + * Is this category included in the default set of enabled lint categories? + * Note that for some categories, command line options can alter this at runtime. + */ + public final boolean enabledByDefault; } /** diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java index f4122cebb64..8d5ad4c4d78 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java @@ -39,10 +39,12 @@ import java.util.Comparator; import java.util.EnumSet; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; import java.util.Locale; import java.util.ServiceLoader; import java.util.Set; import java.util.StringJoiner; +import java.util.TreeMap; import java.util.TreeSet; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -484,24 +486,54 @@ public enum Option { }, HELP_LINT("--help-lint", "opt.help.lint", EXTENDED, INFO) { - private final String LINT_KEY_FORMAT = SMALL_INDENT + SMALL_INDENT + "%-" + + private final String HELP_INDENT = SMALL_INDENT + SMALL_INDENT; + private final String LINT_KEY_FORMAT = HELP_INDENT + "%-" + (DEFAULT_SYNOPSIS_WIDTH - LARGE_INDENT.length()) + "s %s"; @Override public void process(OptionHelper helper, String option) throws InvalidValueException { Log log = helper.getLog(); + + // Print header log.printRawLines(WriterKind.STDOUT, log.localize(PrefixKind.JAVAC, "opt.help.lint.header")); + + // Print "all" option log.printRawLines(WriterKind.STDOUT, String.format(LINT_KEY_FORMAT, LINT_CUSTOM_ALL, log.localize(PrefixKind.JAVAC, "opt.Xlint.all"))); - LintCategory.options().forEach(ident -> log.printRawLines(WriterKind.STDOUT, - String.format(LINT_KEY_FORMAT, - ident, - log.localize(PrefixKind.JAVAC, "opt.Xlint.desc." + ident)))); + + // Alphabetize all the category names and their aliases together, and then list them with their descriptions + TreeMap keyMap = new TreeMap<>(); + Stream.of(LintCategory.values()).forEach(lc -> + lc.optionList.stream() + .forEach(key -> keyMap.put(key, + String.format(LINT_KEY_FORMAT, + key, + key.equals(lc.option) ? + log.localize(PrefixKind.JAVAC, "opt.Xlint.desc." + key) : + log.localize(PrefixKind.JAVAC, "opt.Xlint.alias.of", lc.option, key))))); + keyMap.values().forEach(desc -> log.printRawLines(WriterKind.STDOUT, desc)); + + // Print "none" option log.printRawLines(WriterKind.STDOUT, String.format(LINT_KEY_FORMAT, LINT_CUSTOM_NONE, log.localize(PrefixKind.JAVAC, "opt.Xlint.none"))); + + // Show which lint categories are enabled by default + log.printRawLines(WriterKind.STDOUT, log.localize(PrefixKind.JAVAC, "opt.help.lint.enabled.by.default")); + String defaults = Stream.of(LintCategory.values()) + .filter(lc -> lc.enabledByDefault) + .map(lc -> lc.option) + .sorted() + .collect(Collectors.joining(", ")); + log.printRawLines(WriterKind.STDOUT, + String.format("%s%s.", HELP_INDENT, defaults)); + + // Add trailing blurb about aliases + List aliasExample = LintCategory.IDENTITY.optionList; + log.printRawLines(WriterKind.STDOUT, + log.localize(PrefixKind.JAVAC, "opt.help.lint.footer", aliasExample.get(0), aliasExample.get(1))); super.process(helper, option); } }, diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties index 6c3238cfdfe..15a63da06eb 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties @@ -168,18 +168,20 @@ javac.opt.Xbootclasspath.p=\ javac.opt.Xbootclasspath.a=\ Append to the bootstrap class path javac.opt.Xlint=\ - Enable recommended warning categories + Enable recommended lint warning categories. In this release, all\n\ + available lint warning categories are recommended. javac.opt.Xlint.all=\ - Enable all warning categories + Enable all lint warning categories javac.opt.Xlint.none=\ - Disable all warning categories + Disable all lint warning categories #L10N: do not localize: -Xlint javac.opt.arg.Xlint=\ (,)* javac.opt.Xlint.custom=\ - Warning categories to enable or disable, separated by comma.\n\ - Precede a key by ''-'' to disable the specified warning.\n\ - Use --help-lint to see the supported keys. + Lint warning categories to enable or disable, separated by comma.\n\ + Precede a key by ''-'' to disable the specified category. Use\n\ + ''--help-lint'' to show supported keys and which categories are\n\ + enabled by default. javac.opt.Xlint.desc.auxiliaryclass=\ Warn about an auxiliary class that is hidden in a source file, and is used from other files. @@ -291,16 +293,13 @@ javac.opt.Xlint.desc.preview=\ javac.opt.Xlint.desc.restricted=\ Warn about use of restricted methods. -# L10N: do not localize: identity synchronization -javac.opt.Xlint.desc.synchronization=\ - Warn about synchronization attempts on instances of value-based classes.\n\ -\ This key is a deprecated alias for ''identity'', which has the same uses and\n\ -\ effects. Users are encouraged to use the ''identity'' category for all future\n\ -\ and existing uses of ''synchronization''. - javac.opt.Xlint.desc.identity=\ Warn about uses of value-based classes where an identity class is expected. +javac.opt.Xlint.alias.of=\ + Deprecated alias for ''{0}'' with an identical effect. Users are encouraged to use\n\ +\ ''{0}'' instead of ''{1}'' for all current and future uses. + javac.opt.Xdoclint=\ Enable recommended checks for problems in javadoc comments # L10N: do not localize: all none @@ -334,6 +333,11 @@ javac.opt.help.lint=\ Print the supported keys for -Xlint javac.opt.help.lint.header=\ The supported keys for -Xlint are: +javac.opt.help.lint.enabled.by.default=\ + The following lint warning categories are enabled by default: +javac.opt.help.lint.footer=\ + Categories and their aliases can be used interchangeably; for example, the flag\n\ + ''-Xlint:{0},{1}'' would be redundant. javac.opt.print=\ Print out a textual representation of specified types javac.opt.printRounds=\ @@ -346,8 +350,9 @@ javac.opt.userpathsfirst=\ javac.opt.prefer=\ Specify which file to read when both a source file and class file\n\ are found for an implicitly compiled class +# L10N: do not localize: ''preview'' javac.opt.preview=\ - Enable preview language features.\n\ + Enable preview language features. Also disables the ''preview'' lint category.\n\ To be used in conjunction with either -source or --release. javac.opt.AT=\ Read options and filenames from file diff --git a/src/jdk.compiler/share/classes/module-info.java b/src/jdk.compiler/share/classes/module-info.java index 33cff9379f2..46ada3a12e7 100644 --- a/src/jdk.compiler/share/classes/module-info.java +++ b/src/jdk.compiler/share/classes/module-info.java @@ -141,7 +141,8 @@ import javax.tools.StandardLocation; * * In addition, javac also supports other strings that can be used * to suppress other kinds of warnings. The following table lists all the - * strings that can be used with {@code @SuppressWarnings}. + * strings that are recognized by javac in {@code @SuppressWarnings} + * annotations. Unrecognized strings are ignored. * * * @@ -152,7 +153,6 @@ import javax.tools.StandardLocation; * *
        Strings supported by {@code SuppressWarnings}
        {@code auxiliaryclass} an auxiliary class that is hidden in a source file, and is used * from other files *
        {@code cast} use of unnecessary casts - *
        {@code classfile} issues related to classfile contents *
        {@code dangling-doc-comments} issues related to "dangling" documentation comments, * not attached to a declaration *
        {@code deprecation} use of deprecated items @@ -165,7 +165,6 @@ import javax.tools.StandardLocation; * the next *
        {@code finally} {@code finally} clauses that do not terminate normally *
        {@code identity} use of a value-based class where an identity class is expected - *
        {@code incubating} use of incubating modules *
        {@code lossy-conversions} possible lossy conversions in compound assignment *
        {@code missing-explicit-ctor} missing explicit constructors in public and protected classes * in exported packages @@ -173,13 +172,12 @@ import javax.tools.StandardLocation; *
        {@code opens} issues regarding module opens *
        {@code overloads} issues regarding method overloads *
        {@code overrides} issues regarding method overrides - *
        {@code path} invalid path elements on the command line *
        {@code preview} use of preview language features *
        {@code rawtypes} use of raw types *
        {@code removal} use of API that has been marked for removal - *
        {@code restricted} use of restricted methods *
        {@code requires-automatic} use of automatic modules in the {@code requires} clauses *
        {@code requires-transitive-automatic} automatic modules in {@code requires transitive} + *
        {@code restricted} use of restricted methods *
        {@code serial} {@link java.base/java.io.Serializable Serializable} classes * that do not have a {@code serialVersionUID} field, or other * suspect declarations in {@code Serializable} and @@ -187,11 +185,9 @@ import javax.tools.StandardLocation; * and interfaces *
        {@code static} accessing a static member using an instance *
        {@code strictfp} unnecessary use of the {@code strictfp} modifier - *
        {@code synchronization} synchronization attempts on instances of value-based classes; - * this key is a deprecated alias for {@code identity}, which has - * the same uses and effects. Users are encouraged to use the - * {@code identity} category for all future and existing uses of - * {@code synchronization} + *
        {@code synchronization} deprecated alias for {@code identity} with an identical effect. + * Users are encouraged to use {@code identity} instead of + * {@code synchronization} for all current and future uses. *
        {@code text-blocks} inconsistent white space characters in text block indentation *
        {@code this-escape} superclass constructor leaking {@code this} before subclass initialized *
        {@code try} issues relating to use of {@code try} blocks @@ -207,6 +203,25 @@ import javax.tools.StandardLocation; *
        * + * All of the non-{@code docllint:} strings listed above may also be used with the {@code -Xlint} command line flag. + * The {@code -Xlint} flag also supports these strings not supported by {@code @SuppressWarnings}: + * + * + * + * + * + * + * + *
        Strings supported by {@code -Xlint} but not {@code SuppressWarnings}
        StringWarnings Related To ... + *
        {@code classfile} issues related to classfile contents + *
        {@code incubating} use of incubating modules + *
        {@code options} issues relating to use of command line options + *
        {@code output-file-clash} output files being overwritten due to filename clashes + *
        {@code path} invalid path elements on the command line + *
        {@code processing} issues regarding annotation processing + *
        {@code restricted} use of restricted methods + *
        + * * @toolGuide javac * * @provides java.util.spi.ToolProvider diff --git a/src/jdk.compiler/share/man/javac.md b/src/jdk.compiler/share/man/javac.md index 997023487b0..b8243cc78fb 100644 --- a/src/jdk.compiler/share/man/javac.md +++ b/src/jdk.compiler/share/man/javac.md @@ -208,8 +208,8 @@ file system locations may be directories, JAR files or JMOD files. `-deprecation` option is shorthand for `-Xlint:deprecation`. `--enable-preview` -: Enables preview language features. Used in conjunction with either - [`-source`](#option-source) or [`--release`](#option-release). +: Enables preview language features. Also disables the `preview` lint category. + Used in conjunction with either [`-source`](#option-source) or [`--release`](#option-release). `-encoding` *encoding* : Specifies character encoding used by source files, such as EUC-JP and @@ -557,11 +557,11 @@ file system locations may be directories, JAR files or JMOD files. section of the `javadoc` command documentation. `-Xlint` -: Enables all recommended warnings. In this release, enabling all available - warnings is recommended. +: Enables recommended lint warning categories. In this release, all available + lint warning categories are recommended. `-Xlint:`\[`-`\]*key*(`,`\[`-`\]*key*)\* -: Enables and/or disables warning categories using the one or more of the keys described +: Enables and/or disables lint warning categories using the one or more of the keys described below separated by commas. The keys `all` and `none` enable or disable all categories (respectively); other keys enable the corresponding category, or disable it if preceded by a hyphen (`-`). @@ -648,10 +648,9 @@ file system locations may be directories, JAR files or JMOD files. - `strictfp`: Warns about unnecessary use of the `strictfp` modifier. - - `synchronization`: Warns about synchronization attempts on instances - of value-based classes. This key is a deprecated alias for `identity`, - which has the same uses and effects. Users are encouraged to use the - `identity` category for all future and existing uses of `synchronization`. + - `synchronization`: Deprecated alias for `identity` with an identical + effect. Users are encouraged to use `identity` instead of `synchronization` + for all current and future uses. - `text-blocks`: Warns about inconsistent white space characters in text block indentation. @@ -667,9 +666,13 @@ file system locations may be directories, JAR files or JMOD files. - `none`: Disables all warning categories. - With the exception of `all` and `none`, the keys can be used with - the `@SuppressWarnings` annotation to suppress warnings in a part - of the source code being compiled. + The keys listed above may be used in `@SuppressWarnings` annotations to suppress + warnings within the annotated declaration, with the exception of: `all`, `none`, + `classfile`, `incubating`, `options`, `output-file-clash`, `processing`, and `path`. + + By default, the following lint warning categories are enabled: `dep-ann`, `identity`, + `incubating`, `module`, `opens`, `preview`, `removal`, `requires-transitive-automatic`, + and `strictfp`. See [Examples of Using -Xlint keys]. From 9093d3a04cd2b66425cefb44de2990cb5362a29f Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Sat, 27 Sep 2025 02:37:39 +0000 Subject: [PATCH 269/556] 8368668: Several vmTestbase/vm/gc/compact tests timed out on large memory machine Reviewed-by: lmesnik --- .../AllocateWithoutOomTest/AllocateWithoutOomTest.java | 4 ++-- .../Compact_InternedStrings_Strings/TestDescription.java | 4 ++-- .../vm/gc/compact/Compact_NonbranchyTree/TestDescription.java | 4 ++-- .../Compact_NonbranchyTree_ArrayOf/TestDescription.java | 4 ++-- .../Compact_NonbranchyTree_TwoFields/TestDescription.java | 4 ++-- .../vm/gc/compact/Compact_Strings/TestDescription.java | 4 ++-- .../Compact_Strings_InternedStrings/TestDescription.java | 4 ++-- .../gc/compact/Compact_Strings_TwoFields/TestDescription.java | 4 ++-- .../gc/compact/Humongous_NonbranchyTree/TestDescription.java | 4 ++-- .../vm/gc/compact/Humongous_Strings/TestDescription.java | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test/hotspot/jtreg/vmTestbase/gc/gctests/AllocateWithoutOomTest/AllocateWithoutOomTest.java b/test/hotspot/jtreg/vmTestbase/gc/gctests/AllocateWithoutOomTest/AllocateWithoutOomTest.java index 51307bb996c..a1bd7acedeb 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/gctests/AllocateWithoutOomTest/AllocateWithoutOomTest.java +++ b/test/hotspot/jtreg/vmTestbase/gc/gctests/AllocateWithoutOomTest/AllocateWithoutOomTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,7 +37,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * gc.gctests.AllocateWithoutOomTest.AllocateWithoutOomTest */ diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_InternedStrings_Strings/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_InternedStrings_Strings/TestDescription.java index 4e11e51a894..ad7db17e412 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_InternedStrings_Strings/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_InternedStrings_Strings/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp interned(randomString) diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree/TestDescription.java index b058bb2c84d..be4f9f673b7 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp nonbranchyTree(high) diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_ArrayOf/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_ArrayOf/TestDescription.java index 3beeaa76a84..2ca21ae2ee6 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_ArrayOf/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_ArrayOf/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp nonbranchyTree(high) diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_TwoFields/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_TwoFields/TestDescription.java index c1421263a48..01dc687251b 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_TwoFields/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_NonbranchyTree_TwoFields/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp nonbranchyTree(high) diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings/TestDescription.java index 207822c666b..9ba96b7e493 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp randomString diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_InternedStrings/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_InternedStrings/TestDescription.java index 705e47ea68b..30dcfc7502e 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_InternedStrings/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_InternedStrings/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp randomString diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_TwoFields/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_TwoFields/TestDescription.java index 7d5ddc36fcb..8a62d4b07eb 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_TwoFields/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Compact_Strings_TwoFields/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp randomString diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_NonbranchyTree/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_NonbranchyTree/TestDescription.java index 38c5b1e6bab..c9c117f0b0e 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_NonbranchyTree/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_NonbranchyTree/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp nonbranchyTree(high) diff --git a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_Strings/TestDescription.java b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_Strings/TestDescription.java index 88d11739141..7d4b6ae3a21 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_Strings/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/vm/gc/compact/Humongous_Strings/TestDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -36,7 +36,7 @@ * * @library /vmTestbase * /test/lib - * @run main/othervm + * @run main/othervm/timeout=480 * -XX:-UseGCOverheadLimit * vm.gc.compact.Compact * -gp randomString From af8fb20ac0325a231ee14bd72e9764e02ca07681 Mon Sep 17 00:00:00 2001 From: Kelvin Nilsen Date: Sat, 27 Sep 2025 04:07:29 +0000 Subject: [PATCH 270/556] 8368307: Shenandoah: get_next_bit_impl should special case weak and strong mark bits Reviewed-by: wkemper --- .../gc/shenandoah/shenandoahMarkBitMap.inline.hpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahMarkBitMap.inline.hpp b/src/hotspot/share/gc/shenandoah/shenandoahMarkBitMap.inline.hpp index 637948e2615..3bea8d73959 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahMarkBitMap.inline.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahMarkBitMap.inline.hpp @@ -135,14 +135,12 @@ inline ShenandoahMarkBitMap::idx_t ShenandoahMarkBitMap::get_next_bit_impl(idx_t // Get the word containing l_index, and shift out low bits. idx_t index = to_words_align_down(l_index); bm_word_t cword = (map(index) ^ flip) >> bit_in_word(l_index); - if ((cword & 1) != 0) { - // The first bit is similarly often interesting. When it matters - // (density or features of the calling algorithm make it likely - // the first bit is set), going straight to the next clause compares - // poorly with doing this check first; count_trailing_zeros can be - // relatively expensive, plus there is the additional range check. - // But when the first bit isn't set, the cost of having tested for - // it is relatively small compared to the rest of the search. + if ((cword & 0x03) != 0) { + // The first bits (representing weak mark or strong mark) are similarly often interesting. When it matters + // (density or features of the calling algorithm make it likely the first bits are set), going straight to + // the next clause compares poorly with doing this check first; count_trailing_zeros can be relatively expensive, + // plus there is the additional range check. But when the first bits are not set, the cost of having tested for + // them is relatively small compared to the rest of the search. return l_index; } else if (cword != 0) { // Flipped and shifted first word is non-zero. From 320230db5f9ca95f23218704cb2e69521e03852f Mon Sep 17 00:00:00 2001 From: Sergey Bylokhov Date: Sun, 28 Sep 2025 04:49:58 +0000 Subject: [PATCH 271/556] 8367795: HeadlessMalfunctionTest may fail due to timeout Reviewed-by: prr --- test/jdk/java/awt/Headless/HeadlessMalfunctionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/java/awt/Headless/HeadlessMalfunctionTest.java b/test/jdk/java/awt/Headless/HeadlessMalfunctionTest.java index 61ebf1df3e8..e9ae66492d4 100644 --- a/test/jdk/java/awt/Headless/HeadlessMalfunctionTest.java +++ b/test/jdk/java/awt/Headless/HeadlessMalfunctionTest.java @@ -38,7 +38,7 @@ import java.nio.file.Path; * @run driver jdk.test.lib.helpers.ClassFileInstaller * HeadlessMalfunctionAgent * HeadlessMalfunctionAgent$1 - * @run driver HeadlessMalfunctionTest + * @run driver/timeout=240 HeadlessMalfunctionTest */ public class HeadlessMalfunctionTest { From e19ec6f785e889d254b15c5ef2e801152c59c948 Mon Sep 17 00:00:00 2001 From: Axel Boldt-Christmas Date: Mon, 29 Sep 2025 05:19:56 +0000 Subject: [PATCH 272/556] 8368754: runtime/cds/appcds/SignedJar.java log regex is too strict Reviewed-by: iklam, dholmes --- test/hotspot/jtreg/runtime/cds/appcds/SignedJar.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/runtime/cds/appcds/SignedJar.java b/test/hotspot/jtreg/runtime/cds/appcds/SignedJar.java index 1ad28f99408..9e6cb58bb7f 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/SignedJar.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/SignedJar.java @@ -50,8 +50,8 @@ public class SignedJar { String skipMsg = "Skipping Hello: Signed JAR"; String lambdaInArchive = "klasses.*=.*app.*Hello[$][$]Lambda.*hidden"; - String loadFromJar = ".class,load. Hello source: file:.*signed_hello.jar"; - String lambdaLoadFromHello = ".class.load. Hello[$][$]Lambda.*/0x.*source.*Hello"; + String loadFromJar = ".class,load\s*. Hello source: file:.*signed_hello.jar"; + String lambdaLoadFromHello = ".class.load\s*. Hello[$][$]Lambda.*/0x.*source.*Hello"; for (String mainArg : mainArgs) { output = TestCommon.dump(signedJar, TestCommon.list(mainClass), From d53190ac4485e535f0a603036ecf47d4ff6e4178 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 29 Sep 2025 05:36:18 +0000 Subject: [PATCH 273/556] 8366582: Test jdk/jshell/ToolSimpleTest.java failed: provider not found Reviewed-by: asotona --- .../share/classes/jdk/jshell/SourceCodeAnalysisImpl.java | 7 ++++++- test/langtools/ProblemList.txt | 2 -- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java index 045734d9f55..f27441b7262 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java @@ -2303,7 +2303,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { //if an index exists for the given entry, the existing index is kept unless the timestamp is modified private ClassIndex indexForPath(Path path) { if (!Files.isDirectory(path)) { - if (Files.exists(path)) { + if (Files.exists(path) && + !isJRTMarkerFile(path)) { //don't directly index lib/modules return PATH_TO_INDEX.compute(path, (p, index) -> { try { long lastModified = Files.getLastModifiedTime(p).toMillis(); @@ -2334,6 +2335,10 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { } } + private static boolean isJRTMarkerFile(Path path) { + return path.equals(Paths.get(System.getProperty("java.home"), "lib", "modules")); + } + //create an index based on the content of the given dirs; the original JavaFileManager entry is originalPath. private ClassIndex doIndex(long timestamp, Path originalPath, Iterable dirs) { Set packages = new HashSet<>(); diff --git a/test/langtools/ProblemList.txt b/test/langtools/ProblemList.txt index 59b5b240a29..9d61a20663d 100644 --- a/test/langtools/ProblemList.txt +++ b/test/langtools/ProblemList.txt @@ -52,8 +52,6 @@ jdk/jshell/UserJdiUserRemoteTest.java jdk/jshell/UserInputTest.java 8169536 generic-all jdk/jshell/ToolBasicTest.java 8265357 macosx-aarch64 jdk/jshell/HighlightUITest.java 8284144 generic-all -jdk/jshell/ToolSimpleTest.java 8366582 generic-all -jdk/jshell/ToolLocalSimpleTest.java 8366582 generic-all ########################################################################### # From 75269fdb49aeb9d37acbbc1502c446a822fd30e3 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Mon, 29 Sep 2025 07:26:43 +0000 Subject: [PATCH 274/556] 8368715: Serial: Add GCTraceTime for marking from roots subphases during full gc marking Reviewed-by: fandreuzzi, tschatzl, iwalulya --- src/hotspot/share/gc/serial/serialFullGC.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hotspot/share/gc/serial/serialFullGC.cpp b/src/hotspot/share/gc/serial/serialFullGC.cpp index 271b4b4ebdb..76a335d209f 100644 --- a/src/hotspot/share/gc/serial/serialFullGC.cpp +++ b/src/hotspot/share/gc/serial/serialFullGC.cpp @@ -482,6 +482,8 @@ void SerialFullGC::phase1_mark(bool clear_all_softrefs) { ref_processor()->start_discovery(clear_all_softrefs); { + GCTraceTime(Debug, gc, phases) tm_m("Marking From Roots", gc_timer()); + // Start tracing from roots, there are 3 kinds of roots in full-gc. // // 1. CLD. This method internally takes care of whether class loading is From 08b677bba4b1e23feb55b104d86fe0eef543d59c Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Mon, 29 Sep 2025 10:05:45 +0000 Subject: [PATCH 275/556] 8071277: G1: Merge commits and uncommits of contiguous memory Reviewed-by: iwalulya, ayang --- src/hotspot/share/gc/g1/g1NUMA.cpp | 4 +- .../share/gc/g1/g1RegionToSpaceMapper.cpp | 102 +++++++++++------- .../share/gc/g1/g1RegionToSpaceMapper.hpp | 2 + 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1NUMA.cpp b/src/hotspot/share/gc/g1/g1NUMA.cpp index cd7dc55d0fe..778ed31d7b5 100644 --- a/src/hotspot/share/gc/g1/g1NUMA.cpp +++ b/src/hotspot/share/gc/g1/g1NUMA.cpp @@ -203,9 +203,7 @@ uint G1NUMA::index_for_region(G1HeapRegion* hr) const { // * G1HeapRegion #: |-#0-||-#1-||-#2-||-#3-||-#4-||-#5-||-#6-||-#7-||-#8-||-#9-||#10-||#11-||#12-||#13-||#14-||#15-| // * NUMA node #: |----#0----||----#1----||----#2----||----#3----||----#0----||----#1----||----#2----||----#3----| void G1NUMA::request_memory_on_node(void* aligned_address, size_t size_in_bytes, uint region_index) { - if (!is_enabled()) { - return; - } + assert(is_enabled(), "must be, check before"); if (size_in_bytes == 0) { return; diff --git a/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp b/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp index 1710ceaf73a..5e37c7fa5a1 100644 --- a/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp +++ b/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.cpp @@ -96,13 +96,15 @@ class G1RegionsLargerThanCommitSizeMapper : public G1RegionToSpaceMapper { const size_t start_page = (size_t)start_idx * _pages_per_region; const size_t size_in_pages = num_regions * _pages_per_region; bool zero_filled = _storage.commit(start_page, size_in_pages); - if (_memory_tag == mtJavaHeap) { + + if (should_distribute_across_numa_nodes()) { for (uint region_index = start_idx; region_index < start_idx + num_regions; region_index++ ) { void* address = _storage.page_start(region_index * _pages_per_region); size_t size_in_bytes = _storage.page_size() * _pages_per_region; G1NUMA::numa()->request_memory_on_node(address, size_in_bytes, region_index); } } + if (AlwaysPreTouch) { _storage.pretouch(start_page, size_in_pages, pretouch_workers); } @@ -122,7 +124,7 @@ class G1RegionsLargerThanCommitSizeMapper : public G1RegionToSpaceMapper { // G1RegionToSpaceMapper implementation where the region granularity is smaller // than the commit granularity. -// Basically, the contents of one OS page span several regions. +// Basically, the contents of one OS page spans several regions. class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { size_t _regions_per_page; // Lock to prevent bitmap updates and the actual underlying @@ -148,13 +150,18 @@ class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { return _region_commit_map.find_first_set_bit(region, region_limit) != region_limit; } - void numa_request_on_node(size_t page_idx) { - if (_memory_tag == mtJavaHeap) { - uint region = (uint)(page_idx * _regions_per_page); - void* address = _storage.page_start(page_idx); - size_t size_in_bytes = _storage.page_size(); - G1NUMA::numa()->request_memory_on_node(address, size_in_bytes, region); + bool commit_pages(size_t start_page, size_t size_in_pages) { + bool result = _storage.commit(start_page, size_in_pages); + + if (should_distribute_across_numa_nodes()) { + for (size_t page = start_page; page < start_page + size_in_pages; page++) { + uint region = checked_cast(page * _regions_per_page); + void* address = _storage.page_start(page); + size_t size_in_bytes = _storage.page_size(); + G1NUMA::numa()->request_memory_on_node(address, size_in_bytes, region); + } } + return result; } public: @@ -171,6 +178,21 @@ class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { guarantee((page_size * commit_factor) >= alloc_granularity, "allocation granularity smaller than commit granularity"); } + size_t find_first_uncommitted(size_t page, size_t end) { + assert(page < end, "must be"); + while (page < end && is_page_committed(page)) { + page++; + } + return page; + } + + size_t find_first_committed(size_t page, size_t end) { + while (page < end && !is_page_committed(page)) { + page++; + } + return MIN2(page, end); + } + virtual void commit_regions(uint start_idx, size_t num_regions, WorkerThreads* pretouch_workers) { uint region_limit = (uint)(start_idx + num_regions); assert(num_regions > 0, "Must commit at least one region"); @@ -179,11 +201,11 @@ class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { size_t const NoPage = SIZE_MAX; - size_t first_committed = NoPage; - size_t num_committed = 0; + size_t first_newly_committed = NoPage; + size_t num_committed_pages = 0; - size_t start_page = region_idx_to_page_idx(start_idx); - size_t end_page = region_idx_to_page_idx(region_limit - 1); + size_t const start_page = region_idx_to_page_idx(start_idx); + size_t const end_page = region_idx_to_page_idx(region_limit - 1) + 1; bool all_zero_filled = true; @@ -191,34 +213,27 @@ class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { // underlying OS page. See lock declaration for more details. { MutexLocker ml(&_lock, Mutex::_no_safepoint_check_flag); - for (size_t page = start_page; page <= end_page; page++) { - if (!is_page_committed(page)) { - // Page not committed. - if (num_committed == 0) { - first_committed = page; - } - num_committed++; - if (!_storage.commit(page, 1)) { - // Found dirty region during commit. - all_zero_filled = false; - } + size_t uncommitted_l = find_first_uncommitted(start_page, end_page); + size_t uncommitted_r = find_first_committed(uncommitted_l + 1, end_page); - // Move memory to correct NUMA node for the heap. - numa_request_on_node(page); - } else { - // Page already committed. - all_zero_filled = false; - } + first_newly_committed = uncommitted_l; + num_committed_pages = uncommitted_r - uncommitted_l; + + if (num_committed_pages > 0 && + !commit_pages(first_newly_committed, num_committed_pages)) { + all_zero_filled = false; } + all_zero_filled &= (uncommitted_l == start_page) && (uncommitted_r == end_page); + // Update the commit map for the given range. Not using the par_set_range // since updates to _region_commit_map for this mapper is protected by _lock. _region_commit_map.set_range(start_idx, region_limit, BitMap::unknown_range); } - if (AlwaysPreTouch && num_committed > 0) { - _storage.pretouch(first_committed, num_committed, pretouch_workers); + if (AlwaysPreTouch && num_committed_pages > 0) { + _storage.pretouch(first_newly_committed, num_committed_pages, pretouch_workers); } fire_on_commit(start_idx, num_regions, all_zero_filled); @@ -230,8 +245,8 @@ class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { assert(_region_commit_map.find_first_clear_bit(start_idx, region_limit) == region_limit, "Should only be committed regions in the range [%u, %u)", start_idx, region_limit); - size_t start_page = region_idx_to_page_idx(start_idx); - size_t end_page = region_idx_to_page_idx(region_limit - 1); + size_t const start_page = region_idx_to_page_idx(start_idx); + size_t const end_page = region_idx_to_page_idx(region_limit - 1) + 1; // Concurrent operations might operate on regions sharing the same // underlying OS page. See lock declaration for more details. @@ -240,13 +255,16 @@ class G1RegionsSmallerThanCommitSizeMapper : public G1RegionToSpaceMapper { // updates to _region_commit_map for this mapper is protected by _lock. _region_commit_map.clear_range(start_idx, region_limit, BitMap::unknown_range); - for (size_t page = start_page; page <= end_page; page++) { - // We know all pages were committed before clearing the map. If the - // the page is still marked as committed after the clear we should - // not uncommit it. - if (!is_page_committed(page)) { - _storage.uncommit(page, 1); - } + // We know all pages were committed before clearing the map. If the + // the page is still marked as committed after the clear we should + // not uncommit it. + size_t uncommitted_l = find_first_uncommitted(start_page, end_page); + size_t uncommitted_r = find_first_committed(uncommitted_l + 1, end_page); + + size_t num_uncommitted_pages_found = uncommitted_r - uncommitted_l; + + if (num_uncommitted_pages_found > 0) { + _storage.uncommit(uncommitted_l, num_uncommitted_pages_found); } } }; @@ -257,6 +275,10 @@ void G1RegionToSpaceMapper::fire_on_commit(uint start_idx, size_t num_regions, b } } +bool G1RegionToSpaceMapper::should_distribute_across_numa_nodes() const { + return _memory_tag == mtJavaHeap && G1NUMA::numa()->is_enabled(); +} + G1RegionToSpaceMapper* G1RegionToSpaceMapper::create_mapper(ReservedSpace rs, size_t actual_size, size_t page_size, diff --git a/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.hpp b/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.hpp index 823fa549f36..e2fc3d4fa04 100644 --- a/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.hpp +++ b/src/hotspot/share/gc/g1/g1RegionToSpaceMapper.hpp @@ -58,6 +58,8 @@ class G1RegionToSpaceMapper : public CHeapObj { G1RegionToSpaceMapper(ReservedSpace rs, size_t used_size, size_t page_size, size_t region_granularity, size_t commit_factor, MemTag mem_tag); void fire_on_commit(uint start_idx, size_t num_regions, bool zero_filled); + + bool should_distribute_across_numa_nodes() const; public: MemRegion reserved() { return _storage.reserved(); } From 616592144939d80cae661bd4db26c976a035d543 Mon Sep 17 00:00:00 2001 From: Daniel Gredler Date: Mon, 29 Sep 2025 10:28:45 +0000 Subject: [PATCH 276/556] 7156751: [macosx] Problem with printing Reviewed-by: prr, serb --- .../macosx/native/libawt_lwawt/awt/CTextPipe.m | 10 ++++++---- test/jdk/java/awt/print/PrinterJob/PrintTextTest.java | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m b/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m index 9da3b6648fb..f1c12585728 100644 --- a/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m +++ b/src/java.desktop/macosx/native/libawt_lwawt/awt/CTextPipe.m @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -457,15 +457,17 @@ static inline void doDrawGlyphsPipe_fillGlyphAndAdvanceBuffers } } if (positions != NULL) { + + CGAffineTransform invTx = CGAffineTransformInvert(strike->fFontTx); + CGPoint prev; prev.x = positions[0]; prev.y = positions[1]; + prev = CGPointApplyAffineTransform(prev, invTx); // take the first point, and move the context to that location CGContextTranslateCTM(qsdo->cgRef, prev.x, prev.y); - CGAffineTransform invTx = CGAffineTransformInvert(strike->fFontTx); - // for each position, figure out the advance (since CG won't take positions directly) size_t i; for (i = 0; i < length - 1; i++) @@ -476,7 +478,7 @@ static inline void doDrawGlyphsPipe_fillGlyphAndAdvanceBuffers pt.y = positions[i2+1]; pt = CGPointApplyAffineTransform(pt, invTx); advances[i].width = pt.x - prev.x; - advances[i].height = -(pt.y - prev.y); // negative to translate to device space + advances[i].height = pt.y - prev.y; prev.x = pt.x; prev.y = pt.y; } diff --git a/test/jdk/java/awt/print/PrinterJob/PrintTextTest.java b/test/jdk/java/awt/print/PrinterJob/PrintTextTest.java index 7fb1167a847..06e71c4067f 100644 --- a/test/jdk/java/awt/print/PrinterJob/PrintTextTest.java +++ b/test/jdk/java/awt/print/PrinterJob/PrintTextTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,7 +23,7 @@ /* * @test - * @bug 6425068 7157659 8029204 8132890 8148334 8344637 + * @bug 6425068 7156751 7157659 8029204 8132890 8148334 8344637 * @key printer * @summary Confirm that text prints where we expect to the length we expect. * @library /java/awt/regtesthelpers From fdbba049a2491c591fc1a866e4707bf9aac50f17 Mon Sep 17 00:00:00 2001 From: Daniel Gredler Date: Mon, 29 Sep 2025 10:39:25 +0000 Subject: [PATCH 277/556] 8368775: Remove outdated comment in OutlineTextRenderer Reviewed-by: prr, dnguyen, serb --- .../share/classes/sun/java2d/pipe/OutlineTextRenderer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java b/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java index b657e434d2f..949a914990b 100644 --- a/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java +++ b/src/java.desktop/share/classes/sun/java2d/pipe/OutlineTextRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -73,8 +73,8 @@ public class OutlineTextRenderer implements TextPipe { public void drawString(SunGraphics2D g2d, String str, double x, double y) { - if ("".equals(str)) { - return; // TextLayout constructor throws IAE on "". + if (str.length() == 0) { + return; } TextLayout tl = new TextLayout(str, g2d.getFont(), g2d.getFontRenderContext()); From 9d9c0e06700116288233e3435051a1496cb64b72 Mon Sep 17 00:00:00 2001 From: Roger Riggs Date: Mon, 29 Sep 2025 13:55:49 +0000 Subject: [PATCH 278/556] 8368793: java/lang/StringBuilder/RacingSBThreads.java timed out in Xcomp subtest Reviewed-by: iris, alanb, syan --- test/jdk/java/lang/StringBuilder/RacingSBThreads.java | 1 - 1 file changed, 1 deletion(-) diff --git a/test/jdk/java/lang/StringBuilder/RacingSBThreads.java b/test/jdk/java/lang/StringBuilder/RacingSBThreads.java index 26f5cf9385a..8affa3feffa 100644 --- a/test/jdk/java/lang/StringBuilder/RacingSBThreads.java +++ b/test/jdk/java/lang/StringBuilder/RacingSBThreads.java @@ -28,7 +28,6 @@ * @run main/othervm -esa RacingSBThreads read * @run main/othervm -esa RacingSBThreads insert * @run main/othervm -esa RacingSBThreads append - * @run main/othervm -Xcomp RacingSBThreads */ import java.nio.CharBuffer; From 63688d894e2157bb091be3aa62946f7e5830f384 Mon Sep 17 00:00:00 2001 From: Joe Darcy Date: Mon, 29 Sep 2025 14:48:04 +0000 Subject: [PATCH 279/556] 8368822: Refactor Float16.valueOf(double) Reviewed-by: rgiulietti --- .../classes/jdk/incubator/vector/Float16.java | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java index 45dd52175cc..2173c741e38 100644 --- a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java +++ b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java @@ -222,6 +222,16 @@ public final class Float16 */ public static final int BYTES = SIZE / Byte.SIZE; + /** + * The overflow threshold (for round to nearest) is MAX_VALUE + 1/2 ulp. + */ + private static final double OVERFLOW_THRESH = 0x1.ffcp15 + 0x0.002p15; + + /** + * The underflow threshold (for round to nearest) is MIN_VALUE * 0.5. + */ + private static final double UNDERFLOW_THRESH = 0x1.0p-24d * 0.5d; + /** * Returns a string representation of the {@code Float16} * argument. @@ -340,51 +350,51 @@ public final class Float16 * @param d a {@code double} */ public static Float16 valueOf(double d) { - long doppel = Double.doubleToRawLongBits(d); - - short sign_bit = (short)((doppel & 0x8000_0000_0000_0000L) >> 48); - if (Double.isNaN(d)) { // Have existing float code handle any attempts to // preserve NaN bits. return valueOf((float)d); } + long doppel = Double.doubleToRawLongBits(d); + short sign_bit = (short)((doppel & 0x8000_0000_0000_0000L) >> (64 - 16)); double abs_d = Math.abs(d); - // The overflow threshold is binary16 MAX_VALUE + 1/2 ulp - if (abs_d >= (0x1.ffcp15 + 0x0.002p15) ) { + if (abs_d >= OVERFLOW_THRESH) { // correctly signed infinity return new Float16((short)(sign_bit | 0x7c00)); } - // Smallest magnitude nonzero representable binary16 value - // is equal to 0x1.0p-24; half-way and smaller rounds to zero. - if (abs_d <= 0x1.0p-24d * 0.5d) { // Covers double zeros and subnormals. - return new Float16(sign_bit); // Positive or negative zero + if (abs_d <= UNDERFLOW_THRESH) { // Covers double zeros and subnormals. + // positive or negative zero + return new Float16(sign_bit); } // Dealing with finite values in exponent range of binary16 // (when rounding is done, could still round up) int exp = Math.getExponent(d); - assert -25 <= exp && exp <= 15; + assert + (MIN_EXPONENT - PRECISION) <= exp && + exp <= MAX_EXPONENT; - // For binary16 subnormals, beside forcing exp to -15, retain - // the difference expdelta = E_min - exp. This is the excess - // shift value, in addition to 42, to be used in the + // For target format subnormals, beside forcing exp to + // MIN_EXPONENT-1, retain the difference expdelta = E_min - + // exp. This is the excess shift value, in addition to the + // difference in precision bits, to be used in the // computations below. Further the (hidden) msb with value 1 // in d must be involved as well. int expdelta = 0; long msb = 0x0000_0000_0000_0000L; - if (exp < -14) { - expdelta = -14 - exp; // FIXME? - exp = -15; - msb = 0x0010_0000_0000_0000L; // should be 0x0020_... ? + if (exp < MIN_EXPONENT) { + expdelta = MIN_EXPONENT - exp; + exp = MIN_EXPONENT - 1; + msb = 0x0010_0000_0000_0000L; } long f_signif_bits = doppel & 0x000f_ffff_ffff_ffffL | msb; + int PRECISION_DIFF = Double.PRECISION - PRECISION; // 42 // Significand bits as if using rounding to zero (truncation). - short signif_bits = (short)(f_signif_bits >> (42 + expdelta)); + short signif_bits = (short)(f_signif_bits >> (PRECISION_DIFF + expdelta)); // For round to nearest even, determining whether or not to // round up (in magnitude) is a function of the least @@ -399,9 +409,9 @@ public final class Float16 // 1 1 1 // See "Computer Arithmetic Algorithms," Koren, Table 4.9 - long lsb = f_signif_bits & (1L << 42 + expdelta); - long round = f_signif_bits & (1L << 41 + expdelta); - long sticky = f_signif_bits & ((1L << 41 + expdelta) - 1); + long lsb = f_signif_bits & (1L << (PRECISION_DIFF + expdelta)); + long round = f_signif_bits & (1L << (PRECISION_DIFF - 1) + expdelta); + long sticky = f_signif_bits & ((1L << (PRECISION_DIFF - 1) + expdelta) - 1); if (round != 0 && ((lsb | sticky) != 0 )) { signif_bits++; @@ -412,7 +422,9 @@ public final class Float16 // to implement a carry out from rounding the significand. assert (0xf800 & signif_bits) == 0x0; - return new Float16((short)(sign_bit | ( ((exp + 15) << 10) + signif_bits ) )); + // Exponent bias adjust in the representation is equal to MAX_EXPONENT. + return new Float16((short)(sign_bit | + ( ((exp + MAX_EXPONENT) << (PRECISION - 1)) + signif_bits ) )); } /** From 9d71af108ea2cc3682607527246d60a19fd820ba Mon Sep 17 00:00:00 2001 From: Hamlin Li Date: Mon, 29 Sep 2025 16:04:54 +0000 Subject: [PATCH 280/556] 8367253: RISC-V: refactor dependent cpu extensions Reviewed-by: fyang, luhenry --- src/hotspot/cpu/riscv/vm_version_riscv.cpp | 30 ----- src/hotspot/cpu/riscv/vm_version_riscv.hpp | 146 +++++++++++++-------- 2 files changed, 91 insertions(+), 85 deletions(-) diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.cpp b/src/hotspot/cpu/riscv/vm_version_riscv.cpp index 87366116b3b..ad8d452f50d 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.cpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.cpp @@ -234,36 +234,6 @@ void VM_Version::common_initialize() { warning("CRC32C intrinsics are not available on this CPU."); FLAG_SET_DEFAULT(UseCRC32CIntrinsics, false); } - - // UseZvbb (depends on RVV). - if (UseZvbb && !UseRVV) { - warning("Cannot enable UseZvbb on cpu without RVV support."); - FLAG_SET_DEFAULT(UseZvbb, false); - } - - // UseZvbc (depends on RVV). - if (UseZvbc && !UseRVV) { - warning("Cannot enable UseZvbc on cpu without RVV support."); - FLAG_SET_DEFAULT(UseZvbc, false); - } - - // UseZvkn (depends on RVV). - if (UseZvkn && !UseRVV) { - warning("Cannot enable UseZvkn on cpu without RVV support."); - FLAG_SET_DEFAULT(UseZvkn, false); - } - - // UseZvfh (depends on RVV) - if (UseZvfh) { - if (!UseRVV) { - warning("Cannot enable UseZvfh on cpu without RVV support."); - FLAG_SET_DEFAULT(UseZvfh, false); - } - if (!UseZfh) { - warning("Cannot enable UseZvfh on cpu without Zfh support."); - FLAG_SET_DEFAULT(UseZvfh, false); - } - } } #ifdef COMPILER2 diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.hpp b/src/hotspot/cpu/riscv/vm_version_riscv.hpp index aec975d27be..782f264965d 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.hpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.hpp @@ -70,6 +70,35 @@ class VM_Version : public Abstract_VM_Version { int64_t value() { return _value; } virtual bool enabled() = 0; virtual void update_flag() = 0; + + protected: + bool deps_all_enabled(RVFeatureValue* dep0, ...) { + assert(dep0 != nullptr, "must not"); + + va_list va; + va_start(va, dep0); + RVFeatureValue* next = dep0; + bool enabled = true; + while (next != nullptr && enabled) { + enabled = next->enabled(); + next = va_arg(va, RVFeatureValue*); + } + va_end(va); + return enabled; + } + + void deps_string(stringStream& ss, RVFeatureValue* dep0, ...) { + assert(dep0 != nullptr, "must not"); + ss.print("%s (%s)", dep0->pretty(), dep0->enabled() ? "enabled" : "disabled"); + + va_list va; + va_start(va, dep0); + RVFeatureValue* next = nullptr; + while ((next = va_arg(va, RVFeatureValue*)) != nullptr) { + ss.print(", %s (%s)", next->pretty(), next->enabled() ? "enabled" : "disabled"); + } + va_end(va); + } }; #define UPDATE_DEFAULT(flag) \ @@ -85,27 +114,34 @@ class VM_Version : public Abstract_VM_Version { } \ } \ - #define UPDATE_DEFAULT_DEP(flag, dep) \ - void update_flag() { \ - assert(enabled(), "Must be."); \ - /* dep must be declared before */ \ - assert((uintptr_t)(this) > \ - (uintptr_t)(&dep), "Invalid"); \ - if (FLAG_IS_DEFAULT(flag)) { \ - if (dep.enabled()) { \ - FLAG_SET_DEFAULT(flag, true); \ - } else { \ - FLAG_SET_DEFAULT(flag, false); \ - /* Sync CPU features with flags */ \ - disable_feature(); \ - } \ - } else { \ - /* Sync CPU features with flags */ \ - if (!flag) { \ - disable_feature(); \ - } \ - } \ - } \ + #define UPDATE_DEFAULT_DEP(flag, dep0, ...) \ + void update_flag() { \ + assert(enabled(), "Must be."); \ + if (FLAG_IS_DEFAULT(flag)) { \ + if (this->deps_all_enabled(dep0, ##__VA_ARGS__)) { \ + FLAG_SET_DEFAULT(flag, true); \ + } else { \ + FLAG_SET_DEFAULT(flag, false); \ + stringStream ss; \ + deps_string(ss, dep0, ##__VA_ARGS__); \ + warning("Cannot enable " #flag ", it's missing dependent extension(s) %s", ss.as_string(true)); \ + /* Sync CPU features with flags */ \ + disable_feature(); \ + } \ + } else { \ + /* Sync CPU features with flags */ \ + if (!flag) { \ + disable_feature(); \ + } else if (!deps_all_enabled(dep0, ##__VA_ARGS__)) { \ + FLAG_SET_DEFAULT(flag, false); \ + stringStream ss; \ + deps_string(ss, dep0, ##__VA_ARGS__); \ + warning("Cannot enable " #flag ", it's missing dependent extension(s) %s", ss.as_string(true)); \ + /* Sync CPU features with flags */ \ + disable_feature(); \ + } \ + } \ + } \ #define NO_UPDATE_DEFAULT \ void update_flag() {} \ @@ -202,40 +238,40 @@ class VM_Version : public Abstract_VM_Version { // // Fields description in `decl`: // declaration name, extension name, bit value from linux, feature string?, mapped flag) - #define RV_EXT_FEATURE_FLAGS(decl) \ - decl(ext_I , i , ('I' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_M , m , ('M' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_A , a , ('A' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_F , f , ('F' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_D , d , ('D' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_C , c , ('C' - 'A'), true , UPDATE_DEFAULT(UseRVC)) \ - decl(ext_Q , q , ('Q' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_H , h , ('H' - 'A'), true , NO_UPDATE_DEFAULT) \ - decl(ext_V , v , ('V' - 'A'), true , UPDATE_DEFAULT(UseRVV)) \ - decl(ext_Zicbom , Zicbom , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbom)) \ - decl(ext_Zicboz , Zicboz , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicboz)) \ - decl(ext_Zicbop , Zicbop , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbop)) \ - decl(ext_Zba , Zba , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZba)) \ - decl(ext_Zbb , Zbb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbb)) \ - decl(ext_Zbc , Zbc , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zbs , Zbs , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbs)) \ - decl(ext_Zbkb , Zbkb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbkb)) \ - decl(ext_Zcb , Zcb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZcb)) \ - decl(ext_Zfa , Zfa , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfa)) \ - decl(ext_Zfh , Zfh , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfh)) \ - decl(ext_Zfhmin , Zfhmin , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfhmin)) \ - decl(ext_Zicsr , Zicsr , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zicntr , Zicntr , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zifencei , Zifencei , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ - decl(ext_Zic64b , Zic64b , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZic64b)) \ - decl(ext_Ztso , Ztso , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZtso)) \ - decl(ext_Zihintpause , Zihintpause , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZihintpause)) \ - decl(ext_Zacas , Zacas , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZacas)) \ - decl(ext_Zvbb , Zvbb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbb, ext_V)) \ - decl(ext_Zvbc , Zvbc , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbc, ext_V)) \ - decl(ext_Zvfh , Zvfh , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvfh, ext_V)) \ - decl(ext_Zvkn , Zvkn , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvkn, ext_V)) \ - decl(ext_Zicond , Zicond , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicond)) \ + #define RV_EXT_FEATURE_FLAGS(decl) \ + decl(ext_I , i , ('I' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_M , m , ('M' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_A , a , ('A' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_F , f , ('F' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_D , d , ('D' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_C , c , ('C' - 'A'), true , UPDATE_DEFAULT(UseRVC)) \ + decl(ext_Q , q , ('Q' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_H , h , ('H' - 'A'), true , NO_UPDATE_DEFAULT) \ + decl(ext_V , v , ('V' - 'A'), true , UPDATE_DEFAULT(UseRVV)) \ + decl(ext_Zicbom , Zicbom , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbom)) \ + decl(ext_Zicboz , Zicboz , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicboz)) \ + decl(ext_Zicbop , Zicbop , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicbop)) \ + decl(ext_Zba , Zba , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZba)) \ + decl(ext_Zbb , Zbb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbb)) \ + decl(ext_Zbc , Zbc , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zbs , Zbs , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbs)) \ + decl(ext_Zbkb , Zbkb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZbkb)) \ + decl(ext_Zcb , Zcb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZcb)) \ + decl(ext_Zfa , Zfa , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfa)) \ + decl(ext_Zfh , Zfh , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfh)) \ + decl(ext_Zfhmin , Zfhmin , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZfhmin)) \ + decl(ext_Zicsr , Zicsr , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zicntr , Zicntr , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zifencei , Zifencei , RV_NO_FLAG_BIT, true , NO_UPDATE_DEFAULT) \ + decl(ext_Zic64b , Zic64b , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZic64b)) \ + decl(ext_Ztso , Ztso , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZtso)) \ + decl(ext_Zihintpause , Zihintpause , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZihintpause)) \ + decl(ext_Zacas , Zacas , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZacas)) \ + decl(ext_Zvbb , Zvbb , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbb, &ext_V, nullptr)) \ + decl(ext_Zvbc , Zvbc , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvbc, &ext_V, nullptr)) \ + decl(ext_Zvfh , Zvfh , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvfh, &ext_V, &ext_Zfh, nullptr)) \ + decl(ext_Zvkn , Zvkn , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT_DEP(UseZvkn, &ext_V, nullptr)) \ + decl(ext_Zicond , Zicond , RV_NO_FLAG_BIT, true , UPDATE_DEFAULT(UseZicond)) \ #define DECLARE_RV_EXT_FEATURE(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ struct NAME##RVExtFeatureValue : public RVExtFeatureValue { \ From aabf699dd0f066efe6654db24b520068b256d855 Mon Sep 17 00:00:00 2001 From: Brian Burkhalter Date: Mon, 29 Sep 2025 17:43:35 +0000 Subject: [PATCH 281/556] 8355339: Test java/io/File/GetCanonicalPath.java failed: The specified network name is no longer available Reviewed-by: alanb --- .../windows/native/libjava/canonicalize_md.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/java.base/windows/native/libjava/canonicalize_md.c b/src/java.base/windows/native/libjava/canonicalize_md.c index ecfdf63d091..7e567c7fbb4 100644 --- a/src/java.base/windows/native/libjava/canonicalize_md.c +++ b/src/java.base/windows/native/libjava/canonicalize_md.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,7 +41,7 @@ /* We should also include jdk_util.h here, for the prototype of JDK_Canonicalize. This isn't possible though because canonicalize_md.c is as well used in different contexts within Oracle. - */ +*/ #include "io_util_md.h" /* Copy bytes to dst, not going past dend; return dst + number of bytes copied, @@ -139,7 +139,8 @@ lastErrorReportable() || (errval == ERROR_ACCESS_DENIED) || (errval == ERROR_NETWORK_UNREACHABLE) || (errval == ERROR_NETWORK_ACCESS_DENIED) - || (errval == ERROR_NO_MORE_FILES)) { + || (errval == ERROR_NO_MORE_FILES) + || (errval == ERROR_NETNAME_DELETED)) { return 0; } return 1; @@ -183,7 +184,7 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) /* Copy prefix, assuming path is absolute */ c = src[0]; if (((c <= L'z' && c >= L'a') || (c <= L'Z' && c >= L'A')) - && (src[1] == L':') && (src[2] == L'\\')) { + && (src[1] == L':') && (src[2] == L'\\')) { /* Drive specifier */ *src = towupper(*src); /* Canonicalize drive letter */ if (!(dst = wcp(dst, dend, L'\0', src, src + 2))) { @@ -244,9 +245,9 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) continue; } else { if (!lastErrorReportable()) { - if (!(dst = wcp(dst, dend, L'\0', src, src + wcslen(src)))){ - goto err; - } + if (!(dst = wcp(dst, dend, L'\0', src, src + wcslen(src)))){ + goto err; + } break; } else { goto err; @@ -255,7 +256,7 @@ wcanonicalize(WCHAR *orig_path, WCHAR *result, int size) } if (dst >= dend) { - errno = ENAMETOOLONG; + errno = ENAMETOOLONG; goto err; } *dst = L'\0'; @@ -366,7 +367,7 @@ JDK_Canonicalize(const char *orig, char *out, int len) { // Change return value to success. ret = 0; -finish: + finish: free(wresult); free(wpath); From 3d97e17a31c267161c2be87b551cdb118062ff57 Mon Sep 17 00:00:00 2001 From: Chris Plummer Date: Mon, 29 Sep 2025 17:46:17 +0000 Subject: [PATCH 282/556] 8367318: Test vmTestbase/nsk/jdi/MethodEntryRequest/addClassFilter_rt/filter_rt001/TestDescription.java timed out after passing Reviewed-by: amenkov, sspitsyn --- .../nsk/share/jdi/EventHandler.java | 24 +++++++++---------- .../nsk/share/jdi/TestDebuggerType1.java | 8 +++---- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/EventHandler.java b/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/EventHandler.java index 706b3550dcc..90a6edebd38 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/EventHandler.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/EventHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -345,12 +345,10 @@ public class EventHandler implements Runnable { public boolean eventReceived(Event event) { if (event instanceof VMDisconnectEvent) { display("receieved VMDisconnect"); - synchronized(EventHandler.this) { - vmDisconnected = true; - status = 0; // OK finish - EventHandler.this.notifyAll(); - removeListener(this); - } + vmDisconnected = true; + status = 0; // OK finish + EventHandler.this.notifyAll(); + removeListener(this); return true; } return false; @@ -431,6 +429,10 @@ public class EventHandler implements Runnable { } public boolean eventReceived(Event event) { + if (en.event != null) { + // If we already got the requested event, don't handle this one. + return false; + } EventSet set = en.set; en.set = null; // We'll reset it below if the event matches a request. for (int i = 0; i < requests.length; i++) { @@ -441,11 +443,9 @@ public class EventHandler implements Runnable { if (request.equals(event.request())) { display("waitForRequestedEventCommon: Received event(" + event + ") for request(" + request + ")"); - synchronized (EventHandler.this) { - en.event = event; - en.set = set; - EventHandler.this.notifyAll(); - } + en.event = event; + en.set = set; + EventHandler.this.notifyAll(); return true; // event was handled } } diff --git a/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/TestDebuggerType1.java b/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/TestDebuggerType1.java index aa40fd36fe5..78cfb9e8bba 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/TestDebuggerType1.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/share/jdi/TestDebuggerType1.java @@ -204,11 +204,9 @@ public abstract class TestDebuggerType1 { new EventHandler.EventListener() { public boolean eventReceived(Event event) { if (event instanceof BreakpointEvent && bpRequest.equals(event.request())) { - synchronized(eventHandler) { - display("Received communication breakpoint event."); - bpCount++; - eventHandler.notifyAll(); - } + display("Received communication breakpoint event."); + bpCount++; + eventHandler.notifyAll(); return true; } return false; From 6c8e384c63ac199a5f226b017ef5cd133130d1ac Mon Sep 17 00:00:00 2001 From: Chen Liang Date: Mon, 29 Sep 2025 18:22:24 +0000 Subject: [PATCH 283/556] 8356022: Migrate descriptor parsing from generics to BytecodeDescriptor Reviewed-by: rriggs --- .../share/classes/java/lang/Class.java | 35 ++--- .../sun/invoke/util/BytecodeDescriptor.java | 59 ++++++-- .../reflect/annotation/AnnotationParser.java | 27 +--- .../invoke/util/BytecodeDescriptorTest.java | 128 ++++++++++++++++++ 4 files changed, 193 insertions(+), 56 deletions(-) create mode 100644 test/jdk/sun/invoke/util/BytecodeDescriptorTest.java diff --git a/src/java.base/share/classes/java/lang/Class.java b/src/java.base/share/classes/java/lang/Class.java index 5341249e085..fd3fe3ac2c0 100644 --- a/src/java.base/share/classes/java/lang/Class.java +++ b/src/java.base/share/classes/java/lang/Class.java @@ -84,12 +84,11 @@ import jdk.internal.vm.annotation.AOTSafeClassInitializer; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; +import sun.invoke.util.BytecodeDescriptor; import sun.invoke.util.Wrapper; import sun.reflect.generics.factory.CoreReflectionFactory; import sun.reflect.generics.factory.GenericsFactory; import sun.reflect.generics.repository.ClassRepository; -import sun.reflect.generics.repository.MethodRepository; -import sun.reflect.generics.repository.ConstructorRepository; import sun.reflect.generics.scope.ClassScope; import sun.reflect.annotation.*; @@ -1447,17 +1446,10 @@ public final class Class implements java.io.Serializable, if (!enclosingInfo.isMethod()) return null; - MethodRepository typeInfo = MethodRepository.make(enclosingInfo.getDescriptor(), - getFactory()); - Class returnType = toClass(typeInfo.getReturnType()); - Type [] parameterTypes = typeInfo.getParameterTypes(); - Class[] parameterClasses = new Class[parameterTypes.length]; - - // Convert Types to Classes; returned types *should* - // be class objects since the methodDescriptor's used - // don't have generics information - for(int i = 0; i < parameterClasses.length; i++) - parameterClasses[i] = toClass(parameterTypes[i]); + // Descriptor already validated by VM + List> types = BytecodeDescriptor.parseMethod(enclosingInfo.getDescriptor(), getClassLoader()); + Class returnType = types.removeLast(); + Class[] parameterClasses = types.toArray(EMPTY_CLASS_ARRAY); final Class enclosingCandidate = enclosingInfo.getEnclosingClass(); Method[] candidates = enclosingCandidate.privateGetDeclaredMethods(false); @@ -1576,17 +1568,10 @@ public final class Class implements java.io.Serializable, if (!enclosingInfo.isConstructor()) return null; - ConstructorRepository typeInfo = ConstructorRepository.make(enclosingInfo.getDescriptor(), - getFactory()); - Type [] parameterTypes = typeInfo.getParameterTypes(); - Class[] parameterClasses = new Class[parameterTypes.length]; - - // Convert Types to Classes; returned types *should* - // be class objects since the methodDescriptor's used - // don't have generics information - for (int i = 0; i < parameterClasses.length; i++) - parameterClasses[i] = toClass(parameterTypes[i]); - + // Descriptor already validated by VM + List> types = BytecodeDescriptor.parseMethod(enclosingInfo.getDescriptor(), getClassLoader()); + types.removeLast(); + Class[] parameterClasses = types.toArray(EMPTY_CLASS_ARRAY); final Class enclosingCandidate = enclosingInfo.getEnclosingClass(); Constructor[] candidates = enclosingCandidate @@ -1892,7 +1877,7 @@ public final class Class implements java.io.Serializable, } currentClass = currentClass.getSuperclass(); } - return list.toArray(new Class[0]); + return list.toArray(EMPTY_CLASS_ARRAY); } diff --git a/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java b/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java index 76bbff2a610..bd5ea6d7635 100644 --- a/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java +++ b/src/java.base/share/classes/sun/invoke/util/BytecodeDescriptor.java @@ -37,12 +37,33 @@ public class BytecodeDescriptor { private BytecodeDescriptor() { } // cannot instantiate - /** - * @param loader the class loader in which to look up the types (null means - * bootstrap class loader) - */ - public static List> parseMethod(String bytecodeSignature, ClassLoader loader) { - return parseMethod(bytecodeSignature, 0, bytecodeSignature.length(), loader); + /// Parses and validates a field descriptor string in the {@code loader} context. + /// + /// @param descriptor a field descriptor string + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// @throws IllegalArgumentException if the descriptor is invalid + /// @throws TypeNotPresentException if the descriptor is valid, but + /// the class cannot be found by the loader + public static Class parseClass(String descriptor, ClassLoader loader) { + int[] i = {0}; + var ret = parseSig(descriptor, i, descriptor.length(), loader); + if (i[0] != descriptor.length() || ret == null) { + parseError("not a class descriptor", descriptor); + } + return ret; + } + + /// Parses and validates a method descriptor string in the {@code loader} context. + /// + /// @param descriptor a method descriptor string + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// @throws IllegalArgumentException if the descriptor is invalid + /// @throws TypeNotPresentException if a reference type cannot be found by + /// the loader (before the descriptor is found invalid) + public static List> parseMethod(String descriptor, ClassLoader loader) { + return parseMethod(descriptor, 0, descriptor.length(), loader); } /** @@ -77,10 +98,19 @@ public class BytecodeDescriptor { throw new IllegalArgumentException("bad signature: "+str+": "+msg); } - /** - * @param loader the class loader in which to look up the types (null means - * bootstrap class loader) - */ + /// Parse a single type in a descriptor. Results can be: + /// + /// - A `Class` for successful parsing + /// - `null` for malformed descriptor format + /// - Throwing a [TypeNotPresentException] for valid class name, + /// but class cannot be found + /// + /// @param str contains the string to parse + /// @param i cursor for the next token in the string, modified in-place + /// @param end the limit for parsing + /// @param loader the class loader in which to look up the types (null means + /// bootstrap class loader) + /// private static Class parseSig(String str, int[] i, int end, ClassLoader loader) { if (i[0] == end) return null; char c = str.charAt(i[0]++); @@ -107,7 +137,14 @@ public class BytecodeDescriptor { } return t; } else { - return Wrapper.forBasicType(c).primitiveType(); + Wrapper w; + try { + w = Wrapper.forBasicType(c); + } catch (IllegalArgumentException ex) { + // Our reporting has better error message + return null; + } + return w.primitiveType(); } } diff --git a/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java b/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java index 82751e3fcd2..74ed1e01553 100644 --- a/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java +++ b/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,12 +34,7 @@ import java.util.function.Supplier; import jdk.internal.reflect.ConstantPool; -import sun.reflect.generics.parser.SignatureParser; -import sun.reflect.generics.tree.TypeSignature; -import sun.reflect.generics.factory.GenericsFactory; -import sun.reflect.generics.factory.CoreReflectionFactory; -import sun.reflect.generics.visitor.Reifier; -import sun.reflect.generics.scope.ClassScope; +import sun.invoke.util.BytecodeDescriptor; /** * Parser for Java programming language annotations. Translates @@ -429,19 +424,11 @@ public class AnnotationParser { } private static Class parseSig(String sig, Class container) { - if (sig.equals("V")) return void.class; - SignatureParser parser = SignatureParser.make(); - TypeSignature typeSig = parser.parseTypeSig(sig); - GenericsFactory factory = CoreReflectionFactory.make(container, ClassScope.make(container)); - Reifier reify = Reifier.make(factory); - typeSig.accept(reify); - Type result = reify.getResult(); - return toClass(result); - } - static Class toClass(Type o) { - if (o instanceof GenericArrayType gat) - return toClass(gat.getGenericComponentType()).arrayType(); - return (Class) o; + try { + return BytecodeDescriptor.parseClass(sig, container.getClassLoader()); + } catch (IllegalArgumentException ex) { + throw new GenericSignatureFormatError(ex.getMessage()); + } } /** diff --git a/test/jdk/sun/invoke/util/BytecodeDescriptorTest.java b/test/jdk/sun/invoke/util/BytecodeDescriptorTest.java new file mode 100644 index 00000000000..fa99c20e644 --- /dev/null +++ b/test/jdk/sun/invoke/util/BytecodeDescriptorTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8356022 + * @summary Tests for sun.invoke.util.BytecodeDescriptor + * @library /test/lib + * @modules java.base/sun.invoke.util + * @run junit BytecodeDescriptorTest + */ + +import java.lang.classfile.ClassFile; +import java.lang.constant.ClassDesc; +import java.util.List; +import java.util.Map; + +import jdk.test.lib.ByteCodeLoader; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import sun.invoke.util.BytecodeDescriptor; + +import static org.junit.jupiter.api.Assertions.*; + +class BytecodeDescriptorTest { + + private static final String FOO_NAME = "dummy.Foo"; + private static final String BAR_NAME = "dummy.Bar"; + private static final String FOO_DESC = "L" + FOO_NAME.replace('.', '/') + ";"; + private static final String BAR_DESC = "L" + BAR_NAME.replace('.', '/') + ";"; + private static final String DOES_NOT_EXIST_DESC = "Ldoes/not/Exist;"; + static Class foo1, foo2, bar1; + static ClassLoader cl1, cl2; + + @BeforeAll + static void setup() throws Throwable { + var fooBytes = ClassFile.of().build(ClassDesc.of(FOO_NAME), _ -> {}); + var barBytes = ClassFile.of().build(ClassDesc.of(BAR_NAME), _ -> {}); + cl1 = new ByteCodeLoader(Map.of(FOO_NAME, fooBytes, BAR_NAME, barBytes), ClassLoader.getSystemClassLoader()); + foo1 = cl1.loadClass(FOO_NAME); + bar1 = cl1.loadClass(BAR_NAME); + foo2 = ByteCodeLoader.load(FOO_NAME, fooBytes); + cl2 = foo2.getClassLoader(); + + // Sanity + assertNotSame(foo1, foo2); + assertNotSame(cl1, cl2); + assertSame(cl1, foo1.getClassLoader()); + assertSame(cl1, bar1.getClassLoader()); + assertNotSame(cl1, foo2.getClassLoader()); + assertEquals(FOO_DESC, foo1.descriptorString()); + assertEquals(FOO_DESC, foo2.descriptorString()); + assertEquals(BAR_DESC, bar1.descriptorString()); + } + + @Test + void testParseClass() throws ReflectiveOperationException { + assertSame(void.class, BytecodeDescriptor.parseClass("V", null), "void"); + assertSame(int.class, BytecodeDescriptor.parseClass("I", null), "primitive"); + assertSame(long[][].class, BytecodeDescriptor.parseClass("[[J", null), "array"); + assertSame(Object.class, BytecodeDescriptor.parseClass("Ljava/lang/Object;", null), "class or interface"); + assertThrows(IllegalArgumentException.class, () -> BytecodeDescriptor.parseClass("java/lang/Object", null), "internal name"); + assertThrows(IllegalArgumentException.class, () -> BytecodeDescriptor.parseClass("[V", null), "bad array"); + assertSame(Class.forName("[".repeat(255) + "I"), BytecodeDescriptor.parseClass("[".repeat(255) + "I", null), "good array"); + assertThrows(IllegalArgumentException.class, () -> BytecodeDescriptor.parseClass("[".repeat(256) + "I", null), "bad array"); + + assertSame(foo2, BytecodeDescriptor.parseClass(FOO_DESC, cl2), "class loader"); + assertThrows(TypeNotPresentException.class, () -> BytecodeDescriptor.parseClass(DOES_NOT_EXIST_DESC, null), "not existent"); + assertThrows(TypeNotPresentException.class, () -> BytecodeDescriptor.parseClass(BAR_DESC, cl2), "cross loader"); + } + + @Test + void testParseMethod() { + assertEquals(List.of(void.class), + BytecodeDescriptor.parseMethod("()V", null), + "no-arg"); + assertEquals(List.of(int.class, Object.class, long[].class, void.class), + BytecodeDescriptor.parseMethod("(ILjava/lang/Object;[J)V", null), + "sanity"); + assertThrows(IllegalArgumentException.class, + () -> BytecodeDescriptor.parseMethod("()", null), + "no return"); + assertThrows(IllegalArgumentException.class, + () -> BytecodeDescriptor.parseMethod("(V)V", null), + "bad arg"); + var voidInMsgIAE = assertThrows(IllegalArgumentException.class, + () -> BytecodeDescriptor.parseMethod("([V)I", null), + "bad arg"); + assertTrue(voidInMsgIAE.getMessage().contains("[V"), () -> "missing [V type in: '%s'".formatted(voidInMsgIAE.getMessage())); + assertThrows(IllegalArgumentException.class, + () -> BytecodeDescriptor.parseClass("([".repeat(256) + "I)J", null), + "bad arg"); + + assertEquals(List.of(foo1, bar1), + BytecodeDescriptor.parseMethod("(" + FOO_DESC + ")" + BAR_DESC, cl1), + "class loader"); + assertThrows(TypeNotPresentException.class, + () -> BytecodeDescriptor.parseMethod("(" + FOO_DESC + ")" + BAR_DESC, cl2), + "no bar"); + assertThrows(TypeNotPresentException.class, + () -> BytecodeDescriptor.parseMethod("(" + FOO_DESC + "V)V", null), + "first encounter TNPE"); + assertThrows(IllegalArgumentException.class, + () -> BytecodeDescriptor.parseMethod("(V" + FOO_DESC + ")V", null), + "first encounter IAE"); + } + +} From 59e76af47b23f582bbc21465a1871205d2499f28 Mon Sep 17 00:00:00 2001 From: Hannes Greule Date: Mon, 29 Sep 2025 18:40:43 +0000 Subject: [PATCH 284/556] 8367967: C2: "fatal error: Not monotonic" with Mod nodes Co-authored-by: Christian Hagedorn Reviewed-by: bmaillard, vlivanov, chagedorn, shade --- src/hotspot/share/opto/divnode.cpp | 10 +-- .../compiler/ccp/TestModValueMonotonic.java | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/ccp/TestModValueMonotonic.java diff --git a/src/hotspot/share/opto/divnode.cpp b/src/hotspot/share/opto/divnode.cpp index 213f2e1e9a8..823745ea8e7 100644 --- a/src/hotspot/share/opto/divnode.cpp +++ b/src/hotspot/share/opto/divnode.cpp @@ -1206,6 +1206,11 @@ static const Type* mod_value(const PhaseGVN* phase, const Node* in1, const Node* if (t1 == Type::TOP) { return Type::TOP; } if (t2 == Type::TOP) { return Type::TOP; } + // Mod by zero? Throw exception at runtime! + if (t2 == TypeInteger::zero(bt)) { + return Type::TOP; + } + // We always generate the dynamic check for 0. // 0 MOD X is 0 if (t1 == TypeInteger::zero(bt)) { return t1; } @@ -1215,11 +1220,6 @@ static const Type* mod_value(const PhaseGVN* phase, const Node* in1, const Node* return TypeInteger::zero(bt); } - // Mod by zero? Throw exception at runtime! - if (t2 == TypeInteger::zero(bt)) { - return Type::TOP; - } - const TypeInteger* i1 = t1->is_integer(bt); const TypeInteger* i2 = t2->is_integer(bt); if (i1->is_con() && i2->is_con()) { diff --git a/test/hotspot/jtreg/compiler/ccp/TestModValueMonotonic.java b/test/hotspot/jtreg/compiler/ccp/TestModValueMonotonic.java new file mode 100644 index 00000000000..346b07c547e --- /dev/null +++ b/test/hotspot/jtreg/compiler/ccp/TestModValueMonotonic.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8367967 + * @summary Ensure ModI/LNode::Value is monotonic with potential division by 0 + * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:CompileOnly=compiler.ccp.TestModValueMonotonic::test* + * -XX:+StressCCP -XX:RepeatCompilation=100 -Xcomp compiler.ccp.TestModValueMonotonic + * @run main compiler.ccp.TestModValueMonotonic + */ +package compiler.ccp; + +public class TestModValueMonotonic { + static int iFld; + static long lFld; + static int limit = 1000; + static boolean flag; + + public static void main(String[] args) { + testInt(); + testLong(); + } + + static void testInt() { + int zero = 0; + + // Make sure loop is not counted such that it is not removed. Created a more complex graph for CCP. + for (int i = 1; i < limit; i*=4) { + zero = 34; + } + int three = flag ? 0 : 3; + iFld = three % zero; // phi[0..3] % phi[0..34] + } + + static void testLong() { + long zero = 0; + + // Make sure loop is not counted such that it is not removed. Created a more complex graph for CCP. + for (int i = 1; i < limit; i*=4) { + zero = 34; + } + long three = flag ? 0 : 3; + lFld = three % zero; // phi[0..3] % phi[0..34] + } +} From 2f29b3f24a31bbe58d9c3433d46b69c16002694b Mon Sep 17 00:00:00 2001 From: Afshin Zafari Date: Mon, 29 Sep 2025 19:24:28 +0000 Subject: [PATCH 285/556] 8366884: NMT fails with MallocLimit: reached category "mtCompiler" limit Reviewed-by: phubner, jsjolen --- test/hotspot/jtreg/runtime/NMT/MallocLimitTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/runtime/NMT/MallocLimitTest.java b/test/hotspot/jtreg/runtime/NMT/MallocLimitTest.java index a58ff861a29..8b711304a99 100644 --- a/test/hotspot/jtreg/runtime/NMT/MallocLimitTest.java +++ b/test/hotspot/jtreg/runtime/NMT/MallocLimitTest.java @@ -1,6 +1,6 @@ /* * Copyright (c) 2022 SAP SE. All rights reserved. - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -47,7 +47,7 @@ * @requires vm.flagless * @modules java.base/jdk.internal.misc * @library /test/lib - * @run driver MallocLimitTest compiler-limit-fatal + * @run driver/timeout=480 MallocLimitTest compiler-limit-fatal */ /* From c57003c9b837adb8671a0db636d9c576bd6a89b0 Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Mon, 29 Sep 2025 21:39:42 +0000 Subject: [PATCH 286/556] 8368890: open/test/jdk/tools/jpackage/macosx/NameWithSpaceTest.java fails randomly Reviewed-by: almatvee --- .../helpers/jdk/jpackage/test/MacHelper.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index 88e2819ce15..37e395e1a3e 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -74,10 +75,23 @@ public final class MacHelper { Path mountPoint = null; try { - // The first "dict" item of "system-entities" array property contains "mount-point" string property. - var plist = readPList(attachExecutor.getOutput()).queryArrayValue("system-entities", false).findFirst().map(PListReader.class::cast).orElseThrow(); - mountPoint = Path.of(plist.queryValue("mount-point")); + // One of "dict" items of "system-entities" array property should contain "mount-point" string property. + mountPoint = readPList(attachExecutor.getOutput()).queryArrayValue("system-entities", false).map(PListReader.class::cast).map(dict -> { + try { + return dict.queryValue("mount-point"); + } catch (NoSuchElementException ex) { + return (String)null; + } + }).filter(Objects::nonNull).map(Path::of).findFirst().orElseThrow(); + } finally { + if (mountPoint == null) { + TKit.trace("Unexpected plist file missing `system-entities` array:"); + attachExecutor.getOutput().forEach(TKit::trace); + TKit.trace("Done"); + } + } + try { // code here used to copy just or .app // We now have option to include arbitrary content, so we copy // everything in the mounted image. From 538a722c2e9123cc575355879ff230444cf2dadc Mon Sep 17 00:00:00 2001 From: Fei Yang Date: Tue, 30 Sep 2025 01:40:35 +0000 Subject: [PATCH 287/556] 8368732: RISC-V: Detect support for misaligned vector access via hwprobe Reviewed-by: mli, dzhang --- src/hotspot/cpu/riscv/vm_version_riscv.cpp | 9 ++++- src/hotspot/cpu/riscv/vm_version_riscv.hpp | 25 ++++++++---- .../os_cpu/linux_riscv/riscv_hwprobe.cpp | 38 +++++++++++++++++-- .../linux_riscv/vm_version_linux_riscv.cpp | 2 +- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.cpp b/src/hotspot/cpu/riscv/vm_version_riscv.cpp index ad8d452f50d..9d6ac4963aa 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.cpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.cpp @@ -160,7 +160,7 @@ void VM_Version::common_initialize() { if (FLAG_IS_DEFAULT(AvoidUnalignedAccesses)) { FLAG_SET_DEFAULT(AvoidUnalignedAccesses, - unaligned_access.value() != MISALIGNED_FAST); + unaligned_scalar.value() != MISALIGNED_SCALAR_FAST); } if (!AvoidUnalignedAccesses) { @@ -175,7 +175,12 @@ void VM_Version::common_initialize() { // This machine has fast unaligned memory accesses if (FLAG_IS_DEFAULT(UseUnalignedAccesses)) { FLAG_SET_DEFAULT(UseUnalignedAccesses, - unaligned_access.value() == MISALIGNED_FAST); + (unaligned_scalar.value() == MISALIGNED_SCALAR_FAST)); + } + + if (FLAG_IS_DEFAULT(AlignVector)) { + FLAG_SET_DEFAULT(AlignVector, + unaligned_vector.value() != MISALIGNED_VECTOR_FAST); } #ifdef __riscv_ztso diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.hpp b/src/hotspot/cpu/riscv/vm_version_riscv.hpp index 782f264965d..b3d905dff9d 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.hpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.hpp @@ -227,7 +227,8 @@ class VM_Version : public Abstract_VM_Version { // mvendorid Manufactory JEDEC id encoded, ISA vol 2 3.1.2.. // marchid Id for microarch. Mvendorid plus marchid uniquely identify the microarch. // mimpid A unique encoding of the version of the processor implementation. - // unaligned_access Unaligned memory accesses (unknown, unspported, emulated, slow, firmware, fast) + // unaligned_scalar Performance of misaligned scalar accesses (unknown, emulated, slow, fast, unsupported) + // unaligned_vector Performance of misaligned vector accesses (unknown, unspported, slow, fast) // satp mode SATP bits (number of virtual addr bits) mbare, sv39, sv48, sv57, sv64 public: @@ -287,11 +288,12 @@ class VM_Version : public Abstract_VM_Version { // Non-extension features // #define RV_NON_EXT_FEATURE_FLAGS(decl) \ - decl(unaligned_access , Unaligned , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ decl(mvendorid , VendorId , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ decl(marchid , ArchId , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ decl(mimpid , ImpId , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ decl(satp_mode , SATP , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + decl(unaligned_scalar , UnalignedScalar , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + decl(unaligned_vector , UnalignedVector , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ decl(zicboz_block_size, ZicbozBlockSize , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ #define DECLARE_RV_NON_EXT_FEATURE(NAME, PRETTY, LINUX_BIT, FSTRING, FLAGF) \ @@ -432,12 +434,19 @@ private: static VM_MODE parse_satp_mode(const char* vm_mode); // Values from riscv_hwprobe() - enum UNALIGNED_ACCESS : int { - MISALIGNED_UNKNOWN = 0, - MISALIGNED_EMULATED = 1, - MISALIGNED_SLOW = 2, - MISALIGNED_FAST = 3, - MISALIGNED_UNSUPPORTED = 4 + enum UNALIGNED_SCALAR_ACCESS : int { + MISALIGNED_SCALAR_UNKNOWN = 0, + MISALIGNED_SCALAR_EMULATED = 1, + MISALIGNED_SCALAR_SLOW = 2, + MISALIGNED_SCALAR_FAST = 3, + MISALIGNED_SCALAR_UNSUPPORTED = 4 + }; + + enum UNALIGNED_VECTOR_ACCESS : int { + MISALIGNED_VECTOR_UNKNOWN = 0, + MISALIGNED_VECTOR_SLOW = 2, + MISALIGNED_VECTOR_FAST = 3, + MISALIGNED_VECTOR_UNSUPPORTED = 4 }; // Null terminated list diff --git a/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp b/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp index 3e5fb4610de..a95bfb4ff96 100644 --- a/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp +++ b/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp @@ -89,7 +89,24 @@ #define RISCV_HWPROBE_MISALIGNED_UNSUPPORTED (4 << 0) #define RISCV_HWPROBE_MISALIGNED_MASK (7 << 0) -#define RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE 6 +#define RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE 6 + +#define RISCV_HWPROBE_KEY_HIGHEST_VIRT_ADDRESS 7 + +#define RISCV_HWPROBE_KEY_TIME_CSR_FREQ 8 + +#define RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF 9 +#define RISCV_HWPROBE_MISALIGNED_SCALAR_UNKNOWN 0 +#define RISCV_HWPROBE_MISALIGNED_SCALAR_EMULATED 1 +#define RISCV_HWPROBE_MISALIGNED_SCALAR_SLOW 2 +#define RISCV_HWPROBE_MISALIGNED_SCALAR_FAST 3 +#define RISCV_HWPROBE_MISALIGNED_SCALAR_UNSUPPORTED 4 + +#define RISCV_HWPROBE_KEY_MISALIGNED_VECTOR_PERF 10 +#define RISCV_HWPROBE_MISALIGNED_VECTOR_UNKNOWN 0 +#define RISCV_HWPROBE_MISALIGNED_VECTOR_SLOW 2 +#define RISCV_HWPROBE_MISALIGNED_VECTOR_FAST 3 +#define RISCV_HWPROBE_MISALIGNED_VECTOR_UNSUPPORTED 4 #ifndef NR_riscv_hwprobe #ifndef NR_arch_specific_syscall @@ -117,7 +134,11 @@ static struct riscv_hwprobe query[] = {{RISCV_HWPROBE_KEY_MVENDORID, 0}, {RISCV_HWPROBE_KEY_BASE_BEHAVIOR, 0}, {RISCV_HWPROBE_KEY_IMA_EXT_0, 0}, {RISCV_HWPROBE_KEY_CPUPERF_0, 0}, - {RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE, 0}}; + {RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE, 0}, + {RISCV_HWPROBE_KEY_HIGHEST_VIRT_ADDRESS, 0}, + {RISCV_HWPROBE_KEY_TIME_CSR_FREQ, 0}, + {RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF, 0}, + {RISCV_HWPROBE_KEY_MISALIGNED_VECTOR_PERF, 0}}; bool RiscvHwprobe::probe_features() { assert(!rw_hwprobe_completed, "Called twice."); @@ -246,9 +267,20 @@ void RiscvHwprobe::add_features_from_query_result() { VM_Version::ext_Zicond.enable_feature(); } #endif + // RISCV_HWPROBE_KEY_CPUPERF_0 is deprecated and returns similar values + // to RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF. Keep it there for backward + // compatibility with old kernels. if (is_valid(RISCV_HWPROBE_KEY_CPUPERF_0)) { - VM_Version::unaligned_access.enable_feature( + VM_Version::unaligned_scalar.enable_feature( query[RISCV_HWPROBE_KEY_CPUPERF_0].value & RISCV_HWPROBE_MISALIGNED_MASK); + } else if (is_valid(RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF)) { + VM_Version::unaligned_scalar.enable_feature( + query[RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF].value); + } + + if (is_valid(RISCV_HWPROBE_KEY_MISALIGNED_VECTOR_PERF)) { + VM_Version::unaligned_vector.enable_feature( + query[RISCV_HWPROBE_KEY_MISALIGNED_VECTOR_PERF].value); } if (is_valid(RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE)) { VM_Version::zicboz_block_size.enable_feature(query[RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE].value); diff --git a/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp b/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp index cf9429b6bea..89501aac979 100644 --- a/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp +++ b/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp @@ -303,7 +303,7 @@ void VM_Version::rivos_features() { ext_Zvfh.enable_feature(); - unaligned_access.enable_feature(MISALIGNED_FAST); + unaligned_scalar.enable_feature(MISALIGNED_SCALAR_FAST); satp_mode.enable_feature(VM_SV48); // Features dependent on march/mimpid. From 89af6e13f2354d6e32872791d157144cd478a88f Mon Sep 17 00:00:00 2001 From: Sergey Bylokhov Date: Tue, 30 Sep 2025 03:10:41 +0000 Subject: [PATCH 288/556] 8362204: test/jdk/sun/awt/font/TestDevTransform.java fails on Ubuntu 24.04 Reviewed-by: avu, prr --- test/jdk/sun/awt/font/TestDevTransform.java | 48 +++++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/test/jdk/sun/awt/font/TestDevTransform.java b/test/jdk/sun/awt/font/TestDevTransform.java index 2783401c0f2..39d4255f154 100644 --- a/test/jdk/sun/awt/font/TestDevTransform.java +++ b/test/jdk/sun/awt/font/TestDevTransform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,9 +22,31 @@ */ /* - * @test + * @test id=dialog_double * @bug 4269775 8341535 * @summary Check that different text rendering APIs agree + * @run main/othervm TestDevTransform DIALOG DOUBLE + */ + +/* + * @test id=dialog_float + * @bug 4269775 8341535 + * @summary Check that different text rendering APIs agree + * @run main/othervm TestDevTransform DIALOG FLOAT + */ + +/* + * @test id=monospaced_double + * @bug 4269775 8341535 + * @summary Check that different text rendering APIs agree + * @run main/othervm TestDevTransform MONOSPACED DOUBLE + */ + +/* + * @test id=monospaced_float + * @bug 4269775 8341535 + * @summary Check that different text rendering APIs agree + * @run main/othervm TestDevTransform MONOSPACED FLOAT */ /** @@ -66,6 +88,8 @@ public class TestDevTransform { static String test = "This is only a test"; static double angle = Math.PI / 6.0; // Rotate 30 degrees static final int W = 400, H = 400; + static boolean useDialog; + static boolean useDouble; static void draw(Graphics2D g2d, TextLayout layout, float x, float y, float scalex) { @@ -101,9 +125,19 @@ public class TestDevTransform { g2d.setColor(Color.white); g2d.fillRect(0, 0, W, H); g2d.setColor(Color.black); - g2d.scale(1.481f, 1.481); // Convert to 108 dpi + if (useDouble) { + g2d.scale(1.481, 1.481); // Convert to 108 dpi + } else { + g2d.scale(1.481f, 1.481f); // Convert to 108 dpi + } g2d.addRenderingHints(hints); - Font font = new Font(Font.DIALOG, Font.PLAIN, 12); + String name; + if (useDialog) { + name = Font.DIALOG; + } else { + name = Font.MONOSPACED; + } + Font font = new Font(name, Font.PLAIN, 12); g2d.setFont(font); } @@ -135,6 +169,12 @@ public class TestDevTransform { } public static void main(String args[]) throws Exception { + if (args[0].equals("DIALOG")) { + useDialog = true; + } + if (args[1].equals("DOUBLE")) { + useDouble = true; + } BufferedImage tl_Image = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); { From 2746c1a555891564963299182b3b0293eaefc901 Mon Sep 17 00:00:00 2001 From: Anton Artemov Date: Tue, 30 Sep 2025 05:09:33 +0000 Subject: [PATCH 289/556] 8367485: os::physical_memory is broken in 32-bit JVMs when running on 64-bit OSes Reviewed-by: jsikstro, sgehwolf, stefank, stuefe, aph --- src/hotspot/os/aix/os_aix.cpp | 24 ++++---- src/hotspot/os/aix/os_aix.hpp | 8 +-- src/hotspot/os/bsd/os_bsd.cpp | 34 +++++------ src/hotspot/os/bsd/os_bsd.hpp | 8 +-- src/hotspot/os/linux/os_linux.cpp | 60 +++++++++---------- src/hotspot/os/linux/os_linux.hpp | 8 +-- src/hotspot/os/windows/os_windows.cpp | 42 ++++++------- src/hotspot/os/windows/os_windows.hpp | 28 ++++----- src/hotspot/share/compiler/compileBroker.cpp | 2 +- src/hotspot/share/gc/shared/gcInitLogger.cpp | 5 +- src/hotspot/share/gc/z/zLargePages.cpp | 2 +- src/hotspot/share/jfr/jni/jfrJniMethod.cpp | 2 +- .../share/jfr/periodic/jfrPeriodic.cpp | 10 ++-- .../share/prims/jvmtiRedefineClasses.cpp | 10 ++-- src/hotspot/share/prims/whitebox.cpp | 2 +- src/hotspot/share/runtime/arguments.cpp | 2 +- src/hotspot/share/runtime/os.cpp | 28 ++++----- src/hotspot/share/runtime/os.hpp | 12 ++-- src/hotspot/share/services/heapDumper.cpp | 2 +- .../share/utilities/globalDefinitions.hpp | 6 ++ 20 files changed, 151 insertions(+), 144 deletions(-) diff --git a/src/hotspot/os/aix/os_aix.cpp b/src/hotspot/os/aix/os_aix.cpp index dbdc66b0eb6..0c0c2808fa1 100644 --- a/src/hotspot/os/aix/os_aix.cpp +++ b/src/hotspot/os/aix/os_aix.cpp @@ -169,7 +169,7 @@ static void vmembk_print_on(outputStream* os); //////////////////////////////////////////////////////////////////////////////// // global variables (for a description see os_aix.hpp) -size_t os::Aix::_physical_memory = 0; +physical_memory_size_type os::Aix::_physical_memory = 0; pthread_t os::Aix::_main_thread = ((pthread_t)0); @@ -254,43 +254,43 @@ static bool is_close_to_brk(address a) { return false; } -bool os::free_memory(size_t& value) { +bool os::free_memory(physical_memory_size_type& value) { return Aix::available_memory(value); } -bool os::available_memory(size_t& value) { +bool os::available_memory(physical_memory_size_type& value) { return Aix::available_memory(value); } -bool os::Aix::available_memory(size_t& value) { +bool os::Aix::available_memory(physical_memory_size_type& value) { os::Aix::meminfo_t mi; if (os::Aix::get_meminfo(&mi)) { - value = static_cast(mi.real_free); + value = static_cast(mi.real_free); return true; } else { return false; } } -bool os::total_swap_space(size_t& value) { +bool os::total_swap_space(physical_memory_size_type& value) { perfstat_memory_total_t memory_info; if (libperfstat::perfstat_memory_total(nullptr, &memory_info, sizeof(perfstat_memory_total_t), 1) == -1) { return false; } - value = static_cast(memory_info.pgsp_total * 4 * K); + value = static_cast(memory_info.pgsp_total * 4 * K); return true; } -bool os::free_swap_space(size_t& value) { +bool os::free_swap_space(physical_memory_size_type& value) { perfstat_memory_total_t memory_info; if (libperfstat::perfstat_memory_total(nullptr, &memory_info, sizeof(perfstat_memory_total_t), 1) == -1) { return false; } - value = static_cast(memory_info.pgsp_free * 4 * K); + value = static_cast(memory_info.pgsp_free * 4 * K); return true; } -size_t os::physical_memory() { +physical_memory_size_type os::physical_memory() { return Aix::physical_memory(); } @@ -329,7 +329,7 @@ void os::Aix::initialize_system_info() { if (!os::Aix::get_meminfo(&mi)) { assert(false, "os::Aix::get_meminfo failed."); } - _physical_memory = static_cast(mi.real_total); + _physical_memory = static_cast(mi.real_total); } // Helper function for tracing page sizes. @@ -2192,7 +2192,7 @@ jint os::init_2(void) { os::Posix::init_2(); trcVerbose("processor count: %d", os::_processor_count); - trcVerbose("physical memory: %zu", Aix::_physical_memory); + trcVerbose("physical memory: " PHYS_MEM_TYPE_FORMAT, Aix::_physical_memory); // Initially build up the loaded dll map. LoadedLibraries::reload(); diff --git a/src/hotspot/os/aix/os_aix.hpp b/src/hotspot/os/aix/os_aix.hpp index 1530f2adb76..a7bac40e79b 100644 --- a/src/hotspot/os/aix/os_aix.hpp +++ b/src/hotspot/os/aix/os_aix.hpp @@ -35,7 +35,7 @@ class os::Aix { private: - static size_t _physical_memory; + static physical_memory_size_type _physical_memory; static pthread_t _main_thread; // 0 = uninitialized, otherwise 16 bit number: @@ -54,9 +54,9 @@ class os::Aix { // 1 - EXTSHM=ON static int _extshm; - static bool available_memory(size_t& value); - static bool free_memory(size_t& value); - static size_t physical_memory() { return _physical_memory; } + static bool available_memory(physical_memory_size_type& value); + static bool free_memory(physical_memory_size_type& value); + static physical_memory_size_type physical_memory() { return _physical_memory; } static void initialize_system_info(); // OS recognitions (AIX OS level) call this before calling Aix::os_version(). diff --git a/src/hotspot/os/bsd/os_bsd.cpp b/src/hotspot/os/bsd/os_bsd.cpp index b03dbd48cf8..8c5bbd58a84 100644 --- a/src/hotspot/os/bsd/os_bsd.cpp +++ b/src/hotspot/os/bsd/os_bsd.cpp @@ -114,7 +114,7 @@ //////////////////////////////////////////////////////////////////////////////// // global variables -size_t os::Bsd::_physical_memory = 0; +physical_memory_size_type os::Bsd::_physical_memory = 0; #ifdef __APPLE__ mach_timebase_info_data_t os::Bsd::_timebase_info = {0, 0}; @@ -133,19 +133,19 @@ static volatile int processor_id_next = 0; //////////////////////////////////////////////////////////////////////////////// // utility functions -bool os::available_memory(size_t& value) { +bool os::available_memory(physical_memory_size_type& value) { return Bsd::available_memory(value); } -bool os::free_memory(size_t& value) { +bool os::free_memory(physical_memory_size_type& value) { return Bsd::available_memory(value); } // Available here means free. Note that this number is of no much use. As an estimate // for future memory pressure it is far too conservative, since MacOS will use a lot // of unused memory for caches, and return it willingly in case of needs. -bool os::Bsd::available_memory(size_t& value) { - uint64_t available = static_cast(physical_memory() >> 2); +bool os::Bsd::available_memory(physical_memory_size_type& value) { + physical_memory_size_type available = physical_memory() >> 2; #ifdef __APPLE__ mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; vm_statistics64_data_t vmstat; @@ -160,7 +160,7 @@ bool os::Bsd::available_memory(size_t& value) { return false; } #endif - value = static_cast(available); + value = available; return true; } @@ -180,35 +180,35 @@ void os::Bsd::print_uptime_info(outputStream* st) { } } -bool os::total_swap_space(size_t& value) { +bool os::total_swap_space(physical_memory_size_type& value) { #if defined(__APPLE__) struct xsw_usage vmusage; size_t size = sizeof(vmusage); if (sysctlbyname("vm.swapusage", &vmusage, &size, nullptr, 0) != 0) { return false; } - value = static_cast(vmusage.xsu_total); + value = static_cast(vmusage.xsu_total); return true; #else return false; #endif } -bool os::free_swap_space(size_t& value) { +bool os::free_swap_space(physical_memory_size_type& value) { #if defined(__APPLE__) struct xsw_usage vmusage; size_t size = sizeof(vmusage); if (sysctlbyname("vm.swapusage", &vmusage, &size, nullptr, 0) != 0) { return false; } - value = static_cast(vmusage.xsu_avail); + value = static_cast(vmusage.xsu_avail); return true; #else return false; #endif } -size_t os::physical_memory() { +physical_memory_size_type os::physical_memory() { return Bsd::physical_memory(); } @@ -286,7 +286,7 @@ void os::Bsd::initialize_system_info() { len = sizeof(mem_val); if (sysctl(mib, 2, &mem_val, &len, nullptr, 0) != -1) { assert(len == sizeof(mem_val), "unexpected data size"); - _physical_memory = static_cast(mem_val); + _physical_memory = static_cast(mem_val); } else { _physical_memory = 256 * 1024 * 1024; // fallback (XXXBSD?) } @@ -297,7 +297,7 @@ void os::Bsd::initialize_system_info() { // datasize rlimit restricts us anyway. struct rlimit limits; getrlimit(RLIMIT_DATA, &limits); - _physical_memory = MIN2(_physical_memory, static_cast(limits.rlim_cur)); + _physical_memory = MIN2(_physical_memory, static_cast(limits.rlim_cur)); } #endif } @@ -1469,12 +1469,12 @@ void os::print_memory_info(outputStream* st) { st->print("Memory:"); st->print(" %zuk page", os::vm_page_size()>>10); - size_t phys_mem = os::physical_memory(); - st->print(", physical %zuk", + physical_memory_size_type phys_mem = os::physical_memory(); + st->print(", physical " PHYS_MEM_TYPE_FORMAT "k", phys_mem >> 10); - size_t avail_mem = 0; + physical_memory_size_type avail_mem = 0; (void)os::available_memory(avail_mem); - st->print("(%zuk free)", + st->print("(" PHYS_MEM_TYPE_FORMAT "k free)", avail_mem >> 10); if((sysctlbyname("vm.swapusage", &swap_usage, &size, nullptr, 0) == 0) || (errno == ENOMEM)) { diff --git a/src/hotspot/os/bsd/os_bsd.hpp b/src/hotspot/os/bsd/os_bsd.hpp index 173cc5a40ad..82002917f39 100644 --- a/src/hotspot/os/bsd/os_bsd.hpp +++ b/src/hotspot/os/bsd/os_bsd.hpp @@ -42,12 +42,12 @@ class os::Bsd { protected: - static size_t _physical_memory; + static physical_memory_size_type _physical_memory; static pthread_t _main_thread; - static bool available_memory(size_t& value); - static bool free_memory(size_t& value); - static size_t physical_memory() { return _physical_memory; } + static bool available_memory(physical_memory_size_type& value); + static bool free_memory(physical_memory_size_type& value); + static physical_memory_size_type physical_memory() { return _physical_memory; } static void initialize_system_info(); static void rebuild_cpu_to_node_map(); diff --git a/src/hotspot/os/linux/os_linux.cpp b/src/hotspot/os/linux/os_linux.cpp index 04f361fe53b..7159b2a78c4 100644 --- a/src/hotspot/os/linux/os_linux.cpp +++ b/src/hotspot/os/linux/os_linux.cpp @@ -154,7 +154,7 @@ enum CoredumpFilterBit { //////////////////////////////////////////////////////////////////////////////// // global variables -size_t os::Linux::_physical_memory = 0; +physical_memory_size_type os::Linux::_physical_memory = 0; address os::Linux::_initial_thread_stack_bottom = nullptr; uintptr_t os::Linux::_initial_thread_stack_size = 0; @@ -228,15 +228,15 @@ julong os::Linux::available_memory_in_container() { return avail_mem; } -bool os::available_memory(size_t& value) { +bool os::available_memory(physical_memory_size_type& value) { return Linux::available_memory(value); } -bool os::Linux::available_memory(size_t& value) { +bool os::Linux::available_memory(physical_memory_size_type& value) { julong avail_mem = available_memory_in_container(); if (avail_mem != static_cast(-1L)) { log_trace(os)("available container memory: " JULONG_FORMAT, avail_mem); - value = static_cast(avail_mem); + value = static_cast(avail_mem); return true; } @@ -252,28 +252,28 @@ bool os::Linux::available_memory(size_t& value) { fclose(fp); } if (avail_mem == static_cast(-1L)) { - size_t free_mem = 0; + physical_memory_size_type free_mem = 0; if (!free_memory(free_mem)) { return false; } avail_mem = static_cast(free_mem); } log_trace(os)("available memory: " JULONG_FORMAT, avail_mem); - value = static_cast(avail_mem); + value = static_cast(avail_mem); return true; } -bool os::free_memory(size_t& value) { +bool os::free_memory(physical_memory_size_type& value) { return Linux::free_memory(value); } -bool os::Linux::free_memory(size_t& value) { +bool os::Linux::free_memory(physical_memory_size_type& value) { // values in struct sysinfo are "unsigned long" struct sysinfo si; julong free_mem = available_memory_in_container(); if (free_mem != static_cast(-1L)) { log_trace(os)("free container memory: " JULONG_FORMAT, free_mem); - value = static_cast(free_mem); + value = static_cast(free_mem); return true; } @@ -283,16 +283,16 @@ bool os::Linux::free_memory(size_t& value) { } free_mem = (julong)si.freeram * si.mem_unit; log_trace(os)("free memory: " JULONG_FORMAT, free_mem); - value = static_cast(free_mem); + value = static_cast(free_mem); return true; } -bool os::total_swap_space(size_t& value) { +bool os::total_swap_space(physical_memory_size_type& value) { if (OSContainer::is_containerized()) { jlong memory_and_swap_limit_in_bytes = OSContainer::memory_and_swap_limit_in_bytes(); jlong memory_limit_in_bytes = OSContainer::memory_limit_in_bytes(); if (memory_limit_in_bytes > 0 && memory_and_swap_limit_in_bytes > 0) { - value = static_cast(memory_and_swap_limit_in_bytes - memory_limit_in_bytes); + value = static_cast(memory_and_swap_limit_in_bytes - memory_limit_in_bytes); return true; } } // fallback to the host swap space if the container did return the unbound value of -1 @@ -302,30 +302,30 @@ bool os::total_swap_space(size_t& value) { assert(false, "sysinfo failed in total_swap_space(): %s", os::strerror(errno)); return false; } - value = static_cast(si.totalswap * si.mem_unit); + value = static_cast(si.totalswap) * si.mem_unit; return true; } -static bool host_free_swap_f(size_t& value) { +static bool host_free_swap_f(physical_memory_size_type& value) { struct sysinfo si; int ret = sysinfo(&si); if (ret != 0) { assert(false, "sysinfo failed in host_free_swap_f(): %s", os::strerror(errno)); return false; } - value = static_cast(si.freeswap * si.mem_unit); + value = static_cast(si.freeswap) * si.mem_unit; return true; } -bool os::free_swap_space(size_t& value) { +bool os::free_swap_space(physical_memory_size_type& value) { // os::total_swap_space() might return the containerized limit which might be // less than host_free_swap(). The upper bound of free swap needs to be the lower of the two. - size_t total_swap_space = 0; - size_t host_free_swap = 0; + physical_memory_size_type total_swap_space = 0; + physical_memory_size_type host_free_swap = 0; if (!os::total_swap_space(total_swap_space) || !host_free_swap_f(host_free_swap)) { return false; } - size_t host_free_swap_val = MIN2(total_swap_space, host_free_swap); + physical_memory_size_type host_free_swap_val = MIN2(total_swap_space, host_free_swap); if (OSContainer::is_containerized()) { jlong mem_swap_limit = OSContainer::memory_and_swap_limit_in_bytes(); jlong mem_limit = OSContainer::memory_limit_in_bytes(); @@ -341,31 +341,31 @@ bool os::free_swap_space(size_t& value) { jlong delta_usage = mem_swap_usage - mem_usage; if (delta_usage >= 0) { jlong free_swap = delta_limit - delta_usage; - value = free_swap >= 0 ? static_cast(free_swap) : 0; + value = free_swap >= 0 ? static_cast(free_swap) : 0; return true; } } } // unlimited or not supported. Fall through to return host value log_trace(os,container)("os::free_swap_space: container_swap_limit=" JLONG_FORMAT - " container_mem_limit=" JLONG_FORMAT " returning host value: %zu", + " container_mem_limit=" JLONG_FORMAT " returning host value: " PHYS_MEM_TYPE_FORMAT, mem_swap_limit, mem_limit, host_free_swap_val); } value = host_free_swap_val; return true; } -size_t os::physical_memory() { +physical_memory_size_type os::physical_memory() { if (OSContainer::is_containerized()) { jlong mem_limit; if ((mem_limit = OSContainer::memory_limit_in_bytes()) > 0) { log_trace(os)("total container memory: " JLONG_FORMAT, mem_limit); - return static_cast(mem_limit); + return static_cast(mem_limit); } } - size_t phys_mem = Linux::physical_memory(); - log_trace(os)("total system memory: %zu", phys_mem); + physical_memory_size_type phys_mem = Linux::physical_memory(); + log_trace(os)("total system memory: " PHYS_MEM_TYPE_FORMAT, phys_mem); return phys_mem; } @@ -549,7 +549,7 @@ void os::Linux::initialize_system_info() { fclose(fp); } } - _physical_memory = static_cast(sysconf(_SC_PHYS_PAGES)) * static_cast(sysconf(_SC_PAGESIZE)); + _physical_memory = static_cast(sysconf(_SC_PHYS_PAGES)) * static_cast(sysconf(_SC_PAGESIZE)); assert(processor_count() > 0, "linux error"); } @@ -2603,12 +2603,12 @@ void os::print_memory_info(outputStream* st) { // values in struct sysinfo are "unsigned long" struct sysinfo si; sysinfo(&si); - size_t phys_mem = physical_memory(); - st->print(", physical %zuk", + physical_memory_size_type phys_mem = physical_memory(); + st->print(", physical " PHYS_MEM_TYPE_FORMAT "k", phys_mem >> 10); - size_t avail_mem = 0; + physical_memory_size_type avail_mem = 0; (void)os::available_memory(avail_mem); - st->print("(%zuk free)", + st->print("(" PHYS_MEM_TYPE_FORMAT "k free)", avail_mem >> 10); st->print(", swap " UINT64_FORMAT "k", ((jlong)si.totalswap * si.mem_unit) >> 10); diff --git a/src/hotspot/os/linux/os_linux.hpp b/src/hotspot/os/linux/os_linux.hpp index 8f40e51dfb0..b77cd9f3c81 100644 --- a/src/hotspot/os/linux/os_linux.hpp +++ b/src/hotspot/os/linux/os_linux.hpp @@ -49,11 +49,11 @@ class os::Linux { protected: - static size_t _physical_memory; + static physical_memory_size_type _physical_memory; static pthread_t _main_thread; - static bool available_memory(size_t& value); - static bool free_memory(size_t& value); + static bool available_memory(physical_memory_size_type& value); + static bool free_memory(physical_memory_size_type& value); static void initialize_system_info(); @@ -116,7 +116,7 @@ class os::Linux { static address initial_thread_stack_bottom(void) { return _initial_thread_stack_bottom; } static uintptr_t initial_thread_stack_size(void) { return _initial_thread_stack_size; } - static size_t physical_memory() { return _physical_memory; } + static physical_memory_size_type physical_memory() { return _physical_memory; } static julong host_swap(); static intptr_t* ucontext_get_sp(const ucontext_t* uc); diff --git a/src/hotspot/os/windows/os_windows.cpp b/src/hotspot/os/windows/os_windows.cpp index 34788a53710..875e97ce038 100644 --- a/src/hotspot/os/windows/os_windows.cpp +++ b/src/hotspot/os/windows/os_windows.cpp @@ -834,22 +834,22 @@ jlong os::elapsed_frequency() { } -bool os::available_memory(size_t& value) { +bool os::available_memory(physical_memory_size_type& value) { return win32::available_memory(value); } -bool os::free_memory(size_t& value) { +bool os::free_memory(physical_memory_size_type& value) { return win32::available_memory(value); } -bool os::win32::available_memory(size_t& value) { +bool os::win32::available_memory(physical_memory_size_type& value) { // Use GlobalMemoryStatusEx() because GlobalMemoryStatus() may return incorrect // value if total memory is larger than 4GB MEMORYSTATUSEX ms; ms.dwLength = sizeof(ms); BOOL res = GlobalMemoryStatusEx(&ms); if (res == TRUE) { - value = static_cast(ms.ullAvailPhys); + value = static_cast(ms.ullAvailPhys); return true; } else { assert(false, "GlobalMemoryStatusEx failed in os::win32::available_memory(): %lu", ::GetLastError()); @@ -857,12 +857,12 @@ bool os::win32::available_memory(size_t& value) { } } -bool os::total_swap_space(size_t& value) { +bool os::total_swap_space(physical_memory_size_type& value) { MEMORYSTATUSEX ms; ms.dwLength = sizeof(ms); BOOL res = GlobalMemoryStatusEx(&ms); if (res == TRUE) { - value = static_cast(ms.ullTotalPageFile); + value = static_cast(ms.ullTotalPageFile); return true; } else { assert(false, "GlobalMemoryStatusEx failed in os::total_swap_space(): %lu", ::GetLastError()); @@ -870,12 +870,12 @@ bool os::total_swap_space(size_t& value) { } } -bool os::free_swap_space(size_t& value) { +bool os::free_swap_space(physical_memory_size_type& value) { MEMORYSTATUSEX ms; ms.dwLength = sizeof(ms); BOOL res = GlobalMemoryStatusEx(&ms); if (res == TRUE) { - value = static_cast(ms.ullAvailPageFile); + value = static_cast(ms.ullAvailPageFile); return true; } else { assert(false, "GlobalMemoryStatusEx failed in os::free_swap_space(): %lu", ::GetLastError()); @@ -883,7 +883,7 @@ bool os::free_swap_space(size_t& value) { } } -size_t os::physical_memory() { +physical_memory_size_type os::physical_memory() { return win32::physical_memory(); } @@ -3947,25 +3947,25 @@ int os::current_process_id() { return (_initial_pid ? _initial_pid : _getpid()); } -int os::win32::_processor_type = 0; +int os::win32::_processor_type = 0; // Processor level is not available on non-NT systems, use vm_version instead -int os::win32::_processor_level = 0; -size_t os::win32::_physical_memory = 0; +int os::win32::_processor_level = 0; +physical_memory_size_type os::win32::_physical_memory = 0; -bool os::win32::_is_windows_server = false; +bool os::win32::_is_windows_server = false; // 6573254 // Currently, the bug is observed across all the supported Windows releases, // including the latest one (as of this writing - Windows Server 2012 R2) -bool os::win32::_has_exit_bug = true; +bool os::win32::_has_exit_bug = true; -int os::win32::_major_version = 0; -int os::win32::_minor_version = 0; -int os::win32::_build_number = 0; -int os::win32::_build_minor = 0; +int os::win32::_major_version = 0; +int os::win32::_minor_version = 0; +int os::win32::_build_number = 0; +int os::win32::_build_minor = 0; -bool os::win32::_processor_group_warning_displayed = false; -bool os::win32::_job_object_processor_group_warning_displayed = false; +bool os::win32::_processor_group_warning_displayed = false; +bool os::win32::_job_object_processor_group_warning_displayed = false; void getWindowsInstallationType(char* buffer, int bufferSize) { HKEY hKey; @@ -4184,7 +4184,7 @@ void os::win32::initialize_system_info() { if (res != TRUE) { assert(false, "GlobalMemoryStatusEx failed in os::win32::initialize_system_info(): %lu", ::GetLastError()); } - _physical_memory = static_cast(ms.ullTotalPhys); + _physical_memory = static_cast(ms.ullTotalPhys); if (FLAG_IS_DEFAULT(MaxRAM)) { // Adjust MaxRAM according to the maximum virtual address space available. diff --git a/src/hotspot/os/windows/os_windows.hpp b/src/hotspot/os/windows/os_windows.hpp index 1426dc8be93..efb7b414989 100644 --- a/src/hotspot/os/windows/os_windows.hpp +++ b/src/hotspot/os/windows/os_windows.hpp @@ -38,18 +38,18 @@ class os::win32 { friend class os; protected: - static int _processor_type; - static int _processor_level; - static size_t _physical_memory; - static bool _is_windows_server; - static bool _has_exit_bug; - static bool _processor_group_warning_displayed; - static bool _job_object_processor_group_warning_displayed; + static int _processor_type; + static int _processor_level; + static physical_memory_size_type _physical_memory; + static bool _is_windows_server; + static bool _has_exit_bug; + static bool _processor_group_warning_displayed; + static bool _job_object_processor_group_warning_displayed; - static int _major_version; - static int _minor_version; - static int _build_number; - static int _build_minor; + static int _major_version; + static int _minor_version; + static int _build_number; + static int _build_minor; static void print_windows_version(outputStream* st); static void print_uptime_info(outputStream* st); @@ -102,9 +102,9 @@ class os::win32 { static int processor_level() { return _processor_level; } - static bool available_memory(size_t& value); - static bool free_memory(size_t& value); - static size_t physical_memory() { return _physical_memory; } + static bool available_memory(physical_memory_size_type& value); + static bool free_memory(physical_memory_size_type& value); + static physical_memory_size_type physical_memory() { return _physical_memory; } // load dll from Windows system directory or Windows directory static HINSTANCE load_Windows_dll(const char* name, char *ebuf, int ebuflen); diff --git a/src/hotspot/share/compiler/compileBroker.cpp b/src/hotspot/share/compiler/compileBroker.cpp index 226a6d3ad5c..574f4d6543b 100644 --- a/src/hotspot/share/compiler/compileBroker.cpp +++ b/src/hotspot/share/compiler/compileBroker.cpp @@ -1064,7 +1064,7 @@ void CompileBroker::possibly_add_compiler_threads(JavaThread* THREAD) { if (new_c2_count <= old_c2_count && new_c1_count <= old_c1_count) return; // Now, we do the more expensive operations. - size_t free_memory = 0; + physical_memory_size_type free_memory = 0; // Return value ignored - defaulting to 0 on failure. (void)os::free_memory(free_memory); // If SegmentedCodeCache is off, both values refer to the single heap (with type CodeBlobType::All). diff --git a/src/hotspot/share/gc/shared/gcInitLogger.cpp b/src/hotspot/share/gc/shared/gcInitLogger.cpp index 763c265b65e..ba639945860 100644 --- a/src/hotspot/share/gc/shared/gcInitLogger.cpp +++ b/src/hotspot/share/gc/shared/gcInitLogger.cpp @@ -62,8 +62,9 @@ void GCInitLogger::print_cpu() { } void GCInitLogger::print_memory() { - size_t memory = os::physical_memory(); - log_info_p(gc, init)("Memory: " PROPERFMT, PROPERFMTARGS(memory)); + physical_memory_size_type memory = os::physical_memory(); + log_info_p(gc, init)("Memory: " PHYS_MEM_TYPE_FORMAT "%s", + byte_size_in_proper_unit(memory), proper_unit_for_byte_size(memory)); } void GCInitLogger::print_large_pages() { diff --git a/src/hotspot/share/gc/z/zLargePages.cpp b/src/hotspot/share/gc/z/zLargePages.cpp index 639c9b0a04f..c259448563b 100644 --- a/src/hotspot/share/gc/z/zLargePages.cpp +++ b/src/hotspot/share/gc/z/zLargePages.cpp @@ -31,7 +31,7 @@ bool ZLargePages::_os_enforced_transparent_mode; void ZLargePages::initialize() { pd_initialize(); - const size_t memory = os::physical_memory(); + const size_t memory = static_cast(os::physical_memory()); log_info_p(gc, init)("Memory: " PROPERFMT, PROPERFMTARGS(memory)); log_info_p(gc, init)("Large Page Support: %s", to_string()); } diff --git a/src/hotspot/share/jfr/jni/jfrJniMethod.cpp b/src/hotspot/share/jfr/jni/jfrJniMethod.cpp index 57bea5c268b..cc5bbe1fc60 100644 --- a/src/hotspot/share/jfr/jni/jfrJniMethod.cpp +++ b/src/hotspot/share/jfr/jni/jfrJniMethod.cpp @@ -423,7 +423,7 @@ JVM_ENTRY_NO_ENV(jlong, jfr_host_total_swap_memory(JNIEnv* env, jclass jvm)) // We want the host swap memory, not the container value. return os::Linux::host_swap(); #else - size_t total_swap_space = 0; + physical_memory_size_type total_swap_space = 0; // Return value ignored - defaulting to 0 on failure. (void)os::total_swap_space(total_swap_space); return static_cast(total_swap_space); diff --git a/src/hotspot/share/jfr/periodic/jfrPeriodic.cpp b/src/hotspot/share/jfr/periodic/jfrPeriodic.cpp index a8a9e191ed8..b30ebd8108c 100644 --- a/src/hotspot/share/jfr/periodic/jfrPeriodic.cpp +++ b/src/hotspot/share/jfr/periodic/jfrPeriodic.cpp @@ -528,23 +528,23 @@ TRACE_REQUEST_FUNC(ThreadAllocationStatistics) { * the total memory reported is the amount of memory configured for the guest OS by the hypervisor. */ TRACE_REQUEST_FUNC(PhysicalMemory) { - u8 totalPhysicalMemory = static_cast(os::physical_memory()); + physical_memory_size_type totalPhysicalMemory = os::physical_memory(); EventPhysicalMemory event; event.set_totalSize(totalPhysicalMemory); - size_t avail_mem = 0; + physical_memory_size_type avail_mem = 0; // Return value ignored - defaulting to 0 on failure. (void)os::available_memory(avail_mem); - event.set_usedSize(totalPhysicalMemory - static_cast(avail_mem)); + event.set_usedSize(totalPhysicalMemory - avail_mem); event.commit(); } TRACE_REQUEST_FUNC(SwapSpace) { EventSwapSpace event; - size_t total_swap_space = 0; + physical_memory_size_type total_swap_space = 0; // Return value ignored - defaulting to 0 on failure. (void)os::total_swap_space(total_swap_space); event.set_totalSize(static_cast(total_swap_space)); - size_t free_swap_space = 0; + physical_memory_size_type free_swap_space = 0; // Return value ignored - defaulting to 0 on failure. (void)os::free_swap_space(free_swap_space); event.set_freeSize(static_cast(free_swap_space)); diff --git a/src/hotspot/share/prims/jvmtiRedefineClasses.cpp b/src/hotspot/share/prims/jvmtiRedefineClasses.cpp index 74192d724f6..ef8875d582e 100644 --- a/src/hotspot/share/prims/jvmtiRedefineClasses.cpp +++ b/src/hotspot/share/prims/jvmtiRedefineClasses.cpp @@ -1358,11 +1358,11 @@ jvmtiError VM_RedefineClasses::load_new_class_versions() { // constant pools HandleMark hm(current); InstanceKlass* the_class = get_ik(_class_defs[i].klass); - size_t avail_mem = 0; + physical_memory_size_type avail_mem = 0; // Return value ignored - defaulting to 0 on failure. (void)os::available_memory(avail_mem); log_debug(redefine, class, load) - ("loading name=%s kind=%d (avail_mem=%zuK)", + ("loading name=%s kind=%d (avail_mem=" PHYS_MEM_TYPE_FORMAT "K)", the_class->external_name(), _class_load_kind, avail_mem >> 10); ClassFileStream st((u1*)_class_defs[i].class_bytes, @@ -1530,7 +1530,7 @@ jvmtiError VM_RedefineClasses::load_new_class_versions() { // Return value ignored - defaulting to 0 on failure. (void)os::available_memory(avail_mem); log_debug(redefine, class, load) - ("loaded name=%s (avail_mem=%zuK)", the_class->external_name(), avail_mem >> 10); + ("loaded name=%s (avail_mem=" PHYS_MEM_TYPE_FORMAT "K)", the_class->external_name(), avail_mem >> 10); } return JVMTI_ERROR_NONE; @@ -4438,11 +4438,11 @@ void VM_RedefineClasses::redefine_single_class(Thread* current, jclass the_jclas ResourceMark rm(current); // increment the classRedefinedCount field in the_class and in any // direct and indirect subclasses of the_class - size_t avail_mem = 0; + physical_memory_size_type avail_mem = 0; // Return value ignored - defaulting to 0 on failure. (void)os::available_memory(avail_mem); log_info(redefine, class, load) - ("redefined name=%s, count=%d (avail_mem=%zuK)", + ("redefined name=%s, count=%d (avail_mem=" PHYS_MEM_TYPE_FORMAT "K)", the_class->external_name(), java_lang_Class::classRedefinedCount(the_class->java_mirror()), avail_mem >> 10); Events::log_redefinition(current, "redefined class name=%s, count=%d", the_class->external_name(), diff --git a/src/hotspot/share/prims/whitebox.cpp b/src/hotspot/share/prims/whitebox.cpp index 6d9ab57bb9a..f77b648ba95 100644 --- a/src/hotspot/share/prims/whitebox.cpp +++ b/src/hotspot/share/prims/whitebox.cpp @@ -2510,7 +2510,7 @@ WB_END // Available memory of the host machine (container-aware) WB_ENTRY(jlong, WB_HostAvailableMemory(JNIEnv* env, jobject o)) - size_t avail_mem = 0; + physical_memory_size_type avail_mem = 0; // Return value ignored - defaulting to 0 on failure. (void)os::available_memory(avail_mem); return static_cast(avail_mem); diff --git a/src/hotspot/share/runtime/arguments.cpp b/src/hotspot/share/runtime/arguments.cpp index b7ab68e143c..e43b18209bf 100644 --- a/src/hotspot/share/runtime/arguments.cpp +++ b/src/hotspot/share/runtime/arguments.cpp @@ -1649,7 +1649,7 @@ jint Arguments::set_aggressive_heap_flags() { // Thus, we need to make sure we're using a julong for intermediate // calculations. julong initHeapSize; - size_t phys_mem = os::physical_memory(); + physical_memory_size_type phys_mem = os::physical_memory(); julong total_memory = static_cast(phys_mem); if (total_memory < (julong) 256 * M) { diff --git a/src/hotspot/share/runtime/os.cpp b/src/hotspot/share/runtime/os.cpp index 4f00ff17269..674b0a55841 100644 --- a/src/hotspot/share/runtime/os.cpp +++ b/src/hotspot/share/runtime/os.cpp @@ -1184,13 +1184,13 @@ void os::print_summary_info(outputStream* st, char* buf, size_t buflen) { #endif // PRODUCT get_summary_cpu_info(buf, buflen); st->print("%s, ", buf); - size_t phys_mem = physical_memory(); - size_t mem = phys_mem/G; + physical_memory_size_type phys_mem = physical_memory(); + physical_memory_size_type mem = phys_mem/G; if (mem == 0) { // for low memory systems mem = phys_mem/M; - st->print("%d cores, %zuM, ", processor_count(), mem); + st->print("%d cores, " PHYS_MEM_TYPE_FORMAT "M, ", processor_count(), mem); } else { - st->print("%d cores, %zuG, ", processor_count(), mem); + st->print("%d cores, " PHYS_MEM_TYPE_FORMAT "G, ", processor_count(), mem); } get_summary_os_info(buf, buflen); st->print_raw(buf); @@ -1935,17 +1935,17 @@ bool os::is_server_class_machine() { return true; } // Then actually look at the machine - bool result = false; - const unsigned int server_processors = 2; - const julong server_memory = 2UL * G; + bool result = false; + const unsigned int server_processors = 2; + const physical_memory_size_type server_memory = 2UL * G; // We seem not to get our full complement of memory. // We allow some part (1/8?) of the memory to be "missing", // based on the sizes of DIMMs, and maybe graphics cards. - const julong missing_memory = 256UL * M; - size_t phys_mem = os::physical_memory(); + const physical_memory_size_type missing_memory = 256UL * M; + physical_memory_size_type phys_mem = os::physical_memory(); /* Is this a server class machine? */ if ((os::active_processor_count() >= (int)server_processors) && - (phys_mem >= (server_memory - missing_memory))) { + (phys_mem >= server_memory - missing_memory)) { const unsigned int logical_processors = VM_Version::logical_processors_per_package(); if (logical_processors > 1) { @@ -2204,22 +2204,22 @@ static void assert_nonempty_range(const char* addr, size_t bytes) { p2i(addr), p2i(addr) + bytes); } -bool os::used_memory(size_t& value) { +bool os::used_memory(physical_memory_size_type& value) { #ifdef LINUX if (OSContainer::is_containerized()) { jlong mem_usage = OSContainer::memory_usage_in_bytes(); if (mem_usage > 0) { - value = static_cast(mem_usage); + value = static_cast(mem_usage); return true; } else { return false; } } #endif - size_t avail_mem = 0; + physical_memory_size_type avail_mem = 0; // Return value ignored - defaulting to 0 on failure. (void)os::available_memory(avail_mem); - size_t phys_mem = os::physical_memory(); + physical_memory_size_type phys_mem = os::physical_memory(); value = phys_mem - avail_mem; return true; } diff --git a/src/hotspot/share/runtime/os.hpp b/src/hotspot/share/runtime/os.hpp index ff215105e8e..76695de2c1b 100644 --- a/src/hotspot/share/runtime/os.hpp +++ b/src/hotspot/share/runtime/os.hpp @@ -332,14 +332,14 @@ class os: AllStatic { // For example, on Linux, "available" memory (`MemAvailable` in `/proc/meminfo`) is greater // than "free" memory (`MemFree` in `/proc/meminfo`) because Linux can free memory // aggressively (e.g. clear caches) so that it becomes available. - [[nodiscard]] static bool available_memory(size_t& value); - [[nodiscard]] static bool used_memory(size_t& value); - [[nodiscard]] static bool free_memory(size_t& value); + [[nodiscard]] static bool available_memory(physical_memory_size_type& value); + [[nodiscard]] static bool used_memory(physical_memory_size_type& value); + [[nodiscard]] static bool free_memory(physical_memory_size_type& value); - [[nodiscard]] static bool total_swap_space(size_t& value); - [[nodiscard]] static bool free_swap_space(size_t& value); + [[nodiscard]] static bool total_swap_space(physical_memory_size_type& value); + [[nodiscard]] static bool free_swap_space(physical_memory_size_type& value); - static size_t physical_memory(); + static physical_memory_size_type physical_memory(); static bool is_server_class_machine(); static size_t rss(); diff --git a/src/hotspot/share/services/heapDumper.cpp b/src/hotspot/share/services/heapDumper.cpp index 626434b08a7..bfb4546a8a1 100644 --- a/src/hotspot/share/services/heapDumper.cpp +++ b/src/hotspot/share/services/heapDumper.cpp @@ -2610,7 +2610,7 @@ int HeapDumper::dump(const char* path, outputStream* out, int compression, bool // (DumpWriter buffer, DumperClassCacheTable, GZipCompressor buffers). // For the OOM handling we may already be limited in memory. // Lets ensure we have at least 20MB per thread. - size_t free_memory = 0; + physical_memory_size_type free_memory = 0; // Return value ignored - defaulting to 0 on failure. (void)os::free_memory(free_memory); julong max_threads = free_memory / (20 * M); diff --git a/src/hotspot/share/utilities/globalDefinitions.hpp b/src/hotspot/share/utilities/globalDefinitions.hpp index ea684368888..0d6e3bb2d76 100644 --- a/src/hotspot/share/utilities/globalDefinitions.hpp +++ b/src/hotspot/share/utilities/globalDefinitions.hpp @@ -135,6 +135,7 @@ class oopDesc; #define UINT64_FORMAT_X_0 "0x%016" PRIx64 #define UINT64_FORMAT_W(width) "%" #width PRIu64 #define UINT64_FORMAT_0 "%016" PRIx64 +#define PHYS_MEM_TYPE_FORMAT "%" PRIu64 // Format jlong, if necessary #ifndef JLONG_FORMAT @@ -417,6 +418,11 @@ const uintx max_uintx = (uintx)-1; typedef unsigned int uint; NEEDS_CLEANUP +// This typedef is to address the issue of running a 32-bit VM. In this case the amount +// of physical memory may not fit in size_t, so we have to have a larger type. Once 32-bit +// is deprecated, one can use size_t. +typedef uint64_t physical_memory_size_type; + //---------------------------------------------------------------------------------------------------- // Java type definitions From c0a4c0ba97284d55cfdf857eb5d41fd6189e6c2d Mon Sep 17 00:00:00 2001 From: Hamlin Li Date: Tue, 30 Sep 2025 08:11:02 +0000 Subject: [PATCH 290/556] 8367981: Update CompactHashtable for readability Reviewed-by: iklam, matsaave --- .../share/classfile/compactHashtable.cpp | 27 ++--- .../share/classfile/compactHashtable.hpp | 101 ++++++++++-------- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/src/hotspot/share/classfile/compactHashtable.cpp b/src/hotspot/share/classfile/compactHashtable.cpp index 9f7ec6bd118..6808ae7bb8f 100644 --- a/src/hotspot/share/classfile/compactHashtable.cpp +++ b/src/hotspot/share/classfile/compactHashtable.cpp @@ -72,10 +72,10 @@ CompactHashtableWriter::~CompactHashtableWriter() { FREE_C_HEAP_ARRAY(GrowableArray*, _buckets); } -// Add a symbol entry to the temporary hash table -void CompactHashtableWriter::add(unsigned int hash, u4 value) { +// Add an entry to the temporary hash table +void CompactHashtableWriter::add(unsigned int hash, u4 encoded_value) { int index = hash % _num_buckets; - _buckets[index]->append_if_missing(Entry(hash, value)); + _buckets[index]->append_if_missing(Entry(hash, encoded_value)); _num_entries_written++; } @@ -107,27 +107,28 @@ void CompactHashtableWriter::allocate_table() { SharedSpaceObjectAlignment); } -// Write the compact table's buckets +// Write the compact table's buckets and entries void CompactHashtableWriter::dump_table(NumberSeq* summary) { u4 offset = 0; for (int index = 0; index < _num_buckets; index++) { GrowableArray* bucket = _buckets[index]; int bucket_size = bucket->length(); if (bucket_size == 1) { - // bucket with one entry is compacted and only has the symbol offset _compact_buckets->at_put(index, BUCKET_INFO(offset, VALUE_ONLY_BUCKET_TYPE)); Entry ent = bucket->at(0); - _compact_entries->at_put(offset++, ent.value()); + // bucket with one entry is value_only and only has the encoded_value + _compact_entries->at_put(offset++, ent.encoded_value()); _num_value_only_buckets++; } else { - // regular bucket, each entry is a symbol (hash, offset) pair + // regular bucket, it could contain zero or more than one entry, + // each entry is a pair _compact_buckets->at_put(index, BUCKET_INFO(offset, REGULAR_BUCKET_TYPE)); for (int i=0; iat(i); - _compact_entries->at_put(offset++, u4(ent.hash())); // write entry hash - _compact_entries->at_put(offset++, ent.value()); + _compact_entries->at_put(offset++, u4(ent.hash())); // write entry hash + _compact_entries->at_put(offset++, ent.encoded_value()); // write entry encoded_value } if (bucket_size == 0) { _num_empty_buckets++; @@ -189,15 +190,7 @@ void SimpleCompactHashtable::init(address base_address, u4 entry_count, u4 bucke _entries = entries; } -size_t SimpleCompactHashtable::calculate_header_size() { - // We have 5 fields. Each takes up sizeof(intptr_t). See WriteClosure::do_u4 - size_t bytes = sizeof(intptr_t) * 5; - return bytes; -} - void SimpleCompactHashtable::serialize_header(SerializeClosure* soc) { - // NOTE: if you change this function, you MUST change the number 5 in - // calculate_header_size() accordingly. soc->do_u4(&_entry_count); soc->do_u4(&_bucket_count); soc->do_ptr(&_buckets); diff --git a/src/hotspot/share/classfile/compactHashtable.hpp b/src/hotspot/share/classfile/compactHashtable.hpp index 83299eda8c7..cb241ef5e70 100644 --- a/src/hotspot/share/classfile/compactHashtable.hpp +++ b/src/hotspot/share/classfile/compactHashtable.hpp @@ -35,7 +35,7 @@ template < typename K, typename V, - V (*DECODE)(address base_address, u4 offset), + V (*DECODE)(address base_address, u4 encoded_value), bool (*EQUALS)(V value, K key, int len) > class CompactHashtable; @@ -62,8 +62,9 @@ public: // The compact hash table writer. Used at dump time for writing out // the compact table to the shared archive. // -// At dump time, the CompactHashtableWriter obtains all entries from the -// symbol/string table and adds them to a new temporary hash table. The hash +// At dump time, the CompactHashtableWriter obtains all entries from +// a table (the table could be in any form of a collection of pair) +// and adds them to a new temporary hash table (_buckets). The hash // table size (number of buckets) is calculated using // '(num_entries + bucket_size - 1) / bucket_size'. The default bucket // size is 4 and can be changed by -XX:SharedSymbolTableBucketSize option. @@ -76,10 +77,10 @@ public: // above the CompactHashtable class for the table layout detail. The bucket // offsets are written to the archive as part of the compact table. The // bucket offset is encoded in the low 30-bit (0-29) and the bucket type -// (regular or compact) are encoded in bit[31, 30]. For buckets with more -// than one entry, both hash and entry offset are written to the -// table. For buckets with only one entry, only the entry offset is written -// to the table and the buckets are tagged as compact in their type bits. +// (regular or value_only) are encoded in bit[31, 30]. For buckets with more +// than one entry, both hash and encoded_value are written to the +// table. For buckets with only one entry, only the encoded_value is written +// to the table and the buckets are tagged as value_only in their type bits. // Buckets without entry are skipped from the table. Their offsets are // still written out for faster lookup. // @@ -87,21 +88,21 @@ class CompactHashtableWriter: public StackObj { public: class Entry { unsigned int _hash; - u4 _value; + u4 _encoded_value; public: Entry() {} - Entry(unsigned int hash, u4 val) : _hash(hash), _value(val) {} + Entry(unsigned int hash, u4 encoded_value) : _hash(hash), _encoded_value(encoded_value) {} - u4 value() { - return _value; + u4 encoded_value() { + return _encoded_value; } unsigned int hash() { return _hash; } bool operator==(const CompactHashtableWriter::Entry& other) { - return (_value == other._value && _hash == other._hash); + return (_encoded_value == other._encoded_value && _hash == other._hash); } }; // class CompactHashtableWriter::Entry @@ -121,7 +122,8 @@ public: CompactHashtableWriter(int num_entries, CompactHashtableStats* stats); ~CompactHashtableWriter(); - void add(unsigned int hash, u4 value); + void add(unsigned int hash, u4 encoded_value); + void dump(SimpleCompactHashtable *cht, const char* table_name); private: void allocate_table(); @@ -131,9 +133,6 @@ private: // calculation of num_buckets can result in zero buckets, we need at least one return (num_buckets < 1) ? 1 : num_buckets; } - -public: - void dump(SimpleCompactHashtable *cht, const char* table_name); }; #endif // INCLUDE_CDS @@ -148,7 +147,8 @@ public: ///////////////////////////////////////////////////////////////////////////// // -// CompactHashtable is used to store the CDS archive's symbol/string tables. +// CompactHashtable is used to store the CDS archive's tables. +// A table could be in any form of a collection of pair. // // Because these tables are read-only (no entries can be added/deleted) at run-time // and tend to have large number of entries, we try to minimize the footprint @@ -162,32 +162,47 @@ public: // The size of buckets[] is 'num_buckets + 1'. Each entry of // buckets[] is a 32-bit encoding of the bucket type and bucket offset, // with the type in the left-most 2-bit and offset in the remaining 30-bit. -// The last entry is a special type. It contains the end of the last -// bucket. // -// There are two types of buckets, regular buckets and value_only buckets. The -// value_only buckets have '01' in their highest 2-bit, and regular buckets have -// '00' in their highest 2-bit. +// There are three types of buckets: regular, value_only, and table_end. +// . The regular buckets have '00' in their highest 2-bit. +// . The value_only buckets have '01' in their highest 2-bit. +// . There is only a single table_end bucket that marks the end of buckets[]. +// It has '11' in its highest 2-bit. // -// For normal buckets, each entry is 8 bytes in the entries[]: -// u4 hash; /* symbol/string hash */ -// union { -// u4 offset; /* Symbol* sym = (Symbol*)(base_address + offset) */ -// narrowOop str; /* String narrowOop encoding */ -// } +// For regular buckets, each entry is 8 bytes in the entries[]: +// u4 hash; // entry hash +// u4 encoded_value; // A 32-bit encoding of the template type V. The template parameter DECODE +// // converts this to type V. Many CompactHashtables encode a pointer as a 32-bit offset, where +// // V entry = (V)(base_address + offset) +// // see StringTable, SymbolTable and AdapterHandlerLibrary for examples // +// For value_only buckets, each entry has only the 4-byte 'encoded_value' in the entries[]. // -// For value_only buckets, each entry has only the 4-byte 'offset' in the entries[]. +// The single table_end bucket has no corresponding entry. // -// Example -- note that the second bucket is a VALUE_ONLY_BUCKET_TYPE so the hash code -// is skipped. -// buckets[0, 4, 5, ....] -// | | | -// | | +---+ -// | | | -// | +----+ | -// v v v -// entries[H,O,H,O,O,H,O,H,O.....] +// The number of entries in bucket can be calculated like this: +// my_offset = _buckets[i] & 0x3fffffff; // mask off top 2-bit +// next_offset = _buckets[i+1] & 0x3fffffff +// For REGULAR_BUCKET_TYPE +// num_entries = (next_offset - my_offset) / 8; +// For VALUE_ONLY_BUCKET_TYPE +// num_entries = (next_offset - my_offset) / 4; +// +// If bucket is empty, we have my_offset == next_offset. Empty buckets are +// always encoded as regular buckets. +// +// In the following example: +// - Bucket #0 is a REGULAR_BUCKET_TYPE with two entries +// - Bucket #1 is a VALUE_ONLY_BUCKET_TYPE with one entry. +// - Bucket #2 is a REGULAR_BUCKET_TYPE with zero entries. +// +// buckets[0, 4, 5(empty), 5, ...., N(table_end)] +// | | | | | +// | | +---+-----+ | +// | | | | +// | +----+ + | +// v v v v +// entries[H,O,H,O,O,H,O,H,O........] // // See CompactHashtable::lookup() for how the table is searched at runtime. // See CompactHashtableWriter::dump() for how the table is written at CDS @@ -230,21 +245,19 @@ public: inline size_t entry_count() const { return _entry_count; } - - static size_t calculate_header_size(); }; template < typename K, typename V, - V (*DECODE)(address base_address, u4 offset), + V (*DECODE)(address base_address, u4 encoded_value), bool (*EQUALS)(V value, K key, int len) > class CompactHashtable : public SimpleCompactHashtable { friend class VMStructs; - V decode(u4 offset) const { - return DECODE(_base_address, offset); + V decode(u4 encoded_value) const { + return DECODE(_base_address, encoded_value); } public: @@ -264,7 +277,7 @@ public: } } else { // This is a regular bucket, which has more than one - // entries. Each entry is a pair of entry (hash, offset). + // entries. Each entry is a (hash, value) pair. // Seek until the end of the bucket. u4* entry_max = _entries + BUCKET_OFFSET(_buckets[index + 1]); while (entry < entry_max) { From 586167cff5aaead0949c509f48bc5080834cc362 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Tue, 30 Sep 2025 08:49:08 +0000 Subject: [PATCH 291/556] 8363932: G1: Better distribute KlassCleaningTask Reviewed-by: ayang, coleenp --- .../share/classfile/classLoaderData.hpp | 2 +- .../share/classfile/classLoaderDataGraph.cpp | 63 ++++--------------- .../share/classfile/classLoaderDataGraph.hpp | 18 +++--- .../share/gc/shared/parallelCleaning.cpp | 52 ++++++--------- .../share/gc/shared/parallelCleaning.hpp | 8 +-- src/hotspot/share/oops/klass.cpp | 22 +++---- src/hotspot/share/oops/klass.hpp | 6 +- 7 files changed, 58 insertions(+), 113 deletions(-) diff --git a/src/hotspot/share/classfile/classLoaderData.hpp b/src/hotspot/share/classfile/classLoaderData.hpp index da49a9326e3..64fcfb7519f 100644 --- a/src/hotspot/share/classfile/classLoaderData.hpp +++ b/src/hotspot/share/classfile/classLoaderData.hpp @@ -97,7 +97,7 @@ class ClassLoaderData : public CHeapObj { }; friend class ClassLoaderDataGraph; - friend class ClassLoaderDataGraphKlassIteratorAtomic; + friend class ClassLoaderDataGraphIteratorAtomic; friend class Klass; friend class MetaDataFactory; friend class Method; diff --git a/src/hotspot/share/classfile/classLoaderDataGraph.cpp b/src/hotspot/share/classfile/classLoaderDataGraph.cpp index 4d3d6a951c5..61404fdf9db 100644 --- a/src/hotspot/share/classfile/classLoaderDataGraph.cpp +++ b/src/hotspot/share/classfile/classLoaderDataGraph.cpp @@ -489,62 +489,25 @@ void ClassLoaderDataGraph::purge(bool at_safepoint) { } } -ClassLoaderDataGraphKlassIteratorAtomic::ClassLoaderDataGraphKlassIteratorAtomic() - : _next_klass(nullptr) { +ClassLoaderDataGraphIteratorAtomic::ClassLoaderDataGraphIteratorAtomic() + : _cld(nullptr) { assert(SafepointSynchronize::is_at_safepoint(), "must be at safepoint!"); - ClassLoaderData* cld = ClassLoaderDataGraph::_head; - Klass* klass = nullptr; - - // Find the first klass in the CLDG. - while (cld != nullptr) { - assert_locked_or_safepoint(cld->metaspace_lock()); - klass = cld->_klasses; - if (klass != nullptr) { - _next_klass = klass; - return; - } - cld = cld->next(); - } + _cld = AtomicAccess::load_acquire(&ClassLoaderDataGraph::_head); } -Klass* ClassLoaderDataGraphKlassIteratorAtomic::next_klass_in_cldg(Klass* klass) { - Klass* next = klass->next_link(); - if (next != nullptr) { - return next; - } - - // No more klasses in the current CLD. Time to find a new CLD. - ClassLoaderData* cld = klass->class_loader_data(); - assert_locked_or_safepoint(cld->metaspace_lock()); - while (next == nullptr) { - cld = cld->next(); - if (cld == nullptr) { - break; +ClassLoaderData* ClassLoaderDataGraphIteratorAtomic::next() { + ClassLoaderData* cur = AtomicAccess::load(&_cld); + for (;;) { + if (cur == nullptr) { + return nullptr; } - next = cld->_klasses; - } - - return next; -} - -Klass* ClassLoaderDataGraphKlassIteratorAtomic::next_klass() { - Klass* head = _next_klass; - - while (head != nullptr) { - Klass* next = next_klass_in_cldg(head); - - Klass* old_head = AtomicAccess::cmpxchg(&_next_klass, head, next); - - if (old_head == head) { - return head; // Won the CAS. + ClassLoaderData* next = cur->next(); + ClassLoaderData* old; + if ((old = AtomicAccess::cmpxchg(&_cld, cur, next)) == cur) { + return cur; } - - head = old_head; + cur = old; } - - // Nothing more for the iterator to hand out. - assert(head == nullptr, "head is " PTR_FORMAT ", expected not null:", p2i(head)); - return nullptr; } void ClassLoaderDataGraph::verify() { diff --git a/src/hotspot/share/classfile/classLoaderDataGraph.hpp b/src/hotspot/share/classfile/classLoaderDataGraph.hpp index 1dcca4d1069..803f227dcf3 100644 --- a/src/hotspot/share/classfile/classLoaderDataGraph.hpp +++ b/src/hotspot/share/classfile/classLoaderDataGraph.hpp @@ -34,7 +34,7 @@ class ClassLoaderDataGraph : public AllStatic { friend class ClassLoaderData; - friend class ClassLoaderDataGraphKlassIteratorAtomic; + friend class ClassLoaderDataGraphIteratorAtomic; friend class VMStructs; private: class ClassLoaderDataGraphIterator; @@ -140,14 +140,14 @@ public: } }; -// An iterator that distributes Klasses to parallel worker threads. -class ClassLoaderDataGraphKlassIteratorAtomic : public StackObj { - Klass* volatile _next_klass; - public: - ClassLoaderDataGraphKlassIteratorAtomic(); - Klass* next_klass(); - private: - static Klass* next_klass_in_cldg(Klass* klass); +// An iterator that distributes Klasses to parallel worker threads based on CLDs. +class ClassLoaderDataGraphIteratorAtomic : public StackObj { + ClassLoaderData* volatile _cld; + +public: + ClassLoaderDataGraphIteratorAtomic(); + + ClassLoaderData* next(); }; #endif // SHARE_CLASSFILE_CLASSLOADERDATAGRAPH_HPP diff --git a/src/hotspot/share/gc/shared/parallelCleaning.cpp b/src/hotspot/share/gc/shared/parallelCleaning.cpp index 9d496783ca2..d2f69bfa679 100644 --- a/src/hotspot/share/gc/shared/parallelCleaning.cpp +++ b/src/hotspot/share/gc/shared/parallelCleaning.cpp @@ -27,7 +27,7 @@ #include "code/codeCache.hpp" #include "gc/shared/parallelCleaning.hpp" #include "logging/log.hpp" -#include "memory/resourceArea.hpp" +#include "oops/klass.inline.hpp" #include "runtime/atomicAccess.hpp" CodeCacheUnloadingTask::CodeCacheUnloadingTask(uint num_workers, bool unloading_occurred) : @@ -94,38 +94,26 @@ void CodeCacheUnloadingTask::work(uint worker_id) { } } -KlassCleaningTask::KlassCleaningTask() : - _clean_klass_tree_claimed(false), - _klass_iterator() { -} - -bool KlassCleaningTask::claim_clean_klass_tree_task() { - if (_clean_klass_tree_claimed) { - return false; - } - - return !AtomicAccess::cmpxchg(&_clean_klass_tree_claimed, false, true); -} - -InstanceKlass* KlassCleaningTask::claim_next_klass() { - Klass* klass; - do { - klass =_klass_iterator.next_klass(); - } while (klass != nullptr && !klass->is_instance_klass()); - - // this can be null so don't call InstanceKlass::cast - return static_cast(klass); -} - void KlassCleaningTask::work() { - // One worker will clean the subklass/sibling klass tree. - if (claim_clean_klass_tree_task()) { - Klass::clean_weak_klass_links(true /* class_unloading_occurred */, false /* clean_alive_klasses */); - } + for (ClassLoaderData* cur = _cld_iterator_atomic.next(); cur != nullptr; cur = _cld_iterator_atomic.next()) { + class CleanKlasses : public KlassClosure { + public: - // All workers will help cleaning the classes, - InstanceKlass* klass; - while ((klass = claim_next_klass()) != nullptr) { - Klass::clean_weak_instanceklass_links(klass); + void do_klass(Klass* klass) override { + klass->clean_subklass(true); + + Klass* sibling = klass->next_sibling(true); + klass->set_next_sibling(sibling); + + if (klass->is_instance_klass()) { + Klass::clean_weak_instanceklass_links(InstanceKlass::cast(klass)); + } + + assert(klass->subklass() == nullptr || klass->subklass()->is_loader_alive(), "must be"); + assert(klass->next_sibling(false) == nullptr || klass->next_sibling(false)->is_loader_alive(), "must be"); + } + } cl; + + cur->classes_do(&cl); } } diff --git a/src/hotspot/share/gc/shared/parallelCleaning.hpp b/src/hotspot/share/gc/shared/parallelCleaning.hpp index 4a7c724fef5..b47bf92e2ac 100644 --- a/src/hotspot/share/gc/shared/parallelCleaning.hpp +++ b/src/hotspot/share/gc/shared/parallelCleaning.hpp @@ -54,14 +54,10 @@ public: // Cleans out the Klass tree from stale data. class KlassCleaningTask : public StackObj { - volatile bool _clean_klass_tree_claimed; - ClassLoaderDataGraphKlassIteratorAtomic _klass_iterator; - - bool claim_clean_klass_tree_task(); - InstanceKlass* claim_next_klass(); + ClassLoaderDataGraphIteratorAtomic _cld_iterator_atomic; public: - KlassCleaningTask(); + KlassCleaningTask() : _cld_iterator_atomic() { } void work(); }; diff --git a/src/hotspot/share/oops/klass.cpp b/src/hotspot/share/oops/klass.cpp index a93875b86a5..b3386694b79 100644 --- a/src/hotspot/share/oops/klass.cpp +++ b/src/hotspot/share/oops/klass.cpp @@ -614,8 +614,7 @@ GrowableArray* Klass::compute_secondary_supers(int num_extra_slots, // subklass links. Used by the compiler (and vtable initialization) // May be cleaned concurrently, so must use the Compile_lock. -// The log parameter is for clean_weak_klass_links to report unlinked classes. -Klass* Klass::subklass(bool log) const { +Klass* Klass::subklass() const { // Need load_acquire on the _subklass, because it races with inserts that // publishes freshly initialized data. for (Klass* chain = AtomicAccess::load_acquire(&_subklass); @@ -626,11 +625,6 @@ Klass* Klass::subklass(bool log) const { { if (chain->is_loader_alive()) { return chain; - } else if (log) { - if (log_is_enabled(Trace, class, unload)) { - ResourceMark rm; - log_trace(class, unload)("unlinking class (subclass): %s", chain->external_name()); - } } } return nullptr; @@ -701,15 +695,20 @@ void Klass::append_to_sibling_list() { DEBUG_ONLY(verify();) } -void Klass::clean_subklass() { +// The log parameter is for clean_weak_klass_links to report unlinked classes. +Klass* Klass::clean_subklass(bool log) { for (;;) { // Need load_acquire, due to contending with concurrent inserts Klass* subklass = AtomicAccess::load_acquire(&_subklass); if (subklass == nullptr || subklass->is_loader_alive()) { - return; + return subklass; + } + if (log && log_is_enabled(Trace, class, unload)) { + ResourceMark rm; + log_trace(class, unload)("unlinking class (subclass): %s", subklass->external_name()); } // Try to fix _subklass until it points at something not dead. - AtomicAccess::cmpxchg(&_subklass, subklass, subklass->next_sibling()); + AtomicAccess::cmpxchg(&_subklass, subklass, subklass->next_sibling(log)); } } @@ -728,8 +727,7 @@ void Klass::clean_weak_klass_links(bool unloading_occurred, bool clean_alive_kla assert(current->is_loader_alive(), "just checking, this should be live"); // Find and set the first alive subklass - Klass* sub = current->subklass(true); - current->clean_subklass(); + Klass* sub = current->clean_subklass(true); if (sub != nullptr) { stack.push(sub); } diff --git a/src/hotspot/share/oops/klass.hpp b/src/hotspot/share/oops/klass.hpp index 70d9ce3a881..db3360b080e 100644 --- a/src/hotspot/share/oops/klass.hpp +++ b/src/hotspot/share/oops/klass.hpp @@ -300,7 +300,7 @@ protected: // Use InstanceKlass::contains_field_offset to classify field offsets. // sub/superklass links - Klass* subklass(bool log = false) const; + Klass* subklass() const; Klass* next_sibling(bool log = false) const; void append_to_sibling_list(); // add newly created receiver to superklass' subklass list @@ -413,9 +413,9 @@ protected: virtual ModuleEntry* module() const = 0; virtual PackageEntry* package() const = 0; + void set_next_sibling(Klass* s); protected: // internal accessors void set_subklass(Klass* s); - void set_next_sibling(Klass* s); private: static uint8_t compute_hash_slot(Symbol* s); @@ -743,7 +743,7 @@ public: inline bool is_loader_alive() const; inline bool is_loader_present_and_alive() const; - void clean_subklass(); + Klass* clean_subklass(bool log = false); // Clean out unnecessary weak klass links from the whole klass hierarchy. static void clean_weak_klass_links(bool unloading_occurred, bool clean_alive_klasses = true); From aea71ccab7d21ae72564a07f74199eac14c7a958 Mon Sep 17 00:00:00 2001 From: Afshin Zafari Date: Tue, 30 Sep 2025 08:54:53 +0000 Subject: [PATCH 292/556] 8342730: Get rid of SummaryDiff in VMATree Reviewed-by: jsjolen, phubner --- src/hotspot/share/nmt/memoryFileTracker.cpp | 6 +- src/hotspot/share/nmt/regionsTree.cpp | 8 +- src/hotspot/share/nmt/regionsTree.hpp | 4 +- .../share/nmt/virtualMemoryTracker.cpp | 12 +- src/hotspot/share/nmt/vmatree.cpp | 16 +- src/hotspot/share/nmt/vmatree.hpp | 21 +- test/hotspot/gtest/nmt/test_regions_tree.cpp | 109 +++--- test/hotspot/gtest/nmt/test_vmatree.cpp | 310 +++++++++++------- .../runtime/test_virtualMemoryTracker.cpp | 146 +++++---- 9 files changed, 364 insertions(+), 268 deletions(-) diff --git a/src/hotspot/share/nmt/memoryFileTracker.cpp b/src/hotspot/share/nmt/memoryFileTracker.cpp index 2f3f4f1973a..fe723d09364 100644 --- a/src/hotspot/share/nmt/memoryFileTracker.cpp +++ b/src/hotspot/share/nmt/memoryFileTracker.cpp @@ -42,7 +42,8 @@ void MemoryFileTracker::allocate_memory(MemoryFile* file, size_t offset, MemTag mem_tag) { NativeCallStackStorage::StackIndex sidx = _stack_storage.push(stack); VMATree::RegionData regiondata(sidx, mem_tag); - VMATree::SummaryDiff diff = file->_tree.commit_mapping(offset, size, regiondata); + VMATree::SummaryDiff diff; + file->_tree.commit_mapping(offset, size, regiondata, diff); for (int i = 0; i < mt_number_of_tags; i++) { VirtualMemory* summary = file->_summary.by_tag(NMTUtil::index_to_tag(i)); summary->reserve_memory(diff.tag[i].commit); @@ -51,7 +52,8 @@ void MemoryFileTracker::allocate_memory(MemoryFile* file, size_t offset, } void MemoryFileTracker::free_memory(MemoryFile* file, size_t offset, size_t size) { - VMATree::SummaryDiff diff = file->_tree.release_mapping(offset, size); + VMATree::SummaryDiff diff; + file->_tree.release_mapping(offset, size, diff); for (int i = 0; i < mt_number_of_tags; i++) { VirtualMemory* summary = file->_summary.by_tag(NMTUtil::index_to_tag(i)); summary->reserve_memory(diff.tag[i].commit); diff --git a/src/hotspot/share/nmt/regionsTree.cpp b/src/hotspot/share/nmt/regionsTree.cpp index a2f5a5df67a..83306cbc14f 100644 --- a/src/hotspot/share/nmt/regionsTree.cpp +++ b/src/hotspot/share/nmt/regionsTree.cpp @@ -25,12 +25,12 @@ #include "nmt/regionsTree.inline.hpp" #include "nmt/virtualMemoryTracker.hpp" -VMATree::SummaryDiff RegionsTree::commit_region(address addr, size_t size, const NativeCallStack& stack) { - return commit_mapping((VMATree::position)addr, size, make_region_data(stack, mtNone), /*use tag inplace*/ true); +void RegionsTree::commit_region(address addr, size_t size, const NativeCallStack& stack, VMATree::SummaryDiff& diff) { + commit_mapping((VMATree::position)addr, size, make_region_data(stack, mtNone), diff, /*use tag inplace*/ true); } -VMATree::SummaryDiff RegionsTree::uncommit_region(address addr, size_t size) { - return uncommit_mapping((VMATree::position)addr, size, make_region_data(NativeCallStack::empty_stack(), mtNone)); +void RegionsTree::uncommit_region(address addr, size_t size, VMATree::SummaryDiff& diff) { + uncommit_mapping((VMATree::position)addr, size, make_region_data(NativeCallStack::empty_stack(), mtNone), diff); } #ifdef ASSERT diff --git a/src/hotspot/share/nmt/regionsTree.hpp b/src/hotspot/share/nmt/regionsTree.hpp index 35272c27423..2e1b37d0c1a 100644 --- a/src/hotspot/share/nmt/regionsTree.hpp +++ b/src/hotspot/share/nmt/regionsTree.hpp @@ -48,8 +48,8 @@ class RegionsTree : public VMATree { ReservedMemoryRegion find_reserved_region(address addr); - SummaryDiff commit_region(address addr, size_t size, const NativeCallStack& stack); - SummaryDiff uncommit_region(address addr, size_t size); + void commit_region(address addr, size_t size, const NativeCallStack& stack, SummaryDiff& diff); + void uncommit_region(address addr, size_t size, SummaryDiff& diff); using Node = VMATree::TNode; diff --git a/src/hotspot/share/nmt/virtualMemoryTracker.cpp b/src/hotspot/share/nmt/virtualMemoryTracker.cpp index 643a3d1f8d7..d676d93e040 100644 --- a/src/hotspot/share/nmt/virtualMemoryTracker.cpp +++ b/src/hotspot/share/nmt/virtualMemoryTracker.cpp @@ -69,7 +69,8 @@ void VirtualMemoryTracker::Instance::add_reserved_region(address base_addr, size void VirtualMemoryTracker::add_reserved_region(address base_addr, size_t size, const NativeCallStack& stack, MemTag mem_tag) { - VMATree::SummaryDiff diff = tree()->reserve_mapping((size_t)base_addr, size, tree()->make_region_data(stack, mem_tag)); + VMATree::SummaryDiff diff; + tree()->reserve_mapping((size_t)base_addr, size, tree()->make_region_data(stack, mem_tag), diff); apply_summary_diff(diff); } @@ -148,7 +149,8 @@ void VirtualMemoryTracker::Instance::add_committed_region(address addr, size_t s void VirtualMemoryTracker::add_committed_region(address addr, size_t size, const NativeCallStack& stack) { - VMATree::SummaryDiff diff = tree()->commit_region(addr, size, stack); + VMATree::SummaryDiff diff; + tree()->commit_region(addr, size, stack, diff); apply_summary_diff(diff); } @@ -159,7 +161,8 @@ void VirtualMemoryTracker::Instance::remove_uncommitted_region(address addr, siz void VirtualMemoryTracker::remove_uncommitted_region(address addr, size_t size) { MemTracker::assert_locked(); - VMATree::SummaryDiff diff = tree()->uncommit_region(addr, size); + VMATree::SummaryDiff diff; + tree()->uncommit_region(addr, size, diff); apply_summary_diff(diff); } @@ -169,7 +172,8 @@ void VirtualMemoryTracker::Instance::remove_released_region(address addr, size_t } void VirtualMemoryTracker::remove_released_region(address addr, size_t size) { - VMATree::SummaryDiff diff = tree()->release_mapping((VMATree::position)addr, size); + VMATree::SummaryDiff diff; + tree()->release_mapping((VMATree::position)addr, size, diff); apply_summary_diff(diff); } diff --git a/src/hotspot/share/nmt/vmatree.cpp b/src/hotspot/share/nmt/vmatree.cpp index 69887068cb2..df7b1ac867e 100644 --- a/src/hotspot/share/nmt/vmatree.cpp +++ b/src/hotspot/share/nmt/vmatree.cpp @@ -242,14 +242,14 @@ void VMATree::update_region(TNode* n1, TNode* n2, const RequestInfo& req, Summar } -VMATree::SummaryDiff VMATree::register_mapping(position _A, position _B, StateType state, - const RegionData& metadata, bool use_tag_inplace) { +void VMATree::register_mapping(position _A, position _B, StateType state, + const RegionData& metadata, VMATree::SummaryDiff& diff, bool use_tag_inplace) { + diff.clear(); if (_A == _B) { - return SummaryDiff(); + return; } assert(_A < _B, "should be"); - SummaryDiff diff; RequestInfo req{_A, _B, state, metadata.mem_tag, metadata.stack_idx, use_tag_inplace}; IntervalChange stA{ IntervalState{StateType::Released, empty_regiondata}, @@ -644,8 +644,6 @@ VMATree::SummaryDiff VMATree::register_mapping(position _A, position _B, StateTy while(to_be_removed.length() != 0) { _tree.remove(to_be_removed.pop()); } - - return diff; } #ifdef ASSERT @@ -702,7 +700,8 @@ VMATree::SummaryDiff VMATree::set_tag(const position start, const size size, con // Ignore any released ranges, these must be mtNone and have no stack if (type != StateType::Released) { RegionData new_data = RegionData(out.reserved_stack(), tag); - SummaryDiff result = register_mapping(from, end, type, new_data); + SummaryDiff result; + register_mapping(from, end, type, new_data, result); diff.add(result); } @@ -723,7 +722,8 @@ VMATree::SummaryDiff VMATree::set_tag(const position start, const size size, con if (type != StateType::Released) { RegionData new_data = RegionData(out.reserved_stack(), tag); - SummaryDiff result = register_mapping(from, end, type, new_data); + SummaryDiff result; + register_mapping(from, end, type, new_data, result); diff.add(result); } remsize = remsize - (end - from); diff --git a/src/hotspot/share/nmt/vmatree.hpp b/src/hotspot/share/nmt/vmatree.hpp index dff2491c69c..f7ca8f4c7e0 100644 --- a/src/hotspot/share/nmt/vmatree.hpp +++ b/src/hotspot/share/nmt/vmatree.hpp @@ -241,6 +241,9 @@ public: struct SummaryDiff { SingleDiff tag[mt_number_of_tags]; SummaryDiff() { + clear(); + } + void clear() { for (int i = 0; i < mt_number_of_tags; i++) { tag[i] = SingleDiff{0, 0}; } @@ -283,7 +286,7 @@ public: }; private: - SummaryDiff register_mapping(position A, position B, StateType state, const RegionData& metadata, bool use_tag_inplace = false); + void register_mapping(position A, position B, StateType state, const RegionData& metadata, SummaryDiff& diff, bool use_tag_inplace = false); StateType get_new_state(const StateType existinting_state, const RequestInfo& req) const; MemTag get_new_tag(const MemTag existinting_tag, const RequestInfo& req) const; SIndex get_new_reserve_callstack(const SIndex existinting_stack, const StateType ex, const RequestInfo& req) const; @@ -298,12 +301,12 @@ public: } public: - SummaryDiff reserve_mapping(position from, size size, const RegionData& metadata) { - return register_mapping(from, from + size, StateType::Reserved, metadata, false); + void reserve_mapping(position from, size size, const RegionData& metadata, SummaryDiff& diff ) { + register_mapping(from, from + size, StateType::Reserved, metadata, diff, false); } - SummaryDiff commit_mapping(position from, size size, const RegionData& metadata, bool use_tag_inplace = false) { - return register_mapping(from, from + size, StateType::Committed, metadata, use_tag_inplace); + void commit_mapping(position from, size size, const RegionData& metadata, SummaryDiff& diff, bool use_tag_inplace = false) { + register_mapping(from, from + size, StateType::Committed, metadata, diff, use_tag_inplace); } // Given an interval and a tag, find all reserved and committed ranges at least @@ -312,12 +315,12 @@ public: // Released regions are ignored. SummaryDiff set_tag(position from, size size, MemTag tag); - SummaryDiff uncommit_mapping(position from, size size, const RegionData& metadata) { - return register_mapping(from, from + size, StateType::Reserved, metadata, true); + void uncommit_mapping(position from, size size, const RegionData& metadata, SummaryDiff& diff) { + register_mapping(from, from + size, StateType::Reserved, metadata, diff, true); } - SummaryDiff release_mapping(position from, position sz) { - return register_mapping(from, from + sz, StateType::Released, VMATree::empty_regiondata); + void release_mapping(position from, position sz, SummaryDiff& diff) { + register_mapping(from, from + sz, StateType::Released, VMATree::empty_regiondata, diff); } public: diff --git a/test/hotspot/gtest/nmt/test_regions_tree.cpp b/test/hotspot/gtest/nmt/test_regions_tree.cpp index 394c863746e..a7f4a6962ca 100644 --- a/test/hotspot/gtest/nmt/test_regions_tree.cpp +++ b/test/hotspot/gtest/nmt/test_regions_tree.cpp @@ -41,46 +41,69 @@ TEST_VM_F(NMTRegionsTreeTest, ReserveCommitTwice) { NativeCallStack ncs; VMATree::RegionData rd = rt.make_region_data(ncs, mtTest); VMATree::RegionData rd2 = rt.make_region_data(ncs, mtGC); - VMATree::SummaryDiff diff; - diff = rt.reserve_mapping(0, 100, rd); - EXPECT_EQ(100, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); - diff = rt.commit_region(0, 50, ncs); - diff = rt.reserve_mapping(0, 100, rd); - EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); - EXPECT_EQ(-50, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); - diff = rt.reserve_mapping(0, 100, rd2); - EXPECT_EQ(-100, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); - EXPECT_EQ(100, diff.tag[NMTUtil::tag_to_index(mtGC)].reserve); - diff = rt.commit_region(0, 50, ncs); - EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtGC)].reserve); - EXPECT_EQ(50, diff.tag[NMTUtil::tag_to_index(mtGC)].commit); - diff = rt.commit_region(0, 50, ncs); - EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); - EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); + { + VMATree::SummaryDiff diff; + rt.reserve_mapping(0, 100, rd, diff); + EXPECT_EQ(100, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); + } + { + VMATree::SummaryDiff diff, not_used; + rt.commit_region(0, 50, ncs, not_used); + rt.reserve_mapping(0, 100, rd, diff); + EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); + EXPECT_EQ(-50, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); + } + { + VMATree::SummaryDiff diff; + rt.reserve_mapping(0, 100, rd2, diff); + EXPECT_EQ(-100, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); + EXPECT_EQ(100, diff.tag[NMTUtil::tag_to_index(mtGC)].reserve); + } + + { + VMATree::SummaryDiff diff1, diff2; + rt.commit_region(0, 50, ncs, diff1); + EXPECT_EQ(0, diff1.tag[NMTUtil::tag_to_index(mtGC)].reserve); + EXPECT_EQ(50, diff1.tag[NMTUtil::tag_to_index(mtGC)].commit); + rt.commit_region(0, 50, ncs, diff2); + EXPECT_EQ(0, diff2.tag[NMTUtil::tag_to_index(mtTest)].reserve); + EXPECT_EQ(0, diff2.tag[NMTUtil::tag_to_index(mtTest)].commit); + } } TEST_VM_F(NMTRegionsTreeTest, CommitUncommitRegion) { NativeCallStack ncs; VMATree::RegionData rd = rt.make_region_data(ncs, mtTest); - rt.reserve_mapping(0, 100, rd); - VMATree::SummaryDiff diff = rt.commit_region(0, 50, ncs); - EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); - EXPECT_EQ(50, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); - diff = rt.commit_region((address)60, 10, ncs); - EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); - EXPECT_EQ(10, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); - diff = rt.uncommit_region(0, 50); - EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); - EXPECT_EQ(-50, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); + VMATree::SummaryDiff not_used; + rt.reserve_mapping(0, 100, rd, not_used); + { + VMATree::SummaryDiff diff; + rt.commit_region(0, 50, ncs, diff); + EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); + EXPECT_EQ(50, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); + } + { + VMATree::SummaryDiff diff; + rt.commit_region((address)60, 10, ncs, diff); + EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); + EXPECT_EQ(10, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); + } + { + VMATree::SummaryDiff diff; + rt.uncommit_region(0, 50, diff); + EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); + EXPECT_EQ(-50, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); + } } TEST_VM_F(NMTRegionsTreeTest, FindReservedRegion) { NativeCallStack ncs; VMATree::RegionData rd = rt.make_region_data(ncs, mtTest); - rt.reserve_mapping(1000, 50, rd); - rt.reserve_mapping(1200, 50, rd); - rt.reserve_mapping(1300, 50, rd); - rt.reserve_mapping(1400, 50, rd); + VMATree::SummaryDiff not_used; + rt.reserve_mapping(1000, 50, rd, not_used); + rt.reserve_mapping(1200, 50, rd, not_used); + rt.reserve_mapping(1300, 50, rd, not_used); + rt.reserve_mapping(1400, 50, rd, not_used); ReservedMemoryRegion rmr; rmr = rt.find_reserved_region((address)1205); EXPECT_EQ(rmr.base(), (address)1200); @@ -95,10 +118,11 @@ TEST_VM_F(NMTRegionsTreeTest, FindReservedRegion) { TEST_VM_F(NMTRegionsTreeTest, VisitReservedRegions) { NativeCallStack ncs; VMATree::RegionData rd = rt.make_region_data(ncs, mtTest); - rt.reserve_mapping(1000, 50, rd); - rt.reserve_mapping(1200, 50, rd); - rt.reserve_mapping(1300, 50, rd); - rt.reserve_mapping(1400, 50, rd); + VMATree::SummaryDiff not_used; + rt.reserve_mapping(1000, 50, rd, not_used); + rt.reserve_mapping(1200, 50, rd, not_used); + rt.reserve_mapping(1300, 50, rd, not_used); + rt.reserve_mapping(1400, 50, rd, not_used); rt.visit_reserved_regions([&](const ReservedMemoryRegion& rgn) { EXPECT_EQ(((size_t)rgn.base()) % 100, 0UL); @@ -110,15 +134,16 @@ TEST_VM_F(NMTRegionsTreeTest, VisitReservedRegions) { TEST_VM_F(NMTRegionsTreeTest, VisitCommittedRegions) { NativeCallStack ncs; VMATree::RegionData rd = rt.make_region_data(ncs, mtTest); - rt.reserve_mapping(1000, 50, rd); - rt.reserve_mapping(1200, 50, rd); - rt.reserve_mapping(1300, 50, rd); - rt.reserve_mapping(1400, 50, rd); + VMATree::SummaryDiff not_used; + rt.reserve_mapping(1000, 50, rd, not_used); + rt.reserve_mapping(1200, 50, rd, not_used); + rt.reserve_mapping(1300, 50, rd, not_used); + rt.reserve_mapping(1400, 50, rd, not_used); - rt.commit_region((address)1010, 5UL, ncs); - rt.commit_region((address)1020, 5UL, ncs); - rt.commit_region((address)1030, 5UL, ncs); - rt.commit_region((address)1040, 5UL, ncs); + rt.commit_region((address)1010, 5UL, ncs, not_used); + rt.commit_region((address)1020, 5UL, ncs, not_used); + rt.commit_region((address)1030, 5UL, ncs, not_used); + rt.commit_region((address)1040, 5UL, ncs, not_used); ReservedMemoryRegion rmr((address)1000, 50); size_t count = 0; rt.visit_committed_regions(rmr, [&](CommittedMemoryRegion& crgn) { diff --git a/test/hotspot/gtest/nmt/test_vmatree.cpp b/test/hotspot/gtest/nmt/test_vmatree.cpp index cc4493a0e0f..eed2e5af0be 100644 --- a/test/hotspot/gtest/nmt/test_vmatree.cpp +++ b/test/hotspot/gtest/nmt/test_vmatree.cpp @@ -92,14 +92,15 @@ public: // Adjacent reservations are merged if the properties match. void adjacent_2_nodes(const VMATree::RegionData& rd) { Tree tree; + VMATree::SummaryDiff not_used; for (int i = 0; i < 10; i++) { - tree.reserve_mapping(i * 100, 100, rd); + tree.reserve_mapping(i * 100, 100, rd, not_used); } EXPECT_EQ(2, count_nodes(tree)); // Reserving the exact same space again should result in still having only 2 nodes for (int i = 0; i < 10; i++) { - tree.reserve_mapping(i * 100, 100, rd); + tree.reserve_mapping(i * 100, 100, rd, not_used); } EXPECT_EQ(2, count_nodes(tree)); @@ -111,7 +112,7 @@ public: // ... // 0--100 for (int i = 9; i >= 0; i--) { - tree2.reserve_mapping(i * 100, 100, rd); + tree2.reserve_mapping(i * 100, 100, rd, not_used); } EXPECT_EQ(2, count_nodes(tree2)); } @@ -119,16 +120,17 @@ public: // After removing all ranges we should be left with an entirely empty tree void remove_all_leaves_empty_tree(const VMATree::RegionData& rd) { Tree tree; - tree.reserve_mapping(0, 100 * 10, rd); + VMATree::SummaryDiff not_used; + tree.reserve_mapping(0, 100 * 10, rd, not_used); for (int i = 0; i < 10; i++) { - tree.release_mapping(i * 100, 100); + tree.release_mapping(i * 100, 100, not_used); } EXPECT_EQ(nullptr, rbtree_root(tree)); // Other way around - tree.reserve_mapping(0, 100 * 10, rd); + tree.reserve_mapping(0, 100 * 10, rd, not_used); for (int i = 9; i >= 0; i--) { - tree.release_mapping(i * 100, 100); + tree.release_mapping(i * 100, 100, not_used); } EXPECT_EQ(nullptr, rbtree_root(tree)); } @@ -136,9 +138,10 @@ public: // Committing in a whole reserved range results in 2 nodes void commit_whole(const VMATree::RegionData& rd) { Tree tree; - tree.reserve_mapping(0, 100 * 10, rd); + VMATree::SummaryDiff not_used; + tree.reserve_mapping(0, 100 * 10, rd, not_used); for (int i = 0; i < 10; i++) { - tree.commit_mapping(i * 100, 100, rd); + tree.commit_mapping(i * 100, 100, rd, not_used); } rbtree(tree).visit_in_order([&](TNode* x) { VMATree::StateType in = in_type_of(x); @@ -153,8 +156,9 @@ public: // Committing in middle of reservation ends with a sequence of 4 nodes void commit_middle(const VMATree::RegionData& rd) { Tree tree; - tree.reserve_mapping(0, 100, rd); - tree.commit_mapping(50, 25, rd); + VMATree::SummaryDiff not_used; + tree.reserve_mapping(0, 100, rd, not_used); + tree.commit_mapping(50, 25, rd, not_used); size_t found[16]; size_t wanted[4] = {0, 50, 75, 100}; @@ -342,8 +346,9 @@ public: TEST_VM_F(NMTVMATreeTest, OverlappingReservationsResultInTwoNodes) { VMATree::RegionData rd{si[0], mtTest}; Tree tree; + VMATree::SummaryDiff not_used; for (int i = 99; i >= 0; i--) { - tree.reserve_mapping(i * 100, 101, rd); + tree.reserve_mapping(i * 100, 101, rd, not_used); } EXPECT_EQ(2, count_nodes(tree)); } @@ -351,8 +356,9 @@ TEST_VM_F(NMTVMATreeTest, OverlappingReservationsResultInTwoNodes) { TEST_VM_F(NMTVMATreeTest, DuplicateReserve) { VMATree::RegionData rd{si[0], mtTest}; Tree tree; - tree.reserve_mapping(100, 100, rd); - tree.reserve_mapping(100, 100, rd); + VMATree::SummaryDiff not_used; + tree.reserve_mapping(100, 100, rd, not_used); + tree.reserve_mapping(100, 100, rd, not_used); EXPECT_EQ(2, count_nodes(tree)); VMATree::VMARBTree::Range r = tree.tree().find_enclosing_range(110); EXPECT_EQ(100, (int)(r.end->key() - r.start->key())); @@ -360,15 +366,16 @@ TEST_VM_F(NMTVMATreeTest, DuplicateReserve) { TEST_VM_F(NMTVMATreeTest, UseTagInplace) { Tree tree; + VMATree::SummaryDiff not_used; VMATree::RegionData rd_Test_cs0(si[0], mtTest); VMATree::RegionData rd_None_cs1(si[1], mtNone); - tree.reserve_mapping(0, 100, rd_Test_cs0); + tree.reserve_mapping(0, 100, rd_Test_cs0, not_used); // reserve: 0---------------------100 // commit: 20**********70 // uncommit: 30--40 // post-cond: 0---20**30--40**70----100 - tree.commit_mapping(20, 50, rd_None_cs1, true); - tree.uncommit_mapping(30, 10, rd_None_cs1); + tree.commit_mapping(20, 50, rd_None_cs1, not_used, true); + tree.uncommit_mapping(30, 10, rd_None_cs1, not_used); tree.visit_in_order([&](const TNode* node) { if (node->key() != 100) { EXPECT_EQ(mtTest, node->val().out.mem_tag()) << "failed at: " << node->key(); @@ -395,10 +402,11 @@ TEST_VM_F(NMTVMATreeTest, LowLevel) { { // Identical operation but different metadata should not merge Tree tree; + VMATree::SummaryDiff not_used; VMATree::RegionData rd_Test_cs0{si[0], mtTest}; VMATree::RegionData rd_NMT_cs1{si[1], mtNMT}; - tree.reserve_mapping(0, 100, rd_Test_cs0); - tree.reserve_mapping(100, 100, rd_NMT_cs1); + tree.reserve_mapping(0, 100, rd_Test_cs0, not_used); + tree.reserve_mapping(100, 100, rd_NMT_cs1, not_used); EXPECT_EQ(3, count_nodes(tree)); int found_nodes = 0; @@ -406,10 +414,11 @@ TEST_VM_F(NMTVMATreeTest, LowLevel) { { // Reserving after commit should overwrite commit Tree tree; + VMATree::SummaryDiff not_used; VMATree::RegionData rd_Test_cs0{si[0], mtTest}; VMATree::RegionData rd_NMT_cs1{si[1], mtNMT}; - tree.commit_mapping(50, 50, rd_NMT_cs1); - tree.reserve_mapping(0, 100, rd_Test_cs0); + tree.commit_mapping(50, 50, rd_NMT_cs1, not_used); + tree.reserve_mapping(0, 100, rd_Test_cs0, not_used); rbtree(tree).visit_in_order([&](const TNode* x) { EXPECT_TRUE(x->key() == 0 || x->key() == 100); if (x->key() == 0UL) { @@ -423,20 +432,22 @@ TEST_VM_F(NMTVMATreeTest, LowLevel) { { // Split a reserved region into two different reserved regions Tree tree; + VMATree::SummaryDiff not_used; VMATree::RegionData rd_Test_cs0{si[0], mtTest}; VMATree::RegionData rd_NMT_cs1{si[1], mtNMT}; VMATree::RegionData rd_None_cs0{si[0], mtNone}; - tree.reserve_mapping(0, 100, rd_Test_cs0); - tree.reserve_mapping(0, 50, rd_NMT_cs1); - tree.reserve_mapping(50, 50, rd_None_cs0); + tree.reserve_mapping(0, 100, rd_Test_cs0, not_used); + tree.reserve_mapping(0, 50, rd_NMT_cs1, not_used); + tree.reserve_mapping(50, 50, rd_None_cs0, not_used); EXPECT_EQ(3, count_nodes(tree)); } { // One big reserve + release leaves an empty tree VMATree::RegionData rd_NMT_cs0{si[0], mtNMT}; Tree tree; - tree.reserve_mapping(0, 500000, rd_NMT_cs0); - tree.release_mapping(0, 500000); + VMATree::SummaryDiff not_used; + tree.reserve_mapping(0, 500000, rd_NMT_cs0, not_used); + tree.release_mapping(0, 500000, not_used); EXPECT_EQ(nullptr, rbtree_root(tree)); } @@ -446,8 +457,9 @@ TEST_VM_F(NMTVMATreeTest, LowLevel) { VMATree::RegionData rd_NMT_cs0{si[0], mtNMT}; VMATree::RegionData rd_Test_cs1{si[1], mtTest}; Tree tree; - tree.reserve_mapping(0, 100, rd_NMT_cs0); - tree.commit_mapping(0, 100, rd_Test_cs1); + VMATree::SummaryDiff not_used; + tree.reserve_mapping(0, 100, rd_NMT_cs0, not_used); + tree.commit_mapping(0, 100, rd_Test_cs1, not_used); rbtree(tree).visit_range_in_order(0, 99999, [&](TNode* x) { if (x->key() == 0) { EXPECT_EQ(mtTest, x->val().out.reserved_regiondata().mem_tag); @@ -461,10 +473,11 @@ TEST_VM_F(NMTVMATreeTest, LowLevel) { { // Attempting to reserve or commit an empty region should not change the tree. Tree tree; + VMATree::SummaryDiff not_used; VMATree::RegionData rd_NMT_cs0{si[0], mtNMT}; - tree.reserve_mapping(0, 0, rd_NMT_cs0); + tree.reserve_mapping(0, 0, rd_NMT_cs0, not_used); EXPECT_EQ(nullptr, rbtree_root(tree)); - tree.commit_mapping(0, 0, rd_NMT_cs0); + tree.commit_mapping(0, 0, rd_NMT_cs0, not_used); EXPECT_EQ(nullptr, rbtree_root(tree)); } } @@ -520,8 +533,9 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {500, 600, mtClassShared, si, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; - tree.reserve_mapping(0, 600, rd); + tree.reserve_mapping(0, 600, rd, not_used); tree.set_tag(0, 500, mtGC); tree.set_tag(500, 100, mtClassShared); @@ -540,6 +554,7 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {575, 600, mtClassShared, si, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; // 0---------------------------------------------------600 // 100****225 @@ -548,11 +563,11 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { // 0------100****225---------550***560---565***575-----600 // 0------100****225---500---550***560---565***575-----600 // <-------mtGC---------><-----------mtClassShared-------> - tree.reserve_mapping(0, 600, rd); + tree.reserve_mapping(0, 600, rd, not_used); // The committed areas - tree.commit_mapping(100, 125, rd); - tree.commit_mapping(550, 10, rd); - tree.commit_mapping(565, 10, rd); + tree.commit_mapping(100, 125, rd, not_used); + tree.commit_mapping(550, 10, rd, not_used); + tree.commit_mapping(565, 10, rd, not_used); // OK, set tag tree.set_tag(0, 500, mtGC); tree.set_tag(500, 100, mtClassShared); @@ -564,10 +579,11 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {0, 200, mtGC, si, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; Tree::RegionData gc(si, mtGC); Tree::RegionData compiler(si, mtCompiler); - tree.reserve_mapping(0, 100, gc); - tree.reserve_mapping(100, 100, compiler); + tree.reserve_mapping(0, 100, gc, not_used); + tree.reserve_mapping(100, 100, compiler, not_used); tree.set_tag(0, 200, mtGC); expect_equivalent_form(expected, tree, __LINE__); } @@ -580,10 +596,11 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {100, 200, mtGC, si2, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; Tree::RegionData gc(si1, mtGC); Tree::RegionData compiler(si2, mtCompiler); - tree.reserve_mapping(0, 100, gc); - tree.reserve_mapping(100, 100, compiler); + tree.reserve_mapping(0, 100, gc, not_used); + tree.reserve_mapping(100, 100, compiler, not_used); tree.set_tag(0, 200, mtGC); expect_equivalent_form(expected, tree, __LINE__); } @@ -595,8 +612,9 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {150, 200, mtCompiler, si, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; Tree::RegionData compiler(si, mtCompiler); - tree.reserve_mapping(0, 200, compiler); + tree.reserve_mapping(0, 200, compiler, not_used); tree.set_tag(100, 50, mtGC); expect_equivalent_form(expected, tree, __LINE__); } @@ -608,10 +626,11 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {125, 200, mtCompiler, si, State::Reserved}, }; VMATree tree; + VMATree::SummaryDiff not_used; Tree::RegionData gc(si, mtGC); Tree::RegionData compiler(si, mtCompiler); - tree.reserve_mapping(0, 100, gc); - tree.reserve_mapping(100, 100, compiler); + tree.reserve_mapping(0, 100, gc, not_used); + tree.reserve_mapping(100, 100, compiler, not_used); tree.set_tag(75, 50, mtClass); expect_equivalent_form(expected, tree, __LINE__); } @@ -624,9 +643,10 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {80, 100, mtClassShared, si, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; Tree::RegionData class_shared(si, mtClassShared); - tree.reserve_mapping(0, 50, class_shared); - tree.reserve_mapping(75, 25, class_shared); + tree.reserve_mapping(0, 50, class_shared, not_used); + tree.reserve_mapping(75, 25, class_shared, not_used); tree.set_tag(0, 80, mtGC); expect_equivalent_form(expected, tree, __LINE__); } @@ -636,8 +656,9 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {10, 20, mtCompiler, si, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; Tree::RegionData class_shared(si, mtClassShared); - tree.reserve_mapping(10, 10, class_shared); + tree.reserve_mapping(10, 10, class_shared, not_used); tree.set_tag(0, 100, mtCompiler); expect_equivalent_form(expected, tree, __LINE__); } @@ -651,10 +672,11 @@ TEST_VM_F(NMTVMATreeTest, SetTag) { {99, 100, mtGC, si, State::Reserved} }; VMATree tree; + VMATree::SummaryDiff not_used; Tree::RegionData class_shared(si, mtClassShared); - tree.reserve_mapping(0, 100, class_shared); - tree.release_mapping(1, 49); - tree.release_mapping(75, 24); + tree.reserve_mapping(0, 100, class_shared, not_used); + tree.release_mapping(1, 49, not_used); + tree.release_mapping(75, 24, not_used); tree.set_tag(0, 100, mtGC); expect_equivalent_form(expected, tree, __LINE__); } @@ -666,7 +688,8 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { Tree::RegionData rd_Test_cs0(NCS::StackIndex(), mtTest); Tree::RegionData rd_NMT_cs0(NCS::StackIndex(), mtNMT); Tree tree; - VMATree::SummaryDiff all_diff = tree.reserve_mapping(0, 100, rd_Test_cs0); + VMATree::SummaryDiff all_diff; + tree.reserve_mapping(0, 100, rd_Test_cs0, all_diff); // 1 2 3 4 5 6 7 8 9 10 11 // 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.......... @@ -675,7 +698,7 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { // . - free VMATree::SingleDiff diff = all_diff.tag[NMTUtil::tag_to_index(mtTest)]; EXPECT_EQ(100, diff.reserve); - all_diff = tree.reserve_mapping(50, 25, rd_NMT_cs0); + tree.reserve_mapping(50, 25, rd_NMT_cs0, all_diff); // 1 2 3 4 5 6 7 8 9 10 11 // 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCC.......... @@ -692,7 +715,8 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { { // Fully release reserved mapping Tree::RegionData rd_Test_cs0(NCS::StackIndex(), mtTest); Tree tree; - VMATree::SummaryDiff all_diff = tree.reserve_mapping(0, 100, rd_Test_cs0); + VMATree::SummaryDiff all_diff; + tree.reserve_mapping(0, 100, rd_Test_cs0, all_diff); // 1 2 3 4 5 6 7 8 9 10 11 // 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.......... @@ -701,7 +725,7 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { // . - free VMATree::SingleDiff diff = all_diff.tag[NMTUtil::tag_to_index(mtTest)]; EXPECT_EQ(100, diff.reserve); - all_diff = tree.release_mapping(0, 100); + tree.release_mapping(0, 100, all_diff); // 1 2 3 4 5 6 7 8 9 10 11 // 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 // .............................................................................................................. @@ -712,7 +736,8 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { { // Convert some of a released mapping to a committed one Tree::RegionData rd_Test_cs0(NCS::StackIndex(), mtTest); Tree tree; - VMATree::SummaryDiff all_diff = tree.reserve_mapping(0, 100, rd_Test_cs0); + VMATree::SummaryDiff all_diff; + tree.reserve_mapping(0, 100, rd_Test_cs0, all_diff); // 1 2 3 4 5 6 7 8 9 10 11 // 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.......... @@ -721,7 +746,7 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { // . - free VMATree::SingleDiff diff = all_diff.tag[NMTUtil::tag_to_index(mtTest)]; EXPECT_EQ(diff.reserve, 100); - all_diff = tree.commit_mapping(0, 100, rd_Test_cs0); + tree.commit_mapping(0, 100, rd_Test_cs0, all_diff); // 1 2 3 4 5 6 7 8 9 10 11 // 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.......... @@ -735,7 +760,8 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { { // Adjacent reserved mappings with same type Tree::RegionData rd_Test_cs0(NCS::StackIndex(), mtTest); Tree tree; - VMATree::SummaryDiff all_diff = tree.reserve_mapping(0, 10, rd_Test_cs0); + VMATree::SummaryDiff all_diff; + tree.reserve_mapping(0, 10, rd_Test_cs0, all_diff); // 1 2 // 01234567890123456789 // AAAAAAAAAA.......... @@ -744,7 +770,7 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { // . - free VMATree::SingleDiff diff = all_diff.tag[NMTUtil::tag_to_index(mtTest)]; EXPECT_EQ(diff.reserve, 10); - all_diff = tree.reserve_mapping(10, 10, rd_Test_cs0); + tree.reserve_mapping(10, 10, rd_Test_cs0, all_diff); // 1 2 3 // 012345678901234567890123456789 // AAAAAAAAAAAAAAAAAAAA.......... @@ -758,7 +784,8 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { Tree::RegionData rd_Test_cs0(NCS::StackIndex(), mtTest); Tree::RegionData rd_NMT_cs0(NCS::StackIndex(), mtNMT); Tree tree; - VMATree::SummaryDiff all_diff = tree.reserve_mapping(0, 10, rd_Test_cs0); + VMATree::SummaryDiff all_diff; + tree.reserve_mapping(0, 10, rd_Test_cs0, all_diff); // 1 2 // 01234567890123456789 // AAAAAAAAAA.......... @@ -767,7 +794,7 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { // . - free VMATree::SingleDiff diff = all_diff.tag[NMTUtil::tag_to_index(mtTest)]; EXPECT_EQ(diff.reserve, 10); - all_diff = tree.reserve_mapping(10, 10, rd_NMT_cs0); + tree.reserve_mapping(10, 10, rd_NMT_cs0, all_diff); // 1 2 3 // 012345678901234567890123456789 // AAAAAAAAAABBBBBBBBBB.......... @@ -784,22 +811,23 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { { // A commit with two previous commits inside of it should only register // the new memory in the commit diff. Tree tree; + VMATree::SummaryDiff diff; Tree::RegionData rd_Test_cs0(NCS::StackIndex(), mtTest); - tree.commit_mapping(16, 16, rd_Test_cs0); + tree.commit_mapping(16, 16, rd_Test_cs0, diff); // 1 2 3 4 // 0123456789012345678901234567890123456789 // ................aaaaaaaaaaaaaaaa.......... // Legend: // a - Test (committed) // . - free - tree.commit_mapping(32, 32, rd_Test_cs0); + tree.commit_mapping(32, 32, rd_Test_cs0, diff); // 1 2 3 4 5 6 7 // 0123456789012345678901234567890123456789012345678901234567890123456789 // ................aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.......... // Legend: // a - Test (committed) // . - free - VMATree::SummaryDiff diff = tree.commit_mapping(0, 64, rd_Test_cs0); + tree.commit_mapping(0, 64, rd_Test_cs0, diff); // 1 2 3 4 5 6 7 // 0123456789012345678901234567890123456789012345678901234567890123456789 // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.......... @@ -814,11 +842,12 @@ TEST_VM_F(NMTVMATreeTest, SummaryAccounting) { TEST_VM_F(NMTVMATreeTest, SummaryAccountingReserveAsUncommit) { Tree tree; Tree::RegionData rd(NCS::StackIndex(), mtTest); - VMATree::SummaryDiff diff1 = tree.reserve_mapping(1200, 100, rd); - VMATree::SummaryDiff diff2 = tree.commit_mapping(1210, 50, rd); + VMATree::SummaryDiff diff1, diff2, diff3; + tree.reserve_mapping(1200, 100, rd, diff1); + tree.commit_mapping(1210, 50, rd, diff2); EXPECT_EQ(100, diff1.tag[NMTUtil::tag_to_index(mtTest)].reserve); EXPECT_EQ(50, diff2.tag[NMTUtil::tag_to_index(mtTest)].commit); - VMATree::SummaryDiff diff3 = tree.reserve_mapping(1220, 20, rd); + tree.reserve_mapping(1220, 20, rd, diff3); EXPECT_EQ(-20, diff3.tag[NMTUtil::tag_to_index(mtTest)].commit); EXPECT_EQ(0, diff3.tag[NMTUtil::tag_to_index(mtTest)].reserve); } @@ -951,13 +980,13 @@ TEST_VM_F(NMTVMATreeTest, TestConsistencyWithSimpleTracker) { VMATree::SummaryDiff simple_diff; if (kind == SimpleVMATracker::Reserved) { simple_diff = tr->reserve(start, size, stack, mem_tag); - tree_diff = tree.reserve_mapping(start, size, data); + tree.reserve_mapping(start, size, data, tree_diff); } else if (kind == SimpleVMATracker::Committed) { simple_diff = tr->commit(start, size, stack, mem_tag); - tree_diff = tree.commit_mapping(start, size, data); + tree.commit_mapping(start, size, data, tree_diff); } else { simple_diff = tr->release(start, size); - tree_diff = tree.release_mapping(start, size); + tree.release_mapping(start, size, tree_diff); } for (int j = 0; j < mt_number_of_tags; j++) { @@ -1024,33 +1053,34 @@ TEST_VM_F(NMTVMATreeTest, TestConsistencyWithSimpleTracker) { TEST_VM_F(NMTVMATreeTest, SummaryAccountingWhenUseTagInplace) { Tree tree; + VMATree::SummaryDiff diff; VMATree::RegionData rd_Test_cs0(si[0], mtTest); VMATree::RegionData rd_None_cs1(si[1], mtNone); -// 1 2 3 4 5 -// 012345678901234567890123456789012345678901234567890 -// .................................................. - tree.reserve_mapping(0, 50, rd_Test_cs0); -// 1 2 3 4 5 -// 012345678901234567890123456789012345678901234567890 -// rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr -VMATree::SummaryDiff diff = tree.commit_mapping(0, 25, rd_None_cs1, true); -// 1 2 3 4 5 -// 012345678901234567890123456789012345678901234567890 -// CCCCCCCCCCCCCCCCCCCCCCCCCrrrrrrrrrrrrrrrrrrrrrrrrr + // 1 2 3 4 5 + // 012345678901234567890123456789012345678901234567890 + // .................................................. + tree.reserve_mapping(0, 50, rd_Test_cs0, diff); + // 1 2 3 4 5 + // 012345678901234567890123456789012345678901234567890 + // rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr + tree.commit_mapping(0, 25, rd_None_cs1, diff, true); + // 1 2 3 4 5 + // 012345678901234567890123456789012345678901234567890 + // CCCCCCCCCCCCCCCCCCCCCCCCCrrrrrrrrrrrrrrrrrrrrrrrrr EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); EXPECT_EQ(25, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); - diff = tree.commit_mapping(30, 5, rd_None_cs1, true); -// 1 2 3 4 5 -// 012345678901234567890123456789012345678901234567890 -// CCCCCCCCCCCCCCCCCCCCCCCCCrrrrrCCCCCrrrrrrrrrrrrrrr + tree.commit_mapping(30, 5, rd_None_cs1, diff, true); + // 1 2 3 4 5 + // 012345678901234567890123456789012345678901234567890 + // CCCCCCCCCCCCCCCCCCCCCCCCCrrrrrCCCCCrrrrrrrrrrrrrrr EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); EXPECT_EQ(5, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); - diff = tree.uncommit_mapping(0, 25, rd_None_cs1); -// 1 2 3 4 5 -// 012345678901234567890123456789012345678901234567890 -// rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrCCCCCrrrrrrrrrrrrrrr + tree.uncommit_mapping(0, 25, rd_None_cs1, diff); + // 1 2 3 4 5 + // 012345678901234567890123456789012345678901234567890 + // rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrCCCCCrrrrrrrrrrrrrrr EXPECT_EQ(0, diff.tag[NMTUtil::tag_to_index(mtTest)].reserve); EXPECT_EQ(-25, diff.tag[NMTUtil::tag_to_index(mtTest)].commit); } @@ -1079,7 +1109,8 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { {// Check committing into a reserved region inherits the call stacks Tree tree; - tree.reserve_mapping(0, 50, rd_Test_cs1); // reserve in an empty tree + VMATree::SummaryDiff diff; + tree.reserve_mapping(0, 50, rd_Test_cs1, diff); // reserve in an empty tree // Pre: empty tree. // Post: // 1 2 3 4 5 @@ -1091,7 +1122,7 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { {-1 , si_1 , -1 }, {-1 , -1 , -1 }}; check_tree(tree, et1, __LINE__); - tree.commit_mapping(25, 10, rd_None_cs2, true); // commit at the middle of the region + tree.commit_mapping(25, 10, rd_None_cs2, diff, true); // commit at the middle of the region // Post: // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 @@ -1103,7 +1134,7 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { {-1 , -1 , si_2 , -1 , -1 }}; check_tree(tree, et2, __LINE__); - tree.commit_mapping(0, 20, rd_None_cs2, true); // commit at the beginning of the region + tree.commit_mapping(0, 20, rd_None_cs2, diff, true); // commit at the beginning of the region // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // CCCCCCCCCCCCCCCCCCCCrrrrrCCCCCCCCCCrrrrrrrrrrrrrrr. @@ -1114,7 +1145,7 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { {-1 , si_2 , -1 , si_2 , -1 , -1 }}; check_tree(tree, et3, __LINE__); - tree.commit_mapping(40, 10, rd_None_cs2, true); // commit at the end of the region + tree.commit_mapping(40, 10, rd_None_cs2, diff, true); // commit at the end of the region // Post: // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 @@ -1128,7 +1159,8 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { } {// committing overlapped regions does not destroy the old call-stacks Tree tree; - tree.reserve_mapping(0, 50, rd_Test_cs1); // reserving in an empty tree + VMATree::SummaryDiff diff; + tree.reserve_mapping(0, 50, rd_Test_cs1, diff); // reserving in an empty tree // Pre: empty tree. // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 @@ -1140,7 +1172,7 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { {-1 , -1 , -1 }}; check_tree(tree, et1, __LINE__); - tree.commit_mapping(10, 10, rd_None_cs2, true); + tree.commit_mapping(10, 10, rd_None_cs2, diff, true); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrrCCCCCCCCCCrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr @@ -1154,7 +1186,7 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { SIndex si_3 = si[2]; VMATree::RegionData rd_Test_cs3(si_3, mtTest); // commit with overlap at the region's start - tree.commit_mapping(5, 10, rd_Test_cs3); + tree.commit_mapping(5, 10, rd_Test_cs3, diff); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrCCCCCCCCCCCCCCCrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr @@ -1168,7 +1200,7 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { SIndex si_4 = si[3]; VMATree::RegionData call_stack_4(si_4, mtTest); // commit with overlap at the region's end - tree.commit_mapping(15, 10, call_stack_4); + tree.commit_mapping(15, 10, call_stack_4, diff); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrCCCCCCCCCCCCCCCCCCCCrrrrrrrrrrrrrrrrrrrrrrrrr @@ -1181,13 +1213,14 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { } {// uncommit should not store any call-stack Tree tree; - tree.reserve_mapping(0, 50, rd_Test_cs1); + VMATree::SummaryDiff diff; + tree.reserve_mapping(0, 50, rd_Test_cs1, diff); - tree.commit_mapping(10, 10, rd_None_cs2, true); + tree.commit_mapping(10, 10, rd_None_cs2, diff, true); - tree.commit_mapping(0, 5, rd_None_cs2, true); + tree.commit_mapping(0, 5, rd_None_cs2, diff, true); - tree.uncommit_mapping(0, 3, rd_None_cs2); + tree.uncommit_mapping(0, 3, rd_None_cs2, diff); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrCCrrrrrCCCCCCCCCCrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr @@ -1198,7 +1231,7 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { {-1 , -1 , si_2 , -1 , si_2 , -1 , -1 }}; check_tree(tree, et1, __LINE__); - tree.uncommit_mapping(5, 10, rd_None_cs2); + tree.uncommit_mapping(5, 10, rd_None_cs2, diff); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrCCrrrrrrrrrrCCCCCrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr. @@ -1214,8 +1247,9 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { VMATree::RegionData call_stack_4(si_4, mtTest); Tree tree; - tree.reserve_mapping(0, 50, rd_Test_cs1); - tree.reserve_mapping(10, 10, call_stack_4); + VMATree::SummaryDiff diff; + tree.reserve_mapping(0, 50, rd_Test_cs1, diff); + tree.reserve_mapping(10, 10, call_stack_4, diff); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr @@ -1228,7 +1262,8 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { } {// commit without reserve Tree tree; - tree.commit_mapping(0, 50, rd_Test_cs1); + VMATree::SummaryDiff diff; + tree.commit_mapping(0, 50, rd_Test_cs1, diff); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC @@ -1241,8 +1276,9 @@ TEST_VM_F(NMTVMATreeTest, SeparateStacksForCommitAndReserve) { } {// reserve after commit Tree tree; - tree.commit_mapping(0, 50, rd_None_cs2); - tree.reserve_mapping(0, 50, rd_Test_cs1); + VMATree::SummaryDiff diff; + tree.commit_mapping(0, 50, rd_None_cs2, diff); + tree.reserve_mapping(0, 50, rd_Test_cs1, diff); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr @@ -1287,7 +1323,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows0To3) { {-1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCCCCCCC.......................... @@ -1314,7 +1351,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows0To3) { {-1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 15, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 15, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCC............................... @@ -1359,7 +1397,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows4to7) { {-1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(20, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(20, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrr..........CCCCCCCCCCCCCCCCCCCC........... @@ -1386,7 +1425,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows4to7) { {-1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(10, 10, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(10, 10, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....rrrrrCCCCCCCCCC............................... @@ -1413,7 +1453,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows4to7) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(7, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(7, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrr..CCCCCCCCCCCCCCCCCCCC........................ @@ -1440,7 +1481,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows4to7) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(7, 13, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(7, 13, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrr..CCCCCCCCCCCCC............................... @@ -1492,7 +1534,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows8to11) { {-1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(10, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(10, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrrCCCCCCCCCCCCCCCCCCCC..................... @@ -1519,7 +1562,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows8to11) { {-1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(0, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(0, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // CCCCCCCCCCCCCCCCCCCC............................... @@ -1546,7 +1590,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows8to11) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCCCCCCC.......................... @@ -1573,7 +1618,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows8to11) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 15, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 15, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCC............................... @@ -1619,7 +1665,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows12to15) { {-1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCCCCCCC.....rrrrrrrrrr........... @@ -1646,7 +1693,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows12to15) { {-1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCCCCCCCrrrrr..................... @@ -1673,7 +1721,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows12to15) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCCCCCCC.....rrrrrrrrrr........... @@ -1700,7 +1749,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows12to15) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 15, rd_Test_cs2, false); + VMATree::SummaryDiff diff ; + tree.commit_mapping(5, 15, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // .....CCCCCCCCCCCCCCC..........rrrrrrrrrr........... @@ -1745,7 +1795,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows16to19) { {-1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(15, 10, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(15, 10, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrr.....CCCCCCCCCC.....rrrrrrrrrr........... @@ -1772,7 +1823,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows16to19) { {-1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(15, 10, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(15, 10, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrr.....CCCCCCCCCCrrrrr..................... @@ -1799,7 +1851,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows16to19) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(7, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(7, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrr..CCCCCCCCCCCCCCCCCCCC...rrrrrrrrrr........... @@ -1826,7 +1879,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows16to19) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(7, 13, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(7, 13, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrr..CCCCCCCCCCCCC..........rrrrrrrrrr........... @@ -1872,7 +1926,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows20to23) { {-1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(10, 15, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(10, 15, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrrCCCCCCCCCCCCCCC.....rrrrrrrrrr........... @@ -1899,7 +1954,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows20to23) { {-1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(10, 15, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(10, 15, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrrrrrrCCCCCCCCCCCCCCCrrrrr..................... @@ -1926,7 +1982,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows20to23) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 20, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 20, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrCCCCCCCCCCCCCCCCCCCC.....rrrrrrrrrr........... @@ -1953,7 +2010,8 @@ TEST_VM_F(NMTVMATreeTest, OverlapTableRows20to23) { {-1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 } }; create_tree(tree, pre, __LINE__); - VMATree::SummaryDiff diff = tree.commit_mapping(5, 15, rd_Test_cs2, false); + VMATree::SummaryDiff diff; + tree.commit_mapping(5, 15, rd_Test_cs2, diff, false); // 1 2 3 4 5 // 012345678901234567890123456789012345678901234567890 // rrrrrCCCCCCCCCCCCCCC..........rrrrrrrrrr........... diff --git a/test/hotspot/gtest/runtime/test_virtualMemoryTracker.cpp b/test/hotspot/gtest/runtime/test_virtualMemoryTracker.cpp index 556b9876acc..4242302997a 100644 --- a/test/hotspot/gtest/runtime/test_virtualMemoryTracker.cpp +++ b/test/hotspot/gtest/runtime/test_virtualMemoryTracker.cpp @@ -94,6 +94,7 @@ public: RegionsTree* rtree = vmt.tree(); size_t size = 0x01000000; const address addr = (address)0x0000A000; + VMATree::SummaryDiff diff; vmt.add_reserved_region(addr, size, CALLER_PC, mtTest); @@ -115,45 +116,45 @@ public: // Commit adjacent regions with same stack { // Commit one region - rtree->commit_region(addr + cs, cs, stack); + rtree->commit_region(addr + cs, cs, stack, diff); R r[] = { {addr + cs, cs} }; check(vmt, rmr, r); } { // Commit adjacent - lower address - rtree->commit_region(addr, cs, stack); + rtree->commit_region(addr, cs, stack, diff); R r[] = { {addr, 2 * cs} }; check(vmt, rmr, r); } { // Commit adjacent - higher address - rtree->commit_region(addr + 2 * cs, cs, stack); + rtree->commit_region(addr + 2 * cs, cs, stack, diff); R r[] = { {addr, 3 * cs} }; check(vmt,rmr, r); } // Cleanup - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); ASSERT_EQ(vmt.committed_size(&rmr), 0u); // Commit adjacent regions with different stacks { // Commit one region - rtree->commit_region(addr + cs, cs, stack); + rtree->commit_region(addr + cs, cs, stack, diff); R r[] = { {addr + cs, cs} }; check(vmt, rmr, r); } { // Commit adjacent - lower address - rtree->commit_region(addr, cs, stack2); + rtree->commit_region(addr, cs, stack2, diff); R r[] = { {addr, cs}, {addr + cs, cs} }; check(vmt, rmr, r); } { // Commit adjacent - higher address - rtree->commit_region(addr + 2 * cs, cs, stack2); + rtree->commit_region(addr + 2 * cs, cs, stack2, diff); R r[] = { {addr, cs}, {addr + cs, cs}, {addr + 2 * cs, cs} }; @@ -161,12 +162,13 @@ public: } // Cleanup - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); ASSERT_EQ(vmt.committed_size(&rmr), 0u); } static void test_add_committed_region_adjacent_overlapping() { VirtualMemoryTracker vmt(true); + VMATree::SummaryDiff diff; RegionsTree* rtree = vmt.tree(); size_t size = 0x01000000; const address addr = (address)0x0000A000; @@ -190,46 +192,46 @@ public: // Commit adjacent and overlapping regions with same stack { // Commit two non-adjacent regions - rtree->commit_region(addr, 2 * cs, stack); - rtree->commit_region(addr + 3 * cs, 2 * cs, stack); + rtree->commit_region(addr, 2 * cs, stack, diff); + rtree->commit_region(addr + 3 * cs, 2 * cs, stack, diff); R r[] = { {addr, 2 * cs}, {addr + 3 * cs, 2 * cs} }; check(vmt, rmr, r); } { // Commit adjacent and overlapping - rtree->commit_region(addr + 2 * cs, 2 * cs, stack); + rtree->commit_region(addr + 2 * cs, 2 * cs, stack, diff); R r[] = { {addr, 5 * cs} }; check(vmt, rmr, r); } // revert to two non-adjacent regions - rtree->uncommit_region(addr + 2 * cs, cs); + rtree->uncommit_region(addr + 2 * cs, cs, diff); ASSERT_EQ(vmt.committed_size(&rmr), 4 * cs); { // Commit overlapping and adjacent - rtree->commit_region(addr + cs, 2 * cs, stack); + rtree->commit_region(addr + cs, 2 * cs, stack, diff); R r[] = { {addr, 5 * cs} }; check(vmt, rmr, r); } // Cleanup - rtree->uncommit_region(addr, 5 * cs); + rtree->uncommit_region(addr, 5 * cs, diff); ASSERT_EQ(vmt.committed_size(&rmr), 0u); // Commit adjacent and overlapping regions with different stacks { // Commit two non-adjacent regions - rtree->commit_region(addr, 2 * cs, stack); - rtree->commit_region(addr + 3 * cs, 2 * cs, stack); + rtree->commit_region(addr, 2 * cs, stack, diff); + rtree->commit_region(addr + 3 * cs, 2 * cs, stack, diff); R r[] = { {addr, 2 * cs}, {addr + 3 * cs, 2 * cs} }; check(vmt, rmr, r); } { // Commit adjacent and overlapping - rtree->commit_region(addr + 2 * cs, 2 * cs, stack2); + rtree->commit_region(addr + 2 * cs, 2 * cs, stack2, diff); R r[] = { {addr, 2 * cs}, {addr + 2 * cs, 2 * cs}, {addr + 4 * cs, cs} }; @@ -237,12 +239,12 @@ public: } // revert to two non-adjacent regions - rtree->commit_region(addr, 5 * cs, stack); - rtree->uncommit_region(addr + 2 * cs, cs); + rtree->commit_region(addr, 5 * cs, stack, diff); + rtree->uncommit_region(addr + 2 * cs, cs, diff); ASSERT_EQ(vmt.committed_size(&rmr), 4 * cs); { // Commit overlapping and adjacent - rtree->commit_region(addr + cs, 2 * cs, stack2); + rtree->commit_region(addr + cs, 2 * cs, stack2, diff); R r[] = { {addr, cs}, {addr + cs, 2 * cs}, {addr + 3 * cs, 2 * cs} }; @@ -254,6 +256,7 @@ public: static void test_add_committed_region_overlapping() { VirtualMemoryTracker vmt(true); + VMATree::SummaryDiff diff; RegionsTree* rtree = vmt.tree(); size_t size = 0x01000000; const address addr = (address)0x0000A000; @@ -279,77 +282,77 @@ public: // With same stack { // Commit one region - rtree->commit_region(addr, cs, stack); + rtree->commit_region(addr, cs, stack, diff); R r[] = { {addr, cs} }; check(vmt, rmr, r); } { // Commit the same region - rtree->commit_region(addr, cs, stack); + rtree->commit_region(addr, cs, stack, diff); R r[] = { {addr, cs} }; check(vmt, rmr, r); } { // Commit a succeeding region - rtree->commit_region(addr + cs, cs, stack); + rtree->commit_region(addr + cs, cs, stack, diff); R r[] = { {addr, 2 * cs} }; check(vmt, rmr, r); } { // Commit over two regions - rtree->commit_region(addr, 2 * cs, stack); + rtree->commit_region(addr, 2 * cs, stack, diff); R r[] = { {addr, 2 * cs} }; check(vmt, rmr, r); } {// Commit first part of a region - rtree->commit_region(addr, cs, stack); + rtree->commit_region(addr, cs, stack, diff); R r[] = { {addr, 2 * cs} }; check(vmt, rmr, r); } { // Commit second part of a region - rtree->commit_region(addr + cs, cs, stack); + rtree->commit_region(addr + cs, cs, stack, diff); R r[] = { {addr, 2 * cs} }; check(vmt, rmr, r); } { // Commit a third part - rtree->commit_region(addr + 2 * cs, cs, stack); + rtree->commit_region(addr + 2 * cs, cs, stack, diff); R r[] = { {addr, 3 * cs} }; check(vmt, rmr, r); } { // Commit in the middle of a region - rtree->commit_region(addr + 1 * cs, cs, stack); + rtree->commit_region(addr + 1 * cs, cs, stack, diff); R r[] = { {addr, 3 * cs} }; check(vmt, rmr, r); } // Cleanup - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); ASSERT_EQ(vmt.committed_size(&rmr), 0u); // With preceding region - rtree->commit_region(addr, cs, stack); - rtree->commit_region(addr + 2 * cs, 3 * cs, stack); + rtree->commit_region(addr, cs, stack, diff); + rtree->commit_region(addr + 2 * cs, 3 * cs, stack, diff); - rtree->commit_region(addr + 2 * cs, cs, stack); + rtree->commit_region(addr + 2 * cs, cs, stack, diff); { R r[] = { {addr, cs}, {addr + 2 * cs, 3 * cs} }; check(vmt, rmr, r); } - rtree->commit_region(addr + 3 * cs, cs, stack); + rtree->commit_region(addr + 3 * cs, cs, stack, diff); { R r[] = { {addr, cs}, {addr + 2 * cs, 3 * cs} }; check(vmt, rmr, r); } - rtree->commit_region(addr + 4 * cs, cs, stack); + rtree->commit_region(addr + 4 * cs, cs, stack, diff); { R r[] = { {addr, cs}, {addr + 2 * cs, 3 * cs} }; @@ -357,57 +360,57 @@ public: } // Cleanup - rtree->uncommit_region(addr, 5 * cs); + rtree->uncommit_region(addr, 5 * cs, diff); ASSERT_EQ(vmt.committed_size(&rmr), 0u); // With different stacks { // Commit one region - rtree->commit_region(addr, cs, stack); + rtree->commit_region(addr, cs, stack, diff); R r[] = { {addr, cs} }; check(vmt, rmr, r); } { // Commit the same region - rtree->commit_region(addr, cs, stack2); + rtree->commit_region(addr, cs, stack2, diff); R r[] = { {addr, cs} }; check(vmt, rmr, r); } { // Commit a succeeding region - rtree->commit_region(addr + cs, cs, stack); + rtree->commit_region(addr + cs, cs, stack, diff); R r[] = { {addr, cs}, {addr + cs, cs} }; check(vmt, rmr, r); } { // Commit over two regions - rtree->commit_region(addr, 2 * cs, stack); + rtree->commit_region(addr, 2 * cs, stack, diff); R r[] = { {addr, 2 * cs} }; check(vmt, rmr, r); } {// Commit first part of a region - rtree->commit_region(addr, cs, stack2); + rtree->commit_region(addr, cs, stack2, diff); R r[] = { {addr, cs}, {addr + cs, cs} }; check(vmt, rmr, r); } { // Commit second part of a region - rtree->commit_region(addr + cs, cs, stack2); + rtree->commit_region(addr + cs, cs, stack2, diff); R r[] = { {addr, 2 * cs} }; check(vmt, rmr, r); } { // Commit a third part - rtree->commit_region(addr + 2 * cs, cs, stack2); + rtree->commit_region(addr + 2 * cs, cs, stack2, diff); R r[] = { {addr, 3 * cs} }; check(vmt, rmr, r); } { // Commit in the middle of a region - rtree->commit_region(addr + 1 * cs, cs, stack); + rtree->commit_region(addr + 1 * cs, cs, stack, diff); R r[] = { {addr, cs}, {addr + cs, cs}, {addr + 2 * cs, cs} }; @@ -430,6 +433,7 @@ public: static void test_remove_uncommitted_region() { VirtualMemoryTracker vmt(true); + VMATree::SummaryDiff diff; RegionsTree* rtree = vmt.tree(); size_t size = 0x01000000; const address addr = (address)0x0000A000; @@ -451,105 +455,105 @@ public: const size_t cs = 0x1000; { // Commit regions - rtree->commit_region(addr, 3 * cs, stack); + rtree->commit_region(addr, 3 * cs, stack, diff); R r[] = { {addr, 3 * cs} }; check(vmt, rmr, r); // Remove only existing - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); check_empty(vmt, rmr); } { - rtree->commit_region(addr + 0 * cs, cs, stack); - rtree->commit_region(addr + 2 * cs, cs, stack); - rtree->commit_region(addr + 4 * cs, cs, stack); + rtree->commit_region(addr + 0 * cs, cs, stack, diff); + rtree->commit_region(addr + 2 * cs, cs, stack, diff); + rtree->commit_region(addr + 4 * cs, cs, stack, diff); { // Remove first - rtree->uncommit_region(addr, cs); + rtree->uncommit_region(addr, cs, diff); R r[] = { {addr + 2 * cs, cs}, {addr + 4 * cs, cs} }; check(vmt, rmr, r); } // add back - rtree->commit_region(addr, cs, stack); + rtree->commit_region(addr, cs, stack, diff); { // Remove middle - rtree->uncommit_region(addr + 2 * cs, cs); + rtree->uncommit_region(addr + 2 * cs, cs, diff); R r[] = { {addr + 0 * cs, cs}, {addr + 4 * cs, cs} }; check(vmt, rmr, r); } // add back - rtree->commit_region(addr + 2 * cs, cs, stack); + rtree->commit_region(addr + 2 * cs, cs, stack, diff); { // Remove end - rtree->uncommit_region(addr + 4 * cs, cs); + rtree->uncommit_region(addr + 4 * cs, cs, diff); R r[] = { {addr + 0 * cs, cs}, {addr + 2 * cs, cs} }; check(vmt, rmr, r); } - rtree->uncommit_region(addr, 5 * cs); + rtree->uncommit_region(addr, 5 * cs, diff); check_empty(vmt, rmr); } { // Remove larger region - rtree->commit_region(addr + 1 * cs, cs, stack); - rtree->uncommit_region(addr, 3 * cs); + rtree->commit_region(addr + 1 * cs, cs, stack, diff); + rtree->uncommit_region(addr, 3 * cs, diff); check_empty(vmt, rmr); } { // Remove smaller region - in the middle - rtree->commit_region(addr, 3 * cs, stack); - rtree->uncommit_region(addr + 1 * cs, cs); + rtree->commit_region(addr, 3 * cs, stack, diff); + rtree->uncommit_region(addr + 1 * cs, cs, diff); R r[] = { { addr + 0 * cs, cs}, { addr + 2 * cs, cs} }; check(vmt, rmr, r); - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); check_empty(vmt, rmr); } { // Remove smaller region - at the beginning - rtree->commit_region(addr, 3 * cs, stack); - rtree->uncommit_region(addr + 0 * cs, cs); + rtree->commit_region(addr, 3 * cs, stack, diff); + rtree->uncommit_region(addr + 0 * cs, cs, diff); R r[] = { { addr + 1 * cs, 2 * cs} }; check(vmt, rmr, r); - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); check_empty(vmt, rmr); } { // Remove smaller region - at the end - rtree->commit_region(addr, 3 * cs, stack); - rtree->uncommit_region(addr + 2 * cs, cs); + rtree->commit_region(addr, 3 * cs, stack, diff); + rtree->uncommit_region(addr + 2 * cs, cs, diff); R r[] = { { addr, 2 * cs} }; check(vmt, rmr, r); - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); check_empty(vmt, rmr); } { // Remove smaller, overlapping region - at the beginning - rtree->commit_region(addr + 1 * cs, 4 * cs, stack); - rtree->uncommit_region(addr, 2 * cs); + rtree->commit_region(addr + 1 * cs, 4 * cs, stack, diff); + rtree->uncommit_region(addr, 2 * cs, diff); R r[] = { { addr + 2 * cs, 3 * cs} }; check(vmt, rmr, r); - rtree->uncommit_region(addr + 1 * cs, 4 * cs); + rtree->uncommit_region(addr + 1 * cs, 4 * cs, diff); check_empty(vmt, rmr); } { // Remove smaller, overlapping region - at the end - rtree->commit_region(addr, 3 * cs, stack); - rtree->uncommit_region(addr + 2 * cs, 2 * cs); + rtree->commit_region(addr, 3 * cs, stack, diff); + rtree->uncommit_region(addr + 2 * cs, 2 * cs, diff); R r[] = { { addr, 2 * cs} }; check(vmt, rmr, r); - rtree->uncommit_region(addr, 3 * cs); + rtree->uncommit_region(addr, 3 * cs, diff); check_empty(vmt, rmr); } From b19e872192106f47c5d9b425230cc2bfe3e4786c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Gr=C3=B6nlund?= Date: Tue, 30 Sep 2025 10:35:23 +0000 Subject: [PATCH 293/556] 8362573: Incorrect weight of the first ObjectAllocationSample JFR event (regression) Reviewed-by: egahlin --- .../checkpoint/jfrCheckpointManager.cpp | 30 +++++-- .../checkpoint/jfrCheckpointManager.hpp | 2 +- .../recorder/service/jfrRecorderService.cpp | 27 +----- .../share/jfr/support/jfrAllocationTracer.cpp | 5 +- .../jfr/support/jfrObjectAllocationSample.cpp | 57 +++++++------ .../jfr/support/jfrObjectAllocationSample.hpp | 4 +- ...ectAllocationSampleEventInitialWeight.java | 83 +++++++++++++++++++ 7 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 test/jdk/jdk/jfr/event/allocation/TestObjectAllocationSampleEventInitialWeight.java diff --git a/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.cpp b/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.cpp index b8ff3ba504a..ff688a297ed 100644 --- a/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.cpp +++ b/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.cpp @@ -676,18 +676,36 @@ void JfrCheckpointManager::write_simplified_vthread_checkpoint(traceid vtid) { JfrTypeManager::write_simplified_vthread_checkpoint(vtid); } -class JfrNotifyClosure : public ThreadClosure { +// Reset thread local state used for object allocation sampling. +static void clear_last_allocated_bytes(JavaThread* jt) { + assert(jt != nullptr, "invariant"); + assert(!JfrRecorder::is_recording(), "invariant"); + JfrThreadLocal* const tl = jt->jfr_thread_local(); + assert(tl != nullptr, "invariant"); + if (tl->last_allocated_bytes() != 0) { + tl->clear_last_allocated_bytes(); + } + assert(tl->last_allocated_bytes() == 0, "invariant"); +} + +class JfrNotifyClosure : public StackObj { + private: + bool _clear; public: - void do_thread(Thread* thread) { - assert(thread != nullptr, "invariant"); + JfrNotifyClosure(bool clear) : _clear(clear) {} + void do_thread(JavaThread* jt) { + assert(jt != nullptr, "invariant"); assert_locked_or_safepoint(Threads_lock); - JfrJavaEventWriter::notify(JavaThread::cast(thread)); + JfrJavaEventWriter::notify(jt); + if (_clear) { + clear_last_allocated_bytes(jt); + } } }; -void JfrCheckpointManager::notify_threads() { +void JfrCheckpointManager::notify_threads(bool clear /* false */) { assert(SafepointSynchronize::is_at_safepoint(), "invariant"); - JfrNotifyClosure tc; + JfrNotifyClosure tc(clear); JfrJavaThreadIterator iter; while (iter.has_next()) { tc.do_thread(iter.next()); diff --git a/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.hpp b/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.hpp index f9f8f1c26cf..f713a77e66d 100644 --- a/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.hpp +++ b/src/hotspot/share/jfr/recorder/checkpoint/jfrCheckpointManager.hpp @@ -88,7 +88,7 @@ class JfrCheckpointManager : public JfrCHeapObj { size_t clear(); size_t write(); - void notify_threads(); + void notify_threads(bool clear = false); size_t write_static_type_set(Thread* thread); size_t write_threads(JavaThread* thread); diff --git a/src/hotspot/share/jfr/recorder/service/jfrRecorderService.cpp b/src/hotspot/share/jfr/recorder/service/jfrRecorderService.cpp index 0087980f430..0e68e8a6032 100644 --- a/src/hotspot/share/jfr/recorder/service/jfrRecorderService.cpp +++ b/src/hotspot/share/jfr/recorder/service/jfrRecorderService.cpp @@ -122,30 +122,6 @@ const Thread* JfrRotationLock::_owner_thread = nullptr; const int JfrRotationLock::retry_wait_millis = 10; volatile int JfrRotationLock::_lock = 0; -// Reset thread local state used for object allocation sampling. -class ClearObjectAllocationSampling : public ThreadClosure { - public: - void do_thread(Thread* t) { - assert(t != nullptr, "invariant"); - t->jfr_thread_local()->clear_last_allocated_bytes(); - } -}; - -template -static inline void iterate(Iterator& it, ClearObjectAllocationSampling& coas) { - while (it.has_next()) { - coas.do_thread(it.next()); - } -} - -static void clear_object_allocation_sampling() { - ClearObjectAllocationSampling coas; - JfrJavaThreadIterator jit; - iterate(jit, coas); - JfrNonJavaThreadIterator njit; - iterate(njit, coas); -} - template class Content { private: @@ -472,7 +448,6 @@ void JfrRecorderService::clear() { } void JfrRecorderService::pre_safepoint_clear() { - clear_object_allocation_sampling(); _storage.clear(); JfrStackTraceRepository::clear(); } @@ -486,7 +461,7 @@ void JfrRecorderService::invoke_safepoint_clear() { void JfrRecorderService::safepoint_clear() { assert(SafepointSynchronize::is_at_safepoint(), "invariant"); _storage.clear(); - _checkpoint_manager.notify_threads(); + _checkpoint_manager.notify_threads(true); _chunkwriter.set_time_stamp(); JfrDeprecationManager::on_safepoint_clear(); JfrStackTraceRepository::clear(); diff --git a/src/hotspot/share/jfr/support/jfrAllocationTracer.cpp b/src/hotspot/share/jfr/support/jfrAllocationTracer.cpp index a1245d7bafe..5817ad194c2 100644 --- a/src/hotspot/share/jfr/support/jfrAllocationTracer.cpp +++ b/src/hotspot/share/jfr/support/jfrAllocationTracer.cpp @@ -22,6 +22,7 @@ * */ +#include "jfr/jfrEvents.hpp" #include "jfr/leakprofiler/leakProfiler.hpp" #include "jfr/support/jfrAllocationTracer.hpp" #include "jfr/support/jfrObjectAllocationSample.hpp" @@ -31,5 +32,7 @@ JfrAllocationTracer::JfrAllocationTracer(const Klass* klass, HeapWord* obj, size if (LeakProfiler::is_running()) { LeakProfiler::sample(obj, alloc_size, thread); } - JfrObjectAllocationSample::send_event(klass, alloc_size, outside_tlab, thread); + if (EventObjectAllocationSample::is_enabled()) { + JfrObjectAllocationSample::send_event(klass, alloc_size, outside_tlab, thread); + } } diff --git a/src/hotspot/share/jfr/support/jfrObjectAllocationSample.cpp b/src/hotspot/share/jfr/support/jfrObjectAllocationSample.cpp index 6a99271a80e..c337030bddd 100644 --- a/src/hotspot/share/jfr/support/jfrObjectAllocationSample.cpp +++ b/src/hotspot/share/jfr/support/jfrObjectAllocationSample.cpp @@ -26,7 +26,7 @@ #include "gc/shared/tlab_globals.hpp" #include "jfr/jfrEvents.hpp" #include "jfr/support/jfrObjectAllocationSample.hpp" -#include "jfr/support/jfrThreadLocal.hpp" +#include "runtime/javaThread.hpp" #include "utilities/globalDefinitions.hpp" inline bool send_allocation_sample(const Klass* klass, int64_t allocated_bytes, JfrThreadLocal* tl) { @@ -43,35 +43,42 @@ inline bool send_allocation_sample(const Klass* klass, int64_t allocated_bytes, return false; } -inline int64_t estimate_tlab_size_bytes(Thread* thread) { - const size_t desired_tlab_size_bytes = thread->tlab().desired_size() * HeapWordSize; - const size_t alignment_reserve_bytes = thread->tlab().alignment_reserve_in_bytes(); +inline int64_t estimate_tlab_size_bytes(JavaThread* jt) { + const size_t desired_tlab_size_bytes = jt->tlab().desired_size() * HeapWordSize; + const size_t alignment_reserve_bytes = jt->tlab().alignment_reserve_in_bytes(); assert(desired_tlab_size_bytes >= alignment_reserve_bytes, "invariant"); return static_cast(desired_tlab_size_bytes - alignment_reserve_bytes); } -inline int64_t load_allocated_bytes(JfrThreadLocal* tl, Thread* thread) { - const int64_t allocated_bytes = thread->allocated_bytes(); - return allocated_bytes == tl->last_allocated_bytes() ? 0 : allocated_bytes; +inline int64_t load_allocated_bytes(JfrThreadLocal* tl, JavaThread* jt) { + const int64_t allocated_bytes = jt->allocated_bytes(); + const int64_t last_allocated_bytes = tl->last_allocated_bytes(); + assert(allocated_bytes >= last_allocated_bytes, "invariant"); + if (last_allocated_bytes == 0) { + // Initialization. + tl->set_last_allocated_bytes(allocated_bytes); + return 0; + } + return allocated_bytes == last_allocated_bytes ? 0 : allocated_bytes; } // To avoid large objects from being undersampled compared to the regular TLAB samples, // the data amount is normalized as if it was a TLAB, giving a number of TLAB sampling attempts to the large object. -static void normalize_as_tlab_and_send_allocation_samples(const Klass* klass, int64_t obj_alloc_size_bytes, JfrThreadLocal* tl, Thread* thread) { - const int64_t allocated_bytes = load_allocated_bytes(tl, thread); +static void normalize_as_tlab_and_send_allocation_samples(const Klass* klass, + int64_t obj_alloc_size_bytes, + int64_t allocated_bytes, + JfrThreadLocal* tl, + JavaThread* jt) { assert(allocated_bytes > 0, "invariant"); // obj_alloc_size_bytes is already attributed to allocated_bytes at this point. if (!UseTLAB) { send_allocation_sample(klass, allocated_bytes, tl); return; } - const int64_t tlab_size_bytes = estimate_tlab_size_bytes(thread); - if (tlab_size_bytes <= 0) { + const int64_t tlab_size_bytes = estimate_tlab_size_bytes(jt); + if (tlab_size_bytes <= 0 || allocated_bytes - tl->last_allocated_bytes() < tlab_size_bytes) { // We don't get a TLAB, avoid endless loop below. return; } - if (allocated_bytes - tl->last_allocated_bytes() < tlab_size_bytes) { - return; - } assert(obj_alloc_size_bytes > 0, "invariant"); do { if (send_allocation_sample(klass, allocated_bytes, tl)) { @@ -81,17 +88,17 @@ static void normalize_as_tlab_and_send_allocation_samples(const Klass* klass, in } while (obj_alloc_size_bytes > 0); } -void JfrObjectAllocationSample::send_event(const Klass* klass, size_t alloc_size, bool outside_tlab, Thread* thread) { - assert(thread != nullptr, "invariant"); - JfrThreadLocal* const tl = thread->jfr_thread_local(); +void JfrObjectAllocationSample::send_event(const Klass* klass, size_t alloc_size, bool outside_tlab, JavaThread* jt) { + assert(klass != nullptr, "invariant"); + assert(jt != nullptr, "invariant"); + JfrThreadLocal* const tl = jt->jfr_thread_local(); assert(tl != nullptr, "invariant"); - if (outside_tlab) { - normalize_as_tlab_and_send_allocation_samples(klass, static_cast(alloc_size), tl, thread); - return; + const int64_t allocated_bytes = load_allocated_bytes(tl, jt); + if (allocated_bytes > 0) { + if (outside_tlab) { + normalize_as_tlab_and_send_allocation_samples(klass, static_cast(alloc_size), allocated_bytes, tl, jt); + return; + } + send_allocation_sample(klass, allocated_bytes, tl); } - const int64_t allocated_bytes = load_allocated_bytes(tl, thread); - if (allocated_bytes == 0) { - return; - } - send_allocation_sample(klass, allocated_bytes, tl); } diff --git a/src/hotspot/share/jfr/support/jfrObjectAllocationSample.hpp b/src/hotspot/share/jfr/support/jfrObjectAllocationSample.hpp index f58caf3c2a6..cb63d7e1e29 100644 --- a/src/hotspot/share/jfr/support/jfrObjectAllocationSample.hpp +++ b/src/hotspot/share/jfr/support/jfrObjectAllocationSample.hpp @@ -27,12 +27,12 @@ #include "memory/allStatic.hpp" +class JavaThread; class Klass; -class Thread; class JfrObjectAllocationSample : AllStatic { friend class JfrAllocationTracer; - static void send_event(const Klass* klass, size_t alloc_size, bool outside_tlab, Thread* thread); + static void send_event(const Klass* klass, size_t alloc_size, bool outside_tlab, JavaThread* jt); }; #endif // SHARE_JFR_SUPPORT_JFROBJECTALLOCATIONSAMPLE_HPP diff --git a/test/jdk/jdk/jfr/event/allocation/TestObjectAllocationSampleEventInitialWeight.java b/test/jdk/jdk/jfr/event/allocation/TestObjectAllocationSampleEventInitialWeight.java new file mode 100644 index 00000000000..598efd66a32 --- /dev/null +++ b/test/jdk/jdk/jfr/event/allocation/TestObjectAllocationSampleEventInitialWeight.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jfr.event.allocation; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.CountDownLatch; +import java.util.List; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingStream; +import jdk.test.lib.jfr.EventNames; +import jdk.test.lib.jfr.Events; + +/** + * @test + * @summary Tests that the VM maintains proper initialization state for ObjectAllocationSampleEvent. + * @requires vm.hasJFR + * @library /test/lib + * @run main/othervm -XX:+UseTLAB -XX:TLABSize=2k -XX:-ResizeTLAB jdk.jfr.event.allocation.TestObjectAllocationSampleEventInitialWeight + */ +public class TestObjectAllocationSampleEventInitialWeight { + private static final String EVENT_NAME = EventNames.ObjectAllocationSample; + private static final int OBJECT_SIZE = 4 * 1024; + private static final int OBJECTS_TO_ALLOCATE = 16; + private static final int OBJECTS_TO_ALLOCATE_BEFORE_RECORDING = 1024; + private static final long BEFORE_RECORDING_SAMPLE_WEIGHT = OBJECT_SIZE * OBJECTS_TO_ALLOCATE_BEFORE_RECORDING; + + // Make sure allocation isn't dead code eliminated. + public static byte[] tmp; + + public static void main(String... args) throws Exception { + test(); + // Test again to ensure reset logic works correctly for subsequent physical recordings. + test(); + } + + private static void test() throws Exception { + long currentThreadId = Thread.currentThread().threadId(); + allocate(OBJECTS_TO_ALLOCATE_BEFORE_RECORDING); + try (Recording r = new Recording()) { + r.enable(EVENT_NAME); + r.start(); + allocate(OBJECTS_TO_ALLOCATE); + r.stop(); + List events = Events.fromRecording(r); + Events.hasEvents(events); + for (RecordedEvent event : events) { + if (currentThreadId == event.getThread().getJavaThreadId()) { + if (event.getLong("weight") >= BEFORE_RECORDING_SAMPLE_WEIGHT) { + throw new RuntimeException("Sample weight is not below " + BEFORE_RECORDING_SAMPLE_WEIGHT); + } + } + } + } + } + + private static void allocate(int number) throws Exception { + for (int i = 0; i < number; ++i) { + tmp = new byte[OBJECT_SIZE]; + } + } +} From ba0a6eed1a6a22bd4c1d159592b62e054afa401a Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Tue, 30 Sep 2025 10:41:13 +0000 Subject: [PATCH 294/556] 8368357: Some source files have initial blank lines Reviewed-by: stefank, ayang, serb, jwaters, jpai --- src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp | 1 - src/hotspot/share/runtime/threads.cpp | 1 - src/hotspot/share/runtime/vmStructs.cpp | 1 - src/hotspot/share/services/threadIdTable.cpp | 1 - src/hotspot/share/services/threadIdTable.hpp | 1 - .../share/classes/sun/util/locale/UnicodeLocaleExtension.java | 1 - src/java.desktop/share/classes/java/awt/image/LookupOp.java | 1 - src/jdk.compiler/share/classes/com/sun/source/tree/UsesTree.java | 1 - 8 files changed, 8 deletions(-) diff --git a/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp b/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp index 658d3d31e50..2d0953288b0 100644 --- a/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp +++ b/src/hotspot/share/cds/dumpTimeClassInfo.inline.hpp @@ -1,4 +1,3 @@ - /* * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/hotspot/share/runtime/threads.cpp b/src/hotspot/share/runtime/threads.cpp index 0172fe4d69b..d098343c354 100644 --- a/src/hotspot/share/runtime/threads.cpp +++ b/src/hotspot/share/runtime/threads.cpp @@ -1,4 +1,3 @@ - /* * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2021, Azul Systems, Inc. All rights reserved. diff --git a/src/hotspot/share/runtime/vmStructs.cpp b/src/hotspot/share/runtime/vmStructs.cpp index ac095be6936..10069e849bc 100644 --- a/src/hotspot/share/runtime/vmStructs.cpp +++ b/src/hotspot/share/runtime/vmStructs.cpp @@ -1,4 +1,3 @@ - /* * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/hotspot/share/services/threadIdTable.cpp b/src/hotspot/share/services/threadIdTable.cpp index 24ea28abaf6..1604927a0ac 100644 --- a/src/hotspot/share/services/threadIdTable.cpp +++ b/src/hotspot/share/services/threadIdTable.cpp @@ -1,4 +1,3 @@ - /* * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/hotspot/share/services/threadIdTable.hpp b/src/hotspot/share/services/threadIdTable.hpp index 12772aed88c..b295cec2514 100644 --- a/src/hotspot/share/services/threadIdTable.hpp +++ b/src/hotspot/share/services/threadIdTable.hpp @@ -1,4 +1,3 @@ - /* * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java b/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java index 634932e9e19..7f66febf65f 100644 --- a/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java +++ b/src/java.base/share/classes/sun/util/locale/UnicodeLocaleExtension.java @@ -1,4 +1,3 @@ - /* * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/java.desktop/share/classes/java/awt/image/LookupOp.java b/src/java.desktop/share/classes/java/awt/image/LookupOp.java index 5d11f78e76d..c8d8a703a3f 100644 --- a/src/java.desktop/share/classes/java/awt/image/LookupOp.java +++ b/src/java.desktop/share/classes/java/awt/image/LookupOp.java @@ -1,4 +1,3 @@ - /* * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. diff --git a/src/jdk.compiler/share/classes/com/sun/source/tree/UsesTree.java b/src/jdk.compiler/share/classes/com/sun/source/tree/UsesTree.java index 9b52eb29e1b..d14257de626 100644 --- a/src/jdk.compiler/share/classes/com/sun/source/tree/UsesTree.java +++ b/src/jdk.compiler/share/classes/com/sun/source/tree/UsesTree.java @@ -1,4 +1,3 @@ - /* * Copyright (c) 2009, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. From 64c46d8efc27911b8667c3974275c075cf79a311 Mon Sep 17 00:00:00 2001 From: Yasumasa Suenaga Date: Tue, 30 Sep 2025 11:32:44 +0000 Subject: [PATCH 295/556] 8367953: JFR sampler threads does not appear in thread dump Reviewed-by: mgronlun --- .../jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp | 7 +++++++ .../share/jfr/periodic/sampling/jfrThreadSampler.cpp | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp index 3fca3ad7631..2ce1a93455b 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp @@ -275,6 +275,7 @@ public: void handle_timer_signal(siginfo_t* info, void* context); bool init_timers(); void stop_timer(); + virtual void print_on(outputStream* st) const; void trigger_async_processing_of_cpu_time_jfr_requests(); @@ -788,6 +789,12 @@ void JfrCPUSamplerThread::stop_timer() { VMThread::execute(&op); } +void JfrCPUSamplerThread::print_on(outputStream* st) const { + st->print("\"%s\" ", name()); + Thread::print_on(st); + st->cr(); +} + void JfrCPUSamplerThread::recompute_period_if_needed() { int64_t current_period = get_sampling_period(); int64_t period = _throttle.compute_sampling_period(); diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampler.cpp b/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampler.cpp index ae79c8bb6e3..7e7747ba396 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampler.cpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampler.cpp @@ -85,6 +85,7 @@ class JfrSamplerThread : public NonJavaThread { bool is_JfrSampler_thread() const { return true; } int64_t java_period() const { return AtomicAccess::load(&_java_period_millis); }; int64_t native_period() const { return AtomicAccess::load(&_native_period_millis); }; + virtual void print_on(outputStream* st) const; }; JfrSamplerThread::JfrSamplerThread(int64_t java_period_millis, int64_t native_period_millis, u4 max_frames) : @@ -384,6 +385,12 @@ void JfrSamplerThread::set_native_period(int64_t period_millis) { AtomicAccess::store(&_native_period_millis, period_millis); } +void JfrSamplerThread::print_on(outputStream* st) const { + st->print("\"%s\" ", name()); + Thread::print_on(st); + st->cr(); +} + // JfrThreadSampler; static JfrThreadSampler* _instance = nullptr; From 8606d3f8405b73878a1319ba3574ef69349aa2a1 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Tue, 30 Sep 2025 11:54:37 +0000 Subject: [PATCH 296/556] 8365060: Historical data for JDK 8 should include the jdk.net package Reviewed-by: shade, liach --- .../share/data/symbols/include.list | 1 + .../share/data/symbols/java.base-8.sym.txt | 6 +- .../share/data/symbols/java.base-9.sym.txt | 22 +++--- .../share/data/symbols/java.desktop-8.sym.txt | 6 +- .../share/data/symbols/java.desktop-9.sym.txt | 53 +++++++------ .../data/symbols/java.xml.bind-9.sym.txt | 4 +- .../data/symbols/jdk.management-8.sym.txt | 6 +- .../data/symbols/jdk.management-9.sym.txt | 6 +- .../share/data/symbols/jdk.net-8.sym.txt | 76 +++++++++++++++++++ .../share/data/symbols/jdk.net-9.sym.txt | 33 +------- src/jdk.compiler/share/data/symbols/symbols | 2 +- .../tools/javac/platform/CompilationTest.java | 75 ++++++++++++++++++ .../tools/javac/sym/ElementStructureTest.java | 12 +-- 13 files changed, 219 insertions(+), 83 deletions(-) create mode 100644 src/jdk.compiler/share/data/symbols/jdk.net-8.sym.txt create mode 100644 test/langtools/tools/javac/platform/CompilationTest.java diff --git a/src/jdk.compiler/share/data/symbols/include.list b/src/jdk.compiler/share/data/symbols/include.list index b99422a60e3..b2502f18fa4 100644 --- a/src/jdk.compiler/share/data/symbols/include.list +++ b/src/jdk.compiler/share/data/symbols/include.list @@ -267,6 +267,7 @@ +com/sun/management/ +com/sun/nio/sctp/ +jdk/ ++jdk/net/ # #Exported(true) in 8u40: # diff --git a/src/jdk.compiler/share/data/symbols/java.base-8.sym.txt b/src/jdk.compiler/share/data/symbols/java.base-8.sym.txt index 051003c2799..3f0b74098ef 100644 --- a/src/jdk.compiler/share/data/symbols/java.base-8.sym.txt +++ b/src/jdk.compiler/share/data/symbols/java.base-8.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -1711,6 +1711,7 @@ field name SHARADA descriptor Ljava/lang/Character$UnicodeBlock; flags 19 field name TAKRI descriptor Ljava/lang/Character$UnicodeBlock; flags 19 field name MIAO descriptor Ljava/lang/Character$UnicodeBlock; flags 19 field name ARABIC_MATHEMATICAL_ALPHABETIC_SYMBOLS descriptor Ljava/lang/Character$UnicodeBlock; flags 19 +field name CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E descriptor Ljava/lang/Character$UnicodeBlock; flags 19 method name of descriptor (C)Ljava/lang/Character$UnicodeBlock; flags 9 method name of descriptor (I)Ljava/lang/Character$UnicodeBlock; flags 9 method name forName descriptor (Ljava/lang/String;)Ljava/lang/Character$UnicodeBlock; flags 19 @@ -2078,7 +2079,7 @@ field name POSITIVE_INFINITY descriptor F constantValue Infinity flags 19 field name NEGATIVE_INFINITY descriptor F constantValue -Infinity flags 19 field name NaN descriptor F constantValue NaN flags 19 field name MAX_VALUE descriptor F constantValue 3.4028235E38 flags 19 -field name MIN_NORMAL descriptor F constantValue 1.17549435E-38 flags 19 +field name MIN_NORMAL descriptor F constantValue 1.1754944E-38 flags 19 field name MIN_VALUE descriptor F constantValue 1.4E-45 flags 19 field name MAX_EXPONENT descriptor I constantValue 127 flags 19 field name MIN_EXPONENT descriptor I constantValue -126 flags 19 @@ -3574,6 +3575,7 @@ method name get descriptor ()Ljava/lang/Object; flags 1 signature ()TT; method name clear descriptor ()V flags 1 method name isEnqueued descriptor ()Z flags 1 method name enqueue descriptor ()Z flags 1 +method name clone descriptor ()Ljava/lang/Object; thrownTypes java/lang/CloneNotSupportedException flags 4 class name java/lang/ref/ReferenceQueue header extends java/lang/Object flags 21 signature Ljava/lang/Object; classAnnotations @Ljdk/Profile+Annotation;(value=I1) diff --git a/src/jdk.compiler/share/data/symbols/java.base-9.sym.txt b/src/jdk.compiler/share/data/symbols/java.base-9.sym.txt index 99a4f8e9f0e..7ea597bda46 100644 --- a/src/jdk.compiler/share/data/symbols/java.base-9.sym.txt +++ b/src/jdk.compiler/share/data/symbols/java.base-9.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -165,10 +165,6 @@ header extends java/lang/Object implements java/io/Serializable,java/lang/Compar innerclass innerClass java/lang/Character$UnicodeScript outerClass java/lang/Character innerClassName UnicodeScript flags 4019 innerclass innerClass java/lang/Character$UnicodeBlock outerClass java/lang/Character innerClassName UnicodeBlock flags 19 innerclass innerClass java/lang/Character$Subset outerClass java/lang/Character innerClassName Subset flags 9 -field name DIRECTIONALITY_LEFT_TO_RIGHT_ISOLATE descriptor B constantValue 19 flags 19 -field name DIRECTIONALITY_RIGHT_TO_LEFT_ISOLATE descriptor B constantValue 20 flags 19 -field name DIRECTIONALITY_FIRST_STRONG_ISOLATE descriptor B constantValue 21 flags 19 -field name DIRECTIONALITY_POP_DIRECTIONAL_ISOLATE descriptor B constantValue 22 flags 19 -method name descriptor (C)V -method name valueOf descriptor (C)Ljava/lang/Character; -method name charValue descriptor ()C @@ -176,6 +172,10 @@ field name DIRECTIONALITY_POP_DIRECTIONAL_ISOLATE descriptor B constantValue 22 -method name isJavaLetterOrDigit descriptor (C)Z -method name isSpace descriptor (C)Z -method name reverseBytes descriptor (C)C +field name DIRECTIONALITY_LEFT_TO_RIGHT_ISOLATE descriptor B constantValue 19 flags 19 +field name DIRECTIONALITY_RIGHT_TO_LEFT_ISOLATE descriptor B constantValue 20 flags 19 +field name DIRECTIONALITY_FIRST_STRONG_ISOLATE descriptor B constantValue 21 flags 19 +field name DIRECTIONALITY_POP_DIRECTIONAL_ISOLATE descriptor B constantValue 22 flags 19 method name descriptor (C)V flags 1 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") method name valueOf descriptor (C)Ljava/lang/Character; flags 9 runtimeAnnotations @Ljdk/internal/HotSpotIntrinsicCandidate; method name charValue descriptor ()C flags 1 runtimeAnnotations @Ljdk/internal/HotSpotIntrinsicCandidate; @@ -229,7 +229,6 @@ field name EARLY_DYNASTIC_CUNEIFORM descriptor Ljava/lang/Character$UnicodeBlock field name ANATOLIAN_HIEROGLYPHS descriptor Ljava/lang/Character$UnicodeBlock; flags 19 field name SUTTON_SIGNWRITING descriptor Ljava/lang/Character$UnicodeBlock; flags 19 field name SUPPLEMENTAL_SYMBOLS_AND_PICTOGRAPHS descriptor Ljava/lang/Character$UnicodeBlock; flags 19 -field name CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E descriptor Ljava/lang/Character$UnicodeBlock; flags 19 class name java/lang/Character$UnicodeScript field name CAUCASIAN_ALBANIAN descriptor Ljava/lang/Character$UnicodeScript; flags 4019 @@ -711,7 +710,6 @@ innerclass innerClass java/lang/module/ModuleDescriptor$Opens outerClass java/la innerclass innerClass java/lang/module/ModuleDescriptor$Exports outerClass java/lang/module/ModuleDescriptor innerClassName Exports flags 19 innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang/invoke/MethodHandles innerClassName Lookup flags 19 -field name inCheck descriptor Z -field name inCheck descriptor Z flags 4 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(forRemoval=Ztrue,since="1.2") -method name getInCheck descriptor ()Z -method name getClassContext descriptor ()[Ljava/lang/Class; -method name currentClassLoader descriptor ()Ljava/lang/ClassLoader; @@ -725,6 +723,7 @@ field name inCheck descriptor Z flags 4 deprecated true runtimeAnnotations @Ljav -method name checkSystemClipboardAccess descriptor ()V -method name checkAwtEventQueueAccess descriptor ()V -method name checkMemberAccess descriptor (Ljava/lang/Class;I)V +field name inCheck descriptor Z flags 4 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(forRemoval=Ztrue,since="1.2") method name getInCheck descriptor ()Z flags 1 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(forRemoval=Ztrue,since="1.2") method name getClassContext descriptor ()[Ljava/lang/Class; flags 104 signature ()[Ljava/lang/Class<*>; method name currentClassLoader descriptor ()Ljava/lang/ClassLoader; flags 4 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(forRemoval=Ztrue,since="1.2") @@ -1404,6 +1403,7 @@ method name clean descriptor ()V flags 401 class name java/lang/ref/Reference -method name get descriptor ()Ljava/lang/Object; +-method name clone descriptor ()Ljava/lang/Object; method name get descriptor ()Ljava/lang/Object; flags 1 signature ()TT; runtimeAnnotations @Ljdk/internal/HotSpotIntrinsicCandidate; method name reachabilityFence descriptor (Ljava/lang/Object;)V flags 9 runtimeAnnotations @Ljdk/internal/vm/annotation/DontInline; @@ -1515,6 +1515,9 @@ class name java/math/BigDecimal -field name ROUND_HALF_DOWN descriptor I -field name ROUND_HALF_EVEN descriptor I -field name ROUND_UNNECESSARY descriptor I +-method name divide descriptor (Ljava/math/BigDecimal;II)Ljava/math/BigDecimal; +-method name divide descriptor (Ljava/math/BigDecimal;I)Ljava/math/BigDecimal; +-method name setScale descriptor (II)Ljava/math/BigDecimal; field name ROUND_UP descriptor I constantValue 0 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name ROUND_DOWN descriptor I constantValue 1 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name ROUND_CEILING descriptor I constantValue 2 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") @@ -1523,9 +1526,6 @@ field name ROUND_HALF_UP descriptor I constantValue 4 flags 19 deprecated true r field name ROUND_HALF_DOWN descriptor I constantValue 5 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name ROUND_HALF_EVEN descriptor I constantValue 6 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name ROUND_UNNECESSARY descriptor I constantValue 7 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") --method name divide descriptor (Ljava/math/BigDecimal;II)Ljava/math/BigDecimal; --method name divide descriptor (Ljava/math/BigDecimal;I)Ljava/math/BigDecimal; --method name setScale descriptor (II)Ljava/math/BigDecimal; method name divide descriptor (Ljava/math/BigDecimal;II)Ljava/math/BigDecimal; flags 1 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") method name divide descriptor (Ljava/math/BigDecimal;I)Ljava/math/BigDecimal; flags 1 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") method name sqrt descriptor (Ljava/math/MathContext;)Ljava/math/BigDecimal; flags 1 @@ -2189,8 +2189,8 @@ innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang class name java/time/LocalDate header extends java/lang/Object implements java/time/temporal/Temporal,java/time/temporal/TemporalAdjuster,java/time/chrono/ChronoLocalDate,java/io/Serializable flags 31 innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang/invoke/MethodHandles innerClassName Lookup flags 19 -field name EPOCH descriptor Ljava/time/LocalDate; flags 19 -method name getEra descriptor ()Ljava/time/chrono/Era; +field name EPOCH descriptor Ljava/time/LocalDate; flags 19 method name ofInstant descriptor (Ljava/time/Instant;Ljava/time/ZoneId;)Ljava/time/LocalDate; flags 9 method name getEra descriptor ()Ljava/time/chrono/IsoEra; flags 1 method name datesUntil descriptor (Ljava/time/LocalDate;)Ljava/util/stream/Stream; flags 1 signature (Ljava/time/LocalDate;)Ljava/util/stream/Stream; diff --git a/src/jdk.compiler/share/data/symbols/java.desktop-8.sym.txt b/src/jdk.compiler/share/data/symbols/java.desktop-8.sym.txt index 3daa15e5081..782c96da30c 100644 --- a/src/jdk.compiler/share/data/symbols/java.desktop-8.sym.txt +++ b/src/jdk.compiler/share/data/symbols/java.desktop-8.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -666,7 +666,6 @@ innerclass innerClass java/awt/Component$BaselineResizeBehavior outerClass java/ innerclass innerClass java/awt/Component$FlipBufferStrategy outerClass java/awt/Component innerClassName FlipBufferStrategy flags 4 innerclass innerClass java/awt/Component$BltBufferStrategy outerClass java/awt/Component innerClassName BltBufferStrategy flags 4 innerclass innerClass java/awt/Component$AccessibleAWTComponent outerClass java/awt/Component innerClassName AccessibleAWTComponent flags 404 -innerclass innerClass sun/awt/CausedFocusEvent$Cause outerClass sun/awt/CausedFocusEvent innerClassName Cause flags 4019 field name TOP_ALIGNMENT descriptor F constantValue 0.0 flags 19 field name CENTER_ALIGNMENT descriptor F constantValue 0.5 flags 19 field name BOTTOM_ALIGNMENT descriptor F constantValue 1.0 flags 19 @@ -2116,7 +2115,6 @@ method name postProcessKeyEvent descriptor (Ljava/awt/event/KeyEvent;)Z flags 40 class name java/awt/KeyboardFocusManager header extends java/lang/Object implements java/awt/KeyEventDispatcher,java/awt/KeyEventPostProcessor flags 421 classAnnotations @Ljdk/Profile+Annotation;(value=I4) -innerclass innerClass sun/awt/CausedFocusEvent$Cause outerClass sun/awt/CausedFocusEvent innerClassName Cause flags 4019 field name FORWARD_TRAVERSAL_KEYS descriptor I constantValue 0 flags 19 field name BACKWARD_TRAVERSAL_KEYS descriptor I constantValue 1 flags 19 field name UP_CYCLE_TRAVERSAL_KEYS descriptor I constantValue 2 flags 19 @@ -7473,7 +7471,6 @@ method name select descriptor (I)V flags 401 class name java/awt/peer/ComponentPeer header extends java/lang/Object flags 601 classAnnotations @Ljdk/Profile+Annotation;(value=I4)@Lsun/Proprietary+Annotation; -innerclass innerClass sun/awt/CausedFocusEvent$Cause outerClass sun/awt/CausedFocusEvent innerClassName Cause flags 4019 innerclass innerClass java/awt/BufferCapabilities$FlipContents outerClass java/awt/BufferCapabilities innerClassName FlipContents flags 19 field name SET_LOCATION descriptor I constantValue 1 flags 19 field name SET_SIZE descriptor I constantValue 2 flags 19 @@ -15617,6 +15614,7 @@ method name descriptor (Ljavax/swing/JSpinner;)V flags 1 method name descriptor (Ljavax/swing/JSpinner;Ljava/lang/String;)V flags 1 method name getFormat descriptor ()Ljava/text/DecimalFormat; flags 1 method name getModel descriptor ()Ljavax/swing/SpinnerNumberModel; flags 1 +method name setComponentOrientation descriptor (Ljava/awt/ComponentOrientation;)V flags 1 class name javax/swing/JSplitPane header extends javax/swing/JComponent implements javax/accessibility/Accessible flags 21 classAnnotations @Ljdk/Profile+Annotation;(value=I4) diff --git a/src/jdk.compiler/share/data/symbols/java.desktop-9.sym.txt b/src/jdk.compiler/share/data/symbols/java.desktop-9.sym.txt index cf0ab3466a8..039610548bb 100644 --- a/src/jdk.compiler/share/data/symbols/java.desktop-9.sym.txt +++ b/src/jdk.compiler/share/data/symbols/java.desktop-9.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -789,6 +789,7 @@ innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang -field name BUTTON1_MASK descriptor I -field name BUTTON2_MASK descriptor I -field name BUTTON3_MASK descriptor I +-method name getModifiers descriptor ()I field name SHIFT_MASK descriptor I constantValue 1 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name CTRL_MASK descriptor I constantValue 2 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name META_MASK descriptor I constantValue 4 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") @@ -797,7 +798,6 @@ field name ALT_GRAPH_MASK descriptor I constantValue 32 flags 19 deprecated true field name BUTTON1_MASK descriptor I constantValue 16 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name BUTTON2_MASK descriptor I constantValue 8 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") field name BUTTON3_MASK descriptor I constantValue 4 flags 19 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") --method name getModifiers descriptor ()I method name getModifiers descriptor ()I flags 1 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") class name java/awt/event/InputMethodEvent @@ -1354,10 +1354,10 @@ innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang class name java/beans/beancontext/BeanContextServiceAvailableEvent -field name serviceClass descriptor Ljava/lang/Class; -field name serviceClass descriptor Ljava/lang/Class; flags 4 signature Ljava/lang/Class<*>; -method name descriptor (Ljava/beans/beancontext/BeanContextServices;Ljava/lang/Class;)V -method name getServiceClass descriptor ()Ljava/lang/Class; -method name getCurrentServiceSelectors descriptor ()Ljava/util/Iterator; +field name serviceClass descriptor Ljava/lang/Class; flags 4 signature Ljava/lang/Class<*>; method name descriptor (Ljava/beans/beancontext/BeanContextServices;Ljava/lang/Class;)V flags 1 signature (Ljava/beans/beancontext/BeanContextServices;Ljava/lang/Class<*>;)V method name getServiceClass descriptor ()Ljava/lang/Class; flags 1 signature ()Ljava/lang/Class<*>; method name getCurrentServiceSelectors descriptor ()Ljava/util/Iterator; flags 1 signature ()Ljava/util/Iterator<*>; @@ -1370,10 +1370,10 @@ method name getCurrentServiceSelectors descriptor (Ljava/beans/beancontext/BeanC class name java/beans/beancontext/BeanContextServiceRevokedEvent -field name serviceClass descriptor Ljava/lang/Class; -field name serviceClass descriptor Ljava/lang/Class; flags 4 signature Ljava/lang/Class<*>; -method name descriptor (Ljava/beans/beancontext/BeanContextServices;Ljava/lang/Class;Z)V -method name getServiceClass descriptor ()Ljava/lang/Class; -method name isServiceClass descriptor (Ljava/lang/Class;)Z +field name serviceClass descriptor Ljava/lang/Class; flags 4 signature Ljava/lang/Class<*>; method name descriptor (Ljava/beans/beancontext/BeanContextServices;Ljava/lang/Class;Z)V flags 1 signature (Ljava/beans/beancontext/BeanContextServices;Ljava/lang/Class<*>;Z)V method name getServiceClass descriptor ()Ljava/lang/Class; flags 1 signature ()Ljava/lang/Class<*>; method name isServiceClass descriptor (Ljava/lang/Class;)Z flags 1 signature (Ljava/lang/Class<*>;)Z @@ -1402,8 +1402,6 @@ innerclass innerClass java/util/Map$Entry outerClass java/util/Map innerClassNam innerclass innerClass java/beans/beancontext/BeanContextSupport$BCSIterator outerClass java/beans/beancontext/BeanContextSupport innerClassName BCSIterator flags 1c -field name services descriptor Ljava/util/HashMap; -field name bcsListeners descriptor Ljava/util/ArrayList; -field name services descriptor Ljava/util/HashMap; flags 84 signature Ljava/util/HashMap; -field name bcsListeners descriptor Ljava/util/ArrayList; flags 84 signature Ljava/util/ArrayList; -method name createBCSSServiceProvider descriptor (Ljava/lang/Class;Ljava/beans/beancontext/BeanContextServiceProvider;)Ljava/beans/beancontext/BeanContextServicesSupport$BCSSServiceProvider; -method name addService descriptor (Ljava/lang/Class;Ljava/beans/beancontext/BeanContextServiceProvider;)Z -method name addService descriptor (Ljava/lang/Class;Ljava/beans/beancontext/BeanContextServiceProvider;Z)Z @@ -1414,6 +1412,8 @@ field name bcsListeners descriptor Ljava/util/ArrayList; flags 84 signature Ljav -method name getCurrentServiceSelectors descriptor (Ljava/lang/Class;)Ljava/util/Iterator; -method name fireServiceAdded descriptor (Ljava/lang/Class;)V -method name fireServiceRevoked descriptor (Ljava/lang/Class;Z)V +field name services descriptor Ljava/util/HashMap; flags 84 signature Ljava/util/HashMap; +field name bcsListeners descriptor Ljava/util/ArrayList; flags 84 signature Ljava/util/ArrayList; method name createBCSSServiceProvider descriptor (Ljava/lang/Class;Ljava/beans/beancontext/BeanContextServiceProvider;)Ljava/beans/beancontext/BeanContextServicesSupport$BCSSServiceProvider; flags 4 signature (Ljava/lang/Class<*>;Ljava/beans/beancontext/BeanContextServiceProvider;)Ljava/beans/beancontext/BeanContextServicesSupport$BCSSServiceProvider; method name addService descriptor (Ljava/lang/Class;Ljava/beans/beancontext/BeanContextServiceProvider;)Z flags 1 signature (Ljava/lang/Class<*>;Ljava/beans/beancontext/BeanContextServiceProvider;)Z method name addService descriptor (Ljava/lang/Class;Ljava/beans/beancontext/BeanContextServiceProvider;Z)Z flags 4 signature (Ljava/lang/Class<*>;Ljava/beans/beancontext/BeanContextServiceProvider;Z)Z @@ -1448,12 +1448,12 @@ innerclass innerClass java/beans/beancontext/BeanContextSupport$BCSIterator oute innerclass innerClass java/util/Map$Entry outerClass java/util/Map innerClassName Entry flags 609 -field name children descriptor Ljava/util/HashMap; -field name bcmListeners descriptor Ljava/util/ArrayList; -field name children descriptor Ljava/util/HashMap; flags 84 signature Ljava/util/HashMap; -field name bcmListeners descriptor Ljava/util/ArrayList; flags 84 signature Ljava/util/ArrayList; -method name iterator descriptor ()Ljava/util/Iterator; -method name bcsChildren descriptor ()Ljava/util/Iterator; -method name serialize descriptor (Ljava/io/ObjectOutputStream;Ljava/util/Collection;)V -method name classEquals descriptor (Ljava/lang/Class;Ljava/lang/Class;)Z +field name children descriptor Ljava/util/HashMap; flags 84 signature Ljava/util/HashMap; +field name bcmListeners descriptor Ljava/util/ArrayList; flags 84 signature Ljava/util/ArrayList; method name iterator descriptor ()Ljava/util/Iterator; flags 1 signature ()Ljava/util/Iterator; method name bcsChildren descriptor ()Ljava/util/Iterator; flags 4 signature ()Ljava/util/Iterator; method name serialize descriptor (Ljava/io/ObjectOutputStream;Ljava/util/Collection;)V thrownTypes java/io/IOException flags 14 signature (Ljava/io/ObjectOutputStream;Ljava/util/Collection<*>;)V @@ -2028,10 +2028,10 @@ method name getTagNames descriptor ()Ljava/util/SortedSet; flags 1 signature ()L class name javax/imageio/spi/ImageReaderSpi -field name STANDARD_INPUT_TYPE descriptor [Ljava/lang/Class; -field name inputTypes descriptor [Ljava/lang/Class; -field name STANDARD_INPUT_TYPE descriptor [Ljava/lang/Class; flags 19 deprecated true signature [Ljava/lang/Class<*>; runtimeAnnotations @Ljava/lang/Deprecated; -field name inputTypes descriptor [Ljava/lang/Class; flags 4 signature [Ljava/lang/Class<*>; -method name descriptor (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V -method name getInputTypes descriptor ()[Ljava/lang/Class; +field name STANDARD_INPUT_TYPE descriptor [Ljava/lang/Class; flags 19 deprecated true signature [Ljava/lang/Class<*>; runtimeAnnotations @Ljava/lang/Deprecated; +field name inputTypes descriptor [Ljava/lang/Class; flags 4 signature [Ljava/lang/Class<*>; method name descriptor (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V flags 1 signature (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Class<*>;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V method name getInputTypes descriptor ()[Ljava/lang/Class; flags 1 signature ()[Ljava/lang/Class<*>; @@ -2042,10 +2042,10 @@ innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang class name javax/imageio/spi/ImageWriterSpi -field name STANDARD_OUTPUT_TYPE descriptor [Ljava/lang/Class; -field name outputTypes descriptor [Ljava/lang/Class; -field name STANDARD_OUTPUT_TYPE descriptor [Ljava/lang/Class; flags 19 deprecated true signature [Ljava/lang/Class<*>; runtimeAnnotations @Ljava/lang/Deprecated; -field name outputTypes descriptor [Ljava/lang/Class; flags 4 signature [Ljava/lang/Class<*>; -method name descriptor (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V -method name getOutputTypes descriptor ()[Ljava/lang/Class; +field name STANDARD_OUTPUT_TYPE descriptor [Ljava/lang/Class; flags 19 deprecated true signature [Ljava/lang/Class<*>; runtimeAnnotations @Ljava/lang/Deprecated; +field name outputTypes descriptor [Ljava/lang/Class; flags 4 signature [Ljava/lang/Class<*>; method name descriptor (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Class;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V flags 1 signature (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Class<*>;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V method name getOutputTypes descriptor ()[Ljava/lang/Class; flags 1 signature ()[Ljava/lang/Class<*>; @@ -3485,7 +3485,6 @@ innerclass innerClass javax/swing/JSpinner$NumberEditor outerClass javax/swing/J innerclass innerClass javax/swing/JSpinner$DefaultEditor outerClass javax/swing/JSpinner innerClassName DefaultEditor flags 9 innerclass innerClass javax/swing/JFormattedTextField$AbstractFormatter outerClass javax/swing/JFormattedTextField innerClassName AbstractFormatter flags 409 innerclass innerClass javax/swing/JFormattedTextField$AbstractFormatterFactory outerClass javax/swing/JFormattedTextField innerClassName AbstractFormatterFactory flags 409 -method name setComponentOrientation descriptor (Ljava/awt/ComponentOrientation;)V flags 1 class name javax/swing/JSplitPane header extends javax/swing/JComponent implements javax/accessibility/Accessible flags 21 runtimeAnnotations @Ljava/beans/JavaBean;(defaultProperty="UI") @@ -3592,8 +3591,6 @@ innerclass innerClass javax/swing/UIDefaults$LazyValue outerClass javax/swing/UI innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang/invoke/MethodHandles innerClassName Lookup flags 19 -field name defaultRenderersByColumnClass descriptor Ljava/util/Hashtable; -field name defaultEditorsByColumnClass descriptor Ljava/util/Hashtable; -field name defaultRenderersByColumnClass descriptor Ljava/util/Hashtable; flags 84 signature Ljava/util/Hashtable; -field name defaultEditorsByColumnClass descriptor Ljava/util/Hashtable; flags 84 signature Ljava/util/Hashtable; -method name descriptor (Ljava/util/Vector;Ljava/util/Vector;)V -method name setTableHeader descriptor (Ljavax/swing/table/JTableHeader;)V -method name setRowHeight descriptor (I)V @@ -3638,6 +3635,8 @@ field name defaultEditorsByColumnClass descriptor Ljava/util/Hashtable; flags 84 -method name setFillsViewportHeight descriptor (Z)V -method name setCellEditor descriptor (Ljavax/swing/table/TableCellEditor;)V -method name getAccessibleContext descriptor ()Ljavax/accessibility/AccessibleContext; +field name defaultRenderersByColumnClass descriptor Ljava/util/Hashtable; flags 84 signature Ljava/util/Hashtable; +field name defaultEditorsByColumnClass descriptor Ljava/util/Hashtable; flags 84 signature Ljava/util/Hashtable; method name descriptor (Ljava/util/Vector;Ljava/util/Vector;)V flags 1 signature (Ljava/util/Vector<+Ljava/util/Vector;>;Ljava/util/Vector<*>;)V method name setTableHeader descriptor (Ljavax/swing/table/JTableHeader;)V flags 1 runtimeAnnotations @Ljava/beans/BeanProperty;(description="The\u005C;u0020;JTableHeader\u005C;u0020;instance\u005C;u0020;which\u005C;u0020;renders\u005C;u0020;the\u005C;u0020;column\u005C;u0020;headers.") method name setRowHeight descriptor (I)V flags 1 runtimeAnnotations @Ljava/beans/BeanProperty;(description="The\u005C;u0020;height\u005C;u0020;of\u005C;u0020;the\u005C;u0020;specified\u005C;u0020;row.") @@ -4230,12 +4229,12 @@ innerclass innerClass javax/swing/plaf/basic/BasicComboBoxRenderer$UIResource ou innerclass innerClass javax/swing/plaf/basic/BasicComboBoxEditor$UIResource outerClass javax/swing/plaf/basic/BasicComboBoxEditor innerClassName UIResource flags 9 -field name comboBox descriptor Ljavax/swing/JComboBox; -field name listBox descriptor Ljavax/swing/JList; -field name comboBox descriptor Ljavax/swing/JComboBox; flags 4 signature Ljavax/swing/JComboBox; -field name listBox descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; -method name createRenderer descriptor ()Ljavax/swing/ListCellRenderer; -method name isPopupVisible descriptor (Ljavax/swing/JComboBox;)Z -method name setPopupVisible descriptor (Ljavax/swing/JComboBox;Z)V -method name isFocusTraversable descriptor (Ljavax/swing/JComboBox;)Z +field name comboBox descriptor Ljavax/swing/JComboBox; flags 4 signature Ljavax/swing/JComboBox; +field name listBox descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; method name createRenderer descriptor ()Ljavax/swing/ListCellRenderer; flags 4 signature ()Ljavax/swing/ListCellRenderer; method name isPopupVisible descriptor (Ljavax/swing/JComboBox;)Z flags 1 signature (Ljavax/swing/JComboBox<*>;)Z method name setPopupVisible descriptor (Ljavax/swing/JComboBox;Z)V flags 1 signature (Ljavax/swing/JComboBox<*>;Z)V @@ -4254,13 +4253,13 @@ innerclass innerClass javax/swing/plaf/basic/BasicComboPopup$InvocationMouseMoti innerclass innerClass javax/swing/plaf/basic/BasicComboPopup$InvocationMouseHandler outerClass javax/swing/plaf/basic/BasicComboPopup innerClassName InvocationMouseHandler flags 4 -field name comboBox descriptor Ljavax/swing/JComboBox; -field name list descriptor Ljavax/swing/JList; -field name comboBox descriptor Ljavax/swing/JComboBox; flags 4 signature Ljavax/swing/JComboBox; -field name list descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; -method name getList descriptor ()Ljavax/swing/JList; -method name uninstallComboBoxModelListeners descriptor (Ljavax/swing/ComboBoxModel;)V -method name descriptor (Ljavax/swing/JComboBox;)V -method name createList descriptor ()Ljavax/swing/JList; -method name installComboBoxModelListeners descriptor (Ljavax/swing/ComboBoxModel;)V +field name comboBox descriptor Ljavax/swing/JComboBox; flags 4 signature Ljavax/swing/JComboBox; +field name list descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; method name getList descriptor ()Ljavax/swing/JList; flags 1 signature ()Ljavax/swing/JList; method name uninstallComboBoxModelListeners descriptor (Ljavax/swing/ComboBoxModel;)V flags 4 signature (Ljavax/swing/ComboBoxModel<*>;)V method name descriptor (Ljavax/swing/JComboBox;)V flags 1 signature (Ljavax/swing/JComboBox;)V @@ -4357,11 +4356,11 @@ innerclass innerClass javax/swing/plaf/basic/BasicListUI$MouseInputHandler outer innerclass innerClass javax/swing/JList$DropLocation outerClass javax/swing/JList innerClassName DropLocation flags 19 innerclass innerClass java/awt/Component$BaselineResizeBehavior outerClass java/awt/Component innerClassName BaselineResizeBehavior flags 4019 -field name list descriptor Ljavax/swing/JList; -field name list descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; -method name paintCell descriptor (Ljava/awt/Graphics;ILjava/awt/Rectangle;Ljavax/swing/ListCellRenderer;Ljavax/swing/ListModel;Ljavax/swing/ListSelectionModel;I)V -method name locationToIndex descriptor (Ljavax/swing/JList;Ljava/awt/Point;)I -method name indexToLocation descriptor (Ljavax/swing/JList;I)Ljava/awt/Point; -method name getCellBounds descriptor (Ljavax/swing/JList;II)Ljava/awt/Rectangle; +field name list descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; method name paintCell descriptor (Ljava/awt/Graphics;ILjava/awt/Rectangle;Ljavax/swing/ListCellRenderer;Ljavax/swing/ListModel;Ljavax/swing/ListSelectionModel;I)V flags 4 signature (Ljava/awt/Graphics;ILjava/awt/Rectangle;Ljavax/swing/ListCellRenderer;Ljavax/swing/ListModel;Ljavax/swing/ListSelectionModel;I)V method name locationToIndex descriptor (Ljavax/swing/JList;Ljava/awt/Point;)I flags 1 signature (Ljavax/swing/JList<*>;Ljava/awt/Point;)I method name indexToLocation descriptor (Ljavax/swing/JList;I)Ljava/awt/Point; flags 1 signature (Ljavax/swing/JList<*>;I)Ljava/awt/Point; @@ -4615,12 +4614,12 @@ innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang class name javax/swing/plaf/metal/MetalComboBoxButton -field name comboBox descriptor Ljavax/swing/JComboBox; -field name listBox descriptor Ljavax/swing/JList; -field name comboBox descriptor Ljavax/swing/JComboBox; flags 4 signature Ljavax/swing/JComboBox; -field name listBox descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; -method name getComboBox descriptor ()Ljavax/swing/JComboBox; -method name setComboBox descriptor (Ljavax/swing/JComboBox;)V -method name descriptor (Ljavax/swing/JComboBox;Ljavax/swing/Icon;Ljavax/swing/CellRendererPane;Ljavax/swing/JList;)V -method name descriptor (Ljavax/swing/JComboBox;Ljavax/swing/Icon;ZLjavax/swing/CellRendererPane;Ljavax/swing/JList;)V +field name comboBox descriptor Ljavax/swing/JComboBox; flags 4 signature Ljavax/swing/JComboBox; +field name listBox descriptor Ljavax/swing/JList; flags 4 signature Ljavax/swing/JList; method name getComboBox descriptor ()Ljavax/swing/JComboBox; flags 11 signature ()Ljavax/swing/JComboBox; method name setComboBox descriptor (Ljavax/swing/JComboBox;)V flags 11 signature (Ljavax/swing/JComboBox;)V method name descriptor (Ljavax/swing/JComboBox;Ljavax/swing/Icon;Ljavax/swing/CellRendererPane;Ljavax/swing/JList;)V flags 1 signature (Ljavax/swing/JComboBox;Ljavax/swing/Icon;Ljavax/swing/CellRendererPane;Ljavax/swing/JList;)V @@ -4740,10 +4739,10 @@ field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector class name javax/swing/plaf/multi/MultiComboBoxUI -field name uis descriptor Ljava/util/Vector; -field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; -method name isFocusTraversable descriptor (Ljavax/swing/JComboBox;)Z -method name setPopupVisible descriptor (Ljavax/swing/JComboBox;Z)V -method name isPopupVisible descriptor (Ljavax/swing/JComboBox;)Z +field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; method name isFocusTraversable descriptor (Ljavax/swing/JComboBox;)Z flags 1 signature (Ljavax/swing/JComboBox<*>;)Z method name setPopupVisible descriptor (Ljavax/swing/JComboBox;Z)V flags 1 signature (Ljavax/swing/JComboBox<*>;Z)V method name isPopupVisible descriptor (Ljavax/swing/JComboBox;)Z flags 1 signature (Ljavax/swing/JComboBox<*>;)Z @@ -4770,10 +4769,10 @@ field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector class name javax/swing/plaf/multi/MultiListUI -field name uis descriptor Ljava/util/Vector; -field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; -method name locationToIndex descriptor (Ljavax/swing/JList;Ljava/awt/Point;)I -method name indexToLocation descriptor (Ljavax/swing/JList;I)Ljava/awt/Point; -method name getCellBounds descriptor (Ljavax/swing/JList;II)Ljava/awt/Rectangle; +field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; method name locationToIndex descriptor (Ljavax/swing/JList;Ljava/awt/Point;)I flags 1 signature (Ljavax/swing/JList<*>;Ljava/awt/Point;)I method name indexToLocation descriptor (Ljavax/swing/JList;I)Ljava/awt/Point; flags 1 signature (Ljavax/swing/JList<*>;I)Ljava/awt/Point; method name getCellBounds descriptor (Ljavax/swing/JList;II)Ljava/awt/Rectangle; flags 1 signature (Ljavax/swing/JList<*>;II)Ljava/awt/Rectangle; @@ -4852,11 +4851,11 @@ field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector class name javax/swing/plaf/multi/MultiTextUI -field name uis descriptor Ljava/util/Vector; -field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; -method name modelToView descriptor (Ljavax/swing/text/JTextComponent;I)Ljava/awt/Rectangle; -method name modelToView descriptor (Ljavax/swing/text/JTextComponent;ILjavax/swing/text/Position$Bias;)Ljava/awt/Rectangle; -method name viewToModel descriptor (Ljavax/swing/text/JTextComponent;Ljava/awt/Point;)I -method name viewToModel descriptor (Ljavax/swing/text/JTextComponent;Ljava/awt/Point;[Ljavax/swing/text/Position$Bias;)I +field name uis descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; method name modelToView descriptor (Ljavax/swing/text/JTextComponent;I)Ljava/awt/Rectangle; thrownTypes javax/swing/text/BadLocationException flags 1 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") method name modelToView descriptor (Ljavax/swing/text/JTextComponent;ILjavax/swing/text/Position$Bias;)Ljava/awt/Rectangle; thrownTypes javax/swing/text/BadLocationException flags 1 deprecated true runtimeAnnotations @Ljava/lang/Deprecated;(since="9") method name modelToView2D descriptor (Ljavax/swing/text/JTextComponent;ILjavax/swing/text/Position$Bias;)Ljava/awt/geom/Rectangle2D; thrownTypes javax/swing/text/BadLocationException flags 1 @@ -4982,7 +4981,6 @@ innerclass innerClass javax/swing/JTable$DropLocation outerClass javax/swing/JTa class name javax/swing/table/DefaultTableModel -field name dataVector descriptor Ljava/util/Vector; -field name dataVector descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; -method name descriptor (Ljava/util/Vector;I)V -method name descriptor (Ljava/util/Vector;Ljava/util/Vector;)V -method name getDataVector descriptor ()Ljava/util/Vector; @@ -4993,6 +4991,7 @@ field name dataVector descriptor Ljava/util/Vector; flags 4 signature Ljava/util -method name addColumn descriptor (Ljava/lang/Object;Ljava/util/Vector;)V -method name convertToVector descriptor ([Ljava/lang/Object;)Ljava/util/Vector; -method name convertToVector descriptor ([[Ljava/lang/Object;)Ljava/util/Vector; +field name dataVector descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; method name descriptor (Ljava/util/Vector;I)V flags 1 signature (Ljava/util/Vector<*>;I)V method name descriptor (Ljava/util/Vector;Ljava/util/Vector;)V flags 1 signature (Ljava/util/Vector<+Ljava/util/Vector;>;Ljava/util/Vector<*>;)V method name getDataVector descriptor ()Ljava/util/Vector; flags 1 signature ()Ljava/util/Vector; @@ -5832,13 +5831,13 @@ class name javax/swing/tree/DefaultMutableTreeNode header extends java/lang/Object implements java/lang/Cloneable,javax/swing/tree/MutableTreeNode,java/io/Serializable flags 21 innerclass innerClass java/io/ObjectInputStream$GetField outerClass java/io/ObjectInputStream innerClassName GetField flags 409 -field name children descriptor Ljava/util/Vector; -field name children descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; -method name children descriptor ()Ljava/util/Enumeration; -method name preorderEnumeration descriptor ()Ljava/util/Enumeration; -method name postorderEnumeration descriptor ()Ljava/util/Enumeration; -method name breadthFirstEnumeration descriptor ()Ljava/util/Enumeration; -method name depthFirstEnumeration descriptor ()Ljava/util/Enumeration; -method name pathFromAncestorEnumeration descriptor (Ljavax/swing/tree/TreeNode;)Ljava/util/Enumeration; +field name children descriptor Ljava/util/Vector; flags 4 signature Ljava/util/Vector; method name children descriptor ()Ljava/util/Enumeration; flags 1 signature ()Ljava/util/Enumeration; method name preorderEnumeration descriptor ()Ljava/util/Enumeration; flags 1 signature ()Ljava/util/Enumeration; method name postorderEnumeration descriptor ()Ljava/util/Enumeration; flags 1 signature ()Ljava/util/Enumeration; diff --git a/src/jdk.compiler/share/data/symbols/java.xml.bind-9.sym.txt b/src/jdk.compiler/share/data/symbols/java.xml.bind-9.sym.txt index f6c007544d9..f6a8ebd2f2c 100644 --- a/src/jdk.compiler/share/data/symbols/java.xml.bind-9.sym.txt +++ b/src/jdk.compiler/share/data/symbols/java.xml.bind-9.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -35,10 +35,10 @@ innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang class name javax/xml/bind/JAXBContext -field name JAXB_CONTEXT_FACTORY descriptor Ljava/lang/String; -field name JAXB_CONTEXT_FACTORY descriptor Ljava/lang/String; constantValue javax.xml.bind.JAXBContextFactory flags 19 -method name newInstance descriptor ([Ljava/lang/Class;)Ljavax/xml/bind/JAXBContext; -method name newInstance descriptor ([Ljava/lang/Class;Ljava/util/Map;)Ljavax/xml/bind/JAXBContext; -method name createValidator descriptor ()Ljavax/xml/bind/Validator; +field name JAXB_CONTEXT_FACTORY descriptor Ljava/lang/String; constantValue javax.xml.bind.JAXBContextFactory flags 19 method name newInstance descriptor ([Ljava/lang/Class;)Ljavax/xml/bind/JAXBContext; thrownTypes javax/xml/bind/JAXBException flags 89 signature ([Ljava/lang/Class<*>;)Ljavax/xml/bind/JAXBContext; method name newInstance descriptor ([Ljava/lang/Class;Ljava/util/Map;)Ljavax/xml/bind/JAXBContext; thrownTypes javax/xml/bind/JAXBException flags 9 signature ([Ljava/lang/Class<*>;Ljava/util/Map;)Ljavax/xml/bind/JAXBContext; method name createValidator descriptor ()Ljavax/xml/bind/Validator; thrownTypes javax/xml/bind/JAXBException flags 401 deprecated true runtimeAnnotations @Ljava/lang/Deprecated; diff --git a/src/jdk.compiler/share/data/symbols/jdk.management-8.sym.txt b/src/jdk.compiler/share/data/symbols/jdk.management-8.sym.txt index ac0bdef06ac..15811f99cb7 100644 --- a/src/jdk.compiler/share/data/symbols/jdk.management-8.sym.txt +++ b/src/jdk.compiler/share/data/symbols/jdk.management-8.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -91,6 +91,10 @@ method name getThreadAllocatedBytes descriptor ([J)[J flags 401 method name isThreadAllocatedMemorySupported descriptor ()Z flags 401 method name isThreadAllocatedMemoryEnabled descriptor ()Z flags 401 method name setThreadAllocatedMemoryEnabled descriptor (Z)V flags 401 +method name getCurrentThreadAllocatedBytes descriptor ()J flags 1 +method name getTotalThreadAllocatedBytes descriptor ()J flags 1 +method name getThreadInfo descriptor ([JZZI)[Ljava/lang/management/ThreadInfo; flags 1 +method name dumpAllThreads descriptor (ZZI)[Ljava/lang/management/ThreadInfo; flags 1 class name com/sun/management/UnixOperatingSystemMXBean header extends java/lang/Object implements com/sun/management/OperatingSystemMXBean flags 601 classAnnotations @Ljdk/Profile+Annotation;(value=I3) runtimeAnnotations @Ljdk/Exported; diff --git a/src/jdk.compiler/share/data/symbols/jdk.management-9.sym.txt b/src/jdk.compiler/share/data/symbols/jdk.management-9.sym.txt index 6df3114e44a..8ef9e4bb504 100644 --- a/src/jdk.compiler/share/data/symbols/jdk.management-9.sym.txt +++ b/src/jdk.compiler/share/data/symbols/jdk.management-9.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -48,6 +48,10 @@ header extends java/lang/Object implements java/lang/management/OperatingSystemM class name com/sun/management/ThreadMXBean header extends java/lang/Object implements java/lang/management/ThreadMXBean flags 601 +-method name getCurrentThreadAllocatedBytes descriptor ()J +-method name getTotalThreadAllocatedBytes descriptor ()J +-method name getThreadInfo descriptor ([JZZI)[Ljava/lang/management/ThreadInfo; +-method name dumpAllThreads descriptor (ZZI)[Ljava/lang/management/ThreadInfo; class name com/sun/management/UnixOperatingSystemMXBean header extends java/lang/Object implements com/sun/management/OperatingSystemMXBean flags 601 diff --git a/src/jdk.compiler/share/data/symbols/jdk.net-8.sym.txt b/src/jdk.compiler/share/data/symbols/jdk.net-8.sym.txt new file mode 100644 index 00000000000..a579e8f59ec --- /dev/null +++ b/src/jdk.compiler/share/data/symbols/jdk.net-8.sym.txt @@ -0,0 +1,76 @@ +# +# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# ########################################################## +# ### THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT. ### +# ########################################################## +# +class name jdk/net/ExtendedSocketOptions +header extends java/lang/Object flags 31 classAnnotations @Ljdk/Profile+Annotation;(value=I1) runtimeAnnotations @Ljdk/Exported; +field name SO_FLOW_SLA descriptor Ljava/net/SocketOption; flags 19 signature Ljava/net/SocketOption; +field name TCP_KEEPIDLE descriptor Ljava/net/SocketOption; flags 19 signature Ljava/net/SocketOption; +field name TCP_KEEPINTERVAL descriptor Ljava/net/SocketOption; flags 19 signature Ljava/net/SocketOption; +field name TCP_KEEPCOUNT descriptor Ljava/net/SocketOption; flags 19 signature Ljava/net/SocketOption; + +class name jdk/net/NetworkPermission +header extends java/security/BasicPermission flags 31 classAnnotations @Ljdk/Profile+Annotation;(value=I1) runtimeAnnotations @Ljdk/Exported; +method name descriptor (Ljava/lang/String;)V flags 1 +method name descriptor (Ljava/lang/String;Ljava/lang/String;)V flags 1 + +class name jdk/net/SocketFlow +header extends java/lang/Object flags 21 classAnnotations @Ljdk/Profile+Annotation;(value=I1) runtimeAnnotations @Ljdk/Exported; +innerclass innerClass jdk/net/SocketFlow$Status outerClass jdk/net/SocketFlow innerClassName Status flags 4019 +field name NORMAL_PRIORITY descriptor I constantValue 1 flags 19 +field name HIGH_PRIORITY descriptor I constantValue 2 flags 19 +method name create descriptor ()Ljdk/net/SocketFlow; flags 9 +method name priority descriptor (I)Ljdk/net/SocketFlow; flags 1 +method name bandwidth descriptor (J)Ljdk/net/SocketFlow; flags 1 +method name priority descriptor ()I flags 1 +method name bandwidth descriptor ()J flags 1 +method name status descriptor ()Ljdk/net/SocketFlow$Status; flags 1 + +class name jdk/net/SocketFlow$Status +header extends java/lang/Enum flags 4031 signature Ljava/lang/Enum; runtimeAnnotations @Ljdk/Exported; +innerclass innerClass jdk/net/SocketFlow$Status outerClass jdk/net/SocketFlow innerClassName Status flags 4019 +field name NO_STATUS descriptor Ljdk/net/SocketFlow$Status; flags 4019 +field name OK descriptor Ljdk/net/SocketFlow$Status; flags 4019 +field name NO_PERMISSION descriptor Ljdk/net/SocketFlow$Status; flags 4019 +field name NOT_CONNECTED descriptor Ljdk/net/SocketFlow$Status; flags 4019 +field name NOT_SUPPORTED descriptor Ljdk/net/SocketFlow$Status; flags 4019 +field name ALREADY_CREATED descriptor Ljdk/net/SocketFlow$Status; flags 4019 +field name IN_PROGRESS descriptor Ljdk/net/SocketFlow$Status; flags 4019 +field name OTHER descriptor Ljdk/net/SocketFlow$Status; flags 4019 +method name values descriptor ()[Ljdk/net/SocketFlow$Status; flags 9 +method name valueOf descriptor (Ljava/lang/String;)Ljdk/net/SocketFlow$Status; flags 9 + +class name jdk/net/Sockets +header extends java/lang/Object flags 21 classAnnotations @Ljdk/Profile+Annotation;(value=I1) runtimeAnnotations @Ljdk/Exported; +method name setOption descriptor (Ljava/net/Socket;Ljava/net/SocketOption;Ljava/lang/Object;)V thrownTypes java/io/IOException flags 9 signature (Ljava/net/Socket;Ljava/net/SocketOption;TT;)V +method name getOption descriptor (Ljava/net/Socket;Ljava/net/SocketOption;)Ljava/lang/Object; thrownTypes java/io/IOException flags 9 signature (Ljava/net/Socket;Ljava/net/SocketOption;)TT; +method name setOption descriptor (Ljava/net/ServerSocket;Ljava/net/SocketOption;Ljava/lang/Object;)V thrownTypes java/io/IOException flags 9 signature (Ljava/net/ServerSocket;Ljava/net/SocketOption;TT;)V +method name getOption descriptor (Ljava/net/ServerSocket;Ljava/net/SocketOption;)Ljava/lang/Object; thrownTypes java/io/IOException flags 9 signature (Ljava/net/ServerSocket;Ljava/net/SocketOption;)TT; +method name setOption descriptor (Ljava/net/DatagramSocket;Ljava/net/SocketOption;Ljava/lang/Object;)V thrownTypes java/io/IOException flags 9 signature (Ljava/net/DatagramSocket;Ljava/net/SocketOption;TT;)V +method name getOption descriptor (Ljava/net/DatagramSocket;Ljava/net/SocketOption;)Ljava/lang/Object; thrownTypes java/io/IOException flags 9 signature (Ljava/net/DatagramSocket;Ljava/net/SocketOption;)TT; +method name supportedOptions descriptor (Ljava/lang/Class;)Ljava/util/Set; flags 9 signature (Ljava/lang/Class<*>;)Ljava/util/Set;>; + diff --git a/src/jdk.compiler/share/data/symbols/jdk.net-9.sym.txt b/src/jdk.compiler/share/data/symbols/jdk.net-9.sym.txt index d52c33c546a..7db67409789 100644 --- a/src/jdk.compiler/share/data/symbols/jdk.net-9.sym.txt +++ b/src/jdk.compiler/share/data/symbols/jdk.net-9.sym.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -32,51 +32,26 @@ header exports jdk/net requires name\u0020;java.base\u0020;flags\u0020;8000 flag class name jdk/net/ExtendedSocketOptions header extends java/lang/Object flags 31 innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang/invoke/MethodHandles innerClassName Lookup flags 19 -field name SO_FLOW_SLA descriptor Ljava/net/SocketOption; flags 19 signature Ljava/net/SocketOption; +-field name TCP_KEEPIDLE descriptor Ljava/net/SocketOption; +-field name TCP_KEEPINTERVAL descriptor Ljava/net/SocketOption; +-field name TCP_KEEPCOUNT descriptor Ljava/net/SocketOption; class name jdk/net/NetworkPermission header extends java/security/BasicPermission flags 31 -method name descriptor (Ljava/lang/String;)V flags 1 -method name descriptor (Ljava/lang/String;Ljava/lang/String;)V flags 1 class name jdk/net/SocketFlow header extends java/lang/Object flags 21 innerclass innerClass jdk/net/SocketFlow$Status outerClass jdk/net/SocketFlow innerClassName Status flags 4019 innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang/invoke/MethodHandles innerClassName Lookup flags 19 field name UNSET descriptor I constantValue -1 flags 19 -field name NORMAL_PRIORITY descriptor I constantValue 1 flags 19 -field name HIGH_PRIORITY descriptor I constantValue 2 flags 19 -method name create descriptor ()Ljdk/net/SocketFlow; flags 9 -method name priority descriptor (I)Ljdk/net/SocketFlow; flags 1 -method name bandwidth descriptor (J)Ljdk/net/SocketFlow; flags 1 -method name priority descriptor ()I flags 1 -method name bandwidth descriptor ()J flags 1 -method name status descriptor ()Ljdk/net/SocketFlow$Status; flags 1 method name toString descriptor ()Ljava/lang/String; flags 1 class name jdk/net/SocketFlow$Status header extends java/lang/Enum flags 4031 signature Ljava/lang/Enum; innerclass innerClass jdk/net/SocketFlow$Status outerClass jdk/net/SocketFlow innerClassName Status flags 4019 innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang/invoke/MethodHandles innerClassName Lookup flags 19 -field name NO_STATUS descriptor Ljdk/net/SocketFlow$Status; flags 4019 -field name OK descriptor Ljdk/net/SocketFlow$Status; flags 4019 -field name NO_PERMISSION descriptor Ljdk/net/SocketFlow$Status; flags 4019 -field name NOT_CONNECTED descriptor Ljdk/net/SocketFlow$Status; flags 4019 -field name NOT_SUPPORTED descriptor Ljdk/net/SocketFlow$Status; flags 4019 -field name ALREADY_CREATED descriptor Ljdk/net/SocketFlow$Status; flags 4019 -field name IN_PROGRESS descriptor Ljdk/net/SocketFlow$Status; flags 4019 -field name OTHER descriptor Ljdk/net/SocketFlow$Status; flags 4019 -method name values descriptor ()[Ljdk/net/SocketFlow$Status; flags 9 -method name valueOf descriptor (Ljava/lang/String;)Ljdk/net/SocketFlow$Status; flags 9 class name jdk/net/Sockets header extends java/lang/Object flags 21 innerclass innerClass java/lang/invoke/MethodHandles$Lookup outerClass java/lang/invoke/MethodHandles innerClassName Lookup flags 19 -method name setOption descriptor (Ljava/net/Socket;Ljava/net/SocketOption;Ljava/lang/Object;)V thrownTypes java/io/IOException flags 9 signature (Ljava/net/Socket;Ljava/net/SocketOption;TT;)V -method name getOption descriptor (Ljava/net/Socket;Ljava/net/SocketOption;)Ljava/lang/Object; thrownTypes java/io/IOException flags 9 signature (Ljava/net/Socket;Ljava/net/SocketOption;)TT; -method name setOption descriptor (Ljava/net/ServerSocket;Ljava/net/SocketOption;Ljava/lang/Object;)V thrownTypes java/io/IOException flags 9 signature (Ljava/net/ServerSocket;Ljava/net/SocketOption;TT;)V -method name getOption descriptor (Ljava/net/ServerSocket;Ljava/net/SocketOption;)Ljava/lang/Object; thrownTypes java/io/IOException flags 9 signature (Ljava/net/ServerSocket;Ljava/net/SocketOption;)TT; -method name setOption descriptor (Ljava/net/DatagramSocket;Ljava/net/SocketOption;Ljava/lang/Object;)V thrownTypes java/io/IOException flags 9 signature (Ljava/net/DatagramSocket;Ljava/net/SocketOption;TT;)V -method name getOption descriptor (Ljava/net/DatagramSocket;Ljava/net/SocketOption;)Ljava/lang/Object; thrownTypes java/io/IOException flags 9 signature (Ljava/net/DatagramSocket;Ljava/net/SocketOption;)TT; -method name supportedOptions descriptor (Ljava/lang/Class;)Ljava/util/Set; flags 9 signature (Ljava/lang/Class<*>;)Ljava/util/Set;>; diff --git a/src/jdk.compiler/share/data/symbols/symbols b/src/jdk.compiler/share/data/symbols/symbols index 7793033d22f..34619bed887 100644 --- a/src/jdk.compiler/share/data/symbols/symbols +++ b/src/jdk.compiler/share/data/symbols/symbols @@ -30,7 +30,7 @@ #build.tools.symbolgenerator.CreateSymbols build-description-incremental symbols include.list # generate platforms 8:9:A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P -platform version 8 files java.activation-8.sym.txt:java.base-8.sym.txt:java.compiler-8.sym.txt:java.corba-8.sym.txt:java.datatransfer-8.sym.txt:java.desktop-8.sym.txt:java.instrument-8.sym.txt:java.logging-8.sym.txt:java.management-8.sym.txt:java.management.rmi-8.sym.txt:java.naming-8.sym.txt:java.prefs-8.sym.txt:java.rmi-8.sym.txt:java.scripting-8.sym.txt:java.security.jgss-8.sym.txt:java.security.sasl-8.sym.txt:java.sql-8.sym.txt:java.sql.rowset-8.sym.txt:java.transaction-8.sym.txt:java.xml-8.sym.txt:java.xml.bind-8.sym.txt:java.xml.crypto-8.sym.txt:java.xml.ws-8.sym.txt:java.xml.ws.annotation-8.sym.txt:jdk.httpserver-8.sym.txt:jdk.management-8.sym.txt:jdk.scripting.nashorn-8.sym.txt:jdk.sctp-8.sym.txt:jdk.security.auth-8.sym.txt:jdk.security.jgss-8.sym.txt +platform version 8 files java.activation-8.sym.txt:java.base-8.sym.txt:java.compiler-8.sym.txt:java.corba-8.sym.txt:java.datatransfer-8.sym.txt:java.desktop-8.sym.txt:java.instrument-8.sym.txt:java.logging-8.sym.txt:java.management-8.sym.txt:java.management.rmi-8.sym.txt:java.naming-8.sym.txt:java.prefs-8.sym.txt:java.rmi-8.sym.txt:java.scripting-8.sym.txt:java.security.jgss-8.sym.txt:java.security.sasl-8.sym.txt:java.sql-8.sym.txt:java.sql.rowset-8.sym.txt:java.transaction-8.sym.txt:java.xml-8.sym.txt:java.xml.bind-8.sym.txt:java.xml.crypto-8.sym.txt:java.xml.ws-8.sym.txt:java.xml.ws.annotation-8.sym.txt:jdk.httpserver-8.sym.txt:jdk.management-8.sym.txt:jdk.net-8.sym.txt:jdk.scripting.nashorn-8.sym.txt:jdk.sctp-8.sym.txt:jdk.security.auth-8.sym.txt:jdk.security.jgss-8.sym.txt platform version 9 base 8 files java.activation-9.sym.txt:java.base-9.sym.txt:java.compiler-9.sym.txt:java.corba-9.sym.txt:java.datatransfer-9.sym.txt:java.desktop-9.sym.txt:java.instrument-9.sym.txt:java.logging-9.sym.txt:java.management-9.sym.txt:java.management.rmi-9.sym.txt:java.naming-9.sym.txt:java.prefs-9.sym.txt:java.rmi-9.sym.txt:java.scripting-9.sym.txt:java.se-9.sym.txt:java.se.ee-9.sym.txt:java.security.jgss-9.sym.txt:java.security.sasl-9.sym.txt:java.smartcardio-9.sym.txt:java.sql-9.sym.txt:java.sql.rowset-9.sym.txt:java.transaction-9.sym.txt:java.xml-9.sym.txt:java.xml.bind-9.sym.txt:java.xml.crypto-9.sym.txt:java.xml.ws-9.sym.txt:java.xml.ws.annotation-9.sym.txt:jdk.accessibility-9.sym.txt:jdk.attach-9.sym.txt:jdk.charsets-9.sym.txt:jdk.compiler-9.sym.txt:jdk.crypto.cryptoki-9.sym.txt:jdk.crypto.ec-9.sym.txt:jdk.dynalink-9.sym.txt:jdk.editpad-9.sym.txt:jdk.hotspot.agent-9.sym.txt:jdk.httpserver-9.sym.txt:jdk.incubator.httpclient-9.sym.txt:jdk.jartool-9.sym.txt:jdk.javadoc-9.sym.txt:jdk.jcmd-9.sym.txt:jdk.jconsole-9.sym.txt:jdk.jdeps-9.sym.txt:jdk.jdi-9.sym.txt:jdk.jdwp.agent-9.sym.txt:jdk.jlink-9.sym.txt:jdk.jshell-9.sym.txt:jdk.jsobject-9.sym.txt:jdk.jstatd-9.sym.txt:jdk.localedata-9.sym.txt:jdk.management-9.sym.txt:jdk.management.agent-9.sym.txt:jdk.naming.dns-9.sym.txt:jdk.naming.rmi-9.sym.txt:jdk.net-9.sym.txt:jdk.pack-9.sym.txt:jdk.policytool-9.sym.txt:jdk.rmic-9.sym.txt:jdk.scripting.nashorn-9.sym.txt:jdk.sctp-9.sym.txt:jdk.security.auth-9.sym.txt:jdk.security.jgss-9.sym.txt:jdk.unsupported-9.sym.txt:jdk.xml.dom-9.sym.txt:jdk.zipfs-9.sym.txt platform version A base 9 files java.activation-A.sym.txt:java.base-A.sym.txt:java.compiler-A.sym.txt:java.corba-A.sym.txt:java.datatransfer-A.sym.txt:java.desktop-A.sym.txt:java.instrument-A.sym.txt:java.logging-A.sym.txt:java.management-A.sym.txt:java.management.rmi-A.sym.txt:java.naming-A.sym.txt:java.prefs-A.sym.txt:java.rmi-A.sym.txt:java.scripting-A.sym.txt:java.se-A.sym.txt:java.se.ee-A.sym.txt:java.security.jgss-A.sym.txt:java.security.sasl-A.sym.txt:java.smartcardio-A.sym.txt:java.sql-A.sym.txt:java.sql.rowset-A.sym.txt:java.transaction-A.sym.txt:java.xml-A.sym.txt:java.xml.bind-A.sym.txt:java.xml.crypto-A.sym.txt:java.xml.ws-A.sym.txt:java.xml.ws.annotation-A.sym.txt:jdk.accessibility-A.sym.txt:jdk.attach-A.sym.txt:jdk.charsets-A.sym.txt:jdk.compiler-A.sym.txt:jdk.crypto.cryptoki-A.sym.txt:jdk.crypto.ec-A.sym.txt:jdk.dynalink-A.sym.txt:jdk.editpad-A.sym.txt:jdk.hotspot.agent-A.sym.txt:jdk.httpserver-A.sym.txt:jdk.incubator.httpclient-A.sym.txt:jdk.jartool-A.sym.txt:jdk.javadoc-A.sym.txt:jdk.jcmd-A.sym.txt:jdk.jconsole-A.sym.txt:jdk.jdeps-A.sym.txt:jdk.jdi-A.sym.txt:jdk.jdwp.agent-A.sym.txt:jdk.jlink-A.sym.txt:jdk.jshell-A.sym.txt:jdk.jsobject-A.sym.txt:jdk.jstatd-A.sym.txt:jdk.localedata-A.sym.txt:jdk.management-A.sym.txt:jdk.management.agent-A.sym.txt:jdk.naming.dns-A.sym.txt:jdk.naming.rmi-A.sym.txt:jdk.net-A.sym.txt:jdk.pack-A.sym.txt:jdk.policytool-A.sym.txt:jdk.rmic-A.sym.txt:jdk.scripting.nashorn-A.sym.txt:jdk.sctp-A.sym.txt:jdk.security.auth-A.sym.txt:jdk.security.jgss-A.sym.txt:jdk.unsupported-A.sym.txt:jdk.xml.dom-A.sym.txt:jdk.zipfs-A.sym.txt platform version B base A files java.activation-B.sym.txt:java.base-B.sym.txt:java.compiler-B.sym.txt:java.corba-B.sym.txt:java.datatransfer-B.sym.txt:java.desktop-B.sym.txt:java.instrument-B.sym.txt:java.logging-B.sym.txt:java.management-B.sym.txt:java.management.rmi-B.sym.txt:java.naming-B.sym.txt:java.net.http-B.sym.txt:java.prefs-B.sym.txt:java.rmi-B.sym.txt:java.scripting-B.sym.txt:java.se-B.sym.txt:java.se.ee-B.sym.txt:java.security.jgss-B.sym.txt:java.security.sasl-B.sym.txt:java.smartcardio-B.sym.txt:java.sql-B.sym.txt:java.sql.rowset-B.sym.txt:java.transaction-B.sym.txt:java.transaction.xa-B.sym.txt:java.xml-B.sym.txt:java.xml.bind-B.sym.txt:java.xml.crypto-B.sym.txt:java.xml.ws-B.sym.txt:java.xml.ws.annotation-B.sym.txt:jdk.accessibility-B.sym.txt:jdk.attach-B.sym.txt:jdk.charsets-B.sym.txt:jdk.compiler-B.sym.txt:jdk.crypto.cryptoki-B.sym.txt:jdk.crypto.ec-B.sym.txt:jdk.dynalink-B.sym.txt:jdk.editpad-B.sym.txt:jdk.hotspot.agent-B.sym.txt:jdk.httpserver-B.sym.txt:jdk.incubator.httpclient-B.sym.txt:jdk.jartool-B.sym.txt:jdk.javadoc-B.sym.txt:jdk.jcmd-B.sym.txt:jdk.jconsole-B.sym.txt:jdk.jdeps-B.sym.txt:jdk.jdi-B.sym.txt:jdk.jdwp.agent-B.sym.txt:jdk.jfr-B.sym.txt:jdk.jlink-B.sym.txt:jdk.jshell-B.sym.txt:jdk.jsobject-B.sym.txt:jdk.jstatd-B.sym.txt:jdk.localedata-B.sym.txt:jdk.management-B.sym.txt:jdk.management.agent-B.sym.txt:jdk.management.jfr-B.sym.txt:jdk.naming.dns-B.sym.txt:jdk.naming.rmi-B.sym.txt:jdk.net-B.sym.txt:jdk.pack-B.sym.txt:jdk.rmic-B.sym.txt:jdk.scripting.nashorn-B.sym.txt:jdk.sctp-B.sym.txt:jdk.security.auth-B.sym.txt:jdk.security.jgss-B.sym.txt:jdk.unsupported-B.sym.txt:jdk.xml.dom-B.sym.txt:jdk.zipfs-B.sym.txt diff --git a/test/langtools/tools/javac/platform/CompilationTest.java b/test/langtools/tools/javac/platform/CompilationTest.java new file mode 100644 index 00000000000..93fb2fb2678 --- /dev/null +++ b/test/langtools/tools/javac/platform/CompilationTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8365060 + * @summary Verify javac can compile given snippets with --release + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.main + * jdk.compiler/com.sun.tools.javac.platform + * jdk.compiler/com.sun.tools.javac.util:+open + * @build toolbox.ToolBox CompilationTest + * @run main CompilationTest + */ + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import toolbox.JavacTask; +import toolbox.Task; +import toolbox.ToolBox; + +public class CompilationTest { + + private final ToolBox tb = new ToolBox(); + public static void main(String... args) throws IOException, URISyntaxException { + CompilationTest t = new CompilationTest(); + + t.testJdkNet(); + } + + void testJdkNet() throws IOException { + Path root = Paths.get("."); + Path classes = root.resolve("classes"); + Files.createDirectories(classes); + + new JavacTask(tb) + .outdir(classes) + .options("--release", "8", + "-XDrawDiagnostics") + .sources(""" + import jdk.net.ExtendedSocketOptions; + public class Test { + } + """) + .run() + .writeAll() + .getOutputLines(Task.OutputKind.DIRECT); + } + +} diff --git a/test/langtools/tools/javac/sym/ElementStructureTest.java b/test/langtools/tools/javac/sym/ElementStructureTest.java index f1cc3cd91fe..14fa194501a 100644 --- a/test/langtools/tools/javac/sym/ElementStructureTest.java +++ b/test/langtools/tools/javac/sym/ElementStructureTest.java @@ -132,10 +132,10 @@ public class ElementStructureTest { (byte) 0x3D, (byte) 0xC1, (byte) 0xFE, (byte) 0xCB }; static final byte[] hash8 = new byte[] { - (byte) 0x10, (byte) 0xE6, (byte) 0xE8, (byte) 0x11, - (byte) 0xC8, (byte) 0x02, (byte) 0x63, (byte) 0x9B, - (byte) 0xAB, (byte) 0x11, (byte) 0x9E, (byte) 0x4F, - (byte) 0xFA, (byte) 0x00, (byte) 0x6D, (byte) 0x81 + (byte) 0x07, (byte) 0xAB, (byte) 0x0A, (byte) 0x8D, + (byte) 0x1C, (byte) 0x44, (byte) 0x6D, (byte) 0x71, + (byte) 0x07, (byte) 0x53, (byte) 0x59, (byte) 0x74, + (byte) 0x5B, (byte) 0x49, (byte) 0x60, (byte) 0xAD }; final static Map version2Hash = new HashMap<>(); @@ -283,7 +283,7 @@ public class ElementStructureTest { } JavaFileObject file = new ByteArrayJavaFileObject(data.toByteArray()); try (InputStream in = new ByteArrayInputStream(data.toByteArray())) { - String name = ClassFile.of().parse(in.readAllBytes()).thisClass().name().stringValue(); + String name = ClassFile.of().parse(in.readAllBytes()).thisClass().name().stringValue().replace("/", "."); className2File.put(name, file); file2ClassName.put(file, name); } catch (IOException ex) { @@ -514,6 +514,8 @@ public class ElementStructureTest { out.write(Double.toHexString((Double) value)); } else if (value instanceof Float) { out.write(Float.toHexString((Float) value)); + } else if (value instanceof Character ch && Character.isSurrogate(ch)) { + out.write(Integer.toHexString(ch)); } else { out.write(String.valueOf(value)); } From 444007fc234aeff75025831c2d1b5538c87fa8f1 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Tue, 30 Sep 2025 12:27:22 +0000 Subject: [PATCH 297/556] 8368842: Parallel: Refactor PCAddThreadRootsMarkingTaskClosure Reviewed-by: fandreuzzi, tschatzl --- .../share/gc/parallel/psParallelCompact.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.cpp b/src/hotspot/share/gc/parallel/psParallelCompact.cpp index 484533619a4..af812c652a6 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.cpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.cpp @@ -1053,24 +1053,19 @@ bool PSParallelCompact::invoke(bool clear_all_soft_refs, bool should_do_max_comp } class PCAddThreadRootsMarkingTaskClosure : public ThreadClosure { -private: - uint _worker_id; + ParCompactionManager* _cm; public: - PCAddThreadRootsMarkingTaskClosure(uint worker_id) : _worker_id(worker_id) { } + PCAddThreadRootsMarkingTaskClosure(ParCompactionManager* cm) : _cm(cm) { } void do_thread(Thread* thread) { - assert(ParallelScavengeHeap::heap()->is_stw_gc_active(), "called outside gc"); - ResourceMark rm; - ParCompactionManager* cm = ParCompactionManager::gc_thread_compaction_manager(_worker_id); + MarkingNMethodClosure mark_and_push_in_blobs(&_cm->_mark_and_push_closure); - MarkingNMethodClosure mark_and_push_in_blobs(&cm->_mark_and_push_closure); - - thread->oops_do(&cm->_mark_and_push_closure, &mark_and_push_in_blobs); + thread->oops_do(&_cm->_mark_and_push_closure, &mark_and_push_in_blobs); // Do the real work - cm->follow_marking_stacks(); + _cm->follow_marking_stacks(); } }; @@ -1114,7 +1109,7 @@ public: } { - PCAddThreadRootsMarkingTaskClosure closure(worker_id); + PCAddThreadRootsMarkingTaskClosure closure(cm); Threads::possibly_parallel_threads_do(_active_workers > 1 /* is_par */, &closure); } From 07ea907e4fc8aa8fda01d8fe64c599f9d944eef9 Mon Sep 17 00:00:00 2001 From: Anass Baya Date: Tue, 30 Sep 2025 13:57:07 +0000 Subject: [PATCH 298/556] 8361606: ConsumeNextMnemonicKeyTypedTest.java fails on Windows: character typed with VK_A: a 8321303: Intermittent open/test/jdk/java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java failure on Linux Reviewed-by: dnguyen, honkar, aivanov --- .../classes/javax/swing/plaf/basic/BasicPopupMenuUI.java | 3 ++- test/jdk/ProblemList.txt | 1 - .../ConsumeNextMnemonicKeyTypedTest.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java index e518f509c5a..6fab795e36c 100644 --- a/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java +++ b/src/java.desktop/share/classes/javax/swing/plaf/basic/BasicPopupMenuUI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -377,6 +377,7 @@ public class BasicPopupMenuUI extends PopupMenuUI { } else if (item.isEnabled()) { // we have a menu item manager.clearSelectedPath(); + sun.awt.SunToolkit.consumeNextKeyTyped(e); item.doClick(); } e.consume(); diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 381c0c8b0be..1602baa9894 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -487,7 +487,6 @@ java/awt/Graphics2D/DrawString/RotTransText.java 8316878 linux-all java/awt/KeyboardFocusmanager/TypeAhead/ButtonActionKeyTest/ButtonActionKeyTest.java 8257529 windows-x64 java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeForModalDialogTest/ConsumeForModalDialogTest.java 8302787 windows-all java/awt/KeyboardFocusmanager/TypeAhead/MenuItemActivatedTest/MenuItemActivatedTest.java 8302787 windows-all -java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java 8321303 linux-all java/awt/Dialog/MakeWindowAlwaysOnTop/MakeWindowAlwaysOnTop.java 8266243 macosx-aarch64 java/awt/Dialog/ChoiceModalDialogTest.java 8161475 macosx-all diff --git a/test/jdk/java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java b/test/jdk/java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java index 5b2dc2844f1..60c05fb05a6 100644 --- a/test/jdk/java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java +++ b/test/jdk/java/awt/KeyboardFocusmanager/ConsumeNextMnemonicKeyTypedTest/ConsumeNextMnemonicKeyTypedTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,7 @@ /* @test @key headful - @bug 6346690 + @bug 6346690 8361606 8321303 @summary Tests that key_typed is consumed after mnemonic key_pressed is handled for a menu item. @library /test/lib @build jdk.test.lib.Platform From 8cc54ec6b86fc5b80af02939363eccd8e3e899e7 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Tue, 30 Sep 2025 14:14:53 +0000 Subject: [PATCH 299/556] 8368563: JFR: Improve jfr query help text Reviewed-by: mgronlun --- .../jdk/jfr/internal/query/QueryPrinter.java | 2 ++ .../classes/jdk/jfr/internal/tool/Query.java | 31 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryPrinter.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryPrinter.java index 0d0310dae5b..9d0ce279abd 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryPrinter.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/query/QueryPrinter.java @@ -280,12 +280,14 @@ public final class QueryPrinter { MEDIAN: The numeric median MIN: The numeric minimum P90, P95, P99, P999: The numeric percentile, 90%, 95%, 99% or 99.9% + SET: All unique values in a comma-separated list STDEV: The numeric standard deviation SUM: The numeric sum UNIQUE: The unique number of occurrences of a value Null values are included, but ignored for numeric functions. If no aggregator function is specified, the first non-null value is used. - property, any of the following: + none No formatting for the column cell-height: Maximum height of a table cell missing:whitespace Replace missing values (N/A) with blank space normalized Normalize values between 0 and 1.0 for the column diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Query.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Query.java index d350b4215bf..ec0407a6f04 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Query.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tool/Query.java @@ -73,27 +73,26 @@ final class Query extends Command { p.println(); p.println(" $ jfr query --verbose \"SELECT * FROM ObjectAllocationSample\" recording.jfr"); p.println(); - p.println(" $ jfr query --width 160 \"SELECT pid, path FROM SystemProcess\" recording.jfr"); + p.println(" $ jfr query --width 160 \"SELECT pid, commandLine FROM SystemProcess\" recording.jfr"); p.println(); p.println(" $ jfr query \"SELECT stackTrace.topFrame AS T, SUM(weight)"); p.println(" FROM ObjectAllocationSample GROUP BY T\" recording.jfr"); p.println(); - p.println("$ jfr JFR.query \"COLUMN 'Method', 'Percentage'"); - p.println(" FORMAT default, normalized;width:10"); - p.println(" SELECT stackTrace.topFrame AS T, COUNT(*) AS C"); - p.println(" GROUP BY T"); - p.println(" FROM ExecutionSample ORDER BY C DESC\" recording.jfr"); + p.println(" $ jfr query \"COLUMN 'Method', 'Percentage'"); + p.println(" FORMAT none, normalized"); + p.println(" SELECT stackTrace.topFrame AS T, COUNT(*) AS C"); + p.println(" FROM ExecutionSample GROUP BY T ORDER BY C DESC\" recording.jfr"); p.println(); - p.println("$ jcmd JFR.query \"COLUMN 'Start', 'GC ID', 'Heap Before GC',"); - p.println(" 'Heap After GC', 'Longest Pause'"); - p.println(" SELECT G.startTime, G.gcId, B.heapUsed,"); - p.println(" A.heapUsed, longestPause"); - p.println(" FROM GarbageCollection AS G,"); - p.println(" GCHeapSummary AS B,"); - p.println(" GCHeapSummary AS A"); - p.println(" WHERE B.when = 'Before GC' AND A.when = 'After GC'"); - p.println(" GROUP BY gcId"); - p.println(" ORDER BY G.startTime\" recording.jfr"); + p.println(" $ jfr query \"COLUMN 'Start', 'GC ID', 'Heap Before GC',"); + p.println(" 'Heap After GC', 'Longest Pause'"); + p.println(" SELECT G.startTime, G.gcId, B.heapUsed,"); + p.println(" A.heapUsed, longestPause"); + p.println(" FROM GarbageCollection AS G,"); + p.println(" GCHeapSummary AS B,"); + p.println(" GCHeapSummary AS A"); + p.println(" WHERE B.when = 'Before GC' AND A.when = 'After GC'"); + p.println(" GROUP BY gcId"); + p.println(" ORDER BY gcId\" recording.jfr"); p.println(); p.println("************************************ WARNING ******************************************"); p.println("The query command is only available in debug builds and is targeted towards OpenJDK"); From 6b4b10200ed10365e1ae1ca02ade773ce5a108c3 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Tue, 30 Sep 2025 14:24:05 +0000 Subject: [PATCH 300/556] 8368809: JFR: Remove events from testSettingConfiguration in TestActiveSettingEvent Reviewed-by: mgronlun --- .../event/runtime/TestActiveSettingEvent.java | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/test/jdk/jdk/jfr/event/runtime/TestActiveSettingEvent.java b/test/jdk/jdk/jfr/event/runtime/TestActiveSettingEvent.java index 92298eaece0..96e251aa077 100644 --- a/test/jdk/jdk/jfr/event/runtime/TestActiveSettingEvent.java +++ b/test/jdk/jdk/jfr/event/runtime/TestActiveSettingEvent.java @@ -245,28 +245,6 @@ public final class TestActiveSettingEvent { System.out.println("Testing configuration " + configurationName); Configuration c = Configuration.getConfiguration(configurationName); Map settingValues = c.getSettings(); - // Don't want to add these settings to the jfc-files we ship since they - // are not useful to configure. They are however needed to make the test - // pass. - settingValues.put(EventNames.ActiveSetting + "#stackTrace", "false"); - settingValues.put(EventNames.ActiveSetting + "#threshold", "0 ns"); - settingValues.put(EventNames.ActiveRecording + "#stackTrace", "false"); - settingValues.put(EventNames.ActiveRecording + "#threshold", "0 ns"); - settingValues.put(EventNames.InitialSecurityProperty + "#threshold", "0 ns"); - settingValues.put(EventNames.JavaExceptionThrow + "#threshold", "0 ns"); - settingValues.put(EventNames.JavaErrorThrow + "#threshold", "0 ns"); - settingValues.put(EventNames.SecurityProperty + "#threshold", "0 ns"); - settingValues.put(EventNames.TLSHandshake + "#threshold", "0 ns"); - settingValues.put(EventNames.X509Certificate + "#threshold", "0 ns"); - settingValues.put(EventNames.X509Validation + "#threshold", "0 ns"); - settingValues.put(EventNames.ProcessStart + "#threshold", "0 ns"); - settingValues.put(EventNames.Deserialization + "#threshold", "0 ns"); - settingValues.put(EventNames.VirtualThreadStart + "#threshold", "0 ns"); - settingValues.put(EventNames.VirtualThreadEnd + "#stackTrace", "false"); - settingValues.put(EventNames.VirtualThreadEnd + "#threshold", "0 ns"); - settingValues.put(EventNames.VirtualThreadSubmitFailed + "#threshold", "0 ns"); - settingValues.put(EventNames.SecurityProviderService + "#threshold", "0 ns"); - try (Recording recording = new Recording(c)) { Map eventTypes = new HashMap<>(); for (EventType et : FlightRecorder.getFlightRecorder().getEventTypes()) { @@ -279,7 +257,11 @@ public final class TestActiveSettingEvent { String settingName = type.getName() + "#" + s.getName(); String value = settingValues.get(settingName); if (value == null) { - throw new Exception("Could not find setting with name " + settingName); + String message = "Could not find setting with name " + settingName + "."; + if (settingName.equals("duration") || settingName.equals("stackTrace")) { + message += " Use @RemoveFields(\"" + settingName + "\") to drop the field."; + } + throw new Exception(message); } // Prefer to have ms unit in jfc file if (value.equals("0 ms")) { From 07ecc93dbd0b74e2362d369e22b5141289eb1f76 Mon Sep 17 00:00:00 2001 From: Robbin Ehn Date: Tue, 30 Sep 2025 15:10:30 +0000 Subject: [PATCH 301/556] 8367692: RISC-V: Align post call nop Reviewed-by: fyang, fjiang, mli --- src/hotspot/cpu/riscv/assembler_riscv.hpp | 29 +++++--- .../cpu/riscv/c1_LIRAssembler_riscv.cpp | 6 ++ .../cpu/riscv/macroAssembler_riscv.cpp | 13 ++-- src/hotspot/cpu/riscv/nativeInst_riscv.cpp | 25 ++++--- src/hotspot/cpu/riscv/nativeInst_riscv.hpp | 15 +--- src/hotspot/cpu/riscv/riscv.ad | 26 ++++++- src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp | 69 ++++++++++++------- 7 files changed, 115 insertions(+), 68 deletions(-) diff --git a/src/hotspot/cpu/riscv/assembler_riscv.hpp b/src/hotspot/cpu/riscv/assembler_riscv.hpp index 0828342ee65..0712b60fb2a 100644 --- a/src/hotspot/cpu/riscv/assembler_riscv.hpp +++ b/src/hotspot/cpu/riscv/assembler_riscv.hpp @@ -914,6 +914,17 @@ protected: public: + static uint32_t encode_csrrw(Register Rd, const uint32_t csr, Register Rs1) { + guarantee(is_uimm12(csr), "csr is invalid"); + uint32_t insn = 0; + patch((address)&insn, 6, 0, 0b1110011); + patch((address)&insn, 14, 12, 0b001); + patch_reg((address)&insn, 7, Rd); + patch_reg((address)&insn, 15, Rs1); + patch((address)&insn, 31, 20, csr); + return insn; + } + static uint32_t encode_jal(Register Rd, const int32_t offset) { guarantee(is_simm21(offset) && ((offset % 2) == 0), "offset is invalid."); uint32_t insn = 0; @@ -3693,19 +3704,15 @@ public: // -------------------------- // Upper Immediate Instruction // -------------------------- -#define INSN(NAME) \ - void NAME(Register Rd, int32_t imm) { \ - /* lui -> c.lui */ \ - if (do_compress() && (Rd != x0 && Rd != x2 && imm != 0 && is_simm18(imm))) { \ - c_lui(Rd, imm); \ - return; \ - } \ - _lui(Rd, imm); \ + void lui(Register Rd, int32_t imm) { + /* lui -> c.lui */ + if (do_compress() && (Rd != x0 && Rd != x2 && imm != 0 && is_simm18(imm))) { + c_lui(Rd, imm); + return; + } + _lui(Rd, imm); } - INSN(lui); - -#undef INSN // Cache Management Operations // These instruction may be turned off for user space. diff --git a/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp b/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp index adc79350be6..9d8ae770ccf 100644 --- a/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp +++ b/src/hotspot/cpu/riscv/c1_LIRAssembler_riscv.cpp @@ -1350,6 +1350,7 @@ void LIR_Assembler::align_call(LIR_Code code) { } void LIR_Assembler::call(LIR_OpJavaCall* op, relocInfo::relocType rtype) { + Assembler::IncompressibleScope scope(_masm); address call = __ reloc_call(Address(op->addr(), rtype)); if (call == nullptr) { bailout("reloc call address stub overflow"); @@ -1360,6 +1361,7 @@ void LIR_Assembler::call(LIR_OpJavaCall* op, relocInfo::relocType rtype) { } void LIR_Assembler::ic_call(LIR_OpJavaCall* op) { + Assembler::IncompressibleScope scope(_masm); address call = __ ic_call(op->addr()); if (call == nullptr) { bailout("reloc call address stub overflow"); @@ -1842,6 +1844,10 @@ void LIR_Assembler::leal(LIR_Opr addr, LIR_Opr dest, LIR_PatchCode patch_code, C void LIR_Assembler::rt_call(LIR_Opr result, address dest, const LIR_OprList* args, LIR_Opr tmp, CodeEmitInfo* info) { assert(!tmp->is_valid(), "don't need temporary"); + Assembler::IncompressibleScope scope(_masm); + // Post call nops must be natural aligned due to cmodx rules. + align_call(lir_rtcall); + __ rt_call(dest); if (info != nullptr) { diff --git a/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp b/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp index c9de1db0308..5c85cc13bed 100644 --- a/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp +++ b/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp @@ -355,14 +355,15 @@ void MacroAssembler::call_VM(Register oop_result, } void MacroAssembler::post_call_nop() { + assert(!in_compressible_scope(), "Must be"); + assert_alignment(pc()); if (!Continuations::enabled()) { return; } - relocate(post_call_nop_Relocation::spec(), [&] { - InlineSkippedInstructionsCounter skipCounter(this); - nop(); - li32(zr, 0); - }); + relocate(post_call_nop_Relocation::spec()); + InlineSkippedInstructionsCounter skipCounter(this); + nop(); + li32(zr, 0); } // these are no-ops overridden by InterpreterMacroAssembler @@ -5013,7 +5014,7 @@ address MacroAssembler::reloc_call(Address entry, Register tmp) { address MacroAssembler::ic_call(address entry, jint method_index) { RelocationHolder rh = virtual_call_Relocation::spec(pc(), method_index); - IncompressibleScope scope(this); // relocations + assert(!in_compressible_scope(), "Must be"); movptr(t0, (address)Universe::non_oop_word(), t1); assert_cond(entry != nullptr); return reloc_call(Address(entry, rh)); diff --git a/src/hotspot/cpu/riscv/nativeInst_riscv.cpp b/src/hotspot/cpu/riscv/nativeInst_riscv.cpp index 72cc95a595d..5d1cac72ade 100644 --- a/src/hotspot/cpu/riscv/nativeInst_riscv.cpp +++ b/src/hotspot/cpu/riscv/nativeInst_riscv.cpp @@ -331,13 +331,10 @@ bool NativeInstruction::is_safepoint_poll() { return MacroAssembler::is_lwu_to_zr(address(this)); } -void NativeIllegalInstruction::insert(address code_pos) { - assert_cond(code_pos != nullptr); - Assembler::sd_instr(code_pos, 0xffffffff); // all bits ones is permanently reserved as an illegal instruction -} - bool NativeInstruction::is_stop() { - return uint_at(0) == 0xc0101073; // an illegal instruction, 'csrrw x0, time, x0' + // an illegal instruction, 'csrrw x0, time, x0' + uint32_t encoded = Assembler::encode_csrrw(x0, Assembler::time, x0); + return uint_at(0) == encoded; } //------------------------------------------------------------------- @@ -347,6 +344,8 @@ void NativeGeneralJump::insert_unconditional(address code_pos, address entry) { MacroAssembler a(&cb); Assembler::IncompressibleScope scope(&a); // Fixed length: see NativeGeneralJump::get_instruction_size() + MacroAssembler::assert_alignment(code_pos); + int32_t offset = 0; a.movptr(t1, entry, offset, t0); // lui, lui, slli, add a.jr(t1, offset); // jalr @@ -378,6 +377,7 @@ bool NativePostCallNop::decode(int32_t& oopmap_slot, int32_t& cb_offset) const { } bool NativePostCallNop::patch(int32_t oopmap_slot, int32_t cb_offset) { + MacroAssembler::assert_alignment(addr_at(4)); if (((oopmap_slot & 0xff) != oopmap_slot) || ((cb_offset & 0xffffff) != cb_offset)) { return false; // cannot encode } @@ -389,14 +389,17 @@ bool NativePostCallNop::patch(int32_t oopmap_slot, int32_t cb_offset) { return true; // successfully encoded } -void NativeDeoptInstruction::verify() { +bool NativeDeoptInstruction::is_deopt_at(address instr) { + assert(instr != nullptr, "Must be"); + uint32_t value = Assembler::ld_instr(instr); + uint32_t encoded = Assembler::encode_csrrw(x0, Assembler::instret, x0); + return value == encoded; } // Inserts an undefined instruction at a given pc void NativeDeoptInstruction::insert(address code_pos) { - // 0xc0201073 encodes CSRRW x0, instret, x0 - uint32_t insn = 0xc0201073; - uint32_t *pos = (uint32_t *) code_pos; - *pos = insn; + MacroAssembler::assert_alignment(code_pos); + uint32_t encoded = Assembler::encode_csrrw(x0, Assembler::instret, x0); + Assembler::sd_instr(code_pos, encoded); ICache::invalidate_range(code_pos, 4); } diff --git a/src/hotspot/cpu/riscv/nativeInst_riscv.hpp b/src/hotspot/cpu/riscv/nativeInst_riscv.hpp index 4235e23d421..d990cfbc50d 100644 --- a/src/hotspot/cpu/riscv/nativeInst_riscv.hpp +++ b/src/hotspot/cpu/riscv/nativeInst_riscv.hpp @@ -294,12 +294,6 @@ inline NativeGeneralJump* nativeGeneralJump_at(address addr) { return jump; } -class NativeIllegalInstruction: public NativeInstruction { - public: - // Insert illegal opcode as specific address - static void insert(address code_pos); -}; - inline bool NativeInstruction::is_nop() const { uint32_t insn = Assembler::ld_instr(addr_at(0)); return insn == 0x13; @@ -353,14 +347,7 @@ class NativeDeoptInstruction: public NativeInstruction { address instruction_address() const { return addr_at(instruction_offset); } address next_instruction_address() const { return addr_at(instruction_size); } - void verify(); - - static bool is_deopt_at(address instr) { - assert(instr != nullptr, ""); - uint32_t value = Assembler::ld_instr(instr); - // 0xc0201073 encodes CSRRW x0, instret, x0 - return value == 0xc0201073; - } + static bool is_deopt_at(address instr); // MT-safe patching static void insert(address code_pos); diff --git a/src/hotspot/cpu/riscv/riscv.ad b/src/hotspot/cpu/riscv/riscv.ad index 739a525c9a4..d816f2405c4 100644 --- a/src/hotspot/cpu/riscv/riscv.ad +++ b/src/hotspot/cpu/riscv/riscv.ad @@ -1269,6 +1269,26 @@ int CallDynamicJavaDirectNode::compute_padding(int current_offset) const return align_up(current_offset, alignment_required()) - current_offset; } +int CallRuntimeDirectNode::compute_padding(int current_offset) const +{ + return align_up(current_offset, alignment_required()) - current_offset; +} + +int CallLeafDirectNode::compute_padding(int current_offset) const +{ + return align_up(current_offset, alignment_required()) - current_offset; +} + +int CallLeafDirectVectorNode::compute_padding(int current_offset) const +{ + return align_up(current_offset, alignment_required()) - current_offset; +} + +int CallLeafNoFPDirectNode::compute_padding(int current_offset) const +{ + return align_up(current_offset, alignment_required()) - current_offset; +} + //============================================================================= #ifndef PRODUCT @@ -8175,7 +8195,7 @@ instruct unnecessary_membar_volatile_rvtso() %{ ins_cost(0); size(0); - + format %{ "#@unnecessary_membar_volatile_rvtso (unnecessary so empty encoding)" %} ins_encode %{ __ block_comment("unnecessary_membar_volatile_rvtso"); @@ -10509,6 +10529,7 @@ instruct CallRuntimeDirect(method meth) ins_encode(riscv_enc_java_to_runtime(meth)); ins_pipe(pipe_class_call); + ins_alignment(4); %} // Call Runtime Instruction @@ -10526,6 +10547,7 @@ instruct CallLeafDirect(method meth) ins_encode(riscv_enc_java_to_runtime(meth)); ins_pipe(pipe_class_call); + ins_alignment(4); %} // Call Runtime Instruction without safepoint and with vector arguments @@ -10543,6 +10565,7 @@ instruct CallLeafDirectVector(method meth) ins_encode(riscv_enc_java_to_runtime(meth)); ins_pipe(pipe_class_call); + ins_alignment(4); %} // Call Runtime Instruction @@ -10560,6 +10583,7 @@ instruct CallLeafNoFPDirect(method meth) ins_encode(riscv_enc_java_to_runtime(meth)); ins_pipe(pipe_class_call); + ins_alignment(4); %} // ============================================================================ diff --git a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp index 34f0f63dbc8..94506e9f19d 100644 --- a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp +++ b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp @@ -1002,20 +1002,23 @@ static void gen_continuation_enter(MacroAssembler* masm, __ bnez(c_rarg2, call_thaw); - // Make sure the call is patchable - __ align(NativeInstruction::instruction_size); + address call_pc; + { + Assembler::IncompressibleScope scope(masm); + // Make sure the call is patchable + __ align(NativeInstruction::instruction_size); - const address tr_call = __ reloc_call(resolve); - if (tr_call == nullptr) { - fatal("CodeCache is full at gen_continuation_enter"); + call_pc = __ reloc_call(resolve); + if (call_pc == nullptr) { + fatal("CodeCache is full at gen_continuation_enter"); + } + + oop_maps->add_gc_map(__ pc() - start, map); + __ post_call_nop(); } - - oop_maps->add_gc_map(__ pc() - start, map); - __ post_call_nop(); - __ j(exit); - address stub = CompiledDirectCall::emit_to_interp_stub(masm, tr_call); + address stub = CompiledDirectCall::emit_to_interp_stub(masm, call_pc); if (stub == nullptr) { fatal("CodeCache is full at gen_continuation_enter"); } @@ -1034,26 +1037,36 @@ static void gen_continuation_enter(MacroAssembler* masm, __ bnez(c_rarg2, call_thaw); - // Make sure the call is patchable - __ align(NativeInstruction::instruction_size); + address call_pc; + { + Assembler::IncompressibleScope scope(masm); + // Make sure the call is patchable + __ align(NativeInstruction::instruction_size); - const address tr_call = __ reloc_call(resolve); - if (tr_call == nullptr) { - fatal("CodeCache is full at gen_continuation_enter"); + call_pc = __ reloc_call(resolve); + if (call_pc == nullptr) { + fatal("CodeCache is full at gen_continuation_enter"); + } + + oop_maps->add_gc_map(__ pc() - start, map); + __ post_call_nop(); } - oop_maps->add_gc_map(__ pc() - start, map); - __ post_call_nop(); - __ j(exit); __ bind(call_thaw); - ContinuationEntry::_thaw_call_pc_offset = __ pc() - start; - __ rt_call(CAST_FROM_FN_PTR(address, StubRoutines::cont_thaw())); - oop_maps->add_gc_map(__ pc() - start, map->deep_copy()); - ContinuationEntry::_return_pc_offset = __ pc() - start; - __ post_call_nop(); + // Post call nops must be natural aligned due to cmodx rules. + { + Assembler::IncompressibleScope scope(masm); + __ align(NativeInstruction::instruction_size); + + ContinuationEntry::_thaw_call_pc_offset = __ pc() - start; + __ rt_call(CAST_FROM_FN_PTR(address, StubRoutines::cont_thaw())); + oop_maps->add_gc_map(__ pc() - start, map->deep_copy()); + ContinuationEntry::_return_pc_offset = __ pc() - start; + __ post_call_nop(); + } __ bind(exit); ContinuationEntry::_cleanup_offset = __ pc() - start; @@ -1082,7 +1095,7 @@ static void gen_continuation_enter(MacroAssembler* masm, __ jr(x11); // the exception handler } - address stub = CompiledDirectCall::emit_to_interp_stub(masm, tr_call); + address stub = CompiledDirectCall::emit_to_interp_stub(masm, call_pc); if (stub == nullptr) { fatal("CodeCache is full at gen_continuation_enter"); } @@ -1115,10 +1128,16 @@ static void gen_continuation_yield(MacroAssembler* masm, __ mv(c_rarg1, sp); + // Post call nops must be natural aligned due to cmodx rules. + __ align(NativeInstruction::instruction_size); + frame_complete = __ pc() - start; address the_pc = __ pc(); - __ post_call_nop(); // this must be exactly after the pc value that is pushed into the frame info, we use this nop for fast CodeBlob lookup + { + Assembler::IncompressibleScope scope(masm); + __ post_call_nop(); // this must be exactly after the pc value that is pushed into the frame info, we use this nop for fast CodeBlob lookup + } __ mv(c_rarg0, xthread); __ set_last_Java_frame(sp, fp, the_pc, t0); From fe9dbcc496671a256c61ac52df5580569dbafb0a Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Tue, 30 Sep 2025 16:15:21 +0000 Subject: [PATCH 302/556] 8368599: ShenandoahConcurrentMark could use ThreadsClaimTokenScope Reviewed-by: ayang, shade, wkemper --- .../share/gc/shenandoah/shenandoahConcurrentMark.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahConcurrentMark.cpp b/src/hotspot/share/gc/shenandoah/shenandoahConcurrentMark.cpp index facba2236be..005d6c42f8c 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahConcurrentMark.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahConcurrentMark.cpp @@ -25,7 +25,6 @@ #include "gc/shared/satbMarkQueue.hpp" -#include "gc/shared/strongRootsScope.hpp" #include "gc/shared/taskTerminator.hpp" #include "gc/shenandoah/shenandoahBarrierSet.inline.hpp" #include "gc/shenandoah/shenandoahClosures.inline.hpp" @@ -94,10 +93,12 @@ private: ShenandoahConcurrentMark* _cm; TaskTerminator* _terminator; bool _dedup_string; + ThreadsClaimTokenScope _threads_claim_token_scope; // needed for Threads::possibly_parallel_threads_do public: ShenandoahFinalMarkingTask(ShenandoahConcurrentMark* cm, TaskTerminator* terminator, bool dedup_string) : - WorkerTask("Shenandoah Final Mark"), _cm(cm), _terminator(terminator), _dedup_string(dedup_string) { + WorkerTask("Shenandoah Final Mark"), _cm(cm), _terminator(terminator), _dedup_string(dedup_string), + _threads_claim_token_scope() { } void work(uint worker_id) { @@ -297,7 +298,6 @@ void ShenandoahConcurrentMark::finish_mark_work() { uint nworkers = heap->workers()->active_workers(); task_queues()->reserve(nworkers); - StrongRootsScope scope(nworkers); TaskTerminator terminator(nworkers, task_queues()); switch (_generation->type()) { From 9b02896b4725ef932a23be11ff76ce04bda0d652 Mon Sep 17 00:00:00 2001 From: Mohamed Issa Date: Tue, 30 Sep 2025 21:08:06 +0000 Subject: [PATCH 303/556] 8360558: Use hex literals instead of decimal literals in math intrinsic constants Reviewed-by: mhaessig, sparasa, jbhateja --- .../cpu/x86/stubGenerator_x86_64_cbrt.cpp | 176 ++++----- .../cpu/x86/stubGenerator_x86_64_tanh.cpp | 368 +++++++++--------- 2 files changed, 272 insertions(+), 272 deletions(-) diff --git a/src/hotspot/cpu/x86/stubGenerator_x86_64_cbrt.cpp b/src/hotspot/cpu/x86/stubGenerator_x86_64_cbrt.cpp index c35f1b9f65e..73330dedc0f 100644 --- a/src/hotspot/cpu/x86/stubGenerator_x86_64_cbrt.cpp +++ b/src/hotspot/cpu/x86/stubGenerator_x86_64_cbrt.cpp @@ -49,142 +49,142 @@ /* Represents 0x7FFFFFFFFFFFFFFF double precision in lower 64 bits*/ ATTRIBUTE_ALIGNED(16) static const juint _ABS_MASK[] = { - 4294967295, 2147483647, 0, 0 + 0xFFFFFFFFUL, 0x7FFFFFFFUL, 0x00000000UL, 0x00000000UL }; ATTRIBUTE_ALIGNED(4) static const juint _SIG_MASK[] = { - 0, 1032192 + 0x00000000UL, 0x000FC000UL }; ATTRIBUTE_ALIGNED(4) static const juint _EXP_MASK[] = { - 0, 3220176896 + 0x00000000UL, 0xBFF00000UL }; ATTRIBUTE_ALIGNED(4) static const juint _EXP_MSK2[] = { - 0, 3220193280 + 0x00000000UL, 0xBFF04000UL }; ATTRIBUTE_ALIGNED(4) static const juint _EXP_MSK3[] = { - 4294967295, 1048575 + 0xFFFFFFFFUL, 0x000FFFFFUL }; ATTRIBUTE_ALIGNED(4) static const juint _SCALE63[] = { - 0, 1138753536 + 0x00000000UL, 0x43E00000UL }; ATTRIBUTE_ALIGNED(4) static const juint _ZERON[] = { - 0, 2147483648 + 0x00000000UL, 0x80000000UL }; ATTRIBUTE_ALIGNED(4) static const juint _INF[] = { - 0, 2146435072 + 0x00000000UL, 0x7FF00000UL }; ATTRIBUTE_ALIGNED(4) static const juint _NEG_INF[] = { - 0, 4293918720 + 0x00000000UL, 0xFFF00000UL }; ATTRIBUTE_ALIGNED(16) static const juint _coeff_table[] = { - 1553778919, 3213899486, 3534952507, 3215266280, 1646371399, - 3214412045, 477218588, 3216798151, 3582521621, 1066628362, - 1007461464, 1068473053, 889629714, 1067378449, 1431655765, - 1070945621 + 0x5C9CC8E7UL, 0xBF9036DEUL, 0xD2B3183BUL, 0xBFA511E8UL, 0x6221A247UL, + 0xBF98090DUL, 0x1C71C71CUL, 0xBFBC71C7UL, 0xD588F115UL, 0x3F93750AUL, + 0x3C0CA458UL, 0x3FAF9ADDUL, 0x3506AC12UL, 0x3F9EE711UL, 0x55555555UL, + 0x3FD55555UL }; ATTRIBUTE_ALIGNED(4) static const juint _rcp_table[] = { - 528611360, 3220144632, 2884679527, 3220082993, 1991868891, 3220024928, - 2298714891, 3219970134, 58835168, 3219918343, 3035110223, 3219869313, - 1617585086, 3219822831, 2500867033, 3219778702, 4241943008, 3219736752, - 258732970, 3219696825, 404232216, 3219658776, 2172167368, 3219622476, - 1544257904, 3219587808, 377579543, 3219554664, 1616385542, 3219522945, - 813783277, 3219492562, 3940743189, 3219463431, 2689777499, 3219435478, - 1700977147, 3219408632, 3169102082, 3219382828, 327235604, 3219358008, - 1244336319, 3219334115, 1300311200, 3219311099, 3095471925, 3219288912, - 2166487928, 3219267511, 2913108253, 3219246854, 293672978, 3219226904, - 288737297, 3219207624, 1810275472, 3219188981, 174592167, 3219170945, - 3539053052, 3219153485, 2164392968, 3219136576 + 0x1F81F820UL, 0xBFEF81F8UL, 0xABF0B767UL, 0xBFEE9131UL, 0x76B981DBUL, 0xBFEDAE60UL, + 0x89039B0BUL, 0xBFECD856UL, 0x0381C0E0UL, 0xBFEC0E07UL, 0xB4E81B4FUL, 0xBFEB4E81UL, + 0x606A63BEUL, 0xBFEA98EFUL, 0x951033D9UL, 0xBFE9EC8EUL, 0xFCD6E9E0UL, 0xBFE948B0UL, + 0x0F6BF3AAUL, 0xBFE8ACB9UL, 0x18181818UL, 0xBFE81818UL, 0x8178A4C8UL, 0xBFE78A4CUL, + 0x5C0B8170UL, 0xBFE702E0UL, 0x16816817UL, 0xBFE68168UL, 0x60581606UL, 0xBFE60581UL, + 0x308158EDUL, 0xBFE58ED2UL, 0xEAE2F815UL, 0xBFE51D07UL, 0xA052BF5BUL, 0xBFE4AFD6UL, + 0x6562D9FBUL, 0xBFE446F8UL, 0xBCE4A902UL, 0xBFE3E22CUL, 0x13813814UL, 0xBFE38138UL, + 0x4A2B10BFUL, 0xBFE323E3UL, 0x4D812CA0UL, 0xBFE2C9FBUL, 0xB8812735UL, 0xBFE27350UL, + 0x8121FB78UL, 0xBFE21FB7UL, 0xADA2811DUL, 0xBFE1CF06UL, 0x11811812UL, 0xBFE18118UL, + 0x1135C811UL, 0xBFE135C8UL, 0x6BE69C90UL, 0xBFE0ECF5UL, 0x0A6810A7UL, 0xBFE0A681UL, + 0xD2F1A9FCUL, 0xBFE0624DUL, 0x81020408UL, 0xBFE02040UL }; ATTRIBUTE_ALIGNED(4) static const juint _cbrt_table[] = { - 572345495, 1072698681, 1998204467, 1072709382, 3861501553, 1072719872, - 2268192434, 1072730162, 2981979308, 1072740260, 270859143, 1072750176, - 2958651392, 1072759916, 313113243, 1072769490, 919449400, 1072778903, - 2809328903, 1072788162, 2222981587, 1072797274, 2352530781, 1072806244, - 594152517, 1072815078, 1555767199, 1072823780, 4282421314, 1072832355, - 2355578597, 1072840809, 1162590619, 1072849145, 797864051, 1072857367, - 431273680, 1072865479, 2669831148, 1072873484, 733477752, 1072881387, - 4280220604, 1072889189, 801961634, 1072896896, 2915370760, 1072904508, - 1159613482, 1072912030, 2689944798, 1072919463, 1248687822, 1072926811, - 2967951030, 1072934075, 630170432, 1072941259, 3760898254, 1072948363, - 0, 1072955392, 2370273294, 1072962345, 1261754802, 1072972640, - 546334065, 1072986123, 1054893830, 1072999340, 1571187597, 1073012304, - 1107975175, 1073025027, 3606909377, 1073037519, 1113616747, 1073049792, - 4154744632, 1073061853, 3358931423, 1073073713, 4060702372, 1073085379, - 747576176, 1073096860, 3023138255, 1073108161, 1419988548, 1073119291, - 1914185305, 1073130255, 294389948, 1073141060, 3761802570, 1073151710, - 978281566, 1073162213, 823148820, 1073172572, 2420954441, 1073182792, - 3815449908, 1073192878, 2046058587, 1073202835, 1807524753, 1073212666, - 2628681401, 1073222375, 3225667357, 1073231966, 1555307421, 1073241443, - 3454043099, 1073250808, 1208137896, 1073260066, 3659916772, 1073269218, - 1886261264, 1073278269, 3593647839, 1073287220, 3086012205, 1073296075, - 2769796922, 1073304836, 888716057, 1073317807, 2201465623, 1073334794, - 164369365, 1073351447, 3462666733, 1073367780, 2773905457, 1073383810, - 1342879088, 1073399550, 2543933975, 1073415012, 1684477781, 1073430209, - 3532178543, 1073445151, 1147747300, 1073459850, 1928031793, 1073474314, - 2079717015, 1073488553, 4016765315, 1073502575, 3670431139, 1073516389, - 3549227225, 1073530002, 11637607, 1073543422, 588220169, 1073556654, - 2635407503, 1073569705, 2042029317, 1073582582, 1925128962, 1073595290, - 4136375664, 1073607834, 759964600, 1073620221, 4257606771, 1073632453, - 297278907, 1073644538, 3655053093, 1073656477, 2442253172, 1073668277, - 1111876799, 1073679941, 3330973139, 1073691472, 3438879452, 1073702875, - 3671565478, 1073714153, 1317849547, 1073725310, 1642364115, 1073736348 + 0x221D4C97UL, 0x3FF01539UL, 0x771A2E33UL, 0x3FF03F06UL, 0xE629D671UL, 0x3FF06800UL, + 0x8731DEB2UL, 0x3FF09032UL, 0xB1BD64ACUL, 0x3FF0B7A4UL, 0x1024FB87UL, 0x3FF0DE60UL, + 0xB0597000UL, 0x3FF1046CUL, 0x12A9BA9BUL, 0x3FF129D2UL, 0x36CDAF38UL, 0x3FF14E97UL, + 0xA772F507UL, 0x3FF172C2UL, 0x848001D3UL, 0x3FF1965AUL, 0x8C38C55DUL, 0x3FF1B964UL, + 0x236A0C45UL, 0x3FF1DBE6UL, 0x5CBB1F9FUL, 0x3FF1FDE4UL, 0xFF409042UL, 0x3FF21F63UL, + 0x8C6746E5UL, 0x3FF24069UL, 0x454BB99BUL, 0x3FF260F9UL, 0x2F8E7073UL, 0x3FF28117UL, + 0x19B4B6D0UL, 0x3FF2A0C7UL, 0x9F2263ECUL, 0x3FF2C00CUL, 0x2BB7FB78UL, 0x3FF2DEEBUL, + 0xFF1EFBBCUL, 0x3FF2FD65UL, 0x2FCCF6A2UL, 0x3FF31B80UL, 0xADC50708UL, 0x3FF3393CUL, + 0x451E4C2AUL, 0x3FF3569EUL, 0xA0554CDEUL, 0x3FF373A7UL, 0x4A6D76CEUL, 0x3FF3905BUL, + 0xB0E756B6UL, 0x3FF3ACBBUL, 0x258FA340UL, 0x3FF3C8CBUL, 0xE02AC0CEUL, 0x3FF3E48BUL, + 0x00000000UL, 0x3FF40000UL, 0x8D47800EUL, 0x3FF41B29UL, 0x4B34D9B2UL, 0x3FF44360UL, + 0x20906571UL, 0x3FF4780BUL, 0x3EE06706UL, 0x3FF4ABACUL, 0x5DA66B8DUL, 0x3FF4DE50UL, + 0x420A5C07UL, 0x3FF51003UL, 0xD6FD11C1UL, 0x3FF540CFUL, 0x4260716BUL, 0x3FF570C0UL, + 0xF7A45F38UL, 0x3FF59FDDUL, 0xC83539DFUL, 0x3FF5CE31UL, 0xF20966A4UL, 0x3FF5FBC3UL, + 0x2C8F1B70UL, 0x3FF6289CUL, 0xB4316DCFUL, 0x3FF654C1UL, 0x54A34E44UL, 0x3FF6803BUL, + 0x72182659UL, 0x3FF6AB0FUL, 0x118C08BCUL, 0x3FF6D544UL, 0xE0388D4AUL, 0x3FF6FEDEUL, + 0x3A4F645EUL, 0x3FF727E5UL, 0x31104114UL, 0x3FF7505CUL, 0x904CD549UL, 0x3FF77848UL, + 0xE36B2534UL, 0x3FF79FAEUL, 0x79F4605BUL, 0x3FF7C693UL, 0x6BBCA391UL, 0x3FF7ECFAUL, + 0x9CAE7EB9UL, 0x3FF812E7UL, 0xC043C71DUL, 0x3FF8385EUL, 0x5CB41B9DUL, 0x3FF85D63UL, + 0xCDE083DBUL, 0x3FF881F8UL, 0x4802B8A8UL, 0x3FF8A622UL, 0xDA25E5E4UL, 0x3FF8C9E2UL, + 0x706E1010UL, 0x3FF8ED3DUL, 0xD632B6DFUL, 0x3FF91034UL, 0xB7F0CF2DUL, 0x3FF932CBUL, + 0xA517BF3AUL, 0x3FF95504UL, 0x34F8BB19UL, 0x3FF987AFUL, 0x8337B317UL, 0x3FF9CA0AUL, + 0x09CC13D5UL, 0x3FFA0B17UL, 0xCE6419EDUL, 0x3FFA4AE4UL, 0xA5567031UL, 0x3FFA8982UL, + 0x500AB570UL, 0x3FFAC6FEUL, 0x97A15A17UL, 0x3FFB0364UL, 0x64671755UL, 0x3FFB3EC1UL, + 0xD288C46FUL, 0x3FFB791FUL, 0x44693BE4UL, 0x3FFBB28AUL, 0x72EB6E31UL, 0x3FFBEB0AUL, + 0x7BF5F697UL, 0x3FFC22A9UL, 0xEF6AF983UL, 0x3FFC596FUL, 0xDAC655A3UL, 0x3FFC8F65UL, + 0xD38CE8D9UL, 0x3FFCC492UL, 0x00B19367UL, 0x3FFCF8FEUL, 0x230F8709UL, 0x3FFD2CAEUL, + 0x9D15208FUL, 0x3FFD5FA9UL, 0x79B6E505UL, 0x3FFD91F6UL, 0x72BF2302UL, 0x3FFDC39AUL, + 0xF68C1570UL, 0x3FFDF49AUL, 0x2D4C23B8UL, 0x3FFE24FDUL, 0xFDC5EC73UL, 0x3FFE54C5UL, + 0x11B81DBBUL, 0x3FFE83FAUL, 0xD9DBAF25UL, 0x3FFEB29DUL, 0x9191D374UL, 0x3FFEE0B5UL, + 0x4245E4BFUL, 0x3FFF0E45UL, 0xC68A9DD3UL, 0x3FFF3B50UL, 0xCCF922DCUL, 0x3FFF67DBUL, + 0xDAD7A4A6UL, 0x3FFF93E9UL, 0x4E8CC9CBUL, 0x3FFFBF7EUL, 0x61E47CD3UL, 0x3FFFEA9CUL }; ATTRIBUTE_ALIGNED(4) static const juint _D_table[] = { - 4050900474, 1014427190, 1157977860, 1016444461, 1374568199, 1017271387, - 2809163288, 1016882676, 3742377377, 1013168191, 3101606597, 1017541672, - 65224358, 1017217597, 2691591250, 1017266643, 4020758549, 1017689313, - 1316310992, 1018030788, 1031537856, 1014090882, 3261395239, 1016413641, - 886424999, 1016313335, 3114776834, 1014195875, 1681120620, 1017825416, - 1329600273, 1016625740, 465474623, 1017097119, 4251633980, 1017169077, - 1986990133, 1017710645, 752958613, 1017159641, 2216216792, 1018020163, - 4282860129, 1015924861, 1557627859, 1016039538, 3889219754, 1018086237, - 3684996408, 1017353275, 723532103, 1017717141, 2951149676, 1012528470, - 831890937, 1017830553, 1031212645, 1017387331, 2741737450, 1017604974, - 2863311531, 1003776682, 4276736099, 1013153088, 4111778382, 1015673686, - 1728065769, 1016413986, 2708718031, 1018078833, 1069335005, 1015291224, - 700037144, 1016482032, 2904566452, 1017226861, 4074156649, 1017622651, - 25019565, 1015245366, 3601952608, 1015771755, 3267129373, 1017904664, - 503203103, 1014921629, 2122011730, 1018027866, 3927295461, 1014189456, - 2790625147, 1016024251, 1330460186, 1016940346, 4033568463, 1015538390, - 3695818227, 1017509621, 257573361, 1017208868, 3227697852, 1017337964, - 234118548, 1017169577, 4009025803, 1017278524, 1948343394, 1017749310, - 678398162, 1018144239, 3083864863, 1016669086, 2415453452, 1017890370, - 175467344, 1017330033, 3197359580, 1010339928, 2071276951, 1015941358, - 268372543, 1016737773, 938132959, 1017389108, 1816750559, 1017337448, - 4119203749, 1017152174, 2578653878, 1013108497, 2470331096, 1014678606, - 123855735, 1016553320, 1265650889, 1014782687, 3414398172, 1017182638, - 1040773369, 1016158401, 3483628886, 1016886550, 4140499405, 1016191425, - 3893477850, 1016964495, 3935319771, 1009634717, 2978982660, 1015027112, - 2452709923, 1017990229, 3190365712, 1015835149, 4237588139, 1015832925, - 2610678389, 1017962711, 2127316774, 1017405770, 824267502, 1017959463, - 2165924042, 1017912225, 2774007076, 1013257418, 4123916326, 1017582284, - 1976417958, 1016959909, 4092806412, 1017711279, 119251817, 1015363631, - 3475418768, 1017675415, 1972580503, 1015470684, 815541017, 1017517969, - 2429917451, 1017397776, 4062888482, 1016749897, 68284153, 1017925678, - 2207779246, 1016320298, 1183466520, 1017408657, 143326427, 1017060403 + 0xF173D5FAUL, 0x3C76EE36UL, 0x45055704UL, 0x3C95B62DUL, 0x51EE3F07UL, 0x3CA2545BUL, + 0xA7706E18UL, 0x3C9C65F4UL, 0xDF1025A1UL, 0x3C63B83FUL, 0xB8DEC2C5UL, 0x3CA67428UL, + 0x03E33EA6UL, 0x3CA1823DUL, 0xA06E6C52UL, 0x3CA241D3UL, 0xEFA7E815UL, 0x3CA8B4E1UL, + 0x4E754FD0UL, 0x3CADEAC4UL, 0x3D7C04C0UL, 0x3C71CC82UL, 0xC264F127UL, 0x3C953DC9UL, + 0x34D5C5A7UL, 0x3C93B5F7UL, 0xB9A7B902UL, 0x3C7366A3UL, 0x6433DD6CUL, 0x3CAAC888UL, + 0x4F401711UL, 0x3C987A4CUL, 0x1BBE943FUL, 0x3C9FAB9FUL, 0xFD6AC93CUL, 0x3CA0C4B5UL, + 0x766F1035UL, 0x3CA90835UL, 0x2CE13C95UL, 0x3CA09FD9UL, 0x8418C8D8UL, 0x3CADC143UL, + 0xFF474261UL, 0x3C8DC87DUL, 0x5CD783D3UL, 0x3C8F8872UL, 0xE7D0C8AAUL, 0x3CAEC35DUL, + 0xDBA49538UL, 0x3CA3943BUL, 0x2B203947UL, 0x3CA92195UL, 0xAFE6F86CUL, 0x3C59F556UL, + 0x3195A5F9UL, 0x3CAADC99UL, 0x3D770E65UL, 0x3CA41943UL, 0xA36B97EAUL, 0x3CA76B6EUL, + 0xAAAAAAABUL, 0x3BD46AAAUL, 0xFEE9D063UL, 0x3C637D40UL, 0xF514C24EUL, 0x3C89F356UL, + 0x670030E9UL, 0x3C953F22UL, 0xA173C1CFUL, 0x3CAEA671UL, 0x3FBCC1DDUL, 0x3C841D58UL, + 0x29B9B818UL, 0x3C9648F0UL, 0xAD202AB4UL, 0x3CA1A66DUL, 0xF2D6B269UL, 0x3CA7B07BUL, + 0x017DC4ADUL, 0x3C836A36UL, 0xD6B16F60UL, 0x3C8B726BUL, 0xC2BC701DUL, 0x3CABFE18UL, + 0x1DFE451FUL, 0x3C7E799DUL, 0x7E7B5452UL, 0x3CADDF5AUL, 0xEA15C5E5UL, 0x3C734D90UL, + 0xA6558F7BUL, 0x3C8F4CBBUL, 0x4F4D361AUL, 0x3C9D473AUL, 0xF06B5ECFUL, 0x3C87E2D6UL, + 0xDC49B5F3UL, 0x3CA5F6F5UL, 0x0F5A41F1UL, 0x3CA16024UL, 0xC062C2BCUL, 0x3CA3586CUL, + 0x0DF45D94UL, 0x3CA0C6A9UL, 0xEEF4E10BUL, 0x3CA2703CUL, 0x74215C62UL, 0x3CA99F3EUL, + 0x286F88D2UL, 0x3CAFA5EFUL, 0xB7D00B1FUL, 0x3C99239EUL, 0x8FF8E50CUL, 0x3CABC642UL, + 0x0A756B50UL, 0x3CA33971UL, 0xBE93D5DCUL, 0x3C389058UL, 0x7B752D97UL, 0x3C8E08EEUL, + 0x0FFF0A3FUL, 0x3C9A2FEDUL, 0x37EAC5DFUL, 0x3CA42034UL, 0x6C4969DFUL, 0x3CA35668UL, + 0xF5860FA5UL, 0x3CA082AEUL, 0x99B322B6UL, 0x3C62CF11UL, 0x933E42D8UL, 0x3C7AC44EUL, + 0x0761E377UL, 0x3C975F68UL, 0x4B704CC9UL, 0x3C7C5ADFUL, 0xCB8394DCUL, 0x3CA0F9AEUL, + 0x3E08F0F9UL, 0x3C9158C1UL, 0xCFA3F556UL, 0x3C9C7516UL, 0xF6CB01CDUL, 0x3C91D9C1UL, + 0xE811C1DAUL, 0x3C9DA58FUL, 0xEA9036DBUL, 0x3C2DCD9DUL, 0xB18FAB04UL, 0x3C8015A8UL, + 0x92316223UL, 0x3CAD4C55UL, 0xBE291E10UL, 0x3C8C6A0DUL, 0xFC9476ABUL, 0x3C8C615DUL, + 0x9B9BCA75UL, 0x3CACE0D7UL, 0x7ECC4726UL, 0x3CA4614AUL, 0x312152EEUL, 0x3CACD427UL, + 0x811960CAUL, 0x3CAC1BA1UL, 0xA557FD24UL, 0x3C6514CAUL, 0xF5CDF826UL, 0x3CA712CCUL, + 0x75CDBEA6UL, 0x3C9D93A5UL, 0xF3F3450CUL, 0x3CA90AAFUL, 0x071BA369UL, 0x3C85382FUL, + 0xCF26AE90UL, 0x3CA87E97UL, 0x75933097UL, 0x3C86DA5CUL, 0x309C2B19UL, 0x3CA61791UL, + 0x90D5990BUL, 0x3CA44210UL, 0xF22AC222UL, 0x3C9A5F49UL, 0x0411EEF9UL, 0x3CAC502EUL, + 0x839809AEUL, 0x3C93D12AUL, 0x468A4418UL, 0x3CA46C91UL, 0x088AFCDBUL, 0x3C9F1C33UL }; #define __ _masm-> diff --git a/src/hotspot/cpu/x86/stubGenerator_x86_64_tanh.cpp b/src/hotspot/cpu/x86/stubGenerator_x86_64_tanh.cpp index 372c4898f37..dce4fbfc455 100644 --- a/src/hotspot/cpu/x86/stubGenerator_x86_64_tanh.cpp +++ b/src/hotspot/cpu/x86/stubGenerator_x86_64_tanh.cpp @@ -77,226 +77,226 @@ ATTRIBUTE_ALIGNED(4) static const juint _HALFMASK[] = { - 4160749568, 2147483647 + 0xF8000000UL, 0x7FFFFFFFUL }; ATTRIBUTE_ALIGNED(4) static const juint _ONEMASK[] = { - 0, 1072693248 + 0x00000000UL, 0x3FF00000UL }; ATTRIBUTE_ALIGNED(4) static const juint _TWOMASK[] = { - 0, 1073741824 + 0x00000000UL, 0x40000000UL }; ATTRIBUTE_ALIGNED(16) static const juint _MASK3[] = { - 0, 4294967280, 0, 4294967280 + 0x00000000UL, 0xFFFFFFF0UL, 0x00000000UL, 0xFFFFFFF0UL }; ATTRIBUTE_ALIGNED(16) static const juint _RMASK[] = { - 4294705152, 4294967295, 4294705152, 4294967295 + 0xFFFC0000UL, 0xFFFFFFFFUL, 0xFFFC0000UL, 0xFFFFFFFFUL }; ATTRIBUTE_ALIGNED(16) static const juint _L2E[] = { - 1610612736, 1082594631, 4166901572, 1055174155 + 0x60000000UL, 0x40871547UL, 0xF85DDF44UL, 0x3EE4AE0BUL }; ATTRIBUTE_ALIGNED(16) static const juint _Shifter[] = { - 0, 1127743488, 0, 3275227136 + 0x00000000UL, 0x43380000UL, 0x00000000UL, 0xC3380000UL }; ATTRIBUTE_ALIGNED(16) static const juint _cv[] = { - 3884607281, 3168131199, 3607404735, 3190582024, 1874480759, - 1032041131, 4286760334, 1053736893, 4277811695, 3211144770, - 0, 0 + 0xE78A6731UL, 0xBCD5D87FUL, 0xD704A0BFUL, 0xBE2C6B08UL, 0x6FBA4E77UL, + 0x3D83B2ABUL, 0xFF82C58EUL, 0x3ECEBFBDUL, 0xFEFA39EFUL, 0xBF662E42UL, + 0x00000000UL, 0x00000000UL }; ATTRIBUTE_ALIGNED(4) static const juint _pv[] = { - 236289503, 1064135997, 463583772, 3215696314, 1441186365, - 3212977891, 286331153, 1069617425, 2284589306, 1066820852, - 1431655765, 3218429269 + 0x0E157DDFUL, 0x3F6D6D3DUL, 0x1BA1BA1CUL, 0xBFABA1BAUL, 0x55E6C23DUL, + 0xBF8226E3UL, 0x11111111UL, 0x3FC11111UL, 0x882C10FAUL, 0x3F9664F4UL, + 0x55555555UL, 0xBFD55555UL }; ATTRIBUTE_ALIGNED(16) static const juint _T2_neg_f[] = { - 0, 1072693248, 0, 0, 1797923801, 1072687577, - 1950547427, 1013229059, 730821105, 1072681922, 2523232743, 1012067188, - 915592468, 1072676282, 352947894, 3161024371, 2174652632, 1072670657, - 4087714590, 1014450259, 35929225, 1072665048, 2809788041, 3159436968, - 2912730644, 1072659453, 3490067722, 3163405074, 2038973688, 1072653874, - 892941374, 1016046459, 1533953344, 1072648310, 769171851, 1015665633, - 1222472308, 1072642761, 1054357470, 3161021018, 929806999, 1072637227, - 3205336643, 1015259557, 481706282, 1072631708, 1696079173, 3162710528, - 3999357479, 1072626203, 2258941616, 1015924724, 2719515920, 1072620714, - 2760332941, 1015137933, 764307441, 1072615240, 3021057420, 3163329523, - 2256325230, 1072609780, 580117746, 1015317295, 2728693978, 1072604335, - 396109971, 3163462691, 2009970496, 1072598905, 2159039665, 3162572948, - 4224142467, 1072593489, 3389820386, 1015207202, 610758006, 1072588089, - 1965209397, 3161866232, 3884662774, 1072582702, 2158611599, 1014210185, - 991358482, 1072577331, 838715019, 3163157668, 351641897, 1072571974, - 2172261526, 3163010599, 1796832535, 1072566631, 3176955716, 3160585513, - 863738719, 1072561303, 1326992220, 3162613197, 1679558232, 1072555989, - 2390342287, 3163333970, 4076975200, 1072550689, 2029000899, 1015208535, - 3594158869, 1072545404, 2456521700, 3163256561, 64696965, 1072540134, - 1768797490, 1015816960, 1912561781, 1072534877, 3147495102, 1015678253, - 382305176, 1072529635, 2347622376, 3162578625, 3898795731, 1072524406, - 1249994144, 1011869818, 3707479175, 1072519192, 3613079303, 1014164738, - 3939148246, 1072513992, 3210352148, 1015274323, 135105010, 1072508807, - 1906148728, 3163375739, 721996136, 1072503635, 563754734, 1015371318, - 1242007932, 1072498477, 1132034716, 3163339831, 1532734324, 1072493333, - 3094216535, 3163162857, 1432208378, 1072488203, 1401068914, 3162363963, - 778901109, 1072483087, 2248183955, 3161268751, 3706687593, 1072477984, - 3521726940, 1013253067, 1464976603, 1072472896, 3507292405, 3161977534, - 2483480501, 1072467821, 1216371780, 1013034172, 2307442995, 1072462760, - 3190117721, 3162404539, 777507147, 1072457713, 4282924205, 1015187533, - 2029714210, 1072452679, 613660079, 1015099143, 1610600570, 1072447659, - 3766732298, 1015760183, 3657065772, 1072442652, 399025623, 3162957078, - 3716502172, 1072437659, 2303740125, 1014042725, 1631695677, 1072432680, - 2717633076, 3162344026, 1540824585, 1072427714, 1064017011, 3163487690, - 3287523847, 1072422761, 1625971539, 3157009955, 2420883922, 1072417822, - 2049810052, 1014119888, 3080351519, 1072412896, 3379126788, 3157218001, - 815859274, 1072407984, 240396590, 3163487443, 4062661092, 1072403084, - 1422616006, 3163255318, 4076559943, 1072398198, 2119478331, 3160758351, - 703710506, 1072393326, 1384660846, 1015195891, 2380618042, 1072388466, - 3149557219, 3163320799, 364333489, 1072383620, 3923737744, 3161421373, - 3092190715, 1072378786, 814012168, 3159523422, 1822067026, 1072373966, - 1241994956, 1015340290, 697153126, 1072369159, 1283515429, 3163283189, - 3861050111, 1072364364, 254893773, 3162813180, 2572866477, 1072359583, - 878562433, 1015521741, 977020788, 1072354815, 3065100517, 1015541563, - 3218338682, 1072350059, 3404164304, 3162477108, 557149882, 1072345317, - 3672720709, 1014537265, 1434058175, 1072340587, 251133233, 1015085769, - 1405169241, 1072335870, 2998539689, 3162830951, 321958744, 1072331166, - 3401933767, 1015794558, 2331271250, 1072326474, 812057446, 1012207446, - 2990417245, 1072321795, 3683467745, 3163369326, 2152073944, 1072317129, - 1486860576, 3163203456, 3964284211, 1072312475, 2111583915, 1015427164, - 3985553595, 1072307834, 4002146062, 1015834136, 2069751141, 1072303206, - 1562170675, 3162724681, 2366108318, 1072298590, 2867985102, 3161762254, - 434316067, 1072293987, 2028358766, 1013458122, 424392917, 1072289396, - 2749202995, 3162838718, 2191782032, 1072284817, 2960257726, 1013742662, - 1297350157, 1072280251, 1308022040, 3163412558, 1892288442, 1072275697, - 2446255666, 3162600381, 3833209506, 1072271155, 2722920684, 1013754842, - 2682146384, 1072266626, 2082178513, 3163363419, 2591453363, 1072262109, - 2132396182, 3159074198, 3418903055, 1072257604, 2527457337, 3160820604, - 727685349, 1072253112, 2038246809, 3162358742, 2966275557, 1072248631, - 2176155324, 3159842759, 1403662306, 1072244163, 2788809599, 3161671007, - 194117574, 1072239707, 777528612, 3163412089, 3492293770, 1072235262, - 2248032210, 1015386826, 2568320822, 1072230830, 2732824428, 1014352915, - 1577608921, 1072226410, 1875489510, 3162968394, 380978316, 1072222002, - 854188970, 3160462686, 3134592888, 1072217605, 4232266862, 1015991134, - 1110089947, 1072213221, 1451641639, 1015474673, 2759350287, 1072208848, - 1148526634, 1015894933, 3649726105, 1072204487, 4085036346, 1015649474, - 3643909174, 1072200138, 3537586109, 1014354647, 2604962541, 1072195801, - 2614425274, 3163539192, 396319521, 1072191476, 4172420816, 3159074632, - 1176749997, 1072187162, 2738998779, 3162035844, 515457527, 1072182860, - 836709333, 1015651226, 2571947539, 1072178569, 3558159064, 3163376669, - 2916157145, 1072174290, 219487565, 1015309367, 1413356050, 1072170023, - 1651349291, 3162668166, 2224145553, 1072165767, 3482522030, 3161489169, - 919555682, 1072161523, 3121969534, 1012948226, 1660913392, 1072157290, - 4218599604, 1015135707, 19972402, 1072153069, 3507899862, 1016009292, - 158781403, 1072148859, 2221464712, 3163286453, 1944781191, 1072144660, - 3993278767, 3161724279, 950803702, 1072140473, 1655364926, 1015237032, - 1339972927, 1072136297, 167908909, 1015572152, 2980802057, 1072132132, - 378619896, 1015773303, 1447192521, 1072127979, 1462857171, 3162514521, - 903334909, 1072123837, 1636462108, 1015039997, 1218806132, 1072119706, - 1818613052, 3162548441, 2263535754, 1072115586, 752233586, 3162639008, - 3907805044, 1072111477, 2257091225, 3161550407, 1727278727, 1072107380, - 3562710623, 1011471940, 4182873220, 1072103293, 629542646, 3161996303, - 2555984613, 1072099218, 2652555442, 3162552692, 1013258799, 1072095154, - 1748797611, 3160129082, 3721688645, 1072091100, 3069276937, 1015839401, - 1963711167, 1072087058, 1744767757, 3160574294, 4201977662, 1072083026, - 748330254, 1013594357, 1719614413, 1072079006, 330458198, 3163282740, - 2979960120, 1072074996, 2599109725, 1014498493, 3561793907, 1072070997, - 1157054053, 1011890350, 3339203574, 1072067009, 1483497780, 3162408754, - 2186617381, 1072063032, 2270764084, 3163272713, 4273770423, 1072059065, - 3383180809, 3163218901, 885834528, 1072055110, 1973258547, 3162261564, - 488188413, 1072051165, 3199821029, 1015564048, 2956612997, 1072047230, - 2118169751, 3162735553, 3872257780, 1072043306, 1253592103, 1015958334, - 3111574537, 1072039393, 2606161479, 3162759746, 551349105, 1072035491, - 3821916050, 3162106589, 363667784, 1072031599, 813753950, 1015785209, - 2425981843, 1072027717, 2830390851, 3163346599, 2321106615, 1072023846, - 2171176610, 1009535771, 4222122499, 1072019985, 1277378074, 3163256737, - 3712504873, 1072016135, 88491949, 1015427660, 671025100, 1072012296, - 3832014351, 3163022030, 3566716925, 1072008466, 1536826856, 1014142433, - 3689071823, 1072004647, 2321004996, 3162552716, 917841882, 1072000839, - 18715565, 1015659308, 3723038930, 1071997040, 378465264, 3162569582, - 3395129871, 1071993252, 4025345435, 3162335388, 4109806887, 1071989474, - 422403966, 1014469229, 1453150082, 1071985707, 498154669, 3161488062, - 3896463087, 1071981949, 1139797873, 3161233805, 2731501122, 1071978202, - 1774031855, 3162470021, 2135241198, 1071974465, 1236747871, 1013589147, - 1990012071, 1071970738, 3529070563, 3162813193, 2178460671, 1071967021, - 777878098, 3162842493, 2583551245, 1071963314, 3161094195, 1015606491, - 3088564500, 1071959617, 1762311517, 1015045673, 3577096743, 1071955930, - 2951496418, 1013793687, 3933059031, 1071952253, 2133366768, 3161531832, - 4040676318, 1071948586, 4090609238, 1015663458, 3784486610, 1071944929, - 1581883040, 3161698953, 3049340112, 1071941282, 3062915824, 1013170595, - 1720398391, 1071937645, 3980678963, 3163300080, 3978100823, 1071934017, - 3513027190, 1015845963, 1118294578, 1071930400, 2197495694, 3159909401, - 1617004845, 1071926792, 82804944, 1010342778, 1065662932, 1071923194, - 2533670915, 1014530238, 3645941911, 1071919605, 3814685081, 3161573341, - 654919306, 1071916027, 3232961757, 3163047469, 569847338, 1071912458, - 472945272, 3159290729, 3278348324, 1071908898, 3069497416, 1014750712, - 78413852, 1071905349, 4183226867, 3163017251, 3743175029, 1071901808, - 2072812490, 3162175075, 1276261410, 1071898278, 300981948, 1014684169, - 1156440435, 1071894757, 2351451249, 1013967056, 3272845541, 1071891245, - 928852419, 3163488248, 3219942644, 1071887743, 3798990616, 1015368806, - 887463927, 1071884251, 3596744163, 3160794166, 460407023, 1071880768, - 4237175092, 3163138469, 1829099622, 1071877294, 1016661181, 3163461005, - 589198666, 1071873830, 2664346172, 3163157962, 926591435, 1071870375, - 3208833762, 3162913514, 2732492859, 1071866929, 2691479646, 3162255684, - 1603444721, 1071863493, 1548633640, 3162201326, 1726216749, 1071860066, - 2466808228, 3161676405, 2992903935, 1071856648, 2218154406, 1015228193, - 1000925746, 1071853240, 1018491672, 3163309544, 4232894513, 1071849840, - 2383938684, 1014668519, 3991843581, 1071846450, 4092853457, 1014585763, - 171030293, 1071843070, 3526460132, 1014428778, 1253935211, 1071839698, - 1395382931, 3159702613, 2839424854, 1071836335, 1171596163, 1013041679, - 526652809, 1071832982, 4223459736, 1015879375, 2799960843, 1071829637, - 1423655381, 1015022151, 964107055, 1071826302, 2800439588, 3162833221, - 3504003472, 1071822975, 3594001060, 3157330652, 1724976915, 1071819658, - 420909223, 3163117379, 4112506593, 1071816349, 2947355221, 1014371048, - 1972484976, 1071813050, 675290301, 3161640050, 3790955393, 1071809759, - 2352942462, 3163180090, 874372905, 1071806478, 100263788, 1015940732, - 1709341917, 1071803205, 2571168217, 1014152499, 1897844341, 1071799941, - 1254300460, 1015275938, 1337108031, 1071796686, 3203724452, 1014677845, - 4219606026, 1071793439, 2434574742, 1014681548, 1853186616, 1071790202, - 3066496371, 1015656574, 2725843665, 1071786973, 1433917087, 1014838523, - 2440944790, 1071783753, 2492769774, 1014147454, 897099801, 1071780542, - 754756297, 1015241005, 2288159958, 1071777339, 2169144469, 1014876021, - 2218315341, 1071774145, 2694295388, 3163288868, 586995997, 1071770960, - 41662348, 3162627992, 1588871207, 1071767783, 143439582, 3162963416, - 828946858, 1071764615, 10642492, 1015939438, 2502433899, 1071761455, - 2148595913, 1015023991, 2214878420, 1071758304, 892270087, 3163116422, - 4162030108, 1071755161, 2763428480, 1015529349, 3949972341, 1071752027, - 2068408548, 1014913868, 1480023343, 1071748902, 2247196168, 1015327453, - 948735466, 1071745785, 3516338028, 3162574883, 2257959872, 1071742676, - 3802946148, 1012964927, 1014845819, 1071739576, 3117910646, 3161559105, - 1416741826, 1071736484, 2196380210, 1011413563, 3366293073, 1071733400, - 3119426314, 1014120554, 2471440686, 1071730325, 968836267, 3162214888, - 2930322912, 1071727258, 2599499422, 3162714047, 351405227, 1071724200, - 3125337328, 3159822479, 3228316108, 1071721149, 3010241991, 3158422804, - 2875075254, 1071718107, 4144233330, 3163333716, 3490863953, 1071715073, - 960797498, 3162948880, 685187902, 1071712048, 378731989, 1014843115, - 2952712987, 1071709030, 3293494651, 3160120301, 1608493509, 1071706021, - 3159622171, 3162807737, 852742562, 1071703020, 667253586, 1009793559, - 590962156, 1071700027, 3829346666, 3163275597, 728909815, 1071697042, - 383930225, 1015029468, 1172597893, 1071694065, 114433263, 1015347593, - 1828292879, 1071691096, 1255956747, 1015588398, 2602514713, 1071688135, - 2268929336, 1014354284, 3402036099, 1071685182, 405889334, 1015105656, - 4133881824, 1071682237, 2148155345, 3162931299, 410360776, 1071679301, - 1269990655, 1011975870, 728934454, 1071676372, 1413842688, 1014178612, - 702412510, 1071673451, 3803266087, 3162280415, 238821257, 1071670538, - 1469694871, 3162884987, 3541402996, 1071667632, 2759177317, 1014854626, - 1928746161, 1071664735, 983617676, 1014285177, 3899555717, 1071661845, - 427280750, 3162546972, 772914124, 1071658964, 4004372762, 1012230161, - 1048019041, 1071656090, 1398474845, 3160510595, 339411585, 1071653224, - 264588982, 3161636657, 2851812149, 1071650365, 2595802551, 1015767337, - 4200250559, 1071647514, 2808127345, 3161781938 + 0x00000000UL, 0x3FF00000UL, 0x00000000UL, 0x00000000UL, 0x6B2A23D9UL, 0x3FEFE9D9UL, + 0x7442FDE3UL, 0x3C64A603UL, 0x2B8F71F1UL, 0x3FEFD3C2UL, 0x966579E7UL, 0x3C52EB74UL, + 0x3692D514UL, 0x3FEFBDBAUL, 0x15098EB6UL, 0xBC696773UL, 0x819E90D8UL, 0x3FEFA7C1UL, + 0xF3A5931EUL, 0x3C774853UL, 0x02243C89UL, 0x3FEF91D8UL, 0xA779F689UL, 0xBC512EA8UL, + 0xAD9CBE14UL, 0x3FEF7BFDUL, 0xD006350AUL, 0xBC8DBB12UL, 0x798844F8UL, 0x3FEF6632UL, + 0x3539343EUL, 0x3C8FA37BUL, 0x5B6E4540UL, 0x3FEF5076UL, 0x2DD8A18BUL, 0x3C89D3E1UL, + 0x48DD7274UL, 0x3FEF3AC9UL, 0x3ED837DEUL, 0xBC695A5AUL, 0x376BBA97UL, 0x3FEF252BUL, + 0xBF0D8E43UL, 0x3C83A1A5UL, 0x1CB6412AUL, 0x3FEF0F9CUL, 0x65181D45UL, 0xBC832200UL, + 0xEE615A27UL, 0x3FEEFA1BUL, 0x86A4B6B0UL, 0x3C8DC7F4UL, 0xA2188510UL, 0x3FEEE4AAUL, + 0xA487568DUL, 0x3C81C68DUL, 0x2D8E67F1UL, 0x3FEECF48UL, 0xB411AD8CUL, 0xBC8C93F3UL, + 0x867CCA6EUL, 0x3FEEB9F4UL, 0x2293E4F2UL, 0x3C84832FUL, 0xA2A490DAUL, 0x3FEEA4AFUL, + 0x179C2893UL, 0xBC8E9C23UL, 0x77CDB740UL, 0x3FEE8F79UL, 0x80B054B1UL, 0xBC810894UL, + 0xFBC74C83UL, 0x3FEE7A51UL, 0xCA0C8DE2UL, 0x3C82D522UL, 0x24676D76UL, 0x3FEE6539UL, + 0x7522B735UL, 0xBC763FF8UL, 0xE78B3FF6UL, 0x3FEE502EUL, 0x80A9CC8FUL, 0x3C739E89UL, + 0x3B16EE12UL, 0x3FEE3B33UL, 0x31FDC68BUL, 0xBC89F4A4UL, 0x14F5A129UL, 0x3FEE2646UL, + 0x817A1496UL, 0xBC87B627UL, 0x6B197D17UL, 0x3FEE1167UL, 0xBD5C7F44UL, 0xBC62B529UL, + 0x337B9B5FUL, 0x3FEDFC97UL, 0x4F184B5CUL, 0xBC81A5CDUL, 0x641C0658UL, 0x3FEDE7D5UL, + 0x8E79BA8FUL, 0xBC8CA552UL, 0xF301B460UL, 0x3FEDD321UL, 0x78F018C3UL, 0x3C82DA57UL, + 0xD63A8315UL, 0x3FEDBE7CUL, 0x926B8BE4UL, 0xBC8B76F1UL, 0x03DB3285UL, 0x3FEDA9E6UL, + 0x696DB532UL, 0x3C8C2300UL, 0x71FF6075UL, 0x3FED955DUL, 0xBB9AF6BEUL, 0x3C8A052DUL, + 0x16C98398UL, 0x3FED80E3UL, 0x8BEDDFE8UL, 0xBC811EC1UL, 0xE862E6D3UL, 0x3FED6C76UL, + 0x4A8165A0UL, 0x3C4FE87AUL, 0xDCFBA487UL, 0x3FED5818UL, 0xD75B3707UL, 0x3C72ED02UL, + 0xEACAA1D6UL, 0x3FED43C8UL, 0xBF5A1614UL, 0x3C83DB53UL, 0x080D89F2UL, 0x3FED2F87UL, + 0x719D8578UL, 0xBC8D487BUL, 0x2B08C968UL, 0x3FED1B53UL, 0x219A36EEUL, 0x3C855636UL, + 0x4A07897CUL, 0x3FED072DUL, 0x43797A9CUL, 0xBC8CBC37UL, 0x5B5BAB74UL, 0x3FECF315UL, + 0xB86DFF57UL, 0xBC8A08E9UL, 0x555DC3FAUL, 0x3FECDF0BUL, 0x53829D72UL, 0xBC7DD83BUL, + 0x2E6D1675UL, 0x3FECCB0FUL, 0x86009093UL, 0xBC6D220FUL, 0xDCEF9069UL, 0x3FECB720UL, + 0xD1E949DCUL, 0x3C6503CBUL, 0x5751C4DBUL, 0x3FECA340UL, 0xD10D08F5UL, 0xBC77F2BEUL, + 0x9406E7B5UL, 0x3FEC8F6DUL, 0x48805C44UL, 0x3C61ACBCUL, 0x8988C933UL, 0x3FEC7BA8UL, + 0xBE255559UL, 0xBC7E76BBUL, 0x2E57D14BUL, 0x3FEC67F1UL, 0xFF483CADUL, 0x3C82884DUL, + 0x78FAFB22UL, 0x3FEC5447UL, 0x2493B5AFUL, 0x3C812F07UL, 0x5FFFD07AUL, 0x3FEC40ABUL, + 0xE083C60AUL, 0x3C8B4537UL, 0xD9FA652CUL, 0x3FEC2D1CUL, 0x17C8A5D7UL, 0xBC86E516UL, + 0xDD85529CUL, 0x3FEC199BUL, 0x895048DDUL, 0x3C711065UL, 0x6141B33DUL, 0x3FEC0628UL, + 0xA1FBCA34UL, 0xBC7D8A5AUL, 0x5BD71E09UL, 0x3FEBF2C2UL, 0x3F6B9C73UL, 0xBC8EFDCAUL, + 0xC3F3A207UL, 0x3FEBDF69UL, 0x60EA5B53UL, 0xBC2C2623UL, 0x904BC1D2UL, 0x3FEBCC1EUL, + 0x7A2D9E84UL, 0x3C723DD0UL, 0xB79A6F1FUL, 0x3FEBB8E0UL, 0xC9696204UL, 0xBC2F52D1UL, + 0x30A1064AUL, 0x3FEBA5B0UL, 0x0E54292EUL, 0xBC8EFCD3UL, 0xF22749E4UL, 0x3FEB928CUL, + 0x54CB65C6UL, 0xBC8B7216UL, 0xF2FB5E47UL, 0x3FEB7F76UL, 0x7E54AC3BUL, 0xBC65584FUL, + 0x29F1C52AUL, 0x3FEB6C6EUL, 0x52883F6EUL, 0x3C82A8F3UL, 0x8DE5593AUL, 0x3FEB5972UL, + 0xBBBA6DE3UL, 0xBC8C71DFUL, 0x15B749B1UL, 0x3FEB4684UL, 0xE9DF7C90UL, 0xBC6F763DUL, + 0xB84F15FBUL, 0x3FEB33A2UL, 0x3084D708UL, 0xBC52805EUL, 0x6C9A8952UL, 0x3FEB20CEUL, + 0x4A0756CCUL, 0x3C84DD02UL, 0x298DB666UL, 0x3FEB0E07UL, 0x4C80E425UL, 0xBC8BDEF5UL, + 0xE622F2FFUL, 0x3FEAFB4CUL, 0x0F315ECDUL, 0xBC84B2FCUL, 0x995AD3ADUL, 0x3FEAE89FUL, + 0x345DCC81UL, 0x3C87A1CDUL, 0x3A3C2774UL, 0x3FEAD5FFUL, 0xB6B1B8E5UL, 0x3C87EF3BUL, + 0xBFD3F37AUL, 0x3FEAC36BUL, 0xCAE76CD0UL, 0xBC7F9234UL, 0x21356EBAUL, 0x3FEAB0E5UL, + 0xDAE94545UL, 0x3C789C31UL, 0x5579FDBFUL, 0x3FEA9E6BUL, 0x0EF7FD31UL, 0x3C80FAC9UL, + 0x53C12E59UL, 0x3FEA8BFEUL, 0xB2BA15A9UL, 0xBC84F867UL, 0x1330B358UL, 0x3FEA799EUL, + 0xCAC563C7UL, 0x3C8BCB7EUL, 0x8AF46052UL, 0x3FEA674AUL, 0x30670366UL, 0x3C550F56UL, + 0xB23E255DUL, 0x3FEA5503UL, 0xDB8D41E1UL, 0xBC8D2F6EUL, 0x80460AD8UL, 0x3FEA42C9UL, + 0x589FB120UL, 0xBC8AA780UL, 0xEC4A2D33UL, 0x3FEA309BUL, 0x7DDC36ABUL, 0x3C86305CUL, + 0xED8EB8BBUL, 0x3FEA1E7AUL, 0xEE8BE70EUL, 0x3C8C6618UL, 0x7B5DE565UL, 0x3FEA0C66UL, + 0x5D1CD533UL, 0xBC835949UL, 0x8D07F29EUL, 0x3FE9FA5EUL, 0xAAF1FACEUL, 0xBC74A9CEUL, + 0x19E32323UL, 0x3FE9E863UL, 0x78E64C6EUL, 0x3C6824CAUL, 0x194BB8D5UL, 0x3FE9D674UL, + 0xA3DD8233UL, 0xBC8516BEUL, 0x82A3F090UL, 0x3FE9C491UL, 0xB071F2BEUL, 0x3C6C7C46UL, + 0x4D53FE0DUL, 0x3FE9B2BBUL, 0x4DF6D518UL, 0xBC8DD84EUL, 0x70CA07BAUL, 0x3FE9A0F1UL, + 0x91CEE632UL, 0xBC8173BDUL, 0xE47A22A2UL, 0x3FE98F33UL, 0xA24C78ECUL, 0x3C6CABDAUL, + 0x9FDE4E50UL, 0x3FE97D82UL, 0x7C1B85D1UL, 0xBC8D185BUL, 0x9A7670B3UL, 0x3FE96BDDUL, + 0x7F19C896UL, 0xBC4BA596UL, 0xCBC8520FUL, 0x3FE95A44UL, 0x96A5F039UL, 0xBC664B7CUL, + 0x2B5F98E5UL, 0x3FE948B8UL, 0x797D2D99UL, 0xBC7DC3D6UL, 0xB0CDC5E5UL, 0x3FE93737UL, + 0x81B57EBCUL, 0xBC575FC7UL, 0x53AA2FE2UL, 0x3FE925C3UL, 0xA639DB7FUL, 0xBC73455FUL, + 0x0B91FFC6UL, 0x3FE9145BUL, 0x2E582524UL, 0xBC8DD679UL, 0xD0282C8AUL, 0x3FE902FEUL, + 0x85FE3FD2UL, 0x3C8592CAUL, 0x99157736UL, 0x3FE8F1AEUL, 0xA2E3976CUL, 0x3C75CC13UL, + 0x5E0866D9UL, 0x3FE8E06AUL, 0x6FC9B2E6UL, 0xBC87114AUL, 0x16B5448CUL, 0x3FE8CF32UL, + 0x32E9E3AAUL, 0xBC60D55EUL, 0xBAD61778UL, 0x3FE8BE05UL, 0xFC43446EUL, 0x3C8ECB5EUL, + 0x422AA0DBUL, 0x3FE8ACE5UL, 0x56864B27UL, 0x3C86E9F1UL, 0xA478580FUL, 0x3FE89BD0UL, + 0x4475202AUL, 0x3C8D5395UL, 0xD98A6699UL, 0x3FE88AC7UL, 0xF37CB53AUL, 0x3C8994C2UL, + 0xD931A436UL, 0x3FE879CAUL, 0xD2DB47BDUL, 0x3C75D2D7UL, 0x9B4492EDUL, 0x3FE868D9UL, + 0x9BD4F6BAUL, 0xBC8FC6F8UL, 0x179F5B21UL, 0x3FE857F4UL, 0xF8B216D0UL, 0xBC4BA748UL, + 0x4623C7ADUL, 0x3FE8471AUL, 0xA341CDFBUL, 0xBC78D684UL, 0x1EB941F7UL, 0x3FE8364CUL, + 0x31DF2BD5UL, 0x3C899B9AUL, 0x994CCE13UL, 0x3FE82589UL, 0xD41532D8UL, 0xBC8D4C1DUL, + 0xADD106D9UL, 0x3FE814D2UL, 0x0D151D4DUL, 0x3C846437UL, 0x543E1A12UL, 0x3FE80427UL, + 0x626D972BUL, 0xBC827C86UL, 0x8491C491UL, 0x3FE7F387UL, 0xCF9311AEUL, 0xBC707F11UL, + 0x36CF4E62UL, 0x3FE7E2F3UL, 0xBA15797EUL, 0x3C605D02UL, 0x62FF86F0UL, 0x3FE7D26AUL, + 0xFB72B8B4UL, 0x3C81BDDBUL, 0x0130C132UL, 0x3FE7C1EDUL, 0xD1164DD6UL, 0x3C8F124CUL, + 0x0976CFDBUL, 0x3FE7B17BUL, 0x8468DC88UL, 0xBC8BEBB5UL, 0x73EB0187UL, 0x3FE7A114UL, + 0xEE04992FUL, 0xBC741577UL, 0x38AC1CF6UL, 0x3FE790B9UL, 0x62AADD3EUL, 0x3C8349A8UL, + 0x4FDE5D3FUL, 0x3FE78069UL, 0x0A02162DUL, 0x3C8866B8UL, 0xB1AB6E09UL, 0x3FE77024UL, + 0x169147F8UL, 0x3C8B7877UL, 0x564267C9UL, 0x3FE75FEBUL, 0x57316DD3UL, 0xBC802459UL, + 0x35D7CBFDUL, 0x3FE74FBDUL, 0x618A6E1CUL, 0x3C8047FDUL, 0x48A58174UL, 0x3FE73F9AUL, + 0x6C65D53CUL, 0xBC80A8D9UL, 0x86EAD08AUL, 0x3FE72F82UL, 0x2CD62C72UL, 0xBC820AA0UL, + 0xE8EC5F74UL, 0x3FE71F75UL, 0x86887A99UL, 0xBC716E47UL, 0x66F42E87UL, 0x3FE70F74UL, + 0xD45AA65FUL, 0x3C49D644UL, 0xF9519484UL, 0x3FE6FF7DUL, 0x25860EF6UL, 0xBC783C0FUL, + 0x98593AE5UL, 0x3FE6EF92UL, 0x9E1AC8B2UL, 0xBC80B974UL, 0x3C651A2FUL, 0x3FE6DFB2UL, + 0x683C88ABUL, 0xBC5BBE3AUL, 0xDDD47645UL, 0x3FE6CFDCUL, 0xB6F17309UL, 0x3C8C7AA9UL, + 0x750BDABFUL, 0x3FE6C012UL, 0x67FF0B0DUL, 0xBC628956UL, 0xFA75173EUL, 0x3FE6B052UL, + 0x2C9A9D0EUL, 0x3C6A38F5UL, 0x667F3BCDUL, 0x3FE6A09EUL, 0x13B26456UL, 0xBC8BDD34UL, + 0xB19E9538UL, 0x3FE690F4UL, 0x9AEB445DUL, 0x3C7804BDUL, 0xD44CA973UL, 0x3FE68155UL, + 0x44F73E65UL, 0x3C5038AEUL, 0xC70833F6UL, 0x3FE671C1UL, 0x586C6134UL, 0xBC7E8732UL, + 0x82552225UL, 0x3FE66238UL, 0x87591C34UL, 0xBC8BB609UL, 0xFEBC8FB7UL, 0x3FE652B9UL, + 0xC9A73E09UL, 0xBC8AE3D5UL, 0x34CCC320UL, 0x3FE64346UL, 0x759D8933UL, 0xBC7C483CUL, + 0x1D1929FDUL, 0x3FE633DDUL, 0xBEB964E5UL, 0x3C884710UL, 0xB03A5585UL, 0x3FE6247EUL, + 0x7E40B497UL, 0xBC8383C1UL, 0xE6CDF6F4UL, 0x3FE6152AUL, 0x4AB84C27UL, 0x3C8E4B3EUL, + 0xB976DC09UL, 0x3FE605E1UL, 0x9B56DE47UL, 0xBC83E242UL, 0x20DCEB71UL, 0x3FE5F6A3UL, + 0xE3CDCF92UL, 0xBC79EADDUL, 0x15AD2148UL, 0x3FE5E76FUL, 0x3080E65EUL, 0x3C8BA6F9UL, + 0x90998B93UL, 0x3FE5D845UL, 0xA8B45643UL, 0xBC8CD6A7UL, 0x8A5946B7UL, 0x3FE5C926UL, + 0x816986A2UL, 0x3C2C4B1BUL, 0xFBA87A03UL, 0x3FE5BA11UL, 0x4C233E1AUL, 0xBC8B77A1UL, + 0xDD485429UL, 0x3FE5AB07UL, 0x054647ADUL, 0x3C86324CUL, 0x27FF07CCUL, 0x3FE59C08UL, + 0xE467E60FUL, 0xBC87E2CEUL, 0xD497C7FDUL, 0x3FE58D12UL, 0x5B9A1DE8UL, 0x3C7295E1UL, + 0xDBE2C4CFUL, 0x3FE57E27UL, 0x8A57B9C4UL, 0xBC80B98CUL, 0x36B527DAUL, 0x3FE56F47UL, + 0x011D93ADUL, 0x3C89BB2CUL, 0xDDE910D2UL, 0x3FE56070UL, 0x168EEBF0UL, 0xBC80FB6EUL, + 0xCA5D920FUL, 0x3FE551A4UL, 0xEFEDE59BUL, 0xBC7D689CUL, 0xF4F6AD27UL, 0x3FE542E2UL, + 0x192D5F7EUL, 0x3C77926DUL, 0x569D4F82UL, 0x3FE5342BUL, 0x1DB13CADUL, 0xBC707ABEUL, + 0xE83F4EEFUL, 0x3FE5257DUL, 0x43EFEF71UL, 0xBC6C998DUL, 0xA2CF6642UL, 0x3FE516DAUL, + 0x69BD93EFUL, 0xBC7F7685UL, 0x7F4531EEUL, 0x3FE50841UL, 0x49B7465FUL, 0x3C6A249BUL, + 0x769D2CA7UL, 0x3FE4F9B2UL, 0xD25957E3UL, 0xBC84B309UL, 0x81D8ABFFUL, 0x3FE4EB2DUL, + 0x2E5D7A52UL, 0xBC85257DUL, 0x99FDDD0DUL, 0x3FE4DCB2UL, 0xBC6A7833UL, 0x3C88ECDBUL, + 0xB817C114UL, 0x3FE4CE41UL, 0x690ABD5DUL, 0x3C805E29UL, 0xD5362A27UL, 0x3FE4BFDAUL, + 0xAFEC42E2UL, 0x3C6D4397UL, 0xEA6DB7D7UL, 0x3FE4B17DUL, 0x7F2897F0UL, 0xBC7125B8UL, + 0xF0D7D3DEUL, 0x3FE4A32AUL, 0xF3D1BE56UL, 0x3C89CB62UL, 0xE192AED2UL, 0x3FE494E1UL, + 0x5E499EA0UL, 0xBC73B289UL, 0xB5C13CD0UL, 0x3FE486A2UL, 0xB69062F0UL, 0x3C63C1A3UL, + 0x668B3237UL, 0x3FE4786DUL, 0xED445733UL, 0xBC8C20F0UL, 0xED1D0057UL, 0x3FE46A41UL, + 0xD1648A76UL, 0x3C8C944BUL, 0x42A7D232UL, 0x3FE45C20UL, 0x82FB1F8EUL, 0xBC586419UL, + 0x6061892DUL, 0x3FE44E08UL, 0x04EF80D0UL, 0x3C389B7AUL, 0x3F84B9D4UL, 0x3FE43FFAUL, + 0x9704C003UL, 0x3C7880BEUL, 0xD950A897UL, 0x3FE431F5UL, 0xE35F7999UL, 0xBC71C7DDUL, + 0x2709468AUL, 0x3FE423FBUL, 0xC0B314DDUL, 0xBC88462DUL, 0x21F72E2AUL, 0x3FE4160AUL, + 0x1C309278UL, 0xBC4EF369UL, 0xC367A024UL, 0x3FE40822UL, 0xB6F4D048UL, 0x3C7BDDF8UL, + 0x04AC801CUL, 0x3FE3FA45UL, 0xF956F9F3UL, 0xBC87D023UL, 0xDF1C5175UL, 0x3FE3EC70UL, + 0x7B8C9BCAUL, 0xBC7AF663UL, 0x4C123422UL, 0x3FE3DEA6UL, 0x11F09EBCUL, 0x3C7ADA09UL, + 0x44EDE173UL, 0x3FE3D0E5UL, 0x8C284C71UL, 0x3C6FE8D0UL, 0xC313A8E5UL, 0x3FE3C32DUL, + 0x375D29C3UL, 0xBC8EFFF8UL, 0xBFEC6CF4UL, 0x3FE3B57FUL, 0xE26FFF18UL, 0x3C854C66UL, + 0x34E59FF7UL, 0x3FE3A7DBUL, 0xD661F5E3UL, 0xBC65E436UL, 0x1B7140EFUL, 0x3FE39A40UL, + 0xFC8E2934UL, 0xBC89A9A5UL, 0x6D05D866UL, 0x3FE38CAEUL, 0x3C9904BDUL, 0xBC8E958DUL, + 0x231E754AUL, 0x3FE37F26UL, 0x9ECEB23CUL, 0xBC89F5CAUL, 0x373AA9CBUL, 0x3FE371A7UL, + 0xBF42EAE2UL, 0xBC863AEAUL, 0xA2DE883BUL, 0x3FE36431UL, 0xA06CB85EUL, 0xBC7C3144UL, + 0x5F929FF1UL, 0x3FE356C5UL, 0x5C4E4628UL, 0xBC7B5CEEUL, 0x66E3FA2DUL, 0x3FE34962UL, + 0x930881A4UL, 0xBC735A75UL, 0xB26416FFUL, 0x3FE33C08UL, 0x843659A6UL, 0x3C832721UL, + 0x3BA8EA32UL, 0x3FE32EB8UL, 0x3CB4F318UL, 0xBC8C45E8UL, 0xFC4CD831UL, 0x3FE32170UL, + 0x8E18047CUL, 0x3C7A9CE7UL, 0xEDEEB2FDUL, 0x3FE31432UL, 0xF3F3FCD1UL, 0x3C7959A3UL, + 0x0A31B715UL, 0x3FE306FEUL, 0xD23182E4UL, 0x3C76F46AUL, 0x4ABD886BUL, 0x3FE2F9D2UL, + 0x532BDA93UL, 0xBC553C55UL, 0xA93E2F56UL, 0x3FE2ECAFUL, 0x45D52383UL, 0x3C61CA0FUL, + 0x1F641589UL, 0x3FE2DF96UL, 0xFBBCE198UL, 0x3C8D16CFUL, 0xA6E4030BUL, 0x3FE2D285UL, + 0x54DB41D5UL, 0x3C800247UL, 0x39771B2FUL, 0x3FE2C57EUL, 0xA6EB5124UL, 0xBC850145UL, + 0xD0DAD990UL, 0x3FE2B87FUL, 0xD6381AA4UL, 0xBC310ADCUL, 0x66D10F13UL, 0x3FE2AB8AUL, + 0x191690A7UL, 0xBC895743UL, 0xF51FDEE1UL, 0x3FE29E9DUL, 0xAFAD1255UL, 0x3C7612E8UL, + 0x7591BB70UL, 0x3FE291BAUL, 0x28401CBDUL, 0xBC72CC72UL, 0xE1F56381UL, 0x3FE284DFUL, + 0x8C3F0D7EUL, 0xBC8A4C3AUL, 0x341DDF29UL, 0x3FE2780EUL, 0x05F9E76CUL, 0x3C8E067CUL, + 0x65E27CDDUL, 0x3FE26B45UL, 0x9940E9D9UL, 0x3C72BD33UL, 0x711ECE75UL, 0x3FE25E85UL, + 0x4AC31B2CUL, 0x3C83E1A2UL, 0x4FB2A63FUL, 0x3FE251CEUL, 0xBEF4F4A4UL, 0x3C7AC155UL, + 0xFB82140AUL, 0x3FE2451FUL, 0x911CA996UL, 0x3C7ACFCCUL, 0x6E756238UL, 0x3FE2387AUL, + 0xB6C70573UL, 0x3C89B07EUL, 0xA27912D1UL, 0x3FE22BDDUL, 0x5577D69FUL, 0x3C7D34FBUL, + 0x917DDC96UL, 0x3FE21F49UL, 0x9494A5EEUL, 0x3C72A97EUL, 0x3578A819UL, 0x3FE212BEUL, + 0x2CFCAAC9UL, 0x3C83592DUL, 0x88628CD6UL, 0x3FE2063BUL, 0x814A8495UL, 0x3C7DC775UL, + 0x8438CE4DUL, 0x3FE1F9C1UL, 0xA097AF5CUL, 0xBC8BF524UL, 0x22FCD91DUL, 0x3FE1ED50UL, + 0x027BB78CUL, 0xBC81DF98UL, 0x5EB44027UL, 0x3FE1E0E7UL, 0x088CB6DEUL, 0xBC86FDD8UL, + 0x3168B9AAUL, 0x3FE1D487UL, 0x00A2643CUL, 0x3C8E016EUL, 0x95281C6BUL, 0x3FE1C82FUL, + 0x8010F8C9UL, 0x3C800977UL, 0x84045CD4UL, 0x3FE1BBE0UL, 0x352EF607UL, 0xBC895386UL, + 0xF8138A1CUL, 0x3FE1AF99UL, 0xA4B69280UL, 0x3C87BF85UL, 0xEB6FCB75UL, 0x3FE1A35BUL, + 0x7B4968E4UL, 0x3C7E5B4CUL, 0x58375D2FUL, 0x3FE19726UL, 0x85F17E08UL, 0x3C84AADDUL, + 0x388C8DEAUL, 0x3FE18AF9UL, 0xD1970F6CUL, 0xBC811023UL, 0x8695BBC0UL, 0x3FE17ED4UL, + 0xE2AC5A64UL, 0x3C609E3FUL, 0x3C7D517BUL, 0x3FE172B8UL, 0xB9D78A76UL, 0xBC719041UL, + 0x5471C3C2UL, 0x3FE166A4UL, 0x82EA1A32UL, 0x3C48F23BUL, 0xC8A58E51UL, 0x3FE15A98UL, + 0xB9EEAB0AUL, 0x3C72406AUL, 0x934F312EUL, 0x3FE14E95UL, 0x39BF44ABUL, 0xBC7B91E8UL, + 0xAEA92DE0UL, 0x3FE1429AUL, 0x9AF1369EUL, 0xBC832FBFUL, 0x14F204ABUL, 0x3FE136A8UL, + 0xBA48DCF0UL, 0xBC57108FUL, 0xC06C31CCUL, 0x3FE12ABDUL, 0xB36CA5C7UL, 0xBC41B514UL, + 0xAB5E2AB6UL, 0x3FE11EDBUL, 0xF703FB72UL, 0xBC8CA454UL, 0xD0125B51UL, 0x3FE11301UL, + 0x39449B3AUL, 0xBC86C510UL, 0x28D7233EUL, 0x3FE10730UL, 0x1692FDD5UL, 0x3C7D46EBUL, + 0xAFFED31BUL, 0x3FE0FB66UL, 0xC44EBD7BUL, 0xBC5B9BEDUL, 0x5FDFA9C5UL, 0x3FE0EFA5UL, + 0xBC54021BUL, 0xBC849DB9UL, 0x32D3D1A2UL, 0x3FE0E3ECUL, 0x27C57B52UL, 0x3C303A17UL, + 0x23395DECUL, 0x3FE0D83BUL, 0xE43F316AUL, 0xBC8BC14DUL, 0x2B7247F7UL, 0x3FE0CC92UL, + 0x16E24F71UL, 0x3C801EDCUL, 0x45E46C85UL, 0x3FE0C0F1UL, 0x06D21CEFUL, 0x3C84F989UL, + 0x6CF9890FUL, 0x3FE0B558UL, 0x4ADC610BUL, 0x3C88A62EUL, 0x9B1F3919UL, 0x3FE0A9C7UL, + 0x873D1D38UL, 0x3C75D16CUL, 0xCAC6F383UL, 0x3FE09E3EUL, 0x18316136UL, 0x3C814878UL, + 0xF66607E0UL, 0x3FE092BDUL, 0x800A3FD1UL, 0xBC868063UL, 0x18759BC8UL, 0x3FE08745UL, + 0x4BB284FFUL, 0x3C5186BEUL, 0x2B72A836UL, 0x3FE07BD4UL, 0x54458700UL, 0x3C732334UL, + 0x29DDF6DEUL, 0x3FE0706BUL, 0xE2B13C27UL, 0xBC7C91DFUL, 0x0E3C1F89UL, 0x3FE0650AUL, + 0x5799C397UL, 0xBC85CB7BUL, 0xD3158574UL, 0x3FE059B0UL, 0xA475B465UL, 0x3C7D73E2UL, + 0x72F654B1UL, 0x3FE04E5FUL, 0x3AA0D08CUL, 0x3C74C379UL, 0xE86E7F85UL, 0x3FE04315UL, + 0x1977C96EUL, 0xBC80A31CUL, 0x2E11BBCCUL, 0x3FE037D4UL, 0xEEADE11AUL, 0x3C556811UL, + 0x3E778061UL, 0x3FE02C9AUL, 0x535B085DUL, 0xBC619083UL, 0x143B0281UL, 0x3FE02168UL, + 0x0FC54EB6UL, 0xBC72BF31UL, 0xA9FB3335UL, 0x3FE0163DUL, 0x9AB8CDB7UL, 0x3C8B6129UL, + 0xFA5ABCBFUL, 0x3FE00B1AUL, 0xA7609F71UL, 0xBC74F6B2UL }; #define __ _masm-> From 0366d8823bc844225ca24964e352ce0a57d01683 Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Wed, 1 Oct 2025 00:52:30 +0000 Subject: [PATCH 304/556] 8354894: java/lang/Thread/virtual/Starvation.java timeout on server with high CPUs Co-authored-by: Alan Bateman Reviewed-by: jpai --- test/jdk/java/lang/Thread/virtual/Starvation.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/jdk/java/lang/Thread/virtual/Starvation.java b/test/jdk/java/lang/Thread/virtual/Starvation.java index c55ad3c2494..987c54c1a0c 100644 --- a/test/jdk/java/lang/Thread/virtual/Starvation.java +++ b/test/jdk/java/lang/Thread/virtual/Starvation.java @@ -25,7 +25,7 @@ * @requires vm.continuations * @library /test/lib * @bug 8345294 - * @run main/othervm/timeout=800/native --enable-native-access=ALL-UNNAMED Starvation 100000 + * @run main/othervm/native --enable-native-access=ALL-UNNAMED Starvation */ import java.time.Duration; @@ -37,9 +37,16 @@ import jdk.test.lib.thread.VThreadPinner; public class Starvation { public static void main(String[] args) throws Exception { - int iterations = Integer.parseInt(args[0]); + int iterations; + if (args.length > 0) { + iterations = Integer.parseInt(args[0]); + } else { + int nprocs = Runtime.getRuntime().availableProcessors(); + iterations = 40_000 / nprocs; + } - for (int i = 0; i < iterations; i++) { + for (int i = 1; i <= iterations; i++) { + System.out.format("%s iteration %d of %d ...%n", Instant.now(), i, iterations); var exRef = new AtomicReference(); Thread thread = Thread.startVirtualThread(() -> { try { From 17d8fa8e421db67027c9e7d2ddd634ff0b897cb6 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Wed, 1 Oct 2025 01:40:06 +0000 Subject: [PATCH 305/556] 8367026: Reorder the timeout failure handler commands to have jstack run before the rest Reviewed-by: erikj, lmesnik --- .../src/share/conf/common.properties | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/failure_handler/src/share/conf/common.properties b/test/failure_handler/src/share/conf/common.properties index 5cd2c1c13ca..295a7458d4b 100644 --- a/test/failure_handler/src/share/conf/common.properties +++ b/test/failure_handler/src/share/conf/common.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,13 @@ args=%p ################################################################################ # process info to gather ################################################################################ +# It's important to retain the order of these actions and run the thread dump +# generating commands before the rest, to allow for capturing the test +# process' call stack as soon as the timeout has occurred. That reduces the chances +# of the test completing and thus missing crucial details from the thread dump +# while these timeout actions were being run. onTimeout=\ + thread_dump \ jinfo \ jcmd.compiler.codecache jcmd.compiler.codelist \ jcmd.compiler.queue \ @@ -63,9 +69,13 @@ jcmd.thread.dump_to_file.params.successArtifacts=JavaThread.dump.%p.%iterCount jcmd.thread.vthread_scheduler.args=%p Thread.vthread_scheduler +# use jstack to generate one thread dump +thread_dump.app=jstack +thread_dump.args=-e -l %p + jstack.app=jstack jstack.args=-e -l %p -jstack.params.repeat=6 +jstack.params.repeat=5 jhsdb.app=jhsdb jhsdb.jstack.live.default.args=jstack --pid %p From 8c3ca024c770d3cf3b35234e967e5f0f0d610388 Mon Sep 17 00:00:00 2001 From: Kim Barrett Date: Wed, 1 Oct 2025 03:58:49 +0000 Subject: [PATCH 306/556] 8368817: Convert JDK_Version::to_string to use stringStream instead of jio_snprintf-chain Reviewed-by: fandreuzzi, jsjolen --- src/hotspot/share/runtime/java.cpp | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/hotspot/share/runtime/java.cpp b/src/hotspot/share/runtime/java.cpp index 9363f9055ba..6fda21ac86c 100644 --- a/src/hotspot/share/runtime/java.cpp +++ b/src/hotspot/share/runtime/java.cpp @@ -747,29 +747,19 @@ int JDK_Version::compare(const JDK_Version& other) const { /* See JEP 223 */ void JDK_Version::to_string(char* buffer, size_t buflen) const { - assert(buffer && buflen > 0, "call with useful buffer"); - size_t index = 0; - + assert((buffer != nullptr) && (buflen > 0), "call with useful buffer"); + stringStream ss{buffer, buflen}; if (!is_valid()) { - jio_snprintf(buffer, buflen, "%s", "(uninitialized)"); + ss.print_raw("(uninitialized)"); } else { - int rc = jio_snprintf( - &buffer[index], buflen - index, "%d.%d", _major, _minor); - if (rc == -1) return; - index += rc; + ss.print("%d.%d", _major, _minor); if (_patch > 0) { - rc = jio_snprintf(&buffer[index], buflen - index, ".%d.%d", _security, _patch); - if (rc == -1) return; - index += rc; + ss.print(".%d.%d", _security, _patch); } else if (_security > 0) { - rc = jio_snprintf(&buffer[index], buflen - index, ".%d", _security); - if (rc == -1) return; - index += rc; + ss.print(".%d", _security); } if (_build > 0) { - rc = jio_snprintf(&buffer[index], buflen - index, "+%d", _build); - if (rc == -1) return; - index += rc; + ss.print("+%d", _build); } } } From 394eb80a48fa73238cf897087b99c3da5a616566 Mon Sep 17 00:00:00 2001 From: Kim Barrett Date: Wed, 1 Oct 2025 06:12:05 +0000 Subject: [PATCH 307/556] 8368957: Remove metaprogramming/logical.hpp in favor of C++17 facilities Reviewed-by: mchevalier, iwalulya --- src/hotspot/share/gc/shared/workerUtils.hpp | 7 +- src/hotspot/share/metaprogramming/logical.hpp | 64 ------------- .../gtest/metaprogramming/test_logical.cpp | 90 ------------------- 3 files changed, 4 insertions(+), 157 deletions(-) delete mode 100644 src/hotspot/share/metaprogramming/logical.hpp delete mode 100644 test/hotspot/gtest/metaprogramming/test_logical.cpp diff --git a/src/hotspot/share/gc/shared/workerUtils.hpp b/src/hotspot/share/gc/shared/workerUtils.hpp index 223dfb34eb2..5f167ffb6b2 100644 --- a/src/hotspot/share/gc/shared/workerUtils.hpp +++ b/src/hotspot/share/gc/shared/workerUtils.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,11 +27,12 @@ #include "memory/allocation.hpp" #include "metaprogramming/enableIf.hpp" -#include "metaprogramming/logical.hpp" #include "runtime/mutex.hpp" #include "utilities/debug.hpp" #include "utilities/globalDefinitions.hpp" +#include + // A class that acts as a synchronisation barrier. Workers enter // the barrier and must wait until all other workers have entered // before any of them may leave. @@ -103,7 +104,7 @@ public: // explicitly passed as extra arguments. Every thread in the parallel task // must execute this. template...>::value)> + ENABLE_IF(std::conjunction_v...>)> void all_tasks_claimed(T0 first_skipped, Ts... more_skipped) { static_assert(std::is_convertible::value, "not convertible"); uint skipped[] = { static_cast(first_skipped), static_cast(more_skipped)... }; diff --git a/src/hotspot/share/metaprogramming/logical.hpp b/src/hotspot/share/metaprogramming/logical.hpp deleted file mode 100644 index a488b94f8b9..00000000000 --- a/src/hotspot/share/metaprogramming/logical.hpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef SHARE_METAPROGRAMMING_LOGICAL_HPP -#define SHARE_METAPROGRAMMING_LOGICAL_HPP - -// Stand-ins for C++17 logical operations on types. - -#include - -// Stand-in for C++17 std::bool_constant. -template -using BoolConstant = std::integral_constant; - -// Stand-in for C++17 std::conjunction -template -struct Conjunction : public std::true_type {}; - -template -struct Conjunction : public T1 {}; - -template -struct Conjunction : - public std::conditional_t, T1> -{}; - -// Stand-in for C++17 std::disjunction. -template -struct Disjunction : public std::false_type {}; - -template -struct Disjunction : public T1 {}; - -template -struct Disjunction : - public std::conditional_t> -{}; - -// Stand-in for C++17 std::negation. -template -using Negation = BoolConstant; - -#endif // SHARE_METAPROGRAMMING_LOGICAL_HPP diff --git a/test/hotspot/gtest/metaprogramming/test_logical.cpp b/test/hotspot/gtest/metaprogramming/test_logical.cpp deleted file mode 100644 index 8f2f1940a2b..00000000000 --- a/test/hotspot/gtest/metaprogramming/test_logical.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#include "metaprogramming/logical.hpp" -#include - -class TestBoolConstant { - static_assert(BoolConstant::value, "true"); - static_assert(!BoolConstant::value, "false"); -}; - -class TestConjunction { - class A : public std::true_type {}; - class B : public std::true_type {}; - class C : public std::false_type {}; - class D : public std::false_type {}; - - static_assert(Conjunction<>::value, "nullary value"); - - static_assert(Conjunction::value, "true value"); - static_assert(std::is_base_of>::value, "true type"); - - static_assert(!Conjunction::value, "false value"); - static_assert(std::is_base_of>::value, "false type"); - - static_assert(Conjunction::value, "true/true value"); - static_assert(std::is_base_of>::value, "true/true type"); - - static_assert(!Conjunction::value, "true/false value"); - static_assert(std::is_base_of>::value, "true/false type"); - - static_assert(!Conjunction::value, "false/true value"); - static_assert(std::is_base_of>::value, "false/true type"); - - static_assert(!Conjunction::value, "false/false value"); - static_assert(std::is_base_of>::value, "false/false type"); -}; - -class TestDisjunction { - class A : public std::true_type {}; - class B : public std::true_type {}; - class C : public std::false_type {}; - class D : public std::false_type {}; - - static_assert(!Disjunction<>::value, "nullary value"); - - static_assert(Disjunction::value, "true value"); - static_assert(std::is_base_of>::value, "true type"); - - static_assert(!Disjunction::value, "false value"); - static_assert(std::is_base_of>::value, "false type"); - - static_assert(Disjunction::value, "true/true value"); - static_assert(std::is_base_of>::value, "true/true type"); - - static_assert(Disjunction::value, "true/false value"); - static_assert(std::is_base_of>::value, "true/false type"); - - static_assert(Disjunction::value, "false/true value"); - static_assert(std::is_base_of>::value, "false/true type"); - - static_assert(!Disjunction::value, "false/false value"); - static_assert(std::is_base_of>::value, "false/false type"); -}; - -class TestNegation { - static_assert(Negation::value, "false -> true"); - static_assert(!Negation::value, "true -> false"); -}; From 1188ca55f525554d2bb10691b368c818d98e5ea7 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Wed, 1 Oct 2025 08:07:59 +0000 Subject: [PATCH 308/556] 8368954: G1: Document why G1 uses TLS storage for the current card table reference Reviewed-by: ayang, rcastanedalo, iwalulya --- src/hotspot/share/gc/g1/g1ThreadLocalData.hpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp b/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp index 858081b0581..07c8c569504 100644 --- a/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp +++ b/src/hotspot/share/gc/g1/g1ThreadLocalData.hpp @@ -36,6 +36,15 @@ class G1ThreadLocalData { private: SATBMarkQueue _satb_mark_queue; + // The current base address of the card table. Accessed by the barrier to do + // the card mark. Changed as required by the refinement control thread to + // implement card table switching. + // + // Tests showed that embedding this value in the TLS block is the cheapest + // way for fast access to this value in the barrier. + // E.g. embedding an address to that value directly into the code stream and + // then loading from that was found to be slower on non-x64 architectures. + // Additionally it increases code size a lot. G1CardTable::CardValue* _byte_map_base; // Per-thread cache of pinned object count to reduce atomic operation traffic From 6c2d383492d194eb8a604a58a0336c371cbb1ea5 Mon Sep 17 00:00:00 2001 From: Thomas Schatzl Date: Wed, 1 Oct 2025 08:08:19 +0000 Subject: [PATCH 309/556] 8368953: Document the reason why Serial/Parallel/G1 use zero as dirty card value Co-authored-by: Albert Mingkun Yang Reviewed-by: ayang, iwalulya --- src/hotspot/share/gc/shared/cardTable.hpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/hotspot/share/gc/shared/cardTable.hpp b/src/hotspot/share/gc/shared/cardTable.hpp index 63dcfe7aecb..ab26055e522 100644 --- a/src/hotspot/share/gc/shared/cardTable.hpp +++ b/src/hotspot/share/gc/shared/cardTable.hpp @@ -61,6 +61,20 @@ protected: inline size_t compute_byte_map_size(size_t num_bytes); + // We use 0x00 (zero) to represent Dirty and 0xFF to represent Clean because + // this choice reduces the barrier code by one instruction on architectures with + // a constant-zero register. On such architectures, the Dirty value (0x00) is + // directly accessible through the zero register, eliminating the need to load + // the value explicitly and thereby saving one instruction + // + // E.g. see + // Urs Hölzle. A fast write barrier for generational garbage collectors. + // In Eliot Moss, Paul R. Wilson, and Benjamin Zorn, editors, OOPSLA/ECOOP '93 + // Workshop on Garbage Collection in Object-Oriented Systems, October 1993 + // + // that shows this for SPARC (but aarch32/aarch64/RISC-V are similar in this + // respect). + // enum CardValues { clean_card = (CardValue)-1, From f49849a5ed4e9383e39e69ce76bb8ea74fb443f9 Mon Sep 17 00:00:00 2001 From: Hamlin Li Date: Wed, 1 Oct 2025 08:22:02 +0000 Subject: [PATCH 310/556] 8368893: RISC-V: crash after JDK-8352673 on fastdebug version Reviewed-by: fyang, dzhang --- src/hotspot/cpu/riscv/vm_version_riscv.cpp | 9 ++------- .../os_cpu/linux_riscv/vm_version_linux_riscv.cpp | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.cpp b/src/hotspot/cpu/riscv/vm_version_riscv.cpp index 9d6ac4963aa..53f7ff9bc66 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.cpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.cpp @@ -213,13 +213,8 @@ void VM_Version::common_initialize() { } if (UseRVV) { - if (!ext_V.enabled() && FLAG_IS_DEFAULT(UseRVV)) { - warning("RVV is not supported on this CPU"); - FLAG_SET_DEFAULT(UseRVV, false); - } else { - // read vector length from vector CSR vlenb - _initial_vector_length = cpu_vector_length(); - } + // read vector length from vector CSR vlenb + _initial_vector_length = cpu_vector_length(); } // Misc Intrinsics that could depend on RVV. diff --git a/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp b/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp index 89501aac979..67d405f0656 100644 --- a/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp +++ b/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp @@ -100,7 +100,6 @@ #endif uint32_t VM_Version::cpu_vector_length() { - assert(ext_V.enabled(), "should not call this"); return (uint32_t)read_csr(CSR_VLENB); } From 84e5d63b9fa8af0b35e1d682a81900cb157697fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Sj=C3=B6len?= Date: Wed, 1 Oct 2025 09:01:19 +0000 Subject: [PATCH 311/556] 8368885: NMT CommandLine tests can check for error better Reviewed-by: phubner, azafari, shade --- .../jtreg/runtime/NMT/CommandLineDetail.java | 21 ++++++++++--------- .../jtreg/runtime/NMT/CommandLineSummary.java | 21 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/test/hotspot/jtreg/runtime/NMT/CommandLineDetail.java b/test/hotspot/jtreg/runtime/NMT/CommandLineDetail.java index 57f2302db30..a7561c7792e 100644 --- a/test/hotspot/jtreg/runtime/NMT/CommandLineDetail.java +++ b/test/hotspot/jtreg/runtime/NMT/CommandLineDetail.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,13 +35,14 @@ import jdk.test.lib.process.OutputAnalyzer; public class CommandLineDetail { - public static void main(String args[]) throws Exception { - - ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( - "-XX:NativeMemoryTracking=detail", - "-version"); - OutputAnalyzer output = new OutputAnalyzer(pb.start()); - output.shouldNotContain("error"); - output.shouldHaveExitValue(0); - } + public static void main(String args[]) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-Xlog:nmt=warning", + "-XX:NativeMemoryTracking=detail", + "-version"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldNotContain("NMT initialization failed"); + output.shouldNotContain("Could not create the Java Virtual Machine."); + output.shouldHaveExitValue(0); + } } diff --git a/test/hotspot/jtreg/runtime/NMT/CommandLineSummary.java b/test/hotspot/jtreg/runtime/NMT/CommandLineSummary.java index 02b37a40ef4..381fe3eba34 100644 --- a/test/hotspot/jtreg/runtime/NMT/CommandLineSummary.java +++ b/test/hotspot/jtreg/runtime/NMT/CommandLineSummary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,13 +35,14 @@ import jdk.test.lib.process.OutputAnalyzer; public class CommandLineSummary { - public static void main(String args[]) throws Exception { - - ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( - "-XX:NativeMemoryTracking=summary", - "-version"); - OutputAnalyzer output = new OutputAnalyzer(pb.start()); - output.shouldNotContain("error"); - output.shouldHaveExitValue(0); - } + public static void main(String args[]) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-Xlog:nmt=warning", + "-XX:NativeMemoryTracking=summary", + "-version"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldNotContain("NMT initialization failed"); + output.shouldNotContain("Could not create the Java Virtual Machine."); + output.shouldHaveExitValue(0); + } } From 5a2700f231d72e2241703c1d17b308f031e8566c Mon Sep 17 00:00:00 2001 From: Richard Reingruber Date: Wed, 1 Oct 2025 09:26:43 +0000 Subject: [PATCH 312/556] 8368861: [TEST] compiler/floatingpoint/ScalarFPtoIntCastTest.java expects x86 IR on non-x86 platforms Reviewed-by: sviswanathan, mdoerr --- .../compiler/floatingpoint/ScalarFPtoIntCastTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java b/test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java index e6d1c875250..e8575cfe6b6 100644 --- a/test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java +++ b/test/hotspot/jtreg/compiler/floatingpoint/ScalarFPtoIntCastTest.java @@ -88,6 +88,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_F2I, "> 0"}) @IR(counts = {IRNode.X86_SCONV_F2I, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_F2I_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) @@ -103,6 +104,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_F2L, "> 0"}) @IR(counts = {IRNode.X86_SCONV_F2L, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_F2L_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) @@ -118,6 +120,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_F2I, "> 0"}) @IR(counts = {IRNode.X86_SCONV_F2I, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_F2I_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) @@ -133,6 +136,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_F2I, "> 0"}) @IR(counts = {IRNode.X86_SCONV_F2I, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_F2I_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) @@ -148,6 +152,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_D2I, "> 0"}) @IR(counts = {IRNode.X86_SCONV_D2I, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_D2I_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) @@ -163,6 +168,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_D2L, "> 0"}) @IR(counts = {IRNode.X86_SCONV_D2L, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_D2L_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) @@ -178,6 +184,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_D2I, "> 0"}) @IR(counts = {IRNode.X86_SCONV_D2I, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_D2I_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) @@ -193,6 +200,7 @@ public class ScalarFPtoIntCastTest { @Test @IR(counts = {IRNode.CONV_D2I, "> 0"}) @IR(counts = {IRNode.X86_SCONV_D2I, "> 0"}, + applyIfPlatform = {"x64", "true"}, applyIfCPUFeature = {"avx10_2", "false"}) @IR(counts = {IRNode.X86_SCONV_D2I_AVX10, "> 0"}, applyIfCPUFeature = {"avx10_2", "true"}) From 3607e9986f1582ebdae1b6ad2a13c1a9c239e0d6 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 1 Oct 2025 11:16:44 +0000 Subject: [PATCH 313/556] 8367279: Test tools/javac/tree/TreePosTest.java timed out Reviewed-by: asotona --- .../tools/javac/tree/TreePosTest.java | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/test/langtools/tools/javac/tree/TreePosTest.java b/test/langtools/tools/javac/tree/TreePosTest.java index 0ae62ca940d..9e6dcf61306 100644 --- a/test/langtools/tools/javac/tree/TreePosTest.java +++ b/test/langtools/tools/javac/tree/TreePosTest.java @@ -38,16 +38,11 @@ import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.nio.charset.Charset; +import java.io.UncheckedIOException; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; @@ -71,7 +66,7 @@ import javax.tools.StandardJavaFileManager; import com.sun.source.tree.CaseTree.CaseKind; import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.util.JavacTask; +import com.sun.tools.javac.api.JavacTaskPool; import com.sun.tools.javac.api.JavacTool; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.EndPosTable; @@ -80,7 +75,6 @@ import com.sun.tools.javac.tree.JCTree.JCAnnotatedType; import com.sun.tools.javac.tree.JCTree.JCCase; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; -import com.sun.tools.javac.tree.JCTree.JCImport; import com.sun.tools.javac.tree.JCTree.JCImportBase; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCNewClass; @@ -90,7 +84,6 @@ import com.sun.tools.javac.tree.TreeScanner; import static com.sun.tools.javac.tree.JCTree.Tag.*; import static com.sun.tools.javac.util.Position.NOPOS; -import java.util.stream.Stream; /** * Utility and test program to check validity of tree positions for tree nodes. @@ -275,6 +268,7 @@ public class TreePosTest { PrintWriter pw = new PrintWriter(sw); Reporter r = new Reporter(pw); JavacTool tool = JavacTool.create(); + JavacTaskPool pool = new JavacTaskPool(1); StandardJavaFileManager fm = tool.getStandardFileManager(r, null, null); /** @@ -285,21 +279,25 @@ public class TreePosTest { * @throws TreePosTest.ParseException if any errors occur while parsing the file */ JCCompilationUnit read(File file) throws IOException, ParseException { - JavacTool tool = JavacTool.create(); r.errors = 0; Iterable files = fm.getJavaFileObjects(file); - JavacTask task = tool.getTask(pw, fm, r, List.of("-proc:none"), null, files); - Iterable trees = task.parse(); - pw.flush(); - if (r.errors > 0) - throw new ParseException(sw.toString()); - Iterator iter = trees.iterator(); - if (!iter.hasNext()) - throw new Error("no trees found"); - JCCompilationUnit t = (JCCompilationUnit) iter.next(); - if (iter.hasNext()) - throw new Error("too many trees found"); - return t; + return pool.getTask(pw, fm, r, List.of("-proc:none"), null, files, task -> { + try { + Iterable trees = task.parse(); + pw.flush(); + if (r.errors > 0) + throw new ParseException(sw.toString()); + Iterator iter = trees.iterator(); + if (!iter.hasNext()) + throw new Error("no trees found"); + JCCompilationUnit t = (JCCompilationUnit) iter.next(); + if (iter.hasNext()) + throw new Error("too many trees found"); + return t; + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); } /** @@ -546,7 +544,7 @@ public class TreePosTest { /** * Thrown when errors are found parsing a java file. */ - private static class ParseException extends Exception { + private static class ParseException extends RuntimeException { ParseException(String msg) { super(msg); } From c69456e87aeb8653ce23bc7f579c254511bbf2d1 Mon Sep 17 00:00:00 2001 From: Justin King Date: Wed, 1 Oct 2025 13:23:13 +0000 Subject: [PATCH 314/556] 8368962: hotspot/cpu/aarch64/bytecodes_aarch64.{hpp,cpp} is unused Reviewed-by: aph, mhaessig --- src/hotspot/cpu/aarch64/bytecodes_aarch64.cpp | 28 ----------------- src/hotspot/cpu/aarch64/bytecodes_aarch64.hpp | 31 ------------------- 2 files changed, 59 deletions(-) delete mode 100644 src/hotspot/cpu/aarch64/bytecodes_aarch64.cpp delete mode 100644 src/hotspot/cpu/aarch64/bytecodes_aarch64.hpp diff --git a/src/hotspot/cpu/aarch64/bytecodes_aarch64.cpp b/src/hotspot/cpu/aarch64/bytecodes_aarch64.cpp deleted file mode 100644 index 119ad8baec3..00000000000 --- a/src/hotspot/cpu/aarch64/bytecodes_aarch64.cpp +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2014, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#include "interpreter/bytecodes.hpp" - - diff --git a/src/hotspot/cpu/aarch64/bytecodes_aarch64.hpp b/src/hotspot/cpu/aarch64/bytecodes_aarch64.hpp deleted file mode 100644 index 1daf71388e3..00000000000 --- a/src/hotspot/cpu/aarch64/bytecodes_aarch64.hpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 1998, 2019, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2014, Red Hat Inc. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - * - */ - -#ifndef CPU_AARCH64_BYTECODES_AARCH64_HPP -#define CPU_AARCH64_BYTECODES_AARCH64_HPP - -// No aarch64 specific bytecodes - -#endif // CPU_AARCH64_BYTECODES_AARCH64_HPP From 182fbc2b836d27410ccd0da512acb17bac9363c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Casta=C3=B1eda=20Lozano?= Date: Wed, 1 Oct 2025 13:55:18 +0000 Subject: [PATCH 315/556] 8368675: IGV: nodes are wrongly marked as changed in the difference view Reviewed-by: mchevalier, mhaessig, dfenacci, tholenstein --- .../com/sun/hotspot/igv/data/InputNode.java | 22 +++++++++++++++---- .../igv/data/serialization/Parser.java | 4 ++-- .../hotspot/igv/difference/Difference.java | 4 ++-- .../com/sun/hotspot/igv/graph/Figure.java | 4 ++-- .../sun/hotspot/igv/view/NodeQuickSearch.java | 4 +--- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputNode.java b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputNode.java index 48a000bf7b9..df1cbbacc0c 100644 --- a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputNode.java +++ b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputNode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,6 +32,9 @@ import java.util.Objects; */ public class InputNode extends Properties.Entity { + public static final String LABEL_PROPERTY = "label"; + public static final String COLOR_PROPERTY = "color"; + private int id; public InputNode(InputNode n) { @@ -51,6 +54,17 @@ public class InputNode extends Properties.Entity { return id; } + // Return the node properties that are present in the input graph, excluding + // properties computed by IGV itself. This is useful e.g. to produce the + // difference view, where nodes should be compared based only on their + // intrinsic characteristics. + public Properties getPrimaryProperties() { + Properties primaryProperties = new Properties(getProperties()); + primaryProperties.setProperty(LABEL_PROPERTY, null); + primaryProperties.setProperty(COLOR_PROPERTY, null); + return primaryProperties; + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -72,14 +86,14 @@ public class InputNode extends Properties.Entity { public void setCustomColor(Color color) { if (color != null) { String hexColor = String.format("#%08X", color.getRGB()); - getProperties().setProperty("color", hexColor); + getProperties().setProperty(COLOR_PROPERTY, hexColor); } else { - getProperties().setProperty("color", null); + getProperties().setProperty(COLOR_PROPERTY, null); } } public Color getCustomColor() { - String hexColor = getProperties().get("color"); + String hexColor = getProperties().get(COLOR_PROPERTY); if (hexColor != null) { try { String hex = hexColor.startsWith("#") ? hexColor.substring(1) : hexColor; diff --git a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/serialization/Parser.java b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/serialization/Parser.java index dfe60372eda..06887ba22f4 100644 --- a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/serialization/Parser.java +++ b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/serialization/Parser.java @@ -81,7 +81,7 @@ public class Parser implements GraphParser { public static final String FROM_INDEX_PROPERTY = "fromIndex"; public static final String TO_INDEX_PROPERTY = "toIndex"; public static final String TO_INDEX_ALT_PROPERTY = "index"; - public static final String LABEL_PROPERTY = "label"; + public static final String EDGE_LABEL_PROPERTY = "label"; public static final String METHOD_ELEMENT = "method"; public static final String INLINE_ELEMENT = "inline"; public static final String BYTECODES_ELEMENT = "bytecodes"; @@ -655,7 +655,7 @@ public class Parser implements GraphParser { toIndex = Integer.parseInt(toIndexString); } - label = readAttribute(LABEL_PROPERTY); + label = readAttribute(EDGE_LABEL_PROPERTY); type = readAttribute(TYPE_PROPERTY); from = lookupID(readRequiredAttribute(FROM_PROPERTY)); diff --git a/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java b/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java index c904e8c6083..89b1434663f 100644 --- a/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java +++ b/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java @@ -356,7 +356,7 @@ public class Difference { private static void markAsChanged(InputNode n, InputNode firstNode, InputNode otherNode) { boolean difference = false; - for (Property p : otherNode.getProperties()) { + for (Property p : otherNode.getPrimaryProperties()) { String s = firstNode.getProperties().get(p.getName()); if (!p.getValue().equals(s)) { difference = true; @@ -364,7 +364,7 @@ public class Difference { } } - for (Property p : firstNode.getProperties()) { + for (Property p : firstNode.getPrimaryProperties()) { String s = otherNode.getProperties().get(p.getName()); if (s == null && p.getValue().length() > 0) { difference = true; diff --git a/src/utils/IdealGraphVisualizer/Graph/src/main/java/com/sun/hotspot/igv/graph/Figure.java b/src/utils/IdealGraphVisualizer/Graph/src/main/java/com/sun/hotspot/igv/graph/Figure.java index d85e102f874..f212d1e8b1d 100644 --- a/src/utils/IdealGraphVisualizer/Graph/src/main/java/com/sun/hotspot/igv/graph/Figure.java +++ b/src/utils/IdealGraphVisualizer/Graph/src/main/java/com/sun/hotspot/igv/graph/Figure.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -371,7 +371,7 @@ public class Figure extends Properties.Entity implements Vertex { // search is done on the node label (without line breaks). See also // class NodeQuickSearch in the View module. String label = inputNode.getProperties().resolveString(diagram.getNodeText()); - inputNode.getProperties().setProperty("label", label.replaceAll("\\R", " ")); + inputNode.getProperties().setProperty(InputNode.LABEL_PROPERTY, label.replaceAll("\\R", " ")); // Update figure dimensions, as these are affected by the node text. updateWidth(); diff --git a/src/utils/IdealGraphVisualizer/View/src/main/java/com/sun/hotspot/igv/view/NodeQuickSearch.java b/src/utils/IdealGraphVisualizer/View/src/main/java/com/sun/hotspot/igv/view/NodeQuickSearch.java index 254b8ca15ef..95d6e12d3c4 100644 --- a/src/utils/IdealGraphVisualizer/View/src/main/java/com/sun/hotspot/igv/view/NodeQuickSearch.java +++ b/src/utils/IdealGraphVisualizer/View/src/main/java/com/sun/hotspot/igv/view/NodeQuickSearch.java @@ -48,8 +48,6 @@ import org.openide.NotifyDescriptor.Message; */ public class NodeQuickSearch implements SearchProvider { - private static final String DEFAULT_PROPERTY = "label"; - /** * Method is called by infrastructure when search operation was requested. * Implementors should evaluate given request and fill response object with @@ -72,7 +70,7 @@ public class NodeQuickSearch implements SearchProvider { String value; if (parts.length == 1) { - name = DEFAULT_PROPERTY; + name = InputNode.LABEL_PROPERTY; rawValue = parts[0]; value = ".*" + Pattern.quote(rawValue) + ".*"; } else { From c54dcefbfd2eb44a569767740dc053813c4f6fe1 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Wed, 1 Oct 2025 14:59:14 +0000 Subject: [PATCH 316/556] 8368938: Remove ObjectWaiter::badObjectWaiterPtr Reviewed-by: shade, ayang --- src/hotspot/share/runtime/objectMonitor.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hotspot/share/runtime/objectMonitor.hpp b/src/hotspot/share/runtime/objectMonitor.hpp index 3c925928be2..50aec549045 100644 --- a/src/hotspot/share/runtime/objectMonitor.hpp +++ b/src/hotspot/share/runtime/objectMonitor.hpp @@ -32,6 +32,7 @@ #include "oops/weakHandle.hpp" #include "runtime/javaThread.hpp" #include "utilities/checkedCast.hpp" +#include "utilities/globalDefinitions.hpp" class ObjectMonitor; class ObjectMonitorContentionMark; @@ -72,20 +73,19 @@ class ObjectWaiter : public CHeapObj { void wait_reenter_begin(ObjectMonitor *mon); void wait_reenter_end(ObjectMonitor *mon); - ObjectWaiter* const badObjectWaiterPtr = (ObjectWaiter*) 0xBAD; void set_bad_pointers() { #ifdef ASSERT - this->_prev = badObjectWaiterPtr; - this->_next = badObjectWaiterPtr; + this->_prev = (ObjectWaiter*) badAddressVal; + this->_next = (ObjectWaiter*) badAddressVal; this->TState = ObjectWaiter::TS_RUN; #endif } ObjectWaiter* next() { - assert (_next != badObjectWaiterPtr, "corrupted list!"); + assert (_next != (ObjectWaiter*) badAddressVal, "corrupted list!"); return _next; } ObjectWaiter* prev() { - assert (_prev != badObjectWaiterPtr, "corrupted list!"); + assert (_prev != (ObjectWaiter*) badAddressVal, "corrupted list!"); return _prev; } }; From e44ef0c32b3c2fcd0a6293838d9185b6d0719219 Mon Sep 17 00:00:00 2001 From: Pavel Rappo Date: Wed, 1 Oct 2025 16:05:31 +0000 Subject: [PATCH 317/556] 8367704: Fix minor documentation issues in java.time.** Reviewed-by: naoto, rriggs --- .../share/classes/java/time/Duration.java | 30 +++++++++---------- .../share/classes/java/time/Instant.java | 12 ++++---- .../share/classes/java/time/Period.java | 2 +- .../share/classes/java/time/ZoneOffset.java | 6 ++-- .../classes/java/time/ZonedDateTime.java | 8 ++--- .../share/classes/java/time/package-info.java | 8 ++--- .../java/time/temporal/ChronoField.java | 4 +-- .../java/time/temporal/ValueRange.java | 2 +- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/java.base/share/classes/java/time/Duration.java b/src/java.base/share/classes/java/time/Duration.java index 88d49fa9e45..23577a8a634 100644 --- a/src/java.base/share/classes/java/time/Duration.java +++ b/src/java.base/share/classes/java/time/Duration.java @@ -172,7 +172,7 @@ public final class Duration * Obtains a {@code Duration} representing a number of standard 24 hour days. *

        * The seconds are calculated based on the standard definition of a day, - * where each day is 86400 seconds which implies a 24 hour day. + * where each day is 86,400 seconds which implies a 24 hour day. * The nanosecond in second field is set to zero. * * @param days the number of days, positive or negative @@ -187,7 +187,7 @@ public final class Duration * Obtains a {@code Duration} representing a number of standard hours. *

        * The seconds are calculated based on the standard definition of an hour, - * where each hour is 3600 seconds. + * where each hour is 3,600 seconds. * The nanosecond in second field is set to zero. * * @param hours the number of hours, positive or negative @@ -375,8 +375,8 @@ public final class Duration *

              *    "PT20.345S" -- parses as "20.345 seconds"
              *    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
        -     *    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
        -     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
        +     *    "PT10H"     -- parses as "10 hours" (where an hour is 3,600 seconds)
        +     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86,400 seconds)
              *    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
              *    "PT-6H3M"    -- parses as "-6 hours and +3 minutes"
              *    "-PT6H3M"    -- parses as "-6 hours and -3 minutes"
        @@ -477,7 +477,7 @@ public final class Duration
              * {@link ChronoField#NANO_OF_SECOND NANO_OF_SECOND} field should be supported.
              * 

        * The result of this method can be a negative duration if the end is before the start. - * To guarantee to obtain a positive duration call {@link #abs()} on the result. + * To guarantee a positive duration, call {@link #abs()} on the result. * * @param startInclusive the start instant, inclusive, not null * @param endExclusive the end instant, exclusive, not null @@ -752,7 +752,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in standard 24 hour days added. *

        - * The number of days is multiplied by 86400 to obtain the number of seconds to add. + * The number of days is multiplied by 86,400 to obtain the number of seconds to add. * This is based on the standard definition of a day as 24 hours. *

        * This instance is immutable and unaffected by this method call. @@ -893,7 +893,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in standard 24 hour days subtracted. *

        - * The number of days is multiplied by 86400 to obtain the number of seconds to subtract. + * The number of days is multiplied by 86,400 to obtain the number of seconds to subtract. * This is based on the standard definition of a day as 24 hours. *

        * This instance is immutable and unaffected by this method call. @@ -909,7 +909,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in hours subtracted. *

        - * The number of hours is multiplied by 3600 to obtain the number of seconds to subtract. + * The number of hours is multiplied by 3,600 to obtain the number of seconds to subtract. *

        * This instance is immutable and unaffected by this method call. * @@ -924,7 +924,7 @@ public final class Duration /** * Returns a copy of this duration with the specified duration in minutes subtracted. *

        - * The number of hours is multiplied by 60 to obtain the number of seconds to subtract. + * The number of minutes is multiplied by 60 to obtain the number of seconds to subtract. *

        * This instance is immutable and unaffected by this method call. * @@ -1165,7 +1165,7 @@ public final class Duration * Gets the number of days in this duration. *

        * This returns the total number of days in the duration by dividing the - * number of seconds by 86400. + * number of seconds by 86,400. * This is based on the standard definition of a day as 24 hours. *

        * This instance is immutable and unaffected by this method call. @@ -1180,7 +1180,7 @@ public final class Duration * Gets the number of hours in this duration. *

        * This returns the total number of hours in the duration by dividing the - * number of seconds by 3600. + * number of seconds by 3,600. *

        * This instance is immutable and unaffected by this method call. * @@ -1272,7 +1272,7 @@ public final class Duration * Extracts the number of days in the duration. *

        * This returns the total number of days in the duration by dividing the - * number of seconds by 86400. + * number of seconds by 86,400. * This is based on the standard definition of a day as 24 hours. *

        * This instance is immutable and unaffected by this method call. @@ -1476,10 +1476,10 @@ public final class Duration *

        * Examples: *

        -     *    "20.345 seconds"                 -- "PT20.345S
        +     *    "20.345 seconds"                 -- "PT20.345S"
              *    "15 minutes" (15 * 60 seconds)   -- "PT15M"
        -     *    "10 hours" (10 * 3600 seconds)   -- "PT10H"
        -     *    "2 days" (2 * 86400 seconds)     -- "PT48H"
        +     *    "10 hours" (10 * 3,600 seconds)  -- "PT10H"
        +     *    "2 days" (2 * 86,400 seconds)    -- "PT48H"
              * 
        * Note that multiples of 24 hours are not output as days to avoid confusion * with {@code Period}. diff --git a/src/java.base/share/classes/java/time/Instant.java b/src/java.base/share/classes/java/time/Instant.java index 1c7a41d7f0e..db8db4aaa38 100644 --- a/src/java.base/share/classes/java/time/Instant.java +++ b/src/java.base/share/classes/java/time/Instant.java @@ -114,15 +114,15 @@ import java.util.Objects; *

        * The length of the solar day is the standard way that humans measure time. * This has traditionally been subdivided into 24 hours of 60 minutes of 60 seconds, - * forming a 86400 second day. + * forming an 86,400 second day. *

        * Modern timekeeping is based on atomic clocks which precisely define an SI second * relative to the transitions of a Caesium atom. The length of an SI second was defined - * to be very close to the 86400th fraction of a day. + * to be very close to the 86,400th fraction of a day. *

        * Unfortunately, as the Earth rotates the length of the day varies. * In addition, over time the average length of the day is getting longer as the Earth slows. - * As a result, the length of a solar day in 2012 is slightly longer than 86400 SI seconds. + * As a result, the length of a solar day in 2012 is slightly longer than 86,400 SI seconds. * The actual length of any given day and the amount by which the Earth is slowing * are not predictable and can only be determined by measurement. * The UT1 time-scale captures the accurate length of day, but is only available some @@ -131,7 +131,7 @@ import java.util.Objects; * The UTC time-scale is a standard approach to bundle up all the additional fractions * of a second from UT1 into whole seconds, known as leap-seconds. * A leap-second may be added or removed depending on the Earth's rotational changes. - * As such, UTC permits a day to have 86399 SI seconds or 86401 SI seconds where + * As such, UTC permits a day to have 86,399 SI seconds or 86,401 SI seconds where * necessary in order to keep the day aligned with the Sun. *

        * The modern UTC time-scale was introduced in 1972, introducing the concept of whole leap-seconds. @@ -143,7 +143,7 @@ import java.util.Objects; * Given the complexity of accurate timekeeping described above, this Java API defines * its own time-scale, the Java Time-Scale. *

        - * The Java Time-Scale divides each calendar day into exactly 86400 + * The Java Time-Scale divides each calendar day into exactly 86,400 * subdivisions, known as seconds. These seconds may differ from the * SI second. It closely matches the de facto international civil time * scale, the definition of which changes from time to time. @@ -171,7 +171,7 @@ import java.util.Objects; * This is identical to UTC on days that do not have a leap second. * On days that do have a leap second, the leap second is spread equally * over the last 1000 seconds of the day, maintaining the appearance of - * exactly 86400 seconds per day. + * exactly 86,400 seconds per day. *

        * For the segment prior to 1972-11-03, extending back arbitrarily far, * the consensus international time scale is defined to be UT1, applied diff --git a/src/java.base/share/classes/java/time/Period.java b/src/java.base/share/classes/java/time/Period.java index 5ee80710edb..745714788c3 100644 --- a/src/java.base/share/classes/java/time/Period.java +++ b/src/java.base/share/classes/java/time/Period.java @@ -765,7 +765,7 @@ public final class Period *

        * This instance is immutable and unaffected by this method call. * - * @param daysToSubtract the months to subtract, positive or negative + * @param daysToSubtract the days to subtract, positive or negative * @return a {@code Period} based on this period with the specified days subtracted, not null * @throws ArithmeticException if numeric overflow occurs */ diff --git a/src/java.base/share/classes/java/time/ZoneOffset.java b/src/java.base/share/classes/java/time/ZoneOffset.java index d93c6e2d46d..4199d17735c 100644 --- a/src/java.base/share/classes/java/time/ZoneOffset.java +++ b/src/java.base/share/classes/java/time/ZoneOffset.java @@ -417,9 +417,9 @@ public final class ZoneOffset /** * Obtains an instance of {@code ZoneOffset} specifying the total offset in seconds *

        - * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800. + * The offset must be in the range {@code -18:00} to {@code +18:00}, which corresponds to -64,800 to +64,800. * - * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 + * @param totalSeconds the total time-zone offset in seconds, from -64,800 to +64,800 * @return the ZoneOffset, not null * @throws DateTimeException if the offset is not in the required range */ @@ -450,7 +450,7 @@ public final class ZoneOffset /** * Constructor. * - * @param totalSeconds the total time-zone offset in seconds, from -64800 to +64800 + * @param totalSeconds the total time-zone offset in seconds, from -64,800 to +64,800 */ private ZoneOffset(int totalSeconds) { this.totalSeconds = totalSeconds; diff --git a/src/java.base/share/classes/java/time/ZonedDateTime.java b/src/java.base/share/classes/java/time/ZonedDateTime.java index 962469b3225..57dc98d5c68 100644 --- a/src/java.base/share/classes/java/time/ZonedDateTime.java +++ b/src/java.base/share/classes/java/time/ZonedDateTime.java @@ -136,7 +136,7 @@ import jdk.internal.util.DateTimeHelper; * For Overlaps, the general strategy is that if the local date-time falls in the * middle of an Overlap, then the previous offset will be retained. If there is no * previous offset, or the previous offset is invalid, then the earlier offset is - * used, typically "summer" time.. Two additional methods, + * used, typically "summer" time. Two additional methods, * {@link #withEarlierOffsetAtOverlap()} and {@link #withLaterOffsetAtOverlap()}, * help manage the case of an overlap. *

        @@ -246,7 +246,7 @@ public final class ZonedDateTime * Time-zone rules, such as daylight savings, mean that not every local date-time * is valid for the specified zone, thus the local date-time may be adjusted. *

        - * The local date time and first combined to form a local date-time. + * The local date and time are first combined to form a local date-time. * The local date-time is then resolved to a single instant on the time-line. * This is achieved by finding a valid offset from UTC/Greenwich for the local * date-time as defined by the {@link ZoneRules rules} of the zone ID. @@ -263,7 +263,7 @@ public final class ZonedDateTime * @param date the local date, not null * @param time the local time, not null * @param zone the time-zone, not null - * @return the offset date-time, not null + * @return the zoned date-time, not null */ public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) { return of(LocalDateTime.of(date, time), zone); @@ -333,7 +333,7 @@ public final class ZonedDateTime * @param second the second-of-minute to represent, from 0 to 59 * @param nanoOfSecond the nano-of-second to represent, from 0 to 999,999,999 * @param zone the time-zone, not null - * @return the offset date-time, not null + * @return the zoned date-time, not null * @throws DateTimeException if the value of any field is out of range, or * if the day-of-month is invalid for the month-year */ diff --git a/src/java.base/share/classes/java/time/package-info.java b/src/java.base/share/classes/java/time/package-info.java index 8a4fbe44d8b..d0fb7a6c2bc 100644 --- a/src/java.base/share/classes/java/time/package-info.java +++ b/src/java.base/share/classes/java/time/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -65,7 +65,7 @@ * The main API for dates, times, instants, and durations. *

        *

        - * The classes defined here represent the principle date-time concepts, + * The classes defined here represent the principal date-time concepts, * including instants, durations, dates, times, time-zones and periods. * They are based on the ISO calendar system, which is the de facto world * calendar following the proleptic Gregorian rules. @@ -150,7 +150,7 @@ *

        *

        * {@link java.time.OffsetTime} stores a time and offset from UTC without a date. - * This stores a date like '11:30+01:00'. + * This stores a time like '11:30+01:00'. * The {@link java.time.ZoneOffset ZoneOffset} is of the form '+01:00'. *

        *

        @@ -249,7 +249,7 @@ *

        * Multiple calendar systems is an awkward addition to the design challenges. * The first principle is that most users want the standard ISO calendar system. - * As such, the main classes are ISO-only. The second principle is that most of those that want a + * As such, the main classes are ISO-only. The second principle is that most of those who want a * non-ISO calendar system want it for user interaction, thus it is a UI localization issue. * As such, date and time objects should be held as ISO objects in the data model and persistent * storage, only being converted to and from a local calendar for display. diff --git a/src/java.base/share/classes/java/time/temporal/ChronoField.java b/src/java.base/share/classes/java/time/temporal/ChronoField.java index 9e5de367b0e..506737ff268 100644 --- a/src/java.base/share/classes/java/time/temporal/ChronoField.java +++ b/src/java.base/share/classes/java/time/temporal/ChronoField.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -599,7 +599,7 @@ public enum ChronoField implements TemporalField { * A {@link ZoneOffset} represents the period of time that local time differs from UTC/Greenwich. * This is usually a fixed number of hours and minutes. * It is equivalent to the {@link ZoneOffset#getTotalSeconds() total amount} of the offset in seconds. - * For example, during the winter Paris has an offset of {@code +01:00}, which is 3600 seconds. + * For example, during the winter, Paris has an offset of {@code +01:00}, which is 3,600 seconds. *

        * This field is strictly defined to have the same meaning in all calendar systems. * This is necessary to ensure interoperation between calendars. diff --git a/src/java.base/share/classes/java/time/temporal/ValueRange.java b/src/java.base/share/classes/java/time/temporal/ValueRange.java index 442cf0a2509..27e71e77d66 100644 --- a/src/java.base/share/classes/java/time/temporal/ValueRange.java +++ b/src/java.base/share/classes/java/time/temporal/ValueRange.java @@ -78,7 +78,7 @@ import java.time.DateTimeException; * Only the minimum and maximum values are provided. * It is possible for there to be invalid values within the outer range. * For example, a weird field may have valid values of 1, 2, 4, 6, 7, thus - * have a range of '1 - 7', despite that fact that values 3 and 5 are invalid. + * have a range of '1 - 7', despite the fact that values 3 and 5 are invalid. *

        * Instances of this class are not tied to a specific field. * From 6b72b778039afce0e25986114d15dd29a6786529 Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Wed, 1 Oct 2025 17:57:43 +0000 Subject: [PATCH 318/556] 6177299: [Fmt-Nu] NumberFormat.getPercentInstance() does not work correctly Reviewed-by: naoto --- .../classes/java/text/DecimalFormat.java | 7 ++-- .../Format/NumberFormat/NumberRegression.java | 41 ++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/java.base/share/classes/java/text/DecimalFormat.java b/src/java.base/share/classes/java/text/DecimalFormat.java index 7ace5e136fe..c803a97ad86 100644 --- a/src/java.base/share/classes/java/text/DecimalFormat.java +++ b/src/java.base/share/classes/java/text/DecimalFormat.java @@ -2335,9 +2335,10 @@ public class DecimalFormat extends NumberFormat { // (bug 4162852). if (multiplier != 1 && gotDouble) { longResult = (long)doubleResult; - gotDouble = ((doubleResult != (double)longResult) || - (doubleResult == 0.0 && 1/doubleResult < 0.0)) && - !isParseIntegerOnly(); + gotDouble = ((doubleResult >= Long.MAX_VALUE || doubleResult <= Long.MIN_VALUE) || + (doubleResult != (double)longResult) || + (doubleResult == 0.0 && 1/doubleResult < 0.0)) && + !isParseIntegerOnly(); } // cast inside of ?: because of binary numeric promotion, JLS 15.25 diff --git a/test/jdk/java/text/Format/NumberFormat/NumberRegression.java b/test/jdk/java/text/Format/NumberFormat/NumberRegression.java index dcc87643b2b..1b3452012cd 100644 --- a/test/jdk/java/text/Format/NumberFormat/NumberRegression.java +++ b/test/jdk/java/text/Format/NumberFormat/NumberRegression.java @@ -29,7 +29,8 @@ * 4098741 4099404 4101481 4106658 4106662 4106664 4108738 4110936 4122840 * 4125885 4134034 4134300 4140009 4141750 4145457 4147295 4147706 4162198 * 4162852 4167494 4170798 4176114 4179818 4212072 4212073 4216742 4217661 - * 4243011 4243108 4330377 4233840 4241880 4833877 8008577 8227313 8174269 + * 4243011 4243108 4330377 4233840 4241880 4833877 6177299 8008577 8227313 + * 8174269 * @summary Regression tests for NumberFormat and associated classes * @library /java/text/testlib * @build HexDumpReader TestUtils @@ -56,10 +57,13 @@ import java.util.*; import java.math.BigDecimal; import java.io.*; import java.math.BigInteger; + import sun.util.resources.LocaleData; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; public class NumberRegression { @@ -1847,6 +1851,41 @@ public class NumberRegression { ", got: " + parsed); } } + + /** + * 6177299: DecimalFormat w/ multiplier applied may incorrectly return + * a Double as Long.MAX_VALUE. + */ + @Test + void largePosParseTest() { + var df = NumberFormat.getPercentInstance(Locale.ENGLISH); // Default w/ multiplier 100 + // Parsed string after multiplier applied is beyond long range + assertEquals(9.223372036854777E18, + assertDoesNotThrow(() -> df.parse("922,337,203,685,477,700,000%"))); + // Fails before 6177299 fix and returns as long + assertEquals(9.223372036854776E18, + assertDoesNotThrow(() -> df.parse("922,337,203,685,477,600,000%"))); + // Within long range -> Expect to get longs as long as ulp >= 1 + assertEquals((long) 9.223372036854775E18, + assertDoesNotThrow(() -> df.parse("922,337,203,685,477,500,000%"))); + } + + /** + * 6177299: Negative version of above test. + */ + @Test + void largeNegParseTest() { + var df = NumberFormat.getPercentInstance(Locale.ENGLISH); // Default w/ multiplier 100 + // Parsed string after multiplier applied is beyond long range + assertEquals(-9.223372036854777E18, + assertDoesNotThrow(() -> df.parse("-922,337,203,685,477,700,000%"))); + // Fails before 6177299 fix and returns as long + assertEquals(-9.223372036854776E18, + assertDoesNotThrow(() -> df.parse("-922,337,203,685,477,600,000%"))); + // Within long range -> Expect to get longs as long as ulp >= 1 + assertEquals((long) -9.223372036854775E18, + assertDoesNotThrow(() -> df.parse("-922,337,203,685,477,500,000%"))); + } } @SuppressWarnings("serial") From ef724f40c1f3cdddd215d50edf512bb06825085d Mon Sep 17 00:00:00 2001 From: Joe Darcy Date: Wed, 1 Oct 2025 19:56:05 +0000 Subject: [PATCH 319/556] 8368985: Small Float16 refactorings Reviewed-by: rgiulietti, jbhateja --- .../classes/jdk/incubator/vector/Float16.java | 25 +++++++++++-------- .../vector/BasicFloat16ArithTests.java | 10 +++----- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java index 2173c741e38..b75adf350b6 100644 --- a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java +++ b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java @@ -146,6 +146,10 @@ public final class Float16 */ public static final Float16 NaN = valueOf(Float.NaN); + private static final Float16 ZERO = valueOf(0); + + private static final Float16 ONE = valueOf(1); + /** * A constant holding the largest positive finite value of type * {@code Float16}, @@ -612,7 +616,7 @@ public final class Float16 * Float16}. */ private static final Float16[] FLOAT16_10_POW = { - Float16.valueOf(1), Float16.valueOf(10), Float16.valueOf(100), + Float16.ONE, Float16.valueOf(10), Float16.valueOf(100), Float16.valueOf(1_000), Float16.valueOf(10_000) }; @@ -649,14 +653,14 @@ public final class Float16 private static Float16 fullFloat16Value(BigDecimal bd) { if (BigDecimal.ZERO.compareTo(bd) == 0) { - return Float16.valueOf(0); + return ZERO; } BigInteger w = bd.unscaledValue().abs(); int scale = bd.scale(); long qb = w.bitLength() - (long) Math.ceil(scale * L); Float16 signum = Float16.valueOf(bd.signum()); if (qb < Q_MIN_F16 - 2) { // qb < -26 - return Float16.multiply(signum, Float16.valueOf(0)); + return Float16.multiply(signum, ZERO); } if (qb > Q_MAX_F16 + P_F16 + 1) { // qb > 17 return Float16.multiply(signum, Float16.POSITIVE_INFINITY); @@ -729,7 +733,7 @@ public final class Float16 public static boolean isNaN(Float16 f16) { final short bits = float16ToRawShortBits(f16); // A NaN value has all ones in its exponent and a non-zero significand - return ((bits & 0x7c00) == 0x7c00 && (bits & 0x03ff) != 0); + return ((bits & EXP_BIT_MASK) == EXP_BIT_MASK && (bits & SIGNIF_BIT_MASK) != 0); } /** @@ -748,8 +752,9 @@ public final class Float16 * @see Double#isInfinite(double) */ public static boolean isInfinite(Float16 f16) { - return ((float16ToRawShortBits(f16) ^ - float16ToRawShortBits(POSITIVE_INFINITY)) & 0x7fff) == 0; + final short bits = float16ToRawShortBits(f16); + // An infinite value has all ones in its exponent and a zero significand + return ((bits & EXP_BIT_MASK) == EXP_BIT_MASK && (bits & SIGNIF_BIT_MASK) == 0); } /** @@ -769,7 +774,7 @@ public final class Float16 * @see Double#isFinite(double) */ public static boolean isFinite(Float16 f16) { - return (float16ToRawShortBits(f16) & (short)0x0000_7FFF) <= + return (float16ToRawShortBits(f16) & (EXP_BIT_MASK | SIGNIF_BIT_MASK)) <= float16ToRawShortBits(MAX_VALUE); } @@ -1563,7 +1568,7 @@ public final class Float16 assert exp <= MAX_EXPONENT && exp >= MIN_EXPONENT; // ulp(x) is usually 2^(SIGNIFICAND_WIDTH-1)*(2^ilogb(x)) // Let float -> float16 conversion handle encoding issues. - yield scalb(valueOf(1), exp - (PRECISION - 1)); + yield scalb(ONE, exp - (PRECISION - 1)); } }; } @@ -1685,7 +1690,7 @@ public final class Float16 Float16Consts.SIGNIFICAND_WIDTH + 1; // Make sure scaling factor is in a reasonable range - scaleFactor = Math.max(Math.min(scaleFactor, MAX_SCALE), -MAX_SCALE); + scaleFactor = Math.clamp(scaleFactor, -MAX_SCALE, MAX_SCALE); int DoubleConsts_EXP_BIAS = 1023; /* @@ -1743,7 +1748,7 @@ public final class Float16 * @see Math#signum(double) */ public static Float16 signum(Float16 f) { - return (f.floatValue() == 0.0f || isNaN(f)) ? f : copySign(valueOf(1), f); + return (f.floatValue() == 0.0f || isNaN(f)) ? f : copySign(ONE, f); } // TODO: Temporary location for this functionality while Float16 diff --git a/test/jdk/jdk/incubator/vector/BasicFloat16ArithTests.java b/test/jdk/jdk/incubator/vector/BasicFloat16ArithTests.java index 0c08099031a..755745394fc 100644 --- a/test/jdk/jdk/incubator/vector/BasicFloat16ArithTests.java +++ b/test/jdk/jdk/incubator/vector/BasicFloat16ArithTests.java @@ -408,11 +408,10 @@ public class BasicFloat16ArithTests { for(var testCase : testCases) { float arg = testCase[0]; float expected = testCase[1]; - // Exponents are in-range for Float16 - Float16 result = valueOfExact(getExponent(valueOfExact(arg))); + float result = (float)getExponent(valueOfExact(arg)); - if (Float.compare(expected, result.floatValue()) != 0) { - checkFloat16(result, expected, "getExponent(" + arg + ")"); + if (Float.compare(expected, result) != 0) { + checkFloat16(Float16.valueOf(result), expected, "getExponent(" + arg + ")"); } } return; @@ -444,8 +443,7 @@ public class BasicFloat16ArithTests { for(var testCase : testCases) { float arg = testCase[0]; float expected = testCase[1]; - // Exponents are in-range for Float16 - Float16 result = ulp(valueOfExact(arg)); + Float16 result = ulp(valueOfExact(arg)); if (Float.compare(expected, result.floatValue()) != 0) { checkFloat16(result, expected, "ulp(" + arg + ")"); From db6320df980ebe7cf2a1c727970cc937ab549b97 Mon Sep 17 00:00:00 2001 From: Johannes Graham Date: Wed, 1 Oct 2025 20:00:43 +0000 Subject: [PATCH 320/556] 8368968: FloatingDecimal: Clean up unused code Reviewed-by: rgiulietti --- .../jdk/internal/math/FloatingDecimal.java | 162 +----------------- 1 file changed, 2 insertions(+), 160 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java index e4354b9b958..e90ca4d75f1 100644 --- a/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java +++ b/src/java.base/share/classes/jdk/internal/math/FloatingDecimal.java @@ -27,8 +27,6 @@ package jdk.internal.math; import jdk.internal.vm.annotation.Stable; -import java.util.Arrays; - /** * A class for converting between ASCII and decimal representations of a single * or double precision floating point number. Most conversions are provided via @@ -102,7 +100,6 @@ public class FloatingDecimal{ * values into an ASCII String representation. */ public interface BinaryToASCIIConverter { - int getChars(byte[] result); /** * Retrieves the decimal exponent most closely corresponding to this value. @@ -117,20 +114,6 @@ public class FloatingDecimal{ */ int getDigits(byte[] digits); - /** - * Indicates the sign of the value. - * @return {@code value < 0.0}. - */ - boolean isNegative(); - - /** - * Indicates whether the value is either infinite or not a number. - * - * @return true if and only if the value is NaN - * or infinite. - */ - boolean isExceptional(); - /** * Indicates whether the value was rounded up during the binary to ASCII * conversion. @@ -147,63 +130,9 @@ public class FloatingDecimal{ boolean decimalDigitsExact(); } - /** - * A BinaryToASCIIConverter which represents NaN - * and infinite values. - */ - private static class ExceptionalBinaryToASCIIBuffer implements BinaryToASCIIConverter { - private final String image; - private final boolean isNegative; - - public ExceptionalBinaryToASCIIBuffer(String image, boolean isNegative) { - this.image = image; - this.isNegative = isNegative; - } - - @Override - @SuppressWarnings("deprecation") - public int getChars(byte[] chars) { - image.getBytes(0, image.length(), chars, 0); - return image.length(); - } - - @Override - public int getDecimalExponent() { - throw new IllegalArgumentException("Exceptional value does not have an exponent"); - } - - @Override - public int getDigits(byte[] digits) { - throw new IllegalArgumentException("Exceptional value does not have digits"); - } - - @Override - public boolean isNegative() { - return isNegative; - } - - @Override - public boolean isExceptional() { - return true; - } - - @Override - public boolean digitsRoundedUp() { - throw new IllegalArgumentException("Exceptional value is not rounded"); - } - - @Override - public boolean decimalDigitsExact() { - throw new IllegalArgumentException("Exceptional value is not exact"); - } - } - private static final String INFINITY_REP = "Infinity"; private static final String NAN_REP = "NaN"; - private static final BinaryToASCIIConverter B2AC_POSITIVE_INFINITY = new ExceptionalBinaryToASCIIBuffer(INFINITY_REP, false); - private static final BinaryToASCIIConverter B2AC_NEGATIVE_INFINITY = new ExceptionalBinaryToASCIIBuffer("-" + INFINITY_REP, true); - private static final BinaryToASCIIConverter B2AC_NOT_A_NUMBER = new ExceptionalBinaryToASCIIBuffer(NAN_REP, false); private static final BinaryToASCIIConverter B2AC_POSITIVE_ZERO = new BinaryToASCIIBuffer(false, new byte[]{'0'}); private static final BinaryToASCIIConverter B2AC_NEGATIVE_ZERO = new BinaryToASCIIBuffer(true, new byte[]{'0'}); @@ -260,16 +189,6 @@ public class FloatingDecimal{ return this.nDigits; } - @Override - public boolean isNegative() { - return isNegative; - } - - @Override - public boolean isExceptional() { - return false; - } - @Override public boolean digitsRoundedUp() { return decimalDigitsRoundedUp; @@ -826,83 +745,6 @@ public class FloatingDecimal{ 61, }; - /** - * Converts the decimal representation of a floating-point number into its - * ASCII character representation and stores it in the provided byte array. - * - * @param result the byte array to store the ASCII representation, must have length at least 26 - * @return the number of characters written to the result array - */ - public int getChars(byte[] result) { - assert nDigits <= 19 : nDigits; // generous bound on size of nDigits - int i = 0; - if (isNegative) { - result[0] = '-'; - i = 1; - } - if (decExponent > 0 && decExponent < 8) { - // print digits.digits. - int charLength = Math.min(nDigits, decExponent); - System.arraycopy(digits, firstDigitIndex, result, i, charLength); - i += charLength; - if (charLength < decExponent) { - charLength = decExponent - charLength; - Arrays.fill(result, i, i + charLength, (byte) '0'); - i += charLength; - result[i++] = '.'; - result[i++] = '0'; - } else { - result[i++] = '.'; - if (charLength < nDigits) { - int t = nDigits - charLength; - System.arraycopy(digits, firstDigitIndex + charLength, result, i, t); - i += t; - } else { - result[i++] = '0'; - } - } - } else if (decExponent <= 0 && decExponent > -3) { - result[i++] = '0'; - result[i++] = '.'; - if (decExponent != 0) { - Arrays.fill(result, i, i-decExponent, (byte) '0'); - i -= decExponent; - } - System.arraycopy(digits, firstDigitIndex, result, i, nDigits); - i += nDigits; - } else { - result[i++] = digits[firstDigitIndex]; - result[i++] = '.'; - if (nDigits > 1) { - System.arraycopy(digits, firstDigitIndex+1, result, i, nDigits - 1); - i += nDigits - 1; - } else { - result[i++] = '0'; - } - result[i++] = 'E'; - int e; - if (decExponent <= 0) { - result[i++] = '-'; - e = -decExponent + 1; - } else { - e = decExponent - 1; - } - // decExponent has 1, 2, or 3, digits - if (e <= 9) { - result[i++] = (byte) (e + '0'); - } else if (e <= 99) { - result[i++] = (byte) (e / 10 + '0'); - result[i++] = (byte) (e % 10 + '0'); - } else { - result[i++] = (byte) (e / 100 + '0'); - e %= 100; - result[i++] = (byte) (e / 10 + '0'); - result[i++] = (byte) (e % 10 + '0'); - } - } - return i; - } - } private static final ThreadLocal threadLocalBinaryToASCIIBuffer = @@ -1707,9 +1549,9 @@ public class FloatingDecimal{ // Discover obvious special cases of NaN and Infinity. if ( binExp == (int)(DoubleConsts.EXP_BIT_MASK>>EXP_SHIFT) ) { if ( fractBits == 0L ){ - return isNegative ? B2AC_NEGATIVE_INFINITY : B2AC_POSITIVE_INFINITY; + throw new IllegalArgumentException((isNegative ? "-" : "") + INFINITY_REP); } else { - return B2AC_NOT_A_NUMBER; + throw new IllegalArgumentException(NAN_REP); } } // Finish unpacking From 4df41d2a751e2942c2188ed01313d78e681835bc Mon Sep 17 00:00:00 2001 From: Igor Veresov Date: Wed, 1 Oct 2025 23:15:13 +0000 Subject: [PATCH 321/556] 8368698: runtime/cds/appcds/aotCache/OldClassSupport.java assert(can_add()) failed: Cannot add TrainingData objects Reviewed-by: heidinga, iklam --- src/hotspot/share/oops/trainingData.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hotspot/share/oops/trainingData.cpp b/src/hotspot/share/oops/trainingData.cpp index 41fbc6de994..ca5883a19ca 100644 --- a/src/hotspot/share/oops/trainingData.cpp +++ b/src/hotspot/share/oops/trainingData.cpp @@ -328,7 +328,9 @@ void CompileTrainingData::notice_jit_observation(ciEnv* env, ciBaseObject* what) // This JIT task is (probably) requesting that ik be initialized, // so add him to my _init_deps list. TrainingDataLocker l; - add_init_dep(ktd); + if (l.can_add()) { + add_init_dep(ktd); + } } } } From fa3af820ad310704e8d25cf496f676e09d60797d Mon Sep 17 00:00:00 2001 From: Boris Ulasevich Date: Wed, 1 Oct 2025 23:49:03 +0000 Subject: [PATCH 322/556] 8338197: [ubsan] ad_x86.hpp:6417:11: runtime error: shift exponent 100 is too large for 32-bit type 'unsigned int' Reviewed-by: kvn, dlong --- src/hotspot/share/adlc/output_h.cpp | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/hotspot/share/adlc/output_h.cpp b/src/hotspot/share/adlc/output_h.cpp index 8a5ad72137a..6cb82f4df7f 100644 --- a/src/hotspot/share/adlc/output_h.cpp +++ b/src/hotspot/share/adlc/output_h.cpp @@ -759,20 +759,15 @@ void ArchDesc::declare_pipe_classes(FILE *fp_hpp) { if (_pipeline->_maxcycleused <= 32) { fprintf(fp_hpp, "protected:\n"); - fprintf(fp_hpp, " %s _mask;\n\n", _pipeline->_maxcycleused <= 32 ? "uint" : "uint64_t" ); + fprintf(fp_hpp, " uint32_t _mask;\n\n"); fprintf(fp_hpp, "public:\n"); fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask() : _mask(0) {}\n\n"); - if (_pipeline->_maxcycleused <= 32) - fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask(uint mask) : _mask(mask) {}\n\n"); - else { - fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask(uint mask1, uint mask2) : _mask((((uint64_t)mask1) << 32) | mask2) {}\n\n"); - fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask(uint64_t mask) : _mask(mask) {}\n\n"); - } + fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask(uint32_t mask) : _mask(mask) {}\n\n"); fprintf(fp_hpp, " bool overlaps(const Pipeline_Use_Cycle_Mask &in2) const {\n"); fprintf(fp_hpp, " return ((_mask & in2._mask) != 0);\n"); fprintf(fp_hpp, " }\n\n"); fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask& operator<<=(int n) {\n"); - fprintf(fp_hpp, " _mask <<= n;\n"); + fprintf(fp_hpp, " _mask <<= (n < 32) ? n : 31;\n"); fprintf(fp_hpp, " return *this;\n"); fprintf(fp_hpp, " }\n\n"); fprintf(fp_hpp, " void Or(const Pipeline_Use_Cycle_Mask &in2) {\n"); @@ -785,7 +780,7 @@ void ArchDesc::declare_pipe_classes(FILE *fp_hpp) { fprintf(fp_hpp, "protected:\n"); uint masklen = (_pipeline->_maxcycleused + 31) >> 5; uint l; - fprintf(fp_hpp, " uint "); + fprintf(fp_hpp, " uint32_t "); for (l = 1; l <= masklen; l++) fprintf(fp_hpp, "_mask%d%s", l, l < masklen ? ", " : ";\n\n"); fprintf(fp_hpp, "public:\n"); @@ -794,7 +789,7 @@ void ArchDesc::declare_pipe_classes(FILE *fp_hpp) { fprintf(fp_hpp, "_mask%d(0)%s", l, l < masklen ? ", " : " {}\n\n"); fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask("); for (l = 1; l <= masklen; l++) - fprintf(fp_hpp, "uint mask%d%s", l, l < masklen ? ", " : ") : "); + fprintf(fp_hpp, "uint32_t mask%d%s", l, l < masklen ? ", " : ") : "); for (l = 1; l <= masklen; l++) fprintf(fp_hpp, "_mask%d(mask%d)%s", l, l, l < masklen ? ", " : " {}\n\n"); @@ -805,10 +800,10 @@ void ArchDesc::declare_pipe_classes(FILE *fp_hpp) { fprintf(fp_hpp, " return out;\n"); fprintf(fp_hpp, " }\n\n"); fprintf(fp_hpp, " bool overlaps(const Pipeline_Use_Cycle_Mask &in2) const {\n"); - fprintf(fp_hpp, " return ("); + fprintf(fp_hpp, " return "); for (l = 1; l <= masklen; l++) fprintf(fp_hpp, "((_mask%d & in2._mask%d) != 0)%s", l, l, l < masklen ? " || " : ""); - fprintf(fp_hpp, ") ? true : false;\n"); + fprintf(fp_hpp, ";\n"); fprintf(fp_hpp, " }\n\n"); fprintf(fp_hpp, " Pipeline_Use_Cycle_Mask& operator<<=(int n) {\n"); fprintf(fp_hpp, " if (n >= 32)\n"); @@ -819,10 +814,10 @@ void ArchDesc::declare_pipe_classes(FILE *fp_hpp) { fprintf(fp_hpp, " } while ((n -= 32) >= 32);\n\n"); fprintf(fp_hpp, " if (n > 0) {\n"); fprintf(fp_hpp, " uint m = 32 - n;\n"); - fprintf(fp_hpp, " uint mask = (1 << n) - 1;\n"); - fprintf(fp_hpp, " uint temp%d = mask & (_mask%d >> m); _mask%d <<= n;\n", 2, 1, 1); + fprintf(fp_hpp, " uint32_t mask = (1 << n) - 1;\n"); + fprintf(fp_hpp, " uint32_t temp%d = mask & (_mask%d >> m); _mask%d <<= n;\n", 2, 1, 1); for (l = 2; l < masklen; l++) { - fprintf(fp_hpp, " uint temp%d = mask & (_mask%d >> m); _mask%d <<= n; _mask%d |= temp%d;\n", l+1, l, l, l, l); + fprintf(fp_hpp, " uint32_t temp%d = mask & (_mask%d >> m); _mask%d <<= n; _mask%d |= temp%d;\n", l+1, l, l, l, l); } fprintf(fp_hpp, " _mask%d <<= n; _mask%d |= temp%d;\n", masklen, masklen, masklen); fprintf(fp_hpp, " }\n"); @@ -872,8 +867,7 @@ void ArchDesc::declare_pipe_classes(FILE *fp_hpp) { fprintf(fp_hpp, " }\n\n"); fprintf(fp_hpp, " void step(uint cycles) {\n"); fprintf(fp_hpp, " _used = 0;\n"); - fprintf(fp_hpp, " uint max_shift = 8 * sizeof(_mask) - 1;\n"); - fprintf(fp_hpp, " _mask <<= (cycles < max_shift) ? cycles : max_shift;\n"); + fprintf(fp_hpp, " _mask <<= cycles;\n"); fprintf(fp_hpp, " }\n\n"); fprintf(fp_hpp, " friend class Pipeline_Use;\n"); fprintf(fp_hpp, "};\n\n"); From 5251405ce9ab1cbd84b798a538cb3865ea4675e9 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Thu, 2 Oct 2025 06:52:59 +0000 Subject: [PATCH 323/556] 8368848: JShell's code completion not always working for multi-snippet inputs Reviewed-by: asotona --- .../classes/jdk/jshell/OuterWrapMap.java | 17 ++- .../share/classes/jdk/jshell/SnippetMaps.java | 6 +- .../jdk/jshell/SourceCodeAnalysisImpl.java | 118 +++++++++++++++--- .../jdk/jshell/CompletionSuggestionTest.java | 10 ++ 4 files changed, 125 insertions(+), 26 deletions(-) diff --git a/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java b/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java index 6f7f3fe42c3..61fb3bdfc9a 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java @@ -73,7 +73,11 @@ class OuterWrapMap { OuterWrap wrapInClass(Set except, Collection plus, List snippets, List wraps) { - String imports = state.maps.packageAndImportsExcept(except, plus); + List extraImports = + plus.stream() + .map(psi -> psi.importLine(state)) + .toList(); + String imports = state.maps.packageAndImportsExcept(except, extraImports); // className is unique to the set of snippets and their version (seq) String className = REPL_CLASS_PREFIX + snippets.stream() .sorted((sn1, sn2) -> sn1.key().index() - sn2.key().index()) @@ -86,9 +90,16 @@ class OuterWrapMap { } OuterWrap wrapInTrialClass(Wrap wrap) { - String imports = state.maps.packageAndImportsExcept(null, null); + return wrapInTrialClass(List.of(), List.of(), wrap); + } + + OuterWrap wrapInTrialClass(List extraImports, List preWraps, Wrap wrap) { + String imports = state.maps.packageAndImportsExcept(null, extraImports); + List allWraps = new ArrayList<>(); + allWraps.addAll(preWraps); + allWraps.add(wrap); CompoundWrap w = wrappedInClass(REPL_DOESNOTMATTER_CLASS_NAME, imports, - Collections.singletonList(wrap)); + allWraps); return new OuterWrap(w); } diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java b/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java index bd0c9d86db3..992d97a7040 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java @@ -105,7 +105,7 @@ final class SnippetMaps { return new ArrayList<>(snippets); } - String packageAndImportsExcept(Set except, Collection plus) { + String packageAndImportsExcept(Set except, Collection extraImports) { StringBuilder sb = new StringBuilder(); sb.append("package ").append(REPL_PACKAGE).append(";\n"); for (Snippet si : keyIndexToSnippet) { @@ -113,9 +113,7 @@ final class SnippetMaps { sb.append(si.importLine(state)); } } - if (plus != null) { - plus.forEach(psi -> sb.append(psi.importLine(state))); - } + extraImports.forEach(sb::append); return sb.toString(); } diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java index f27441b7262..19bad110a5d 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java @@ -300,17 +300,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { identifier = m.group(); } } - code = code.substring(0, cursor); - if (code.trim().isEmpty()) { //TODO: comment handling - code += ";"; - } - boolean[] moduleImport = new boolean[1]; - OuterWrap codeWrap = switch (guessKind(code, moduleImport)) { - case IMPORT -> moduleImport[0] ? proc.outerMap.wrapImport(Wrap.simpleWrap(code), null) - : proc.outerMap.wrapImport(Wrap.simpleWrap(code + "any.any"), null); - case CLASS, METHOD -> proc.outerMap.wrapInTrialClass(Wrap.classMemberWrap(code)); - default -> proc.outerMap.wrapInTrialClass(Wrap.methodWrap(code)); - }; + + OuterWrap codeWrap = wrapCodeForCompletion(code, cursor, true); String[] requiredPrefix = new String[] {identifier}; return computeSuggestions(codeWrap, code, cursor, requiredPrefix, anchor).stream() .filter(s -> filteringText(s).startsWith(requiredPrefix[0]) && !s.continuation().equals(REPL_DOESNOTMATTER_CLASS_NAME)) @@ -1740,15 +1731,11 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { }; private List documentationImpl(String code, int cursor, boolean computeJavadoc) { - code = code.substring(0, cursor); - if (code.trim().isEmpty()) { //TODO: comment handling - code += ";"; - } - - if (guessKind(code) == Kind.IMPORT) + OuterWrap codeWrap = wrapCodeForCompletion(code, cursor, false); + if (codeWrap == null) { + //import: return Collections.emptyList(); - - OuterWrap codeWrap = proc.outerMap.wrapInTrialClass(Wrap.methodWrap(code)); + } return proc.taskFactory.analyze(codeWrap, List.of(keepParameterNames), at -> { SourcePositions sp = at.trees().getSourcePositions(); CompilationUnitTree topLevel = at.firstCuTree(); @@ -2434,6 +2421,99 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis { INDEXER.submit(() -> {}).get(); } + private OuterWrap wrapCodeForCompletion(String code, int cursor, boolean wrapImports) { + code = code.substring(0, cursor); + if (code.trim().isEmpty()) { //TODO: comment handling + code += ";"; + } + + List imports = new ArrayList<>(); + List declarationParts = new ArrayList<>(); + String lastImport = null; + boolean lastImportIsModuleImport = false; + Wrap declarationWrap = null; + Wrap pendingWrap = null; + String input = code; + boolean cont = true; + int startOffset = 0; + + while (cont) { + if (lastImport != null) { + imports.add(lastImport); + lastImport = null; + } + if (declarationWrap != null) { + declarationParts.add(declarationWrap); + declarationWrap = null; + pendingWrap = null; + } + + String current; + SourceCodeAnalysis.CompletionInfo completeness = analyzeCompletion(input); + int newStartOffset; + + if (completeness.completeness().isComplete() && !completeness.remaining().isBlank()) { + current = input.substring(0, input.length() - completeness.remaining().length()); + newStartOffset = startOffset + input.length() - completeness.remaining().length(); + input = completeness.remaining(); + cont = true; + } else { + current = input; + cont = false; + newStartOffset = startOffset; + } + + boolean[] moduleImport = new boolean[1]; + + switch (guessKind(current, moduleImport)) { + case IMPORT -> { + lastImport = current; + lastImportIsModuleImport = moduleImport[0]; + } + case CLASS, METHOD -> { + pendingWrap = declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current); + } + case VARIABLE -> { + declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current); + pendingWrap = Wrap.methodWrap(whitespaces(code, startOffset) + current); + } + default -> { + pendingWrap = declarationWrap = Wrap.methodWrap(whitespaces(code, startOffset) + current); + } + } + + startOffset = newStartOffset; + } + + if (lastImport != null) { + if (wrapImports) { + return proc.outerMap.wrapImport(Wrap.simpleWrap(whitespaces(code, startOffset) + lastImport + (!lastImportIsModuleImport ? "any.any" : "")), null); + } else { + return null; + } + } + + if (pendingWrap != null) { + return proc.outerMap.wrapInTrialClass(imports, declarationParts, pendingWrap); + } + + throw new IllegalStateException("No pending wrap for: " + code); + } + + private static String whitespaces(String input, int offset) { + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < offset; i++) { + if (input.charAt(i) == '\n') { + result.append('\n'); + } else { + result.append(' '); + } + } + + return result.toString(); + } + /** * A candidate for continuation of the given user's input. */ diff --git a/test/langtools/jdk/jshell/CompletionSuggestionTest.java b/test/langtools/jdk/jshell/CompletionSuggestionTest.java index 19f7b89a1b3..7907fa4d027 100644 --- a/test/langtools/jdk/jshell/CompletionSuggestionTest.java +++ b/test/langtools/jdk/jshell/CompletionSuggestionTest.java @@ -941,4 +941,14 @@ public class CompletionSuggestionTest extends KullaTesting { assertEval("import static java.lang.annotation.RetentionPolicy.*;"); assertCompletion("@AnnA(C|", true, "CLASS"); } + + @Test + public void testMultiSnippet() { + assertCompletion("String s = \"\"; s.len|", true, "length()"); + assertCompletion("String s() { return \"\"; } s().len|", true, "length()"); + assertCompletion("String s() { return \"\"; } import java.util.List; List.o|", true, "of("); + assertCompletion("String s() { return \"\"; } import java.ut| ", true, "util."); + assertCompletion("class S { public int length() { return 0; } } new S().len|", true, "length()"); + assertSignature("void f() { } f(|", "void f()"); + } } From dfd383224dbc2e41c9f44b1acd09ffb179cc38f3 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Thu, 2 Oct 2025 08:58:54 +0000 Subject: [PATCH 324/556] 8368966: Remove spurious VMStructs friends Reviewed-by: stefank, ayang --- src/hotspot/share/ci/ciObjectFactory.hpp | 1 - src/hotspot/share/classfile/compactHashtable.hpp | 1 - src/hotspot/share/code/codeBlob.hpp | 4 ---- src/hotspot/share/code/dependencyContext.hpp | 2 -- src/hotspot/share/code/nmethod.hpp | 1 - src/hotspot/share/compiler/abstractCompiler.hpp | 2 -- src/hotspot/share/compiler/oopMap.hpp | 1 - src/hotspot/share/gc/epsilon/epsilonBarrierSet.hpp | 2 -- src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp | 2 -- src/hotspot/share/gc/g1/g1Allocator.hpp | 2 -- src/hotspot/share/gc/g1/g1BarrierSet.hpp | 1 - src/hotspot/share/gc/g1/g1BlockOffsetTable.hpp | 4 +--- src/hotspot/share/gc/g1/g1CardTable.hpp | 1 - src/hotspot/share/gc/g1/g1ConcurrentMarkThread.hpp | 2 -- src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp | 1 - src/hotspot/share/gc/g1/g1HeapRegionRemSet.hpp | 2 -- src/hotspot/share/gc/g1/g1PageBasedVirtualSpace.hpp | 1 - src/hotspot/share/gc/parallel/mutableNUMASpace.hpp | 2 -- src/hotspot/share/gc/serial/cSpaceCounters.hpp | 2 -- src/hotspot/share/gc/shared/collectorCounters.hpp | 2 -- src/hotspot/share/gc/shared/gcPolicyCounters.hpp | 2 -- src/hotspot/share/gc/shared/generationCounters.hpp | 2 -- src/hotspot/share/gc/shared/hSpaceCounters.hpp | 2 -- src/hotspot/share/gc/shared/scavengableNMethods.hpp | 2 -- src/hotspot/share/gc/shared/stringdedup/stringDedupThread.hpp | 2 -- src/hotspot/share/gc/shenandoah/shenandoahCardTable.hpp | 2 -- src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp | 2 -- .../gc/shenandoah/shenandoahGenerationalControlThread.hpp | 2 -- src/hotspot/share/gc/shenandoah/shenandoahRegulatorThread.hpp | 2 -- src/hotspot/share/interpreter/templateInterpreter.hpp | 1 - src/hotspot/share/interpreter/zero/bytecodeInterpreter.hpp | 1 - src/hotspot/share/interpreter/zero/zeroInterpreter.hpp | 1 - src/hotspot/share/memory/heap.hpp | 1 - src/hotspot/share/memory/metaspaceCounters.cpp | 1 - src/hotspot/share/oops/instanceClassLoaderKlass.hpp | 1 - src/hotspot/share/oops/instanceMirrorKlass.hpp | 1 - src/hotspot/share/oops/instanceStackChunkKlass.hpp | 1 - src/hotspot/share/opto/matcher.hpp | 2 -- src/hotspot/share/opto/parse.hpp | 2 -- src/hotspot/share/runtime/java.hpp | 1 - src/hotspot/share/runtime/monitorDeflationThread.hpp | 1 - src/hotspot/share/runtime/nonJavaThread.hpp | 3 --- src/hotspot/share/runtime/notificationThread.hpp | 1 - src/hotspot/share/runtime/os.hpp | 1 - src/hotspot/share/runtime/serviceThread.hpp | 1 - src/hotspot/share/runtime/vmStructs.cpp | 4 ---- src/hotspot/share/utilities/growableArray.hpp | 3 +-- 47 files changed, 2 insertions(+), 79 deletions(-) diff --git a/src/hotspot/share/ci/ciObjectFactory.hpp b/src/hotspot/share/ci/ciObjectFactory.hpp index 15ff2e48eb6..fd7ca6bb801 100644 --- a/src/hotspot/share/ci/ciObjectFactory.hpp +++ b/src/hotspot/share/ci/ciObjectFactory.hpp @@ -37,7 +37,6 @@ // which ensures that for each oop, at most one ciObject is created. // This invariant allows efficient implementation of ciObject. class ciObjectFactory : public ArenaObj { - friend class VMStructs; friend class ciEnv; private: diff --git a/src/hotspot/share/classfile/compactHashtable.hpp b/src/hotspot/share/classfile/compactHashtable.hpp index cb241ef5e70..402c8f197fa 100644 --- a/src/hotspot/share/classfile/compactHashtable.hpp +++ b/src/hotspot/share/classfile/compactHashtable.hpp @@ -254,7 +254,6 @@ template < bool (*EQUALS)(V value, K key, int len) > class CompactHashtable : public SimpleCompactHashtable { - friend class VMStructs; V decode(u4 encoded_value) const { return DECODE(_base_address, encoded_value); diff --git a/src/hotspot/share/code/codeBlob.hpp b/src/hotspot/share/code/codeBlob.hpp index 0b4760e7e1e..0469b6c71b1 100644 --- a/src/hotspot/share/code/codeBlob.hpp +++ b/src/hotspot/share/code/codeBlob.hpp @@ -325,7 +325,6 @@ public: // RuntimeBlob: used for non-compiled method code (adapters, stubs, blobs) class RuntimeBlob : public CodeBlob { - friend class VMStructs; public: // Creation @@ -634,7 +633,6 @@ class DeoptimizationBlob: public SingletonBlob { #ifdef COMPILER2 class UncommonTrapBlob: public SingletonBlob { - friend class VMStructs; private: // Creation support UncommonTrapBlob( @@ -658,7 +656,6 @@ class UncommonTrapBlob: public SingletonBlob { // ExceptionBlob: used for exception unwinding in compiled code (currently only used by Compiler 2) class ExceptionBlob: public SingletonBlob { - friend class VMStructs; private: // Creation support ExceptionBlob( @@ -695,7 +692,6 @@ class ExceptionBlob: public SingletonBlob { // SafepointBlob: handles illegal_instruction exceptions during a safepoint class SafepointBlob: public SingletonBlob { - friend class VMStructs; private: // Creation support SafepointBlob( diff --git a/src/hotspot/share/code/dependencyContext.hpp b/src/hotspot/share/code/dependencyContext.hpp index 7e8f7163509..b08300ec645 100644 --- a/src/hotspot/share/code/dependencyContext.hpp +++ b/src/hotspot/share/code/dependencyContext.hpp @@ -42,7 +42,6 @@ class DepChange; // finding nmethods which might need to be deoptimized. // class nmethodBucket: public CHeapObj { - friend class VMStructs; private: nmethod* _nmethod; nmethodBucket* volatile _next; @@ -68,7 +67,6 @@ class nmethodBucket: public CHeapObj { // and uint64_t integer recording the safepoint counter at the last cleanup. // class DependencyContext : public StackObj { - friend class VMStructs; friend class TestDependencyContext; private: nmethodBucket* volatile* _dependency_context_addr; diff --git a/src/hotspot/share/code/nmethod.hpp b/src/hotspot/share/code/nmethod.hpp index 020370c504b..8dfccb07891 100644 --- a/src/hotspot/share/code/nmethod.hpp +++ b/src/hotspot/share/code/nmethod.hpp @@ -90,7 +90,6 @@ class ExceptionCache : public CHeapObj { // cache pc descs found in earlier inquiries class PcDescCache { - friend class VMStructs; private: enum { cache_size = 4 }; // The array elements MUST be volatile! Several threads may modify diff --git a/src/hotspot/share/compiler/abstractCompiler.hpp b/src/hotspot/share/compiler/abstractCompiler.hpp index d61ae639c3f..adc85efbed5 100644 --- a/src/hotspot/share/compiler/abstractCompiler.hpp +++ b/src/hotspot/share/compiler/abstractCompiler.hpp @@ -33,8 +33,6 @@ typedef void (*initializer)(void); // Per-compiler statistics class CompilerStatistics { - friend class VMStructs; - class Data { friend class VMStructs; public: diff --git a/src/hotspot/share/compiler/oopMap.hpp b/src/hotspot/share/compiler/oopMap.hpp index 4b962444738..ef8845ac9aa 100644 --- a/src/hotspot/share/compiler/oopMap.hpp +++ b/src/hotspot/share/compiler/oopMap.hpp @@ -479,7 +479,6 @@ private: // pointers are updated based on their base pointers new value and an offset. #if COMPILER2_OR_JVMCI class DerivedPointerTable : public AllStatic { - friend class VMStructs; private: class Entry; static bool _active; // do not record pointers for verify pass etc. diff --git a/src/hotspot/share/gc/epsilon/epsilonBarrierSet.hpp b/src/hotspot/share/gc/epsilon/epsilonBarrierSet.hpp index 7a8083df6bb..c146ff39680 100644 --- a/src/hotspot/share/gc/epsilon/epsilonBarrierSet.hpp +++ b/src/hotspot/share/gc/epsilon/epsilonBarrierSet.hpp @@ -30,8 +30,6 @@ // No interaction with application is required for Epsilon, and therefore // the barrier set is empty. class EpsilonBarrierSet: public BarrierSet { - friend class VMStructs; - public: EpsilonBarrierSet(); diff --git a/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp b/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp index 33a2690f777..51d0a8356d2 100644 --- a/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp +++ b/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp @@ -32,8 +32,6 @@ #include "services/memoryService.hpp" class EpsilonSpaceCounters: public CHeapObj { - friend class VMStructs; - private: PerfVariable* _capacity; PerfVariable* _used; diff --git a/src/hotspot/share/gc/g1/g1Allocator.hpp b/src/hotspot/share/gc/g1/g1Allocator.hpp index 8c382824b27..19b19c06e92 100644 --- a/src/hotspot/share/gc/g1/g1Allocator.hpp +++ b/src/hotspot/share/gc/g1/g1Allocator.hpp @@ -37,8 +37,6 @@ class G1NUMA; // some accessors (e.g. allocating into them, or getting their occupancy). // Also keeps track of retained regions across GCs. class G1Allocator : public CHeapObj { - friend class VMStructs; - private: G1CollectedHeap* _g1h; G1NUMA* _numa; diff --git a/src/hotspot/share/gc/g1/g1BarrierSet.hpp b/src/hotspot/share/gc/g1/g1BarrierSet.hpp index 40e87c373b7..20642cfc7e6 100644 --- a/src/hotspot/share/gc/g1/g1BarrierSet.hpp +++ b/src/hotspot/share/gc/g1/g1BarrierSet.hpp @@ -62,7 +62,6 @@ class Thread; // cards. // class G1BarrierSet: public CardTableBarrierSet { - friend class VMStructs; private: BufferNode::Allocator _satb_mark_queue_buffer_allocator; G1SATBMarkQueueSet _satb_mark_queue_set; diff --git a/src/hotspot/share/gc/g1/g1BlockOffsetTable.hpp b/src/hotspot/share/gc/g1/g1BlockOffsetTable.hpp index 59d5c4efcef..3b97efc4f0f 100644 --- a/src/hotspot/share/gc/g1/g1BlockOffsetTable.hpp +++ b/src/hotspot/share/gc/g1/g1BlockOffsetTable.hpp @@ -35,9 +35,7 @@ // into "N"-word subregions (where "N" = 2^"LogN". An array with an entry // for each such subregion indicates how far back one must go to find the // start of the chunk that includes the first word of the subregion. -class G1BlockOffsetTable: public CHeapObj { - friend class VMStructs; - +class G1BlockOffsetTable : public CHeapObj { private: // The reserved region covered by the table. MemRegion _reserved; diff --git a/src/hotspot/share/gc/g1/g1CardTable.hpp b/src/hotspot/share/gc/g1/g1CardTable.hpp index 060e5459778..ddbfa841c24 100644 --- a/src/hotspot/share/gc/g1/g1CardTable.hpp +++ b/src/hotspot/share/gc/g1/g1CardTable.hpp @@ -45,7 +45,6 @@ class G1CardTableChangedListener : public G1MappingChangedListener { }; class G1CardTable : public CardTable { - friend class VMStructs; friend class G1CardTableChangedListener; G1CardTableChangedListener _listener; diff --git a/src/hotspot/share/gc/g1/g1ConcurrentMarkThread.hpp b/src/hotspot/share/gc/g1/g1ConcurrentMarkThread.hpp index 5f9ec4ef404..22be7d9ffbb 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentMarkThread.hpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentMarkThread.hpp @@ -33,8 +33,6 @@ class G1Policy; // The concurrent mark thread triggers the various steps of the concurrent marking // cycle, including various marking cleanup. class G1ConcurrentMarkThread: public ConcurrentGCThread { - friend class VMStructs; - G1ConcurrentMark* _cm; enum ServiceState : uint { diff --git a/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp b/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp index 8e635247cd3..7cdc001d348 100644 --- a/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp +++ b/src/hotspot/share/gc/g1/g1ConcurrentRefineThread.hpp @@ -36,7 +36,6 @@ class G1ConcurrentRefine; // Concurrent refinement control thread watching card mark accrual on the card table // and starting refinement work. class G1ConcurrentRefineThread: public ConcurrentGCThread { - friend class VMStructs; friend class G1CollectedHeap; Monitor _notifier; diff --git a/src/hotspot/share/gc/g1/g1HeapRegionRemSet.hpp b/src/hotspot/share/gc/g1/g1HeapRegionRemSet.hpp index 023216f989d..0f78e0d6271 100644 --- a/src/hotspot/share/gc/g1/g1HeapRegionRemSet.hpp +++ b/src/hotspot/share/gc/g1/g1HeapRegionRemSet.hpp @@ -40,8 +40,6 @@ class G1CSetCandidateGroup; class outputStream; class G1HeapRegionRemSet : public CHeapObj { - friend class VMStructs; - // A set of nmethods whose code contains pointers into // the region that owns this RSet. G1CodeRootSet _code_roots; diff --git a/src/hotspot/share/gc/g1/g1PageBasedVirtualSpace.hpp b/src/hotspot/share/gc/g1/g1PageBasedVirtualSpace.hpp index 303acf62d0b..15155c1d668 100644 --- a/src/hotspot/share/gc/g1/g1PageBasedVirtualSpace.hpp +++ b/src/hotspot/share/gc/g1/g1PageBasedVirtualSpace.hpp @@ -45,7 +45,6 @@ class WorkerThreads; // The implementation gives an error when trying to commit or uncommit pages that // have already been committed or uncommitted. class G1PageBasedVirtualSpace { - friend class VMStructs; private: // Reserved area addresses. char* _low_boundary; diff --git a/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp b/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp index 3699dcde964..dc37b10292a 100644 --- a/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp +++ b/src/hotspot/share/gc/parallel/mutableNUMASpace.hpp @@ -60,8 +60,6 @@ */ class MutableNUMASpace : public MutableSpace { - friend class VMStructs; - class LGRPSpace : public CHeapObj { uint _lgrp_id; MutableSpace* _space; diff --git a/src/hotspot/share/gc/serial/cSpaceCounters.hpp b/src/hotspot/share/gc/serial/cSpaceCounters.hpp index b378f379afe..14c68e2b397 100644 --- a/src/hotspot/share/gc/serial/cSpaceCounters.hpp +++ b/src/hotspot/share/gc/serial/cSpaceCounters.hpp @@ -33,8 +33,6 @@ // that track a space; class CSpaceCounters: public CHeapObj { - friend class VMStructs; - private: PerfVariable* _capacity; PerfVariable* _used; diff --git a/src/hotspot/share/gc/shared/collectorCounters.hpp b/src/hotspot/share/gc/shared/collectorCounters.hpp index 64e59db86e4..273f8fb2a4e 100644 --- a/src/hotspot/share/gc/shared/collectorCounters.hpp +++ b/src/hotspot/share/gc/shared/collectorCounters.hpp @@ -31,8 +31,6 @@ // that track a collector class CollectorCounters: public CHeapObj { - friend class VMStructs; - private: PerfCounter* _invocations; PerfCounter* _time; diff --git a/src/hotspot/share/gc/shared/gcPolicyCounters.hpp b/src/hotspot/share/gc/shared/gcPolicyCounters.hpp index 1f8026d21ac..e2ddb537f6e 100644 --- a/src/hotspot/share/gc/shared/gcPolicyCounters.hpp +++ b/src/hotspot/share/gc/shared/gcPolicyCounters.hpp @@ -31,8 +31,6 @@ // that track a generation class GCPolicyCounters: public CHeapObj { - friend class VMStructs; - // Constant PerfData types don't need to retain a reference. // However, it's a good idea to document them here. // PerfStringConstant* _name; diff --git a/src/hotspot/share/gc/shared/generationCounters.hpp b/src/hotspot/share/gc/shared/generationCounters.hpp index cb835b44e25..3985f40a3aa 100644 --- a/src/hotspot/share/gc/shared/generationCounters.hpp +++ b/src/hotspot/share/gc/shared/generationCounters.hpp @@ -32,8 +32,6 @@ // that track a generation class GenerationCounters: public CHeapObj { - friend class VMStructs; - PerfVariable* _current_size; // Constant PerfData types don't need to retain a reference. diff --git a/src/hotspot/share/gc/shared/hSpaceCounters.hpp b/src/hotspot/share/gc/shared/hSpaceCounters.hpp index 01310e456f6..b55b24f7601 100644 --- a/src/hotspot/share/gc/shared/hSpaceCounters.hpp +++ b/src/hotspot/share/gc/shared/hSpaceCounters.hpp @@ -33,8 +33,6 @@ // that track a collections (logical spaces) in a heap; class HSpaceCounters: public CHeapObj { - friend class VMStructs; - private: PerfVariable* _capacity; PerfVariable* _used; diff --git a/src/hotspot/share/gc/shared/scavengableNMethods.hpp b/src/hotspot/share/gc/shared/scavengableNMethods.hpp index 31093a0482c..00db556577e 100644 --- a/src/hotspot/share/gc/shared/scavengableNMethods.hpp +++ b/src/hotspot/share/gc/shared/scavengableNMethods.hpp @@ -33,8 +33,6 @@ class nmethod; class NMethodToOopClosure; class ScavengableNMethods : public AllStatic { - friend class VMStructs; - static nmethod* _head; static BoolObjectClosure* _is_scavengable; diff --git a/src/hotspot/share/gc/shared/stringdedup/stringDedupThread.hpp b/src/hotspot/share/gc/shared/stringdedup/stringDedupThread.hpp index 504e1fe617f..b1feb1b4ca9 100644 --- a/src/hotspot/share/gc/shared/stringdedup/stringDedupThread.hpp +++ b/src/hotspot/share/gc/shared/stringdedup/stringDedupThread.hpp @@ -38,8 +38,6 @@ // not an inner class of StringDedup. This is because we need a simple public // identifier for use by VMStructs. class StringDedupThread : public JavaThread { - friend class VMStructs; - StringDedupThread(); ~StringDedupThread() = default; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahCardTable.hpp b/src/hotspot/share/gc/shenandoah/shenandoahCardTable.hpp index 33a4705b8d2..69b5a1abf72 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahCardTable.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahCardTable.hpp @@ -33,8 +33,6 @@ #define ShenandoahMinCardSizeInBytes 128 class ShenandoahCardTable: public CardTable { - friend class VMStructs; - private: // We maintain two copies of the card table to facilitate concurrent remembered set scanning // and concurrent clearing of stale remembered set information. During the init_mark safepoint, diff --git a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp index 9d95b5df7ed..4975404bbaa 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahControlThread.hpp @@ -33,8 +33,6 @@ #include "gc/shenandoah/shenandoahSharedVariables.hpp" class ShenandoahControlThread: public ShenandoahController { - friend class VMStructs; - private: typedef enum { none, diff --git a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp index 6a4f5bde578..b7dbedd5e84 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahGenerationalControlThread.hpp @@ -38,8 +38,6 @@ class ShenandoahGenerationalHeap; class ShenandoahHeap; class ShenandoahGenerationalControlThread: public ShenandoahController { - friend class VMStructs; - public: typedef enum { none, diff --git a/src/hotspot/share/gc/shenandoah/shenandoahRegulatorThread.hpp b/src/hotspot/share/gc/shenandoah/shenandoahRegulatorThread.hpp index a72d6004beb..c23690b15d6 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahRegulatorThread.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahRegulatorThread.hpp @@ -45,8 +45,6 @@ class ShenandoahOldHeuristics; * sleep time. */ class ShenandoahRegulatorThread: public ConcurrentGCThread { - friend class VMStructs; - public: explicit ShenandoahRegulatorThread(ShenandoahGenerationalControlThread* control_thread); diff --git a/src/hotspot/share/interpreter/templateInterpreter.hpp b/src/hotspot/share/interpreter/templateInterpreter.hpp index 8f118bb6ec8..fd6888ffab8 100644 --- a/src/hotspot/share/interpreter/templateInterpreter.hpp +++ b/src/hotspot/share/interpreter/templateInterpreter.hpp @@ -84,7 +84,6 @@ class DispatchTable { }; class TemplateInterpreter: public AbstractInterpreter { - friend class VMStructs; friend class InterpreterMacroAssembler; friend class TemplateInterpreterGenerator; friend class TemplateTable; diff --git a/src/hotspot/share/interpreter/zero/bytecodeInterpreter.hpp b/src/hotspot/share/interpreter/zero/bytecodeInterpreter.hpp index 9941055bfd4..0032109982a 100644 --- a/src/hotspot/share/interpreter/zero/bytecodeInterpreter.hpp +++ b/src/hotspot/share/interpreter/zero/bytecodeInterpreter.hpp @@ -79,7 +79,6 @@ friend class AbstractInterpreterGenerator; friend class ZeroInterpreterGenerator; friend class InterpreterMacroAssembler; friend class frame; -friend class VMStructs; public: enum messages { diff --git a/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp b/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp index 4cc78532379..79528de639b 100644 --- a/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp +++ b/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp @@ -36,7 +36,6 @@ class InterpreterCodelet; // of the c++ interpreter class ZeroInterpreter: public AbstractInterpreter { - friend class VMStructs; public: // Initialization/debugging static void initialize_stub(); diff --git a/src/hotspot/share/memory/heap.hpp b/src/hotspot/share/memory/heap.hpp index 5056f0f6c21..9db8aae79cb 100644 --- a/src/hotspot/share/memory/heap.hpp +++ b/src/hotspot/share/memory/heap.hpp @@ -70,7 +70,6 @@ class HeapBlock { }; class FreeBlock: public HeapBlock { - friend class VMStructs; protected: FreeBlock* _link; diff --git a/src/hotspot/share/memory/metaspaceCounters.cpp b/src/hotspot/share/memory/metaspaceCounters.cpp index b57373516f9..7396ac78655 100644 --- a/src/hotspot/share/memory/metaspaceCounters.cpp +++ b/src/hotspot/share/memory/metaspaceCounters.cpp @@ -32,7 +32,6 @@ #include "utilities/exceptions.hpp" class MetaspacePerfCounters { - friend class VMStructs; PerfVariable* _capacity; PerfVariable* _used; PerfVariable* _max_capacity; diff --git a/src/hotspot/share/oops/instanceClassLoaderKlass.hpp b/src/hotspot/share/oops/instanceClassLoaderKlass.hpp index 985c81c3cd6..f6aedc312aa 100644 --- a/src/hotspot/share/oops/instanceClassLoaderKlass.hpp +++ b/src/hotspot/share/oops/instanceClassLoaderKlass.hpp @@ -37,7 +37,6 @@ class ClassFileParser; // the list later? class InstanceClassLoaderKlass: public InstanceKlass { - friend class VMStructs; friend class InstanceKlass; public: static const KlassKind Kind = InstanceClassLoaderKlassKind; diff --git a/src/hotspot/share/oops/instanceMirrorKlass.hpp b/src/hotspot/share/oops/instanceMirrorKlass.hpp index e9928647db9..4546f904eca 100644 --- a/src/hotspot/share/oops/instanceMirrorKlass.hpp +++ b/src/hotspot/share/oops/instanceMirrorKlass.hpp @@ -41,7 +41,6 @@ class ClassFileParser; class InstanceMirrorKlass: public InstanceKlass { - friend class VMStructs; friend class InstanceKlass; public: diff --git a/src/hotspot/share/oops/instanceStackChunkKlass.hpp b/src/hotspot/share/oops/instanceStackChunkKlass.hpp index 19ce37c4ddd..5afbc5d00eb 100644 --- a/src/hotspot/share/oops/instanceStackChunkKlass.hpp +++ b/src/hotspot/share/oops/instanceStackChunkKlass.hpp @@ -100,7 +100,6 @@ Chunk layout: class InstanceStackChunkKlass: public InstanceKlass { - friend class VMStructs; friend class InstanceKlass; friend class Continuations; diff --git a/src/hotspot/share/opto/matcher.hpp b/src/hotspot/share/opto/matcher.hpp index 2ee2ded17b6..f4b0a7db873 100644 --- a/src/hotspot/share/opto/matcher.hpp +++ b/src/hotspot/share/opto/matcher.hpp @@ -43,8 +43,6 @@ class MachOper; //---------------------------Matcher------------------------------------------- class Matcher : public PhaseTransform { - friend class VMStructs; - public: // Machine-dependent definitions diff --git a/src/hotspot/share/opto/parse.hpp b/src/hotspot/share/opto/parse.hpp index 83b211828ce..19b302a8924 100644 --- a/src/hotspot/share/opto/parse.hpp +++ b/src/hotspot/share/opto/parse.hpp @@ -41,8 +41,6 @@ class SwitchRange; //------------------------------InlineTree------------------------------------- class InlineTree : public AnyObj { - friend class VMStructs; - Compile* C; // cache JVMState* _caller_jvms; // state of caller ciMethod* _method; // method being called by the caller_jvms diff --git a/src/hotspot/share/runtime/java.hpp b/src/hotspot/share/runtime/java.hpp index 21cbd76431f..67cf3fb686a 100644 --- a/src/hotspot/share/runtime/java.hpp +++ b/src/hotspot/share/runtime/java.hpp @@ -68,7 +68,6 @@ extern bool is_vm_statically_linked(); * string prior to JDK 1.6 was removed (partial initialization) */ class JDK_Version { - friend class VMStructs; friend class Universe; friend void JDK_Version_init(); private: diff --git a/src/hotspot/share/runtime/monitorDeflationThread.hpp b/src/hotspot/share/runtime/monitorDeflationThread.hpp index c0ae9dd2d6f..6b681617a4c 100644 --- a/src/hotspot/share/runtime/monitorDeflationThread.hpp +++ b/src/hotspot/share/runtime/monitorDeflationThread.hpp @@ -30,7 +30,6 @@ // A hidden from external view JavaThread for deflating idle monitors. class MonitorDeflationThread : public JavaThread { - friend class VMStructs; private: static void monitor_deflation_thread_entry(JavaThread* thread, TRAPS); diff --git a/src/hotspot/share/runtime/nonJavaThread.hpp b/src/hotspot/share/runtime/nonJavaThread.hpp index 6d9095924d9..b2ac6a31737 100644 --- a/src/hotspot/share/runtime/nonJavaThread.hpp +++ b/src/hotspot/share/runtime/nonJavaThread.hpp @@ -28,8 +28,6 @@ #include "runtime/thread.hpp" class NonJavaThread: public Thread { - friend class VMStructs; - NonJavaThread* volatile _next; class List; @@ -103,7 +101,6 @@ class NamedThread: public NonJavaThread { // A single WatcherThread is used for simulating timer interrupts. class WatcherThread: public NonJavaThread { - friend class VMStructs; protected: virtual void run(); diff --git a/src/hotspot/share/runtime/notificationThread.hpp b/src/hotspot/share/runtime/notificationThread.hpp index 17977e93539..e8e72052e4a 100644 --- a/src/hotspot/share/runtime/notificationThread.hpp +++ b/src/hotspot/share/runtime/notificationThread.hpp @@ -33,7 +33,6 @@ // breakpoints inside registered MXBean notification listeners. class NotificationThread : public JavaThread { - friend class VMStructs; private: static void notification_thread_entry(JavaThread* thread, TRAPS); diff --git a/src/hotspot/share/runtime/os.hpp b/src/hotspot/share/runtime/os.hpp index 76695de2c1b..e008f29eecc 100644 --- a/src/hotspot/share/runtime/os.hpp +++ b/src/hotspot/share/runtime/os.hpp @@ -168,7 +168,6 @@ public: }; class os: AllStatic { - friend class VMStructs; friend class JVMCIVMStructs; friend class MallocTracker; diff --git a/src/hotspot/share/runtime/serviceThread.hpp b/src/hotspot/share/runtime/serviceThread.hpp index d029c64590e..f65847ece00 100644 --- a/src/hotspot/share/runtime/serviceThread.hpp +++ b/src/hotspot/share/runtime/serviceThread.hpp @@ -34,7 +34,6 @@ class JvmtiDeferredEvent; class ServiceThread : public JavaThread { - friend class VMStructs; private: DEBUG_ONLY(static JavaThread* _instance;) static JvmtiDeferredEvent* _jvmti_event; diff --git a/src/hotspot/share/runtime/vmStructs.cpp b/src/hotspot/share/runtime/vmStructs.cpp index 10069e849bc..5a3850feac8 100644 --- a/src/hotspot/share/runtime/vmStructs.cpp +++ b/src/hotspot/share/runtime/vmStructs.cpp @@ -58,11 +58,8 @@ #include "oops/constMethod.hpp" #include "oops/cpCache.hpp" #include "oops/fieldInfo.hpp" -#include "oops/instanceClassLoaderKlass.hpp" #include "oops/instanceKlass.hpp" -#include "oops/instanceMirrorKlass.hpp" #include "oops/instanceOop.hpp" -#include "oops/instanceStackChunkKlass.hpp" #include "oops/klass.hpp" #include "oops/klassVtable.hpp" #include "oops/markWord.hpp" @@ -84,7 +81,6 @@ #include "runtime/deoptimization.hpp" #include "runtime/flags/jvmFlag.hpp" #include "runtime/globals.hpp" -#include "runtime/java.hpp" #include "runtime/javaCalls.hpp" #include "runtime/javaThread.hpp" #include "runtime/jniHandles.hpp" diff --git a/src/hotspot/share/utilities/growableArray.hpp b/src/hotspot/share/utilities/growableArray.hpp index 53403ca5cf1..1fef271c8c0 100644 --- a/src/hotspot/share/utilities/growableArray.hpp +++ b/src/hotspot/share/utilities/growableArray.hpp @@ -317,8 +317,6 @@ public: // - void Derived::deallocate(E*) - member function responsible for deallocation template class GrowableArrayWithAllocator : public GrowableArrayView { - friend class VMStructs; - void expand_to(int j); void grow(int j); @@ -714,6 +712,7 @@ public: template class GrowableArray : public GrowableArrayWithAllocator> { + friend class VMStructs; friend class GrowableArrayWithAllocator; friend class GrowableArrayTest; From 8be16160d2a6275ff619ea4cebb725475c646052 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Thu, 2 Oct 2025 11:41:30 +0000 Subject: [PATCH 325/556] 8367609: serviceability/sa/ClhsdbPmap.java fails when built with Clang Reviewed-by: kevinw, cjplummer --- src/jdk.hotspot.agent/linux/native/libsaproc/ps_core.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/jdk.hotspot.agent/linux/native/libsaproc/ps_core.c b/src/jdk.hotspot.agent/linux/native/libsaproc/ps_core.c index c3aaaf440a3..9c4f0b0d357 100644 --- a/src/jdk.hotspot.agent/linux/native/libsaproc/ps_core.c +++ b/src/jdk.hotspot.agent/linux/native/libsaproc/ps_core.c @@ -420,9 +420,15 @@ static bool read_lib_segments(struct ps_prochandle* ph, int lib_fd, ELF_EHDR* li // Coredump stores value of p_memsz elf field // rounded up to page boundary. + // Account for the PH being at some vaddr offset from mapping in core file. + uint64_t lib_memsz = lib_php->p_memsz; + if (target_vaddr > existing_map->vaddr) { + lib_memsz += target_vaddr - existing_map->vaddr; + } + if ((existing_map->memsz != page_size) && (existing_map->fd != lib_fd) && - (ROUNDUP(existing_map->memsz, page_size) != ROUNDUP(lib_php->p_memsz, page_size))) { + (ROUNDUP(existing_map->memsz, page_size) != ROUNDUP(lib_memsz, page_size))) { print_error("address conflict @ 0x%lx (existing map size = %ld, size = %ld, flags = %d)\n", target_vaddr, existing_map->memsz, lib_php->p_memsz, lib_php->p_flags); From cc563c87cd277fbc96fb77af1e99f6c018ccc020 Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Thu, 2 Oct 2025 12:37:27 +0000 Subject: [PATCH 326/556] 8368866: compiler/codecache/stress/UnexpectedDeoptimizationTest.java intermittent timed out Reviewed-by: shade, mhaessig --- .../stress/UnexpectedDeoptimizationTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/hotspot/jtreg/compiler/codecache/stress/UnexpectedDeoptimizationTest.java b/test/hotspot/jtreg/compiler/codecache/stress/UnexpectedDeoptimizationTest.java index b3def9455c4..5ad047c826e 100644 --- a/test/hotspot/jtreg/compiler/codecache/stress/UnexpectedDeoptimizationTest.java +++ b/test/hotspot/jtreg/compiler/codecache/stress/UnexpectedDeoptimizationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,26 +31,26 @@ * * @build jdk.test.whitebox.WhiteBox compiler.codecache.stress.Helper compiler.codecache.stress.TestCaseImpl * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox - * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions + * @run main/othervm/timeout=240 -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions * -XX:+WhiteBoxAPI * -XX:+IgnoreUnrecognizedVMOptions -XX:-DeoptimizeRandom * -XX:CompileCommand=dontinline,compiler.codecache.stress.Helper$TestCase::method * -XX:-SegmentedCodeCache * compiler.codecache.stress.UnexpectedDeoptimizationTest - * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions + * @run main/othervm/timeout=240 -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions * -XX:+WhiteBoxAPI * -XX:+IgnoreUnrecognizedVMOptions -XX:-DeoptimizeRandom * -XX:CompileCommand=dontinline,compiler.codecache.stress.Helper$TestCase::method * -XX:+SegmentedCodeCache * compiler.codecache.stress.UnexpectedDeoptimizationTest - * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions + * @run main/othervm/timeout=240 -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions * -XX:+WhiteBoxAPI * -XX:+IgnoreUnrecognizedVMOptions -XX:-DeoptimizeRandom * -XX:CompileCommand=dontinline,compiler.codecache.stress.Helper$TestCase::method * -XX:-SegmentedCodeCache * -DhelperVirtualThread=true * compiler.codecache.stress.UnexpectedDeoptimizationTest - * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions + * @run main/othervm/timeout=240 -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions * -XX:+WhiteBoxAPI * -XX:+IgnoreUnrecognizedVMOptions -XX:-DeoptimizeRandom * -XX:CompileCommand=dontinline,compiler.codecache.stress.Helper$TestCase::method @@ -75,7 +75,7 @@ public class UnexpectedDeoptimizationTest implements Runnable { public void run() { Helper.WHITE_BOX.deoptimizeFrames(rng.nextBoolean()); // Sleep a short while to allow the stacks to grow - otherwise - // we end up running almost all code in the interpreter + // we end up running almost all code in the interpreter try { Thread.sleep(10); } catch (Exception e) { From 56baf64ada04f233fbfe4e0cd033c86183e22015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jeli=C5=84ski?= Date: Thu, 2 Oct 2025 13:29:45 +0000 Subject: [PATCH 327/556] 8368520: TLS 1.3 KeyUpdate fails with SunPKCS11 provider Reviewed-by: valeriep --- .../security/ssl/SSLTrafficKeyDerivation.java | 22 +++++++++---------- .../security/pkcs11/tls/fips/FipsModeTLS.java | 10 ++++++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java b/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java index 1db07c77160..5cb78ed44f7 100644 --- a/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java +++ b/src/java.base/share/classes/sun/security/ssl/SSLTrafficKeyDerivation.java @@ -29,13 +29,11 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.ProviderException; -import java.security.spec.AlgorithmParameterSpec; import javax.crypto.KDF; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.HKDFParameterSpec; import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.SSLHandshakeException; import sun.security.internal.spec.TlsKeyMaterialParameterSpec; import sun.security.internal.spec.TlsKeyMaterialSpec; @@ -191,26 +189,26 @@ enum SSLTrafficKeyDerivation implements SSLKeyDerivationGenerator { private enum KeySchedule { // Note that we use enum name as the key name. - TlsKey ("key", false), - TlsIv ("iv", true), - TlsUpdateNplus1 ("traffic upd", false); + TlsKey ("key"), + TlsIv ("iv"), + TlsUpdateNplus1 ("traffic upd"); private final byte[] label; - private final boolean isIv; - KeySchedule(String label, boolean isIv) { + KeySchedule(String label) { this.label = ("tls13 " + label).getBytes(); - this.isIv = isIv; } int getKeyLength(CipherSuite cs) { - if (this == KeySchedule.TlsUpdateNplus1) - return cs.hashAlg.hashLength; - return isIv ? cs.bulkCipher.ivSize : cs.bulkCipher.keySize; + return switch (this) { + case TlsUpdateNplus1 -> cs.hashAlg.hashLength; + case TlsIv -> cs.bulkCipher.ivSize; + case TlsKey -> cs.bulkCipher.keySize; + }; } String getAlgorithm(CipherSuite cs, String algorithm) { - return isIv ? algorithm : cs.bulkCipher.algorithm; + return this == TlsKey ? cs.bulkCipher.algorithm : algorithm; } } diff --git a/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java index a883239281c..c227e99d12b 100644 --- a/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java +++ b/test/jdk/sun/security/pkcs11/tls/fips/FipsModeTLS.java @@ -24,7 +24,7 @@ /* * @test - * @bug 8029661 8325164 8368073 8368514 + * @bug 8029661 8325164 8368073 8368514 8368520 * @summary Test TLS 1.2 and TLS 1.3 * @modules java.base/sun.security.internal.spec * java.base/sun.security.util @@ -88,6 +88,9 @@ public final class FipsModeTLS extends SecmodTest { private static PublicKey publicKey; public static void main(String[] args) throws Exception { + // reduce the limit to trigger a key update later + Security.setProperty("jdk.tls.keyLimits", + "AES/GCM/NoPadding KeyUpdate 10000"); try { initialize(); } catch (Exception e) { @@ -305,10 +308,11 @@ public final class FipsModeTLS extends SecmodTest { cTOs = ByteBuffer.allocateDirect(netBufferMax); sTOc = ByteBuffer.allocateDirect(netBufferMax); + // big enough to trigger a key update clientOut = ByteBuffer.wrap( - "Hi Server, I'm Client".getBytes()); + "a".repeat(16000).getBytes()); serverOut = ByteBuffer.wrap( - "Hello Client, I'm Server".getBytes()); + "b".repeat(16000).getBytes()); SSLEngineResult clientResult; SSLEngineResult serverResult; From 2c7f7380ea828e5ec928e1cb05b13806646ecb3d Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Thu, 2 Oct 2025 13:32:09 +0000 Subject: [PATCH 328/556] 8368825: Use switch expression for DateTimeFormatterBuilder pattern character lookup Reviewed-by: rriggs, naoto, scolebourne --- .../time/format/DateTimeFormatterBuilder.java | 91 ++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java index 7a2142e5113..9f5b82775b9 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -1937,7 +1937,7 @@ public final class DateTimeFormatterBuilder { padNext(pad); // pad and continue parsing } // main rules - TemporalField field = FIELD_MAP.get(cur); + TemporalField field = getField(cur); if (field != null) { parseField(cur, count, field); } else if (cur == 'z') { @@ -2185,48 +2185,55 @@ public final class DateTimeFormatterBuilder { } } - /** Map of letters to fields. */ - private static final Map FIELD_MAP = new HashMap<>(); - static { + /** + * Returns the TemporalField for the given pattern character. + * + * @param ch the pattern character + * @return the TemporalField for the given pattern character, or null if not applicable + */ + private static TemporalField getField(char ch) { // SDF = SimpleDateFormat - FIELD_MAP.put('G', ChronoField.ERA); // SDF, LDML (different to both for 1/2 chars) - FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // SDF, LDML - FIELD_MAP.put('u', ChronoField.YEAR); // LDML (different in SDF) - FIELD_MAP.put('Q', IsoFields.QUARTER_OF_YEAR); // LDML (removed quarter from 310) - FIELD_MAP.put('q', IsoFields.QUARTER_OF_YEAR); // LDML (stand-alone) - FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // SDF, LDML - FIELD_MAP.put('L', ChronoField.MONTH_OF_YEAR); // SDF, LDML (stand-alone) - FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // SDF, LDML - FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // SDF, LDML - FIELD_MAP.put('F', ChronoField.ALIGNED_WEEK_OF_MONTH); // SDF, LDML - FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // SDF, LDML (different to both for 1/2 chars) - FIELD_MAP.put('c', ChronoField.DAY_OF_WEEK); // LDML (stand-alone) - FIELD_MAP.put('e', ChronoField.DAY_OF_WEEK); // LDML (needs localized week number) - FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // SDF, LDML - FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // SDF, LDML - FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // SDF, LDML - FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // SDF, LDML - FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // SDF, LDML - FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // SDF, LDML - FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // SDF, LDML - FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // LDML (SDF uses milli-of-second number) - FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // LDML - FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 (proposed for LDML) - FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 (proposed for LDML) - FIELD_MAP.put('g', JulianFields.MODIFIED_JULIAN_DAY); - // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 - // 310 - Z - matches SimpleDateFormat and LDML - // 310 - V - time-zone id, matches LDML - // 310 - v - general timezone names, not matching exactly with LDML because LDML specify to fall back - // to 'VVVV' if general-nonlocation unavailable but here it's not falling back because of lack of data - // 310 - p - prefix for padding - // 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5 - // 310 - x - matches LDML - // 310 - w, W, and Y are localized forms matching LDML - // LDML - B - day periods - // LDML - U - cycle year name, not supported by 310 yet - // LDML - l - deprecated - // LDML - j - not relevant + return switch (ch) { + case 'G' -> ChronoField.ERA; // SDF, LDML (different to both for 1/2 chars) + case 'y' -> ChronoField.YEAR_OF_ERA; // SDF, LDML + case 'u' -> ChronoField.YEAR; // LDML (different in SDF) + case 'Q' -> IsoFields.QUARTER_OF_YEAR; // LDML (removed quarter from 310) + case 'q' -> IsoFields.QUARTER_OF_YEAR; // LDML (stand-alone) + case 'M' -> ChronoField.MONTH_OF_YEAR; // SDF, LDML + case 'L' -> ChronoField.MONTH_OF_YEAR; // SDF, LDML (stand-alone) + case 'D' -> ChronoField.DAY_OF_YEAR; // SDF, LDML + case 'd' -> ChronoField.DAY_OF_MONTH; // SDF, LDML + case 'F' -> ChronoField.ALIGNED_WEEK_OF_MONTH; // SDF, LDML + case 'E' -> ChronoField.DAY_OF_WEEK; // SDF, LDML (different to both for 1/2 chars) + case 'c' -> ChronoField.DAY_OF_WEEK; // LDML (stand-alone) + case 'e' -> ChronoField.DAY_OF_WEEK; // LDML (needs localized week number) + case 'a' -> ChronoField.AMPM_OF_DAY; // SDF, LDML + case 'H' -> ChronoField.HOUR_OF_DAY; // SDF, LDML + case 'k' -> ChronoField.CLOCK_HOUR_OF_DAY; // SDF, LDML + case 'K' -> ChronoField.HOUR_OF_AMPM; // SDF, LDML + case 'h' -> ChronoField.CLOCK_HOUR_OF_AMPM; // SDF, LDML + case 'm' -> ChronoField.MINUTE_OF_HOUR; // SDF, LDML + case 's' -> ChronoField.SECOND_OF_MINUTE; // SDF, LDML + case 'S' -> ChronoField.NANO_OF_SECOND; // LDML (SDF uses milli-of-second number) + case 'A' -> ChronoField.MILLI_OF_DAY; // LDML + case 'n' -> ChronoField.NANO_OF_SECOND; // 310 (proposed for LDML) + case 'N' -> ChronoField.NANO_OF_DAY; // 310 (proposed for LDML) + case 'g' -> JulianFields.MODIFIED_JULIAN_DAY; + default -> null; + // 310 - z - time-zone names, matches LDML and SimpleDateFormat 1 to 4 + // 310 - Z - matches SimpleDateFormat and LDML + // 310 - V - time-zone id, matches LDML + // 310 - v - general timezone names, not matching exactly with LDML because LDML specify to fall back + // to 'VVVV' if general-nonlocation unavailable but here it's not falling back because of lack of data + // 310 - p - prefix for padding + // 310 - X - matches LDML, almost matches SDF for 1, exact match 2&3, extended 4&5 + // 310 - x - matches LDML + // 310 - w, W, and Y are localized forms matching LDML + // LDML - B - day periods + // LDML - U - cycle year name, not supported by 310 yet + // LDML - l - deprecated + // LDML - j - not relevant + }; } //----------------------------------------------------------------------- From 5252262349cccb09f693ebd431fe2987ec0917f0 Mon Sep 17 00:00:00 2001 From: Casper Norrbin Date: Thu, 2 Oct 2025 13:38:41 +0000 Subject: [PATCH 329/556] 8292984: Refactor internal container-related interfaces for clarity Reviewed-by: sgehwolf, eosterlund --- .../os/linux/cgroupSubsystem_linux.cpp | 28 ++++------- .../os/linux/cgroupSubsystem_linux.hpp | 16 +++---- .../os/linux/cgroupV1Subsystem_linux.cpp | 46 +++++++++---------- .../os/linux/cgroupV1Subsystem_linux.hpp | 10 ++-- .../os/linux/cgroupV2Subsystem_linux.cpp | 24 +++++----- .../os/linux/cgroupV2Subsystem_linux.hpp | 6 +-- src/hotspot/os/linux/osContainer_linux.cpp | 38 ++++++++++++--- src/hotspot/os/linux/osContainer_linux.hpp | 1 + src/hotspot/os/linux/os_linux.cpp | 44 +++++++----------- 9 files changed, 109 insertions(+), 104 deletions(-) diff --git a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp index f935e2cbb9c..f5c4abeb4ca 100644 --- a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp @@ -665,15 +665,13 @@ int CgroupSubsystem::active_processor_count() { * -1 for unlimited * OSCONTAINER_ERROR for not supported */ -jlong CgroupSubsystem::memory_limit_in_bytes() { +jlong CgroupSubsystem::memory_limit_in_bytes(julong upper_bound) { CachingCgroupController* contrl = memory_controller(); CachedMetric* memory_limit = contrl->metrics_cache(); if (!memory_limit->should_check_metric()) { return memory_limit->value(); } - julong phys_mem = static_cast(os::Linux::physical_memory()); - log_trace(os, container)("total physical memory: " JULONG_FORMAT, phys_mem); - jlong mem_limit = contrl->controller()->read_memory_limit_in_bytes(phys_mem); + jlong mem_limit = contrl->controller()->read_memory_limit_in_bytes(upper_bound); // Update cached metric to avoid re-reading container settings too often memory_limit->set_value(mem_limit, OSCONTAINER_CACHE_TIMEOUT); return mem_limit; @@ -841,21 +839,16 @@ jlong CgroupController::limit_from_str(char* limit_str) { // CgroupSubsystem implementations -jlong CgroupSubsystem::memory_and_swap_limit_in_bytes() { - julong phys_mem = static_cast(os::Linux::physical_memory()); - julong host_swap = os::Linux::host_swap(); - return memory_controller()->controller()->memory_and_swap_limit_in_bytes(phys_mem, host_swap); +jlong CgroupSubsystem::memory_and_swap_limit_in_bytes(julong upper_mem_bound, julong upper_swap_bound) { + return memory_controller()->controller()->memory_and_swap_limit_in_bytes(upper_mem_bound, upper_swap_bound); } -jlong CgroupSubsystem::memory_and_swap_usage_in_bytes() { - julong phys_mem = static_cast(os::Linux::physical_memory()); - julong host_swap = os::Linux::host_swap(); - return memory_controller()->controller()->memory_and_swap_usage_in_bytes(phys_mem, host_swap); +jlong CgroupSubsystem::memory_and_swap_usage_in_bytes(julong upper_mem_bound, julong upper_swap_bound) { + return memory_controller()->controller()->memory_and_swap_usage_in_bytes(upper_mem_bound, upper_swap_bound); } -jlong CgroupSubsystem::memory_soft_limit_in_bytes() { - julong phys_mem = static_cast(os::Linux::physical_memory()); - return memory_controller()->controller()->memory_soft_limit_in_bytes(phys_mem); +jlong CgroupSubsystem::memory_soft_limit_in_bytes(julong upper_bound) { + return memory_controller()->controller()->memory_soft_limit_in_bytes(upper_bound); } jlong CgroupSubsystem::memory_throttle_limit_in_bytes() { @@ -894,7 +887,6 @@ jlong CgroupSubsystem::cpu_usage_in_micros() { return cpuacct_controller()->cpu_usage_in_micros(); } -void CgroupSubsystem::print_version_specific_info(outputStream* st) { - julong phys_mem = static_cast(os::Linux::physical_memory()); - memory_controller()->controller()->print_version_specific_info(st, phys_mem); +void CgroupSubsystem::print_version_specific_info(outputStream* st, julong upper_mem_bound) { + memory_controller()->controller()->print_version_specific_info(st, upper_mem_bound); } diff --git a/src/hotspot/os/linux/cgroupSubsystem_linux.hpp b/src/hotspot/os/linux/cgroupSubsystem_linux.hpp index 22e57d56c93..62a61432665 100644 --- a/src/hotspot/os/linux/cgroupSubsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupSubsystem_linux.hpp @@ -233,14 +233,14 @@ class CgroupMemoryController: public CHeapObj { public: virtual jlong read_memory_limit_in_bytes(julong upper_bound) = 0; virtual jlong memory_usage_in_bytes() = 0; - virtual jlong memory_and_swap_limit_in_bytes(julong host_mem, julong host_swap) = 0; - virtual jlong memory_and_swap_usage_in_bytes(julong host_mem, julong host_swap) = 0; + virtual jlong memory_and_swap_limit_in_bytes(julong upper_mem_bound, julong upper_swap_bound) = 0; + virtual jlong memory_and_swap_usage_in_bytes(julong upper_mem_bound, julong upper_swap_bound) = 0; virtual jlong memory_soft_limit_in_bytes(julong upper_bound) = 0; virtual jlong memory_throttle_limit_in_bytes() = 0; virtual jlong memory_max_usage_in_bytes() = 0; virtual jlong rss_usage_in_bytes() = 0; virtual jlong cache_usage_in_bytes() = 0; - virtual void print_version_specific_info(outputStream* st, julong host_mem) = 0; + virtual void print_version_specific_info(outputStream* st, julong upper_mem_bound) = 0; virtual bool needs_hierarchy_adjustment() = 0; virtual bool is_read_only() = 0; virtual const char* subsystem_path() = 0; @@ -251,7 +251,7 @@ class CgroupMemoryController: public CHeapObj { class CgroupSubsystem: public CHeapObj { public: - jlong memory_limit_in_bytes(); + jlong memory_limit_in_bytes(julong upper_bound); int active_processor_count(); virtual jlong pids_max() = 0; @@ -272,14 +272,14 @@ class CgroupSubsystem: public CHeapObj { jlong cpu_usage_in_micros(); jlong memory_usage_in_bytes(); - jlong memory_and_swap_limit_in_bytes(); - jlong memory_and_swap_usage_in_bytes(); - jlong memory_soft_limit_in_bytes(); + jlong memory_and_swap_limit_in_bytes(julong upper_mem_bound, julong upper_swap_bound); + jlong memory_and_swap_usage_in_bytes(julong upper_mem_bound, julong upper_swap_bound); + jlong memory_soft_limit_in_bytes(julong upper_bound); jlong memory_throttle_limit_in_bytes(); jlong memory_max_usage_in_bytes(); jlong rss_usage_in_bytes(); jlong cache_usage_in_bytes(); - void print_version_specific_info(outputStream* st); + void print_version_specific_info(outputStream* st, julong upper_mem_bound); }; // Utility class for storing info retrieved from /proc/cgroups, diff --git a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp index 64cb53eda28..90f01565b84 100644 --- a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp @@ -136,35 +136,35 @@ bool CgroupV1Controller::needs_hierarchy_adjustment() { } static inline -void verbose_log(julong read_mem_limit, julong host_mem) { +void verbose_log(julong read_mem_limit, julong upper_mem_bound) { if (log_is_enabled(Debug, os, container)) { jlong mem_limit = (jlong)read_mem_limit; // account for negative values - if (mem_limit < 0 || read_mem_limit >= host_mem) { + if (mem_limit < 0 || read_mem_limit >= upper_mem_bound) { const char *reason; if (mem_limit == OSCONTAINER_ERROR) { reason = "failed"; } else if (mem_limit == -1) { reason = "unlimited"; } else { - assert(read_mem_limit >= host_mem, "Expected read value exceeding host_mem"); + assert(read_mem_limit >= upper_mem_bound, "Expected read value exceeding upper memory bound"); // Exceeding physical memory is treated as unlimited. This implementation // caps it at host_mem since Cg v1 has no value to represent 'max'. reason = "ignored"; } - log_debug(os, container)("container memory limit %s: " JLONG_FORMAT ", using host value " JLONG_FORMAT, - reason, mem_limit, host_mem); + log_debug(os, container)("container memory limit %s: " JLONG_FORMAT ", upper bound is " JLONG_FORMAT, + reason, mem_limit, upper_mem_bound); } } } -jlong CgroupV1MemoryController::read_memory_limit_in_bytes(julong phys_mem) { +jlong CgroupV1MemoryController::read_memory_limit_in_bytes(julong upper_bound) { julong memlimit; CONTAINER_READ_NUMBER_CHECKED(reader(), "/memory.limit_in_bytes", "Memory Limit", memlimit); - if (memlimit >= phys_mem) { - verbose_log(memlimit, phys_mem); + if (memlimit >= upper_bound) { + verbose_log(memlimit, upper_bound); return (jlong)-1; } else { - verbose_log(memlimit, phys_mem); + verbose_log(memlimit, upper_bound); return (jlong)memlimit; } } @@ -181,10 +181,10 @@ jlong CgroupV1MemoryController::read_memory_limit_in_bytes(julong phys_mem) { * * -1 if there isn't any limit in place (note: includes values which exceed a physical * upper bound) */ -jlong CgroupV1MemoryController::read_mem_swap(julong host_total_memsw) { +jlong CgroupV1MemoryController::read_mem_swap(julong upper_memsw_bound) { julong memswlimit; CONTAINER_READ_NUMBER_CHECKED(reader(), "/memory.memsw.limit_in_bytes", "Memory and Swap Limit", memswlimit); - if (memswlimit >= host_total_memsw) { + if (memswlimit >= upper_memsw_bound) { log_trace(os, container)("Memory and Swap Limit is: Unlimited"); return (jlong)-1; } else { @@ -192,8 +192,8 @@ jlong CgroupV1MemoryController::read_mem_swap(julong host_total_memsw) { } } -jlong CgroupV1MemoryController::memory_and_swap_limit_in_bytes(julong host_mem, julong host_swap) { - jlong memory_swap = read_mem_swap(host_mem + host_swap); +jlong CgroupV1MemoryController::memory_and_swap_limit_in_bytes(julong upper_mem_bound, julong upper_swap_bound) { + jlong memory_swap = read_mem_swap(upper_mem_bound + upper_swap_bound); if (memory_swap == -1) { return memory_swap; } @@ -202,7 +202,7 @@ jlong CgroupV1MemoryController::memory_and_swap_limit_in_bytes(julong host_mem, // supported. jlong swappiness = read_mem_swappiness(); if (swappiness == 0 || memory_swap == OSCONTAINER_ERROR) { - jlong memlimit = read_memory_limit_in_bytes(host_mem); + jlong memlimit = read_memory_limit_in_bytes(upper_mem_bound); if (memory_swap == OSCONTAINER_ERROR) { log_trace(os, container)("Memory and Swap Limit has been reset to " JLONG_FORMAT " because swap is not supported", memlimit); } else { @@ -220,9 +220,9 @@ jlong memory_swap_usage_impl(CgroupController* ctrl) { return (jlong)memory_swap_usage; } -jlong CgroupV1MemoryController::memory_and_swap_usage_in_bytes(julong phys_mem, julong host_swap) { - jlong memory_sw_limit = memory_and_swap_limit_in_bytes(phys_mem, host_swap); - jlong memory_limit = read_memory_limit_in_bytes(phys_mem); +jlong CgroupV1MemoryController::memory_and_swap_usage_in_bytes(julong upper_mem_bound, julong upper_swap_bound) { + jlong memory_sw_limit = memory_and_swap_limit_in_bytes(upper_mem_bound, upper_swap_bound); + jlong memory_limit = read_memory_limit_in_bytes(upper_mem_bound); if (memory_sw_limit > 0 && memory_limit > 0) { jlong delta_swap = memory_sw_limit - memory_limit; if (delta_swap > 0) { @@ -238,10 +238,10 @@ jlong CgroupV1MemoryController::read_mem_swappiness() { return (jlong)swappiness; } -jlong CgroupV1MemoryController::memory_soft_limit_in_bytes(julong phys_mem) { +jlong CgroupV1MemoryController::memory_soft_limit_in_bytes(julong upper_bound) { julong memsoftlimit; CONTAINER_READ_NUMBER_CHECKED(reader(), "/memory.soft_limit_in_bytes", "Memory Soft Limit", memsoftlimit); - if (memsoftlimit >= phys_mem) { + if (memsoftlimit >= upper_bound) { log_trace(os, container)("Memory Soft Limit is: Unlimited"); return (jlong)-1; } else { @@ -336,10 +336,10 @@ jlong CgroupV1MemoryController::kernel_memory_usage_in_bytes() { return (jlong)kmem_usage; } -jlong CgroupV1MemoryController::kernel_memory_limit_in_bytes(julong phys_mem) { +jlong CgroupV1MemoryController::kernel_memory_limit_in_bytes(julong upper_bound) { julong kmem_limit; CONTAINER_READ_NUMBER_CHECKED(reader(), "/memory.kmem.limit_in_bytes", "Kernel Memory Limit", kmem_limit); - if (kmem_limit >= phys_mem) { + if (kmem_limit >= upper_bound) { return (jlong)-1; } return (jlong)kmem_limit; @@ -351,9 +351,9 @@ jlong CgroupV1MemoryController::kernel_memory_max_usage_in_bytes() { return (jlong)kmem_max_usage; } -void CgroupV1MemoryController::print_version_specific_info(outputStream* st, julong phys_mem) { +void CgroupV1MemoryController::print_version_specific_info(outputStream* st, julong mem_bound) { jlong kmem_usage = kernel_memory_usage_in_bytes(); - jlong kmem_limit = kernel_memory_limit_in_bytes(phys_mem); + jlong kmem_limit = kernel_memory_limit_in_bytes(mem_bound); jlong kmem_max_usage = kernel_memory_max_usage_in_bytes(); OSContainer::print_container_helper(st, kmem_limit, "kernel_memory_limit_in_bytes"); diff --git a/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp b/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp index 33bf2ed6e55..02b2c6a9fce 100644 --- a/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp @@ -79,17 +79,17 @@ class CgroupV1MemoryController final : public CgroupMemoryController { } jlong read_memory_limit_in_bytes(julong upper_bound) override; jlong memory_usage_in_bytes() override; - jlong memory_and_swap_limit_in_bytes(julong host_mem, julong host_swap) override; - jlong memory_and_swap_usage_in_bytes(julong host_mem, julong host_swap) override; + jlong memory_and_swap_limit_in_bytes(julong upper_mem_bound, julong upper_swap_bound) override; + jlong memory_and_swap_usage_in_bytes(julong upper_mem_bound, julong upper_swap_bound) override; jlong memory_soft_limit_in_bytes(julong upper_bound) override; jlong memory_throttle_limit_in_bytes() override; jlong memory_max_usage_in_bytes() override; jlong rss_usage_in_bytes() override; jlong cache_usage_in_bytes() override; jlong kernel_memory_usage_in_bytes(); - jlong kernel_memory_limit_in_bytes(julong host_mem); + jlong kernel_memory_limit_in_bytes(julong upper_bound); jlong kernel_memory_max_usage_in_bytes(); - void print_version_specific_info(outputStream* st, julong host_mem) override; + void print_version_specific_info(outputStream* st, julong upper_mem_bound) override; bool needs_hierarchy_adjustment() override { return reader()->needs_hierarchy_adjustment(); } @@ -101,7 +101,7 @@ class CgroupV1MemoryController final : public CgroupMemoryController { const char* cgroup_path() override { return reader()->cgroup_path(); } private: jlong read_mem_swappiness(); - jlong read_mem_swap(julong host_total_memsw); + jlong read_mem_swap(julong upper_memsw_bound); public: CgroupV1MemoryController(const CgroupV1Controller& reader) diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp index 661f7e909b5..38258a1f049 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp @@ -181,7 +181,7 @@ jlong CgroupV2MemoryController::memory_usage_in_bytes() { return (jlong)memusage; } -jlong CgroupV2MemoryController::memory_soft_limit_in_bytes(julong phys_mem) { +jlong CgroupV2MemoryController::memory_soft_limit_in_bytes(julong upper_bound) { jlong mem_soft_limit; CONTAINER_READ_NUMBER_CHECKED_MAX(reader(), "/memory.low", "Memory Soft Limit", mem_soft_limit); return mem_soft_limit; @@ -224,19 +224,19 @@ jlong CgroupV2MemoryController::cache_usage_in_bytes() { // respectively. In order to properly report a cgroup v1 like // compound value we need to sum the two values. Setting a swap limit // without also setting a memory limit is not allowed. -jlong CgroupV2MemoryController::memory_and_swap_limit_in_bytes(julong phys_mem, - julong host_swap /* unused in cg v2 */) { +jlong CgroupV2MemoryController::memory_and_swap_limit_in_bytes(julong upper_mem_bound, + julong upper_swap_bound /* unused in cg v2 */) { jlong swap_limit; bool is_ok = reader()->read_number_handle_max("/memory.swap.max", &swap_limit); if (!is_ok) { // Some container tests rely on this trace logging to happen. log_trace(os, container)("Swap Limit failed: %d", OSCONTAINER_ERROR); // swap disabled at kernel level, treat it as no swap - return read_memory_limit_in_bytes(phys_mem); + return read_memory_limit_in_bytes(upper_mem_bound); } log_trace(os, container)("Swap Limit is: " JLONG_FORMAT, swap_limit); if (swap_limit >= 0) { - jlong memory_limit = read_memory_limit_in_bytes(phys_mem); + jlong memory_limit = read_memory_limit_in_bytes(upper_mem_bound); assert(memory_limit >= 0, "swap limit without memory limit?"); return memory_limit + swap_limit; } @@ -252,7 +252,7 @@ jlong memory_swap_current_value(CgroupV2Controller* ctrl) { return (jlong)swap_current; } -jlong CgroupV2MemoryController::memory_and_swap_usage_in_bytes(julong host_mem, julong host_swap) { +jlong CgroupV2MemoryController::memory_and_swap_usage_in_bytes(julong upper_mem_bound, julong upper_swap_bound) { jlong memory_usage = memory_usage_in_bytes(); if (memory_usage >= 0) { jlong swap_current = memory_swap_current_value(reader()); @@ -276,7 +276,7 @@ jlong memory_limit_value(CgroupV2Controller* ctrl) { * memory limit in bytes or * -1 for unlimited, OSCONTAINER_ERROR for an error */ -jlong CgroupV2MemoryController::read_memory_limit_in_bytes(julong phys_mem) { +jlong CgroupV2MemoryController::read_memory_limit_in_bytes(julong upper_bound) { jlong limit = memory_limit_value(reader()); if (log_is_enabled(Trace, os, container)) { if (limit == -1) { @@ -287,18 +287,18 @@ jlong CgroupV2MemoryController::read_memory_limit_in_bytes(julong phys_mem) { } if (log_is_enabled(Debug, os, container)) { julong read_limit = (julong)limit; // avoid signed/unsigned compare - if (limit < 0 || read_limit >= phys_mem) { + if (limit < 0 || read_limit >= upper_bound) { const char* reason; if (limit == -1) { reason = "unlimited"; } else if (limit == OSCONTAINER_ERROR) { reason = "failed"; } else { - assert(read_limit >= phys_mem, "Expected mem limit to exceed host memory"); + assert(read_limit >= upper_bound, "Expected mem limit to exceed upper memory bound"); reason = "ignored"; } - log_debug(os, container)("container memory limit %s: " JLONG_FORMAT ", using host value " JLONG_FORMAT, - reason, limit, phys_mem); + log_debug(os, container)("container memory limit %s: " JLONG_FORMAT ", upper bound is " JLONG_FORMAT, + reason, limit, upper_bound); } } return limit; @@ -327,7 +327,7 @@ bool CgroupV2Controller::needs_hierarchy_adjustment() { return strcmp(_cgroup_path, "/") != 0; } -void CgroupV2MemoryController::print_version_specific_info(outputStream* st, julong phys_mem) { +void CgroupV2MemoryController::print_version_specific_info(outputStream* st, julong upper_mem_bound) { jlong swap_current = memory_swap_current_value(reader()); jlong swap_limit = memory_swap_limit_value(reader()); diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp index e26f37925ca..07db126ce90 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp @@ -115,15 +115,15 @@ class CgroupV2MemoryController final: public CgroupMemoryController { } jlong read_memory_limit_in_bytes(julong upper_bound) override; - jlong memory_and_swap_limit_in_bytes(julong host_mem, julong host_swp) override; - jlong memory_and_swap_usage_in_bytes(julong host_mem, julong host_swp) override; + jlong memory_and_swap_limit_in_bytes(julong upper_mem_bound, julong upper_swap_bound) override; + jlong memory_and_swap_usage_in_bytes(julong upper_mem_bound, julong upper_swap_bound) override; jlong memory_soft_limit_in_bytes(julong upper_bound) override; jlong memory_throttle_limit_in_bytes() override; jlong memory_usage_in_bytes() override; jlong memory_max_usage_in_bytes() override; jlong rss_usage_in_bytes() override; jlong cache_usage_in_bytes() override; - void print_version_specific_info(outputStream* st, julong host_mem) override; + void print_version_specific_info(outputStream* st, julong upper_mem_bound) override; bool is_read_only() override { return reader()->is_read_only(); } diff --git a/src/hotspot/os/linux/osContainer_linux.cpp b/src/hotspot/os/linux/osContainer_linux.cpp index 899e7535fde..561f2d4926c 100644 --- a/src/hotspot/os/linux/osContainer_linux.cpp +++ b/src/hotspot/os/linux/osContainer_linux.cpp @@ -84,8 +84,8 @@ void OSContainer::init() { // We can be in one of two cases: // 1.) On a physical Linux system without any limit // 2.) On a physical Linux system with a limit enforced by other means (like systemd slice) - any_mem_cpu_limit_present = cgroup_subsystem->memory_limit_in_bytes() > 0 || - os::Linux::active_processor_count() != cgroup_subsystem->active_processor_count(); + any_mem_cpu_limit_present = memory_limit_in_bytes() > 0 || + os::Linux::active_processor_count() != active_processor_count(); if (any_mem_cpu_limit_present) { reason = " because either a cpu or a memory limit is present"; } else { @@ -103,24 +103,47 @@ const char * OSContainer::container_type() { return cgroup_subsystem->container_type(); } +bool OSContainer::available_memory_in_container(julong& value) { + jlong mem_limit = memory_limit_in_bytes(); + jlong mem_usage = memory_usage_in_bytes(); + + if (mem_limit > 0 && mem_usage <= 0) { + log_debug(os, container)("container memory usage failed: " JLONG_FORMAT, mem_usage); + } + + if (mem_limit <= 0 || mem_usage <= 0) { + return false; + } + + value = mem_limit > mem_usage ? static_cast(mem_limit - mem_usage) : 0; + + return true; +} + jlong OSContainer::memory_limit_in_bytes() { assert(cgroup_subsystem != nullptr, "cgroup subsystem not available"); - return cgroup_subsystem->memory_limit_in_bytes(); + julong phys_mem = static_cast(os::Linux::physical_memory()); + return cgroup_subsystem->memory_limit_in_bytes(phys_mem); } jlong OSContainer::memory_and_swap_limit_in_bytes() { assert(cgroup_subsystem != nullptr, "cgroup subsystem not available"); - return cgroup_subsystem->memory_and_swap_limit_in_bytes(); + julong phys_mem = static_cast(os::Linux::physical_memory()); + julong host_swap = os::Linux::host_swap(); + return cgroup_subsystem->memory_and_swap_limit_in_bytes(phys_mem, host_swap); } jlong OSContainer::memory_and_swap_usage_in_bytes() { assert(cgroup_subsystem != nullptr, "cgroup subsystem not available"); - return cgroup_subsystem->memory_and_swap_usage_in_bytes(); + julong phys_mem = static_cast(os::Linux::physical_memory()); + julong host_swap = os::Linux::host_swap(); + return cgroup_subsystem->memory_and_swap_usage_in_bytes(phys_mem, host_swap); } jlong OSContainer::memory_soft_limit_in_bytes() { assert(cgroup_subsystem != nullptr, "cgroup subsystem not available"); - return cgroup_subsystem->memory_soft_limit_in_bytes(); + julong phys_mem = static_cast(os::Linux::physical_memory()); + return cgroup_subsystem->memory_soft_limit_in_bytes(phys_mem); } jlong OSContainer::memory_throttle_limit_in_bytes() { @@ -150,7 +173,8 @@ jlong OSContainer::cache_usage_in_bytes() { void OSContainer::print_version_specific_info(outputStream* st) { assert(cgroup_subsystem != nullptr, "cgroup subsystem not available"); - cgroup_subsystem->print_version_specific_info(st); + julong phys_mem = static_cast(os::Linux::physical_memory()); + cgroup_subsystem->print_version_specific_info(st, phys_mem); } char * OSContainer::cpu_cpuset_cpus() { diff --git a/src/hotspot/os/linux/osContainer_linux.hpp b/src/hotspot/os/linux/osContainer_linux.hpp index d9b024dbbb5..6258714c48b 100644 --- a/src/hotspot/os/linux/osContainer_linux.hpp +++ b/src/hotspot/os/linux/osContainer_linux.hpp @@ -50,6 +50,7 @@ class OSContainer: AllStatic { static inline bool is_containerized(); static const char * container_type(); + static bool available_memory_in_container(julong& value); static jlong memory_limit_in_bytes(); static jlong memory_and_swap_limit_in_bytes(); static jlong memory_and_swap_usage_in_bytes(); diff --git a/src/hotspot/os/linux/os_linux.cpp b/src/hotspot/os/linux/os_linux.cpp index 7159b2a78c4..772b170d11c 100644 --- a/src/hotspot/os/linux/os_linux.cpp +++ b/src/hotspot/os/linux/os_linux.cpp @@ -213,33 +213,20 @@ static bool suppress_primordial_thread_resolution = false; // utility functions -julong os::Linux::available_memory_in_container() { - julong avail_mem = static_cast(-1L); - if (OSContainer::is_containerized()) { - jlong mem_limit = OSContainer::memory_limit_in_bytes(); - jlong mem_usage; - if (mem_limit > 0 && (mem_usage = OSContainer::memory_usage_in_bytes()) < 1) { - log_debug(os, container)("container memory usage failed: " JLONG_FORMAT ", using host value", mem_usage); - } - if (mem_limit > 0 && mem_usage > 0) { - avail_mem = mem_limit > mem_usage ? (julong)mem_limit - (julong)mem_usage : 0; - } - } - return avail_mem; -} - bool os::available_memory(physical_memory_size_type& value) { - return Linux::available_memory(value); -} - -bool os::Linux::available_memory(physical_memory_size_type& value) { - julong avail_mem = available_memory_in_container(); - if (avail_mem != static_cast(-1L)) { + julong avail_mem = 0; + if (OSContainer::is_containerized() && OSContainer::available_memory_in_container(avail_mem)) { log_trace(os)("available container memory: " JULONG_FORMAT, avail_mem); value = static_cast(avail_mem); return true; } + return Linux::available_memory(value); +} + +bool os::Linux::available_memory(physical_memory_size_type& value) { + julong avail_mem = static_cast(-1L); + FILE *fp = os::fopen("/proc/meminfo", "r"); if (fp != nullptr) { char buf[80]; @@ -264,24 +251,25 @@ bool os::Linux::available_memory(physical_memory_size_type& value) { } bool os::free_memory(physical_memory_size_type& value) { + julong free_mem = 0; + if (OSContainer::is_containerized() && OSContainer::available_memory_in_container(free_mem)) { + log_trace(os)("free container memory: " JULONG_FORMAT, free_mem); + value = static_cast(free_mem); + return true; + } + return Linux::free_memory(value); } bool os::Linux::free_memory(physical_memory_size_type& value) { // values in struct sysinfo are "unsigned long" struct sysinfo si; - julong free_mem = available_memory_in_container(); - if (free_mem != static_cast(-1L)) { - log_trace(os)("free container memory: " JULONG_FORMAT, free_mem); - value = static_cast(free_mem); - return true; - } int ret = sysinfo(&si); if (ret != 0) { return false; } - free_mem = (julong)si.freeram * si.mem_unit; + julong free_mem = (julong)si.freeram * si.mem_unit; log_trace(os)("free memory: " JULONG_FORMAT, free_mem); value = static_cast(free_mem); return true; From 7ad9bdef1ed250a824ee9dee69b37bbcc8f7c924 Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Thu, 2 Oct 2025 15:19:26 +0000 Subject: [PATCH 330/556] 8368630: java/net/httpclient/http3/H3ServerPushTest.java succeeds but fails in jtreg timeout Reviewed-by: syan, jpai --- test/jdk/java/net/httpclient/http3/H3ServerPushTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java b/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java index 9b4858f50c9..ed5a14d8576 100644 --- a/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java +++ b/test/jdk/java/net/httpclient/http3/H3ServerPushTest.java @@ -78,7 +78,7 @@ import static org.junit.jupiter.api.Assertions.fail; * /test/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer * jdk.test.lib.net.SimpleSSLContext - * @run junit H3ServerPushTest + * @run junit/othervm/timeout=240 H3ServerPushTest */ /** From 1a03a1fbb1c7a83469128106341591c59428437a Mon Sep 17 00:00:00 2001 From: Igor Veresov Date: Thu, 2 Oct 2025 15:38:51 +0000 Subject: [PATCH 331/556] 8369033: Remove dead code in training data Reviewed-by: rcastanedalo, kvn --- src/hotspot/share/oops/trainingData.cpp | 17 ++++------------- src/hotspot/share/oops/trainingData.hpp | 15 +++++---------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/hotspot/share/oops/trainingData.cpp b/src/hotspot/share/oops/trainingData.cpp index ca5883a19ca..dcf95cf4653 100644 --- a/src/hotspot/share/oops/trainingData.cpp +++ b/src/hotspot/share/oops/trainingData.cpp @@ -25,7 +25,6 @@ #include "cds/cdsConfig.hpp" #include "ci/ciEnv.hpp" #include "ci/ciMetadata.hpp" -#include "classfile/classLoaderData.hpp" #include "classfile/compactHashtable.hpp" #include "classfile/javaClasses.hpp" #include "classfile/symbolTable.hpp" @@ -312,7 +311,6 @@ void CompileTrainingData::notice_jit_observation(ciEnv* env, ciBaseObject* what) CompileTask* task = env->task(); assert(task != nullptr, ""); Method* method = task->method(); - InstanceKlass* compiling_klass = method->method_holder(); if (what->is_metadata()) { ciMetadata* md = what->as_metadata(); if (md->is_loaded() && md->is_instance_klass()) { @@ -341,13 +339,7 @@ void KlassTrainingData::prepare(Visitor& visitor) { return; } visitor.visit(this); - ClassLoaderData* loader_data = nullptr; - if (_holder != nullptr) { - loader_data = _holder->class_loader_data(); - } else { - loader_data = java_lang_ClassLoader::loader_data(SystemDictionary::java_system_loader()); // default CLD - } - _comp_deps.prepare(loader_data); + _comp_deps.prepare(); } void MethodTrainingData::prepare(Visitor& visitor) { @@ -375,9 +367,8 @@ void CompileTrainingData::prepare(Visitor& visitor) { } visitor.visit(this); method()->prepare(visitor); - ClassLoaderData* loader_data = _method->klass()->class_loader_data(); - _init_deps.prepare(loader_data); - _ci_records.prepare(loader_data); + _init_deps.prepare(); + _ci_records.prepare(); } KlassTrainingData* KlassTrainingData::make(InstanceKlass* holder, bool null_if_not_found) { @@ -769,7 +760,7 @@ void CompileTrainingData::metaspace_pointers_do(MetaspaceClosure* iter) { } template -void TrainingData::DepList::prepare(ClassLoaderData* loader_data) { +void TrainingData::DepList::prepare() { if (_deps == nullptr && _deps_dyn != nullptr) { int len = _deps_dyn->length(); _deps = MetadataFactory::new_array_from_c_heap(len, mtClassShared); diff --git a/src/hotspot/share/oops/trainingData.hpp b/src/hotspot/share/oops/trainingData.hpp index d214a03a284..fbecb5c46bf 100644 --- a/src/hotspot/share/oops/trainingData.hpp +++ b/src/hotspot/share/oops/trainingData.hpp @@ -26,7 +26,6 @@ #define SHARE_OOPS_TRAININGDATA_HPP #include "cds/cdsConfig.hpp" -#include "classfile/classLoaderData.hpp" #include "classfile/compactHashtable.hpp" #include "compiler/compiler_globals.hpp" #include "compiler/compilerDefinitions.hpp" @@ -402,7 +401,7 @@ private: _deps_dyn = nullptr; } #endif - void prepare(ClassLoaderData* loader_data); + void prepare(); void metaspace_pointers_do(MetaspaceClosure *iter); }; @@ -479,10 +478,6 @@ class KlassTrainingData : public TrainingData { } virtual KlassTrainingData* as_KlassTrainingData() const { return const_cast(this); }; - ClassLoaderData* class_loader_data() { - assert(has_holder(), ""); - return holder()->class_loader_data(); - } void notice_fully_initialized() NOT_CDS_RETURN; void print_on(outputStream* st, bool name_only) const; @@ -620,8 +615,8 @@ public: #if INCLUDE_CDS void remove_unshareable_info() { _data.remove_unshareable_info(); } #endif - void prepare(ClassLoaderData* loader_data) { - _data.prepare(loader_data); + void prepare() { + _data.prepare(); } void metaspace_pointers_do(MetaspaceClosure *iter) { _data.metaspace_pointers_do(iter); @@ -639,8 +634,8 @@ public: ciMethod__inline_instructions_size.remove_unshareable_info(); } #endif - void prepare(ClassLoaderData* loader_data) { - ciMethod__inline_instructions_size.prepare(loader_data); + void prepare() { + ciMethod__inline_instructions_size.prepare(); } void metaspace_pointers_do(MetaspaceClosure *iter) { ciMethod__inline_instructions_size.metaspace_pointers_do(iter); From 5fccabff15ae8bcc3d03156fa331bbc0fefb0cbe Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Thu, 2 Oct 2025 15:51:46 +0000 Subject: [PATCH 332/556] 8368670: Deadlock in JFR on event register + class load Reviewed-by: mgronlun --- .../share/classes/jdk/jfr/internal/MetadataRepository.java | 2 +- src/jdk.jfr/share/classes/jdk/jfr/internal/SettingsManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/MetadataRepository.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/MetadataRepository.java index e574ab47992..22d55e6e99f 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/MetadataRepository.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/MetadataRepository.java @@ -291,7 +291,7 @@ public final class MetadataRepository { } } - synchronized boolean isEnabled(String eventName) { + boolean isEnabled(String eventName) { return settingsManager.isEnabled(eventName); } diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/SettingsManager.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/SettingsManager.java index 84caf7cb460..bd8fa078dc8 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/SettingsManager.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/SettingsManager.java @@ -129,7 +129,7 @@ final class SettingsManager { } } - private Map availableSettings = new LinkedHashMap<>(); + private volatile Map availableSettings = new LinkedHashMap<>(); void setSettings(List> activeSettings, boolean writeSettingEvents) { // store settings so they are available if a new event class is loaded From 3d113af9e33ddf3d80452cb72f1b47b4936ec6a0 Mon Sep 17 00:00:00 2001 From: Joe Darcy Date: Thu, 2 Oct 2025 16:43:01 +0000 Subject: [PATCH 333/556] 8369051: More small Float16 refactorings Reviewed-by: rgiulietti --- .../share/classes/jdk/incubator/vector/Float16.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java index b75adf350b6..a564cdfed0f 100644 --- a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java +++ b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java @@ -34,6 +34,7 @@ import java.math.BigInteger; import static jdk.incubator.vector.Float16Consts.SIGN_BIT_MASK; import static jdk.incubator.vector.Float16Consts.EXP_BIT_MASK; import static jdk.incubator.vector.Float16Consts.SIGNIF_BIT_MASK; +import static jdk.incubator.vector.Float16Consts.MAG_BIT_MASK; import static java.lang.Float.float16ToFloat; import static java.lang.Float.floatToFloat16; @@ -396,7 +397,7 @@ public final class Float16 } long f_signif_bits = doppel & 0x000f_ffff_ffff_ffffL | msb; - int PRECISION_DIFF = Double.PRECISION - PRECISION; // 42 + final int PRECISION_DIFF = Double.PRECISION - PRECISION; // 42 // Significand bits as if using rounding to zero (truncation). short signif_bits = (short)(f_signif_bits >> (PRECISION_DIFF + expdelta)); @@ -774,7 +775,7 @@ public final class Float16 * @see Double#isFinite(double) */ public static boolean isFinite(Float16 f16) { - return (float16ToRawShortBits(f16) & (EXP_BIT_MASK | SIGNIF_BIT_MASK)) <= + return (float16ToRawShortBits(f16) & MAG_BIT_MASK) <= float16ToRawShortBits(MAX_VALUE); } @@ -1093,7 +1094,7 @@ public final class Float16 * 2) performing the operation in the wider format * 3) converting the result from 2) to the narrower format * - * For example, this property hold between the formats used for the + * For example, this property holds between the formats used for the * float and double types. Therefore, the following is a valid * implementation of a float addition: * @@ -1477,7 +1478,7 @@ public final class Float16 // operation. Therefore, in this case do _not_ use the float // unary minus as an implementation as that is not guaranteed // to flip the sign bit of a NaN. - return shortBitsToFloat16((short)(f16.value ^ (short)0x0000_8000)); + return shortBitsToFloat16((short)(f16.value ^ SIGN_BIT_MASK)); } /** @@ -1495,7 +1496,7 @@ public final class Float16 public static Float16 abs(Float16 f16) { // Zero out sign bit. Per IEE 754-2019 section 5.5.1, abs is a // bit-level operation and not a logical operation. - return shortBitsToFloat16((short)(f16.value & (short)0x0000_7FFF)); + return shortBitsToFloat16((short)(f16.value & MAG_BIT_MASK)); } /** From 1d55adee11fc2fdbf2e009e1308b763fd7217dad Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Thu, 2 Oct 2025 18:49:00 +0000 Subject: [PATCH 334/556] 8368989: Use NMethodMarkingScope and ThreadsClaimTokenScope in shenandoahSTWMark Reviewed-by: shade, ayang --- src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp b/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp index 260c1e0276f..53391a3e224 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahSTWMark.cpp @@ -24,8 +24,7 @@ */ - -#include "gc/shared/strongRootsScope.hpp" +#include "code/nmethod.hpp" #include "gc/shared/taskTerminator.hpp" #include "gc/shared/workerThread.hpp" #include "gc/shenandoah/shenandoahClosures.inline.hpp" @@ -36,10 +35,13 @@ #include "gc/shenandoah/shenandoahRootProcessor.inline.hpp" #include "gc/shenandoah/shenandoahSTWMark.hpp" #include "gc/shenandoah/shenandoahVerifier.hpp" +#include "runtime/threads.hpp" class ShenandoahSTWMarkTask : public WorkerTask { private: ShenandoahSTWMark* const _mark; + NMethodMarkingScope _nmethod_marking_scope; + ThreadsClaimTokenScope _threads_claim_token_scope; public: ShenandoahSTWMarkTask(ShenandoahSTWMark* mark); @@ -48,7 +50,9 @@ public: ShenandoahSTWMarkTask::ShenandoahSTWMarkTask(ShenandoahSTWMark* mark) : WorkerTask("Shenandoah STW mark"), - _mark(mark) { + _mark(mark), + _nmethod_marking_scope(), + _threads_claim_token_scope() { } void ShenandoahSTWMarkTask::work(uint worker_id) { @@ -98,7 +102,6 @@ void ShenandoahSTWMark::mark() { _generation->scan_remembered_set(false /* is_concurrent */); } - StrongRootsScope scope(nworkers); ShenandoahSTWMarkTask task(this); heap->workers()->run_task(&task); From 3f27a03bba4760694a276376d08fb1ba97d08f7e Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Thu, 2 Oct 2025 20:00:58 +0000 Subject: [PATCH 335/556] 8368727: CDS custom loader support causes asserts during class unloading Reviewed-by: coleenp, dholmes --- .../classfile/systemDictionaryShared.cpp | 1 - src/hotspot/share/oops/klass.cpp | 7 ++- .../UnloadUnregisteredLoaderTest.java | 18 ++++++-- .../test-classes/CustomLoadee5.java | 2 +- .../UnloadUnregisteredLoader.java | 45 ++++++++++++++++++- 5 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/hotspot/share/classfile/systemDictionaryShared.cpp b/src/hotspot/share/classfile/systemDictionaryShared.cpp index da022436292..b092e71f4e7 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.cpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.cpp @@ -174,7 +174,6 @@ InstanceKlass* SystemDictionaryShared::acquire_class_for_current_thread( // No longer holding SharedDictionary_lock // No need to lock, as can be held only by a single thread. - loader_data->add_class(ik); // Get the package entry. PackageEntry* pkg_entry = CDSProtectionDomain::get_package_entry_from_class(ik, class_loader); diff --git a/src/hotspot/share/oops/klass.cpp b/src/hotspot/share/oops/klass.cpp index b3386694b79..208a274d766 100644 --- a/src/hotspot/share/oops/klass.cpp +++ b/src/hotspot/share/oops/klass.cpp @@ -872,11 +872,10 @@ void Klass::restore_unshareable_info(ClassLoaderData* loader_data, Handle protec // modify the CLD list outside a safepoint. if (class_loader_data() == nullptr) { set_class_loader_data(loader_data); - - // Add to class loader list first before creating the mirror - // (same order as class file parsing) - loader_data->add_class(this); } + // Add to class loader list first before creating the mirror + // (same order as class file parsing) + loader_data->add_class(this); Handle loader(THREAD, loader_data->class_loader()); ModuleEntry* module_entry = nullptr; diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/UnloadUnregisteredLoaderTest.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/UnloadUnregisteredLoaderTest.java index b7f472f5c0e..db26c6cbd18 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/UnloadUnregisteredLoaderTest.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/UnloadUnregisteredLoaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,9 +29,10 @@ * @requires vm.cds * @requires vm.cds.custom.loaders * @requires vm.opt.final.ClassUnloading - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds + * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds /test/hotspot/jtreg/runtime/cds/appcds/test-classes * @build jdk.test.whitebox.WhiteBox jdk.test.lib.classloader.ClassUnloadCommon * @compile test-classes/UnloadUnregisteredLoader.java test-classes/CustomLoadee.java + * test-classes/CustomLoadee5.java test-classes/CustomLoadee5Child.java * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox * jdk.test.lib.classloader.ClassUnloadCommon * jdk.test.lib.classloader.ClassUnloadCommon$1 @@ -49,12 +50,19 @@ public class UnloadUnregisteredLoaderTest { CDSOptions.disableRuntimePrefixForEpsilonGC(); } public static void main(String[] args) throws Exception { - String appJar1 = JarBuilder.build("UnloadUnregisteredLoader_app1", "UnloadUnregisteredLoader"); + String appJar1 = JarBuilder.build("UnloadUnregisteredLoader_app1", + "UnloadUnregisteredLoader", + "UnloadUnregisteredLoader$CustomLoader", + "CustomLoadee5", + "Util"); String appJar2 = JarBuilder.build(true, "UnloadUnregisteredLoader_app2", "jdk/test/lib/classloader/ClassUnloadCommon", "jdk/test/lib/classloader/ClassUnloadCommon$1", "jdk/test/lib/classloader/ClassUnloadCommon$TestFailure"); - String customJarPath = JarBuilder.build("UnloadUnregisteredLoader_custom", "CustomLoadee"); + String customJarPath = JarBuilder.build("UnloadUnregisteredLoader_custom", + "CustomLoadee", + "CustomLoadee5", + "CustomLoadee5Child"); String wbJar = JarBuilder.build(true, "WhiteBox", "jdk/test/whitebox/WhiteBox"); String use_whitebox_jar = "-Xbootclasspath/a:" + wbJar; @@ -66,6 +74,8 @@ public class UnloadUnregisteredLoaderTest { "jdk/test/lib/classloader/ClassUnloadCommon$TestFailure", "java/lang/Object id: 1", "CustomLoadee id: 2 super: 1 source: " + customJarPath, + "CustomLoadee5 id: 3 super: 1 source: " + customJarPath, + "CustomLoadee5Child id: 4 super: 3 source: " + customJarPath, }; OutputAnalyzer output; diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java index f3cda51c9fa..ba9f6dafa95 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/CustomLoadee5.java @@ -21,7 +21,7 @@ * questions. */ -class CustomLoadee5 { +public class CustomLoadee5 { public String toString() { return "this is CustomLoadee5"; } diff --git a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/UnloadUnregisteredLoader.java b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/UnloadUnregisteredLoader.java index 0e0e814327e..e6bd7407466 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/UnloadUnregisteredLoader.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/customLoader/test-classes/UnloadUnregisteredLoader.java @@ -23,6 +23,7 @@ */ import java.io.File; +import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import jdk.test.whitebox.WhiteBox; @@ -46,9 +47,11 @@ public class UnloadUnregisteredLoader { } } - public static void doit(URL urls[], String className, boolean isFirstTime) throws Exception { + public static void doit(URL urls[], String className, boolean isFirstTime) throws Exception { ClassLoader appLoader = UnloadUnregisteredLoader.class.getClassLoader(); - URLClassLoader custLoader = new URLClassLoader(urls, appLoader); + CustomLoader custLoader = new CustomLoader(urls, appLoader); + + // Part 1 -- load CustomLoadee. It should be loaded from archive when isFirstTime==true Class klass = custLoader.loadClass(className); WhiteBox wb = WhiteBox.getWhiteBox(); @@ -68,5 +71,43 @@ public class UnloadUnregisteredLoader { } } } + + // Part 2 + // + // CustomLoadee5 is never loaded from the archive, because the classfile bytes don't match + // CustomLoadee5Child is never loaded from the archive, its super is not loaded from the archive + try (InputStream in = appLoader.getResourceAsStream("CustomLoadee5.class")) { + byte[] b = in.readAllBytes(); + Util.replace(b, "this is", "DAS IST"); // Modify the bytecodes + Class c = custLoader.myDefineClass(b, 0, b.length); + System.out.println(c.newInstance()); + if (!"DAS IST CustomLoadee5".equals(c.newInstance().toString())) { + throw new RuntimeException("Bytecode modification not successful"); + } + if (wb.isSharedClass(c)) { + throw new RuntimeException(c + "should not be loaded from CDS"); + } + } + + // When isFirstTime==true, the VM will try to load the archived copy of CustomLoadee5Child, + // but it will fail (because CustomLoadee5 was not loaded from the archive) and will recover + // by decoding the class from its classfile data. + // This failure should not leave the JVM in an inconsistent state. + Class child = custLoader.loadClass("CustomLoadee5Child"); + if (wb.isSharedClass(child)) { + throw new RuntimeException(child + "should not be loaded from CDS"); + } + } + + static class CustomLoader extends URLClassLoader { + public CustomLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + public Class myDefineClass(byte[] b, int off, int len) + throws ClassFormatError + { + return super.defineClass(b, off, len); + } } } From fa6e884105ac247b3b83a5a2329f9c18888bd7d0 Mon Sep 17 00:00:00 2001 From: Damon Nguyen Date: Thu, 2 Oct 2025 22:09:14 +0000 Subject: [PATCH 336/556] 8298823: [macos] java/awt/Mouse/EnterExitEvents/DragWindowTest.java continues to fail with "No MouseReleased event on label!" Reviewed-by: aivanov, azvegint --- test/jdk/ProblemList.txt | 1 - .../Mouse/EnterExitEvents/DragWindowTest.java | 115 +++++++----------- 2 files changed, 46 insertions(+), 70 deletions(-) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 1602baa9894..8d13670805f 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -188,7 +188,6 @@ java/awt/Mixing/AWT_Mixing/JTextFieldOverlapping.java 8158801 windows-all java/awt/Mixing/AWT_Mixing/JToggleButtonInGlassPaneOverlapping.java 8158801 windows-all java/awt/Mixing/AWT_Mixing/JToggleButtonOverlapping.java 8158801 windows-all java/awt/Mixing/NonOpaqueInternalFrame.java 7124549 macosx-all -java/awt/Mouse/EnterExitEvents/DragWindowTest.java 8298823 macosx-all java/awt/Focus/ActualFocusedWindowTest/ActualFocusedWindowRetaining.java 6829264 generic-all java/awt/datatransfer/DragImage/MultiResolutionDragImageTest.java 8080982 generic-all java/awt/datatransfer/SystemFlavorMap/AddFlavorTest.java 8079268 linux-all diff --git a/test/jdk/java/awt/Mouse/EnterExitEvents/DragWindowTest.java b/test/jdk/java/awt/Mouse/EnterExitEvents/DragWindowTest.java index 4f789668c4a..2a77f294298 100644 --- a/test/jdk/java/awt/Mouse/EnterExitEvents/DragWindowTest.java +++ b/test/jdk/java/awt/Mouse/EnterExitEvents/DragWindowTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,11 +26,8 @@ * @key headful * @bug 7154048 * @summary Window created under a mouse does not receive mouse enter event. - * Mouse Entered/Exited events are wrongly generated during dragging the window - * from one component to another - * @library ../../regtesthelpers - * @build Util - * @author alexandr.scherbatiy area=awt.event + * Mouse Entered/Exited events are wrongly generated during dragging the + * window from one component to another * @run main DragWindowTest */ @@ -50,76 +47,67 @@ import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.SwingUtilities; -import java.util.concurrent.Callable; - -import test.java.awt.regtesthelpers.Util; - public class DragWindowTest { - private static volatile int dragWindowMouseEnteredCount = 0; - private static volatile int dragWindowMouseReleasedCount = 0; private static volatile int buttonMouseEnteredCount = 0; private static volatile int labelMouseReleasedCount = 0; + + private static volatile Point pointToClick; + private static volatile Point pointToDrag; + private static MyDragWindow dragWindow; private static JLabel label; private static JButton button; + private static JFrame frame; public static void main(String[] args) throws Exception { + try { + Robot robot = new Robot(); + robot.setAutoDelay(100); - Robot robot = new Robot(); - robot.setAutoDelay(100); + SwingUtilities.invokeAndWait(DragWindowTest::createAndShowGUI); - SwingUtilities.invokeAndWait(new Runnable() { + robot.delay(250); + robot.waitForIdle(); - @Override - public void run() { - createAndShowGUI(); + SwingUtilities.invokeAndWait(() -> { + pointToClick = getCenterPoint(label); + pointToDrag = getCenterPoint(button); + }); + + robot.mouseMove(pointToClick.x, pointToClick.y); + robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); + robot.waitForIdle(); + robot.delay(250); + + if (dragWindowMouseEnteredCount != 1) { + throw new RuntimeException("No MouseEntered event on Drag Window!"); } - }); - robot.delay(250); - robot.waitForIdle(); + // Reset entered count to check if mouse entered starting from here + buttonMouseEnteredCount = 0; + robot.mouseMove(pointToDrag.x, pointToDrag.y); + robot.waitForIdle(); + robot.delay(250); - Point pointToClick = Util.invokeOnEDT(new Callable() { - - @Override - public Point call() throws Exception { - return getCenterPoint(label); + if (buttonMouseEnteredCount != 0) { + throw new RuntimeException("Extra MouseEntered event on button!"); } - }); + robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); + robot.waitForIdle(); + robot.delay(250); - robot.mouseMove(pointToClick.x, pointToClick.y); - robot.mousePress(InputEvent.BUTTON1_MASK); - robot.waitForIdle(); - - if (dragWindowMouseEnteredCount != 1) { - throw new RuntimeException("No MouseEntered event on Drag Window!"); - } - - Point pointToDrag = Util.invokeOnEDT(new Callable() { - - @Override - public Point call() throws Exception { - button.addMouseListener(new ButtonMouseListener()); - return getCenterPoint(button); + if (labelMouseReleasedCount != 1) { + throw new RuntimeException("No MouseReleased event on label!"); } - }); - - robot.mouseMove(pointToDrag.x, pointToDrag.y); - robot.waitForIdle(); - - if (buttonMouseEnteredCount != 0) { - throw new RuntimeException("Extra MouseEntered event on button!"); + } finally { + SwingUtilities.invokeAndWait(() -> { + if (frame != null) { + frame.dispose(); + } + }); } - - robot.mouseRelease(InputEvent.BUTTON1_MASK); - robot.waitForIdle(); - - if (labelMouseReleasedCount != 1) { - throw new RuntimeException("No MouseReleased event on label!"); - } - } private static Point getCenterPoint(Component comp) { @@ -129,8 +117,7 @@ public class DragWindowTest { } private static void createAndShowGUI() { - - JFrame frame = new JFrame("Main Frame"); + frame = new JFrame("DragWindowTest"); frame.setSize(300, 200); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); @@ -142,6 +129,7 @@ public class DragWindowTest { button = new JButton("Button"); Panel panel = new Panel(new BorderLayout()); + button.addMouseListener(new ButtonMouseListener()); panel.add(label, BorderLayout.NORTH); panel.add(button, BorderLayout.CENTER); @@ -149,7 +137,6 @@ public class DragWindowTest { frame.getContentPane().add(panel); frame.setLocationRelativeTo(null); frame.setVisible(true); - } private static Point getAbsoluteLocation(MouseEvent e) { @@ -157,7 +144,6 @@ public class DragWindowTest { } static class MyDragWindow extends Window { - static int d = 30; public MyDragWindow(Window parent, Point location) { @@ -176,8 +162,6 @@ public class DragWindowTest { } static class LabelMouseListener extends MouseAdapter { - - Point origin; Window parent; public LabelMouseListener(Window parent) { @@ -210,20 +194,13 @@ public class DragWindowTest { } static class DragWindowMouseListener extends MouseAdapter { - @Override public void mouseEntered(MouseEvent e) { dragWindowMouseEnteredCount++; } - - @Override - public void mouseReleased(MouseEvent e) { - dragWindowMouseReleasedCount++; - } } static class ButtonMouseListener extends MouseAdapter { - @Override public void mouseEntered(MouseEvent e) { buttonMouseEnteredCount++; From da7121aff9eccb046b82a75093034f1cdbd9b9e4 Mon Sep 17 00:00:00 2001 From: Dean Long Date: Thu, 2 Oct 2025 22:21:13 +0000 Subject: [PATCH 337/556] 8366461: Remove obsolete method handle invoke logic Reviewed-by: vlivanov, mhaessig --- src/hotspot/cpu/aarch64/aarch64.ad | 4 - .../cpu/aarch64/c1_FrameMap_aarch64.cpp | 7 -- src/hotspot/cpu/aarch64/frame_aarch64.cpp | 45 +------ src/hotspot/cpu/aarch64/frame_aarch64.hpp | 9 +- .../cpu/aarch64/frame_aarch64.inline.hpp | 3 - src/hotspot/cpu/arm/arm.ad | 36 ------ src/hotspot/cpu/arm/arm_32.ad | 5 +- src/hotspot/cpu/arm/c1_FrameMap_arm.cpp | 5 - src/hotspot/cpu/arm/c1_Runtime1_arm.cpp | 12 -- src/hotspot/cpu/arm/frame_arm.cpp | 50 -------- src/hotspot/cpu/arm/frame_arm.hpp | 11 +- src/hotspot/cpu/arm/frame_arm.inline.hpp | 4 +- src/hotspot/cpu/arm/register_arm.hpp | 3 +- src/hotspot/cpu/arm/runtime_arm.cpp | 5 - src/hotspot/cpu/ppc/c1_FrameMap_ppc.cpp | 9 -- src/hotspot/cpu/ppc/ppc.ad | 5 - src/hotspot/cpu/riscv/c1_FrameMap_riscv.cpp | 5 - src/hotspot/cpu/riscv/frame_riscv.cpp | 46 +------ src/hotspot/cpu/riscv/frame_riscv.hpp | 9 +- src/hotspot/cpu/riscv/frame_riscv.inline.hpp | 5 +- src/hotspot/cpu/riscv/riscv.ad | 4 - src/hotspot/cpu/s390/c1_FrameMap_s390.cpp | 7 -- src/hotspot/cpu/s390/s390.ad | 5 - src/hotspot/cpu/x86/c1_FrameMap_x86.cpp | 7 -- src/hotspot/cpu/x86/frame_x86.cpp | 44 +------ src/hotspot/cpu/x86/frame_x86.hpp | 9 +- src/hotspot/cpu/x86/frame_x86.inline.hpp | 3 - src/hotspot/cpu/x86/x86_64.ad | 5 - src/hotspot/os/posix/signals_posix.cpp | 4 +- src/hotspot/os/windows/os_windows.cpp | 4 +- src/hotspot/share/asm/codeBuffer.hpp | 2 - src/hotspot/share/c1/c1_Compilation.cpp | 9 -- src/hotspot/share/c1/c1_Compilation.hpp | 5 - src/hotspot/share/c1/c1_FrameMap.hpp | 3 - src/hotspot/share/c1/c1_IR.cpp | 4 +- src/hotspot/share/c1/c1_IR.hpp | 8 +- src/hotspot/share/c1/c1_LIR.cpp | 5 - src/hotspot/share/c1/c1_LIR.hpp | 3 - src/hotspot/share/c1/c1_LIRAssembler.cpp | 6 - src/hotspot/share/c1/c1_LIRGenerator.cpp | 19 --- src/hotspot/share/c1/c1_Runtime1.cpp | 7 -- src/hotspot/share/code/debugInfoRec.cpp | 2 - src/hotspot/share/code/debugInfoRec.hpp | 3 +- src/hotspot/share/code/nmethod.cpp | 30 ----- src/hotspot/share/code/nmethod.hpp | 11 -- src/hotspot/share/code/nmethod.inline.hpp | 6 +- src/hotspot/share/code/pcDesc.hpp | 14 +-- .../share/jvmci/jvmciCodeInstaller.cpp | 16 +-- .../share/jvmci/jvmciCodeInstaller.hpp | 6 +- src/hotspot/share/jvmci/jvmciRuntime.cpp | 8 -- src/hotspot/share/jvmci/vmStructs_jvmci.cpp | 1 - src/hotspot/share/opto/callGenerator.cpp | 4 - src/hotspot/share/opto/callnode.hpp | 4 - src/hotspot/share/opto/compile.cpp | 2 - src/hotspot/share/opto/compile.hpp | 6 - src/hotspot/share/opto/lcm.cpp | 11 -- src/hotspot/share/opto/machnode.cpp | 7 +- src/hotspot/share/opto/machnode.hpp | 1 - src/hotspot/share/opto/matcher.cpp | 115 ++---------------- src/hotspot/share/opto/matcher.hpp | 7 +- src/hotspot/share/opto/output.cpp | 17 --- src/hotspot/share/opto/runtime.cpp | 3 - src/hotspot/share/runtime/deoptimization.cpp | 7 +- src/hotspot/share/runtime/frame.cpp | 9 +- src/hotspot/share/runtime/frame.inline.hpp | 7 +- src/hotspot/share/runtime/javaThread.cpp | 1 - src/hotspot/share/runtime/javaThread.hpp | 3 - src/hotspot/share/runtime/sharedRuntime.cpp | 5 - src/hotspot/share/runtime/vmStructs.cpp | 3 - .../classes/sun/jvm/hotspot/code/NMethod.java | 18 +-- .../classes/sun/jvm/hotspot/code/PCDesc.java | 9 +- .../sun/jvm/hotspot/runtime/Frame.java | 20 ++- .../hotspot/runtime/aarch64/AARCH64Frame.java | 42 +------ .../jvm/hotspot/runtime/ppc64/PPC64Frame.java | 29 +---- .../hotspot/runtime/riscv64/RISCV64Frame.java | 42 +------ .../sun/jvm/hotspot/runtime/x86/X86Frame.java | 41 +------ 76 files changed, 74 insertions(+), 877 deletions(-) diff --git a/src/hotspot/cpu/aarch64/aarch64.ad b/src/hotspot/cpu/aarch64/aarch64.ad index e0459716122..51cdf8c71df 100644 --- a/src/hotspot/cpu/aarch64/aarch64.ad +++ b/src/hotspot/cpu/aarch64/aarch64.ad @@ -2568,10 +2568,6 @@ RegMask Matcher::modL_proj_mask() { return RegMask(); } -const RegMask Matcher::method_handle_invoke_SP_save_mask() { - return FP_REG_mask(); -} - bool size_fits_all_mem_uses(AddPNode* addp, int shift) { for (DUIterator_Fast imax, i = addp->fast_outs(imax); i < imax; i++) { Node* u = addp->fast_out(i); diff --git a/src/hotspot/cpu/aarch64/c1_FrameMap_aarch64.cpp b/src/hotspot/cpu/aarch64/c1_FrameMap_aarch64.cpp index 9d30092b45a..83d0952dcb4 100644 --- a/src/hotspot/cpu/aarch64/c1_FrameMap_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/c1_FrameMap_aarch64.cpp @@ -383,13 +383,6 @@ LIR_Opr FrameMap::stack_pointer() { return FrameMap::sp_opr; } - -// JSR 292 -LIR_Opr FrameMap::method_handle_invoke_SP_save_opr() { - return LIR_OprFact::illegalOpr; // Not needed on aarch64 -} - - bool FrameMap::validate_frame() { return true; } diff --git a/src/hotspot/cpu/aarch64/frame_aarch64.cpp b/src/hotspot/cpu/aarch64/frame_aarch64.cpp index aff50b9cf2f..bdbef53bfdb 100644 --- a/src/hotspot/cpu/aarch64/frame_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/frame_aarch64.cpp @@ -228,8 +228,7 @@ bool frame::safe_for_sender(JavaThread *thread) { nmethod* nm = sender_blob->as_nmethod_or_null(); if (nm != nullptr) { - if (nm->is_deopt_mh_entry(sender_pc) || nm->is_deopt_entry(sender_pc) || - nm->method()->is_method_handle_intrinsic()) { + if (nm->is_deopt_entry(sender_pc) || nm->method()->is_method_handle_intrinsic()) { return false; } } @@ -454,48 +453,6 @@ JavaThread** frame::saved_thread_address(const frame& f) { return thread_addr; } -//------------------------------------------------------------------------------ -// frame::verify_deopt_original_pc -// -// Verifies the calculated original PC of a deoptimization PC for the -// given unextended SP. -#ifdef ASSERT -void frame::verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp) { - frame fr; - - // This is ugly but it's better than to change {get,set}_original_pc - // to take an SP value as argument. And it's only a debugging - // method anyway. - fr._unextended_sp = unextended_sp; - - address original_pc = nm->get_original_pc(&fr); - assert(nm->insts_contains_inclusive(original_pc), - "original PC must be in the main code section of the compiled method (or must be immediately following it)"); -} -#endif - -//------------------------------------------------------------------------------ -// frame::adjust_unextended_sp -#ifdef ASSERT -void frame::adjust_unextended_sp() { - // On aarch64, sites calling method handle intrinsics and lambda forms are treated - // as any other call site. Therefore, no special action is needed when we are - // returning to any of these call sites. - - if (_cb != nullptr) { - nmethod* sender_nm = _cb->as_nmethod_or_null(); - if (sender_nm != nullptr) { - // If the sender PC is a deoptimization point, get the original PC. - if (sender_nm->is_deopt_entry(_pc) || - sender_nm->is_deopt_mh_entry(_pc)) { - verify_deopt_original_pc(sender_nm, _unextended_sp); - } - } - } -} -#endif - - //------------------------------------------------------------------------------ // frame::sender_for_interpreter_frame frame frame::sender_for_interpreter_frame(RegisterMap* map) const { diff --git a/src/hotspot/cpu/aarch64/frame_aarch64.hpp b/src/hotspot/cpu/aarch64/frame_aarch64.hpp index da020b4234d..231710df7d7 100644 --- a/src/hotspot/cpu/aarch64/frame_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/frame_aarch64.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -141,8 +141,6 @@ int _offset_unextended_sp; // for use in stack-chunk frames }; - void adjust_unextended_sp() NOT_DEBUG_RETURN; - // true means _sp value is correct and we can use it to get the sender's sp // of the compiled frame, otherwise, _sp value may be invalid and we can use // _fp to get the sender's sp if PreserveFramePointer is enabled. @@ -152,11 +150,6 @@ return (intptr_t*) addr_at(offset); } -#ifdef ASSERT - // Used in frame::sender_for_{interpreter,compiled}_frame - static void verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp); -#endif - public: // Constructors diff --git a/src/hotspot/cpu/aarch64/frame_aarch64.inline.hpp b/src/hotspot/cpu/aarch64/frame_aarch64.inline.hpp index 47ae93a4932..cb53d8663ad 100644 --- a/src/hotspot/cpu/aarch64/frame_aarch64.inline.hpp +++ b/src/hotspot/cpu/aarch64/frame_aarch64.inline.hpp @@ -116,8 +116,6 @@ inline void frame::init(intptr_t* sp, intptr_t* fp, address pc) { } inline void frame::setup(address pc) { - adjust_unextended_sp(); - address original_pc = get_deopt_original_pc(); if (original_pc != nullptr) { _pc = original_pc; @@ -223,7 +221,6 @@ inline frame::frame(intptr_t* sp, intptr_t* fp) { // assert(_pc != nullptr, "no pc?"); _cb = CodeCache::find_blob(_pc); - adjust_unextended_sp(); address original_pc = get_deopt_original_pc(); if (original_pc != nullptr) { diff --git a/src/hotspot/cpu/arm/arm.ad b/src/hotspot/cpu/arm/arm.ad index 2835a256153..68fece5263d 100644 --- a/src/hotspot/cpu/arm/arm.ad +++ b/src/hotspot/cpu/arm/arm.ad @@ -1154,10 +1154,6 @@ RegMask Matcher::modL_proj_mask() { return RegMask(); } -const RegMask Matcher::method_handle_invoke_SP_save_mask() { - return FP_REGP_mask(); -} - bool maybe_far_call(const CallNode *n) { return !MacroAssembler::_reachable_from_cache(n->as_Call()->entry_point()); } @@ -1248,23 +1244,6 @@ encode %{ __ set_inst_mark(mark); %} - enc_class preserve_SP %{ - // preserve mark - address mark = __ inst_mark(); - DEBUG_ONLY(int off0 = __ offset()); - // FP is preserved across all calls, even compiled calls. - // Use it to preserve SP in places where the callee might change the SP. - __ mov(Rmh_SP_save, SP); - DEBUG_ONLY(int off1 = __ offset()); - assert(off1 - off0 == 4, "correct size prediction"); - // restore mark - __ set_inst_mark(mark); - %} - - enc_class restore_SP %{ - __ mov(SP, Rmh_SP_save); - %} - enc_class Java_Dynamic_Call (method meth) %{ Register R8_ic_reg = reg_to_register_object(Matcher::inline_cache_reg_encode()); assert(R8_ic_reg == Ricklass, "should be"); @@ -8799,7 +8778,6 @@ instruct safePoint_poll(iRegP poll, R12RegI tmp, flagsReg icc) %{ // Call Java Static Instruction instruct CallStaticJavaDirect( method meth ) %{ match(CallStaticJava); - predicate(! ((CallStaticJavaNode*)n)->is_method_handle_invoke()); effect(USE meth); ins_cost(CALL_COST); @@ -8808,20 +8786,6 @@ instruct CallStaticJavaDirect( method meth ) %{ ins_pipe(simple_call); %} -// Call Java Static Instruction (method handle version) -instruct CallStaticJavaHandle( method meth ) %{ - match(CallStaticJava); - predicate(((CallStaticJavaNode*)n)->is_method_handle_invoke()); - effect(USE meth); - // FP is saved by all callees (for interpreter stack correction). - // We use it here for a similar purpose, in {preserve,restore}_FP. - - ins_cost(CALL_COST); - format %{ "CALL,static/MethodHandle ==> " %} - ins_encode( SetInstMark, preserve_SP, Java_Static_Call( meth ), restore_SP, call_epilog, ClearInstMark ); - ins_pipe(simple_call); -%} - // Call Java Dynamic Instruction instruct CallDynamicJavaDirect( method meth ) %{ match(CallDynamicJava); diff --git a/src/hotspot/cpu/arm/arm_32.ad b/src/hotspot/cpu/arm/arm_32.ad index 1c15d55fbc3..00bf3bd61e4 100644 --- a/src/hotspot/cpu/arm/arm_32.ad +++ b/src/hotspot/cpu/arm/arm_32.ad @@ -1,5 +1,5 @@ // -// Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. // DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. // // This code is free software; you can redistribute it and/or modify it @@ -432,8 +432,7 @@ OptoRegPair c2::return_value(int ideal_reg) { int MachCallStaticJavaNode::ret_addr_offset() { bool far = (_method == nullptr) ? maybe_far_call(this) : !cache_reachable(); - return ((far ? 3 : 1) + (_method_handle_invoke ? 1 : 0)) * - NativeInstruction::instruction_size; + return (far ? 3 : 1) * NativeInstruction::instruction_size; } int MachCallDynamicJavaNode::ret_addr_offset() { diff --git a/src/hotspot/cpu/arm/c1_FrameMap_arm.cpp b/src/hotspot/cpu/arm/c1_FrameMap_arm.cpp index 0fd113c8ceb..a820da4d283 100644 --- a/src/hotspot/cpu/arm/c1_FrameMap_arm.cpp +++ b/src/hotspot/cpu/arm/c1_FrameMap_arm.cpp @@ -174,11 +174,6 @@ LIR_Opr FrameMap::stack_pointer() { return FrameMap::SP_opr; } -LIR_Opr FrameMap::method_handle_invoke_SP_save_opr() { - assert(Rmh_SP_save == FP, "Fix register used for saving SP for MethodHandle calls"); - return FP_opr; -} - bool FrameMap::validate_frame() { int max_offset = in_bytes(framesize_in_bytes()); int java_index = 0; diff --git a/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp b/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp index 32da2c24d26..9983f2ce7a2 100644 --- a/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp +++ b/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp @@ -275,14 +275,6 @@ OopMapSet* Runtime1::generate_exception_throw(StubAssembler* sasm, address targe } -static void restore_sp_for_method_handle(StubAssembler* sasm) { - // Restore SP from its saved reg (FP) if the exception PC is a MethodHandle call site. - __ ldr_s32(Rtemp, Address(Rthread, JavaThread::is_method_handle_return_offset())); - __ cmp(Rtemp, 0); - __ mov(SP, Rmh_SP_save, ne); -} - - OopMapSet* Runtime1::generate_handle_exception(StubId id, StubAssembler* sasm) { __ block_comment("generate_handle_exception"); @@ -339,7 +331,6 @@ OopMapSet* Runtime1::generate_handle_exception(StubId id, StubAssembler* sasm) { break; case StubId::c1_handle_exception_from_callee_id: restore_live_registers_without_return(sasm); // must not jump immediately to handler - restore_sp_for_method_handle(sasm); __ ret(); break; default: ShouldNotReachHere(); @@ -372,9 +363,6 @@ void Runtime1::generate_unwind_exception(StubAssembler* sasm) { // Jump to handler __ verify_not_null_oop(Rexception_obj); - // JSR292 extension - restore_sp_for_method_handle(sasm); - __ jump(R0); } diff --git a/src/hotspot/cpu/arm/frame_arm.cpp b/src/hotspot/cpu/arm/frame_arm.cpp index 2722f93edec..7a23296a3d4 100644 --- a/src/hotspot/cpu/arm/frame_arm.cpp +++ b/src/hotspot/cpu/arm/frame_arm.cpp @@ -329,56 +329,6 @@ JavaThread** frame::saved_thread_address(const frame& f) { return nullptr; } -//------------------------------------------------------------------------------ -// frame::verify_deopt_original_pc -// -// Verifies the calculated original PC of a deoptimization PC for the -// given unextended SP. The unextended SP might also be the saved SP -// for MethodHandle call sites. -#ifdef ASSERT -void frame::verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp, bool is_method_handle_return) { - frame fr; - - // This is ugly but it's better than to change {get,set}_original_pc - // to take an SP value as argument. And it's only a debugging - // method anyway. - fr._unextended_sp = unextended_sp; - - address original_pc = nm->get_original_pc(&fr); - assert(nm->insts_contains_inclusive(original_pc), - "original PC must be in the main code section of the compiled method (or must be immediately following it)"); - assert(nm->is_method_handle_return(original_pc) == is_method_handle_return, "must be"); -} -#endif - -//------------------------------------------------------------------------------ -// frame::adjust_unextended_sp -void frame::adjust_unextended_sp() { - // same as on x86 - - // If we are returning to a compiled MethodHandle call site, the - // saved_fp will in fact be a saved value of the unextended SP. The - // simplest way to tell whether we are returning to such a call site - // is as follows: - - nmethod* sender_nm = (_cb == nullptr) ? nullptr : _cb->as_nmethod_or_null(); - if (sender_nm != nullptr) { - // If the sender PC is a deoptimization point, get the original - // PC. For MethodHandle call site the unextended_sp is stored in - // saved_fp. - if (sender_nm->is_deopt_mh_entry(_pc)) { - DEBUG_ONLY(verify_deopt_mh_original_pc(sender_nm, _fp)); - _unextended_sp = _fp; - } - else if (sender_nm->is_deopt_entry(_pc)) { - DEBUG_ONLY(verify_deopt_original_pc(sender_nm, _unextended_sp)); - } - else if (sender_nm->is_method_handle_return(_pc)) { - _unextended_sp = _fp; - } - } -} - //------------------------------------------------------------------------------ // frame::update_map_with_saved_link void frame::update_map_with_saved_link(RegisterMap* map, intptr_t** link_addr) { diff --git a/src/hotspot/cpu/arm/frame_arm.hpp b/src/hotspot/cpu/arm/frame_arm.hpp index dec27554a47..6d4ac042831 100644 --- a/src/hotspot/cpu/arm/frame_arm.hpp +++ b/src/hotspot/cpu/arm/frame_arm.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -85,20 +85,11 @@ // original sp. intptr_t* _unextended_sp; - void adjust_unextended_sp(); intptr_t* ptr_at_addr(int offset) const { return (intptr_t*) addr_at(offset); } -#ifdef ASSERT - // Used in frame::sender_for_{interpreter,compiled}_frame - static void verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp, bool is_method_handle_return = false); - static void verify_deopt_mh_original_pc(nmethod* nm, intptr_t* unextended_sp) { - verify_deopt_original_pc(nm, unextended_sp, true); - } -#endif - public: // Constructors diff --git a/src/hotspot/cpu/arm/frame_arm.inline.hpp b/src/hotspot/cpu/arm/frame_arm.inline.hpp index 4be190f0504..42e034cfdc7 100644 --- a/src/hotspot/cpu/arm/frame_arm.inline.hpp +++ b/src/hotspot/cpu/arm/frame_arm.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -112,8 +112,6 @@ inline void frame::init(intptr_t* sp, intptr_t* unextended_sp, intptr_t* fp, add } inline void frame::setup(address pc) { - adjust_unextended_sp(); - address original_pc = get_deopt_original_pc(); if (original_pc != nullptr) { _pc = original_pc; diff --git a/src/hotspot/cpu/arm/register_arm.hpp b/src/hotspot/cpu/arm/register_arm.hpp index fca23d07fee..e0688af0d36 100644 --- a/src/hotspot/cpu/arm/register_arm.hpp +++ b/src/hotspot/cpu/arm/register_arm.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -364,7 +364,6 @@ class VFPSystemRegisterImpl : public AbstractRegisterImpl { // This does not seem to conflict with Rexception_pc // In case of issues, R3 might be OK but adapters calling the runtime would have to save it #define R5_mh R5 // MethodHandle register, used during the call setup -#define Rmh_SP_save FP // for C1 /* * C++ Interpreter Register Defines diff --git a/src/hotspot/cpu/arm/runtime_arm.cpp b/src/hotspot/cpu/arm/runtime_arm.cpp index ea4c9a76381..8d48de5795a 100644 --- a/src/hotspot/cpu/arm/runtime_arm.cpp +++ b/src/hotspot/cpu/arm/runtime_arm.cpp @@ -264,11 +264,6 @@ ExceptionBlob* OptoRuntime::generate_exception_blob() { __ raw_pop(FP, LR); - // Restore SP from its saved reg (FP) if the exception PC is a MethodHandle call site. - __ ldr(Rtemp, Address(Rthread, JavaThread::is_method_handle_return_offset())); - __ cmp(Rtemp, 0); - __ mov(SP, Rmh_SP_save, ne); - // R0 contains handler address // Since this may be the deopt blob we must set R5 to look like we returned // from the original pc that threw the exception diff --git a/src/hotspot/cpu/ppc/c1_FrameMap_ppc.cpp b/src/hotspot/cpu/ppc/c1_FrameMap_ppc.cpp index 8ce324a570b..cb9368f2ce9 100644 --- a/src/hotspot/cpu/ppc/c1_FrameMap_ppc.cpp +++ b/src/hotspot/cpu/ppc/c1_FrameMap_ppc.cpp @@ -374,15 +374,6 @@ LIR_Opr FrameMap::stack_pointer() { return SP_opr; } - -// JSR 292 -// On PPC64, there is no need to save the SP, because neither -// method handle intrinsics, nor compiled lambda forms modify it. -LIR_Opr FrameMap::method_handle_invoke_SP_save_opr() { - return LIR_OprFact::illegalOpr; -} - - bool FrameMap::validate_frame() { int max_offset = in_bytes(framesize_in_bytes()); int java_index = 0; diff --git a/src/hotspot/cpu/ppc/ppc.ad b/src/hotspot/cpu/ppc/ppc.ad index 290369360fc..2c83b2d5765 100644 --- a/src/hotspot/cpu/ppc/ppc.ad +++ b/src/hotspot/cpu/ppc/ppc.ad @@ -2473,10 +2473,6 @@ RegMask Matcher::modL_proj_mask() { return RegMask(); } -const RegMask Matcher::method_handle_invoke_SP_save_mask() { - return RegMask(); -} - %} //----------ENCODING BLOCK----------------------------------------------------- @@ -3434,7 +3430,6 @@ encode %{ // Create the call node. CallDynamicJavaDirectSchedNode *call = new CallDynamicJavaDirectSchedNode(); - call->_method_handle_invoke = _method_handle_invoke; call->_vtable_index = _vtable_index; call->_method = _method; call->_optimized_virtual = _optimized_virtual; diff --git a/src/hotspot/cpu/riscv/c1_FrameMap_riscv.cpp b/src/hotspot/cpu/riscv/c1_FrameMap_riscv.cpp index d3ccd46048b..b3a0b261f54 100644 --- a/src/hotspot/cpu/riscv/c1_FrameMap_riscv.cpp +++ b/src/hotspot/cpu/riscv/c1_FrameMap_riscv.cpp @@ -377,11 +377,6 @@ LIR_Opr FrameMap::stack_pointer() { return FrameMap::sp_opr; } -// JSR 292 -LIR_Opr FrameMap::method_handle_invoke_SP_save_opr() { - return LIR_OprFact::illegalOpr; // Not needed on riscv -} - bool FrameMap::validate_frame() { return true; } diff --git a/src/hotspot/cpu/riscv/frame_riscv.cpp b/src/hotspot/cpu/riscv/frame_riscv.cpp index b677c980c78..19dbdd6aeae 100644 --- a/src/hotspot/cpu/riscv/frame_riscv.cpp +++ b/src/hotspot/cpu/riscv/frame_riscv.cpp @@ -217,8 +217,7 @@ bool frame::safe_for_sender(JavaThread *thread) { nmethod* nm = sender_blob->as_nmethod_or_null(); if (nm != nullptr) { - if (nm->is_deopt_mh_entry(sender_pc) || nm->is_deopt_entry(sender_pc) || - nm->method()->is_method_handle_intrinsic()) { + if (nm->is_deopt_entry(sender_pc) || nm->method()->is_method_handle_intrinsic()) { return false; } } @@ -427,49 +426,6 @@ JavaThread** frame::saved_thread_address(const frame& f) { return thread_addr; } -//------------------------------------------------------------------------------ -// frame::verify_deopt_original_pc -// -// Verifies the calculated original PC of a deoptimization PC for the -// given unextended SP. -#ifdef ASSERT -void frame::verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp) { - frame fr; - - // This is ugly but it's better than to change {get,set}_original_pc - // to take an SP value as argument. And it's only a debugging - // method anyway. - fr._unextended_sp = unextended_sp; - - assert_cond(nm != nullptr); - address original_pc = nm->get_original_pc(&fr); - assert(nm->insts_contains_inclusive(original_pc), - "original PC must be in the main code section of the compiled method (or must be immediately following it)"); -} -#endif - -//------------------------------------------------------------------------------ -// frame::adjust_unextended_sp -#ifdef ASSERT -void frame::adjust_unextended_sp() { - // On riscv, sites calling method handle intrinsics and lambda forms are treated - // as any other call site. Therefore, no special action is needed when we are - // returning to any of these call sites. - - if (_cb != nullptr) { - nmethod* sender_nm = _cb->as_nmethod_or_null(); - if (sender_nm != nullptr) { - // If the sender PC is a deoptimization point, get the original PC. - if (sender_nm->is_deopt_entry(_pc) || - sender_nm->is_deopt_mh_entry(_pc)) { - verify_deopt_original_pc(sender_nm, _unextended_sp); - } - } - } -} -#endif - - //------------------------------------------------------------------------------ // frame::sender_for_interpreter_frame frame frame::sender_for_interpreter_frame(RegisterMap* map) const { diff --git a/src/hotspot/cpu/riscv/frame_riscv.hpp b/src/hotspot/cpu/riscv/frame_riscv.hpp index b4540c45ab8..ce5a8dde230 100644 --- a/src/hotspot/cpu/riscv/frame_riscv.hpp +++ b/src/hotspot/cpu/riscv/frame_riscv.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, 2022, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -179,17 +179,10 @@ int _offset_unextended_sp; // for use in stack-chunk frames }; - void adjust_unextended_sp() NOT_DEBUG_RETURN; - intptr_t* ptr_at_addr(int offset) const { return (intptr_t*) addr_at(offset); } -#ifdef ASSERT - // Used in frame::sender_for_{interpreter,compiled}_frame - static void verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp); -#endif - public: // Constructors diff --git a/src/hotspot/cpu/riscv/frame_riscv.inline.hpp b/src/hotspot/cpu/riscv/frame_riscv.inline.hpp index fb31760e20b..51a203c548c 100644 --- a/src/hotspot/cpu/riscv/frame_riscv.inline.hpp +++ b/src/hotspot/cpu/riscv/frame_riscv.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, Red Hat Inc. All rights reserved. * Copyright (c) 2020, 2023, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -114,8 +114,6 @@ inline void frame::init(intptr_t* ptr_sp, intptr_t* ptr_fp, address pc) { } inline void frame::setup(address pc) { - adjust_unextended_sp(); - address original_pc = get_deopt_original_pc(); if (original_pc != nullptr) { _pc = original_pc; @@ -215,7 +213,6 @@ inline frame::frame(intptr_t* ptr_sp, intptr_t* ptr_fp) { // value. _cb = CodeCache::find_blob(_pc); - adjust_unextended_sp(); address original_pc = get_deopt_original_pc(); if (original_pc != nullptr) { diff --git a/src/hotspot/cpu/riscv/riscv.ad b/src/hotspot/cpu/riscv/riscv.ad index d816f2405c4..1f14c499c34 100644 --- a/src/hotspot/cpu/riscv/riscv.ad +++ b/src/hotspot/cpu/riscv/riscv.ad @@ -2152,10 +2152,6 @@ RegMask Matcher::modL_proj_mask() { return RegMask(); } -const RegMask Matcher::method_handle_invoke_SP_save_mask() { - return FP_REG_mask(); -} - bool size_fits_all_mem_uses(AddPNode* addp, int shift) { assert_cond(addp != nullptr); for (DUIterator_Fast imax, i = addp->fast_outs(imax); i < imax; i++) { diff --git a/src/hotspot/cpu/s390/c1_FrameMap_s390.cpp b/src/hotspot/cpu/s390/c1_FrameMap_s390.cpp index ddba445154a..e219e9bbb40 100644 --- a/src/hotspot/cpu/s390/c1_FrameMap_s390.cpp +++ b/src/hotspot/cpu/s390/c1_FrameMap_s390.cpp @@ -282,13 +282,6 @@ LIR_Opr FrameMap::stack_pointer() { return Z_SP_opr; } -// JSR 292 -// On ZARCH_64, there is no need to save the SP, because neither -// method handle intrinsics nor compiled lambda forms modify it. -LIR_Opr FrameMap::method_handle_invoke_SP_save_opr() { - return LIR_OprFact::illegalOpr; -} - bool FrameMap::validate_frame() { return true; } diff --git a/src/hotspot/cpu/s390/s390.ad b/src/hotspot/cpu/s390/s390.ad index e9377733d2d..cfc8b19534b 100644 --- a/src/hotspot/cpu/s390/s390.ad +++ b/src/hotspot/cpu/s390/s390.ad @@ -1980,11 +1980,6 @@ RegMask Matcher::modL_proj_mask() { return _Z_RARG3_LONG_REG_mask; } -// Copied from sparc. -const RegMask Matcher::method_handle_invoke_SP_save_mask() { - return RegMask(); -} - // Should the matcher clone input 'm' of node 'n'? bool Matcher::pd_clone_node(Node* n, Node* m, Matcher::MStack& mstack) { if (is_encode_and_store_pattern(n, m)) { diff --git a/src/hotspot/cpu/x86/c1_FrameMap_x86.cpp b/src/hotspot/cpu/x86/c1_FrameMap_x86.cpp index bdbab432180..68c9814fd20 100644 --- a/src/hotspot/cpu/x86/c1_FrameMap_x86.cpp +++ b/src/hotspot/cpu/x86/c1_FrameMap_x86.cpp @@ -326,13 +326,6 @@ LIR_Opr FrameMap::stack_pointer() { return FrameMap::rsp_opr; } -// JSR 292 -// On x86, there is no need to save the SP, because neither -// method handle intrinsics, nor compiled lambda forms modify it. -LIR_Opr FrameMap::method_handle_invoke_SP_save_opr() { - return LIR_OprFact::illegalOpr; -} - bool FrameMap::validate_frame() { return true; } diff --git a/src/hotspot/cpu/x86/frame_x86.cpp b/src/hotspot/cpu/x86/frame_x86.cpp index 46ffda93699..5f52f2fabf2 100644 --- a/src/hotspot/cpu/x86/frame_x86.cpp +++ b/src/hotspot/cpu/x86/frame_x86.cpp @@ -219,8 +219,7 @@ bool frame::safe_for_sender(JavaThread *thread) { nmethod* nm = sender_blob->as_nmethod_or_null(); if (nm != nullptr) { - if (nm->is_deopt_mh_entry(sender_pc) || nm->is_deopt_entry(sender_pc) || - nm->method()->is_method_handle_intrinsic()) { + if (nm->is_deopt_entry(sender_pc) || nm->method()->is_method_handle_intrinsic()) { return false; } } @@ -443,47 +442,6 @@ JavaThread** frame::saved_thread_address(const frame& f) { return thread_addr; } -//------------------------------------------------------------------------------ -// frame::verify_deopt_original_pc -// -// Verifies the calculated original PC of a deoptimization PC for the -// given unextended SP. -#ifdef ASSERT -void frame::verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp) { - frame fr; - - // This is ugly but it's better than to change {get,set}_original_pc - // to take an SP value as argument. And it's only a debugging - // method anyway. - fr._unextended_sp = unextended_sp; - - address original_pc = nm->get_original_pc(&fr); - assert(nm->insts_contains_inclusive(original_pc), - "original PC must be in the main code section of the compiled method (or must be immediately following it) original_pc: " INTPTR_FORMAT " unextended_sp: " INTPTR_FORMAT " name: %s", p2i(original_pc), p2i(unextended_sp), nm->name()); -} -#endif - -//------------------------------------------------------------------------------ -// frame::adjust_unextended_sp -#ifdef ASSERT -void frame::adjust_unextended_sp() { - // On x86, sites calling method handle intrinsics and lambda forms are treated - // as any other call site. Therefore, no special action is needed when we are - // returning to any of these call sites. - - if (_cb != nullptr) { - nmethod* sender_nm = _cb->as_nmethod_or_null(); - if (sender_nm != nullptr) { - // If the sender PC is a deoptimization point, get the original PC. - if (sender_nm->is_deopt_entry(_pc) || - sender_nm->is_deopt_mh_entry(_pc)) { - verify_deopt_original_pc(sender_nm, _unextended_sp); - } - } - } -} -#endif - //------------------------------------------------------------------------------ // frame::sender_for_interpreter_frame frame frame::sender_for_interpreter_frame(RegisterMap* map) const { diff --git a/src/hotspot/cpu/x86/frame_x86.hpp b/src/hotspot/cpu/x86/frame_x86.hpp index f3034ee9263..19f37c42cf4 100644 --- a/src/hotspot/cpu/x86/frame_x86.hpp +++ b/src/hotspot/cpu/x86/frame_x86.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -138,17 +138,10 @@ int _offset_unextended_sp; // for use in stack-chunk frames }; - void adjust_unextended_sp() NOT_DEBUG_RETURN; - intptr_t* ptr_at_addr(int offset) const { return (intptr_t*) addr_at(offset); } -#ifdef ASSERT - // Used in frame::sender_for_{interpreter,compiled}_frame - static void verify_deopt_original_pc(nmethod* nm, intptr_t* unextended_sp); -#endif - public: // Constructors diff --git a/src/hotspot/cpu/x86/frame_x86.inline.hpp b/src/hotspot/cpu/x86/frame_x86.inline.hpp index afc4ab8767b..ca51fe66786 100644 --- a/src/hotspot/cpu/x86/frame_x86.inline.hpp +++ b/src/hotspot/cpu/x86/frame_x86.inline.hpp @@ -111,8 +111,6 @@ inline void frame::init(intptr_t* sp, intptr_t* fp, address pc) { } inline void frame::setup(address pc) { - adjust_unextended_sp(); - address original_pc = get_deopt_original_pc(); if (original_pc != nullptr) { _pc = original_pc; @@ -209,7 +207,6 @@ inline frame::frame(intptr_t* sp, intptr_t* fp) { // assert(_pc != nullptr, "no pc?"); _cb = CodeCache::find_blob(_pc); - adjust_unextended_sp(); address original_pc = get_deopt_original_pc(); if (original_pc != nullptr) { diff --git a/src/hotspot/cpu/x86/x86_64.ad b/src/hotspot/cpu/x86/x86_64.ad index 0b254966db6..b40f9e2924a 100644 --- a/src/hotspot/cpu/x86/x86_64.ad +++ b/src/hotspot/cpu/x86/x86_64.ad @@ -1697,11 +1697,6 @@ RegMask Matcher::modL_proj_mask() { return LONG_RDX_REG_mask(); } -// Register for saving SP into on method handle invokes. Not used on x86_64. -const RegMask Matcher::method_handle_invoke_SP_save_mask() { - return NO_REG_mask(); -} - %} //----------ENCODING BLOCK----------------------------------------------------- diff --git a/src/hotspot/os/posix/signals_posix.cpp b/src/hotspot/os/posix/signals_posix.cpp index 886bd453415..714eac12d22 100644 --- a/src/hotspot/os/posix/signals_posix.cpp +++ b/src/hotspot/os/posix/signals_posix.cpp @@ -621,9 +621,7 @@ int JVM_HANDLE_XXX_SIGNAL(int sig, siginfo_t* info, if (cb != nullptr && cb->is_nmethod()) { nmethod* nm = cb->as_nmethod(); assert(nm->insts_contains_inclusive(pc), ""); - address deopt = nm->is_method_handle_return(pc) ? - nm->deopt_mh_handler_begin() : - nm->deopt_handler_begin(); + address deopt = nm->deopt_handler_begin(); assert(deopt != nullptr, ""); frame fr = os::fetch_frame_from_context(uc); diff --git a/src/hotspot/os/windows/os_windows.cpp b/src/hotspot/os/windows/os_windows.cpp index 875e97ce038..c9ef08e3510 100644 --- a/src/hotspot/os/windows/os_windows.cpp +++ b/src/hotspot/os/windows/os_windows.cpp @@ -2797,9 +2797,7 @@ LONG WINAPI topLevelExceptionFilter(struct _EXCEPTION_POINTERS* exceptionInfo) { if (cb != nullptr && cb->is_nmethod()) { nmethod* nm = cb->as_nmethod(); frame fr = os::fetch_frame_from_context((void*)exceptionInfo->ContextRecord); - address deopt = nm->is_method_handle_return(pc) ? - nm->deopt_mh_handler_begin() : - nm->deopt_handler_begin(); + address deopt = nm->deopt_handler_begin(); assert(nm->insts_contains_inclusive(pc), ""); nm->set_original_pc(&fr, pc); // Set pc to handler diff --git a/src/hotspot/share/asm/codeBuffer.hpp b/src/hotspot/share/asm/codeBuffer.hpp index 35bbd2f657f..430d4949467 100644 --- a/src/hotspot/share/asm/codeBuffer.hpp +++ b/src/hotspot/share/asm/codeBuffer.hpp @@ -57,7 +57,6 @@ public: OSR_Entry, Exceptions, // Offset where exception handler lives Deopt, // Offset where deopt handler lives - DeoptMH, // Offset where MethodHandle deopt handler lives UnwindHandler, // Offset to default unwind handler max_Entries }; @@ -77,7 +76,6 @@ public: _values[OSR_Entry ] = 0; _values[Exceptions ] = -1; _values[Deopt ] = -1; - _values[DeoptMH ] = -1; _values[UnwindHandler ] = -1; } diff --git a/src/hotspot/share/c1/c1_Compilation.cpp b/src/hotspot/share/c1/c1_Compilation.cpp index bb5ceb0106b..368cf604eeb 100644 --- a/src/hotspot/share/c1/c1_Compilation.cpp +++ b/src/hotspot/share/c1/c1_Compilation.cpp @@ -310,14 +310,6 @@ void Compilation::emit_code_epilog(LIR_Assembler* assembler) { code_offsets->set_value(CodeOffsets::Deopt, assembler->emit_deopt_handler()); CHECK_BAILOUT(); - // Emit the MethodHandle deopt handler code (if required). - if (has_method_handle_invokes()) { - // We can use the same code as for the normal deopt handler, we - // just need a different entry point address. - code_offsets->set_value(CodeOffsets::DeoptMH, assembler->emit_deopt_handler()); - CHECK_BAILOUT(); - } - // Emit the handler to remove the activation from the stack and // dispatch to the caller. offsets()->set_value(CodeOffsets::UnwindHandler, assembler->emit_unwind_handler()); @@ -574,7 +566,6 @@ Compilation::Compilation(AbstractCompiler* compiler, ciEnv* env, ciMethod* metho , _has_unsafe_access(false) , _has_irreducible_loops(false) , _would_profile(false) -, _has_method_handle_invokes(false) , _has_reserved_stack_access(method->has_reserved_stack_access()) , _has_monitors(method->is_synchronized() || method->has_monitor_bytecodes()) , _has_scoped_access(method->is_scoped()) diff --git a/src/hotspot/share/c1/c1_Compilation.hpp b/src/hotspot/share/c1/c1_Compilation.hpp index 0c6b95e66c5..5125e0bbe0a 100644 --- a/src/hotspot/share/c1/c1_Compilation.hpp +++ b/src/hotspot/share/c1/c1_Compilation.hpp @@ -79,7 +79,6 @@ class Compilation: public StackObj { bool _has_unsafe_access; bool _has_irreducible_loops; bool _would_profile; - bool _has_method_handle_invokes; // True if this method has MethodHandle invokes. bool _has_reserved_stack_access; bool _has_monitors; // Fastpath monitors detection for Continuations bool _has_scoped_access; // For shared scope closure @@ -180,10 +179,6 @@ class Compilation: public StackObj { // Statistics gathering void notice_inlined_method(ciMethod* method); - // JSR 292 - bool has_method_handle_invokes() const { return _has_method_handle_invokes; } - void set_has_method_handle_invokes(bool z) { _has_method_handle_invokes = z; } - bool has_reserved_stack_access() const { return _has_reserved_stack_access; } void set_has_reserved_stack_access(bool z) { _has_reserved_stack_access = z; } diff --git a/src/hotspot/share/c1/c1_FrameMap.hpp b/src/hotspot/share/c1/c1_FrameMap.hpp index f10c4d3f226..67ae92e9875 100644 --- a/src/hotspot/share/c1/c1_FrameMap.hpp +++ b/src/hotspot/share/c1/c1_FrameMap.hpp @@ -155,9 +155,6 @@ class FrameMap : public CompilationResourceObj { // Opr representing the stack_pointer on this platform static LIR_Opr stack_pointer(); - // JSR 292 - static LIR_Opr method_handle_invoke_SP_save_opr(); - static BasicTypeArray* signature_type_array_for(const ciMethod* method); // for outgoing calls, these also update the reserved area to diff --git a/src/hotspot/share/c1/c1_IR.cpp b/src/hotspot/share/c1/c1_IR.cpp index ae8332116b3..238a9bdda0d 100644 --- a/src/hotspot/share/c1/c1_IR.cpp +++ b/src/hotspot/share/c1/c1_IR.cpp @@ -190,7 +190,6 @@ CodeEmitInfo::CodeEmitInfo(ValueStack* stack, XHandlers* exception_handlers, boo , _exception_handlers(exception_handlers) , _oop_map(nullptr) , _stack(stack) - , _is_method_handle_invoke(false) , _deoptimize_on_exception(deoptimize_on_exception) , _force_reexecute(false) { assert(_stack != nullptr, "must be non null"); @@ -203,7 +202,6 @@ CodeEmitInfo::CodeEmitInfo(CodeEmitInfo* info, ValueStack* stack) , _exception_handlers(nullptr) , _oop_map(nullptr) , _stack(stack == nullptr ? info->_stack : stack) - , _is_method_handle_invoke(info->_is_method_handle_invoke) , _deoptimize_on_exception(info->_deoptimize_on_exception) , _force_reexecute(info->_force_reexecute) { @@ -218,7 +216,7 @@ void CodeEmitInfo::record_debug_info(DebugInformationRecorder* recorder, int pc_ // record the safepoint before recording the debug info for enclosing scopes recorder->add_safepoint(pc_offset, _oop_map->deep_copy()); bool reexecute = _force_reexecute || _scope_debug_info->should_reexecute(); - _scope_debug_info->record_debug_info(recorder, pc_offset, reexecute, _is_method_handle_invoke); + _scope_debug_info->record_debug_info(recorder, pc_offset, reexecute); recorder->end_safepoint(pc_offset); } diff --git a/src/hotspot/share/c1/c1_IR.hpp b/src/hotspot/share/c1/c1_IR.hpp index a9a7a026390..d6a4cddb9d7 100644 --- a/src/hotspot/share/c1/c1_IR.hpp +++ b/src/hotspot/share/c1/c1_IR.hpp @@ -234,7 +234,7 @@ class IRScopeDebugInfo: public CompilationResourceObj { //Whether we should reexecute this bytecode for deopt bool should_reexecute(); - void record_debug_info(DebugInformationRecorder* recorder, int pc_offset, bool reexecute, bool is_method_handle_invoke = false) { + void record_debug_info(DebugInformationRecorder* recorder, int pc_offset, bool reexecute) { if (caller() != nullptr) { // Order is significant: Must record caller first. caller()->record_debug_info(recorder, pc_offset, false/*reexecute*/); @@ -248,7 +248,7 @@ class IRScopeDebugInfo: public CompilationResourceObj { bool has_ea_local_in_scope = false; bool arg_escape = false; recorder->describe_scope(pc_offset, methodHandle(), scope()->method(), bci(), - reexecute, rethrow_exception, is_method_handle_invoke, return_oop, + reexecute, rethrow_exception, return_oop, has_ea_local_in_scope, arg_escape, locvals, expvals, monvals); } }; @@ -262,7 +262,6 @@ class CodeEmitInfo: public CompilationResourceObj { XHandlers* _exception_handlers; OopMap* _oop_map; ValueStack* _stack; // used by deoptimization (contains also monitors - bool _is_method_handle_invoke; // true if the associated call site is a MethodHandle call site. bool _deoptimize_on_exception; bool _force_reexecute; // force the reexecute flag on, used for patching stub @@ -288,9 +287,6 @@ class CodeEmitInfo: public CompilationResourceObj { void add_register_oop(LIR_Opr opr); void record_debug_info(DebugInformationRecorder* recorder, int pc_offset); - bool is_method_handle_invoke() const { return _is_method_handle_invoke; } - void set_is_method_handle_invoke(bool x) { _is_method_handle_invoke = x; } - bool force_reexecute() const { return _force_reexecute; } void set_force_reexecute() { _force_reexecute = true; } diff --git a/src/hotspot/share/c1/c1_LIR.cpp b/src/hotspot/share/c1/c1_LIR.cpp index f11e178bd55..012c0f92f25 100644 --- a/src/hotspot/share/c1/c1_LIR.cpp +++ b/src/hotspot/share/c1/c1_LIR.cpp @@ -709,11 +709,6 @@ void LIR_OpVisitState::visit(LIR_Op* op) { } if (opJavaCall->_info) do_info(opJavaCall->_info); - if (FrameMap::method_handle_invoke_SP_save_opr() != LIR_OprFact::illegalOpr && - opJavaCall->is_method_handle_invoke()) { - opJavaCall->_method_handle_invoke_SP_save_opr = FrameMap::method_handle_invoke_SP_save_opr(); - do_temp(opJavaCall->_method_handle_invoke_SP_save_opr); - } do_call(); if (opJavaCall->_result->is_valid()) do_output(opJavaCall->_result); diff --git a/src/hotspot/share/c1/c1_LIR.hpp b/src/hotspot/share/c1/c1_LIR.hpp index 0427c868e6f..847184731ce 100644 --- a/src/hotspot/share/c1/c1_LIR.hpp +++ b/src/hotspot/share/c1/c1_LIR.hpp @@ -1176,7 +1176,6 @@ class LIR_OpJavaCall: public LIR_OpCall { private: ciMethod* _method; LIR_Opr _receiver; - LIR_Opr _method_handle_invoke_SP_save_opr; // Used in LIR_OpVisitState::visit to store the reference to FrameMap::method_handle_invoke_SP_save_opr. public: LIR_OpJavaCall(LIR_Code code, ciMethod* method, @@ -1186,7 +1185,6 @@ class LIR_OpJavaCall: public LIR_OpCall { : LIR_OpCall(code, addr, result, arguments, info) , _method(method) , _receiver(receiver) - , _method_handle_invoke_SP_save_opr(LIR_OprFact::illegalOpr) { assert(is_in_range(code, begin_opJavaCall, end_opJavaCall), "code check"); } LIR_OpJavaCall(LIR_Code code, ciMethod* method, @@ -1195,7 +1193,6 @@ class LIR_OpJavaCall: public LIR_OpCall { : LIR_OpCall(code, (address)vtable_offset, result, arguments, info) , _method(method) , _receiver(receiver) - , _method_handle_invoke_SP_save_opr(LIR_OprFact::illegalOpr) { assert(is_in_range(code, begin_opJavaCall, end_opJavaCall), "code check"); } LIR_Opr receiver() const { return _receiver; } diff --git a/src/hotspot/share/c1/c1_LIRAssembler.cpp b/src/hotspot/share/c1/c1_LIRAssembler.cpp index 6dbd35f054f..e6963c00c6a 100644 --- a/src/hotspot/share/c1/c1_LIRAssembler.cpp +++ b/src/hotspot/share/c1/c1_LIRAssembler.cpp @@ -478,12 +478,6 @@ void LIR_Assembler::emit_call(LIR_OpJavaCall* op) { fatal("unexpected op code: %s", op->name()); break; } - - // JSR 292 - // Record if this method has MethodHandle invokes. - if (op->is_method_handle_invoke()) { - compilation()->set_has_method_handle_invokes(true); - } } diff --git a/src/hotspot/share/c1/c1_LIRGenerator.cpp b/src/hotspot/share/c1/c1_LIRGenerator.cpp index 66adfa5ed66..f6807abcd7a 100644 --- a/src/hotspot/share/c1/c1_LIRGenerator.cpp +++ b/src/hotspot/share/c1/c1_LIRGenerator.cpp @@ -2712,19 +2712,7 @@ void LIRGenerator::do_Invoke(Invoke* x) { // emit invoke code assert(receiver->is_illegal() || receiver->is_equal(LIR_Assembler::receiverOpr()), "must match"); - // JSR 292 - // Preserve the SP over MethodHandle call sites, if needed. ciMethod* target = x->target(); - bool is_method_handle_invoke = (// %%% FIXME: Are both of these relevant? - target->is_method_handle_intrinsic() || - target->is_compiled_lambda_form()); - if (is_method_handle_invoke) { - info->set_is_method_handle_invoke(true); - if(FrameMap::method_handle_invoke_SP_save_opr() != LIR_OprFact::illegalOpr) { - __ move(FrameMap::stack_pointer(), FrameMap::method_handle_invoke_SP_save_opr()); - } - } - switch (x->code()) { case Bytecodes::_invokestatic: __ call_static(target, result_register, @@ -2757,13 +2745,6 @@ void LIRGenerator::do_Invoke(Invoke* x) { break; } - // JSR 292 - // Restore the SP after MethodHandle call sites, if needed. - if (is_method_handle_invoke - && FrameMap::method_handle_invoke_SP_save_opr() != LIR_OprFact::illegalOpr) { - __ move(FrameMap::method_handle_invoke_SP_save_opr(), FrameMap::stack_pointer()); - } - if (result_register->is_valid()) { LIR_Opr result = rlock_result(x); __ move(result_register, result); diff --git a/src/hotspot/share/c1/c1_Runtime1.cpp b/src/hotspot/share/c1/c1_Runtime1.cpp index 637c7c46ef4..a4c956ff5be 100644 --- a/src/hotspot/share/c1/c1_Runtime1.cpp +++ b/src/hotspot/share/c1/c1_Runtime1.cpp @@ -541,9 +541,6 @@ extern void vm_exit(int code); // unpack_with_exception entry instead. This makes life for the exception blob easier // because making that same check and diverting is painful from assembly language. JRT_ENTRY_NO_ASYNC(static address, exception_handler_for_pc_helper(JavaThread* current, oopDesc* ex, address pc, nmethod*& nm)) - // Reset method handle flag. - current->set_is_method_handle_return(false); - Handle exception(current, ex); // This function is called when we are about to throw an exception. Therefore, @@ -622,8 +619,6 @@ JRT_ENTRY_NO_ASYNC(static address, exception_handler_for_pc_helper(JavaThread* c if (guard_pages_enabled) { address fast_continuation = nm->handler_for_exception_and_pc(exception, pc); if (fast_continuation != nullptr) { - // Set flag if return address is a method handle call site. - current->set_is_method_handle_return(nm->is_method_handle_return(pc)); return fast_continuation; } } @@ -660,8 +655,6 @@ JRT_ENTRY_NO_ASYNC(static address, exception_handler_for_pc_helper(JavaThread* c } current->set_vm_result_oop(exception()); - // Set flag if return address is a method handle call site. - current->set_is_method_handle_return(nm->is_method_handle_return(pc)); if (log_is_enabled(Info, exceptions)) { ResourceMark rm; diff --git a/src/hotspot/share/code/debugInfoRec.cpp b/src/hotspot/share/code/debugInfoRec.cpp index 8449f5d6929..41d944ec76f 100644 --- a/src/hotspot/share/code/debugInfoRec.cpp +++ b/src/hotspot/share/code/debugInfoRec.cpp @@ -283,7 +283,6 @@ void DebugInformationRecorder::describe_scope(int pc_offset, int bci, bool reexecute, bool rethrow_exception, - bool is_method_handle_invoke, bool return_oop, bool has_ea_local_in_scope, bool arg_escape, @@ -301,7 +300,6 @@ void DebugInformationRecorder::describe_scope(int pc_offset, // Record flags into pcDesc. last_pd->set_should_reexecute(reexecute); last_pd->set_rethrow_exception(rethrow_exception); - last_pd->set_is_method_handle_invoke(is_method_handle_invoke); last_pd->set_return_oop(return_oop); last_pd->set_has_ea_local_in_scope(has_ea_local_in_scope); last_pd->set_arg_escape(arg_escape); diff --git a/src/hotspot/share/code/debugInfoRec.hpp b/src/hotspot/share/code/debugInfoRec.hpp index f6d7cf6d278..d18208e17c4 100644 --- a/src/hotspot/share/code/debugInfoRec.hpp +++ b/src/hotspot/share/code/debugInfoRec.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -105,7 +105,6 @@ class DebugInformationRecorder: public ResourceObj { int bci, bool reexecute, bool rethrow_exception = false, - bool is_method_handle_invoke = false, bool return_oop = false, bool has_ea_local_in_scope = false, bool arg_escape = false, diff --git a/src/hotspot/share/code/nmethod.cpp b/src/hotspot/share/code/nmethod.cpp index c0091cd2f65..1540fa0333a 100644 --- a/src/hotspot/share/code/nmethod.cpp +++ b/src/hotspot/share/code/nmethod.cpp @@ -465,14 +465,6 @@ static int adjust_pcs_size(int pcs_size) { return nsize; } -bool nmethod::is_method_handle_return(address return_pc) { - if (!has_method_handle_invokes()) return false; - PcDesc* pd = pc_desc_at(return_pc); - if (pd == nullptr) - return false; - return pd->is_method_handle_invoke(); -} - // Returns a string version of the method state. const char* nmethod::state() const { int state = get_state(); @@ -1231,7 +1223,6 @@ void nmethod::init_defaults(CodeBuffer *code_buffer, CodeOffsets* offsets) { _state = not_installed; _has_unsafe_access = 0; - _has_method_handle_invokes = 0; _has_wide_vectors = 0; _has_monitors = 0; _has_scoped_access = 0; @@ -1310,7 +1301,6 @@ nmethod::nmethod( // Native wrappers do not have deopt handlers. Make the values // something that will never match a pc like the nmethod vtable entry _deopt_handler_offset = 0; - _deopt_mh_handler_offset = 0; _unwind_handler_offset = 0; CHECKED_CAST(_oops_size, uint16_t, align_up(code_buffer->total_oop_size(), oopSize)); @@ -1458,11 +1448,6 @@ nmethod::nmethod( } else { _deopt_handler_offset = -1; } - if (offsets->value(CodeOffsets::DeoptMH) != -1) { - _deopt_mh_handler_offset = code_offset() + offsets->value(CodeOffsets::DeoptMH); - } else { - _deopt_mh_handler_offset = -1; - } } else #endif { @@ -1472,11 +1457,6 @@ nmethod::nmethod( _exception_offset = _stub_offset + offsets->value(CodeOffsets::Exceptions); _deopt_handler_offset = _stub_offset + offsets->value(CodeOffsets::Deopt); - if (offsets->value(CodeOffsets::DeoptMH) != -1) { - _deopt_mh_handler_offset = _stub_offset + offsets->value(CodeOffsets::DeoptMH); - } else { - _deopt_mh_handler_offset = -1; - } } if (offsets->value(CodeOffsets::UnwindHandler) != -1) { // C1 generates UnwindHandler at the end of instructions section. @@ -2693,15 +2673,6 @@ void nmethod::copy_scopes_pcs(PcDesc* pcs, int count) { "must end with a sentinel"); #endif //ASSERT - // Search for MethodHandle invokes and tag the nmethod. - for (int i = 0; i < count; i++) { - if (pcs[i].is_method_handle_invoke()) { - set_has_method_handle_invokes(true); - break; - } - } - assert(has_method_handle_invokes() == (_deopt_mh_handler_offset != -1), "must have deopt mh handler"); - int size = count * sizeof(PcDesc); assert(scopes_pcs_size() >= size, "oob"); memcpy(scopes_pcs_begin(), pcs, size); @@ -3711,7 +3682,6 @@ const char* nmethod::nmethod_section_label(address pos) const { if (pos == code_begin()) label = "[Instructions begin]"; if (pos == entry_point()) label = "[Entry Point]"; if (pos == verified_entry_point()) label = "[Verified Entry Point]"; - if (has_method_handle_invokes() && (pos == deopt_mh_handler_begin())) label = "[Deopt MH Handler Code]"; if (pos == consts_begin() && pos != insts_begin()) label = "[Constants]"; // Check stub_code before checking exception_handler or deopt_handler. if (pos == this->stub_begin()) label = "[Stub Code]"; diff --git a/src/hotspot/share/code/nmethod.hpp b/src/hotspot/share/code/nmethod.hpp index 8dfccb07891..1e876657098 100644 --- a/src/hotspot/share/code/nmethod.hpp +++ b/src/hotspot/share/code/nmethod.hpp @@ -226,9 +226,6 @@ class nmethod : public CodeBlob { // All deoptee's will resume execution at this location described by // this offset. int _deopt_handler_offset; - // All deoptee's at a MethodHandle call site will resume execution - // at this location described by this offset. - int _deopt_mh_handler_offset; // Offset (from insts_end) of the unwind handler if it exists int16_t _unwind_handler_offset; // Number of arguments passed on the stack @@ -267,7 +264,6 @@ class nmethod : public CodeBlob { // set during construction uint8_t _has_unsafe_access:1, // May fault due to unsafe access. - _has_method_handle_invokes:1,// Has this method MethodHandle invokes? _has_wide_vectors:1, // Preserve wide vectors at safepoints _has_monitors:1, // Fastpath monitor detection for continuations _has_scoped_access:1, // used by for shared scope closure (scopedMemoryAccess.cpp) @@ -606,7 +602,6 @@ public: address stub_end () const { return code_end() ; } address exception_begin () const { return header_begin() + _exception_offset ; } address deopt_handler_begin () const { return header_begin() + _deopt_handler_offset ; } - address deopt_mh_handler_begin() const { return header_begin() + _deopt_mh_handler_offset ; } address unwind_handler_begin () const { return _unwind_handler_offset != -1 ? (insts_end() - _unwind_handler_offset) : nullptr; } oop* oops_begin () const { return (oop*) data_begin(); } oop* oops_end () const { return (oop*) data_end(); } @@ -745,9 +740,6 @@ public: bool has_scoped_access() const { return _has_scoped_access; } void set_has_scoped_access(bool z) { _has_scoped_access = z; } - bool has_method_handle_invokes() const { return _has_method_handle_invokes; } - void set_has_method_handle_invokes(bool z) { _has_method_handle_invokes = z; } - bool has_wide_vectors() const { return _has_wide_vectors; } void set_has_wide_vectors(bool z) { _has_wide_vectors = z; } @@ -818,12 +810,9 @@ public: ExceptionCache* exception_cache_entry_for_exception(Handle exception); - // MethodHandle - bool is_method_handle_return(address return_pc); // Deopt // Return true is the PC is one would expect if the frame is being deopted. inline bool is_deopt_pc(address pc); - inline bool is_deopt_mh_entry(address pc); inline bool is_deopt_entry(address pc); // Accessor/mutator for the original pc of a frame before a frame was deopted. diff --git a/src/hotspot/share/code/nmethod.inline.hpp b/src/hotspot/share/code/nmethod.inline.hpp index 62c8eb723ea..44331db669c 100644 --- a/src/hotspot/share/code/nmethod.inline.hpp +++ b/src/hotspot/share/code/nmethod.inline.hpp @@ -31,16 +31,12 @@ #include "runtime/atomicAccess.hpp" #include "runtime/frame.hpp" -inline bool nmethod::is_deopt_pc(address pc) { return is_deopt_entry(pc) || is_deopt_mh_entry(pc); } +inline bool nmethod::is_deopt_pc(address pc) { return is_deopt_entry(pc); } inline bool nmethod::is_deopt_entry(address pc) { return pc == deopt_handler_begin(); } -inline bool nmethod::is_deopt_mh_entry(address pc) { - return pc == deopt_mh_handler_begin(); -} - // class ExceptionCache methods inline int ExceptionCache::count() { return AtomicAccess::load_acquire(&_count); } diff --git a/src/hotspot/share/code/pcDesc.hpp b/src/hotspot/share/code/pcDesc.hpp index 8048c3909c7..d8abe19ad35 100644 --- a/src/hotspot/share/code/pcDesc.hpp +++ b/src/hotspot/share/code/pcDesc.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -40,11 +40,10 @@ class PcDesc { enum { PCDESC_reexecute = 1 << 0, - PCDESC_is_method_handle_invoke = 1 << 1, - PCDESC_return_oop = 1 << 2, - PCDESC_rethrow_exception = 1 << 3, - PCDESC_has_ea_local_in_scope = 1 << 4, - PCDESC_arg_escape = 1 << 5 + PCDESC_return_oop = 1 << 1, + PCDESC_rethrow_exception = 1 << 2, + PCDESC_has_ea_local_in_scope = 1 << 3, + PCDESC_arg_escape = 1 << 4 }; int _flags; @@ -85,9 +84,6 @@ class PcDesc { _flags == pd->_flags; } - bool is_method_handle_invoke() const { return (_flags & PCDESC_is_method_handle_invoke) != 0; } - void set_is_method_handle_invoke(bool z) { set_flag(PCDESC_is_method_handle_invoke, z); } - bool return_oop() const { return (_flags & PCDESC_return_oop) != 0; } void set_return_oop(bool z) { set_flag(PCDESC_return_oop, z); } diff --git a/src/hotspot/share/jvmci/jvmciCodeInstaller.cpp b/src/hotspot/share/jvmci/jvmciCodeInstaller.cpp index 3a9fbc54bf9..ce617a4a514 100644 --- a/src/hotspot/share/jvmci/jvmciCodeInstaller.cpp +++ b/src/hotspot/share/jvmci/jvmciCodeInstaller.cpp @@ -1141,7 +1141,7 @@ int CodeInstaller::map_jvmci_bci(int bci) { return bci; } -void CodeInstaller::record_scope(jint pc_offset, HotSpotCompiledCodeStream* stream, u1 debug_info_flags, bool full_info, bool is_mh_invoke, bool return_oop, JVMCI_TRAPS) { +void CodeInstaller::record_scope(jint pc_offset, HotSpotCompiledCodeStream* stream, u1 debug_info_flags, bool full_info, bool return_oop, JVMCI_TRAPS) { if (full_info) { read_virtual_objects(stream, JVMCI_CHECK); } @@ -1184,7 +1184,7 @@ void CodeInstaller::record_scope(jint pc_offset, HotSpotCompiledCodeStream* stre // has_ea_local_in_scope and arg_escape should be added to JVMCI const bool has_ea_local_in_scope = false; const bool arg_escape = false; - _debug_recorder->describe_scope(pc_offset, method, nullptr, bci, reexecute, rethrow_exception, is_mh_invoke, return_oop, + _debug_recorder->describe_scope(pc_offset, method, nullptr, bci, reexecute, rethrow_exception, return_oop, has_ea_local_in_scope, arg_escape, locals_token, stack_token, monitors_token); } @@ -1242,14 +1242,8 @@ void CodeInstaller::site_Call(CodeBuffer& buffer, u1 tag, jint pc_offset, HotSpo _debug_recorder->add_safepoint(next_pc_offset, map); if (!method.is_null()) { - vmIntrinsics::ID iid = method->intrinsic_id(); - bool is_mh_invoke = false; - if (direct_call) { - is_mh_invoke = !method->is_static() && (iid == vmIntrinsics::_compiledLambdaForm || - (MethodHandles::is_signature_polymorphic(iid) && MethodHandles::is_signature_polymorphic_intrinsic(iid))); - } bool return_oop = method->is_returning_oop(); - record_scope(next_pc_offset, stream, flags, true, is_mh_invoke, return_oop, JVMCI_CHECK); + record_scope(next_pc_offset, stream, flags, true, return_oop, JVMCI_CHECK); } else { record_scope(next_pc_offset, stream, flags, true, JVMCI_CHECK); } @@ -1339,9 +1333,6 @@ void CodeInstaller::site_Mark(CodeBuffer& buffer, jint pc_offset, HotSpotCompile case DEOPT_HANDLER_ENTRY: _offsets.set_value(CodeOffsets::Deopt, pc_offset); break; - case DEOPT_MH_HANDLER_ENTRY: - _offsets.set_value(CodeOffsets::DeoptMH, pc_offset); - break; case FRAME_COMPLETE: _offsets.set_value(CodeOffsets::Frame_Complete, pc_offset); break; @@ -1369,6 +1360,7 @@ void CodeInstaller::site_Mark(CodeBuffer& buffer, jint pc_offset, HotSpotCompile case VERIFY_OOP_BITS: case VERIFY_OOP_MASK: case VERIFY_OOP_COUNT_ADDRESS: + case DEOPT_MH_HANDLER_ENTRY: break; default: diff --git a/src/hotspot/share/jvmci/jvmciCodeInstaller.hpp b/src/hotspot/share/jvmci/jvmciCodeInstaller.hpp index a8279e99dc2..bdebed2d9e8 100644 --- a/src/hotspot/share/jvmci/jvmciCodeInstaller.hpp +++ b/src/hotspot/share/jvmci/jvmciCodeInstaller.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -411,10 +411,10 @@ protected: void record_oop_patch(HotSpotCompiledCodeStream* stream, address dest, u1 read_tag, bool narrow, JVMCI_TRAPS); // full_info: if false, only BytecodePosition is in stream otherwise all DebugInfo is in stream - void record_scope(jint pc_offset, HotSpotCompiledCodeStream* stream, u1 debug_info_flags, bool full_info, bool is_mh_invoke, bool return_oop, JVMCI_TRAPS); + void record_scope(jint pc_offset, HotSpotCompiledCodeStream* stream, u1 debug_info_flags, bool full_info, bool return_oop, JVMCI_TRAPS); void record_scope(jint pc_offset, HotSpotCompiledCodeStream* stream, u1 debug_info_flags, bool full_info, JVMCI_TRAPS) { - record_scope(pc_offset, stream, debug_info_flags, full_info, false /* is_mh_invoke */, false /* return_oop */, JVMCIENV); + record_scope(pc_offset, stream, debug_info_flags, full_info, false /* return_oop */, JVMCIENV); } void record_object_value(ObjectValue* sv, HotSpotCompiledCodeStream* stream, JVMCI_TRAPS); diff --git a/src/hotspot/share/jvmci/jvmciRuntime.cpp b/src/hotspot/share/jvmci/jvmciRuntime.cpp index e75527235f0..a178ac66344 100644 --- a/src/hotspot/share/jvmci/jvmciRuntime.cpp +++ b/src/hotspot/share/jvmci/jvmciRuntime.cpp @@ -228,9 +228,6 @@ extern void vm_exit(int code); // unpack_with_exception entry instead. This makes life for the exception blob easier // because making that same check and diverting is painful from assembly language. JRT_ENTRY_NO_ASYNC(static address, exception_handler_for_pc_helper(JavaThread* current, oopDesc* ex, address pc, nmethod*& nm)) - // Reset method handle flag. - current->set_is_method_handle_return(false); - Handle exception(current, ex); // The frame we rethrow the exception to might not have been processed by the GC yet. @@ -305,8 +302,6 @@ JRT_ENTRY_NO_ASYNC(static address, exception_handler_for_pc_helper(JavaThread* c if (guard_pages_enabled) { address fast_continuation = nm->handler_for_exception_and_pc(exception, pc); if (fast_continuation != nullptr) { - // Set flag if return address is a method handle call site. - current->set_is_method_handle_return(nm->is_method_handle_return(pc)); return fast_continuation; } } @@ -343,9 +338,6 @@ JRT_ENTRY_NO_ASYNC(static address, exception_handler_for_pc_helper(JavaThread* c } } - // Set flag if return address is a method handle call site. - current->set_is_method_handle_return(nm->is_method_handle_return(pc)); - if (log_is_enabled(Info, exceptions)) { ResourceMark rm; log_info(exceptions)("Thread " PTR_FORMAT " continuing at PC " PTR_FORMAT diff --git a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp index b91de1c9b35..b4f033a4f9a 100644 --- a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp +++ b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp @@ -242,7 +242,6 @@ nonstatic_field(JavaThread, _stack_overflow_state._stack_overflow_limit, address) \ volatile_nonstatic_field(JavaThread, _exception_oop, oop) \ volatile_nonstatic_field(JavaThread, _exception_pc, address) \ - volatile_nonstatic_field(JavaThread, _is_method_handle_return, int) \ volatile_nonstatic_field(JavaThread, _doing_unsafe_access, bool) \ nonstatic_field(JavaThread, _osthread, OSThread*) \ nonstatic_field(JavaThread, _saved_exception_pc, address) \ diff --git a/src/hotspot/share/opto/callGenerator.cpp b/src/hotspot/share/opto/callGenerator.cpp index e09d8cabe2c..03225044f5e 100644 --- a/src/hotspot/share/opto/callGenerator.cpp +++ b/src/hotspot/share/opto/callGenerator.cpp @@ -169,10 +169,6 @@ JVMState* DirectCallGenerator::generate(JVMState* jvms) { } // Mark the call node as virtual, sort of: call->set_optimized_virtual(true); - if (method()->is_method_handle_intrinsic() || - method()->is_compiled_lambda_form()) { - call->set_method_handle_invoke(true); - } } kit.set_arguments_for_java_call(call); kit.set_edges_for_java_call(call, false, _separate_io_proj); diff --git a/src/hotspot/share/opto/callnode.hpp b/src/hotspot/share/opto/callnode.hpp index 093c47194ef..9029a009989 100644 --- a/src/hotspot/share/opto/callnode.hpp +++ b/src/hotspot/share/opto/callnode.hpp @@ -778,7 +778,6 @@ protected: ciMethod* _method; // Method being direct called bool _optimized_virtual; - bool _method_handle_invoke; bool _override_symbolic_info; // Override symbolic call site info from bytecode bool _arg_escape; // ArgEscape in parameter list public: @@ -786,7 +785,6 @@ public: : CallNode(tf, addr, TypePtr::BOTTOM), _method(method), _optimized_virtual(false), - _method_handle_invoke(false), _override_symbolic_info(false), _arg_escape(false) { @@ -798,8 +796,6 @@ public: void set_method(ciMethod *m) { _method = m; } void set_optimized_virtual(bool f) { _optimized_virtual = f; } bool is_optimized_virtual() const { return _optimized_virtual; } - void set_method_handle_invoke(bool f) { _method_handle_invoke = f; } - bool is_method_handle_invoke() const { return _method_handle_invoke; } void set_override_symbolic_info(bool f) { _override_symbolic_info = f; } bool override_symbolic_info() const { return _override_symbolic_info; } void set_arg_escape(bool f) { _arg_escape = f; } diff --git a/src/hotspot/share/opto/compile.cpp b/src/hotspot/share/opto/compile.cpp index f828dfb6928..47de5acc2f2 100644 --- a/src/hotspot/share/opto/compile.cpp +++ b/src/hotspot/share/opto/compile.cpp @@ -650,7 +650,6 @@ Compile::Compile(ciEnv* ci_env, ciMethod* target, int osr_bci, _igv_idx(0), _trace_opto_output(directive->TraceOptoOutputOption), #endif - _has_method_handle_invokes(false), _clinit_barrier_on_entry(false), _stress_seed(0), _comp_arena(mtCompiler, Arena::Tag::tag_comp), @@ -925,7 +924,6 @@ Compile::Compile(ciEnv* ci_env, _igv_idx(0), _trace_opto_output(directive->TraceOptoOutputOption), #endif - _has_method_handle_invokes(false), _clinit_barrier_on_entry(false), _stress_seed(0), _comp_arena(mtCompiler, Arena::Tag::tag_comp), diff --git a/src/hotspot/share/opto/compile.hpp b/src/hotspot/share/opto/compile.hpp index 1cfcdca9610..05c5f22dad9 100644 --- a/src/hotspot/share/opto/compile.hpp +++ b/src/hotspot/share/opto/compile.hpp @@ -354,8 +354,6 @@ class Compile : public Phase { bool _parsed_irreducible_loop; // True if ciTypeFlow detected irreducible loops during parsing #endif bool _has_irreducible_loop; // Found irreducible loops - // JSR 292 - bool _has_method_handle_invokes; // True if this method has MethodHandle invokes. bool _has_monitors; // Metadata transfered to nmethod to enable Continuations lock-detection fastpath bool _has_scoped_access; // For shared scope closure bool _clinit_barrier_on_entry; // True if clinit barrier is needed on nmethod entry @@ -666,10 +664,6 @@ public: bool has_irreducible_loop() const { return _has_irreducible_loop; } void set_has_irreducible_loop(bool z) { _has_irreducible_loop = z; } - // JSR 292 - bool has_method_handle_invokes() const { return _has_method_handle_invokes; } - void set_has_method_handle_invokes(bool z) { _has_method_handle_invokes = z; } - Ticks _latest_stage_start_counter; void begin_method(); diff --git a/src/hotspot/share/opto/lcm.cpp b/src/hotspot/share/opto/lcm.cpp index ca1863b685e..fd7644f8587 100644 --- a/src/hotspot/share/opto/lcm.cpp +++ b/src/hotspot/share/opto/lcm.cpp @@ -946,17 +946,6 @@ uint PhaseCFG::sched_call(Block* block, uint node_cnt, Node_List& worklist, Grow // references but there no way to handle oops differently than other // pointers as far as the kill mask goes. bool exclude_soe = op == Op_CallRuntime; - - // If the call is a MethodHandle invoke, we need to exclude the - // register which is used to save the SP value over MH invokes from - // the mask. Otherwise this register could be used for - // deoptimization information. - if (op == Op_CallStaticJava) { - MachCallStaticJavaNode* mcallstaticjava = (MachCallStaticJavaNode*) mcall; - if (mcallstaticjava->_method_handle_invoke) - proj->_rout.OR(Matcher::method_handle_invoke_SP_save_mask()); - } - add_call_kills(proj, regs, save_policy, exclude_soe); return node_cnt; diff --git a/src/hotspot/share/opto/machnode.cpp b/src/hotspot/share/opto/machnode.cpp index e5acad98c09..5da929e4748 100644 --- a/src/hotspot/share/opto/machnode.cpp +++ b/src/hotspot/share/opto/machnode.cpp @@ -772,8 +772,6 @@ bool MachCallJavaNode::cmp( const Node &n ) const { } #ifndef PRODUCT void MachCallJavaNode::dump_spec(outputStream *st) const { - if (_method_handle_invoke) - st->print("MethodHandle "); if (_method) { _method->print_short_name(st); st->print(" "); @@ -794,10 +792,7 @@ const RegMask &MachCallJavaNode::in_RegMask(uint idx) const { } // Values outside the domain represent debug info Matcher* m = Compile::current()->matcher(); - // If this call is a MethodHandle invoke we have to use a different - // debugmask which does not include the register we use to save the - // SP over MH invokes. - RegMask** debugmask = _method_handle_invoke ? m->idealreg2mhdebugmask : m->idealreg2debugmask; + RegMask** debugmask = m->idealreg2debugmask; return *debugmask[in(idx)->ideal_reg()]; } diff --git a/src/hotspot/share/opto/machnode.hpp b/src/hotspot/share/opto/machnode.hpp index 5490f717a25..43e9a35df34 100644 --- a/src/hotspot/share/opto/machnode.hpp +++ b/src/hotspot/share/opto/machnode.hpp @@ -960,7 +960,6 @@ public: ciMethod* _method; // Method being direct called bool _override_symbolic_info; // Override symbolic call site info from bytecode bool _optimized_virtual; // Tells if node is a static call or an optimized virtual - bool _method_handle_invoke; // Tells if the call has to preserve SP bool _arg_escape; // ArgEscape in parameter list MachCallJavaNode() : MachCallNode(), _override_symbolic_info(false) { init_class_id(Class_MachCallJava); diff --git a/src/hotspot/share/opto/matcher.cpp b/src/hotspot/share/opto/matcher.cpp index b43bc05c465..7d73487cf88 100644 --- a/src/hotspot/share/opto/matcher.cpp +++ b/src/hotspot/share/opto/matcher.cpp @@ -50,8 +50,6 @@ const RegMask *Matcher::idealreg2regmask[_last_machine_leaf]; RegMask Matcher::mreg2regmask[_last_Mach_Reg]; RegMask Matcher::caller_save_regmask; RegMask Matcher::caller_save_regmask_exclude_soe; -RegMask Matcher::mh_caller_save_regmask; -RegMask Matcher::mh_caller_save_regmask_exclude_soe; RegMask Matcher::STACK_ONLY_mask; RegMask Matcher::c_frame_ptr_mask; const uint Matcher::_begin_rematerialize = _BEGIN_REMATERIALIZE; @@ -114,21 +112,6 @@ Matcher::Matcher() idealreg2debugmask [Op_RegFlags] = nullptr; idealreg2debugmask [Op_RegVectMask] = nullptr; - idealreg2mhdebugmask[Op_RegI] = nullptr; - idealreg2mhdebugmask[Op_RegN] = nullptr; - idealreg2mhdebugmask[Op_RegL] = nullptr; - idealreg2mhdebugmask[Op_RegF] = nullptr; - idealreg2mhdebugmask[Op_RegD] = nullptr; - idealreg2mhdebugmask[Op_RegP] = nullptr; - idealreg2mhdebugmask[Op_VecA] = nullptr; - idealreg2mhdebugmask[Op_VecS] = nullptr; - idealreg2mhdebugmask[Op_VecD] = nullptr; - idealreg2mhdebugmask[Op_VecX] = nullptr; - idealreg2mhdebugmask[Op_VecY] = nullptr; - idealreg2mhdebugmask[Op_VecZ] = nullptr; - idealreg2mhdebugmask[Op_RegFlags] = nullptr; - idealreg2mhdebugmask[Op_RegVectMask] = nullptr; - DEBUG_ONLY(_mem_node = nullptr;) // Ideal memory node consumed by mach node } @@ -465,7 +448,7 @@ int Matcher::scalable_predicate_reg_slots() { return round_up_power_of_2(slots); } -#define NOF_STACK_MASKS (3*13) +#define NOF_STACK_MASKS (2*13) // Create the initial stack mask used by values spilling to the stack. // Disallow any debug info in outgoing argument areas by setting the @@ -480,51 +463,12 @@ void Matcher::init_first_stack_mask() { new (rms + i) RegMask(C->comp_arena()); } - idealreg2spillmask [Op_RegN] = &rms[0]; - idealreg2spillmask [Op_RegI] = &rms[1]; - idealreg2spillmask [Op_RegL] = &rms[2]; - idealreg2spillmask [Op_RegF] = &rms[3]; - idealreg2spillmask [Op_RegD] = &rms[4]; - idealreg2spillmask [Op_RegP] = &rms[5]; - - idealreg2debugmask [Op_RegN] = &rms[6]; - idealreg2debugmask [Op_RegI] = &rms[7]; - idealreg2debugmask [Op_RegL] = &rms[8]; - idealreg2debugmask [Op_RegF] = &rms[9]; - idealreg2debugmask [Op_RegD] = &rms[10]; - idealreg2debugmask [Op_RegP] = &rms[11]; - - idealreg2mhdebugmask[Op_RegN] = &rms[12]; - idealreg2mhdebugmask[Op_RegI] = &rms[13]; - idealreg2mhdebugmask[Op_RegL] = &rms[14]; - idealreg2mhdebugmask[Op_RegF] = &rms[15]; - idealreg2mhdebugmask[Op_RegD] = &rms[16]; - idealreg2mhdebugmask[Op_RegP] = &rms[17]; - - idealreg2spillmask [Op_VecA] = &rms[18]; - idealreg2spillmask [Op_VecS] = &rms[19]; - idealreg2spillmask [Op_VecD] = &rms[20]; - idealreg2spillmask [Op_VecX] = &rms[21]; - idealreg2spillmask [Op_VecY] = &rms[22]; - idealreg2spillmask [Op_VecZ] = &rms[23]; - - idealreg2debugmask [Op_VecA] = &rms[24]; - idealreg2debugmask [Op_VecS] = &rms[25]; - idealreg2debugmask [Op_VecD] = &rms[26]; - idealreg2debugmask [Op_VecX] = &rms[27]; - idealreg2debugmask [Op_VecY] = &rms[28]; - idealreg2debugmask [Op_VecZ] = &rms[29]; - - idealreg2mhdebugmask[Op_VecA] = &rms[30]; - idealreg2mhdebugmask[Op_VecS] = &rms[31]; - idealreg2mhdebugmask[Op_VecD] = &rms[32]; - idealreg2mhdebugmask[Op_VecX] = &rms[33]; - idealreg2mhdebugmask[Op_VecY] = &rms[34]; - idealreg2mhdebugmask[Op_VecZ] = &rms[35]; - - idealreg2spillmask [Op_RegVectMask] = &rms[36]; - idealreg2debugmask [Op_RegVectMask] = &rms[37]; - idealreg2mhdebugmask[Op_RegVectMask] = &rms[38]; + int index = 0; + for (int i = Op_RegN; i <= Op_RegVectMask; ++i) { + idealreg2spillmask[i] = &rms[index++]; + idealreg2debugmask[i] = &rms[index++]; + } + assert(index == NOF_STACK_MASKS, "wrong size"); // At first, start with the empty mask C->FIRST_STACK_mask().Clear(); @@ -710,26 +654,10 @@ void Matcher::init_first_stack_mask() { *idealreg2debugmask [Op_VecY] = *idealreg2spillmask[Op_VecY]; *idealreg2debugmask [Op_VecZ] = *idealreg2spillmask[Op_VecZ]; - *idealreg2mhdebugmask[Op_RegN] = *idealreg2spillmask[Op_RegN]; - *idealreg2mhdebugmask[Op_RegI] = *idealreg2spillmask[Op_RegI]; - *idealreg2mhdebugmask[Op_RegL] = *idealreg2spillmask[Op_RegL]; - *idealreg2mhdebugmask[Op_RegF] = *idealreg2spillmask[Op_RegF]; - *idealreg2mhdebugmask[Op_RegD] = *idealreg2spillmask[Op_RegD]; - *idealreg2mhdebugmask[Op_RegP] = *idealreg2spillmask[Op_RegP]; - *idealreg2mhdebugmask[Op_RegVectMask] = *idealreg2spillmask[Op_RegVectMask]; - - *idealreg2mhdebugmask[Op_VecA] = *idealreg2spillmask[Op_VecA]; - *idealreg2mhdebugmask[Op_VecS] = *idealreg2spillmask[Op_VecS]; - *idealreg2mhdebugmask[Op_VecD] = *idealreg2spillmask[Op_VecD]; - *idealreg2mhdebugmask[Op_VecX] = *idealreg2spillmask[Op_VecX]; - *idealreg2mhdebugmask[Op_VecY] = *idealreg2spillmask[Op_VecY]; - *idealreg2mhdebugmask[Op_VecZ] = *idealreg2spillmask[Op_VecZ]; - // Prevent stub compilations from attempting to reference // callee-saved (SOE) registers from debug info bool exclude_soe = !Compile::current()->is_method_compilation(); RegMask* caller_save_mask = exclude_soe ? &caller_save_regmask_exclude_soe : &caller_save_regmask; - RegMask* mh_caller_save_mask = exclude_soe ? &mh_caller_save_regmask_exclude_soe : &mh_caller_save_regmask; idealreg2debugmask[Op_RegN]->SUBTRACT(*caller_save_mask); idealreg2debugmask[Op_RegI]->SUBTRACT(*caller_save_mask); @@ -745,21 +673,6 @@ void Matcher::init_first_stack_mask() { idealreg2debugmask[Op_VecX]->SUBTRACT(*caller_save_mask); idealreg2debugmask[Op_VecY]->SUBTRACT(*caller_save_mask); idealreg2debugmask[Op_VecZ]->SUBTRACT(*caller_save_mask); - - idealreg2mhdebugmask[Op_RegN]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_RegI]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_RegL]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_RegF]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_RegD]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_RegP]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_RegVectMask]->SUBTRACT(*mh_caller_save_mask); - - idealreg2mhdebugmask[Op_VecA]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_VecS]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_VecD]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_VecX]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_VecY]->SUBTRACT(*mh_caller_save_mask); - idealreg2mhdebugmask[Op_VecZ]->SUBTRACT(*mh_caller_save_mask); } //---------------------------is_save_on_entry---------------------------------- @@ -984,23 +897,15 @@ void Matcher::init_spill_mask( Node *ret ) { if (_register_save_policy[i] == 'C' || _register_save_policy[i] == 'A') { caller_save_regmask.Insert(i); - mh_caller_save_regmask.Insert(i); } // Exclude save-on-entry registers from debug masks for stub compilations. if (_register_save_policy[i] == 'C' || _register_save_policy[i] == 'A' || _register_save_policy[i] == 'E') { caller_save_regmask_exclude_soe.Insert(i); - mh_caller_save_regmask_exclude_soe.Insert(i); } } - // Also exclude the register we use to save the SP for MethodHandle - // invokes to from the corresponding MH debug masks - const RegMask sp_save_mask = method_handle_invoke_SP_save_mask(); - mh_caller_save_regmask.OR(sp_save_mask); - mh_caller_save_regmask_exclude_soe.OR(sp_save_mask); - // Grab the Frame Pointer Node *fp = ret->in(TypeFunc::FramePtr); // Share frame pointer while making spill ops @@ -1272,7 +1177,6 @@ MachNode *Matcher::match_sfpt( SafePointNode *sfpt ) { CallNode *call; const TypeTuple *domain; ciMethod* method = nullptr; - bool is_method_handle_invoke = false; // for special kill effects if( sfpt->is_Call() ) { call = sfpt->as_Call(); domain = call->tf()->domain(); @@ -1298,13 +1202,8 @@ MachNode *Matcher::match_sfpt( SafePointNode *sfpt ) { method = call_java->method(); mcall_java->_method = method; mcall_java->_optimized_virtual = call_java->is_optimized_virtual(); - is_method_handle_invoke = call_java->is_method_handle_invoke(); - mcall_java->_method_handle_invoke = is_method_handle_invoke; mcall_java->_override_symbolic_info = call_java->override_symbolic_info(); mcall_java->_arg_escape = call_java->arg_escape(); - if (is_method_handle_invoke) { - C->set_has_method_handle_invokes(true); - } if( mcall_java->is_MachCallStaticJava() ) mcall_java->as_MachCallStaticJava()->_name = call_java->as_CallStaticJava()->_name; diff --git a/src/hotspot/share/opto/matcher.hpp b/src/hotspot/share/opto/matcher.hpp index f4b0a7db873..0b609b70ab5 100644 --- a/src/hotspot/share/opto/matcher.hpp +++ b/src/hotspot/share/opto/matcher.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -177,7 +177,6 @@ public: static const RegMask *idealreg2regmask[]; RegMask *idealreg2spillmask [_last_machine_leaf]; RegMask *idealreg2debugmask [_last_machine_leaf]; - RegMask *idealreg2mhdebugmask[_last_machine_leaf]; void init_spill_mask( Node *ret ); // Convert machine register number to register mask static uint mreg2regmask_max; @@ -185,8 +184,6 @@ public: static RegMask STACK_ONLY_mask; static RegMask caller_save_regmask; static RegMask caller_save_regmask_exclude_soe; - static RegMask mh_caller_save_regmask; - static RegMask mh_caller_save_regmask_exclude_soe; MachNode* mach_null() const { return _mach_null; } @@ -424,8 +421,6 @@ public: // a code which use multiply for division by constant. static bool use_asm_for_ldiv_by_con( jlong divisor ); - static const RegMask method_handle_invoke_SP_save_mask(); - // Java-Interpreter calling convention // (what you use when calling between compiled-Java and Interpreted-Java diff --git a/src/hotspot/share/opto/output.cpp b/src/hotspot/share/opto/output.cpp index 90d24b609a7..84c01c68e38 100644 --- a/src/hotspot/share/opto/output.cpp +++ b/src/hotspot/share/opto/output.cpp @@ -992,7 +992,6 @@ void PhaseOutput::Process_OopMap_Node(MachNode *mach, int current_offset) { MachCallNode *mcall; int safepoint_pc_offset = current_offset; - bool is_method_handle_invoke = false; bool return_oop = false; bool has_ea_local_in_scope = sfn->_has_ea_local_in_scope; bool arg_escape = false; @@ -1004,12 +1003,7 @@ void PhaseOutput::Process_OopMap_Node(MachNode *mach, int current_offset) { } else { mcall = mach->as_MachCall(); - // Is the call a MethodHandle call? if (mcall->is_MachCallJava()) { - if (mcall->as_MachCallJava()->_method_handle_invoke) { - assert(C->has_method_handle_invokes(), "must have been set during call generation"); - is_method_handle_invoke = true; - } arg_escape = mcall->as_MachCallJava()->_arg_escape; } @@ -1192,7 +1186,6 @@ void PhaseOutput::Process_OopMap_Node(MachNode *mach, int current_offset) { jvms->bci(), jvms->should_reexecute(), rethrow_exception, - is_method_handle_invoke, return_oop, has_ea_local_in_scope, arg_escape, @@ -1370,9 +1363,6 @@ CodeBuffer* PhaseOutput::init_buffer() { exception_handler_req + deopt_handler_req; // deopt handler - if (C->has_method_handle_invokes()) - total_req += deopt_handler_req; // deopt MH handler - CodeBuffer* cb = code_buffer(); cb->set_const_section_alignment(constant_table().alignment()); cb->initialize(total_req, _buf_sizes._reloc); @@ -1806,13 +1796,6 @@ void PhaseOutput::fill_buffer(C2_MacroAssembler* masm, uint* blk_starts) { } // Emit the deopt handler code. _code_offsets.set_value(CodeOffsets::Deopt, HandlerImpl::emit_deopt_handler(masm)); - - // Emit the MethodHandle deopt handler code (if required). - if (C->has_method_handle_invokes() && !C->failing()) { - // We can use the same code as for the normal deopt handler, we - // just need a different entry point address. - _code_offsets.set_value(CodeOffsets::DeoptMH, HandlerImpl::emit_deopt_handler(masm)); - } } // One last check for failed CodeBuffer::expand: diff --git a/src/hotspot/share/opto/runtime.cpp b/src/hotspot/share/opto/runtime.cpp index 0d148edda6e..072b1384921 100644 --- a/src/hotspot/share/opto/runtime.cpp +++ b/src/hotspot/share/opto/runtime.cpp @@ -1913,9 +1913,6 @@ JRT_ENTRY_NO_ASYNC(address, OptoRuntime::handle_exception_C_helper(JavaThread* c current->set_exception_pc(pc); current->set_exception_handler_pc(handler_address); - - // Check if the exception PC is a MethodHandle call site. - current->set_is_method_handle_return(nm->is_method_handle_return(pc)); } // Restore correct return pc. Was saved above. diff --git a/src/hotspot/share/runtime/deoptimization.cpp b/src/hotspot/share/runtime/deoptimization.cpp index a96c18a18aa..853c6554022 100644 --- a/src/hotspot/share/runtime/deoptimization.cpp +++ b/src/hotspot/share/runtime/deoptimization.cpp @@ -589,12 +589,7 @@ Deoptimization::UnrollBlock* Deoptimization::fetch_unroll_info_helper(JavaThread // Verify we have the right vframeArray assert(cb->frame_size() >= 0, "Unexpected frame size"); intptr_t* unpack_sp = stub_frame.sp() + cb->frame_size(); - - // If the deopt call site is a MethodHandle invoke call site we have - // to adjust the unpack_sp. - nmethod* deoptee_nm = deoptee.cb()->as_nmethod_or_null(); - if (deoptee_nm != nullptr && deoptee_nm->is_method_handle_return(deoptee.pc())) - unpack_sp = deoptee.unextended_sp(); + assert(unpack_sp == deoptee.unextended_sp(), "must be"); #ifdef ASSERT assert(cb->is_deoptimization_stub() || diff --git a/src/hotspot/share/runtime/frame.cpp b/src/hotspot/share/runtime/frame.cpp index 75c6e388b0d..e578e614440 100644 --- a/src/hotspot/share/runtime/frame.cpp +++ b/src/hotspot/share/runtime/frame.cpp @@ -206,10 +206,7 @@ address frame::raw_pc() const { if (is_deoptimized_frame()) { nmethod* nm = cb()->as_nmethod_or_null(); assert(nm != nullptr, "only nmethod is expected here"); - if (nm->is_method_handle_return(pc())) - return nm->deopt_mh_handler_begin() - pc_return_offset; - else - return nm->deopt_handler_begin() - pc_return_offset; + return nm->deopt_handler_begin() - pc_return_offset; } else { return (pc() - pc_return_offset); } @@ -358,9 +355,7 @@ void frame::deoptimize(JavaThread* thread) { // If the call site is a MethodHandle call site use the MH deopt handler. nmethod* nm = _cb->as_nmethod(); - address deopt = nm->is_method_handle_return(pc()) ? - nm->deopt_mh_handler_begin() : - nm->deopt_handler_begin(); + address deopt = nm->deopt_handler_begin(); NativePostCallNop* inst = nativePostCallNop_at(pc()); diff --git a/src/hotspot/share/runtime/frame.inline.hpp b/src/hotspot/share/runtime/frame.inline.hpp index 449abddd443..cbf01dd5763 100644 --- a/src/hotspot/share/runtime/frame.inline.hpp +++ b/src/hotspot/share/runtime/frame.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -76,7 +76,10 @@ inline address frame::get_deopt_original_pc() const { nmethod* nm = _cb->as_nmethod_or_null(); if (nm != nullptr && nm->is_deopt_pc(_pc)) { - return nm->get_original_pc(this); + address original_pc = nm->get_original_pc(this); + assert(nm->insts_contains_inclusive(original_pc), + "original PC must be in the main code section of the compiled method (or must be immediately following it)"); + return original_pc; } return nullptr; } diff --git a/src/hotspot/share/runtime/javaThread.cpp b/src/hotspot/share/runtime/javaThread.cpp index f5cd43b1769..e5af8d7bedd 100644 --- a/src/hotspot/share/runtime/javaThread.cpp +++ b/src/hotspot/share/runtime/javaThread.cpp @@ -476,7 +476,6 @@ JavaThread::JavaThread(MemTag mem_tag) : _exception_oop(oop()), _exception_pc(nullptr), _exception_handler_pc(nullptr), - _is_method_handle_return(0), _jni_active_critical(0), _pending_jni_exception_check_fn(nullptr), diff --git a/src/hotspot/share/runtime/javaThread.hpp b/src/hotspot/share/runtime/javaThread.hpp index 00bc5969196..89c3191669a 100644 --- a/src/hotspot/share/runtime/javaThread.hpp +++ b/src/hotspot/share/runtime/javaThread.hpp @@ -450,7 +450,6 @@ class JavaThread: public Thread { volatile oop _exception_oop; // Exception thrown in compiled code volatile address _exception_pc; // PC where exception happened volatile address _exception_handler_pc; // PC for handler of exception - volatile int _is_method_handle_return; // true (== 1) if the current exception PC is a MethodHandle call site. private: // support for JNI critical regions @@ -817,7 +816,6 @@ public: void set_exception_oop(oop o); void set_exception_pc(address a) { _exception_pc = a; } void set_exception_handler_pc(address a) { _exception_handler_pc = a; } - void set_is_method_handle_return(bool value) { _is_method_handle_return = value ? 1 : 0; } void clear_exception_oop_and_pc() { set_exception_oop(nullptr); @@ -866,7 +864,6 @@ public: static ByteSize exception_oop_offset() { return byte_offset_of(JavaThread, _exception_oop); } static ByteSize exception_pc_offset() { return byte_offset_of(JavaThread, _exception_pc); } static ByteSize exception_handler_pc_offset() { return byte_offset_of(JavaThread, _exception_handler_pc); } - static ByteSize is_method_handle_return_offset() { return byte_offset_of(JavaThread, _is_method_handle_return); } static ByteSize active_handles_offset() { return byte_offset_of(JavaThread, _active_handles); } diff --git a/src/hotspot/share/runtime/sharedRuntime.cpp b/src/hotspot/share/runtime/sharedRuntime.cpp index c3a6e0a4dc3..c81c975372c 100644 --- a/src/hotspot/share/runtime/sharedRuntime.cpp +++ b/src/hotspot/share/runtime/sharedRuntime.cpp @@ -525,9 +525,6 @@ address SharedRuntime::raw_exception_handler_for_return_address(JavaThread* curr assert(frame::verify_return_pc(return_address), "must be a return address: " INTPTR_FORMAT, p2i(return_address)); assert(current->frames_to_pop_failed_realloc() == 0 || Interpreter::contains(return_address), "missed frames to pop?"); - // Reset method handle flag. - current->set_is_method_handle_return(false); - #if INCLUDE_JVMCI // JVMCI's ExceptionHandlerStub expects the thread local exception PC to be clear // and other exception handler continuations do not read it @@ -542,8 +539,6 @@ address SharedRuntime::raw_exception_handler_for_return_address(JavaThread* curr CodeBlob* blob = CodeCache::find_blob(return_address); nmethod* nm = (blob != nullptr) ? blob->as_nmethod_or_null() : nullptr; if (nm != nullptr) { - // Set flag if return address is a method handle call site. - current->set_is_method_handle_return(nm->is_method_handle_return(return_address)); // native nmethods don't have exception handlers assert(!nm->is_native_method() || nm->method()->is_continuation_enter_intrinsic(), "no exception handler"); assert(nm->header_begin() != nm->exception_begin(), "no exception handler"); diff --git a/src/hotspot/share/runtime/vmStructs.cpp b/src/hotspot/share/runtime/vmStructs.cpp index 5a3850feac8..dee0a5d4eb7 100644 --- a/src/hotspot/share/runtime/vmStructs.cpp +++ b/src/hotspot/share/runtime/vmStructs.cpp @@ -536,7 +536,6 @@ nonstatic_field(nmethod, _state, volatile signed char) \ nonstatic_field(nmethod, _exception_offset, int) \ nonstatic_field(nmethod, _deopt_handler_offset, int) \ - nonstatic_field(nmethod, _deopt_mh_handler_offset, int) \ nonstatic_field(nmethod, _orig_pc_offset, int) \ nonstatic_field(nmethod, _stub_offset, int) \ nonstatic_field(nmethod, _scopes_pcs_offset, int) \ @@ -608,7 +607,6 @@ volatile_nonstatic_field(JavaThread, _suspend_flags, uint32_t) \ volatile_nonstatic_field(JavaThread, _exception_oop, oop) \ volatile_nonstatic_field(JavaThread, _exception_pc, address) \ - volatile_nonstatic_field(JavaThread, _is_method_handle_return, int) \ nonstatic_field(JavaThread, _saved_exception_pc, address) \ volatile_nonstatic_field(JavaThread, _thread_state, JavaThreadState) \ nonstatic_field(JavaThread, _stack_base, address) \ @@ -1711,7 +1709,6 @@ /**********************/ \ \ declare_constant(PcDesc::PCDESC_reexecute) \ - declare_constant(PcDesc::PCDESC_is_method_handle_invoke) \ declare_constant(PcDesc::PCDESC_return_oop) \ \ /**********************/ \ diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java index a3228765e91..c8ba2e8b5af 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java @@ -48,7 +48,6 @@ public class NMethod extends CodeBlob { /** Offsets for different nmethod parts */ private static CIntegerField exceptionOffsetField; private static CIntegerField deoptHandlerOffsetField; - private static CIntegerField deoptMhHandlerOffsetField; private static CIntegerField origPCOffsetField; private static CIntegerField stubOffsetField; private static CIntField handlerTableOffsetField; @@ -86,7 +85,6 @@ public class NMethod extends CodeBlob { immutableDataSizeField = type.getCIntegerField("_immutable_data_size"); exceptionOffsetField = type.getCIntegerField("_exception_offset"); deoptHandlerOffsetField = type.getCIntegerField("_deopt_handler_offset"); - deoptMhHandlerOffsetField = type.getCIntegerField("_deopt_mh_handler_offset"); origPCOffsetField = type.getCIntegerField("_orig_pc_offset"); stubOffsetField = type.getCIntegerField("_stub_offset"); scopesPCsOffsetField = type.getCIntegerField("_scopes_pcs_offset"); @@ -125,7 +123,6 @@ public class NMethod extends CodeBlob { public Address instsEnd() { return headerBegin().addOffsetTo(getStubOffset()); } public Address exceptionBegin() { return headerBegin().addOffsetTo(getExceptionOffset()); } public Address deoptHandlerBegin() { return headerBegin().addOffsetTo(getDeoptHandlerOffset()); } - public Address deoptMhHandlerBegin() { return headerBegin().addOffsetTo(getDeoptMhHandlerOffset()); } public Address stubBegin() { return headerBegin().addOffsetTo(getStubOffset()); } public Address stubEnd() { return dataBegin(); } public Address oopsBegin() { return dataBegin(); } @@ -250,22 +247,10 @@ public class NMethod extends CodeBlob { return VMObjectFactory.newObject(NMethod.class, osrLinkField.getValue(addr)); } - // MethodHandle - public boolean isMethodHandleReturn(Address returnPc) { - // Hard to read a bit fields from Java and it's only there for performance - // so just go directly to the PCDesc - // if (!hasMethodHandleInvokes()) return false; - PCDesc pd = getPCDescAt(returnPc); - if (pd == null) - return false; - return pd.isMethodHandleInvoke(); - } - // Deopt // Return true is the PC is one would expect if the frame is being deopted. - public boolean isDeoptPc (Address pc) { return isDeoptEntry(pc) || isDeoptMhEntry(pc); } + public boolean isDeoptPc (Address pc) { return isDeoptEntry(pc); } public boolean isDeoptEntry (Address pc) { return pc == deoptHandlerBegin(); } - public boolean isDeoptMhEntry (Address pc) { return pc == deoptMhHandlerBegin(); } /** Tells whether frames described by this nmethod can be deoptimized. Note: native wrappers cannot be deoptimized. */ @@ -494,7 +479,6 @@ public class NMethod extends CodeBlob { private int getEntryBCI() { return (int) entryBCIField .getValue(addr); } private int getExceptionOffset() { return (int) exceptionOffsetField .getValue(addr); } private int getDeoptHandlerOffset() { return (int) deoptHandlerOffsetField .getValue(addr); } - private int getDeoptMhHandlerOffset() { return (int) deoptMhHandlerOffsetField.getValue(addr); } private int getStubOffset() { return (int) stubOffsetField .getValue(addr); } private int getScopesDataOffset() { return (int) scopesDataOffsetField .getValue(addr); } private int getScopesPCsOffset() { return (int) scopesPCsOffsetField .getValue(addr); } diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/PCDesc.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/PCDesc.java index 58f358c01aa..037c26b53f5 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/PCDesc.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/PCDesc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,7 +41,6 @@ public class PCDesc extends VMObject { private static CIntegerField objDecodeOffsetField; private static CIntegerField pcFlagsField; private static int reexecuteMask; - private static int isMethodHandleInvokeMask; private static int returnOopMask; static { @@ -61,7 +60,6 @@ public class PCDesc extends VMObject { pcFlagsField = type.getCIntegerField("_flags"); reexecuteMask = db.lookupIntConstant("PcDesc::PCDESC_reexecute"); - isMethodHandleInvokeMask = db.lookupIntConstant("PcDesc::PCDESC_is_method_handle_invoke"); returnOopMask = db.lookupIntConstant("PcDesc::PCDESC_return_oop"); } @@ -93,11 +91,6 @@ public class PCDesc extends VMObject { return (flags & reexecuteMask) != 0; } - public boolean isMethodHandleInvoke() { - int flags = (int)pcFlagsField.getValue(addr); - return (flags & isMethodHandleInvokeMask) != 0; - } - public void print(NMethod code) { printOn(System.out, code); } diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Frame.java index a8c17a9f59e..27efb631f79 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Frame.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -81,6 +81,24 @@ public abstract class Frame implements Cloneable { return pcReturnOffset; } + protected void adjustForDeopt() { + if (pc != null) { + // Look for a deopt pc and if it is deopted convert to original pc + CodeBlob cb = VM.getVM().getCodeCache().findBlob(pc); + if (cb != null && cb.isJavaMethod()) { + NMethod nm = (NMethod) cb; + if (pc.equals(nm.deoptHandlerBegin())) { + if (Assert.ASSERTS_ENABLED) { + Assert.that(this.getUnextendedSP() != null, "null SP in Java frame"); + } + // adjust pc if frame is deoptimized. + pc = this.getUnextendedSP().getAddressAt(nm.origPCOffset()); + deoptimized = true; + } + } + } + } + private static synchronized void initialize(TypeDataBase db) { Type ConstMethodType = db.lookupType("ConstMethod"); // FIXME: not sure whether alignment here is correct or how to diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java index dca5f2efa3b..a5aa7ce4405 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2015, 2019, Red Hat Inc. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -106,30 +106,11 @@ public class AARCH64Frame extends Frame { private AARCH64Frame() { } - private void adjustForDeopt() { - if ( pc != null) { - // Look for a deopt pc and if it is deopted convert to original pc - CodeBlob cb = VM.getVM().getCodeCache().findBlob(pc); - if (cb != null && cb.isJavaMethod()) { - NMethod nm = (NMethod) cb; - if (pc.equals(nm.deoptHandlerBegin())) { - if (Assert.ASSERTS_ENABLED) { - Assert.that(this.getUnextendedSP() != null, "null SP in Java frame"); - } - // adjust pc if frame is deoptimized. - pc = this.getUnextendedSP().getAddressAt(nm.origPCOffset()); - deoptimized = true; - } - } - } - } - public AARCH64Frame(Address raw_sp, Address raw_fp, Address pc) { this.raw_sp = raw_sp; this.raw_unextendedSP = raw_sp; this.raw_fp = raw_fp; this.pc = pc; - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); @@ -153,8 +134,6 @@ public class AARCH64Frame extends Frame { this.pc = savedPC; } - adjustUnextendedSP(); - // Frame must be fully constructed before this call adjustForDeopt(); @@ -169,7 +148,6 @@ public class AARCH64Frame extends Frame { this.raw_unextendedSP = raw_unextendedSp; this.raw_fp = raw_fp; this.pc = pc; - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); @@ -355,24 +333,6 @@ public class AARCH64Frame extends Frame { return fr; } - //------------------------------------------------------------------------------ - // frame::adjust_unextended_sp - private void adjustUnextendedSP() { - // Sites calling method handle intrinsics and lambda forms are - // treated as any other call site. Therefore, no special action is - // needed when we are returning to any of these call sites. - - CodeBlob cb = cb(); - NMethod senderNm = (cb == null) ? null : cb.asNMethodOrNull(); - if (senderNm != null) { - // If the sender PC is a deoptimization point, get the original PC. - if (senderNm.isDeoptEntry(getPC()) || - senderNm.isDeoptMhEntry(getPC())) { - // DEBUG_ONLY(verifyDeoptriginalPc(senderNm, raw_unextendedSp)); - } - } - } - private Frame senderForInterpreterFrame(AARCH64RegisterMap map) { if (DEBUG) { System.out.println("senderForInterpreterFrame"); diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ppc64/PPC64Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ppc64/PPC64Frame.java index a47a632c286..f4ce5337e2f 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ppc64/PPC64Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ppc64/PPC64Frame.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -85,24 +85,6 @@ public class PPC64Frame extends Frame { private PPC64Frame() { } - private void adjustForDeopt() { - if ( pc != null) { - // Look for a deopt pc and if it is deopted convert to original pc - CodeBlob cb = VM.getVM().getCodeCache().findBlob(pc); - if (cb != null && cb.isJavaMethod()) { - NMethod nm = (NMethod) cb; - if (pc.equals(nm.deoptHandlerBegin())) { - if (Assert.ASSERTS_ENABLED) { - Assert.that(this.getUnextendedSP() != null, "null SP in Java frame"); - } - // adjust pc if frame is deoptimized. - pc = this.getUnextendedSP().getAddressAt(nm.origPCOffset()); - deoptimized = true; - } - } - } - } - public PPC64Frame(Address raw_sp, Address raw_fp, Address pc) { this.raw_sp = raw_sp; this.raw_unextendedSP = raw_sp; @@ -116,7 +98,6 @@ public class PPC64Frame extends Frame { } else { this.pc = pc; } - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); @@ -136,7 +117,6 @@ public class PPC64Frame extends Frame { this.raw_fp = raw_fp; } this.pc = raw_sp.getAddressAt(2 * VM.getVM().getAddressSize()); - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); @@ -160,7 +140,6 @@ public class PPC64Frame extends Frame { } else { this.pc = pc; } - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); @@ -342,12 +321,6 @@ public class PPC64Frame extends Frame { return fr; } - //------------------------------------------------------------------------------ - // frame::adjust_unextended_sp - private void adjustUnextendedSP() { - // Nothing to do. senderForInterpreterFrame finds the correct unextendedSP. - } - private Frame senderForInterpreterFrame(PPC64RegisterMap map) { if (DEBUG) { System.out.println("senderForInterpreterFrame"); diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java index 948a3008016..e02e056f028 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2015, 2019, Red Hat Inc. * Copyright (c) 2021, 2023, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -101,30 +101,11 @@ public class RISCV64Frame extends Frame { private RISCV64Frame() { } - private void adjustForDeopt() { - if ( pc != null) { - // Look for a deopt pc and if it is deopted convert to original pc - CodeBlob cb = VM.getVM().getCodeCache().findBlob(pc); - if (cb != null && cb.isJavaMethod()) { - NMethod nm = (NMethod) cb; - if (pc.equals(nm.deoptHandlerBegin())) { - if (Assert.ASSERTS_ENABLED) { - Assert.that(this.getUnextendedSP() != null, "null SP in Java frame"); - } - // adjust pc if frame is deoptimized. - pc = this.getUnextendedSP().getAddressAt(nm.origPCOffset()); - deoptimized = true; - } - } - } - } - public RISCV64Frame(Address raw_sp, Address raw_fp, Address pc) { this.raw_sp = raw_sp; this.raw_unextendedSP = raw_sp; this.raw_fp = raw_fp; this.pc = pc; - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); @@ -148,8 +129,6 @@ public class RISCV64Frame extends Frame { this.pc = savedPC; } - adjustUnextendedSP(); - // Frame must be fully constructed before this call adjustForDeopt(); @@ -164,7 +143,6 @@ public class RISCV64Frame extends Frame { this.raw_unextendedSP = raw_unextendedSp; this.raw_fp = raw_fp; this.pc = pc; - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); @@ -347,24 +325,6 @@ public class RISCV64Frame extends Frame { return fr; } - //------------------------------------------------------------------------------ - // frame::adjust_unextended_sp - private void adjustUnextendedSP() { - // Sites calling method handle intrinsics and lambda forms are - // treated as any other call site. Therefore, no special action is - // needed when we are returning to any of these call sites. - - CodeBlob cb = cb(); - NMethod senderNm = (cb == null) ? null : cb.asNMethodOrNull(); - if (senderNm != null) { - // If the sender PC is a deoptimization point, get the original PC. - if (senderNm.isDeoptEntry(getPC()) || - senderNm.isDeoptMhEntry(getPC())) { - // DEBUG_ONLY(verifyDeoptriginalPc(senderNm, raw_unextendedSp)); - } - } - } - private Frame senderForInterpreterFrame(RISCV64RegisterMap map) { if (DEBUG) { System.out.println("senderForInterpreterFrame"); diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java index 169ecea1565..3ee4f0a8158 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -102,24 +102,6 @@ public class X86Frame extends Frame { private X86Frame() { } - private void adjustForDeopt() { - if ( pc != null) { - // Look for a deopt pc and if it is deopted convert to original pc - CodeBlob cb = VM.getVM().getCodeCache().findBlob(pc); - if (cb != null && cb.isJavaMethod()) { - NMethod nm = (NMethod) cb; - if (pc.equals(nm.deoptHandlerBegin())) { - if (Assert.ASSERTS_ENABLED) { - Assert.that(this.getUnextendedSP() != null, "null SP in Java frame"); - } - // adjust pc if frame is deoptimized. - pc = this.getUnextendedSP().getAddressAt(nm.origPCOffset()); - deoptimized = true; - } - } - } - } - private void initFrame(Address raw_sp, Address raw_fp, Address pc, Address raw_unextendedSp, Address live_bcp) { this.raw_sp = raw_sp; this.raw_fp = raw_fp; @@ -134,11 +116,10 @@ public class X86Frame extends Frame { this.pc = pc; } this.live_bcp = live_bcp; - adjustUnextendedSP(); // Frame must be fully constructed before this call adjustForDeopt(); -} + } public X86Frame(Address raw_sp, Address raw_fp, Address pc) { @@ -352,24 +333,6 @@ public class X86Frame extends Frame { return fr; } - //------------------------------------------------------------------------------ - // frame::adjust_unextended_sp - private void adjustUnextendedSP() { - // On x86, sites calling method handle intrinsics and lambda forms are treated - // as any other call site. Therefore, no special action is needed when we are - // returning to any of these call sites. - - CodeBlob cb = cb(); - NMethod senderNm = (cb == null) ? null : cb.asNMethodOrNull(); - if (senderNm != null) { - // If the sender PC is a deoptimization point, get the original PC. - if (senderNm.isDeoptEntry(getPC()) || - senderNm.isDeoptMhEntry(getPC())) { - // DEBUG_ONLY(verifyDeoptriginalPc(senderNm, raw_unextendedSp)); - } - } - } - private Frame senderForInterpreterFrame(X86RegisterMap map) { if (DEBUG) { System.out.println("senderForInterpreterFrame"); From 854b384b120fa2af41adca3048070866fe3cafd4 Mon Sep 17 00:00:00 2001 From: Alex Menkov Date: Thu, 2 Oct 2025 23:39:37 +0000 Subject: [PATCH 338/556] 8304811: vmTestbase/vm/mlvm/indy/func/jvmti/stepBreakPopReturn/INDIFY_Test.java fails with JVMTI_ERROR_TYPE_MISMATCH Reviewed-by: lmesnik, dholmes, sspitsyn --- .../func/jvmti/stepBreakPopReturn/stepBreakPopReturn.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/hotspot/jtreg/vmTestbase/vm/mlvm/indy/func/jvmti/stepBreakPopReturn/stepBreakPopReturn.cpp b/test/hotspot/jtreg/vmTestbase/vm/mlvm/indy/func/jvmti/stepBreakPopReturn/stepBreakPopReturn.cpp index 413450892a5..0f7d1826937 100644 --- a/test/hotspot/jtreg/vmTestbase/vm/mlvm/indy/func/jvmti/stepBreakPopReturn/stepBreakPopReturn.cpp +++ b/test/hotspot/jtreg/vmTestbase/vm/mlvm/indy/func/jvmti/stepBreakPopReturn/stepBreakPopReturn.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -89,7 +89,7 @@ MethodEntry(jvmtiEnv *jvmti_env, gIsMethodEntryWorking = JNI_TRUE; if (!gIsBreakpointSet) - NSK_JVMTI_VERIFY(jvmti_env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_SINGLE_STEP, nullptr)); + NSK_JVMTI_VERIFY(jvmti_env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_SINGLE_STEP, thread)); } } @@ -116,7 +116,7 @@ SingleStep(jvmtiEnv *jvmti_env, free(locStr); } - NSK_JVMTI_VERIFY(gJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_SINGLE_STEP, nullptr)); + NSK_JVMTI_VERIFY(gJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_SINGLE_STEP, thread)); if (!gIsDebuggerCompatible) { if (!NSK_JVMTI_VERIFY(jvmti_env->SetBreakpoint(method, location))) From f62b9eca08694bbbe80d9e7d7b704db4f2423830 Mon Sep 17 00:00:00 2001 From: Ashutosh Mehra Date: Fri, 3 Oct 2025 02:43:14 +0000 Subject: [PATCH 339/556] 8364929: Assign unique id to each AdapterBlob stored in AOTCodeCache Reviewed-by: kvn, iklam --- src/hotspot/share/runtime/sharedRuntime.cpp | 35 ++++++++++++++------- src/hotspot/share/runtime/sharedRuntime.hpp | 14 ++++++--- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/hotspot/share/runtime/sharedRuntime.cpp b/src/hotspot/share/runtime/sharedRuntime.cpp index c81c975372c..cf949fe1e7c 100644 --- a/src/hotspot/share/runtime/sharedRuntime.cpp +++ b/src/hotspot/share/runtime/sharedRuntime.cpp @@ -2515,6 +2515,7 @@ ArchivedAdapterTable AdapterHandlerLibrary::_aot_adapter_handler_table; #endif // INCLUDE_CDS static const int AdapterHandlerLibrary_size = 16*K; BufferBlob* AdapterHandlerLibrary::_buffer = nullptr; +volatile uint AdapterHandlerLibrary::_id_counter = 0; BufferBlob* AdapterHandlerLibrary::buffer_blob() { assert(_buffer != nullptr, "should be initialized"); @@ -2595,7 +2596,9 @@ void AdapterHandlerLibrary::initialize() { } AdapterHandlerEntry* AdapterHandlerLibrary::new_entry(AdapterFingerPrint* fingerprint) { - return AdapterHandlerEntry::allocate(fingerprint); + uint id = (uint)AtomicAccess::add((int*)&_id_counter, 1); + assert(id > 0, "we can never overflow because AOT cache cannot contain more than 2^32 methods"); + return AdapterHandlerEntry::allocate(id, fingerprint); } AdapterHandlerEntry* AdapterHandlerLibrary::get_simple_adapter(const methodHandle& method) { @@ -2749,8 +2752,8 @@ AdapterHandlerEntry* AdapterHandlerLibrary::get_adapter(const methodHandle& meth void AdapterHandlerLibrary::lookup_aot_cache(AdapterHandlerEntry* handler) { ResourceMark rm; - const char* name = AdapterHandlerLibrary::name(handler->fingerprint()); - const uint32_t id = AdapterHandlerLibrary::id(handler->fingerprint()); + const char* name = AdapterHandlerLibrary::name(handler); + const uint32_t id = AdapterHandlerLibrary::id(handler); CodeBlob* blob = AOTCodeCache::load_code_blob(AOTCodeEntry::Adapter, id, name); if (blob != nullptr) { @@ -2845,8 +2848,8 @@ bool AdapterHandlerLibrary::generate_adapter_code(AdapterHandlerEntry* handler, handler->set_adapter_blob(adapter_blob); if (!is_transient && AOTCodeCache::is_dumping_adapter()) { // try to save generated code - const char* name = AdapterHandlerLibrary::name(handler->fingerprint()); - const uint32_t id = AdapterHandlerLibrary::id(handler->fingerprint()); + const char* name = AdapterHandlerLibrary::name(handler); + const uint32_t id = AdapterHandlerLibrary::id(handler); bool success = AOTCodeCache::store_code_blob(*adapter_blob, AOTCodeEntry::Adapter, id, name); assert(success || !AOTCodeCache::is_dumping_adapter(), "caching of adapter must be disabled"); } @@ -2986,11 +2989,22 @@ void AdapterHandlerEntry::link() { } void AdapterHandlerLibrary::link_aot_adapters() { + uint max_id = 0; assert(AOTCodeCache::is_using_adapter(), "AOT adapters code should be available"); - _aot_adapter_handler_table.iterate([](AdapterHandlerEntry* entry) { + /* It is possible that some adapters generated in assembly phase are not stored in the cache. + * That implies adapter ids of the adapters in the cache may not be contiguous. + * If the size of the _aot_adapter_handler_table is used to initialize _id_counter, then it may + * result in collision of adapter ids between AOT stored handlers and runtime generated handlers. + * To avoid such situation, initialize the _id_counter with the largest adapter id among the AOT stored handlers. + */ + _aot_adapter_handler_table.iterate([&](AdapterHandlerEntry* entry) { assert(!entry->is_linked(), "AdapterHandlerEntry is already linked!"); entry->link(); + max_id = MAX2(max_id, entry->id()); }); + // Set adapter id to the maximum id found in the AOTCache + assert(_id_counter == 0, "Did not expect new AdapterHandlerEntry to be created at this stage"); + _id_counter = max_id; } // This method is called during production run to lookup simple adapters @@ -3355,13 +3369,12 @@ bool AdapterHandlerLibrary::contains(const CodeBlob* b) { return found; } -const char* AdapterHandlerLibrary::name(AdapterFingerPrint* fingerprint) { - return fingerprint->as_basic_args_string(); +const char* AdapterHandlerLibrary::name(AdapterHandlerEntry* handler) { + return handler->fingerprint()->as_basic_args_string(); } -uint32_t AdapterHandlerLibrary::id(AdapterFingerPrint* fingerprint) { - unsigned int hash = fingerprint->compute_hash(); - return hash; +uint32_t AdapterHandlerLibrary::id(AdapterHandlerEntry* handler) { + return handler->id(); } void AdapterHandlerLibrary::print_handler_on(outputStream* st, const CodeBlob* b) { diff --git a/src/hotspot/share/runtime/sharedRuntime.hpp b/src/hotspot/share/runtime/sharedRuntime.hpp index 374985ad921..6544e380d99 100644 --- a/src/hotspot/share/runtime/sharedRuntime.hpp +++ b/src/hotspot/share/runtime/sharedRuntime.hpp @@ -686,6 +686,7 @@ class AdapterHandlerEntry : public MetaspaceObj { private: AdapterFingerPrint* _fingerprint; AdapterBlob* _adapter_blob; + uint _id; bool _linked; static const char *_entry_names[]; @@ -697,9 +698,10 @@ class AdapterHandlerEntry : public MetaspaceObj { int _saved_code_length; #endif - AdapterHandlerEntry(AdapterFingerPrint* fingerprint) : + AdapterHandlerEntry(int id, AdapterFingerPrint* fingerprint) : _fingerprint(fingerprint), _adapter_blob(nullptr), + _id(id), _linked(false) #ifdef ASSERT , _saved_code(nullptr), @@ -720,8 +722,8 @@ class AdapterHandlerEntry : public MetaspaceObj { } public: - static AdapterHandlerEntry* allocate(AdapterFingerPrint* fingerprint) { - return new(0) AdapterHandlerEntry(fingerprint); + static AdapterHandlerEntry* allocate(uint id, AdapterFingerPrint* fingerprint) { + return new(0) AdapterHandlerEntry(id, fingerprint); } static void deallocate(AdapterHandlerEntry *handler) { @@ -772,6 +774,7 @@ class AdapterHandlerEntry : public MetaspaceObj { AdapterBlob* adapter_blob() const { return _adapter_blob; } bool is_linked() const { return _linked; } + uint id() const { return _id; } AdapterFingerPrint* fingerprint() const { return _fingerprint; } #ifdef ASSERT @@ -798,6 +801,7 @@ class ArchivedAdapterTable; class AdapterHandlerLibrary: public AllStatic { friend class SharedRuntime; private: + static volatile uint _id_counter; // counter for generating unique adapter ids, range = [1,UINT_MAX] static BufferBlob* _buffer; // the temporary code buffer in CodeCache static AdapterHandlerEntry* _no_arg_handler; static AdapterHandlerEntry* _int_arg_handler; @@ -837,8 +841,8 @@ class AdapterHandlerLibrary: public AllStatic { static void print_handler(const CodeBlob* b) { print_handler_on(tty, b); } static void print_handler_on(outputStream* st, const CodeBlob* b); static bool contains(const CodeBlob* b); - static const char* name(AdapterFingerPrint* fingerprint); - static uint32_t id(AdapterFingerPrint* fingerprint); + static const char* name(AdapterHandlerEntry* handler); + static uint32_t id(AdapterHandlerEntry* handler); #ifndef PRODUCT static void print_statistics(); #endif // PRODUCT From 3790965df3e7cba3b9792b8719d1e2ead046da15 Mon Sep 17 00:00:00 2001 From: Joe Wang Date: Fri, 3 Oct 2025 03:50:01 +0000 Subject: [PATCH 340/556] 8336695: Update Commons BCEL to Version 6.10.0 Reviewed-by: lancea, naoto, iris --- .../sun/org/apache/bcel/internal/Const.java | 828 ++++++++++-------- .../apache/bcel/internal/ExceptionConst.java | 7 +- .../org/apache/bcel/internal/Repository.java | 6 +- .../bcel/internal/classfile/AccessFlags.java | 232 ++++- .../internal/classfile/AnnotationEntry.java | 15 +- .../bcel/internal/classfile/Annotations.java | 10 +- .../internal/classfile/ArrayElementValue.java | 4 +- .../bcel/internal/classfile/Attribute.java | 8 +- .../internal/classfile/BootstrapMethod.java | 15 +- .../internal/classfile/BootstrapMethods.java | 6 +- .../classfile/ClassFormatException.java | 10 +- .../bcel/internal/classfile/ClassParser.java | 12 +- .../apache/bcel/internal/classfile/Code.java | 45 +- .../internal/classfile/CodeException.java | 8 +- .../bcel/internal/classfile/Constant.java | 31 +- .../bcel/internal/classfile/ConstantCP.java | 12 +- .../internal/classfile/ConstantDouble.java | 6 +- .../internal/classfile/ConstantFloat.java | 6 +- .../internal/classfile/ConstantInteger.java | 6 +- .../bcel/internal/classfile/ConstantLong.java | 6 +- .../internal/classfile/ConstantObject.java | 7 +- .../bcel/internal/classfile/ConstantPool.java | 29 +- .../bcel/internal/classfile/ConstantUtf8.java | 23 +- .../internal/classfile/ConstantValue.java | 2 +- .../bcel/internal/classfile/Deprecated.java | 2 +- .../internal/classfile/DescendingVisitor.java | 41 +- .../bcel/internal/classfile/ElementValue.java | 7 +- .../bcel/internal/classfile/EmptyVisitor.java | 9 + .../internal/classfile/ExceptionTable.java | 11 +- .../apache/bcel/internal/classfile/Field.java | 38 +- .../internal/classfile/FieldOrMethod.java | 38 +- .../bcel/internal/classfile/InnerClass.java | 2 +- .../bcel/internal/classfile/InnerClasses.java | 8 +- .../InvalidMethodSignatureException.java | 53 ++ .../bcel/internal/classfile/JavaClass.java | 152 +++- .../bcel/internal/classfile/LineNumber.java | 4 +- .../internal/classfile/LineNumberTable.java | 23 +- .../classfile/LocalVariableTable.java | 10 +- .../classfile/LocalVariableTypeTable.java | 11 +- .../bcel/internal/classfile/Method.java | 49 +- .../internal/classfile/MethodParameter.java | 12 +- .../internal/classfile/MethodParameters.java | 8 +- .../bcel/internal/classfile/Module.java | 66 +- .../internal/classfile/ModuleExports.java | 49 +- .../internal/classfile/ModuleMainClass.java | 2 +- .../bcel/internal/classfile/ModuleOpens.java | 49 +- .../internal/classfile/ModulePackages.java | 11 +- .../internal/classfile/ModuleProvides.java | 45 +- .../internal/classfile/ModuleRequires.java | 39 +- .../bcel/internal/classfile/NestMembers.java | 11 +- .../apache/bcel/internal/classfile/Node.java | 2 +- .../bcel/internal/classfile/PMGClass.java | 2 +- .../classfile/ParameterAnnotationEntry.java | 18 +- .../classfile/ParameterAnnotations.java | 8 +- .../bcel/internal/classfile/Record.java | 153 ++++ .../classfile/RecordComponentInfo.java | 139 +++ .../RuntimeInvisibleAnnotations.java | 8 +- .../RuntimeInvisibleParameterAnnotations.java | 2 + .../classfile/RuntimeVisibleAnnotations.java | 8 +- .../RuntimeVisibleParameterAnnotations.java | 2 + .../bcel/internal/classfile/Signature.java | 10 +- .../classfile/SimpleElementValue.java | 20 +- .../bcel/internal/classfile/SourceFile.java | 2 +- .../bcel/internal/classfile/StackMap.java | 10 +- .../internal/classfile/StackMapEntry.java | 13 +- .../bcel/internal/classfile/StackMapType.java | 29 +- .../bcel/internal/classfile/Synthetic.java | 2 +- .../bcel/internal/classfile/Utility.java | 60 +- .../bcel/internal/classfile/Visitor.java | 32 + .../bcel/internal/classfile/package-info.java | 25 + .../bcel/internal/generic/ARRAYLENGTH.java | 6 +- .../apache/bcel/internal/generic/ATHROW.java | 7 +- .../internal/generic/AnnotationEntryGen.java | 22 +- .../generic/ArrayElementValueGen.java | 15 +- .../bcel/internal/generic/ArrayType.java | 10 +- .../bcel/internal/generic/BranchHandle.java | 2 +- .../bcel/internal/generic/CPInstruction.java | 6 +- .../generic/ClassElementValueGen.java | 8 +- .../bcel/internal/generic/ClassGen.java | 82 +- .../internal/generic/CodeExceptionGen.java | 10 +- .../internal/generic/ElementValuePairGen.java | 4 +- .../internal/generic/EnumElementValueGen.java | 26 +- .../internal/generic/ExceptionThrower.java | 2 +- .../bcel/internal/generic/FieldGen.java | 42 +- .../internal/generic/FieldGenOrMethodGen.java | 12 +- .../bcel/internal/generic/FieldOrMethod.java | 4 +- .../apache/bcel/internal/generic/ICONST.java | 1 - .../bcel/internal/generic/INVOKEDYNAMIC.java | 8 +- .../bcel/internal/generic/Instruction.java | 6 +- .../internal/generic/InstructionConst.java | 2 +- .../internal/generic/InstructionFactory.java | 28 +- .../internal/generic/InstructionHandle.java | 15 +- .../internal/generic/InstructionList.java | 69 +- .../internal/generic/InstructionTargeter.java | 9 +- .../apache/bcel/internal/generic/LCMP.java | 1 - .../org/apache/bcel/internal/generic/LDC.java | 9 +- .../bcel/internal/generic/LineNumberGen.java | 4 +- .../internal/generic/LocalVariableGen.java | 2 +- .../generic/LocalVariableInstruction.java | 6 +- .../bcel/internal/generic/MethodGen.java | 53 +- .../bcel/internal/generic/ObjectType.java | 6 +- .../org/apache/bcel/internal/generic/RET.java | 2 +- .../bcel/internal/generic/ReferenceType.java | 62 +- .../apache/bcel/internal/generic/SWITCH.java | 2 +- .../apache/bcel/internal/generic/Select.java | 14 +- .../generic/SimpleElementValueGen.java | 6 +- .../internal/generic/TargetLostException.java | 28 +- .../apache/bcel/internal/generic/Type.java | 70 +- .../internal/generic/TypedInstruction.java | 2 +- .../bcel/internal/generic/package-info.java | 26 + .../apache/bcel/internal/package-info.java | 26 + .../bcel/internal/util/BCELComparator.java | 23 +- .../bcel/internal/util/BCELFactory.java | 8 +- .../apache/bcel/internal/util/BCELifier.java | 91 +- .../apache/bcel/internal/util/Class2HTML.java | 10 +- .../apache/bcel/internal/util/ClassSet.java | 4 +- .../apache/bcel/internal/util/CodeHTML.java | 2 +- .../bcel/internal/util/InstructionFinder.java | 7 +- .../apache/bcel/internal/util/Repository.java | 6 +- .../bcel/internal/util/package-info.java | 32 + .../share/classes/jdk/xml/internal/Utils.java | 60 +- src/java.xml/share/legal/bcel.md | 2 +- 122 files changed, 2461 insertions(+), 1128 deletions(-) create mode 100644 src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InvalidMethodSignatureException.java create mode 100644 src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Record.java create mode 100644 src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RecordComponentInfo.java create mode 100644 src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/package-info.java create mode 100644 src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/package-info.java create mode 100644 src/java.xml/share/classes/com/sun/org/apache/bcel/internal/package-info.java create mode 100644 src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/package-info.java diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Const.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Const.java index bca72ab3f95..c6b97db142e 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Const.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Const.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -26,12 +26,12 @@ import java.util.Collections; * Constants for the project, mostly defined in the JVM specification. * * @since 6.0 (intended to replace the Constants interface) - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class Const { /** - * Java class file format Magic number (0xCAFEBABE) + * Java class file format Magic number: {@value}. * * @see The ClassFile Structure * in The Java Virtual Machine Specification @@ -39,201 +39,201 @@ public final class Const { public static final int JVM_CLASSFILE_MAGIC = 0xCAFEBABE; /** - * Major version number of class files for Java 1.1. + * Major version number of class files for Java 1.1: {@value}. * * @see #MINOR_1_1 */ public static final short MAJOR_1_1 = 45; /** - * Minor version number of class files for Java 1.1. + * Minor version number of class files for Java 1.1: {@value}. * * @see #MAJOR_1_1 */ public static final short MINOR_1_1 = 3; /** - * Major version number of class files for Java 1.2. + * Major version number of class files for Java 1.2: {@value}. * * @see #MINOR_1_2 */ public static final short MAJOR_1_2 = 46; /** - * Minor version number of class files for Java 1.2. + * Minor version number of class files for Java 1.2: {@value}. * * @see #MAJOR_1_2 */ public static final short MINOR_1_2 = 0; /** - * Major version number of class files for Java 1.2. + * Major version number of class files for Java 1.2: {@value}. * * @see #MINOR_1_2 */ public static final short MAJOR_1_3 = 47; /** - * Minor version number of class files for Java 1.3. + * Minor version number of class files for Java 1.3: {@value}. * * @see #MAJOR_1_3 */ public static final short MINOR_1_3 = 0; /** - * Major version number of class files for Java 1.3. + * Major version number of class files for Java 1.3: {@value}. * * @see #MINOR_1_3 */ public static final short MAJOR_1_4 = 48; /** - * Minor version number of class files for Java 1.4. + * Minor version number of class files for Java 1.4: {@value}. * * @see #MAJOR_1_4 */ public static final short MINOR_1_4 = 0; /** - * Major version number of class files for Java 1.4. + * Major version number of class files for Java 1.4: {@value}. * * @see #MINOR_1_4 */ public static final short MAJOR_1_5 = 49; /** - * Minor version number of class files for Java 1.5. + * Minor version number of class files for Java 1.5: {@value}. * * @see #MAJOR_1_5 */ public static final short MINOR_1_5 = 0; /** - * Major version number of class files for Java 1.6. + * Major version number of class files for Java 1.6: {@value}. * * @see #MINOR_1_6 */ public static final short MAJOR_1_6 = 50; /** - * Minor version number of class files for Java 1.6. + * Minor version number of class files for Java 1.6: {@value}. * * @see #MAJOR_1_6 */ public static final short MINOR_1_6 = 0; /** - * Major version number of class files for Java 1.7. + * Major version number of class files for Java 1.7: {@value}. * * @see #MINOR_1_7 */ public static final short MAJOR_1_7 = 51; /** - * Minor version number of class files for Java 1.7. + * Minor version number of class files for Java 1.7: {@value}. * * @see #MAJOR_1_7 */ public static final short MINOR_1_7 = 0; /** - * Major version number of class files for Java 1.8. + * Major version number of class files for Java 1.8: {@value}. * * @see #MINOR_1_8 */ public static final short MAJOR_1_8 = 52; /** - * Minor version number of class files for Java 1.8. + * Minor version number of class files for Java 1.8: {@value}. * * @see #MAJOR_1_8 */ public static final short MINOR_1_8 = 0; /** - * Major version number of class files for Java 9. + * Major version number of class files for Java 9: {@value}. * * @see #MINOR_9 */ public static final short MAJOR_9 = 53; /** - * Minor version number of class files for Java 9. + * Minor version number of class files for Java 9: {@value}. * * @see #MAJOR_9 */ public static final short MINOR_9 = 0; /** - * @deprecated Use {@link #MAJOR_9} instead + * @deprecated Use {@link #MAJOR_9} ({@value}) instead. */ @Deprecated public static final short MAJOR_1_9 = MAJOR_9; /** - * @deprecated Use {@link #MINOR_9} instead + * @deprecated Use {@link #MINOR_9} ({@value}) instead. */ @Deprecated public static final short MINOR_1_9 = MINOR_9; /** - * Major version number of class files for Java 10. + * Major version number of class files for Java 10: {@value}. * * @see #MINOR_10 */ public static final short MAJOR_10 = 54; /** - * Minor version number of class files for Java 10. + * Minor version number of class files for Java 10: {@value}. * * @see #MAJOR_10 */ public static final short MINOR_10 = 0; /** - * Major version number of class files for Java 11. + * Major version number of class files for Java 11: {@value}. * * @see #MINOR_11 */ public static final short MAJOR_11 = 55; /** - * Minor version number of class files for Java 11. + * Minor version number of class files for Java 11: {@value}. * * @see #MAJOR_11 */ public static final short MINOR_11 = 0; /** - * Major version number of class files for Java 12. + * Major version number of class files for Java 12: {@value}. * * @see #MINOR_12 */ public static final short MAJOR_12 = 56; /** - * Minor version number of class files for Java 12. + * Minor version number of class files for Java 12: {@value}. * * @see #MAJOR_12 */ public static final short MINOR_12 = 0; /** - * Major version number of class files for Java 13. + * Major version number of class files for Java 13: {@value}. * * @see #MINOR_13 */ public static final short MAJOR_13 = 57; /** - * Minor version number of class files for Java 13. + * Minor version number of class files for Java 13: {@value}. * * @see #MAJOR_13 */ public static final short MINOR_13 = 0; /** - * Minor version number of class files for Java 14. + * Minor version number of class files for Java 14: {@value}. * * @see #MAJOR_14 * @since 6.4.0 @@ -241,7 +241,7 @@ public final class Const { public static final short MINOR_14 = 0; /** - * Minor version number of class files for Java 15. + * Minor version number of class files for Java 15: {@value}. * * @see #MAJOR_15 * @since 6.6.0 @@ -249,7 +249,7 @@ public final class Const { public static final short MINOR_15 = 0; /** - * Minor version number of class files for Java 16. + * Minor version number of class files for Java 16: {@value}. * * @see #MAJOR_16 * @since 6.6.0 @@ -257,7 +257,7 @@ public final class Const { public static final short MINOR_16 = 0; /** - * Minor version number of class files for Java 17. + * Minor version number of class files for Java 17: {@value}. * * @see #MAJOR_17 * @since 6.6.0 @@ -265,7 +265,7 @@ public final class Const { public static final short MINOR_17 = 0; /** - * Minor version number of class files for Java 18. + * Minor version number of class files for Java 18: {@value}. * * @see #MAJOR_18 * @since 6.6.0 @@ -273,7 +273,7 @@ public final class Const { public static final short MINOR_18 = 0; /** - * Minor version number of class files for Java 19. + * Minor version number of class files for Java 19: {@value}. * * @see #MAJOR_19 * @since 6.6.0 @@ -281,7 +281,47 @@ public final class Const { public static final short MINOR_19 = 0; /** - * Major version number of class files for Java 14. + * Minor version number of class files for Java 20: {@value}. + * + * @see #MAJOR_20 + * @since 6.8.0 + */ + public static final short MINOR_20 = 0; + + /** + * Minor version number of class files for Java 21: {@value}. + * + * @see #MAJOR_21 + * @since 6.8.0 + */ + public static final short MINOR_21 = 0; + + /** + * Minor version number of class files for Java 22: {@value}. + * + * @see #MAJOR_22 + * @since 6.10.0 + */ + public static final short MINOR_22 = 0; + + /** + * Minor version number of class files for Java 23: {@value}. + * + * @see #MAJOR_23 + * @since 6.10.0 + */ + public static final short MINOR_23 = 0; + + /** + * Minor version number of class files for Java 24: {@value}. + * + * @see #MAJOR_24 + * @since 6.10.0 + */ + public static final short MINOR_24 = 0; + + /** + * Major version number of class files for Java 14: {@value}. * * @see #MINOR_14 * @since 6.4.0 @@ -289,7 +329,7 @@ public final class Const { public static final short MAJOR_14 = 58; /** - * Major version number of class files for Java 15. + * Major version number of class files for Java 15: {@value}. * * @see #MINOR_15 * @since 6.6.0 @@ -297,7 +337,7 @@ public final class Const { public static final short MAJOR_15 = 59; /** - * Major version number of class files for Java 16. + * Major version number of class files for Java 16: {@value}. * * @see #MINOR_16 * @since 6.6.0 @@ -305,7 +345,7 @@ public final class Const { public static final short MAJOR_16 = 60; /** - * Major version number of class files for Java 17. + * Major version number of class files for Java 17: {@value}. * * @see #MINOR_17 * @since 6.6.0 @@ -313,7 +353,7 @@ public final class Const { public static final short MAJOR_17 = 61; /** - * Major version number of class files for Java 18. + * Major version number of class files for Java 18: {@value}. * * @see #MINOR_18 * @since 6.6.0 @@ -321,7 +361,7 @@ public final class Const { public static final short MAJOR_18 = 62; /** - * Major version number of class files for Java 19. + * Major version number of class files for Java 19: {@value}. * * @see #MINOR_19 * @since 6.6.0 @@ -329,31 +369,71 @@ public final class Const { public static final short MAJOR_19 = 63; /** - * Default major version number. Class file is for Java 1.1. + * Major version number of class files for Java 20: {@value}. + * + * @see #MINOR_20 + * @since 6.8.0 + */ + public static final short MAJOR_20 = 64; + + /** + * Major version number of class files for Java 21: {@value}. + * + * @see #MINOR_21 + * @since 6.8.0 + */ + public static final short MAJOR_21 = 65; + + /** + * Major version number of class files for Java 22: {@value}. + * + * @see #MINOR_22 + * @since 6.10.0 + */ + public static final short MAJOR_22 = 66; + + /** + * Major version number of class files for Java 23: {@value}. + * + * @see #MINOR_23 + * @since 6.10.0 + */ + public static final short MAJOR_23 = 67; + + /** + * Major version number of class files for Java 24: {@value}. + * + * @see #MINOR_24 + * @since 6.10.0 + */ + public static final short MAJOR_24 = 68; + + /** + * Default major version number. Class file is for Java 1.1: {@value}. * * @see #MAJOR_1_1 */ public static final short MAJOR = MAJOR_1_1; /** - * Default major version number. Class file is for Java 1.1. + * Default major version number. Class file is for Java 1.1: {@value}. * * @see #MAJOR_1_1 */ public static final short MINOR = MINOR_1_1; /** - * Maximum value for an unsigned short. + * Maximum value for an unsigned short: {@value}. */ public static final int MAX_SHORT = 65535; // 2^16 - 1 /** - * Maximum value for an unsigned byte. + * Maximum value for an unsigned byte: {@value}. */ public static final int MAX_BYTE = 255; // 2^8 - 1 /** - * One of the access flags for fields, methods, or classes. + * One of the access flags for fields, methods, or classes: {@value}. * * @see Flag definitions for * Classes in the Java Virtual Machine Specification (Java SE 9 Edition). @@ -367,140 +447,140 @@ public final class Const { public static final short ACC_PUBLIC = 0x0001; /** - * One of the access flags for fields, methods, or classes. + * One of the access flags for fields, methods, or classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_PRIVATE = 0x0002; /** - * One of the access flags for fields, methods, or classes. + * One of the access flags for fields, methods, or classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_PROTECTED = 0x0004; /** - * One of the access flags for fields, methods, or classes. + * One of the access flags for fields, methods, or classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_STATIC = 0x0008; /** - * One of the access flags for fields, methods, or classes. + * One of the access flags for fields, methods, or classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_FINAL = 0x0010; /** - * One of the access flags for the Module attribute. + * One of the access flags for the Module attribute: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_OPEN = 0x0020; /** - * One of the access flags for classes. + * One of the access flags for classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_SUPER = 0x0020; /** - * One of the access flags for methods. + * One of the access flags for methods: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_SYNCHRONIZED = 0x0020; /** - * One of the access flags for the Module attribute. + * One of the access flags for the Module attribute: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_TRANSITIVE = 0x0020; /** - * One of the access flags for methods. + * One of the access flags for methods: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_BRIDGE = 0x0040; /** - * One of the access flags for the Module attribute. + * One of the access flags for the Module attribute: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_STATIC_PHASE = 0x0040; /** - * One of the access flags for fields. + * One of the access flags for fields: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_VOLATILE = 0x0040; /** - * One of the access flags for fields. + * One of the access flags for fields: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_TRANSIENT = 0x0080; /** - * One of the access flags for methods. + * One of the access flags for methods: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_VARARGS = 0x0080; /** - * One of the access flags for methods. + * One of the access flags for methods: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_NATIVE = 0x0100; /** - * One of the access flags for classes. + * One of the access flags for classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_INTERFACE = 0x0200; /** - * One of the access flags for methods or classes. + * One of the access flags for methods or classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_ABSTRACT = 0x0400; /** - * One of the access flags for methods. + * One of the access flags for methods: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_STRICT = 0x0800; /** - * One of the access flags for fields, methods, classes, MethodParameter attribute, or Module attribute. + * One of the access flags for fields, methods, classes, MethodParameter attribute, or Module attribute: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_SYNTHETIC = 0x1000; /** - * One of the access flags for classes. + * One of the access flags for classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_ANNOTATION = 0x2000; /** - * One of the access flags for fields or classes. + * One of the access flags for fields or classes: {@value}. * * @see #ACC_PUBLIC */ @@ -508,21 +588,21 @@ public final class Const { // Applies to classes compiled by new compilers only /** - * One of the access flags for MethodParameter or Module attributes. + * One of the access flags for MethodParameter or Module attributes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_MANDATED = (short) 0x8000; /** - * One of the access flags for classes. + * One of the access flags for classes: {@value}. * * @see #ACC_PUBLIC */ public static final short ACC_MODULE = (short) 0x8000; /** - * One of the access flags for fields, methods, or classes. + * One of the access flags for fields, methods, or classes: {@value}. * * @see #ACC_PUBLIC * @deprecated Use {@link #MAX_ACC_FLAG_I} @@ -531,7 +611,7 @@ public final class Const { public static final short MAX_ACC_FLAG = ACC_ENUM; /** - * One of the access flags for fields, methods, or classes. ACC_MODULE is negative as a short. + * One of the access flags for fields, methods, or classes. ACC_MODULE is negative as a short: {@value}. * * @see #ACC_PUBLIC * @since 6.4.0 @@ -553,7 +633,7 @@ public final class Const { public static final int ACCESS_NAMES_LENGTH = ACCESS_NAMES.length; /** - * Marks a constant pool entry as type UTF-8. + * Marks a constant pool entry as type UTF-8: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -566,7 +646,7 @@ public final class Const { */ /** - * Marks a constant pool entry as type Integer. + * Marks a constant pool entry as type Integer: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -574,7 +654,7 @@ public final class Const { public static final byte CONSTANT_Integer = 3; /** - * Marks a constant pool entry as type Float. + * Marks a constant pool entry as type Float: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -582,7 +662,7 @@ public final class Const { public static final byte CONSTANT_Float = 4; /** - * Marks a constant pool entry as type Long. + * Marks a constant pool entry as type Long: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -590,7 +670,7 @@ public final class Const { public static final byte CONSTANT_Long = 5; /** - * Marks a constant pool entry as type Double. + * Marks a constant pool entry as type Double: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -598,7 +678,7 @@ public final class Const { public static final byte CONSTANT_Double = 6; /** - * Marks a constant pool entry as a Class + * Marks a constant pool entry as a Class: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -606,7 +686,7 @@ public final class Const { public static final byte CONSTANT_Class = 7; /** - * Marks a constant pool entry as a Field Reference. + * Marks a constant pool entry as a Field Reference: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -614,7 +694,7 @@ public final class Const { public static final byte CONSTANT_Fieldref = 9; /** - * Marks a constant pool entry as type String + * Marks a constant pool entry as type String: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -622,7 +702,7 @@ public final class Const { public static final byte CONSTANT_String = 8; /** - * Marks a constant pool entry as a Method Reference. + * Marks a constant pool entry as a Method Reference: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -630,7 +710,7 @@ public final class Const { public static final byte CONSTANT_Methodref = 10; /** - * Marks a constant pool entry as an Interface Method Reference. + * Marks a constant pool entry as an Interface Method Reference: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -638,7 +718,7 @@ public final class Const { public static final byte CONSTANT_InterfaceMethodref = 11; /** - * Marks a constant pool entry as a name and type. + * Marks a constant pool entry as a name and type: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -646,7 +726,7 @@ public final class Const { public static final byte CONSTANT_NameAndType = 12; /** - * Marks a constant pool entry as a Method Handle. + * Marks a constant pool entry as a Method Handle: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -654,7 +734,7 @@ public final class Const { public static final byte CONSTANT_MethodHandle = 15; /** - * Marks a constant pool entry as a Method Type. + * Marks a constant pool entry as a Method Type: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -662,16 +742,16 @@ public final class Const { public static final byte CONSTANT_MethodType = 16; /** - * Marks a constant pool entry as dynamically computed. + * Marks a constant pool entry as dynamically computed: {@value}. * - * @see Change request for JEP - * 309 + * @see The Constant Pool in The + * Java Virtual Machine Specification * @since 6.3 */ public static final byte CONSTANT_Dynamic = 17; /** - * Marks a constant pool entry as an Invoke Dynamic + * Marks a constant pool entry as an Invoke Dynamic: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -679,7 +759,7 @@ public final class Const { public static final byte CONSTANT_InvokeDynamic = 18; /** - * Marks a constant pool entry as a Module Reference. + * Marks a constant pool entry as a Module Reference: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -688,7 +768,7 @@ public final class Const { public static final byte CONSTANT_Module = 19; /** - * Marks a constant pool entry as a Package Reference. + * Marks a constant pool entry as a Package Reference: {@value}. * * @see The Constant Pool in The * Java Virtual Machine Specification @@ -705,23 +785,23 @@ public final class Const { /** * The name of the static initializer, also called "class initialization method" or "interface - * initialization method". This is "<clinit>". + * initialization method". This is {@value}. */ public static final String STATIC_INITIALIZER_NAME = ""; /** * The name of every constructor method in a class, also called "instance initialization method". This is - * "<init>". + * {@value}. */ public static final String CONSTRUCTOR_NAME = ""; /** - * The names of the interfaces implemented by arrays + * The names of the interfaces implemented by arrays. */ private static final String[] INTERFACES_IMPLEMENTED_BY_ARRAYS = {"java.lang.Cloneable", "java.io.Serializable"}; /** - * Maximum Constant Pool entries. One of the limitations of the Java Virtual Machine. + * Maximum Constant Pool entries: {@value}. One of the limitations of the Java Virtual Machine. * * @see The Java Virtual * Machine Specification, Java SE 8 Edition, page 330, chapter 4.11. @@ -729,21 +809,25 @@ public final class Const { public static final int MAX_CP_ENTRIES = 65535; /** - * Maximum code size (plus one; the code size must be LESS than this) One of the limitations of the Java Virtual - * Machine. Note vmspec2 page 152 ("Limitations") says: "The amount of code per non-native, non-abstract method is - * limited to 65536 bytes by the sizes of the indices in the exception_table of the Code attribute (4.7.3), in the - * LineNumberTable attribute (4.7.8), and in the LocalVariableTable attribute (4.7.9)." However this should be taken - * as an upper limit rather than the defined maximum. On page 134 (4.8.1 Static Constants) of the same spec, it says: - * "The value of the code_length item must be less than 65536." The entry in the Limitations section has been removed - * from later versions of the spec; it is not present in the Java SE 8 edition. + * Maximum code size (plus one; the code size must be LESS than this): {@value}. + *

        + * One of the limitations of the Java Virtual Machine. Note vmspec2 page 152 ("Limitations") says: + *

        + *
        "The amount of code per non-native, non-abstract method is limited to 65536 bytes by the sizes of the indices in the exception_table of the Code
        +     * attribute (4.7.3), in the LineNumberTable attribute (4.7.8), and in the LocalVariableTable attribute (4.7.9)." However this should be taken as an
        +     * upper limit rather than the defined maximum. On page 134 (4.8.1 Static Constants) of the same spec, it says: "The value of the code_length item must be
        +     * less than 65536."
        + *

        + * The entry in the Limitations section has been removed from later versions of the specification; it is not present in the Java SE 8 edition. + *

        * - * @see The Java Virtual - * Machine Specification, Java SE 8 Edition, page 104, chapter 4.7. + * @see The Java Virtual Machine Specification, Java SE 8 + * Edition, page 104, chapter 4.7. */ public static final int MAX_CODE_SIZE = 65536; // bytes /** - * The maximum number of dimensions in an array ({@value}). One of the limitations of the Java Virtual Machine. + * The maximum number of dimensions in an array: {@value}. One of the limitations of the Java Virtual Machine. * * @see Field Descriptors in * The Java Virtual Machine Specification @@ -751,7 +835,7 @@ public final class Const { public static final int MAX_ARRAY_DIMENSIONS = 255; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -759,7 +843,7 @@ public final class Const { public static final short NOP = 0; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -767,7 +851,7 @@ public final class Const { public static final short ACONST_NULL = 1; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -775,7 +859,7 @@ public final class Const { public static final short ICONST_M1 = 2; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -783,7 +867,7 @@ public final class Const { public static final short ICONST_0 = 3; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -791,7 +875,7 @@ public final class Const { public static final short ICONST_1 = 4; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -799,7 +883,7 @@ public final class Const { public static final short ICONST_2 = 5; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -807,7 +891,7 @@ public final class Const { public static final short ICONST_3 = 6; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -815,7 +899,7 @@ public final class Const { public static final short ICONST_4 = 7; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -823,7 +907,7 @@ public final class Const { public static final short ICONST_5 = 8; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -831,7 +915,7 @@ public final class Const { public static final short LCONST_0 = 9; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -839,7 +923,7 @@ public final class Const { public static final short LCONST_1 = 10; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -847,7 +931,7 @@ public final class Const { public static final short FCONST_0 = 11; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -855,7 +939,7 @@ public final class Const { public static final short FCONST_1 = 12; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -863,7 +947,7 @@ public final class Const { public static final short FCONST_2 = 13; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -871,7 +955,7 @@ public final class Const { public static final short DCONST_0 = 14; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -879,7 +963,7 @@ public final class Const { public static final short DCONST_1 = 15; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -887,7 +971,7 @@ public final class Const { public static final short BIPUSH = 16; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -895,7 +979,7 @@ public final class Const { public static final short SIPUSH = 17; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -903,7 +987,7 @@ public final class Const { public static final short LDC = 18; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -911,7 +995,7 @@ public final class Const { public static final short LDC_W = 19; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -919,7 +1003,7 @@ public final class Const { public static final short LDC2_W = 20; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -927,7 +1011,7 @@ public final class Const { public static final short ILOAD = 21; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -935,7 +1019,7 @@ public final class Const { public static final short LLOAD = 22; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -943,7 +1027,7 @@ public final class Const { public static final short FLOAD = 23; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -951,7 +1035,7 @@ public final class Const { public static final short DLOAD = 24; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -959,7 +1043,7 @@ public final class Const { public static final short ALOAD = 25; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -967,7 +1051,7 @@ public final class Const { public static final short ILOAD_0 = 26; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -975,7 +1059,7 @@ public final class Const { public static final short ILOAD_1 = 27; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -983,7 +1067,7 @@ public final class Const { public static final short ILOAD_2 = 28; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -991,7 +1075,7 @@ public final class Const { public static final short ILOAD_3 = 29; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -999,7 +1083,7 @@ public final class Const { public static final short LLOAD_0 = 30; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1007,7 +1091,7 @@ public final class Const { public static final short LLOAD_1 = 31; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1015,7 +1099,7 @@ public final class Const { public static final short LLOAD_2 = 32; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1023,7 +1107,7 @@ public final class Const { public static final short LLOAD_3 = 33; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1031,7 +1115,7 @@ public final class Const { public static final short FLOAD_0 = 34; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1039,7 +1123,7 @@ public final class Const { public static final short FLOAD_1 = 35; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1047,7 +1131,7 @@ public final class Const { public static final short FLOAD_2 = 36; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1055,7 +1139,7 @@ public final class Const { public static final short FLOAD_3 = 37; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1063,7 +1147,7 @@ public final class Const { public static final short DLOAD_0 = 38; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1071,7 +1155,7 @@ public final class Const { public static final short DLOAD_1 = 39; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1079,7 +1163,7 @@ public final class Const { public static final short DLOAD_2 = 40; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1087,7 +1171,7 @@ public final class Const { public static final short DLOAD_3 = 41; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1095,7 +1179,7 @@ public final class Const { public static final short ALOAD_0 = 42; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1103,7 +1187,7 @@ public final class Const { public static final short ALOAD_1 = 43; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1111,7 +1195,7 @@ public final class Const { public static final short ALOAD_2 = 44; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1119,7 +1203,7 @@ public final class Const { public static final short ALOAD_3 = 45; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1127,7 +1211,7 @@ public final class Const { public static final short IALOAD = 46; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1135,7 +1219,7 @@ public final class Const { public static final short LALOAD = 47; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1143,7 +1227,7 @@ public final class Const { public static final short FALOAD = 48; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1151,7 +1235,7 @@ public final class Const { public static final short DALOAD = 49; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1159,7 +1243,7 @@ public final class Const { public static final short AALOAD = 50; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1167,7 +1251,7 @@ public final class Const { public static final short BALOAD = 51; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1175,7 +1259,7 @@ public final class Const { public static final short CALOAD = 52; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1183,7 +1267,7 @@ public final class Const { public static final short SALOAD = 53; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1191,7 +1275,7 @@ public final class Const { public static final short ISTORE = 54; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1199,7 +1283,7 @@ public final class Const { public static final short LSTORE = 55; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1207,7 +1291,7 @@ public final class Const { public static final short FSTORE = 56; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1215,7 +1299,7 @@ public final class Const { public static final short DSTORE = 57; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1223,7 +1307,7 @@ public final class Const { public static final short ASTORE = 58; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1231,7 +1315,7 @@ public final class Const { public static final short ISTORE_0 = 59; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1239,7 +1323,7 @@ public final class Const { public static final short ISTORE_1 = 60; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1247,7 +1331,7 @@ public final class Const { public static final short ISTORE_2 = 61; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1255,7 +1339,7 @@ public final class Const { public static final short ISTORE_3 = 62; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1263,7 +1347,7 @@ public final class Const { public static final short LSTORE_0 = 63; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1271,7 +1355,7 @@ public final class Const { public static final short LSTORE_1 = 64; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1279,7 +1363,7 @@ public final class Const { public static final short LSTORE_2 = 65; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1287,7 +1371,7 @@ public final class Const { public static final short LSTORE_3 = 66; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1295,7 +1379,7 @@ public final class Const { public static final short FSTORE_0 = 67; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1303,7 +1387,7 @@ public final class Const { public static final short FSTORE_1 = 68; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1311,7 +1395,7 @@ public final class Const { public static final short FSTORE_2 = 69; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1319,7 +1403,7 @@ public final class Const { public static final short FSTORE_3 = 70; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1327,7 +1411,7 @@ public final class Const { public static final short DSTORE_0 = 71; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1335,7 +1419,7 @@ public final class Const { public static final short DSTORE_1 = 72; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1343,7 +1427,7 @@ public final class Const { public static final short DSTORE_2 = 73; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1351,7 +1435,7 @@ public final class Const { public static final short DSTORE_3 = 74; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1359,7 +1443,7 @@ public final class Const { public static final short ASTORE_0 = 75; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1367,7 +1451,7 @@ public final class Const { public static final short ASTORE_1 = 76; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1375,7 +1459,7 @@ public final class Const { public static final short ASTORE_2 = 77; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -1383,7 +1467,7 @@ public final class Const { public static final short ASTORE_3 = 78; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1391,7 +1475,7 @@ public final class Const { public static final short IASTORE = 79; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1399,7 +1483,7 @@ public final class Const { public static final short LASTORE = 80; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1407,7 +1491,7 @@ public final class Const { public static final short FASTORE = 81; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1415,7 +1499,7 @@ public final class Const { public static final short DASTORE = 82; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1423,7 +1507,7 @@ public final class Const { public static final short AASTORE = 83; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1431,7 +1515,7 @@ public final class Const { public static final short BASTORE = 84; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1439,7 +1523,7 @@ public final class Const { public static final short CASTORE = 85; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1447,7 +1531,7 @@ public final class Const { public static final short SASTORE = 86; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1455,7 +1539,7 @@ public final class Const { public static final short POP = 87; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1463,7 +1547,7 @@ public final class Const { public static final short POP2 = 88; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1471,7 +1555,7 @@ public final class Const { public static final short DUP = 89; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1479,7 +1563,7 @@ public final class Const { public static final short DUP_X1 = 90; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1487,7 +1571,7 @@ public final class Const { public static final short DUP_X2 = 91; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1495,7 +1579,7 @@ public final class Const { public static final short DUP2 = 92; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1503,7 +1587,7 @@ public final class Const { public static final short DUP2_X1 = 93; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1511,7 +1595,7 @@ public final class Const { public static final short DUP2_X2 = 94; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1519,7 +1603,7 @@ public final class Const { public static final short SWAP = 95; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1527,7 +1611,7 @@ public final class Const { public static final short IADD = 96; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1535,7 +1619,7 @@ public final class Const { public static final short LADD = 97; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1543,7 +1627,7 @@ public final class Const { public static final short FADD = 98; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1551,7 +1635,7 @@ public final class Const { public static final short DADD = 99; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1559,7 +1643,7 @@ public final class Const { public static final short ISUB = 100; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1567,7 +1651,7 @@ public final class Const { public static final short LSUB = 101; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1575,7 +1659,7 @@ public final class Const { public static final short FSUB = 102; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1583,7 +1667,7 @@ public final class Const { public static final short DSUB = 103; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1591,7 +1675,7 @@ public final class Const { public static final short IMUL = 104; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1599,7 +1683,7 @@ public final class Const { public static final short LMUL = 105; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1607,7 +1691,7 @@ public final class Const { public static final short FMUL = 106; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1615,7 +1699,7 @@ public final class Const { public static final short DMUL = 107; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1623,7 +1707,7 @@ public final class Const { public static final short IDIV = 108; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1631,7 +1715,7 @@ public final class Const { public static final short LDIV = 109; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1639,7 +1723,7 @@ public final class Const { public static final short FDIV = 110; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1647,7 +1731,7 @@ public final class Const { public static final short DDIV = 111; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1655,7 +1739,7 @@ public final class Const { public static final short IREM = 112; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1663,7 +1747,7 @@ public final class Const { public static final short LREM = 113; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1671,7 +1755,7 @@ public final class Const { public static final short FREM = 114; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1679,7 +1763,7 @@ public final class Const { public static final short DREM = 115; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1687,7 +1771,7 @@ public final class Const { public static final short INEG = 116; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1695,7 +1779,7 @@ public final class Const { public static final short LNEG = 117; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1703,7 +1787,7 @@ public final class Const { public static final short FNEG = 118; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1711,7 +1795,7 @@ public final class Const { public static final short DNEG = 119; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1719,7 +1803,7 @@ public final class Const { public static final short ISHL = 120; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1727,7 +1811,7 @@ public final class Const { public static final short LSHL = 121; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1735,7 +1819,7 @@ public final class Const { public static final short ISHR = 122; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1743,7 +1827,7 @@ public final class Const { public static final short LSHR = 123; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1751,7 +1835,7 @@ public final class Const { public static final short IUSHR = 124; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1759,7 +1843,7 @@ public final class Const { public static final short LUSHR = 125; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1767,7 +1851,7 @@ public final class Const { public static final short IAND = 126; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1775,7 +1859,7 @@ public final class Const { public static final short LAND = 127; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1783,7 +1867,7 @@ public final class Const { public static final short IOR = 128; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1791,7 +1875,7 @@ public final class Const { public static final short LOR = 129; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1799,7 +1883,7 @@ public final class Const { public static final short IXOR = 130; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1807,7 +1891,7 @@ public final class Const { public static final short LXOR = 131; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1815,7 +1899,7 @@ public final class Const { public static final short IINC = 132; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1823,7 +1907,7 @@ public final class Const { public static final short I2L = 133; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1831,7 +1915,7 @@ public final class Const { public static final short I2F = 134; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1839,7 +1923,7 @@ public final class Const { public static final short I2D = 135; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1847,7 +1931,7 @@ public final class Const { public static final short L2I = 136; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1855,7 +1939,7 @@ public final class Const { public static final short L2F = 137; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1863,7 +1947,7 @@ public final class Const { public static final short L2D = 138; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1871,7 +1955,7 @@ public final class Const { public static final short F2I = 139; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1879,7 +1963,7 @@ public final class Const { public static final short F2L = 140; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1887,7 +1971,7 @@ public final class Const { public static final short F2D = 141; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1895,7 +1979,7 @@ public final class Const { public static final short D2I = 142; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1903,7 +1987,7 @@ public final class Const { public static final short D2L = 143; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1911,7 +1995,7 @@ public final class Const { public static final short D2F = 144; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1919,7 +2003,7 @@ public final class Const { public static final short I2B = 145; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1927,7 +2011,7 @@ public final class Const { public static final short INT2BYTE = 145; // Old notation /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1935,7 +2019,7 @@ public final class Const { public static final short I2C = 146; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1943,7 +2027,7 @@ public final class Const { public static final short INT2CHAR = 146; // Old notation /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1951,7 +2035,7 @@ public final class Const { public static final short I2S = 147; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -1959,7 +2043,7 @@ public final class Const { public static final short INT2SHORT = 147; // Old notation /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1967,7 +2051,7 @@ public final class Const { public static final short LCMP = 148; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1975,7 +2059,7 @@ public final class Const { public static final short FCMPL = 149; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1983,7 +2067,7 @@ public final class Const { public static final short FCMPG = 150; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1991,7 +2075,7 @@ public final class Const { public static final short DCMPL = 151; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -1999,7 +2083,7 @@ public final class Const { public static final short DCMPG = 152; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2007,7 +2091,7 @@ public final class Const { public static final short IFEQ = 153; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2015,7 +2099,7 @@ public final class Const { public static final short IFNE = 154; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2023,7 +2107,7 @@ public final class Const { public static final short IFLT = 155; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2031,7 +2115,7 @@ public final class Const { public static final short IFGE = 156; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2039,7 +2123,7 @@ public final class Const { public static final short IFGT = 157; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2047,7 +2131,7 @@ public final class Const { public static final short IFLE = 158; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2055,7 +2139,7 @@ public final class Const { public static final short IF_ICMPEQ = 159; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2063,7 +2147,7 @@ public final class Const { public static final short IF_ICMPNE = 160; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2071,7 +2155,7 @@ public final class Const { public static final short IF_ICMPLT = 161; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2079,7 +2163,7 @@ public final class Const { public static final short IF_ICMPGE = 162; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2087,7 +2171,7 @@ public final class Const { public static final short IF_ICMPGT = 163; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2095,7 +2179,7 @@ public final class Const { public static final short IF_ICMPLE = 164; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2103,7 +2187,7 @@ public final class Const { public static final short IF_ACMPEQ = 165; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2111,7 +2195,7 @@ public final class Const { public static final short IF_ACMPNE = 166; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2119,7 +2203,7 @@ public final class Const { public static final short GOTO = 167; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -2127,7 +2211,7 @@ public final class Const { public static final short JSR = 168; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -2135,7 +2219,7 @@ public final class Const { public static final short RET = 169; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2143,7 +2227,7 @@ public final class Const { public static final short TABLESWITCH = 170; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2151,7 +2235,7 @@ public final class Const { public static final short LOOKUPSWITCH = 171; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2159,7 +2243,7 @@ public final class Const { public static final short IRETURN = 172; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2167,7 +2251,7 @@ public final class Const { public static final short LRETURN = 173; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2175,7 +2259,7 @@ public final class Const { public static final short FRETURN = 174; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2183,7 +2267,7 @@ public final class Const { public static final short DRETURN = 175; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2191,7 +2275,7 @@ public final class Const { public static final short ARETURN = 176; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2199,7 +2283,7 @@ public final class Const { public static final short RETURN = 177; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2207,7 +2291,7 @@ public final class Const { public static final short GETSTATIC = 178; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2215,7 +2299,7 @@ public final class Const { public static final short PUTSTATIC = 179; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2223,7 +2307,7 @@ public final class Const { public static final short GETFIELD = 180; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2231,7 +2315,7 @@ public final class Const { public static final short PUTFIELD = 181; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2239,7 +2323,7 @@ public final class Const { public static final short INVOKEVIRTUAL = 182; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2247,7 +2331,7 @@ public final class Const { public static final short INVOKESPECIAL = 183; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -2255,7 +2339,7 @@ public final class Const { public static final short INVOKENONVIRTUAL = 183; // Old name in JDK 1.0 /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2263,7 +2347,7 @@ public final class Const { public static final short INVOKESTATIC = 184; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2271,7 +2355,7 @@ public final class Const { public static final short INVOKEINTERFACE = 185; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2279,7 +2363,7 @@ public final class Const { public static final short INVOKEDYNAMIC = 186; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in The * Java Virtual Machine Specification @@ -2287,7 +2371,7 @@ public final class Const { public static final short NEW = 187; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2295,7 +2379,7 @@ public final class Const { public static final short NEWARRAY = 188; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2303,7 +2387,7 @@ public final class Const { public static final short ANEWARRAY = 189; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2311,7 +2395,7 @@ public final class Const { public static final short ARRAYLENGTH = 190; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2319,7 +2403,7 @@ public final class Const { public static final short ATHROW = 191; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2327,7 +2411,7 @@ public final class Const { public static final short CHECKCAST = 192; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2335,7 +2419,7 @@ public final class Const { public static final short INSTANCEOF = 193; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2343,7 +2427,7 @@ public final class Const { public static final short MONITORENTER = 194; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2351,7 +2435,7 @@ public final class Const { public static final short MONITOREXIT = 195; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2359,7 +2443,7 @@ public final class Const { public static final short WIDE = 196; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode * definitions in The Java Virtual Machine Specification @@ -2367,7 +2451,7 @@ public final class Const { public static final short MULTIANEWARRAY = 197; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2375,7 +2459,7 @@ public final class Const { public static final short IFNULL = 198; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions * in The Java Virtual Machine Specification @@ -2383,7 +2467,7 @@ public final class Const { public static final short IFNONNULL = 199; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2391,7 +2475,7 @@ public final class Const { public static final short GOTO_W = 200; /** - * Java VM opcode. + * Java VM opcode {@value}. * * @see Opcode definitions in * The Java Virtual Machine Specification @@ -2399,7 +2483,7 @@ public final class Const { public static final short JSR_W = 201; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see Reserved opcodes in the Java * Virtual Machine Specification @@ -2407,7 +2491,7 @@ public final class Const { public static final short BREAKPOINT = 202; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2418,7 +2502,7 @@ public final class Const { public static final short LDC_QUICK = 203; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2429,7 +2513,7 @@ public final class Const { public static final short LDC_W_QUICK = 204; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2440,7 +2524,7 @@ public final class Const { public static final short LDC2_W_QUICK = 205; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2451,7 +2535,7 @@ public final class Const { public static final short GETFIELD_QUICK = 206; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2462,7 +2546,7 @@ public final class Const { public static final short PUTFIELD_QUICK = 207; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2473,7 +2557,7 @@ public final class Const { public static final short GETFIELD2_QUICK = 208; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2484,7 +2568,7 @@ public final class Const { public static final short PUTFIELD2_QUICK = 209; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2495,7 +2579,7 @@ public final class Const { public static final short GETSTATIC_QUICK = 210; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2506,7 +2590,7 @@ public final class Const { public static final short PUTSTATIC_QUICK = 211; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2517,7 +2601,7 @@ public final class Const { public static final short GETSTATIC2_QUICK = 212; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2528,7 +2612,7 @@ public final class Const { public static final short PUTSTATIC2_QUICK = 213; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2539,7 +2623,7 @@ public final class Const { public static final short INVOKEVIRTUAL_QUICK = 214; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2550,7 +2634,7 @@ public final class Const { public static final short INVOKENONVIRTUAL_QUICK = 215; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2561,7 +2645,7 @@ public final class Const { public static final short INVOKESUPER_QUICK = 216; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2572,7 +2656,7 @@ public final class Const { public static final short INVOKESTATIC_QUICK = 217; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2583,7 +2667,7 @@ public final class Const { public static final short INVOKEINTERFACE_QUICK = 218; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2594,7 +2678,7 @@ public final class Const { public static final short INVOKEVIRTUALOBJECT_QUICK = 219; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2605,7 +2689,7 @@ public final class Const { public static final short NEW_QUICK = 221; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2616,7 +2700,7 @@ public final class Const { public static final short ANEWARRAY_QUICK = 222; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2627,7 +2711,7 @@ public final class Const { public static final short MULTIANEWARRAY_QUICK = 223; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2638,7 +2722,7 @@ public final class Const { public static final short CHECKCAST_QUICK = 224; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2649,7 +2733,7 @@ public final class Const { public static final short INSTANCEOF_QUICK = 225; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2660,7 +2744,7 @@ public final class Const { public static final short INVOKEVIRTUAL_QUICK_W = 226; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2671,7 +2755,7 @@ public final class Const { public static final short GETFIELD_QUICK_W = 227; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see @@ -2682,7 +2766,7 @@ public final class Const { public static final short PUTFIELD_QUICK_W = 228; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see Reserved opcodes in the Java * Virtual Machine Specification @@ -2690,7 +2774,7 @@ public final class Const { public static final short IMPDEP1 = 254; /** - * JVM internal opcode. + * JVM internal opcode {@value}. * * @see Reserved opcodes in the Java * Virtual Machine Specification @@ -2698,34 +2782,44 @@ public final class Const { public static final short IMPDEP2 = 255; /** - * BCEL virtual instruction for pushing an arbitrary data type onto the stack. Will be converted to the appropriate JVM + * BCEL virtual instruction for pushing an arbitrary data type onto the stack: {@value}. Will be converted to the appropriate JVM * opcode when the class is dumped. */ public static final short PUSH = 4711; /** - * BCEL virtual instruction for either LOOKUPSWITCH or TABLESWITCH. Will be converted to the appropriate JVM opcode when + * BCEL virtual instruction for either LOOKUPSWITCH or TABLESWITCH: {@value}. Will be converted to the appropriate JVM opcode when * the class is dumped. */ public static final short SWITCH = 4712; - /** Illegal opcode. */ + /** + * Illegal opcode: {@value}. + */ public static final short UNDEFINED = -1; - /** Illegal opcode. */ + /** + * Illegal opcode: {@value}. + */ public static final short UNPREDICTABLE = -2; - /** Illegal opcode. */ + /** + * Illegal opcode: {@value}. + */ public static final short RESERVED = -3; - /** Mnemonic for an illegal opcode. */ + /** + * Mnemonic for an illegal opcode: {@value}. + */ public static final String ILLEGAL_OPCODE = ""; - /** Mnemonic for an illegal type. */ + /** + * Mnemonic for an illegal type: {@value}. + */ public static final String ILLEGAL_TYPE = ""; /** - * Boolean data type. + * Boolean data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2733,7 +2827,7 @@ public final class Const { public static final byte T_BOOLEAN = 4; /** - * Char data type. + * Char data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2741,7 +2835,7 @@ public final class Const { public static final byte T_CHAR = 5; /** - * Float data type. + * Float data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2749,7 +2843,7 @@ public final class Const { public static final byte T_FLOAT = 6; /** - * Double data type. + * Double data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2757,7 +2851,7 @@ public final class Const { public static final byte T_DOUBLE = 7; /** - * Byte data type. + * Byte data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2765,7 +2859,7 @@ public final class Const { public static final byte T_BYTE = 8; /** - * Short data type. + * Short data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2773,7 +2867,7 @@ public final class Const { public static final byte T_SHORT = 9; /** - * Int data type. + * Int data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2781,7 +2875,7 @@ public final class Const { public static final byte T_INT = 10; /** - * Long data type. + * Long data type: {@value}. * * @see Static Constraints in * the Java Virtual Machine Specification @@ -2827,7 +2921,7 @@ public final class Const { /** * The signature characters corresponding to primitive types, e.g., SHORT_TYPE_NAMES[T_INT] = "I" */ - private static final String[] SHORT_TYPE_NAMES = {ILLEGAL_TYPE, ILLEGAL_TYPE, ILLEGAL_TYPE, ILLEGAL_TYPE, "Z", "C", "F", "D", "B", "S", "I", "J", "V", + public static final String[] SHORT_TYPE_NAMES = {ILLEGAL_TYPE, ILLEGAL_TYPE, ILLEGAL_TYPE, ILLEGAL_TYPE, "Z", "C", "F", "D", "B", "S", "I", "J", "V", ILLEGAL_TYPE, ILLEGAL_TYPE, ILLEGAL_TYPE}; /** @@ -3036,11 +3130,13 @@ public final class Const { public static final byte ATTR_MODULE_MAIN_CLASS = 24; public static final byte ATTR_NEST_HOST = 25; public static final byte ATTR_NEST_MEMBERS = 26; - public static final short KNOWN_ATTRIBUTES = 27; // count of attributes + public static final byte ATTR_RECORD = 27; + + public static final short KNOWN_ATTRIBUTES = 28; // count of attributes private static final String[] ATTRIBUTE_NAMES = {"SourceFile", "ConstantValue", "Code", "Exceptions", "LineNumberTable", "LocalVariableTable", "InnerClasses", "Synthetic", "Deprecated", "PMGClass", "Signature", "StackMap", "RuntimeVisibleAnnotations", "RuntimeInvisibleAnnotations", "RuntimeVisibleParameterAnnotations", "RuntimeInvisibleParameterAnnotations", "AnnotationDefault", "LocalVariableTypeTable", "EnclosingMethod", - "StackMapTable", "BootstrapMethods", "MethodParameters", "Module", "ModulePackages", "ModuleMainClass", "NestHost", "NestMembers"}; + "StackMapTable", "BootstrapMethods", "MethodParameters", "Module", "ModulePackages", "ModuleMainClass", "NestHost", "NestMembers", "Record"}; /** * Constants used in the StackMap attribute. */ @@ -3070,6 +3166,7 @@ public final class Const { public static final int SAME_FRAME_EXTENDED = 251; public static final int APPEND_FRAME = 252; public static final int FULL_FRAME = 255; + /** * Constants that define the maximum value of those constants which store ranges. */ @@ -3090,6 +3187,7 @@ public final class Const { public static final byte REF_invokeSpecial = 7; public static final byte REF_newInvokeSpecial = 8; public static final byte REF_invokeInterface = 9; + /** * The names of the reference_kinds of a CONSTANT_MethodHandle_info. */ @@ -3097,7 +3195,7 @@ public final class Const { "newInvokeSpecial", "invokeInterface"}; /** - * @param index + * @param index index into {@code ACCESS_NAMES}. * @return the ACCESS_NAMES entry at the given index * @since 6.0 */ @@ -3107,7 +3205,7 @@ public final class Const { /** * - * @param index + * @param index index into {@code ACCESS_NAMES}. * @return the attribute name * @since 6.0 */ @@ -3118,7 +3216,7 @@ public final class Const { /** * The primitive class names corresponding to the T_XX constants, e.g., CLASS_TYPE_NAMES[T_INT] = "java.lang.Integer" * - * @param index + * @param index index into {@code CLASS_TYPE_NAMES}. * @return the class name * @since 6.0 */ @@ -3128,7 +3226,7 @@ public final class Const { /** * - * @param index + * @param index index into {@code CONSTANT_NAMES}. * @return the CONSTANT_NAMES entry at the given index * @since 6.0 */ @@ -3140,7 +3238,7 @@ public final class Const { /** * - * @param index + * @param index index into {@code CONSUME_STACK}. * @return Number of words consumed on operand stack * @since 6.0 */ @@ -3157,7 +3255,7 @@ public final class Const { /** * - * @param index + * @param index index into {@code ITEM_NAMES}. * @return the item name * @since 6.0 */ @@ -3167,7 +3265,7 @@ public final class Const { /** * - * @param index + * @param index index into {@code METHODHANDLE_NAMES}. * @return the method handle name * @since 6.0 */ @@ -3177,7 +3275,7 @@ public final class Const { /** * - * @param index + * @param index index into {@code NO_OF_OPERANDS}. * @return Number of byte code operands * @since 6.0 */ diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/ExceptionConst.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/ExceptionConst.java index d45c5794b7b..89cf3f835da 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/ExceptionConst.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/ExceptionConst.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -26,7 +26,7 @@ import jdk.xml.internal.Utils; * Exception constants. * * @since 6.0 (intended to replace the InstructionConstant interface) - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class ExceptionConst { @@ -52,7 +52,6 @@ public final class ExceptionConst { * Super class of any linking exception (aka Linkage Error) */ public static final Class LINKING_EXCEPTION = LinkageError.class; - /** * Linking Exceptions */ @@ -67,10 +66,10 @@ public final class ExceptionConst { public static final Class NO_SUCH_METHOD_ERROR = NoSuchMethodError.class; public static final Class NO_CLASS_DEF_FOUND_ERROR = NoClassDefFoundError.class; public static final Class UNSATISFIED_LINK_ERROR = UnsatisfiedLinkError.class; + public static final Class VERIFY_ERROR = VerifyError.class; /* UnsupportedClassVersionError is new in JDK 1.2 */ // public static final Class UnsupportedClassVersionError = UnsupportedClassVersionError.class; - /** * Run-Time Exceptions */ diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Repository.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Repository.java index d36260cc23a..10f6a1a8ff4 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Repository.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/Repository.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -30,7 +30,7 @@ import com.sun.org.apache.bcel.internal.util.SyntheticRepository; * @see com.sun.org.apache.bcel.internal.util.Repository * @see SyntheticRepository * - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public abstract class Repository { @@ -174,7 +174,7 @@ public abstract class Repository { } /** - * Lookups class somewhere found on your CLASSPATH, or wherever the repository instance looks for it. + * Lookups class somewhere found on your CLASSPATH, or whereever the repository instance looks for it. * * @return class object for given fully qualified class name * @throws ClassNotFoundException if the class could not be found or parsed correctly diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AccessFlags.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AccessFlags.java index 61ec9c4d690..f1d350894c9 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AccessFlags.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AccessFlags.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -25,27 +25,36 @@ import com.sun.org.apache.bcel.internal.Const; * Super class for all objects that have modifiers like private, final, ... I.e. * classes, fields, and methods. * - * @LastModified: Jan 2020 + * @LastModified: Sept 2025 */ public abstract class AccessFlags { /** - * @deprecated (since 6.0) will be made private; do not access directly, use getter/setter + * Access flags. + * + * @deprecated (since 6.0) will be made private; do not access directly, use getter/setter. */ @java.lang.Deprecated protected int access_flags; // TODO not used externally at present + /** + * Constructs a new instance. + */ public AccessFlags() { } /** - * @param a initial access flags + * Constructs a new instance. + * + * @param accessFlags initial access flags. */ - public AccessFlags(final int a) { - access_flags = a; + public AccessFlags(final int accessFlags) { + access_flags = accessFlags; } /** + * Gets access flags. + * * @return Access flags of the object aka. "modifiers". */ public final int getAccessFlags() { @@ -53,142 +62,303 @@ public abstract class AccessFlags { } /** - * @return Access flags of the object aka. "modifiers". + * Gets access flags. + * + * @return Access flags of the object also known as modifiers. */ public final int getModifiers() { return access_flags; } + /** + * Tests whether the abstract bit is on. + * + * @return whether the abstract bit is on. + */ public final boolean isAbstract() { - return (access_flags & Const.ACC_ABSTRACT) != 0; + return test(Const.ACC_ABSTRACT); } + /** + * Sets the abstract bit. + * + * @param flag The new value. + */ public final void isAbstract(final boolean flag) { setFlag(Const.ACC_ABSTRACT, flag); } + /** + * Tests whether the annotation bit is on. + * + * @return whether the annotation bit is on. + */ public final boolean isAnnotation() { - return (access_flags & Const.ACC_ANNOTATION) != 0; + return test(Const.ACC_ANNOTATION); } + /** + * Sets the annotation bit. + * + * @param flag The new value. + */ public final void isAnnotation(final boolean flag) { setFlag(Const.ACC_ANNOTATION, flag); } - + /** + * Tests whether the enum bit is on. + * + * @return whether the enum bit is on. + */ public final boolean isEnum() { - return (access_flags & Const.ACC_ENUM) != 0; + return test(Const.ACC_ENUM); } + /** + * Sets the enum bit. + * + * @param flag The new value. + */ public final void isEnum(final boolean flag) { setFlag(Const.ACC_ENUM, flag); } + /** + * Tests whether the final bit is on. + * + * @return whether the final bit is on. + */ public final boolean isFinal() { - return (access_flags & Const.ACC_FINAL) != 0; + return test(Const.ACC_FINAL); } + /** + * Sets the final bit. + * + * @param flag The new value. + */ public final void isFinal(final boolean flag) { setFlag(Const.ACC_FINAL, flag); } + /** + * Tests whether the interface bit is on. + * + * @return whether the interface bit is on. + */ public final boolean isInterface() { - return (access_flags & Const.ACC_INTERFACE) != 0; + return test(Const.ACC_INTERFACE); } + /** + * Sets the interface bit. + * + * @param flag The new value. + */ public final void isInterface(final boolean flag) { setFlag(Const.ACC_INTERFACE, flag); } + /** + * Tests whether the native bit is on. + * + * @return whether the native bit is on. + */ public final boolean isNative() { - return (access_flags & Const.ACC_NATIVE) != 0; + return test(Const.ACC_NATIVE); } + /** + * Sets the native bit. + * + * @param flag The new value. + */ public final void isNative(final boolean flag) { setFlag(Const.ACC_NATIVE, flag); } + /** + * Tests whether the private bit is on. + * + * @return whether the private bit is on. + */ public final boolean isPrivate() { - return (access_flags & Const.ACC_PRIVATE) != 0; + return test(Const.ACC_PRIVATE); } + /** + * Sets the private bit. + * + * @param flag The new value. + */ public final void isPrivate(final boolean flag) { setFlag(Const.ACC_PRIVATE, flag); } + /** + * Tests whether the protected bit is on. + * + * @return whether the protected bit is on. + */ public final boolean isProtected() { - return (access_flags & Const.ACC_PROTECTED) != 0; + return test(Const.ACC_PROTECTED); } + /** + * Sets the protected bit. + * + * @param flag The new value. + */ public final void isProtected(final boolean flag) { setFlag(Const.ACC_PROTECTED, flag); } + /** + * Tests whether the public bit is on. + * + * @return whether the public bit is on. + */ public final boolean isPublic() { - return (access_flags & Const.ACC_PUBLIC) != 0; + return test(Const.ACC_PUBLIC); } + /** + * Sets the public bit. + * + * @param flag The new value. + */ public final void isPublic(final boolean flag) { setFlag(Const.ACC_PUBLIC, flag); } + /** + * Tests whether the static bit is on. + * + * @return whether the static bit is on. + */ public final boolean isStatic() { - return (access_flags & Const.ACC_STATIC) != 0; + return test(Const.ACC_STATIC); } + /** + * Sets the static bit. + * + * @param flag The new value. + */ public final void isStatic(final boolean flag) { setFlag(Const.ACC_STATIC, flag); } + /** + * Tests whether the strict bit is on. + * + * @return whether the strict bit is on. + */ public final boolean isStrictfp() { - return (access_flags & Const.ACC_STRICT) != 0; + return test(Const.ACC_STRICT); } + /** + * Sets the strict bit. + * + * @param flag The new value. + */ public final void isStrictfp(final boolean flag) { setFlag(Const.ACC_STRICT, flag); } + /** + * Tests whether the synchronized bit is on. + * + * @return whether the synchronized bit is on. + */ public final boolean isSynchronized() { - return (access_flags & Const.ACC_SYNCHRONIZED) != 0; + return test(Const.ACC_SYNCHRONIZED); } + /** + * Sets the synchronized bit. + * + * @param flag The new value. + */ public final void isSynchronized(final boolean flag) { setFlag(Const.ACC_SYNCHRONIZED, flag); } + /** + * Tests whether the synthetic bit is on. + * + * @return whether the synthetic bit is on. + */ public final boolean isSynthetic() { - return (access_flags & Const.ACC_SYNTHETIC) != 0; + return test(Const.ACC_SYNTHETIC); } + /** + * Sets the synthetic bit. + * + * @param flag The new value. + */ public final void isSynthetic(final boolean flag) { setFlag(Const.ACC_SYNTHETIC, flag); } + /** + * Tests whether the transient bit is on. + * + * @return whether the varargs bit is on. + */ public final boolean isTransient() { - return (access_flags & Const.ACC_TRANSIENT) != 0; + return test(Const.ACC_TRANSIENT); } + /** + * Sets the varargs bit. + * + * @param flag The new value. + */ public final void isTransient(final boolean flag) { setFlag(Const.ACC_TRANSIENT, flag); } + /** + * Tests whether the varargs bit is on. + * + * @return whether the varargs bit is on. + */ public final boolean isVarArgs() { - return (access_flags & Const.ACC_VARARGS) != 0; + return test(Const.ACC_VARARGS); } + /** + * Sets the varargs bit. + * + * @param flag The new value. + */ public final void isVarArgs(final boolean flag) { setFlag(Const.ACC_VARARGS, flag); } + /** + * Tests whether the volatile bit is on. + * + * @return whether the volatile bit is on. + */ public final boolean isVolatile() { - return (access_flags & Const.ACC_VOLATILE) != 0; + return test(Const.ACC_VOLATILE); } + /** + * Sets the volatile bit. + * + * @param flag The new value. + */ public final void isVolatile(final boolean flag) { setFlag(Const.ACC_VOLATILE, flag); } /** - * Set access flags aka "modifiers". + * Sets access flags also known as modifiers. * * @param accessFlags Access flags of the object. */ @@ -207,11 +377,21 @@ public abstract class AccessFlags { } /** - * Set access flags aka "modifiers". + * Sets access flags aka "modifiers". * * @param accessFlags Access flags of the object. */ public final void setModifiers(final int accessFlags) { setAccessFlags(accessFlags); } + + /** + * Tests whether the bit is on. + * + * @param test the bit to test. + * @return whether the bit is on. + */ + private boolean test(final short test) { + return (access_flags & test) != 0; + } } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AnnotationEntry.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AnnotationEntry.java index 466e9bf3404..c9503332a6f 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AnnotationEntry.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/AnnotationEntry.java @@ -1,6 +1,5 @@ /* - * reserved comment block - * DO NOT REMOVE OR ALTER! + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -27,20 +26,22 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import jdk.xml.internal.Utils; /** * Represents one annotation in the annotation table * * @since 6.0 + * @LastModified: Sept 2025 */ public class AnnotationEntry implements Node { public static final AnnotationEntry[] EMPTY_ARRAY = {}; - public static AnnotationEntry[] createAnnotationEntries(final Attribute[] attrs) { + public static AnnotationEntry[] createAnnotationEntries(final Attribute[] attributes) { // Find attributes that contain annotation data - return Stream.of(attrs).filter(Annotations.class::isInstance).flatMap(e -> Stream.of(((Annotations) e).getAnnotationEntries())) - .toArray(AnnotationEntry[]::new); + return Utils.streamOfIfNonNull(attributes).filter(Annotations.class::isInstance).flatMap(e -> Stream.of(((Annotations) e).getAnnotationEntries())) + .toArray(AnnotationEntry[]::new); } /** @@ -55,7 +56,6 @@ public class AnnotationEntry implements Node { public static AnnotationEntry read(final DataInput input, final ConstantPool constantPool, final boolean isRuntimeVisible) throws IOException { final AnnotationEntry annotationEntry = new AnnotationEntry(input.readUnsignedShort(), constantPool, isRuntimeVisible); final int numElementValuePairs = input.readUnsignedShort(); - annotationEntry.elementValuePairs = new ArrayList<>(); for (int i = 0; i < numElementValuePairs; i++) { annotationEntry.elementValuePairs .add(new ElementValuePair(input.readUnsignedShort(), ElementValue.readElementValue(input, constantPool), constantPool)); @@ -69,12 +69,13 @@ public class AnnotationEntry implements Node { private final boolean isRuntimeVisible; - private List elementValuePairs; + private final List elementValuePairs; public AnnotationEntry(final int typeIndex, final ConstantPool constantPool, final boolean isRuntimeVisible) { this.typeIndex = typeIndex; this.constantPool = constantPool; this.isRuntimeVisible = isRuntimeVisible; + this.elementValuePairs = new ArrayList<>(); } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Annotations.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Annotations.java index 52ac9d0dd98..6ff9b4a046f 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Annotations.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Annotations.java @@ -52,7 +52,7 @@ public abstract class Annotations extends Attribute implements Iterable The class File Format : * The BootstrapMethods Attribute * @since 6.0 + * @LastModified: Sept 2025 */ public class BootstrapMethod implements Cloneable { + static final BootstrapMethod[] EMPTY_ARRAY = {}; + /** Index of the CONSTANT_MethodHandle_info structure in the constant_pool table */ private int bootstrapMethodRef; @@ -54,7 +57,7 @@ public class BootstrapMethod implements Cloneable { } /** - * Construct object from input stream. + * Constructs object from input stream. * * @param input Input stream * @throws IOException if an I/O error occurs. @@ -78,7 +81,7 @@ public class BootstrapMethod implements Cloneable { */ public BootstrapMethod(final int bootstrapMethodRef, final int[] bootstrapArguments) { this.bootstrapMethodRef = bootstrapMethodRef; - this.bootstrapArguments = bootstrapArguments; + setBootstrapArguments(bootstrapArguments); } /** @@ -87,7 +90,7 @@ public class BootstrapMethod implements Cloneable { public BootstrapMethod copy() { try { return (BootstrapMethod) clone(); - } catch (final CloneNotSupportedException e) { + } catch (final CloneNotSupportedException ignore) { // TODO should this throw? } return null; @@ -132,7 +135,7 @@ public class BootstrapMethod implements Cloneable { * @param bootstrapArguments int[] indices into constant_pool of CONSTANT_[type]_info */ public void setBootstrapArguments(final int[] bootstrapArguments) { - this.bootstrapArguments = bootstrapArguments; + this.bootstrapArguments = Utils.createEmptyArrayIfNull(bootstrapArguments); } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/BootstrapMethods.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/BootstrapMethods.java index 6f9930f1b16..ef2475e1856 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/BootstrapMethods.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/BootstrapMethods.java @@ -58,11 +58,11 @@ public class BootstrapMethods extends Attribute implements Iterable - * Note that the detail message associated with {@code cause} is not automatically incorporated in this runtime exception's detail message. * * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). - * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that - * the cause is nonexistent or unknown.) + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). A {@code null} value is permitted, and indicates that + * the cause is nonexistent or unknown. * @since 6.0 */ public ClassFormatException(final String message, final Throwable cause) { @@ -63,8 +61,8 @@ public class ClassFormatException extends RuntimeException { * Constructs a new instance with the specified cause and a detail message of {@code (cause==null ? null : cause.toString())} (which typically contains the * class and detail message of {@code cause}). This constructor is useful for runtime exceptions that are little more than wrappers for other throwables. * - * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A {@code null} value is permitted, and indicates that the - * cause is nonexistent or unknown.) + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). A {@code null} value is permitted, and indicates that the + * cause is nonexistent or unknown. * @since 6.7.0 */ public ClassFormatException(final Throwable cause) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ClassParser.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ClassParser.java index c9daaeabf9b..0dbda722ec4 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ClassParser.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ClassParser.java @@ -37,7 +37,7 @@ import com.sun.org.apache.bcel.internal.Const; * appropriate exception is propagated back to the caller. * * The structure and the names comply, except for a few conveniences, exactly with the - * JVM specification 1.0. See this paper for further details about + * JVM specification 1.0. See this paper for further details about * the structure of a bytecode file. */ public final class ClassParser { @@ -57,7 +57,7 @@ public final class ClassParser { private Field[] fields; // class fields, i.e., its variables private Method[] methods; // methods defined in the class private Attribute[] attributes; // attributes defined in the class - private final boolean isZip; // Loaded from zip file + private final boolean isZip; // Loaded from ZIP file /** * Parses class from the given stream. @@ -91,7 +91,7 @@ public final class ClassParser { /** * Parses class from given .class file in a ZIP-archive * - * @param zipFile zip file name + * @param zipFile ZIP file name * @param fileName file name */ public ClassParser(final String zipFile, final String fileName) { @@ -104,7 +104,7 @@ public final class ClassParser { /** * Parses the given Java class file and return an object that represents the contained data, i.e., constants, methods, * fields and commands. A ClassFormatException is raised, if the file is not a valid .class file. (This does - * not include verification of the byte code as it is performed by the java interpreter). + * not include verification of the byte code as it is performed by the Java interpreter). * * @return Class object representing the parsed class file * @throws IOException if an I/O error occurs. @@ -151,11 +151,11 @@ public final class ClassParser { // for (int i=0; i < u.length; i++) // System.err.println("WARNING: " + u[i]); // Everything should have been read now - // if(file.available() > 0) { + // if (file.available() > 0) { // int bytes = file.available(); // byte[] buf = new byte[bytes]; // file.read(buf); - // if(!(isZip && (buf.length == 1))) { + // if (!(isZip && (buf.length == 1))) { // System.err.println("WARNING: Trailing garbage at end of " + fileName); // System.err.println(bytes + " extra bytes: " + Utility.toHexString(buf)); // } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Code.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Code.java index 498a8c71b18..7573a5412e7 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Code.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Code.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -27,6 +27,7 @@ import java.util.Arrays; import com.sun.org.apache.bcel.internal.Const; import com.sun.org.apache.bcel.internal.util.Args; +import jdk.xml.internal.Utils; /** * This class represents a chunk of Java byte code contained in a method. It is instantiated by the @@ -59,7 +60,7 @@ import com.sun.org.apache.bcel.internal.util.Args; * @see CodeException * @see LineNumberTable * @see LocalVariableTable - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class Code extends Attribute { @@ -93,7 +94,7 @@ public final class Code extends Attribute { code = new byte[codeLength]; // Read byte code file.readFully(code); /* - * Read exception table that contains all regions where an exception handler is active, i.e., a try { ... } catch() + * Read exception table that contains all regions where an exception handler is active, i.e., a try { ... } catch () * block. */ final int exceptionTableLength = file.readUnsignedShort(); @@ -107,7 +108,7 @@ public final class Code extends Attribute { final int attributesCount = file.readUnsignedShort(); attributes = new Attribute[attributesCount]; for (int i = 0; i < attributesCount; i++) { - attributes[i] = Attribute.readAttribute(file, constantPool); + attributes[i] = readAttribute(file, constantPool); } /* * Adjust length, because of setAttributes in this(), s.b. length is incorrect, because it didn't take the internal @@ -131,8 +132,8 @@ public final class Code extends Attribute { super(Const.ATTR_CODE, nameIndex, length, constantPool); this.maxStack = Args.requireU2(maxStack, "maxStack"); this.maxLocals = Args.requireU2(maxLocals, "maxLocals"); - this.code = code != null ? code : Const.EMPTY_BYTE_ARRAY; - this.exceptionTable = exceptionTable != null ? exceptionTable : CodeException.EMPTY_CODE_EXCEPTION_ARRAY; + this.code = Utils.createEmptyArrayIfNull(code); + this.exceptionTable = Utils.createEmptyArrayIfNull(exceptionTable, CodeException[].class); Args.requireU2(this.exceptionTable.length, "exceptionTable.length"); this.attributes = attributes != null ? attributes : EMPTY_ARRAY; super.setLength(calculateLength()); // Adjust length @@ -263,6 +264,20 @@ public final class Code extends Attribute { return null; } + /** + * Gets the local variable type table attribute {@link LocalVariableTypeTable}. + * @return LocalVariableTypeTable of Code, if it has one, null otherwise. + * @since 6.10.0 + */ + public LocalVariableTypeTable getLocalVariableTypeTable() { + for (final Attribute attribute : attributes) { + if (attribute instanceof LocalVariableTypeTable) { + return (LocalVariableTypeTable) attribute; + } + } + return null; + } + /** * @return Number of local variables. */ @@ -277,6 +292,20 @@ public final class Code extends Attribute { return maxStack; } + /** + * Finds the attribute of {@link StackMap} instance. + * @return StackMap of Code, if it has one, else null. + * @since 6.8.0 + */ + public StackMap getStackMap() { + for (final Attribute attribute : attributes) { + if (attribute instanceof StackMap) { + return (StackMap) attribute; + } + } + return null; + } + /** * @param attributes the attributes to set for this Code */ @@ -289,7 +318,7 @@ public final class Code extends Attribute { * @param code byte code */ public void setCode(final byte[] code) { - this.code = code != null ? code : Const.EMPTY_BYTE_ARRAY; + this.code = Utils.createEmptyArrayIfNull(code); super.setLength(calculateLength()); // Adjust length } @@ -297,7 +326,7 @@ public final class Code extends Attribute { * @param exceptionTable exception table */ public void setExceptionTable(final CodeException[] exceptionTable) { - this.exceptionTable = exceptionTable != null ? exceptionTable : CodeException.EMPTY_CODE_EXCEPTION_ARRAY; + this.exceptionTable = exceptionTable != null ? exceptionTable : CodeException.EMPTY_ARRAY; super.setLength(calculateLength()); // Adjust length } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/CodeException.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/CodeException.java index ee224e6b348..b8bf109239e 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/CodeException.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/CodeException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -52,19 +52,19 @@ import com.sun.org.apache.bcel.internal.util.Args; *
        * * @see Code - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class CodeException implements Cloneable, Node { /** * Empty array. */ - static final CodeException[] EMPTY_CODE_EXCEPTION_ARRAY = {}; + static final CodeException[] EMPTY_ARRAY = {}; /** Range in the code the exception handler. */ private int startPc; - /** active. startPc is inclusive, endPc exclusive. */ + /** Active. startPc is inclusive, endPc exclusive. */ private int endPc; /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Constant.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Constant.java index 885e1b599eb..9b3d6f31e17 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Constant.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Constant.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -32,30 +32,29 @@ import com.sun.org.apache.bcel.internal.util.BCELComparator; * in the constant pool of a class file. The classes keep closely to * the JVM specification. * - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public abstract class Constant implements Cloneable, Node { - private static BCELComparator bcelComparator = new BCELComparator() { + static final Constant[] EMPTY_ARRAY = {}; + + private static BCELComparator bcelComparator = new BCELComparator() { @Override - public boolean equals(final Object o1, final Object o2) { - final Constant THIS = (Constant) o1; - final Constant THAT = (Constant) o2; - return Objects.equals(THIS.toString(), THAT.toString()); + public boolean equals(final Constant a, final Constant b) { + return a == b || a != null && b != null && Objects.equals(a.toString(), b.toString()); } @Override - public int hashCode(final Object o) { - final Constant THIS = (Constant) o; - return THIS.toString().hashCode(); + public int hashCode(final Constant o) { + return o != null ? Objects.hashCode(o.toString()) : 0; } }; /** - * @return Comparison strategy object + * @return Comparison strategy object. */ - public static BCELComparator getComparator() { + public static BCELComparator getComparator() { return bcelComparator; } @@ -113,7 +112,7 @@ public abstract class Constant implements Cloneable, Node { /** * @param comparator Comparison strategy object */ - public static void setComparator(final BCELComparator comparator) { + public static void setComparator(final BCELComparator comparator) { bcelComparator = comparator; } @@ -148,7 +147,7 @@ public abstract class Constant implements Cloneable, Node { try { return super.clone(); } catch (final CloneNotSupportedException e) { - throw new Error("Clone Not Supported"); // never happens + throw new UnsupportedOperationException("Clone Not Supported", e); // never happens } } @@ -174,7 +173,7 @@ public abstract class Constant implements Cloneable, Node { */ @Override public boolean equals(final Object obj) { - return bcelComparator.equals(this, obj); + return obj instanceof Constant && bcelComparator.equals(this, (Constant) obj); } /** @@ -185,7 +184,7 @@ public abstract class Constant implements Cloneable, Node { } /** - * Returns value as defined by given BCELComparator strategy. By default return the hashcode of the result of + * Returns value as defined by given BCELComparator strategy. By default return the hash code of the result of * toString(). * * @see Object#hashCode() diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantCP.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantCP.java index 71fd91b249a..51113a1aeaa 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantCP.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantCP.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -28,11 +28,11 @@ import com.sun.org.apache.bcel.internal.Const; /** * Abstract super class for Fieldref, Methodref, InterfaceMethodref and InvokeDynamic constants. * - * @see ConstantFieldref - * @see ConstantMethodref - * @see ConstantInterfaceMethodref - * @see ConstantInvokeDynamic - * @LastModified: Jun 2019 + * @see ConstantFieldref + * @see ConstantMethodref + * @see ConstantInterfaceMethodref + * @see ConstantInvokeDynamic + * @LastModified: Sept 2025 */ public abstract class ConstantCP extends Constant { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantDouble.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantDouble.java index ebb3d0af46a..14b43937c92 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantDouble.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantDouble.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -29,8 +29,8 @@ import com.sun.org.apache.bcel.internal.Const; /** * This class is derived from the abstract {@link Constant} and represents a reference to a Double object. * - * @see Constant - * @LastModified: Jun 2019 + * @see Constant + * @LastModified: Sept 2025 */ public final class ConstantDouble extends Constant implements ConstantObject { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantFloat.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantFloat.java index 9c30c9e4fdb..e86bbb94e66 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantFloat.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantFloat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -29,8 +29,8 @@ import com.sun.org.apache.bcel.internal.Const; /** * This class is derived from the abstract {@link Constant} and represents a reference to a float object. * - * @see Constant - * @LastModified: Jun 2019 + * @see Constant + * @LastModified: Sept 2025 */ public final class ConstantFloat extends Constant implements ConstantObject { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantInteger.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantInteger.java index cabd2e15b03..20c0717acb6 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantInteger.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantInteger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -29,8 +29,8 @@ import com.sun.org.apache.bcel.internal.Const; /** * This class is derived from the abstract {@link Constant} and represents a reference to an int object. * - * @see Constant - * @LastModified: Jun 2019 + * @see Constant + * @LastModified: Sept 2025 */ public final class ConstantInteger extends Constant implements ConstantObject { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantLong.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantLong.java index c1e683abadb..6936162c264 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantLong.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantLong.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -29,8 +29,8 @@ import com.sun.org.apache.bcel.internal.Const; /** * This class is derived from the abstract {@link Constant} and represents a reference to a long object. * - * @see Constant - * @LastModified: Jan 2020 + * @see Constant + * @LastModified: Sept 2025 */ public final class ConstantLong extends Constant implements ConstantObject { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantObject.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantObject.java index cb28f7dacb8..ab187b7e4f8 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantObject.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantObject.java @@ -29,7 +29,10 @@ package com.sun.org.apache.bcel.internal.classfile; public interface ConstantObject { /** - * @return object representing the constant, e.g., Long for ConstantLong + * Gets the object representing the constant, e.g., Long for ConstantLong. + * + * @param constantPool the constant. + * @return object representing the constant, e.g., Long for ConstantLong. */ - Object getConstantValue(ConstantPool cp); + Object getConstantValue(ConstantPool constantPool); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantPool.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantPool.java index 5fb4ef1b080..ae1e3977c99 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantPool.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantPool.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -35,7 +35,7 @@ import com.sun.org.apache.bcel.internal.Const; * * @see Constant * @see com.sun.org.apache.bcel.internal.generic.ConstantPoolGen - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class ConstantPool implements Cloneable, Node, Iterable { @@ -73,7 +73,7 @@ public class ConstantPool implements Cloneable, Node, Iterable { * @param constantPool Array of constants */ public ConstantPool(final Constant[] constantPool) { - this.constantPool = constantPool; + setConstantPool(constantPool); } /** @@ -88,6 +88,7 @@ public class ConstantPool implements Cloneable, Node, Iterable { constantPool = new Constant[constantPoolCount]; /* * constantPool[0] is unused by the compiler and may be used freely by the implementation. + * constantPool[0] is currently unused by the implementation. */ for (int i = 1; i < constantPoolCount; i++) { constantPool[i] = Constant.readConstant(input); @@ -288,7 +289,7 @@ public class ConstantPool implements Cloneable, Node, Iterable { */ public T getConstant(final int index, final byte tag, final Class castTo) throws ClassFormatException { final T c = getConstant(index); - if (c.getTag() != tag) { + if (c == null || c.getTag() != tag) { throw new ClassFormatException("Expected class '" + Const.getConstantName(tag) + "' at index " + index + " and got " + c); } return c; @@ -313,15 +314,17 @@ public class ConstantPool implements Cloneable, Node, Iterable { throw new ClassFormatException("Invalid constant pool reference at index: " + index + ". Expected " + castTo + " but was " + constantPool[index].getClass()); } + if (index > 1) { + final Constant prev = constantPool[index - 1]; + if (prev != null && (prev.getTag() == Const.CONSTANT_Double || prev.getTag() == Const.CONSTANT_Long)) { + throw new ClassFormatException("Constant pool at index " + index + " is invalid. The index is unused due to the preceeding " + + Const.getConstantName(prev.getTag()) + "."); + } + } // Previous check ensures this won't throw a ClassCastException final T c = castTo.cast(constantPool[index]); - if (c == null - // the 0th element is always null - && index != 0) { - final Constant prev = constantPool[index - 1]; - if (prev == null || prev.getTag() != Const.CONSTANT_Double && prev.getTag() != Const.CONSTANT_Long) { - throw new ClassFormatException("Constant pool at index " + index + " is null."); - } + if (c == null) { + throw new ClassFormatException("Constant pool at index " + index + " is null."); } return c; } @@ -402,7 +405,7 @@ public class ConstantPool implements Cloneable, Node, Iterable { * @return Length of constant pool. */ public int getLength() { - return constantPool == null ? 0 : constantPool.length; + return constantPool.length; } @Override @@ -421,7 +424,7 @@ public class ConstantPool implements Cloneable, Node, Iterable { * @param constantPool */ public void setConstantPool(final Constant[] constantPool) { - this.constantPool = constantPool; + this.constantPool = constantPool != null ? constantPool : Constant.EMPTY_ARRAY; } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantUtf8.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantUtf8.java index ec875554c1a..90e45c807e5 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantUtf8.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantUtf8.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -36,11 +36,11 @@ import com.sun.org.apache.bcel.internal.Const; * The following system properties govern caching this class performs. *

        *
          - *
        • {@value #SYS_PROP_CACHE_MAX_ENTRIES} (since 6.4): The size of the cache, by default 0, meaning caching is + *
        • {@link #SYS_PROP_CACHE_MAX_ENTRIES} (since 6.4): The size of the cache, by default 0, meaning caching is * disabled.
        • - *
        • {@value #SYS_PROP_CACHE_MAX_ENTRY_SIZE} (since 6.0): The maximum size of the values to cache, by default 200, 0 + *
        • {@link #SYS_PROP_CACHE_MAX_ENTRY_SIZE} (since 6.0): The maximum size of the values to cache, by default 200, 0 * disables caching. Values larger than this are not cached.
        • - *
        • {@value #SYS_PROP_STATISTICS} (since 6.0): Prints statistics on the console when the JVM exits.
        • + *
        • {@link #SYS_PROP_STATISTICS} (since 6.0): Prints statistics on the console when the JVM exits.
        • *
        *

        * Here is a sample Maven invocation with caching disabled: @@ -58,11 +58,11 @@ import com.sun.org.apache.bcel.internal.Const; * * * @see Constant - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class ConstantUtf8 extends Constant { - private static class Cache { + private static final class Cache { private static final boolean BCEL_STATISTICS = false; private static final int MAX_ENTRIES = 20000; @@ -82,7 +82,7 @@ public final class ConstantUtf8 extends Constant { private static final int MAX_ENTRY_SIZE = 200; static boolean isEnabled() { - return Cache.MAX_ENTRIES > 0 && MAX_ENTRY_SIZE > 0; + return MAX_ENTRIES > 0 && MAX_ENTRY_SIZE > 0; } } @@ -117,6 +117,11 @@ public final class ConstantUtf8 extends Constant { hits = considered = skipped = created = 0; } + // Avoid Spotbugs complaint about Write to static field + private static void countCreated() { + created++; + } + /** * Gets a new or cached instance of the given value. *

        @@ -203,7 +208,7 @@ public final class ConstantUtf8 extends Constant { ConstantUtf8(final DataInput dataInput) throws IOException { super(Const.CONSTANT_Utf8); value = dataInput.readUTF(); - created++; + countCreated(); } /** @@ -212,7 +217,7 @@ public final class ConstantUtf8 extends Constant { public ConstantUtf8(final String value) { super(Const.CONSTANT_Utf8); this.value = Objects.requireNonNull(value, "value"); - created++; + countCreated(); } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantValue.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantValue.java index 311e9a33fa3..143c2a25544 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantValue.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ConstantValue.java @@ -56,7 +56,7 @@ public final class ConstantValue extends Attribute { } /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Name index in constant pool * @param length Content length in bytes diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Deprecated.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Deprecated.java index 90841d96081..c561afe33c5 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Deprecated.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Deprecated.java @@ -59,7 +59,7 @@ public final class Deprecated extends Attribute { } /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Index in constant pool to CONSTANT_Utf8 * @param length Content length in bytes diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/DescendingVisitor.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/DescendingVisitor.java index 3c475891acd..934b608d6ac 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/DescendingVisitor.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/DescendingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -22,12 +22,13 @@ package com.sun.org.apache.bcel.internal.classfile; import java.util.Objects; import java.util.Stack; import java.util.stream.Stream; +import jdk.xml.internal.Utils; /** - * Traverses a JavaClass with another Visitor object 'piggy-backed' that is - * applied to all components of a JavaClass object. I.e. this class supplies the - * traversal strategy, other classes can make use of it. + * Traverses a JavaClass with another Visitor object 'piggy-backed' that is applied to all components of a JavaClass + * object. I.e. this class supplies the traversal strategy, other classes can make use of it. * + * @LastModified: Sept 2025 */ public class DescendingVisitor implements Visitor { private final JavaClass clazz; @@ -46,7 +47,7 @@ public class DescendingVisitor implements Visitor { } private void accept(final E[] node) { - Stream.of(node).forEach(e -> e.accept(this)); + Utils.streamOfIfNonNull(node).forEach(e -> e.accept(this)); } /** @@ -507,6 +508,21 @@ public class DescendingVisitor implements Visitor { stack.pop(); } + @Override + public void visitRecord(final Record record) { + stack.push(record); + record.accept(visitor); + accept(record.getComponents()); + stack.pop(); + } + + @Override + public void visitRecordComponent(final RecordComponentInfo recordComponentInfo) { + stack.push(recordComponentInfo); + recordComponentInfo.accept(visitor); + stack.pop(); + } + @Override public void visitSignature(final Signature attribute) { stack.push(attribute); @@ -531,6 +547,20 @@ public class DescendingVisitor implements Visitor { @Override public void visitStackMapEntry(final StackMapEntry var) { + stack.push(var); + var.accept(visitor); + accept(var.getTypesOfLocals()); + accept(var.getTypesOfStackItems()); + stack.pop(); + } + + /** + * Visits a {@link StackMapType} object. + * @param var object to visit + * @since 6.8.0 + */ + @Override + public void visitStackMapType(final StackMapType var) { stack.push(var); var.accept(visitor); stack.pop(); @@ -549,4 +579,5 @@ public class DescendingVisitor implements Visitor { attribute.accept(visitor); stack.pop(); } + } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ElementValue.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ElementValue.java index 5c3d9b3172b..d87307c0e7d 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ElementValue.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ElementValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -50,7 +50,7 @@ import com.sun.org.apache.bcel.internal.Const; *} * * @since 6.0 - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public abstract class ElementValue { @@ -67,6 +67,7 @@ public abstract class ElementValue { public static final byte PRIMITIVE_LONG = 'J'; public static final byte PRIMITIVE_SHORT = 'S'; public static final byte PRIMITIVE_BOOLEAN = 'Z'; + static final ElementValue[] EMPTY_ARRAY = {}; /** * Reads an {@code element_value} as an {@code ElementValue}. @@ -124,7 +125,7 @@ public abstract class ElementValue { final int numArrayVals = input.readUnsignedShort(); final ElementValue[] evalues = new ElementValue[numArrayVals]; for (int j = 0; j < numArrayVals; j++) { - evalues[j] = ElementValue.readElementValue(input, cpool, arrayNesting); + evalues[j] = readElementValue(input, cpool, arrayNesting); } return new ArrayElementValue(ARRAY, evalues, cpool); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/EmptyVisitor.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/EmptyVisitor.java index 826ba41af70..db33c871656 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/EmptyVisitor.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/EmptyVisitor.java @@ -315,6 +315,15 @@ public class EmptyVisitor implements Visitor { public void visitStackMapEntry(final StackMapEntry obj) { } + /** + * Visits a {@link StackMapType} object. + * @param obj object to visit + * @since 6.8.0 + */ + @Override + public void visitStackMapType(final StackMapType obj) { + } + @Override public void visitSynthetic(final Synthetic obj) { } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ExceptionTable.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ExceptionTable.java index 90eaa3eb062..1b6004b3740 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ExceptionTable.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ExceptionTable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -27,6 +27,7 @@ import java.util.Arrays; import com.sun.org.apache.bcel.internal.Const; import com.sun.org.apache.bcel.internal.util.Args; +import jdk.xml.internal.Utils; /** * This class represents the table of exceptions that are thrown by a method. This attribute may be used once per @@ -43,7 +44,7 @@ import com.sun.org.apache.bcel.internal.util.Args; * } * * @see Code - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class ExceptionTable extends Attribute { @@ -60,7 +61,7 @@ public final class ExceptionTable extends Attribute { } /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Index in constant pool * @param length Content length in bytes @@ -85,7 +86,7 @@ public final class ExceptionTable extends Attribute { */ public ExceptionTable(final int nameIndex, final int length, final int[] exceptionIndexTable, final ConstantPool constantPool) { super(Const.ATTR_EXCEPTIONS, nameIndex, length, constantPool); - this.exceptionIndexTable = exceptionIndexTable != null ? exceptionIndexTable : Const.EMPTY_INT_ARRAY; + this.exceptionIndexTable = Utils.createEmptyArrayIfNull(exceptionIndexTable); Args.requireU2(this.exceptionIndexTable.length, "exceptionIndexTable.length"); } @@ -156,7 +157,7 @@ public final class ExceptionTable extends Attribute { * length. */ public void setExceptionIndexTable(final int[] exceptionIndexTable) { - this.exceptionIndexTable = exceptionIndexTable != null ? exceptionIndexTable : Const.EMPTY_INT_ARRAY; + this.exceptionIndexTable = Utils.createEmptyArrayIfNull(exceptionIndexTable); } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Field.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Field.java index 7e6ccb2ecb5..8ffc796a0ea 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Field.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Field.java @@ -42,45 +42,37 @@ public final class Field extends FieldOrMethod { */ public static final Field[] EMPTY_ARRAY = {}; - private static BCELComparator bcelComparator = new BCELComparator() { + private static BCELComparator bcelComparator = new BCELComparator() { @Override - public boolean equals(final Object o1, final Object o2) { - final Field THIS = (Field) o1; - final Field THAT = (Field) o2; - return Objects.equals(THIS.getName(), THAT.getName()) && Objects.equals(THIS.getSignature(), THAT.getSignature()); + public boolean equals(final Field a, final Field b) { + return a == b || a != null && b != null && Objects.equals(a.getName(), b.getName()) && Objects.equals(a.getSignature(), b.getSignature()); } @Override - public int hashCode(final Object o) { - final Field THIS = (Field) o; - return THIS.getSignature().hashCode() ^ THIS.getName().hashCode(); + public int hashCode(final Field o) { + return o != null ? Objects.hash(o.getSignature(), o.getName()) : 0; } }; /** - * Empty array. + * @return Comparison strategy object. */ - static final Field[] EMPTY_FIELD_ARRAY = {}; - - /** - * @return Comparison strategy object - */ - public static BCELComparator getComparator() { + public static BCELComparator getComparator() { return bcelComparator; } /** - * @param comparator Comparison strategy object + * @param comparator Comparison strategy object. */ - public static void setComparator(final BCELComparator comparator) { + public static void setComparator(final BCELComparator comparator) { bcelComparator = comparator; } /** - * Construct object from file stream. + * Constructs object from file stream. * - * @param file Input stream + * @param file Input stream. */ Field(final DataInput file, final ConstantPool constantPool) throws IOException, ClassFormatException { super(file, constantPool); @@ -133,7 +125,7 @@ public final class Field extends FieldOrMethod { */ @Override public boolean equals(final Object obj) { - return bcelComparator.equals(this, obj); + return obj instanceof Field && bcelComparator.equals(this, (Field) obj); } /** @@ -149,14 +141,16 @@ public final class Field extends FieldOrMethod { } /** + * See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.2.2 + * * @return type of field */ public Type getType() { - return Type.getReturnType(getSignature()); + return Type.getType(getSignature()); } /** - * Return value as defined by given BCELComparator strategy. By default return the hashcode of the field's name XOR + * Return value as defined by given BCELComparator strategy. By default return the hash code of the field's name XOR * signature. * * @see Object#hashCode() diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/FieldOrMethod.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/FieldOrMethod.java index 1daa6a62fd5..679d5a9eb7c 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/FieldOrMethod.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/FieldOrMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -28,7 +28,7 @@ import java.util.Arrays; /** * Abstract super class for fields and methods. * - * @LastModified: Jan 2020 + * @LastModified: Sept 2025 */ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, Node { @@ -72,7 +72,7 @@ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, No } /** - * Construct object from file stream. + * Constructs object from file stream. * * @param file Input stream * @throws IOException if an I/O error occurs. @@ -88,7 +88,7 @@ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, No } /** - * Construct object from file stream. + * Constructs object from file stream. * * @param file Input stream * @throws IOException if an I/O error occurs. @@ -137,7 +137,7 @@ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, No Arrays.setAll(c.attributes, i -> attributes[i].copy(constantPool)); return c; } catch (final CloneNotSupportedException e) { - throw new IllegalStateException(e); + throw new UnsupportedOperationException(e); } } @@ -152,10 +152,8 @@ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, No file.writeShort(name_index); file.writeShort(signature_index); file.writeShort(attributes_count); - if (attributes != null) { - for (final Attribute attribute : attributes) { - attribute.dump(file); - } + for (final Attribute attribute : attributes) { + attribute.dump(file); } } @@ -171,6 +169,22 @@ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, No return annotationEntries; } + /** + * Gets attribute for given tag. + * @return Attribute for given tag, null if not found. + * Refer to {@link com.sun.org.apache.bcel.internal.Const#ATTR_UNKNOWN} constants named ATTR_* for possible values. + * @since 6.10.0 + */ + @SuppressWarnings("unchecked") + public final T getAttribute(final byte tag) { + for (final Attribute attribute : getAttributes()) { + if (attribute.getTag() == tag) { + return (T) attribute; + } + } + return null; + } + /** * @return Collection of object attributes. */ @@ -221,7 +235,7 @@ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, No } /** - * @return String representation of object's type signature (java style) + * @return String representation of object's type signature (Java style) */ public final String getSignature() { return constant_pool.getConstantUtf8(signature_index).getBytes(); @@ -238,8 +252,8 @@ public abstract class FieldOrMethod extends AccessFlags implements Cloneable, No * @param attributes Collection of object attributes. */ public final void setAttributes(final Attribute[] attributes) { - this.attributes = attributes; - this.attributes_count = attributes != null ? attributes.length : 0; // init deprecated field + this.attributes = attributes != null ? attributes : Attribute.EMPTY_ARRAY; + this.attributes_count = this.attributes.length; // init deprecated field } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClass.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClass.java index d77582815b7..82900dcca10 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClass.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClass.java @@ -41,7 +41,7 @@ public final class InnerClass implements Cloneable, Node { private int innerAccessFlags; /** - * Construct object from file stream. + * Constructs object from file stream. * * @param file Input stream * @throws IOException if an I/O error occurs. diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClasses.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClasses.java index 2295ca5c625..b61be1effa0 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClasses.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/InnerClasses.java @@ -42,7 +42,7 @@ public final class InnerClasses extends Attribute implements IterableClassGen class. * * @see com.sun.org.apache.bcel.internal.generic.ClassGen - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparable { @@ -67,26 +68,23 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl public static final byte HEAP = 1; public static final byte FILE = 2; public static final byte ZIP = 3; - private static BCELComparator bcelComparator = new BCELComparator() { + private static BCELComparator bcelComparator = new BCELComparator() { @Override - public boolean equals(final Object o1, final Object o2) { - final JavaClass THIS = (JavaClass) o1; - final JavaClass THAT = (JavaClass) o2; - return Objects.equals(THIS.getClassName(), THAT.getClassName()); + public boolean equals(final JavaClass a, final JavaClass b) { + return a == b || a != null && b != null && Objects.equals(a.getClassName(), b.getClassName()); } @Override - public int hashCode(final Object o) { - final JavaClass THIS = (JavaClass) o; - return THIS.getClassName().hashCode(); + public int hashCode(final JavaClass o) { + return o != null ? Objects.hashCode(o.getClassName()) : 0; } }; /** - * @return Comparison strategy object + * @return Comparison strategy object. */ - public static BCELComparator getComparator() { + public static BCELComparator getComparator() { return bcelComparator; } @@ -100,9 +98,9 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl } /** - * @param comparator Comparison strategy object + * @param comparator Comparison strategy object. */ - public static void setComparator(final BCELComparator comparator) { + public static void setComparator(final BCELComparator comparator) { bcelComparator = comparator; } @@ -128,8 +126,10 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl private boolean isAnonymous; private boolean isNested; + private boolean isRecord; private boolean computedNestedTypeStatus; + private boolean computedRecord; /** * In cases where we go ahead and create something, use the default SyntheticRepository, because we don't know any @@ -177,17 +177,15 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl public JavaClass(final int classNameIndex, final int superclassNameIndex, final String fileName, final int major, final int minor, final int accessFlags, final ConstantPool constantPool, int[] interfaces, Field[] fields, Method[] methods, Attribute[] attributes, final byte source) { super(accessFlags); - if (interfaces == null) { - interfaces = Const.EMPTY_INT_ARRAY; - } + interfaces = Utils.createEmptyArrayIfNull(interfaces); if (attributes == null) { attributes = Attribute.EMPTY_ARRAY; } if (fields == null) { - fields = Field.EMPTY_FIELD_ARRAY; + fields = Field.EMPTY_ARRAY; } if (methods == null) { - methods = Method.EMPTY_METHOD_ARRAY; + methods = Method.EMPTY_ARRAY; } this.classNameIndex = classNameIndex; this.superclassNameIndex = superclassNameIndex; @@ -254,6 +252,19 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl return getClassName().compareTo(obj.getClassName()); } + private void computeIsRecord() { + if (computedRecord) { + return; + } + for (final Attribute attribute : this.attributes) { + if (attribute instanceof Record) { + isRecord = true; + break; + } + } + this.computedRecord = true; + } + private void computeNestedTypeStatus() { if (computedNestedTypeStatus) { return; @@ -384,11 +395,51 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl */ @Override public boolean equals(final Object obj) { - return bcelComparator.equals(this, obj); + return obj instanceof JavaClass && bcelComparator.equals(this, (JavaClass) obj); } /** - * Get all interfaces implemented by this JavaClass (transitively). + * Finds a visible field by name and type in this class and its super classes. + * @param fieldName the field name to find + * @param fieldType the field type to find + * @return field matching given name and type, null if field is not found or not accessible from this class. + * @throws ClassNotFoundException + * @since 6.8.0 + */ + public Field findField(final String fieldName, final Type fieldType) throws ClassNotFoundException { + for (final Field field : fields) { + if (field.getName().equals(fieldName)) { + final Type fType = Type.getType(field.getSignature()); + /* + * TODO: Check if assignment compatibility is sufficient. What does Sun do? + */ + if (fType.equals(fieldType)) { + return field; + } + } + } + + final JavaClass superclass = getSuperClass(); + if (superclass != null && !"java.lang.Object".equals(superclass.getClassName())) { + final Field f = superclass.findField(fieldName, fieldType); + if (f != null && (f.isPublic() || f.isProtected() || !f.isPrivate() && packageName.equals(superclass.getPackageName()))) { + return f; + } + } + final JavaClass[] implementedInterfaces = getInterfaces(); + if (implementedInterfaces != null) { + for (final JavaClass implementedInterface : implementedInterfaces) { + final Field f = implementedInterface.findField(fieldName, fieldType); + if (f != null) { + return f; + } + } + } + return null; + } + + /** + * Gets all interfaces implemented by this JavaClass (transitively). * * @throws ClassNotFoundException if any of the class's superclasses or interfaces can't be found. */ @@ -409,7 +460,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl queue.enqueue(iface); } } - return allInterfaces.toArray(JavaClass.EMPTY_ARRAY); + return allInterfaces.toArray(EMPTY_ARRAY); } /** @@ -424,6 +475,22 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl return annotations; } + /** + * Gets attribute for given tag. + * @return Attribute for given tag, null if not found. + * Refer to {@link com.sun.org.apache.bcel.internal.Const#ATTR_UNKNOWN} constants named ATTR_* for possible values. + * @since 6.10.0 + */ + @SuppressWarnings("unchecked") + public final T getAttribute(final byte tag) { + for (final Attribute attribute : getAttributes()) { + if (attribute.getTag() == tag) { + return (T) attribute; + } + } + return null; + } + /** * @return Attributes of the class. */ @@ -495,7 +562,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl } /** - * Get interfaces directly implemented by this JavaClass. + * Gets interfaces directly implemented by this JavaClass. * * @throws ClassNotFoundException if any of the class's interfaces can't be found. */ @@ -587,7 +654,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl } /** - * @return the superclass for this JavaClass object, or null if this is java.lang.Object + * @return the superclass for this JavaClass object, or null if this is {@link Object} * @throws ClassNotFoundException if the superclass can't be found */ public JavaClass getSuperClass() throws ClassNotFoundException { @@ -607,12 +674,12 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl for (clazz = clazz.getSuperClass(); clazz != null; clazz = clazz.getSuperClass()) { allSuperClasses.add(clazz); } - return allSuperClasses.toArray(JavaClass.EMPTY_ARRAY); + return allSuperClasses.toArray(EMPTY_ARRAY); } /** - * returns the super class name of this class. In the case that this class is java.lang.Object, it will return itself - * (java.lang.Object). This is probably incorrect but isn't fixed at this time to not break existing clients. + * returns the super class name of this class. In the case that this class is {@link Object}, it will return itself + * ({@link Object}). This is probably incorrect but isn't fixed at this time to not break existing clients. * * @return Superclass name. */ @@ -628,7 +695,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl } /** - * Return value as defined by given BCELComparator strategy. By default return the hashcode of the class name. + * Return value as defined by given BCELComparator strategy. By default return the hash code of the class name. * * @see Object#hashCode() */ @@ -645,7 +712,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl if (!inter.isInterface()) { throw new IllegalArgumentException(inter.getClassName() + " is no interface"); } - if (this.equals(inter)) { + if (equals(inter)) { return true; } final JavaClass[] superInterfaces = getAllInterfaces(); @@ -664,7 +731,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl * @throws ClassNotFoundException if superclasses or superinterfaces of this object can't be found */ public final boolean instanceOf(final JavaClass superclass) throws ClassNotFoundException { - if (this.equals(superclass)) { + if (equals(superclass)) { return true; } for (final JavaClass clazz : getSuperClasses()) { @@ -698,6 +765,17 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl return this.isNested; } + /** + * Tests whether this class was declared as a record + * + * @return true if a record attribute is present, false otherwise. + * @since 6.9.0 + */ + public boolean isRecord() { + computeIsRecord(); + return this.isRecord; + } + public final boolean isSuper() { return (super.getAccessFlags() & Const.ACC_SUPER) != 0; } @@ -706,7 +784,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl * @param attributes . */ public void setAttributes(final Attribute[] attributes) { - this.attributes = attributes; + this.attributes = attributes != null ? attributes : Attribute.EMPTY_ARRAY; } /** @@ -734,11 +812,11 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl * @param fields . */ public void setFields(final Field[] fields) { - this.fields = fields; + this.fields = fields != null ? fields : Field.EMPTY_ARRAY; } /** - * Set File name of class, aka SourceFile attribute value + * Sets File name of class, aka SourceFile attribute value */ public void setFileName(final String fileName) { this.fileName = fileName; @@ -748,14 +826,14 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl * @param interfaceNames . */ public void setInterfaceNames(final String[] interfaceNames) { - this.interfaceNames = interfaceNames; + this.interfaceNames = Utils.createEmptyArrayIfNull(interfaceNames, String[].class); } /** * @param interfaces . */ public void setInterfaces(final int[] interfaces) { - this.interfaces = interfaces; + this.interfaces = Utils.createEmptyArrayIfNull(interfaces); } /** @@ -769,7 +847,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl * @param methods . */ public void setMethods(final Method[] methods) { - this.methods = methods; + this.methods = methods != null ? methods : Method.EMPTY_ARRAY; } /** @@ -787,7 +865,7 @@ public class JavaClass extends AccessFlags implements Cloneable, Node, Comparabl } /** - * Set absolute path to file this class was read from. + * Sets absolute path to file this class was read from. */ public void setSourceFileName(final String sourceFileName) { this.sourceFileName = sourceFileName; diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumber.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumber.java index 4380d04bc06..dc41ad065f8 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumber.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumber.java @@ -40,11 +40,11 @@ public final class LineNumber implements Cloneable, Node { /** Program Counter (PC) corresponds to line */ private int startPc; - /** number in source file */ + /** Number in source file */ private int lineNumber; /** - * Construct object from file stream. + * Constructs object from file stream. * * @param file Input stream * @throws IOException if an I/O Exception occurs in readUnsignedShort diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumberTable.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumberTable.java index 6251fc514cc..96541f309bd 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumberTable.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/LineNumberTable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -36,7 +36,7 @@ import jdk.xml.internal.SecuritySupport; * * @see Code * @see LineNumber - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public final class LineNumberTable extends Attribute implements Iterable { @@ -44,7 +44,7 @@ public final class LineNumberTable extends Attribute implements Iterable + * Note that both objects use the same references (shallow copy). Use copy() for a physical copy. + *

        */ public LineNumberTable(final LineNumberTable c) { this(c.getNameIndex(), c.getLength(), c.getLineNumberTable(), c.getConstantPool()); @@ -190,7 +191,7 @@ public final class LineNumberTable extends Attribute implements Iterable { + private static final LocalVariable[] EMPTY_ARRAY = {}; + private LocalVariable[] localVariableTable; // variables /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Index in constant pool * @param length Content length in bytes @@ -68,7 +70,7 @@ public class LocalVariableTable extends Attribute implements Iterable { + private static final LocalVariable[] EMPTY_ARRAY = {}; + private LocalVariable[] localVariableTypeTable; // variables LocalVariableTypeTable(final int nameIdx, final int len, final DataInput input, final ConstantPool cpool) throws IOException { this(nameIdx, len, (LocalVariable[]) null, cpool); - final int localVariableTypeTableLength = input.readUnsignedShort(); localVariableTypeTable = new LocalVariable[localVariableTypeTableLength]; - for (int i = 0; i < localVariableTypeTableLength; i++) { localVariableTypeTable[i] = new LocalVariable(input, cpool); } @@ -97,7 +97,6 @@ public class LocalVariableTypeTable extends Attribute implements Iterable localVariableTypeTable[i].copy()); c.setConstantPool(constantPool); @@ -119,7 +118,6 @@ public class LocalVariableTypeTable extends Attribute implements Iterable bcelComparator = new BCELComparator() { @Override - public boolean equals(final Object o1, final Object o2) { - final Method THIS = (Method) o1; - final Method THAT = (Method) o2; - return Objects.equals(THIS.getName(), THAT.getName()) && Objects.equals(THIS.getSignature(), THAT.getSignature()); + public boolean equals(final Method a, final Method b) { + return a == b || a != null && b != null && Objects.equals(a.getName(), b.getName()) && Objects.equals(a.getSignature(), b.getSignature()); } @Override - public int hashCode(final Object o) { - final Method THIS = (Method) o; - return THIS.getSignature().hashCode() ^ THIS.getName().hashCode(); + public int hashCode(final Method o) { + return o != null ? Objects.hash(o.getSignature(), o.getName()) : 0; } }; /** - * Empty array. + * @return Comparison strategy object. */ - static final Method[] EMPTY_METHOD_ARRAY = {}; - - /** - * @return Comparison strategy object - */ - public static BCELComparator getComparator() { + public static BCELComparator getComparator() { return bcelComparator; } /** - * @param comparator Comparison strategy object + * @param comparator Comparison strategy object. */ - public static void setComparator(final BCELComparator comparator) { + public static void setComparator(final BCELComparator comparator) { bcelComparator = comparator; } - // annotations defined on the parameters of a method + /** Annotations defined on the parameters of a method. */ private ParameterAnnotationEntry[] parameterAnnotationEntries; /** @@ -85,7 +77,7 @@ public final class Method extends FieldOrMethod { } /** - * Construct object from file stream. + * Constructs object from file stream. * * @param file Input stream * @throws IOException if an I/O error occurs. @@ -142,7 +134,7 @@ public final class Method extends FieldOrMethod { */ @Override public boolean equals(final Object obj) { - return bcelComparator.equals(this, obj); + return obj instanceof Method && bcelComparator.equals(this, (Method) obj); } /** @@ -189,7 +181,7 @@ public final class Method extends FieldOrMethod { } /** - * @return LocalVariableTable of code attribute if any, i.e. the call is forwarded to the Code atribute. + * @return LocalVariableTable of code attribute if any, i.e. the call is forwarded to the Code attribute. */ public LocalVariableTable getLocalVariableTable() { final Code code = getCode(); @@ -199,6 +191,19 @@ public final class Method extends FieldOrMethod { return code.getLocalVariableTable(); } + /** + * Gets the local variable type table attribute {@link LocalVariableTypeTable}. + * @return LocalVariableTypeTable of code attribute if any, i.e. the call is forwarded to the Code attribute. + * @since 6.10.0 + */ + public LocalVariableTypeTable getLocalVariableTypeTable() { + final Code code = getCode(); + if (code == null) { + return null; + } + return code.getLocalVariableTypeTable(); + } + /** * @return Annotations on the parameters of a method * @since 6.0 @@ -218,7 +223,7 @@ public final class Method extends FieldOrMethod { } /** - * Return value as defined by given BCELComparator strategy. By default return the hashcode of the method's name XOR + * Return value as defined by given BCELComparator strategy. By default return the hash code of the method's name XOR * signature. * * @see Object#hashCode() diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameter.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameter.java index ffc1a20f80a..865e154d334 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameter.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameter.java @@ -29,6 +29,9 @@ import com.sun.org.apache.bcel.internal.Const; /** * Entry of the parameters table. + *

        + * Implements {@link Node} as of 6.7.0. + *

        * * @see The class File Format : * The MethodParameters Attribute @@ -46,7 +49,7 @@ public class MethodParameter implements Cloneable, Node { } /** - * Construct object from input stream. + * Constructs an instance from a DataInput. * * @param input Input stream * @throws IOException if an I/O error occurs. @@ -75,7 +78,7 @@ public class MethodParameter implements Cloneable, Node { } /** - * Dump object to file stream on binary format. + * Dumps object to file stream on binary format. * * @param file Output file stream * @throws IOException if an I/O error occurs. @@ -94,7 +97,10 @@ public class MethodParameter implements Cloneable, Node { } /** - * Returns the name of the parameter. + * Gets the name of the parameter. + * + * @param constantPool The pool to query. + * @return Constant from the given pool. */ public String getParameterName(final ConstantPool constantPool) { if (nameIndex == 0) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameters.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameters.java index 5b5d1d77f6f..2f5bffd8d3c 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameters.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/MethodParameters.java @@ -42,13 +42,12 @@ public class MethodParameters extends Attribute implements Iterable parameters[i].copy()); c.setConstantPool(constantPool); return c; @@ -96,6 +94,6 @@ public class MethodParameters extends Attribute implements IterableAttribute and represents the list of packages that are exported or opened by the * Module attribute. There may be at most one ModulePackages attribute in a ClassFile structure. * * @see Attribute - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class ModulePackages extends Attribute { private int[] packageIndexTable; /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Index in constant pool * @param length Content length in bytes @@ -65,7 +66,7 @@ public final class ModulePackages extends Attribute { */ public ModulePackages(final int nameIndex, final int length, final int[] packageIndexTable, final ConstantPool constantPool) { super(Const.ATTR_MODULE_PACKAGES, nameIndex, length, constantPool); - this.packageIndexTable = packageIndexTable != null ? packageIndexTable : Const.EMPTY_INT_ARRAY; + this.packageIndexTable = Utils.createEmptyArrayIfNull(packageIndexTable); Args.requireU2(this.packageIndexTable.length, "packageIndexTable.length"); } @@ -145,7 +146,7 @@ public final class ModulePackages extends Attribute { * @param packageIndexTable the list of package indexes Also redefines number_of_packages according to table length. */ public void setPackageIndexTable(final int[] packageIndexTable) { - this.packageIndexTable = packageIndexTable != null ? packageIndexTable : Const.EMPTY_INT_ARRAY; + this.packageIndexTable = Utils.createEmptyArrayIfNull(packageIndexTable); } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleProvides.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleProvides.java index f6c6058dfbb..490e9cd5d44 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleProvides.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleProvides.java @@ -36,12 +36,20 @@ import com.sun.org.apache.bcel.internal.Const; */ public final class ModuleProvides implements Cloneable, Node { + private static String getImplementationClassNameAtIndex(final ConstantPool constantPool, final int index, final boolean compactClassName) { + final String className = constantPool.getConstantString(index, Const.CONSTANT_Class); + if (compactClassName) { + return Utility.compactClassName(className, false); + } + return className; + } private final int providesIndex; // points to CONSTANT_Class_info private final int providesWithCount; + private final int[] providesWithIndex; // points to CONSTANT_Class_info /** - * Construct object from file stream. + * Constructs object from file stream. * * @param file Input stream * @throws IOException if an I/O Exception occurs in readUnsignedShort @@ -66,8 +74,6 @@ public final class ModuleProvides implements Cloneable, Node { v.visitModuleProvides(this); } - // TODO add more getters and setters? - /** * @return deep copy of this object */ @@ -94,6 +100,31 @@ public final class ModuleProvides implements Cloneable, Node { } } + /** + * Gets the array of implementation class names for this ModuleProvides. + * @param constantPool Array of constants usually obtained from the ClassFile object + * @param compactClassName false for original constant pool value, true to replace '/' with '.' + * @return array of implementation class names + * @since 6.10.0 + */ + public String[] getImplementationClassNames(final ConstantPool constantPool, final boolean compactClassName) { + final String[] implementationClassNames = new String[providesWithCount]; + for (int i = 0; i < providesWithCount; i++) { + implementationClassNames[i] = getImplementationClassNameAtIndex(constantPool, providesWithIndex[i], compactClassName); + } + return implementationClassNames; + } + + /** + * Gets the interface name for this ModuleProvides. + * @param constantPool Array of constants usually obtained from the ClassFile object + * @return interface name + * @since 6.10.0 + */ + public String getInterfaceName(final ConstantPool constantPool) { + return constantPool.constantToString(providesIndex, Const.CONSTANT_Class); + } + /** * @return String representation */ @@ -107,12 +138,12 @@ public final class ModuleProvides implements Cloneable, Node { */ public String toString(final ConstantPool constantPool) { final StringBuilder buf = new StringBuilder(); - final String interfaceName = constantPool.constantToString(providesIndex, Const.CONSTANT_Class); - buf.append(Utility.compactClassName(interfaceName, false)); + final String interfaceName = getInterfaceName(constantPool); + buf.append(interfaceName); buf.append(", with(").append(providesWithCount).append("):\n"); for (final int index : providesWithIndex) { - final String className = constantPool.getConstantString(index, Const.CONSTANT_Class); - buf.append(" ").append(Utility.compactClassName(className, false)).append("\n"); + final String className = getImplementationClassNameAtIndex(constantPool, index, true); + buf.append(" ").append(className).append("\n"); } return buf.substring(0, buf.length() - 1); // remove the last newline } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleRequires.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleRequires.java index c9c26c20649..3149a18290b 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleRequires.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ModuleRequires.java @@ -41,7 +41,7 @@ public final class ModuleRequires implements Cloneable, Node { private final int requiresVersionIndex; // either 0 or points to CONSTANT_Utf8_info /** - * Construct object from file stream. + * Constructs object from file stream. * * @param file Input stream * @throws IOException if an I/O Exception occurs in readUnsignedShort @@ -63,8 +63,6 @@ public final class ModuleRequires implements Cloneable, Node { v.visitModuleRequires(this); } - // TODO add more getters and setters? - /** * @return deep copy of this object */ @@ -89,6 +87,35 @@ public final class ModuleRequires implements Cloneable, Node { file.writeShort(requiresVersionIndex); } + /** + * Gets the module name from the constant pool. + * @param constantPool Array of constants usually obtained from the ClassFile object + * @return module name + * @since 6.10.0 + */ + public String getModuleName(final ConstantPool constantPool) { + return constantPool.constantToString(requiresIndex, Const.CONSTANT_Module); + } + + /** + * Gets the flags for this ModuleRequires. + * @return the requiresFlags + * @since 6.10.0 + */ + public int getRequiresFlags() { + return requiresFlags; + } + + /** + * Gets the required version from the constant pool. + * @param constantPool Array of constants usually obtained from the ClassFile object + * @return required version, "0" if version index is 0. + * @since 6.10.0 + */ + public String getVersion(final ConstantPool constantPool) { + return requiresVersionIndex == 0 ? "0" : constantPool.getConstantString(requiresVersionIndex, Const.CONSTANT_Utf8); + } + /** * @return String representation */ @@ -102,10 +129,10 @@ public final class ModuleRequires implements Cloneable, Node { */ public String toString(final ConstantPool constantPool) { final StringBuilder buf = new StringBuilder(); - final String moduleName = constantPool.constantToString(requiresIndex, Const.CONSTANT_Module); - buf.append(Utility.compactClassName(moduleName, false)); + final String moduleName = getModuleName(constantPool); + buf.append(moduleName); buf.append(", ").append(String.format("%04x", requiresFlags)); - final String version = requiresVersionIndex == 0 ? "0" : constantPool.getConstantString(requiresVersionIndex, Const.CONSTANT_Utf8); + final String version = getVersion(constantPool); buf.append(", ").append(version); return buf.toString(); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/NestMembers.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/NestMembers.java index 05d982ca6e8..261f57d98a8 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/NestMembers.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/NestMembers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -27,6 +27,7 @@ import java.util.Arrays; import com.sun.org.apache.bcel.internal.Const; import com.sun.org.apache.bcel.internal.util.Args; +import jdk.xml.internal.Utils; /** * This class is derived from Attribute and records the classes and interfaces that are authorized to claim @@ -34,14 +35,14 @@ import com.sun.org.apache.bcel.internal.util.Args; * ClassFile structure. * * @see Attribute - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class NestMembers extends Attribute { private int[] classes; /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Index in constant pool * @param length Content length in bytes @@ -66,7 +67,7 @@ public final class NestMembers extends Attribute { */ public NestMembers(final int nameIndex, final int length, final int[] classes, final ConstantPool constantPool) { super(Const.ATTR_NEST_MEMBERS, nameIndex, length, constantPool); - this.classes = classes != null ? classes : Const.EMPTY_INT_ARRAY; + this.classes = Utils.createEmptyArrayIfNull(classes); Args.requireU2(this.classes.length, "classes.length"); } @@ -146,7 +147,7 @@ public final class NestMembers extends Attribute { * @param classes the list of class indexes Also redefines number_of_classes according to table length. */ public void setClasses(final int[] classes) { - this.classes = classes != null ? classes : Const.EMPTY_INT_ARRAY; + this.classes = Utils.createEmptyArrayIfNull(classes); } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Node.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Node.java index c0395732d79..792ef31cb72 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Node.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Node.java @@ -26,5 +26,5 @@ package com.sun.org.apache.bcel.internal.classfile; */ public interface Node { - void accept(Visitor obj); + void accept(Visitor visitor); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/PMGClass.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/PMGClass.java index 9b1dd4c7b41..b7b5e1f1d99 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/PMGClass.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/PMGClass.java @@ -38,7 +38,7 @@ public final class PMGClass extends Attribute { private int pmgIndex; /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Index in constant pool to CONSTANT_Utf8 * @param length Content length in bytes diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotationEntry.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotationEntry.java index a3070fa7e0c..6ebe60c8049 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotationEntry.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotationEntry.java @@ -37,22 +37,28 @@ public class ParameterAnnotationEntry implements Node { static final ParameterAnnotationEntry[] EMPTY_ARRAY = {}; - public static ParameterAnnotationEntry[] createParameterAnnotationEntries(final Attribute[] attrs) { + public static ParameterAnnotationEntry[] createParameterAnnotationEntries(final Attribute[] attributes) { + if (attributes == null) { + return EMPTY_ARRAY; + } // Find attributes that contain parameter annotation data - final List accumulatedAnnotations = new ArrayList<>(attrs.length); - for (final Attribute attribute : attrs) { + final List accumulatedAnnotations = new ArrayList<>(attributes.length); + for (final Attribute attribute : attributes) { if (attribute instanceof ParameterAnnotations) { final ParameterAnnotations runtimeAnnotations = (ParameterAnnotations) attribute; - Collections.addAll(accumulatedAnnotations, runtimeAnnotations.getParameterAnnotationEntries()); + final ParameterAnnotationEntry[] parameterAnnotationEntries = runtimeAnnotations.getParameterAnnotationEntries(); + if (parameterAnnotationEntries != null) { + Collections.addAll(accumulatedAnnotations, parameterAnnotationEntries); + } } } - return accumulatedAnnotations.toArray(ParameterAnnotationEntry.EMPTY_ARRAY); + return accumulatedAnnotations.toArray(EMPTY_ARRAY); } private final AnnotationEntry[] annotationTable; /** - * Construct object from input stream. + * Constructs object from input stream. * * @param input Input stream * @throws IOException if an I/O error occurs. diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotations.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotations.java index 4817793120f..1e056b5d4f8 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotations.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/ParameterAnnotations.java @@ -34,10 +34,14 @@ import java.util.stream.Stream; */ public abstract class ParameterAnnotations extends Attribute implements Iterable { + private static final ParameterAnnotationEntry[] EMPTY_ARRAY = {}; + /** Table of parameter annotations */ private ParameterAnnotationEntry[] parameterAnnotationTable; /** + * Constructs a new instance. + * * @param parameterAnnotationType the subclass type of the parameter annotation * @param nameIndex Index pointing to the name Code * @param length Content length in bytes @@ -55,6 +59,8 @@ public abstract class ParameterAnnotations extends Attribute implements Iterable } /** + * Constructs a new instance. + * * @param parameterAnnotationType the subclass type of the parameter annotation * @param nameIndex Index pointing to the name Code * @param length Content length in bytes @@ -120,6 +126,6 @@ public abstract class ParameterAnnotations extends Attribute implements Iterable * @param parameterAnnotationTable the entries to set in this parameter annotation */ public final void setParameterAnnotationTable(final ParameterAnnotationEntry[] parameterAnnotationTable) { - this.parameterAnnotationTable = parameterAnnotationTable; + this.parameterAnnotationTable = parameterAnnotationTable != null ? parameterAnnotationTable : EMPTY_ARRAY; } } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Record.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Record.java new file mode 100644 index 00000000000..f59cfa37ca4 --- /dev/null +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Record.java @@ -0,0 +1,153 @@ +/* + * reserved comment block + * DO NOT REMOVE OR ALTER! + */ +/* + * 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 com.sun.org.apache.bcel.internal.classfile; + +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; + +import com.sun.org.apache.bcel.internal.Const; +import com.sun.org.apache.bcel.internal.util.Args; + +/** + * Extends {@link Attribute} and records the classes and + * interfaces that are authorized to claim membership in the nest hosted by the + * current class or interface. There may be at most one Record attribute in a + * ClassFile structure. + * + * @see Attribute + * @since 6.9.0 + */ +public final class Record extends Attribute { + + private static final RecordComponentInfo[] EMPTY_RCI_ARRAY = {}; + + private static RecordComponentInfo[] readComponents(final DataInput input, final ConstantPool constantPool) + throws IOException { + final int classCount = input.readUnsignedShort(); + final RecordComponentInfo[] components = new RecordComponentInfo[classCount]; + for (int i = 0; i < classCount; i++) { + components[i] = new RecordComponentInfo(input, constantPool); + } + return components; + } + + private RecordComponentInfo[] components; + + /** + * Constructs object from input stream. + * + * @param nameIndex Index in constant pool + * @param length Content length in bytes + * @param input Input stream + * @param constantPool Array of constants + * @throws IOException if an I/O error occurs. + */ + Record(final int nameIndex, final int length, final DataInput input, final ConstantPool constantPool) + throws IOException { + this(nameIndex, length, readComponents(input, constantPool), constantPool); + } + + /** + * Constructs a new instance using components. + * + * @param nameIndex Index in constant pool + * @param length Content length in bytes + * @param classes Array of Record Component Info elements + * @param constantPool Array of constants + */ + public Record(final int nameIndex, final int length, final RecordComponentInfo[] classes, + final ConstantPool constantPool) { + super(Const.ATTR_RECORD, nameIndex, length, constantPool); + this.components = classes != null ? classes : EMPTY_RCI_ARRAY; + Args.requireU2(this.components.length, "attributes.length"); + } + + /** + * Called by objects that are traversing the nodes of the tree implicitly + * defined by the contents of a Java class. For example, the hierarchy of methods, + * fields, attributes, etc. spawns a tree of objects. + * + * @param v Visitor object + */ + @Override + public void accept(final Visitor v) { + v.visitRecord(this); + } + + /** + * Copies this instance and its components. + * + * @return a deep copy of this instance and its components. + */ + @Override + public Attribute copy(final ConstantPool constantPool) { + final Record c = (Record) clone(); + if (components.length > 0) { + c.components = components.clone(); + } + c.setConstantPool(constantPool); + return c; + } + + /** + * Dumps this instance into a file stream in binary format. + * + * @param file output stream. + * @throws IOException if an I/O error occurs. + */ + @Override + public void dump(final DataOutputStream file) throws IOException { + super.dump(file); + file.writeShort(components.length); + for (final RecordComponentInfo component : components) { + component.dump(file); + } + } + + /** + * Gets all the record components. + * + * @return array of Record Component Info elements. + */ + public RecordComponentInfo[] getComponents() { + return components; + } + + /** + * Converts this instance to a String suitable for debugging. + * + * @return String a String suitable for debugging. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("Record("); + buf.append(components.length); + buf.append("):\n"); + for (final RecordComponentInfo component : components) { + buf.append(" ").append(component.toString()).append("\n"); + } + return buf.substring(0, buf.length() - 1); // remove the last newline + } + +} diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RecordComponentInfo.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RecordComponentInfo.java new file mode 100644 index 00000000000..3679647d409 --- /dev/null +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RecordComponentInfo.java @@ -0,0 +1,139 @@ +/* + * reserved comment block + * DO NOT REMOVE OR ALTER! + */ +/* + * 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 com.sun.org.apache.bcel.internal.classfile; + +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; + +import com.sun.org.apache.bcel.internal.Const; + +/** + * Record component info from a record. Instances from this class maps + * every component from a given record. + * + * @see + * The Java Virtual Machine Specification, Java SE 14 Edition, Records (preview) + * @since 6.9.0 + */ +public class RecordComponentInfo implements Node { + + private final int index; + private final int descriptorIndex; + private final Attribute[] attributes; + private final ConstantPool constantPool; + + /** + * Constructs a new instance from an input stream. + * + * @param input Input stream + * @param constantPool Array of constants + * @throws IOException if an I/O error occurs. + */ + public RecordComponentInfo(final DataInput input, final ConstantPool constantPool) throws IOException { + this.index = input.readUnsignedShort(); + this.descriptorIndex = input.readUnsignedShort(); + final int attributesCount = input.readUnsignedShort(); + this.attributes = new Attribute[attributesCount]; + for (int j = 0; j < attributesCount; j++) { + attributes[j] = Attribute.readAttribute(input, constantPool); + } + this.constantPool = constantPool; + } + + @Override + public void accept(final Visitor v) { + v.visitRecordComponent(this); + } + + /** + * Dumps contents into a file stream in binary format. + * + * @param file Output file stream + * @throws IOException if an I/O error occurs. + */ + public void dump(final DataOutputStream file) throws IOException { + file.writeShort(index); + file.writeShort(descriptorIndex); + file.writeShort(attributes.length); + for (final Attribute attribute : attributes) { + attribute.dump(file); + } + } + + /** + * Gets all attributes. + * + * @return all attributes. + */ + public Attribute[] getAttributes() { + return attributes; + } + + /** + * Gets the constant pool. + * + * @return Constant pool. + */ + public ConstantPool getConstantPool() { + return constantPool; + } + + /** + * Gets the description index. + * + * @return index in constant pool of this record component descriptor. + */ + public int getDescriptorIndex() { + return descriptorIndex; + } + + /** + * Gets the name index. + * + * @return index in constant pool of this record component name. + */ + public int getIndex() { + return index; + } + + /** + * Converts this instance to a String suitable for debugging. + * + * @return a String suitable for debugging. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("RecordComponentInfo("); + buf.append(constantPool.getConstantString(index, Const.CONSTANT_Utf8)); + buf.append(","); + buf.append(constantPool.getConstantString(descriptorIndex, Const.CONSTANT_Utf8)); + buf.append(","); + buf.append(attributes.length); + buf.append("):\n"); + for (final Attribute attribute : attributes) { + buf.append(" ").append(attribute.toString()).append("\n"); + } + return buf.substring(0, buf.length() - 1); // remove the last newline + } + +} diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleAnnotations.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleAnnotations.java index 7afb8719559..7a7c539f15a 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleAnnotations.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleAnnotations.java @@ -28,13 +28,15 @@ import java.io.IOException; import com.sun.org.apache.bcel.internal.Const; /** - * represents an annotation that is represented in the class file but is not provided to the JVM. + * An annotation that is represented in the class file but is not provided to the JVM. * * @since 6.0 */ public class RuntimeInvisibleAnnotations extends Annotations { /** + * Constructs a new instance. + * * @param nameIndex Index pointing to the name Code * @param length Content length in bytes * @param input Input stream @@ -46,7 +48,9 @@ public class RuntimeInvisibleAnnotations extends Annotations { } /** - * @return deep copy of this attribute + * Creates a deep copy of this attribute. + * + * @return deep copy of this attribute. */ @Override public Attribute copy(final ConstantPool constantPool) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleParameterAnnotations.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleParameterAnnotations.java index e4c3276f968..3d50ce16d40 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleParameterAnnotations.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeInvisibleParameterAnnotations.java @@ -34,6 +34,8 @@ import com.sun.org.apache.bcel.internal.Const; public class RuntimeInvisibleParameterAnnotations extends ParameterAnnotations { /** + * Constructs a new instance. + * * @param nameIndex Index pointing to the name Code * @param length Content length in bytes * @param input Input stream diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleAnnotations.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleAnnotations.java index c91c77387b9..4bf8e6f7197 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleAnnotations.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleAnnotations.java @@ -28,13 +28,15 @@ import java.io.IOException; import com.sun.org.apache.bcel.internal.Const; /** - * represents an annotation that is represented in the class file and is provided to the JVM. + * An annotation that is represented in the class file and is provided to the JVM. * * @since 6.0 */ public class RuntimeVisibleAnnotations extends Annotations { /** + * Constructs a new instance. + * * @param nameIndex Index pointing to the name Code * @param length Content length in bytes * @param input Input stream @@ -46,7 +48,9 @@ public class RuntimeVisibleAnnotations extends Annotations { } /** - * @return deep copy of this attribute + * Creates a deep copy of this attribute. + * + * @return deep copy of this attribute. */ @Override public Attribute copy(final ConstantPool constantPool) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleParameterAnnotations.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleParameterAnnotations.java index 7e5d7eaaca3..ab5355235f6 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleParameterAnnotations.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/RuntimeVisibleParameterAnnotations.java @@ -34,6 +34,8 @@ import com.sun.org.apache.bcel.internal.Const; public class RuntimeVisibleParameterAnnotations extends ParameterAnnotations { /** + * Constructs a new instance. + * * @param nameIndex Index pointing to the name Code * @param length Content length in bytes * @param input Input stream diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Signature.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Signature.java index 4f5d3a341b3..2161bbcb6ec 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Signature.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Signature.java @@ -110,7 +110,7 @@ public final class Signature extends Attribute { if ((ch = in.read()) == -1) { throw new IllegalArgumentException("Illegal signature: " + in.getData() + " no ident, reaching EOF"); } - // System.out.println("return from ident:" + (char)ch); + // System.out.println("return from ident:" + (char) ch); if (!identStart(ch)) { final StringBuilder buf2 = new StringBuilder(); int count = 1; @@ -128,7 +128,7 @@ public final class Signature extends Attribute { buf.append(buf2); ch = in.read(); in.unread(); - // System.out.println("so far:" + buf2 + ":next:" +(char)ch); + // System.out.println("so far:" + buf2 + ":next:" +(char) ch); } else { for (int i = 0; i < count; i++) { in.unread(); @@ -141,10 +141,10 @@ public final class Signature extends Attribute { do { buf2.append((char) ch); ch = in.read(); - // System.out.println("within ident:"+ (char)ch); + // System.out.println("within ident:"+ (char) ch); } while (ch != -1 && (Character.isJavaIdentifierPart((char) ch) || ch == '/')); buf.append(Utility.pathToPackage(buf2.toString())); - // System.out.println("regular return ident:"+ (char)ch + ":" + buf2); + // System.out.println("regular return ident:"+ (char) ch + ":" + buf2); if (ch != -1) { in.unread(); } @@ -160,7 +160,7 @@ public final class Signature extends Attribute { private int signatureIndex; /** - * Construct object from file stream. + * Constructs object from file stream. * * @param nameIndex Index in constant pool to CONSTANT_Utf8 * @param length Content length in bytes diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SimpleElementValue.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SimpleElementValue.java index 5e4e98d94df..e3e1cf40031 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SimpleElementValue.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SimpleElementValue.java @@ -54,7 +54,7 @@ public class SimpleElementValue extends ElementValue { dos.writeShort(getIndex()); break; default: - throw new ClassFormatException("SimpleElementValue doesnt know how to write out type " + type); + throw new ClassFormatException("SimpleElementValue doesn't know how to write out type " + type); } } @@ -67,7 +67,7 @@ public class SimpleElementValue extends ElementValue { public boolean getValueBoolean() { if (super.getType() != PRIMITIVE_BOOLEAN) { - throw new IllegalStateException("Dont call getValueBoolean() on a non BOOLEAN ElementValue"); + throw new IllegalStateException("Don't call getValueBoolean() on a non BOOLEAN ElementValue"); } final ConstantInteger bo = (ConstantInteger) super.getConstantPool().getConstant(getIndex()); return bo.getBytes() != 0; @@ -75,21 +75,21 @@ public class SimpleElementValue extends ElementValue { public byte getValueByte() { if (super.getType() != PRIMITIVE_BYTE) { - throw new IllegalStateException("Dont call getValueByte() on a non BYTE ElementValue"); + throw new IllegalStateException("Don't call getValueByte() on a non BYTE ElementValue"); } return (byte) super.getConstantPool().getConstantInteger(getIndex()).getBytes(); } public char getValueChar() { if (super.getType() != PRIMITIVE_CHAR) { - throw new IllegalStateException("Dont call getValueChar() on a non CHAR ElementValue"); + throw new IllegalStateException("Don't call getValueChar() on a non CHAR ElementValue"); } return (char) super.getConstantPool().getConstantInteger(getIndex()).getBytes(); } public double getValueDouble() { if (super.getType() != PRIMITIVE_DOUBLE) { - throw new IllegalStateException("Dont call getValueDouble() on a non DOUBLE ElementValue"); + throw new IllegalStateException("Don't call getValueDouble() on a non DOUBLE ElementValue"); } final ConstantDouble d = (ConstantDouble) super.getConstantPool().getConstant(getIndex()); return d.getBytes(); @@ -97,7 +97,7 @@ public class SimpleElementValue extends ElementValue { public float getValueFloat() { if (super.getType() != PRIMITIVE_FLOAT) { - throw new IllegalStateException("Dont call getValueFloat() on a non FLOAT ElementValue"); + throw new IllegalStateException("Don't call getValueFloat() on a non FLOAT ElementValue"); } final ConstantFloat f = (ConstantFloat) super.getConstantPool().getConstant(getIndex()); return f.getBytes(); @@ -105,14 +105,14 @@ public class SimpleElementValue extends ElementValue { public int getValueInt() { if (super.getType() != PRIMITIVE_INT) { - throw new IllegalStateException("Dont call getValueInt() on a non INT ElementValue"); + throw new IllegalStateException("Don't call getValueInt() on a non INT ElementValue"); } return super.getConstantPool().getConstantInteger(getIndex()).getBytes(); } public long getValueLong() { if (super.getType() != PRIMITIVE_LONG) { - throw new IllegalStateException("Dont call getValueLong() on a non LONG ElementValue"); + throw new IllegalStateException("Don't call getValueLong() on a non LONG ElementValue"); } final ConstantLong j = (ConstantLong) super.getConstantPool().getConstant(getIndex()); return j.getBytes(); @@ -120,7 +120,7 @@ public class SimpleElementValue extends ElementValue { public short getValueShort() { if (super.getType() != PRIMITIVE_SHORT) { - throw new IllegalStateException("Dont call getValueShort() on a non SHORT ElementValue"); + throw new IllegalStateException("Don't call getValueShort() on a non SHORT ElementValue"); } final ConstantInteger s = (ConstantInteger) super.getConstantPool().getConstant(getIndex()); return (short) s.getBytes(); @@ -128,7 +128,7 @@ public class SimpleElementValue extends ElementValue { public String getValueString() { if (super.getType() != STRING) { - throw new IllegalStateException("Dont call getValueString() on a non STRING ElementValue"); + throw new IllegalStateException("Don't call getValueString() on a non STRING ElementValue"); } return super.getConstantPool().getConstantUtf8(getIndex()).getBytes(); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SourceFile.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SourceFile.java index e9ceed21957..bfa9cbf8fcb 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SourceFile.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/SourceFile.java @@ -40,7 +40,7 @@ public final class SourceFile extends Attribute { private int sourceFileIndex; /** - * Construct object from input stream. + * Constructs object from input stream. * * @param nameIndex Index in constant pool to CONSTANT_Utf8 * @param length Content length in bytes diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMap.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMap.java index 1f8ce5ea410..317638e6b2d 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMap.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -30,8 +30,8 @@ import com.sun.org.apache.bcel.internal.util.Args; /** * This class represents a stack map attribute used for preverification of Java classes for the - * Java 2 Micro Edition (J2ME). This attribute is used by the - * KVM and contained within the Code attribute of a method. See CLDC + * Java 2 Micro Edition (J2ME). This attribute is used by the + * KVM and contained within the Code attribute of a method. See CLDC * specification 5.3.1.2 * *
        @@ -46,14 +46,14 @@ import com.sun.org.apache.bcel.internal.util.Args;
          * @see Code
          * @see StackMapEntry
          * @see StackMapType
        - * @LastModified: Oct 2020
        + * @LastModified: Sept 2025
          */
         public final class StackMap extends Attribute {
         
             private StackMapEntry[] table; // Table of stack map entries
         
             /**
        -     * Construct object from input stream.
        +     * Constructs object from input stream.
              *
              * @param nameIndex Index of name
              * @param length Content length in bytes
        diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapEntry.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapEntry.java
        index 110e30392ab..015083dd066 100644
        --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapEntry.java
        +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapEntry.java
        @@ -59,7 +59,7 @@ public final class StackMapEntry implements Node, Cloneable {
             private ConstantPool constantPool;
         
             /**
        -     * Construct object from input stream.
        +     * Constructs object from input stream.
              *
              * @param dataInput Input stream
              * @throws IOException if an I/O error occurs.
        @@ -75,9 +75,7 @@ public final class StackMapEntry implements Node, Cloneable {
                 } else if (frameType == Const.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) {
                     byteCodeOffset = dataInput.readUnsignedShort();
                     typesOfStackItems = new StackMapType[] { new StackMapType(dataInput, constantPool) };
        -        } else if (frameType >= Const.CHOP_FRAME && frameType <= Const.CHOP_FRAME_MAX) {
        -            byteCodeOffset = dataInput.readUnsignedShort();
        -        } else if (frameType == Const.SAME_FRAME_EXTENDED) {
        +        } else if (frameType >= Const.CHOP_FRAME && frameType <= Const.CHOP_FRAME_MAX || frameType == Const.SAME_FRAME_EXTENDED) {
                     byteCodeOffset = dataInput.readUnsignedShort();
                 } else if (frameType >= Const.APPEND_FRAME && frameType <= Const.APPEND_FRAME_MAX) {
                     byteCodeOffset = dataInput.readUnsignedShort();
        @@ -167,7 +165,7 @@ public final class StackMapEntry implements Node, Cloneable {
                 try {
                     e = (StackMapEntry) clone();
                 } catch (final CloneNotSupportedException ex) {
        -            throw new Error("Clone Not Supported");
        +            throw new UnsupportedOperationException("Clone Not Supported", ex);
                 }
         
                 e.typesOfLocals = new StackMapType[typesOfLocals.length];
        @@ -190,9 +188,7 @@ public final class StackMapEntry implements Node, Cloneable {
                 } else if (frameType == Const.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) {
                     file.writeShort(byteCodeOffset);
                     typesOfStackItems[0].dump(file);
        -        } else if (frameType >= Const.CHOP_FRAME && frameType <= Const.CHOP_FRAME_MAX) {
        -            file.writeShort(byteCodeOffset);
        -        } else if (frameType == Const.SAME_FRAME_EXTENDED) {
        +        } else if (frameType >= Const.CHOP_FRAME && frameType <= Const.CHOP_FRAME_MAX || frameType == Const.SAME_FRAME_EXTENDED) {
                     file.writeShort(byteCodeOffset);
                 } else if (frameType >= Const.APPEND_FRAME && frameType <= Const.APPEND_FRAME_MAX) {
                     file.writeShort(byteCodeOffset);
        @@ -232,7 +228,6 @@ public final class StackMapEntry implements Node, Cloneable {
         
             /**
              * Calculate stack map entry size
        -     *
              */
             int getMapEntrySize() {
                 if (frameType >= Const.SAME_FRAME && frameType <= Const.SAME_FRAME_MAX) {
        diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapType.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapType.java
        index 4575b31dfce..b93066d53b7 100644
        --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapType.java
        +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/StackMapType.java
        @@ -34,9 +34,9 @@ import com.sun.org.apache.bcel.internal.Const;
          * @see StackMap
          * @see Const
          */
        -public final class StackMapType implements Cloneable {
        +public final class StackMapType implements Node, Cloneable {
         
        -    public static final StackMapType[] EMPTY_ARRAY = {}; // must be public because BCELifier code generator writes calls to it
        +    public static final StackMapType[] EMPTY_ARRAY = {}; // BCELifier code generator writes calls to constructor translating null to EMPTY_ARRAY
         
             private byte type;
             private int index = -1; // Index to CONSTANT_Class or offset
        @@ -53,7 +53,7 @@ public final class StackMapType implements Cloneable {
             }
         
             /**
        -     * Construct object from file stream.
        +     * Constructs object from file stream.
              *
              * @param file Input stream
              * @throws IOException if an I/O error occurs.
        @@ -66,6 +66,18 @@ public final class StackMapType implements Cloneable {
                 this.constantPool = constantPool;
             }
         
        +    /**
        +     * Called by objects that are traversing the nodes of the tree implicitly defined by the contents of a Java class.
        +     * I.e., the hierarchy of methods, fields, attributes, etc. spawns a tree of objects.
        +     *
        +     * @param v Visitor object
        +     * @since 6.8.0
        +     */
        +    @Override
        +    public void accept(final Visitor v) {
        +        v.visitStackMapType(this);
        +    }
        +
             private byte checkType(final byte type) {
                 if (type < Const.ITEM_Bogus || type > Const.ITEM_NewObject) {
                     throw new ClassFormatException("Illegal type for StackMapType: " + type);
        @@ -98,6 +110,15 @@ public final class StackMapType implements Cloneable {
                 }
             }
         
        +    /**
        +     * Gets the class name of this StackMapType from the constant pool at index position.
        +     * @return the fully qualified name of the class for this StackMapType.
        +     * @since 6.8.0
        +     */
        +    public String getClassName() {
        +        return constantPool.constantToString(index, Const.CONSTANT_Class);
        +    }
        +
             /**
              * @return Constant pool used by this object.
              */
        @@ -129,7 +150,7 @@ public final class StackMapType implements Cloneable {
                     if (index < 0) {
                         return ", class=";
                     }
        -            return ", class=" + constantPool.constantToString(index, Const.CONSTANT_Class);
        +            return ", class=" + getClassName();
                 }
                 if (type == Const.ITEM_NewObject) {
                     return ", offset=" + index;
        diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Synthetic.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Synthetic.java
        index 3683fd6437e..c7fef8fcebc 100644
        --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Synthetic.java
        +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Synthetic.java
        @@ -52,7 +52,7 @@ public final class Synthetic extends Attribute {
             }
         
             /**
        -     * Construct object from input stream.
        +     * Constructs object from input stream.
              *
              * @param nameIndex Index in constant pool to CONSTANT_Utf8
              * @param length Content length in bytes
        diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Utility.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Utility.java
        index 12dbbe4828a..6967dcefd94 100644
        --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Utility.java
        +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Utility.java
        @@ -1,5 +1,5 @@
         /*
        - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved.
        + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
          */
         /*
          * Licensed to the Apache Software Foundation (ASF) under one or more
        @@ -43,7 +43,7 @@ import com.sun.org.apache.bcel.internal.util.ByteSequence;
         /**
          * Utility functions that do not really belong to any class in particular.
          *
        - * @LastModified: Feb 2023
        + * @LastModified: Sept 2025
          */
         // @since 6.0 methods are no longer final
         public abstract class Utility {
        @@ -51,7 +51,7 @@ public abstract class Utility {
             /**
              * Decode characters into bytes. Used by decode()
              */
        -    private static class JavaReader extends FilterReader {
        +    private static final class JavaReader extends FilterReader {
         
                 public JavaReader(final Reader in) {
                     super(in);
        @@ -88,10 +88,10 @@ public abstract class Utility {
             }
         
             /**
        -     * Encode bytes into valid java identifier characters. Used by
        +     * Encode bytes into valid Java identifier characters. Used by
              * encode()
              */
        -    private static class JavaWriter extends FilterWriter {
        +    private static final class JavaWriter extends FilterWriter {
         
                 public JavaWriter(final Writer out) {
                     super(out);
        @@ -437,7 +437,9 @@ public abstract class Utility {
                 case Const.NEW:
                 case Const.CHECKCAST:
                     buf.append("\t");
        -            //$FALL-THROUGH$
        +            index = bytes.readUnsignedShort();
        +            buf.append("\t<").append(constantPool.constantToString(index, Const.CONSTANT_Class)).append(">").append(verbose ? " (" + index + ")" : "");
        +            break;
                 case Const.INSTANCEOF:
                     index = bytes.readUnsignedShort();
                     buf.append("\t<").append(constantPool.constantToString(index, Const.CONSTANT_Class)).append(">").append(verbose ? " (" + index + ")" : "");
        @@ -864,7 +866,7 @@ public abstract class Utility {
                     // Skip any type arguments to read argument declarations between '(' and ')'
                     index = signature.indexOf('(') + 1;
                     if (index <= 0) {
        -                throw new ClassFormatException("Invalid method signature: " + signature);
        +                throw new InvalidMethodSignatureException(signature);
                     }
                     while (signature.charAt(index) != ')') {
                         vec.add(typeSignatureToString(signature.substring(index), chopit));
        @@ -872,7 +874,7 @@ public abstract class Utility {
                         index += unwrap(CONSUMER_CHARS); // update position
                     }
                 } catch (final StringIndexOutOfBoundsException e) { // Should never occur
        -            throw new ClassFormatException("Invalid method signature: " + signature, e);
        +            throw new InvalidMethodSignatureException(signature, e);
                 }
                 return vec.toArray(Const.EMPTY_STRING_ARRAY);
             }
        @@ -903,11 +905,11 @@ public abstract class Utility {
                     // Read return type after ')'
                     index = signature.lastIndexOf(')') + 1;
                     if (index <= 0) {
        -                throw new ClassFormatException("Invalid method signature: " + signature);
        +                throw new InvalidMethodSignatureException(signature);
                     }
                     type = typeSignatureToString(signature.substring(index), chopit);
                 } catch (final StringIndexOutOfBoundsException e) { // Should never occur
        -            throw new ClassFormatException("Invalid method signature: " + signature, e);
        +            throw new InvalidMethodSignatureException(signature, e);
                 }
                 return type;
             }
        @@ -959,7 +961,7 @@ public abstract class Utility {
                     // Skip any type arguments to read argument declarations between '(' and ')'
                     index = signature.indexOf('(') + 1;
                     if (index <= 0) {
        -                throw new ClassFormatException("Invalid method signature: " + signature);
        +                throw new InvalidMethodSignatureException(signature);
                     }
                     while (signature.charAt(index) != ')') {
                         final String paramType = typeSignatureToString(signature.substring(index), chopit);
        @@ -985,7 +987,7 @@ public abstract class Utility {
                     // Read return type after ')'
                     type = typeSignatureToString(signature.substring(index), chopit);
                 } catch (final StringIndexOutOfBoundsException e) { // Should never occur
        -            throw new ClassFormatException("Invalid method signature: " + signature, e);
        +            throw new InvalidMethodSignatureException(signature, e);
                 }
                 // ignore any throws information in the signature
                 if (buf.length() > 1) {
        @@ -1172,7 +1174,7 @@ public abstract class Utility {
                     type = typeParams + typeSignaturesToString(signature.substring(index), chopit, ')');
                     index += unwrap(CONSUMER_CHARS); // update position
                     // add return type
        -            type = type + typeSignatureToString(signature.substring(index), chopit);
        +            type += typeSignatureToString(signature.substring(index), chopit);
                     index += unwrap(CONSUMER_CHARS); // update position
                     // ignore any throws information in the signature
                     return type;
        @@ -1237,12 +1239,12 @@ public abstract class Utility {
                 int index;
                 try {
                     if (signature.charAt(0) != '(') {
        -                throw new ClassFormatException("Invalid method signature: " + signature);
        +                throw new InvalidMethodSignatureException(signature);
                     }
                     index = signature.lastIndexOf(')') + 1;
                     return typeOfSignature(signature.substring(index));
                 } catch (final StringIndexOutOfBoundsException e) {
        -            throw new ClassFormatException("Invalid method signature: " + signature, e);
        +            throw new InvalidMethodSignatureException(signature, e);
                 }
             }
         
        @@ -1286,10 +1288,10 @@ public abstract class Utility {
                     case '*':
                         return typeOfSignature(signature.substring(1));
                     default:
        -                throw new ClassFormatException("Invalid method signature: " + signature);
        +                throw new InvalidMethodSignatureException(signature);
                     }
                 } catch (final StringIndexOutOfBoundsException e) {
        -            throw new ClassFormatException("Invalid method signature: " + signature, e);
        +            throw new InvalidMethodSignatureException(signature, e);
                 }
             }
         
        @@ -1469,8 +1471,8 @@ public abstract class Utility {
                         } else {
                             type.append(typeSignatureToString(signature.substring(consumedChars), chopit));
                             // update our consumed count by the number of characters the for type argument
        -                    consumedChars = unwrap(Utility.CONSUMER_CHARS) + consumedChars;
        -                    wrap(Utility.CONSUMER_CHARS, consumedChars);
        +                    consumedChars = unwrap(CONSUMER_CHARS) + consumedChars;
        +                    wrap(CONSUMER_CHARS, consumedChars);
                         }
         
                         // are there more TypeArguments?
        @@ -1490,8 +1492,8 @@ public abstract class Utility {
                             } else {
                                 type.append(typeSignatureToString(signature.substring(consumedChars), chopit));
                                 // update our consumed count by the number of characters the for type argument
        -                        consumedChars = unwrap(Utility.CONSUMER_CHARS) + consumedChars;
        -                        wrap(Utility.CONSUMER_CHARS, consumedChars);
        +                        consumedChars = unwrap(CONSUMER_CHARS) + consumedChars;
        +                        wrap(CONSUMER_CHARS, consumedChars);
                             }
                         }
         
        @@ -1508,14 +1510,14 @@ public abstract class Utility {
                             // update our consumed count by the number of characters the for type argument
                             // note that this count includes the "L" we added, but that is ok
                             // as it accounts for the "." we didn't consume
        -                    consumedChars = unwrap(Utility.CONSUMER_CHARS) + consumedChars;
        -                    wrap(Utility.CONSUMER_CHARS, consumedChars);
        +                    consumedChars = unwrap(CONSUMER_CHARS) + consumedChars;
        +                    wrap(CONSUMER_CHARS, consumedChars);
                             return type.toString();
                         }
                         if (signature.charAt(consumedChars) != ';') {
                             throw new ClassFormatException("Invalid signature: " + signature);
                         }
        -                wrap(Utility.CONSUMER_CHARS, consumedChars + 1); // remove final ";"
        +                wrap(CONSUMER_CHARS, consumedChars + 1); // remove final ";"
                         return type.toString();
                     }
                     case 'S':
        @@ -1536,9 +1538,9 @@ public abstract class Utility {
                         // The rest of the string denotes a ''
                         type = typeSignatureToString(signature.substring(n), chopit);
                         // corrected concurrent private static field acess
        -                // Utility.consumed_chars += consumed_chars; is replaced by:
        -                final int temp = unwrap(Utility.CONSUMER_CHARS) + consumedChars;
        -                wrap(Utility.CONSUMER_CHARS, temp);
        +                // consumed_chars += consumed_chars; is replaced by:
        +                final int temp = unwrap(CONSUMER_CHARS) + consumedChars;
        +                wrap(CONSUMER_CHARS, temp);
                         return type + brackets.toString();
                     }
                     case 'V':
        @@ -1552,11 +1554,11 @@ public abstract class Utility {
             }
         
             private static int unwrap(final ThreadLocal tl) {
        -        return tl.get();
        +        return tl.get().intValue();
             }
         
             private static void wrap(final ThreadLocal tl, final int value) {
        -        tl.set(value);
        +        tl.set(Integer.valueOf(value));
             }
         
         }
        diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Visitor.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Visitor.java
        index 74cb8400d3e..1f6fe9c96ee 100644
        --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Visitor.java
        +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/Visitor.java
        @@ -217,11 +217,32 @@ public interface Visitor {
              */
             void visitParameterAnnotation(ParameterAnnotations obj);
         
        +
             /**
              * @since 6.0
              */
             void visitParameterAnnotationEntry(ParameterAnnotationEntry obj);
         
        +    /**
        +     * Visits a {@link Record} object.
        +     *
        +     * @param obj Record to visit
        +     * @since 6.9.0
        +     */
        +    default void visitRecord(final Record obj) {
        +        // empty
        +    }
        +
        +    /**
        +     * Visits a {@link RecordComponentInfo} object.
        +     *
        +     * @param record component to visit
        +     * @since 6.9.0
        +     */
        +    default void visitRecordComponent(final RecordComponentInfo record) {
        +     // noop
        +    }
        +
             void visitSignature(Signature obj);
         
             void visitSourceFile(SourceFile obj);
        @@ -230,7 +251,18 @@ public interface Visitor {
         
             void visitStackMapEntry(StackMapEntry obj);
         
        +    /**
        +     * Visits a {@link StackMapType} object.
        +     *
        +     * @param obj object to visit
        +     * @since 6.8.0
        +     */
        +    default void visitStackMapType(final StackMapType obj) {
        +      // empty
        +    }
        +
             void visitSynthetic(Synthetic obj);
         
             void visitUnknown(Unknown obj);
        +
         }
        diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/package-info.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/package-info.java
        new file mode 100644
        index 00000000000..4e8d383bcab
        --- /dev/null
        +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/classfile/package-info.java
        @@ -0,0 +1,25 @@
        +/*
        + * reserved comment block
        + * DO NOT REMOVE OR ALTER!
        + */
        +/*
        + * 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.
        + */
        +
        +/**
        + * Classes that describe the structure of a Java class file and a class file parser.
        + */
        +package com.sun.org.apache.bcel.internal.classfile;
        diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ARRAYLENGTH.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ARRAYLENGTH.java
        index 2d7188e9174..db756700085 100644
        --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ARRAYLENGTH.java
        +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ARRAYLENGTH.java
        @@ -1,5 +1,5 @@
         /*
        - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved.
        + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
          */
         /*
          * Licensed to the Apache Software Foundation (ASF) under one or more
        @@ -28,12 +28,12 @@ import com.sun.org.apache.bcel.internal.ExceptionConst;
          * 
          * Stack: ..., arrayref -> ..., length
          * 
        - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class ARRAYLENGTH extends Instruction implements ExceptionThrower, StackProducer, StackConsumer /* since 6.0 */ { /** - * Get length of array + * Gets length of array */ public ARRAYLENGTH() { super(com.sun.org.apache.bcel.internal.Const.ARRAYLENGTH, (short) 1); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ATHROW.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ATHROW.java index bb2e953f850..07171fefde1 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ATHROW.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ATHROW.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -28,8 +28,10 @@ import com.sun.org.apache.bcel.internal.ExceptionConst; *
          * Stack: ..., objectref -> objectref
          * 
        + * + * @LastModified: Sept 2025 */ -public class ATHROW extends Instruction implements UnconditionalBranch, ExceptionThrower { +public class ATHROW extends Instruction implements UnconditionalBranch, ExceptionThrower, StackConsumer { /** * Throw exception @@ -48,6 +50,7 @@ public class ATHROW extends Instruction implements UnconditionalBranch, Exceptio public void accept(final Visitor v) { v.visitUnconditionalBranch(this); v.visitExceptionThrower(this); + v.visitStackConsumer(this); v.visitATHROW(this); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/AnnotationEntryGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/AnnotationEntryGen.java index ea01a837175..0fad6ea7389 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/AnnotationEntryGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/AnnotationEntryGen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -28,6 +28,7 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import com.sun.org.apache.bcel.internal.classfile.AnnotationEntry; import com.sun.org.apache.bcel.internal.classfile.Attribute; @@ -37,10 +38,11 @@ import com.sun.org.apache.bcel.internal.classfile.RuntimeInvisibleAnnotations; import com.sun.org.apache.bcel.internal.classfile.RuntimeInvisibleParameterAnnotations; import com.sun.org.apache.bcel.internal.classfile.RuntimeVisibleAnnotations; import com.sun.org.apache.bcel.internal.classfile.RuntimeVisibleParameterAnnotations; +import jdk.xml.internal.Utils; /** * @since 6.0 - * @LastModified: Jan 2020 + * @LastModified: Sept 2025 */ public class AnnotationEntryGen { @@ -53,7 +55,7 @@ public class AnnotationEntryGen { * @param annotationEntryGens An array of AnnotationGen objects */ static Attribute[] getAnnotationAttributes(final ConstantPoolGen cp, final AnnotationEntryGen[] annotationEntryGens) { - if (annotationEntryGens.length == 0) { + if (annotationEntryGens == null && annotationEntryGens.length == 0) { return Attribute.EMPTY_ARRAY; } @@ -255,11 +257,7 @@ public class AnnotationEntryGen { } private List copyValues(final ElementValuePair[] in, final ConstantPoolGen cpool, final boolean copyPoolEntries) { - final List out = new ArrayList<>(); - for (final ElementValuePair nvp : in) { - out.add(new ElementValuePairGen(nvp, cpool, copyPoolEntries)); - } - return out; + return Utils.streamOfIfNonNull(in).map(nvp -> new ElementValuePairGen(nvp, cpool, copyPoolEntries)).collect(Collectors.toList()); } public void dump(final DataOutputStream dos) throws IOException { @@ -286,18 +284,20 @@ public class AnnotationEntryGen { } public final String getTypeName() { - return getTypeSignature();// BCELBUG: Should I use this instead? + return getTypeSignature(); // BCELBUG: Should I use this instead? // Utility.signatureToString(getTypeSignature()); } public final String getTypeSignature() { - // ConstantClass c = (ConstantClass)cpool.getConstant(typeIndex); + // ConstantClass c = (ConstantClass) cpool.getConstant(typeIndex); final ConstantUtf8 utf8 = (ConstantUtf8) cpool.getConstant(typeIndex/* c.getNameIndex() */); return utf8.getBytes(); } /** - * Returns list of ElementNameValuePair objects + * Returns list of ElementNameValuePair objects. + * + * @return list of ElementNameValuePair objects. */ public List getValues() { return evs; diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayElementValueGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayElementValueGen.java index 71374877efe..59b774a9afc 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayElementValueGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayElementValueGen.java @@ -1,6 +1,5 @@ /* - * reserved comment block - * DO NOT REMOVE OR ALTER! + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -25,12 +24,15 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import com.sun.org.apache.bcel.internal.classfile.ArrayElementValue; import com.sun.org.apache.bcel.internal.classfile.ElementValue; +import jdk.xml.internal.Utils; /** * @since 6.0 + * @LastModified: Sept 2025 */ public class ArrayElementValueGen extends ElementValueGen { // J5TODO: Should we make this an array or a list? A list would be easier to @@ -46,7 +48,7 @@ public class ArrayElementValueGen extends ElementValueGen { evalues = new ArrayList<>(); final ElementValue[] in = value.getElementValuesArray(); for (final ElementValue element : in) { - evalues.add(ElementValueGen.copy(element, cpool, copyPoolEntries)); + evalues.add(copy(element, cpool, copyPoolEntries)); } } @@ -55,15 +57,12 @@ public class ArrayElementValueGen extends ElementValueGen { evalues = new ArrayList<>(); } - public ArrayElementValueGen(final int type, final ElementValue[] datums, final ConstantPoolGen cpool) { + public ArrayElementValueGen(final int type, final ElementValue[] elementValues, final ConstantPoolGen cpool) { super(type, cpool); if (type != ARRAY) { throw new IllegalArgumentException("Only element values of type array can be built with this ctor - type specified: " + type); } - this.evalues = new ArrayList<>(); - for (final ElementValue datum : datums) { - evalues.add(ElementValueGen.copy(datum, cpool, true)); - } + this.evalues = Utils.streamOfIfNonNull(elementValues).map(e -> copy(e, cpool, true)).collect(Collectors.toList()); } public void addElement(final ElementValueGen gen) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayType.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayType.java index 138999ebe90..78a676e875e 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayType.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ArrayType.java @@ -1,6 +1,5 @@ /* - * reserved comment block - * DO NOT REMOVE OR ALTER! + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -24,6 +23,8 @@ import com.sun.org.apache.bcel.internal.Const; /** * Denotes array type, such as int[][] + * + * @LastModified: Sept 2025 */ public final class ArrayType extends ReferenceType { @@ -43,7 +44,7 @@ public final class ArrayType extends ReferenceType { /** * Convenience constructor for reference array type, e.g. Object[] * - * @param className complete name of class (java.lang.String, e.g.) + * @param className complete name of class ({@link String}, for example) * @param dimensions array dimensions */ public ArrayType(final String className, final int dimensions) { @@ -56,6 +57,7 @@ public final class ArrayType extends ReferenceType { * @param type type of array (may be an array itself) * @param dimensions array dimensions */ + @SuppressWarnings("deprecation") //signature public ArrayType(final Type type, final int dimensions) { super(Const.T_ARRAY, ""); if (dimensions < 1 || dimensions > Const.MAX_BYTE) { @@ -79,7 +81,7 @@ public final class ArrayType extends ReferenceType { buf.append('['); } buf.append(basicType.getSignature()); - super.setSignature(buf.toString()); + this.signature = buf.toString(); } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/BranchHandle.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/BranchHandle.java index 91ab9ac5463..f489f9a7658 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/BranchHandle.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/BranchHandle.java @@ -63,7 +63,7 @@ public final class BranchHandle extends InstructionHandle { } /** - * Set new contents. Old instruction is disposed and may not be used anymore. + * Sets new contents. Old instruction is disposed and may not be used anymore. */ @Override // This is only done in order to apply the additional type check; could be merged with super impl. public void setInstruction(final Instruction i) { // TODO could be package-protected? diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CPInstruction.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CPInstruction.java index 1dfd244141a..ff45e5cde93 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CPInstruction.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CPInstruction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -35,7 +35,7 @@ import com.sun.org.apache.bcel.internal.util.ByteSequence; * @see LDC * @see INVOKEVIRTUAL * - * @LastModified: Jan 2020 + * @LastModified: Sept 2025 */ public abstract class CPInstruction extends Instruction implements TypedInstruction, IndexedInstruction { @@ -104,7 +104,7 @@ public abstract class CPInstruction extends Instruction implements TypedInstruct } /** - * Set the index to constant pool. + * Sets the index to constant pool. * * @param index in constant pool. */ diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassElementValueGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassElementValueGen.java index 1c1c032dbd5..8e024eebaa4 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassElementValueGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassElementValueGen.java @@ -48,12 +48,12 @@ public class ClassElementValueGen extends ElementValueGen { } protected ClassElementValueGen(final int typeIdx, final ConstantPoolGen cpool) { - super(ElementValueGen.CLASS, cpool); + super(CLASS, cpool); this.idx = typeIdx; } public ClassElementValueGen(final ObjectType t, final ConstantPoolGen cpool) { - super(ElementValueGen.CLASS, cpool); + super(CLASS, cpool); // this.idx = cpool.addClass(t); idx = cpool.addUtf8(t.getSignature()); } @@ -67,9 +67,9 @@ public class ClassElementValueGen extends ElementValueGen { public String getClassString() { final ConstantUtf8 cu8 = (ConstantUtf8) getConstantPool().getConstant(idx); return cu8.getBytes(); - // ConstantClass c = (ConstantClass)getConstantPool().getConstant(idx); + // ConstantClass c = (ConstantClass) getConstantPool().getConstant(idx); // ConstantUtf8 utf8 = - // (ConstantUtf8)getConstantPool().getConstant(c.getNameIndex()); + // (ConstantUtf8) getConstantPool().getConstant(c.getNameIndex()); // return utf8.getBytes(); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassGen.java index acaf4519567..debf930fe90 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ClassGen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -40,40 +40,37 @@ import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.BCELComparator; /** - * Template class for building up a java class. May be initialized with an existing java class (file). + * Template class for building up a java class. May be initialized with an existing Java class (file). * * @see JavaClass - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class ClassGen extends AccessFlags implements Cloneable { - private static BCELComparator bcelComparator = new BCELComparator() { + private static BCELComparator bcelComparator = new BCELComparator() { @Override - public boolean equals(final Object o1, final Object o2) { - final ClassGen THIS = (ClassGen) o1; - final ClassGen THAT = (ClassGen) o2; - return Objects.equals(THIS.getClassName(), THAT.getClassName()); + public boolean equals(final ClassGen a, final ClassGen b) { + return a == b || a != null && b != null && Objects.equals(a.getClassName(), b.getClassName()); } @Override - public int hashCode(final Object o) { - final ClassGen THIS = (ClassGen) o; - return THIS.getClassName().hashCode(); + public int hashCode(final ClassGen o) { + return o != null ? Objects.hashCode(o.getClassName()) : 0; } }; /** * @return Comparison strategy object */ - public static BCELComparator getComparator() { + public static BCELComparator getComparator() { return bcelComparator; } /** * @param comparator Comparison strategy object */ - public static void setComparator(final BCELComparator comparator) { + public static void setComparator(final BCELComparator comparator) { bcelComparator = comparator; } @@ -101,7 +98,7 @@ public class ClassGen extends AccessFlags implements Cloneable { private List observers; /** - * Initialize with existing class. + * Constructs a new instance from an existing class. * * @param clazz JavaClass object (e.g. read from file) */ @@ -118,15 +115,26 @@ public class ClassGen extends AccessFlags implements Cloneable { final Attribute[] attributes = clazz.getAttributes(); // J5TODO: Could make unpacking lazy, done on first reference final AnnotationEntryGen[] annotations = unpackAnnotations(attributes); - Collections.addAll(interfaceList, clazz.getInterfaceNames()); - for (final Attribute attribute : attributes) { - if (!(attribute instanceof Annotations)) { - addAttribute(attribute); + final String[] interfaceNames = clazz.getInterfaceNames(); + if (interfaceNames != null) { + Collections.addAll(interfaceList, interfaceNames); + } + if (attributes != null) { + for (final Attribute attribute : attributes) { + if (!(attribute instanceof Annotations)) { + addAttribute(attribute); + } } } Collections.addAll(annotationList, annotations); - Collections.addAll(methodList, clazz.getMethods()); - Collections.addAll(fieldList, clazz.getFields()); + final Method[] methods = clazz.getMethods(); + if (methods != null) { + Collections.addAll(methodList, methods); + } + final Field[] fields = clazz.getFields(); + if (fields != null) { + Collections.addAll(fieldList, fields); + } } /** @@ -242,7 +250,7 @@ public class ClassGen extends AccessFlags implements Cloneable { try { return super.clone(); } catch (final CloneNotSupportedException e) { - throw new Error("Clone Not Supported"); // never happens + throw new UnsupportedOperationException("Clone Not Supported", e); // never happens } } @@ -282,7 +290,7 @@ public class ClassGen extends AccessFlags implements Cloneable { */ @Override public boolean equals(final Object obj) { - return bcelComparator.equals(this, obj); + return obj instanceof ClassGen && bcelComparator.equals(this, (ClassGen) obj); } // J5TODO: Should we make calling unpackAnnotations() lazy and put it in here? @@ -379,7 +387,7 @@ public class ClassGen extends AccessFlags implements Cloneable { } /** - * Return value as defined by given BCELComparator strategy. By default return the hashcode of the class name. + * Return value as defined by given BCELComparator strategy. By default return the hash code of the class name. * * @see Object#hashCode() */ @@ -478,7 +486,7 @@ public class ClassGen extends AccessFlags implements Cloneable { } /** - * Set major version number of class file, default value is 45 (JDK 1.1) + * Sets major version number of class file, default value is 45 (JDK 1.1) * * @param major major version number */ @@ -492,11 +500,13 @@ public class ClassGen extends AccessFlags implements Cloneable { public void setMethods(final Method[] methods) { methodList.clear(); - Collections.addAll(methodList, methods); + if (methods != null) { + Collections.addAll(methodList, methods); + } } /** - * Set minor version number of class file, default value is 3 (JDK 1.1) + * Sets minor version number of class file, default value is 3 (JDK 1.1) * * @param minor minor version number */ @@ -515,17 +525,19 @@ public class ClassGen extends AccessFlags implements Cloneable { } /** - * Look for attributes representing annotations and unpack them. + * Unpacks attributes representing annotations. */ - private AnnotationEntryGen[] unpackAnnotations(final Attribute[] attrs) { + private AnnotationEntryGen[] unpackAnnotations(final Attribute[] attributes) { final List annotationGenObjs = new ArrayList<>(); - for (final Attribute attr : attrs) { - if (attr instanceof RuntimeVisibleAnnotations) { - final RuntimeVisibleAnnotations rva = (RuntimeVisibleAnnotations) attr; - rva.forEach(a -> annotationGenObjs.add(new AnnotationEntryGen(a, getConstantPool(), false))); - } else if (attr instanceof RuntimeInvisibleAnnotations) { - final RuntimeInvisibleAnnotations ria = (RuntimeInvisibleAnnotations) attr; - ria.forEach(a -> annotationGenObjs.add(new AnnotationEntryGen(a, getConstantPool(), false))); + if (attributes != null) { + for (final Attribute attr : attributes) { + if (attr instanceof RuntimeVisibleAnnotations) { + final RuntimeVisibleAnnotations rva = (RuntimeVisibleAnnotations) attr; + rva.forEach(a -> annotationGenObjs.add(new AnnotationEntryGen(a, getConstantPool(), false))); + } else if (attr instanceof RuntimeInvisibleAnnotations) { + final RuntimeInvisibleAnnotations ria = (RuntimeInvisibleAnnotations) attr; + ria.forEach(a -> annotationGenObjs.add(new AnnotationEntryGen(a, getConstantPool(), false))); + } } } return annotationGenObjs.toArray(AnnotationEntryGen.EMPTY_ARRAY); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CodeExceptionGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CodeExceptionGen.java index fa660954d15..2518131ccee 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CodeExceptionGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/CodeExceptionGen.java @@ -63,7 +63,7 @@ public final class CodeExceptionGen implements InstructionTargeter, Cloneable { try { return super.clone(); } catch (final CloneNotSupportedException e) { - throw new Error("Clone Not Supported"); // never happens + throw new UnsupportedOperationException("Clone Not Supported", e); // never happens } } @@ -81,7 +81,7 @@ public final class CodeExceptionGen implements InstructionTargeter, Cloneable { } /** - * Get CodeException object.
        + * Gets CodeException object.
        * * This relies on that the instruction list has already been dumped to byte code or that the 'setPositions' methods * has been called for the instruction list. @@ -120,7 +120,7 @@ public final class CodeExceptionGen implements InstructionTargeter, Cloneable { } /* - * Set end of handler + * Sets end of handler * * @param endPc End of handled region (inclusive) */ @@ -130,7 +130,7 @@ public final class CodeExceptionGen implements InstructionTargeter, Cloneable { } /* - * Set handler code + * Sets handler code * * @param handlerPc Start of handler */ @@ -140,7 +140,7 @@ public final class CodeExceptionGen implements InstructionTargeter, Cloneable { } /* - * Set start of handler + * Sets start of handler * * @param startPc Start of handled region (inclusive) */ diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ElementValuePairGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ElementValuePairGen.java index bdc9c517a86..878f392b6b7 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ElementValuePairGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ElementValuePairGen.java @@ -44,7 +44,7 @@ public class ElementValuePairGen { // Could assert nvp.getNameString() points to the same thing as // constantPoolGen.getConstant(nvp.getNameIndex()) // if - // (!nvp.getNameString().equals(((ConstantUtf8)constantPoolGen.getConstant(nvp.getNameIndex())).getBytes())) + // (!nvp.getNameString().equals(((ConstantUtf8) constantPoolGen.getConstant(nvp.getNameIndex())).getBytes())) // { // throw new IllegalArgumentException("envp buggered"); // } @@ -86,7 +86,7 @@ public class ElementValuePairGen { } public final String getNameString() { - // ConstantString cu8 = (ConstantString)constantPoolGen.getConstant(nameIdx); + // ConstantString cu8 = (ConstantString) constantPoolGen.getConstant(nameIdx); return ((ConstantUtf8) constantPoolGen.getConstant(nameIdx)).getBytes(); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/EnumElementValueGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/EnumElementValueGen.java index 95ac794f2d7..95c00ed3813 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/EnumElementValueGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/EnumElementValueGen.java @@ -40,10 +40,8 @@ public class EnumElementValueGen extends ElementValueGen { public EnumElementValueGen(final EnumElementValue value, final ConstantPoolGen cpool, final boolean copyPoolEntries) { super(ENUM_CONSTANT, cpool); if (copyPoolEntries) { - typeIdx = cpool.addUtf8(value.getEnumTypeString());// was - // addClass(value.getEnumTypeString()); - valueIdx = cpool.addUtf8(value.getEnumValueString()); // was - // addString(value.getEnumValueString()); + typeIdx = cpool.addUtf8(value.getEnumTypeString()); // was addClass(value.getEnumTypeString()); + valueIdx = cpool.addUtf8(value.getEnumValueString()); // was addString(value.getEnumValueString()); } else { typeIdx = value.getTypeIndex(); valueIdx = value.getValueIndex(); @@ -55,7 +53,7 @@ public class EnumElementValueGen extends ElementValueGen { * This ctor is used for deserialization */ protected EnumElementValueGen(final int typeIdx, final int valueIdx, final ConstantPoolGen cpool) { - super(ElementValueGen.ENUM_CONSTANT, cpool); + super(ENUM_CONSTANT, cpool); if (super.getElementValueType() != ENUM_CONSTANT) { throw new IllegalArgumentException("Only element values of type enum can be built with this ctor - type specified: " + super.getElementValueType()); } @@ -64,9 +62,9 @@ public class EnumElementValueGen extends ElementValueGen { } public EnumElementValueGen(final ObjectType t, final String value, final ConstantPoolGen cpool) { - super(ElementValueGen.ENUM_CONSTANT, cpool); - typeIdx = cpool.addUtf8(t.getSignature());// was addClass(t); - valueIdx = cpool.addUtf8(value);// was addString(value); + super(ENUM_CONSTANT, cpool); + typeIdx = cpool.addUtf8(t.getSignature()); // was addClass(t); + valueIdx = cpool.addUtf8(value); // was addString(value); } @Override @@ -90,9 +88,9 @@ public class EnumElementValueGen extends ElementValueGen { public String getEnumTypeString() { // Constant cc = getConstantPool().getConstant(typeIdx); // ConstantClass cu8 = - // (ConstantClass)getConstantPool().getConstant(typeIdx); + // (ConstantClass) getConstantPool().getConstant(typeIdx); // return - // ((ConstantUtf8)getConstantPool().getConstant(cu8.getNameIndex())).getBytes(); + // ((ConstantUtf8) getConstantPool().getConstant(cu8.getNameIndex())).getBytes(); return ((ConstantUtf8) getConstantPool().getConstant(typeIdx)).getBytes(); // return Utility.signatureToString(cu8.getBytes()); } @@ -100,9 +98,9 @@ public class EnumElementValueGen extends ElementValueGen { public String getEnumValueString() { return ((ConstantUtf8) getConstantPool().getConstant(valueIdx)).getBytes(); // ConstantString cu8 = - // (ConstantString)getConstantPool().getConstant(valueIdx); + // (ConstantString) getConstantPool().getConstant(valueIdx); // return - // ((ConstantUtf8)getConstantPool().getConstant(cu8.getStringIndex())).getBytes(); + // ((ConstantUtf8) getConstantPool().getConstant(cu8.getStringIndex())).getBytes(); } public int getTypeIndex() { @@ -118,8 +116,8 @@ public class EnumElementValueGen extends ElementValueGen { final ConstantUtf8 cu8 = (ConstantUtf8) getConstantPool().getConstant(valueIdx); return cu8.getBytes(); // ConstantString cu8 = - // (ConstantString)getConstantPool().getConstant(valueIdx); + // (ConstantString) getConstantPool().getConstant(valueIdx); // return - // ((ConstantUtf8)getConstantPool().getConstant(cu8.getStringIndex())).getBytes(); + // ((ConstantUtf8) getConstantPool().getConstant(cu8.getStringIndex())).getBytes(); } } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ExceptionThrower.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ExceptionThrower.java index 86a0cad256c..2a488c85ede 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ExceptionThrower.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ExceptionThrower.java @@ -23,7 +23,7 @@ package com.sun.org.apache.bcel.internal.generic; /** * Denote an instruction that may throw a run-time or a linking exception (or both) during execution. This is not quite - * the truth as such; because all instructions may throw an java.lang.VirtualMachineError. These exceptions are omitted. + * the truth as such; because all instructions may throw a {@link VirtualMachineError}. These exceptions are omitted. * * The Lava Language Specification specifies exactly which RUN-TIME and which LINKING exceptions each * instruction may throw which is reflected by the implementers. Due to the structure of the JVM specification, it may diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGen.java index 9d1f4d41d93..30786370d29 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -40,37 +40,34 @@ import com.sun.org.apache.bcel.internal.util.BCELComparator; * to a field (which must of course be compatible with to the declared type). * * @see Field - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public class FieldGen extends FieldGenOrMethodGen { - private static BCELComparator bcelComparator = new BCELComparator() { + private static BCELComparator bcelComparator = new BCELComparator() { @Override - public boolean equals(final Object o1, final Object o2) { - final FieldGen THIS = (FieldGen) o1; - final FieldGen THAT = (FieldGen) o2; - return Objects.equals(THIS.getName(), THAT.getName()) && Objects.equals(THIS.getSignature(), THAT.getSignature()); + public boolean equals(final FieldGen a, final FieldGen b) { + return a == b || a != null && b != null && Objects.equals(a.getName(), b.getName()) && Objects.equals(a.getSignature(), b.getSignature()); } @Override - public int hashCode(final Object o) { - final FieldGen THIS = (FieldGen) o; - return THIS.getSignature().hashCode() ^ THIS.getName().hashCode(); + public int hashCode(final FieldGen o) { + return o != null ? Objects.hash(o.getSignature(), o.getName()) : 0; } }; /** - * @return Comparison strategy object + * @return Comparison strategy object. */ - public static BCELComparator getComparator() { + public static BCELComparator getComparator() { return bcelComparator; } /** - * @param comparator Comparison strategy object + * @param comparator Comparison strategy object. */ - public static void setComparator(final BCELComparator comparator) { + public static void setComparator(final BCELComparator comparator) { bcelComparator = comparator; } @@ -81,8 +78,8 @@ public class FieldGen extends FieldGenOrMethodGen { /** * Instantiate from existing field. * - * @param field Field object - * @param cp constant pool (must contain the same entries as the field's constant pool) + * @param field Field object. + * @param cp constant pool (must contain the same entries as the field's constant pool). */ public FieldGen(final Field field, final ConstantPoolGen cp) { this(field.getAccessFlags(), Type.getType(field.getSignature()), field.getName(), cp); @@ -187,11 +184,11 @@ public class FieldGen extends FieldGenOrMethodGen { */ @Override public boolean equals(final Object obj) { - return bcelComparator.equals(this, obj); + return obj instanceof FieldGen && bcelComparator.equals(this, (FieldGen) obj); } /** - * Get field object after having set up all necessary values. + * Gets field object after having set up all necessary values. */ public Field getField() { final String signature = getSignature(); @@ -207,10 +204,7 @@ public class FieldGen extends FieldGenOrMethodGen { } public String getInitValue() { - if (value != null) { - return value.toString(); - } - return null; + return Objects.toString(value, null); } @Override @@ -219,7 +213,7 @@ public class FieldGen extends FieldGenOrMethodGen { } /** - * Return value as defined by given BCELComparator strategy. By default return the hashcode of the field's name XOR + * Return value as defined by given BCELComparator strategy. By default return the hash code of the field's name XOR * signature. * * @see Object#hashCode() @@ -295,7 +289,7 @@ public class FieldGen extends FieldGenOrMethodGen { } /** - * Set (optional) initial value of field, otherwise it will be set to null/0/false by the JVM automatically. + * Sets (optional) initial value of field, otherwise it will be set to null/0/false by the JVM automatically. */ public void setInitValue(final String str) { checkType(ObjectType.getInstance("java.lang.String")); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGenOrMethodGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGenOrMethodGen.java index 6555392e9d4..bc1fbc8627e 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGenOrMethodGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldGenOrMethodGen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -30,7 +30,7 @@ import com.sun.org.apache.bcel.internal.classfile.Attribute; /** * Super class for FieldGen and MethodGen objects, since they have some methods in common! * - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public abstract class FieldGenOrMethodGen extends AccessFlags implements NamedAndTyped, Cloneable { @@ -67,8 +67,10 @@ public abstract class FieldGenOrMethodGen extends AccessFlags implements NamedAn super(accessFlags); } - protected void addAll(final Attribute[] attrs) { - Collections.addAll(attributeList, attrs); + protected void addAll(final Attribute[] attributes) { + if (attributes != null) { + Collections.addAll(attributeList, attributes); + } } /** @@ -93,7 +95,7 @@ public abstract class FieldGenOrMethodGen extends AccessFlags implements NamedAn try { return super.clone(); } catch (final CloneNotSupportedException e) { - throw new Error("Clone Not Supported"); // never happens + throw new UnsupportedOperationException("Clone Not Supported", e); // never happens } } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldOrMethod.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldOrMethod.java index 1c646f48292..87ba4cb913e 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldOrMethod.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/FieldOrMethod.java @@ -53,7 +53,6 @@ public abstract class FieldOrMethod extends CPInstruction implements LoadClass { * generated by Java 1.5, this answer is sometimes wrong (e.g., if the "clone()" method is called on an * array). A better idea is to use the {@link #getReferenceType(ConstantPoolGen)} method, which correctly * distinguishes between class types and array types. - * */ @Deprecated public String getClassName(final ConstantPoolGen cpg) { @@ -89,6 +88,9 @@ public abstract class FieldOrMethod extends CPInstruction implements LoadClass { if (rt instanceof ObjectType) { return (ObjectType) rt; } + if (rt instanceof ArrayType) { + return Type.OBJECT; + } throw new ClassGenException(rt.getClass().getCanonicalName() + " " + rt.getSignature() + " does not represent an ObjectType"); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ICONST.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ICONST.java index 5effd7edcd9..b3eb14a9ddf 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ICONST.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ICONST.java @@ -25,7 +25,6 @@ package com.sun.org.apache.bcel.internal.generic; *
          * Stack: ... -> ...,
          * 
        - * */ public class ICONST extends Instruction implements ConstantPushInstruction { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/INVOKEDYNAMIC.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/INVOKEDYNAMIC.java index 2865a158de2..998d072e067 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/INVOKEDYNAMIC.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/INVOKEDYNAMIC.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -37,7 +37,7 @@ import com.sun.org.apache.bcel.internal.util.ByteSequence; * @see The * invokedynamic instruction in The Java Virtual Machine Specification * @since 6.0 - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class INVOKEDYNAMIC extends InvokeInstruction { @@ -104,11 +104,11 @@ public class INVOKEDYNAMIC extends InvokeInstruction { } /** - * Since InvokeDynamic doesn't refer to a reference type, just return java.lang.Object, as that is the only type we can + * Since InvokeDynamic doesn't refer to a reference type, just return {@link Object}, as that is the only type we can * say for sure the reference will be. * * @param cpg the ConstantPoolGen used to create the instruction - * @return an ObjectType for java.lang.Object + * @return an ObjectType for {@link Object} * @since 6.1 */ @Override diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Instruction.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Instruction.java index 16c8e2444b4..a7124409f12 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Instruction.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Instruction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -29,7 +29,7 @@ import com.sun.org.apache.bcel.internal.util.ByteSequence; /** * Abstract super class for all Java byte codes. * - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public abstract class Instruction implements Cloneable { @@ -461,7 +461,7 @@ public abstract class Instruction implements Cloneable { public Instruction copy() { Instruction i = null; // "Constant" instruction, no need to duplicate - if (InstructionConst.getInstruction(this.getOpcode()) != null) { + if (InstructionConst.getInstruction(getOpcode()) != null) { i = this; } else { try { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionConst.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionConst.java index 439268e35eb..7b95bfc99b1 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionConst.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionConst.java @@ -170,7 +170,7 @@ public final class InstructionConst { public static final LocalVariableInstruction ISTORE_2 = new ISTORE(2); /** - * Get object via its opcode, for immutable instructions like branch instructions entries are set to null. + * Gets object via its opcode, for immutable instructions like branch instructions entries are set to null. */ static final Instruction[] INSTRUCTIONS = new Instruction[256]; diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionFactory.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionFactory.java index 5e9220354c3..3c4b3e9f9da 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionFactory.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -30,11 +30,11 @@ import com.sun.org.apache.bcel.internal.Const; * * @see Const * @see InstructionConst - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class InstructionFactory { - private static class MethodObject { + private static final class MethodObject { final Type[] argTypes; final Type resultType; @@ -53,10 +53,12 @@ public class InstructionFactory { private static final String FQCN_STRING_BUFFER = "java.lang.StringBuffer"; - // N.N. These must agree with the order of Constants.T_CHAR through T_LONG - private static final String[] shortNames = {"C", "F", "D", "B", "S", "I", "L"}; + /** + * These must agree with the order of Constants.T_CHAR through T_LONG. + */ + private static final String[] SHORT_NAMES = {"C", "F", "D", "B", "S", "I", "L"}; - private static final MethodObject[] appendMethodObjects = { + private static final MethodObject[] APPEND_METHOD_OBJECTS = { new MethodObject(FQCN_STRING_BUFFER, APPEND, Type.STRINGBUFFER, new Type[] { Type.STRING }), new MethodObject(FQCN_STRING_BUFFER, APPEND, Type.STRINGBUFFER, new Type[] { Type.OBJECT }), null, null, // indices 2, 3 new MethodObject(FQCN_STRING_BUFFER, APPEND, Type.STRINGBUFFER, new Type[] { Type.BOOLEAN }), @@ -484,7 +486,7 @@ public class InstructionFactory { public Instruction createAppend(final Type type) { final byte t = type.getType(); if (isString(type)) { - return createInvoke(appendMethodObjects[0], Const.INVOKEVIRTUAL); + return createInvoke(APPEND_METHOD_OBJECTS[0], Const.INVOKEVIRTUAL); } switch (t) { case Const.T_BOOLEAN: @@ -495,10 +497,10 @@ public class InstructionFactory { case Const.T_SHORT: case Const.T_INT: case Const.T_LONG: - return createInvoke(appendMethodObjects[t], Const.INVOKEVIRTUAL); + return createInvoke(APPEND_METHOD_OBJECTS[t], Const.INVOKEVIRTUAL); case Const.T_ARRAY: case Const.T_OBJECT: - return createInvoke(appendMethodObjects[1], Const.INVOKEVIRTUAL); + return createInvoke(APPEND_METHOD_OBJECTS[1], Const.INVOKEVIRTUAL); default: throw new IllegalArgumentException("No append for this type? " + type); } @@ -515,7 +517,7 @@ public class InstructionFactory { if (dest == Const.T_LONG && (src == Const.T_CHAR || src == Const.T_BYTE || src == Const.T_SHORT)) { src = Const.T_INT; } - final String name = "com.sun.org.apache.bcel.internal.generic." + shortNames[src - Const.T_CHAR] + "2" + shortNames[dest - Const.T_CHAR]; + final String name = "com.sun.org.apache.bcel.internal.generic." + SHORT_NAMES[src - Const.T_CHAR] + "2" + SHORT_NAMES[dest - Const.T_CHAR]; Instruction i = null; try { i = (Instruction) Class.forName(name).getDeclaredConstructor().newInstance();; @@ -642,8 +644,10 @@ public class InstructionFactory { int index; int nargs = 0; final String signature = Type.getMethodSignature(retType, argTypes); - for (final Type argType : argTypes) { - nargs += argType.getSize(); + if (argTypes != null) { + for (final Type argType : argTypes) { + nargs += argType.getSize(); + } } if (useInterface) { index = cp.addInterfaceMethodref(className, name, signature); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionHandle.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionHandle.java index 5e962354d16..2c94b770265 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionHandle.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -38,7 +38,7 @@ import com.sun.org.apache.bcel.internal.classfile.Utility; * @see Instruction * @see BranchHandle * @see InstructionList - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public class InstructionHandle { @@ -118,7 +118,7 @@ public class InstructionHandle { if (targeters == null) { targeters = new HashSet<>(); } - // if(!targeters.contains(t)) + // if (!targeters.contains(t)) targeters.add(t); } @@ -135,15 +135,12 @@ public class InstructionHandle { } /** - * Get attribute of an instruction handle. + * Gets attribute of an instruction handle. * * @param key the key object to store/retrieve the attribute */ public Object getAttribute(final Object key) { - if (attributes != null) { - return attributes.get(key); - } - return null; + return attributes != null ? attributes.get(key) : null; } /** @@ -247,7 +244,7 @@ public class InstructionHandle { } /** - * Set the position, i.e., the byte code offset of the contained instruction. + * Sets the position, i.e., the byte code offset of the contained instruction. */ void setPosition(final int pos) { i_position = pos; diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionList.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionList.java index 7ffc3a8228e..579efc9fe3b 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionList.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionList.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -33,6 +33,7 @@ import java.util.NoSuchElementException; import com.sun.org.apache.bcel.internal.Const; import com.sun.org.apache.bcel.internal.classfile.Constant; import com.sun.org.apache.bcel.internal.util.ByteSequence; +import jdk.xml.internal.Utils; /** * This class is a container for a list of Instruction objects. Instructions can be @@ -46,7 +47,7 @@ import com.sun.org.apache.bcel.internal.util.ByteSequence; * @see Instruction * @see InstructionHandle * @see BranchHandle - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class InstructionList implements Iterable { @@ -60,23 +61,25 @@ public class InstructionList implements Iterable { * @return target position's instruction handle if available */ public static InstructionHandle findHandle(final InstructionHandle[] ihs, final int[] pos, final int count, final int target) { - int l = 0; - int r = count - 1; - /* - * Do a binary search since the pos array is orderd. - */ - do { - final int i = l + r >>> 1; - final int j = pos[i]; - if (j == target) { - return ihs[i]; - } - if (target < j) { - r = i - 1; - } else { - l = i + 1; - } - } while (l <= r); + if (ihs != null && pos != null) { + int l = 0; + int r = count - 1; + /* + * Do a binary search since the pos array is orderd. + */ + do { + final int i = l + r >>> 1; + final int j = pos[i]; + if (j == target) { + return ihs[i]; + } + if (target < j) { + r = i - 1; + } else { + l = i + 1; + } + } while (l <= r); + } return null; } @@ -513,7 +516,7 @@ public class InstructionList implements Iterable { } /** - * Get instruction handle for instruction at byte code position pos. This only works properly, if the list is freshly + * Gets instruction handle for instruction at byte code position pos. This only works properly, if the list is freshly * initialized from a byte array or setPositions() has been called before this method. * * @param pos byte code position to search for @@ -605,7 +608,7 @@ public class InstructionList implements Iterable { } /** - * Get positions (offsets) of all instructions in the list. This relies on that the list has been freshly created from + * Gets positions (offsets) of all instructions in the list. This relies on that the list has been freshly created from * an byte code array, or that setPositions() has been called. Otherwise this may be inaccurate. * * @return array containing all instruction's offset in byte code @@ -959,7 +962,7 @@ public class InstructionList implements Iterable { * @see MethodGen */ public void redirectExceptionHandlers(final CodeExceptionGen[] exceptions, final InstructionHandle oldTarget, final InstructionHandle newTarget) { - for (final CodeExceptionGen exception : exceptions) { + Utils.streamOfIfNonNull(exceptions).forEach(exception -> { if (exception.getStartPC() == oldTarget) { exception.setStartPC(newTarget); } @@ -969,7 +972,7 @@ public class InstructionList implements Iterable { if (exception.getHandlerPC() == oldTarget) { exception.setHandlerPC(newTarget); } - } + }); } /** @@ -981,16 +984,14 @@ public class InstructionList implements Iterable { * @see MethodGen */ public void redirectLocalVariables(final LocalVariableGen[] lg, final InstructionHandle oldTarget, final InstructionHandle newTarget) { - for (final LocalVariableGen element : lg) { - final InstructionHandle start = element.getStart(); - final InstructionHandle end = element.getEnd(); - if (start == oldTarget) { + Utils.streamOfIfNonNull(lg).forEach(element -> { + if (element.getStart() == oldTarget) { element.setStart(newTarget); } - if (end == oldTarget) { + if (element.getEnd() == oldTarget) { element.setEnd(newTarget); } - } + }); } /** @@ -1120,7 +1121,7 @@ public class InstructionList implements Iterable { ih.setPosition(index); pos[count++] = index; /* - * Get an estimate about how many additional bytes may be added, because BranchInstructions may have variable length + * Gets an estimate about how many additional bytes may be added, because BranchInstructions may have variable length * depending on the target offset (short vs. int) or alignment issues (TABLESWITCH and LOOKUPSWITCH). */ switch (i.getOpcode()) { @@ -1132,11 +1133,14 @@ public class InstructionList implements Iterable { case Const.LOOKUPSWITCH: maxAdditionalBytes += 3; break; + default: + // TODO should this be an error? + break; } index += i.getLength(); } /* - * Pass 2: Expand the variable-length (Branch)Instructions depending on the target offset (short or int) and ensure that + * Pass 2: Expand the variable-length (Branch) Instructions depending on the target offset (short or int) and ensure that * branch targets are within this list. */ for (InstructionHandle ih = start; ih != null; ih = ih.getNext()) { @@ -1152,8 +1156,7 @@ public class InstructionList implements Iterable { pos[count++] = index; index += i.getLength(); } - bytePositions = new int[count]; // Trim to proper size - System.arraycopy(pos, 0, bytePositions, 0, count); + bytePositions = Arrays.copyOfRange(pos, 0, count); // Trim to proper size } /** diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionTargeter.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionTargeter.java index 5146408ef49..0681476b5ee 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionTargeter.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/InstructionTargeter.java @@ -22,7 +22,7 @@ package com.sun.org.apache.bcel.internal.generic; /** - * Denote that a class targets InstructionHandles within an InstructionList. Namely the following implementers: + * Denotes that a class targets InstructionHandles within an InstructionList. * * @see BranchHandle * @see LocalVariableGen @@ -33,9 +33,12 @@ public interface InstructionTargeter { // static final InstructionTargeter[] EMPTY_ARRAY = new InstructionTargeter[0]; /** - * Checks whether this targeter targets the specified instruction handle. + * Tests whether this targeter targets the specified instruction handle. + * + * @param instructionHandle the instruction handle to test. + * @return whether this targeter targets the specified instruction handle. */ - boolean containsTarget(InstructionHandle ih); + boolean containsTarget(InstructionHandle instructionHandle); /** * Replaces the target of this targeter from this old handle to the new handle. diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LCMP.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LCMP.java index c517b492f0a..188ac95f6ef 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LCMP.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LCMP.java @@ -27,7 +27,6 @@ package com.sun.org.apache.bcel.internal.generic; *
          * Stack: ..., value1.word1, value1.word2, value2.word1, value2.word2 -> ..., result <= -1, 0, 1>
          * 
        - * */ public class LCMP extends Instruction implements TypedInstruction, StackProducer, StackConsumer { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LDC.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LDC.java index d95bade23bd..13ca0a84cfa 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LDC.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LDC.java @@ -94,6 +94,8 @@ public class LDC extends CPInstruction implements PushInstruction, ExceptionThro return Type.INT; case com.sun.org.apache.bcel.internal.Const.CONSTANT_Class: return Type.CLASS; + case com.sun.org.apache.bcel.internal.Const.CONSTANT_Dynamic: + return Type.OBJECT; default: // Never reached throw new IllegalArgumentException("Unknown or invalid constant type at " + super.getIndex()); } @@ -113,7 +115,10 @@ public class LDC extends CPInstruction implements PushInstruction, ExceptionThro case com.sun.org.apache.bcel.internal.Const.CONSTANT_Class: final int nameIndex = ((com.sun.org.apache.bcel.internal.classfile.ConstantClass) c).getNameIndex(); c = cpg.getConstantPool().getConstant(nameIndex); - return Type.getType(((com.sun.org.apache.bcel.internal.classfile.ConstantUtf8) c).getBytes()); + return Type.getType(Type.internalTypeNameToSignature(((com.sun.org.apache.bcel.internal.classfile.ConstantUtf8) c).getBytes())); + case com.sun.org.apache.bcel.internal.Const.CONSTANT_Dynamic: + // Really not sure what to return here, maybe a BootstrapMethod instance but how do we get it? + return c; default: // Never reached throw new IllegalArgumentException("Unknown or invalid constant type at " + super.getIndex()); } @@ -129,7 +134,7 @@ public class LDC extends CPInstruction implements PushInstruction, ExceptionThro } /** - * Set the index to constant pool and adjust size. + * Sets the index to constant pool and adjust size. */ @Override public final void setIndex(final int index) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LineNumberGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LineNumberGen.java index 3773c21e7b3..68bb2abf513 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LineNumberGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LineNumberGen.java @@ -54,7 +54,7 @@ public class LineNumberGen implements InstructionTargeter, Cloneable { try { return super.clone(); } catch (final CloneNotSupportedException e) { - throw new Error("Clone Not Supported"); // never happens + throw new UnsupportedOperationException("Clone Not Supported", e); // never happens } } @@ -71,7 +71,7 @@ public class LineNumberGen implements InstructionTargeter, Cloneable { } /** - * Get LineNumber attribute. + * Gets LineNumber attribute. * * This relies on that the instruction list has already been dumped to byte code or that the 'setPositions' methods * has been called for the instruction list. diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableGen.java index 830564d42c9..71cfa0cf1c2 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableGen.java @@ -85,7 +85,7 @@ public class LocalVariableGen implements InstructionTargeter, NamedAndTyped, Clo try { return super.clone(); } catch (final CloneNotSupportedException e) { - throw new Error("Clone Not Supported"); // never happens + throw new UnsupportedOperationException("Clone Not Supported", e); // never happens } } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableInstruction.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableInstruction.java index 67184c0b9f1..f952a65880b 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableInstruction.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/LocalVariableInstruction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -28,7 +28,7 @@ import com.sun.org.apache.bcel.internal.util.ByteSequence; /** * Abstract super class for instructions dealing with local variables. * - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public abstract class LocalVariableInstruction extends Instruction implements TypedInstruction, IndexedInstruction { @@ -162,7 +162,7 @@ public abstract class LocalVariableInstruction extends Instruction implements Ty } /** - * Set the local variable index. also updates opcode and length TODO Why? + * Sets the local variable index. also updates opcode and length TODO Why? * * @see #setIndexOnly(int) */ diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/MethodGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/MethodGen.java index f6e8333be79..be09b0a5159 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/MethodGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/MethodGen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.Stack; +import java.util.stream.Collectors; import com.sun.org.apache.bcel.internal.Const; import com.sun.org.apache.bcel.internal.classfile.AnnotationEntry; @@ -46,6 +47,7 @@ import com.sun.org.apache.bcel.internal.classfile.ParameterAnnotations; import com.sun.org.apache.bcel.internal.classfile.RuntimeVisibleParameterAnnotations; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.BCELComparator; +import jdk.xml.internal.Utils; /** * Template class for building up a method. This is done by defining exception handlers, adding thrown exceptions, local @@ -57,7 +59,7 @@ import com.sun.org.apache.bcel.internal.util.BCELComparator; * * @see InstructionList * @see Method - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class MethodGen extends FieldGenOrMethodGen { @@ -102,19 +104,16 @@ public class MethodGen extends FieldGenOrMethodGen { } } - private static BCELComparator bcelComparator = new BCELComparator() { + private static BCELComparator bcelComparator = new BCELComparator() { @Override - public boolean equals(final Object o1, final Object o2) { - final FieldGenOrMethodGen THIS = (FieldGenOrMethodGen) o1; - final FieldGenOrMethodGen THAT = (FieldGenOrMethodGen) o2; - return Objects.equals(THIS.getName(), THAT.getName()) && Objects.equals(THIS.getSignature(), THAT.getSignature()); + public boolean equals(final FieldGenOrMethodGen a, final FieldGenOrMethodGen b) { + return a == b || a != null && b != null && Objects.equals(a.getName(), b.getName()) && Objects.equals(a.getSignature(), b.getSignature()); } @Override - public int hashCode(final Object o) { - final FieldGenOrMethodGen THIS = (FieldGenOrMethodGen) o; - return THIS.getSignature().hashCode() ^ THIS.getName().hashCode(); + public int hashCode(final FieldGenOrMethodGen o) { + return o != null ? Objects.hash(o.getSignature(), o.getName()) : 0; } }; @@ -127,9 +126,9 @@ public class MethodGen extends FieldGenOrMethodGen { } /** - * @return Comparison strategy object + * @return Comparison strategy object. */ - public static BCELComparator getComparator() { + public static BCELComparator getComparator() { return bcelComparator; } @@ -206,9 +205,9 @@ public class MethodGen extends FieldGenOrMethodGen { } /** - * @param comparator Comparison strategy object + * @param comparator Comparison strategy object. */ - public static void setComparator(final BCELComparator comparator) { + public static void setComparator(final BCELComparator comparator) { bcelComparator = comparator; } @@ -636,7 +635,7 @@ public class MethodGen extends FieldGenOrMethodGen { */ @Override public boolean equals(final Object obj) { - return bcelComparator.equals(this, obj); + return obj instanceof FieldGenOrMethodGen && bcelComparator.equals(this, (FieldGenOrMethodGen) obj); } // J5TODO: Should paramAnnotations be an array of arrays? Rather than an array of lists, this @@ -790,7 +789,7 @@ public class MethodGen extends FieldGenOrMethodGen { } /** - * Get method object. Never forget to call setMaxStack() or setMaxStack(max), respectively, before calling this method + * Gets method object. Never forget to call setMaxStack() or setMaxStack(max), respectively, before calling this method * (the same applies for max locals). * * @return method object @@ -888,7 +887,7 @@ public class MethodGen extends FieldGenOrMethodGen { } /** - * Return value as defined by given BCELComparator strategy. By default return the hashcode of the method's name XOR + * Return value as defined by given BCELComparator strategy. By default return the hash code of the method's name XOR * signature. * * @see Object#hashCode() @@ -899,11 +898,7 @@ public class MethodGen extends FieldGenOrMethodGen { } private List makeMutableVersion(final AnnotationEntry[] mutableArray) { - final List result = new ArrayList<>(); - for (final AnnotationEntry element : mutableArray) { - result.add(new AnnotationEntryGen(element, getConstantPool(), false)); - } - return result; + return Utils.streamOfIfNonNull(mutableArray).map(ae -> new AnnotationEntryGen(ae, getConstantPool(), false)).collect(Collectors.toList()); } /** @@ -1027,10 +1022,8 @@ public class MethodGen extends FieldGenOrMethodGen { * * @since 6.5.0 */ - public void removeRuntimeAttributes(final Attribute[] attrs) { - for (final Attribute attr : attrs) { - removeAttribute(attr); - } + public void removeRuntimeAttributes(final Attribute[] attributes) { + Utils.streamOfIfNonNull(attributes).forEach(this::removeAttribute); } public void setArgumentName(final int i, final String name) { @@ -1038,7 +1031,7 @@ public class MethodGen extends FieldGenOrMethodGen { } public void setArgumentNames(final String[] argNames) { - this.argNames = argNames; + this.argNames = Utils.createEmptyArrayIfNull(argNames, String[].class); } public void setArgumentType(final int i, final Type type) { @@ -1046,7 +1039,7 @@ public class MethodGen extends FieldGenOrMethodGen { } public void setArgumentTypes(final Type[] argTypes) { - this.argTypes = argTypes; + this.argTypes = argTypes != null ? argTypes : Type.NO_ARGS; } public void setClassName(final String className) { // TODO could be package-protected? @@ -1084,7 +1077,7 @@ public class MethodGen extends FieldGenOrMethodGen { } /** - * Set maximum number of local variables. + * Sets maximum number of local variables. */ public void setMaxLocals(final int m) { maxLocals = m; @@ -1102,7 +1095,7 @@ public class MethodGen extends FieldGenOrMethodGen { } /** - * Set maximum stack size for this method. + * Sets maximum stack size for this method. */ public void setMaxStack(final int m) { // TODO could be package-protected? maxStack = m; diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ObjectType.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ObjectType.java index 46378a1b71e..622a3cdd961 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ObjectType.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ObjectType.java @@ -27,7 +27,7 @@ import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; /** - * Denotes reference such as java.lang.String. + * Denotes reference such as {@link String}. */ public class ObjectType extends ReferenceType { @@ -47,7 +47,7 @@ public class ObjectType extends ReferenceType { /** * Constructs a new instance. * - * @param className fully qualified class name, e.g. java.lang.String + * @param className fully qualified class name, e.g. {@link String} */ public ObjectType(final String className) { super(Const.T_REFERENCE, "L" + Utility.packageToPath(className) + ";"); @@ -151,7 +151,7 @@ public class ObjectType extends ReferenceType { * @throws ClassNotFoundException if any of this class's superclasses can't be found */ public boolean subclassOf(final ObjectType superclass) throws ClassNotFoundException { - if (this.referencesInterfaceExact() || superclass.referencesInterfaceExact()) { + if (referencesInterfaceExact() || superclass.referencesInterfaceExact()) { return false; } return Repository.instanceOf(this.className, superclass.className); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/RET.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/RET.java index a7cacc7c165..99a1efbed16 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/RET.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/RET.java @@ -110,7 +110,7 @@ public class RET extends Instruction implements IndexedInstruction, TypedInstruc } /** - * Set index of local variable containg the return address + * Sets index of local variable containg the return address */ @Override public final void setIndex(final int n) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ReferenceType.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ReferenceType.java index fe75792e213..9b94a1dcf2d 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ReferenceType.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/ReferenceType.java @@ -42,10 +42,10 @@ public abstract class ReferenceType extends Type { /** * This commutative operation returns the first common superclass (narrowest ReferenceType referencing a class, not an - * interface). If one of the types is a superclass of the other, the former is returned. If "this" is Type.NULL, then t - * is returned. If t is Type.NULL, then "this" is returned. If "this" equals t ['this.equals(t)'] "this" is returned. If - * "this" or t is an ArrayType, then Type.OBJECT is returned. If "this" or t is a ReferenceType referencing an - * interface, then Type.OBJECT is returned. If not all of the two classes' superclasses cannot be found, "null" is + * interface). If one of the types is a superclass of the other, the former is returned. If "this" is NULL, then t + * is returned. If t is NULL, then "this" is returned. If "this" equals t ['this.equals(t)'] "this" is returned. If + * "this" or t is an ArrayType, then {@link #OBJECT} is returned. If "this" or t is a ReferenceType referencing an + * interface, then {@link #OBJECT} is returned. If not all of the two classes' superclasses cannot be found, "null" is * returned. See the JVM specification edition 2, "4.9.2 The Bytecode Verifier". * * @deprecated use getFirstCommonSuperclass(ReferenceType t) which has slightly changed semantics. @@ -53,46 +53,46 @@ public abstract class ReferenceType extends Type { */ @Deprecated public ReferenceType firstCommonSuperclass(final ReferenceType t) throws ClassNotFoundException { - if (this.equals(Type.NULL)) { + if (equals(NULL)) { return t; } - if (t.equals(Type.NULL) || this.equals(t)) { + if (t.equals(NULL) || equals(t)) { return this; /* - * TODO: Above sounds a little arbitrary. On the other hand, there is no object referenced by Type.NULL so we can also - * say all the objects referenced by Type.NULL were derived from java.lang.Object. However, the Java Language's - * "instanceof" operator proves us wrong: "null" is not referring to an instance of java.lang.Object :) + * TODO: Above sounds a little arbitrary. On the other hand, there is no object referenced by {@link #NULL} so we can also + * say all the objects referenced by {@link #NULL} were derived from {@link Object}. However, the Java Language's + * "instanceof" operator proves us wrong: "null" is not referring to an instance of {@link Object} :) */ } if (this instanceof ArrayType || t instanceof ArrayType) { - return Type.OBJECT; - // TODO: Is there a proof of OBJECT being the direct ancestor of every ArrayType? + return OBJECT; + // TODO: Is there a proof of {@link #OBJECT} being the direct ancestor of every ArrayType? } return getFirstCommonSuperclassInternal(t); } /** * This commutative operation returns the first common superclass (narrowest ReferenceType referencing a class, not an - * interface). If one of the types is a superclass of the other, the former is returned. If "this" is Type.NULL, then t - * is returned. If t is Type.NULL, then "this" is returned. If "this" equals t ['this.equals(t)'] "this" is returned. If - * "this" or t is an ArrayType, then Type.OBJECT is returned; unless their dimensions match. Then an ArrayType of the + * interface). If one of the types is a superclass of the other, the former is returned. If "this" is NULL, then t + * is returned. If t is NULL, then "this" is returned. If "this" equals t ['this.equals(t)'] "this" is returned. If + * "this" or t is an ArrayType, then {@link #OBJECT} is returned; unless their dimensions match. Then an ArrayType of the * same number of dimensions is returned, with its basic type being the first common super class of the basic types of - * "this" and t. If "this" or t is a ReferenceType referencing an interface, then Type.OBJECT is returned. If not all of + * "this" and t. If "this" or t is a ReferenceType referencing an interface, then {@link #OBJECT} is returned. If not all of * the two classes' superclasses cannot be found, "null" is returned. See the JVM specification edition 2, "4.9.2 The * Bytecode Verifier". * * @throws ClassNotFoundException on failure to find superclasses of this type, or the type passed as a parameter */ public ReferenceType getFirstCommonSuperclass(final ReferenceType t) throws ClassNotFoundException { - if (this.equals(Type.NULL)) { + if (equals(NULL)) { return t; } - if (t.equals(Type.NULL) || this.equals(t)) { + if (t.equals(NULL) || equals(t)) { return this; /* - * TODO: Above sounds a little arbitrary. On the other hand, there is no object referenced by Type.NULL so we can also - * say all the objects referenced by Type.NULL were derived from java.lang.Object. However, the Java Language's - * "instanceof" operator proves us wrong: "null" is not referring to an instance of java.lang.Object :) + * TODO: Above sounds a little arbitrary. On the other hand, there is no object referenced by {@link #NULL} so we can also + * say all the objects referenced by {@link #NULL} were derived from {@link Object}. However, the Java Language's + * "instanceof" operator proves us wrong: "null" is not referring to an instance of {@link Object} :) */ } /* This code is from a bug report by Konstantin Shagin */ @@ -106,8 +106,8 @@ public abstract class ReferenceType extends Type { } } if (this instanceof ArrayType || t instanceof ArrayType) { - return Type.OBJECT; - // TODO: Is there a proof of OBJECT being the direct ancestor of every ArrayType? + return OBJECT; + // TODO: Is there a proof of {@link #OBJECT} being the direct ancestor of every ArrayType? } return getFirstCommonSuperclassInternal(t); } @@ -115,7 +115,7 @@ public abstract class ReferenceType extends Type { private ReferenceType getFirstCommonSuperclassInternal(final ReferenceType t) throws ClassNotFoundException { if (this instanceof ObjectType && ((ObjectType) this).referencesInterfaceExact() || t instanceof ObjectType && ((ObjectType) t).referencesInterfaceExact()) { - return Type.OBJECT; + return OBJECT; // TODO: The above line is correct comparing to the vmspec2. But one could // make class file verification a bit stronger here by using the notion of // superinterfaces or even castability or assignment compatibility. @@ -142,7 +142,7 @@ public abstract class ReferenceType extends Type { } } } - // Huh? Did you ask for Type.OBJECT's superclass?? + // Huh? Did you ask for OBJECT's superclass?? return null; } @@ -158,7 +158,7 @@ public abstract class ReferenceType extends Type { return false; } final ReferenceType T = (ReferenceType) t; - if (this.equals(Type.NULL)) { + if (equals(NULL)) { return true; // This is not explicitly stated, but clear. Isn't it? } /* @@ -169,7 +169,7 @@ public abstract class ReferenceType extends Type { * If T is a class type, then this must be the same class as T, or this must be a subclass of T; */ if (T instanceof ObjectType && ((ObjectType) T).referencesClassExact() - && (this.equals(T) || Repository.instanceOf(((ObjectType) this).getClassName(), ((ObjectType) T).getClassName()))) { + && (equals(T) || Repository.instanceOf(((ObjectType) this).getClassName(), ((ObjectType) T).getClassName()))) { return true; } /* @@ -187,14 +187,14 @@ public abstract class ReferenceType extends Type { /* * If T is a class type, then T must be Object (2.4.7). */ - if (T instanceof ObjectType && ((ObjectType) T).referencesClassExact() && T.equals(Type.OBJECT)) { + if (T instanceof ObjectType && ((ObjectType) T).referencesClassExact() && T.equals(OBJECT)) { return true; } /* * If T is an interface type, then T must be the same interface as this or a superinterface of this (2.13.2). */ if (T instanceof ObjectType && ((ObjectType) T).referencesInterfaceExact() - && (this.equals(T) || Repository.implementationOf(((ObjectType) this).getClassName(), ((ObjectType) T).getClassName()))) { + && (equals(T) || Repository.implementationOf(((ObjectType) this).getClassName(), ((ObjectType) T).getClassName()))) { return true; } } @@ -205,7 +205,7 @@ public abstract class ReferenceType extends Type { /* * If T is a class type, then T must be Object (2.4.7). */ - if (T instanceof ObjectType && ((ObjectType) T).referencesClassExact() && T.equals(Type.OBJECT)) { + if (T instanceof ObjectType && ((ObjectType) T).referencesClassExact() && T.equals(OBJECT)) { return true; } /* @@ -246,14 +246,14 @@ public abstract class ReferenceType extends Type { /** * Return true iff this type is castable to another type t as defined in the JVM specification. The case where this is - * Type.NULL is not defined (see the CHECKCAST definition in the JVM specification). However, because e.g. CHECKCAST + * {@link #NULL} is not defined (see the CHECKCAST definition in the JVM specification). However, because e.g. CHECKCAST * doesn't throw a ClassCastException when casting a null reference to any Object, true is returned in this case. * * @throws ClassNotFoundException if any classes or interfaces required to determine assignment compatibility can't be * found */ public boolean isCastableTo(final Type t) throws ClassNotFoundException { - if (this.equals(Type.NULL)) { + if (equals(NULL)) { return t instanceof ReferenceType; // If this is ever changed in isAssignmentCompatible() } return isAssignmentCompatibleWith(t); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SWITCH.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SWITCH.java index aed1626ec0d..f929176c174 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SWITCH.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SWITCH.java @@ -95,7 +95,7 @@ public final class SWITCH implements CompoundInstruction { * @param maxGap maximum gap that may between case branches */ public SWITCH(final int[] match, final InstructionHandle[] targets, final InstructionHandle target, final int maxGap) { - int[] matchClone = match.clone(); + final int[] matchClone = match.clone(); final InstructionHandle[] targetsClone = targets.clone(); final int matchLength = match.length; if (matchLength < 2) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Select.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Select.java index a90e795e99f..a3086ac1664 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Select.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Select.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -33,7 +33,7 @@ import com.sun.org.apache.bcel.internal.util.ByteSequence; * @see LOOKUPSWITCH * @see TABLESWITCH * @see InstructionList - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public abstract class Select extends BranchInstruction implements VariableLengthInstruction, StackConsumer /* @since 6.0 */, StackProducer { @@ -87,7 +87,7 @@ public abstract class Select extends BranchInstruction implements VariableLength * @param defaultTarget default instruction target */ Select(final short opcode, final int[] match, final InstructionHandle[] targets, final InstructionHandle defaultTarget) { - // don't set default target before instuction is built + // don't set default target before instruction is built super(opcode, null); this.match = match; this.targets = targets; @@ -288,7 +288,7 @@ public abstract class Select extends BranchInstruction implements VariableLength } /** - * Set branch target for 'i'th case + * Sets branch target for 'i'th case */ public void setTarget(final int i, final InstructionHandle target) { // TODO could be package-protected? notifyTarget(targets[i], target, this); @@ -314,7 +314,11 @@ public abstract class Select extends BranchInstruction implements VariableLength for (int i = 0; i < match_length; i++) { String s = "null"; if (targets[i] != null) { - s = targets[i].getInstruction().toString(); + if (targets[i].getInstruction() == this) { + s = ""; + } else { + s = targets[i].getInstruction().toString(); + } } buf.append("(").append(match[i]).append(", ").append(s).append(" = {").append(indices[i]).append("})"); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SimpleElementValueGen.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SimpleElementValueGen.java index a4de20315d7..363b3237632 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SimpleElementValueGen.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SimpleElementValueGen.java @@ -166,7 +166,7 @@ public class SimpleElementValueGen extends ElementValueGen { dos.writeShort(idx); break; default: - throw new IllegalStateException("SimpleElementValueGen doesnt know how to write out type " + super.getElementValueType()); + throw new IllegalStateException("SimpleElementValueGen doesn't know how to write out type " + super.getElementValueType()); } } @@ -184,7 +184,7 @@ public class SimpleElementValueGen extends ElementValueGen { public int getValueInt() { if (super.getElementValueType() != PRIMITIVE_INT) { - throw new IllegalStateException("Dont call getValueString() on a non STRING ElementValue"); + throw new IllegalStateException("Don't call getValueString() on a non STRING ElementValue"); } final ConstantInteger c = (ConstantInteger) getConstantPool().getConstant(idx); return c.getBytes(); @@ -192,7 +192,7 @@ public class SimpleElementValueGen extends ElementValueGen { public String getValueString() { if (super.getElementValueType() != STRING) { - throw new IllegalStateException("Dont call getValueString() on a non STRING ElementValue"); + throw new IllegalStateException("Don't call getValueString() on a non STRING ElementValue"); } final ConstantUtf8 c = (ConstantUtf8) getConstantPool().getConstant(idx); return c.getBytes(); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TargetLostException.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TargetLostException.java index 40ed6ff4c38..038bbe63148 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TargetLostException.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TargetLostException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -21,32 +21,32 @@ package com.sun.org.apache.bcel.internal.generic; /** - * Thrown by InstructionList.remove() when one or multiple disposed instructions are still being referenced by an - * InstructionTargeter object. I.e. the InstructionTargeter has to be notified that (one of) the InstructionHandle it is - * referencing is being removed from the InstructionList and thus not valid anymore. + * Thrown by {@link InstructionList} when one or multiple disposed instructions are still being referenced by an {@link InstructionTargeter} object. I.e. the + * {@link InstructionTargeter} has to be notified that (one of) the {@link InstructionHandle} it is referencing is being removed from the + * {@link InstructionList} and thus not valid anymore. * *

        - * Making this an exception instead of a return value forces the user to handle these case explicitly in a try { ... } - * catch. The following code illustrates how this may be done: + * Making this an exception instead of a return value forces the user to handle these case explicitly in a try { ... } catch. The following code illustrates how + * this may be done: *

        * - *
        + * 
          *     ...
          *     try {
          *         il.delete(start_ih, end_ih);
        - *     } catch(TargetLostException e) {
        + *     } catch (TargetLostException e) {
          *         for (InstructionHandle target : e.getTargets()) {
          *             for (InstructionTargeter targeter : target.getTargeters()) {
          *                 targeter.updateTarget(target, new_target);
          *             }
          *         }
          *     }
        - * 
        + *
        * * @see InstructionHandle * @see InstructionList * @see InstructionTargeter - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public final class TargetLostException extends Exception { @@ -54,12 +54,14 @@ public final class TargetLostException extends Exception { @SuppressWarnings("serial") // Array component type is not Serializable private final InstructionHandle[] targets; - TargetLostException(final InstructionHandle[] t, final String mesg) { - super(mesg); - targets = t; + TargetLostException(final InstructionHandle[] targets, final String message) { + super(message); + this.targets = targets; } /** + * Gets the list of instructions still being targeted. + * * @return list of instructions still being targeted. */ public InstructionHandle[] getTargets() { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Type.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Type.java index ea20710af2e..b58645b402e 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Type.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/Type.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -26,12 +26,14 @@ import java.util.Objects; import com.sun.org.apache.bcel.internal.Const; import com.sun.org.apache.bcel.internal.classfile.ClassFormatException; +import com.sun.org.apache.bcel.internal.classfile.InvalidMethodSignatureException; import com.sun.org.apache.bcel.internal.classfile.Utility; +import jdk.xml.internal.Utils; /** - * Abstract super class for all possible java types, namely basic types such as int, object types like String and array + * Abstract super class for all possible Java types, namely basic types such as int, object types like String and array * types, e.g. int[] - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public abstract class Type { @@ -88,15 +90,15 @@ public abstract class Type { // Skip any type arguments to read argument declarations between '(' and ')' index = signature.indexOf('(') + 1; if (index <= 0) { - throw new ClassFormatException("Invalid method signature: " + signature); + throw new InvalidMethodSignatureException(signature); } while (signature.charAt(index) != ')') { vec.add(getType(signature.substring(index))); - // corrected concurrent private static field acess + // corrected concurrent private static field access index += unwrap(CONSUMED_CHARS); // update position } } catch (final StringIndexOutOfBoundsException e) { // Should never occur - throw new ClassFormatException("Invalid method signature: " + signature, e); + throw new InvalidMethodSignatureException(signature, e); } final Type[] types = new Type[vec.size()]; vec.toArray(types); @@ -110,7 +112,7 @@ public abstract class Type { // Skip any type arguments to read argument declarations between '(' and ')' index = signature.indexOf('(') + 1; if (index <= 0) { - throw new ClassFormatException("Invalid method signature: " + signature); + throw new InvalidMethodSignatureException(signature); } while (signature.charAt(index) != ')') { final int coded = getTypeSize(signature.substring(index)); @@ -118,7 +120,7 @@ public abstract class Type { index += consumed(coded); } } catch (final StringIndexOutOfBoundsException e) { // Should never occur - throw new ClassFormatException("Invalid method signature: " + signature, e); + throw new InvalidMethodSignatureException(signature, e); } return res; } @@ -154,13 +156,13 @@ public abstract class Type { final int index = signature.lastIndexOf(')') + 1; return getType(signature.substring(index)); } catch (final StringIndexOutOfBoundsException e) { // Should never occur - throw new ClassFormatException("Invalid method signature: " + signature, e); + throw new InvalidMethodSignatureException(signature, e); } } static int getReturnTypeSize(final String signature) { final int index = signature.lastIndexOf(')') + 1; - return Type.size(getTypeSize(signature.substring(index))); + return size(getTypeSize(signature.substring(index))); } public static String getSignature(final java.lang.reflect.Method meth) { @@ -175,7 +177,7 @@ public abstract class Type { } /** - * Convert runtime java.lang.Class to BCEL Type object. + * Convert runtime {@link Class} to BCEL Type object. * * @param cls Java class * @return corresponding Type object @@ -183,7 +185,7 @@ public abstract class Type { public static Type getType(final Class cls) { Objects.requireNonNull(cls, "cls"); /* - * That's an amzingly easy case, because getName() returns the signature. That's what we would have liked anyway. + * That's an amazingly easy case, because getName() returns the signature. That's what we would have liked anyway. */ if (cls.isArray()) { return getType(cls.getName()); @@ -230,7 +232,7 @@ public abstract class Type { public static Type getType(final String signature) throws StringIndexOutOfBoundsException { final byte type = Utility.typeOfSignature(signature); if (type <= Const.T_VOID) { - // corrected concurrent private static field acess + // corrected concurrent private static field access wrap(CONSUMED_CHARS, 1); return BasicType.getType(type); } @@ -246,7 +248,7 @@ public abstract class Type { } while (signature.charAt(dim) == '['); // Recurse, but just once, if the signature is ok final Type t = getType(signature.substring(dim)); - // corrected concurrent private static field acess + // corrected concurrent private static field access // consumed_chars += dim; // update counter - is replaced by final int temp = unwrap(CONSUMED_CHARS) + dim; wrap(CONSUMED_CHARS, temp); @@ -254,7 +256,7 @@ public abstract class Type { } /** - * Convert runtime java.lang.Class[] to BCEL Type objects. + * Convert runtime {@code java.lang.Class[]} to BCEL Type objects. * * @param classes an array of runtime class objects * @return array of corresponding Type objects @@ -286,6 +288,24 @@ public abstract class Type { return encode(1, index + 1); } + static String internalTypeNameToSignature(final String internalTypeName) { + if (Utils.isEmpty(internalTypeName) || Arrays.asList(Const.SHORT_TYPE_NAMES).contains(internalTypeName)) { + return internalTypeName; + } + switch (internalTypeName.charAt(0)) { + case '[': + return internalTypeName; + case 'L': + case 'T': + if (internalTypeName.charAt(internalTypeName.length() - 1) == ';') { + return internalTypeName; + } + return 'L' + internalTypeName + ';'; + default: + return 'L' + internalTypeName + ';'; + } + } + static int size(final int coded) { return coded & 3; } @@ -361,7 +381,7 @@ public abstract class Type { } /** - * @return hashcode of Type + * @return hash code of Type */ @Override public int hashCode() { @@ -369,31 +389,23 @@ public abstract class Type { } /** - * boolean, short and char variable are considered as int in the stack or local variable area. Returns {@link Type#INT} - * for {@link Type#BOOLEAN}, {@link Type#SHORT} or {@link Type#CHAR}, otherwise returns the given type. + * boolean, short and char variable are considered as int in the stack or local variable area. Returns {@link #INT} + * for {@link #BOOLEAN}, {@link #SHORT} or {@link #CHAR}, otherwise returns the given type. * * @since 6.0 */ public Type normalizeForStackOrLocal() { - if (this == Type.BOOLEAN || this == Type.BYTE || this == Type.SHORT || this == Type.CHAR) { - return Type.INT; + if (this == BOOLEAN || this == BYTE || this == SHORT || this == CHAR) { + return INT; } return this; } - /* - * Currently only used by the ArrayType constructor. The signature has a complicated dependency on other parameter so - * it's tricky to do it in a call to the super ctor. - */ - void setSignature(final String signature) { - this.signature = signature; - } - /** * @return Type string, e.g. 'int[]' */ @Override public String toString() { - return this.equals(Type.NULL) || type >= Const.T_UNKNOWN ? signature : Utility.signatureToString(signature, false); + return equals(NULL) || type >= Const.T_UNKNOWN ? signature : Utility.signatureToString(signature, false); } } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TypedInstruction.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TypedInstruction.java index 5a22942a6a7..27b952f3996 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TypedInstruction.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/TypedInstruction.java @@ -22,7 +22,7 @@ package com.sun.org.apache.bcel.internal.generic; /** - * Get the type associated with an instruction, int for ILOAD, or the type of the field of a PUTFIELD instruction, e.g.. + * Gets the type associated with an instruction, int for ILOAD, or the type of the field of a PUTFIELD instruction, e.g.. */ public interface TypedInstruction { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/package-info.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/package-info.java new file mode 100644 index 00000000000..63d4dc876ce --- /dev/null +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/package-info.java @@ -0,0 +1,26 @@ +/* + * reserved comment block + * DO NOT REMOVE OR ALTER! + */ +/* + * 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. + */ + +/** + * Generic part of the Apache Byte Code Engineering Library (BCEL), classes to dynamically modify class objects + * and byte code instructions. + */ +package com.sun.org.apache.bcel.internal.generic; diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/package-info.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/package-info.java new file mode 100644 index 00000000000..581037d7981 --- /dev/null +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/package-info.java @@ -0,0 +1,26 @@ +/* + * reserved comment block + * DO NOT REMOVE OR ALTER! + */ +/* + * 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. + */ + +/** + * Basic classes for the Apache Byte Code Engineering Library (BCEL) and constants defined by the + * JVM specification. + */ +package com.sun.org.apache.bcel.internal; diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELComparator.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELComparator.java index 2e663bfdaa1..fb32dc35e0b 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELComparator.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELComparator.java @@ -22,26 +22,27 @@ package com.sun.org.apache.bcel.internal.util; /** - * Used for BCEL comparison strategy + * Used for BCEL comparison strategy. * + * @param What type we are comparing. * @since 5.2 */ -public interface BCELComparator { +public interface BCELComparator { /** - * Compare two objects and return what THIS.equals(THAT) should return + * Compares two objects and return what a.equals(b) should return. * - * @param THIS - * @param THAT - * @return true if and only if THIS equals THAT + * @param a an object. + * @param b an object to be compared with {@code a} for equality. + * @return {@code true} if the arguments are equal to each other and {@code false} otherwise. */ - boolean equals(Object THIS, Object THAT); + boolean equals(T a, T b); /** - * Return hashcode for THIS.hashCode() + * Gets the hash code for o.hashCode() * - * @param THIS - * @return hashcode for THIS.hashCode() + * @param o + * @return hash code for o.hashCode() */ - int hashCode(Object THIS); + int hashCode(T o); } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELFactory.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELFactory.java index c931fb1e422..93f22e7d0ad 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELFactory.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -63,9 +63,9 @@ import com.sun.org.apache.bcel.internal.generic.Type; * Factory creates il.append() statements, and sets instruction targets. A helper class for BCELifier. * * @see BCELifier - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ -class BCELFactory extends EmptyVisitor { +final class BCELFactory extends EmptyVisitor { private static final String CONSTANT_PREFIX = Const.class.getSimpleName() + "."; private final MethodGen methodGen; @@ -88,7 +88,7 @@ class BCELFactory extends EmptyVisitor { if (value instanceof String) { embed = '"' + Utility.convertString(embed) + '"'; } else if (value instanceof Character) { - embed = "(char)0x" + Integer.toHexString(((Character) value).charValue()); + embed = "(char) 0x" + Integer.toHexString(((Character) value).charValue()); } else if (value instanceof Float) { final Float f = (Float) value; if (Float.isNaN(f)) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELifier.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELifier.java index 151083ea71b..0eb9bfb4ee1 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELifier.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/BCELifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -30,11 +30,15 @@ import java.util.Locale; import com.sun.org.apache.bcel.internal.Const; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.ClassParser; +import com.sun.org.apache.bcel.internal.classfile.Code; import com.sun.org.apache.bcel.internal.classfile.ConstantValue; import com.sun.org.apache.bcel.internal.classfile.ExceptionTable; import com.sun.org.apache.bcel.internal.classfile.Field; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Method; +import com.sun.org.apache.bcel.internal.classfile.StackMap; +import com.sun.org.apache.bcel.internal.classfile.StackMapEntry; +import com.sun.org.apache.bcel.internal.classfile.StackMapType; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.generic.ArrayType; import com.sun.org.apache.bcel.internal.generic.ConstantPoolGen; @@ -46,7 +50,7 @@ import com.sun.org.apache.bcel.internal.generic.Type; * This gives new users of BCEL a useful example showing how things are done with BCEL. It does not cover all features * of BCEL, but tries to mimic hand-written code as close as possible. * - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class BCELifier extends com.sun.org.apache.bcel.internal.classfile.EmptyVisitor { @@ -74,7 +78,7 @@ public class BCELifier extends com.sun.org.apache.bcel.internal.classfile.EmptyV /** * Default main method */ - public static void _main(final String[] argv) throws Exception { + public static void main(final String[] argv) throws Exception { if (argv.length != 1) { System.out.println("Usage: BCELifier className"); System.out.println("\tThe class must exist on the classpath"); @@ -311,6 +315,13 @@ public class BCELifier extends com.sun.org.apache.bcel.internal.classfile.EmptyV printWriter.println("\");"); } } + final Code code = method.getCode(); + if (code != null) { + final StackMap stackMap = code.getStackMap(); + if (stackMap != null) { + stackMap.accept(this); + } + } printWriter.println(); final BCELFactory factory = new BCELFactory(mg, printWriter); factory.start(); @@ -319,4 +330,78 @@ public class BCELifier extends com.sun.org.apache.bcel.internal.classfile.EmptyV printWriter.println(" _cg.addMethod(method.getMethod());"); printWriter.println(" il.dispose();"); } + + @Override + public void visitStackMap(final StackMap stackMap) { + super.visitStackMap(stackMap); + printWriter.print(" method.addCodeAttribute("); + printWriter.print("new StackMap(_cp.addUtf8(\""); + printWriter.print(stackMap.getName()); + printWriter.print("\"), "); + printWriter.print(stackMap.getLength()); + printWriter.print(", "); + printWriter.print("new StackMapEntry[] {"); + final StackMapEntry[] table = stackMap.getStackMap(); + for (int i = 0; i < table.length; i++) { + table[i].accept(this); + if (i < table.length - 1) { + printWriter.print(", "); + } else { + printWriter.print(" }"); + } + } + printWriter.print(", _cp.getConstantPool())"); + printWriter.println(");"); + } + + @Override + public void visitStackMapEntry(final StackMapEntry stackMapEntry) { + super.visitStackMapEntry(stackMapEntry); + printWriter.print("new StackMapEntry("); + printWriter.print(stackMapEntry.getFrameType()); + printWriter.print(", "); + printWriter.print(stackMapEntry.getByteCodeOffset()); + printWriter.print(", "); + visitStackMapTypeArray(stackMapEntry.getTypesOfLocals()); + printWriter.print(", "); + visitStackMapTypeArray(stackMapEntry.getTypesOfStackItems()); + printWriter.print(", _cp.getConstantPool())"); + } + + /** + * Visits a {@link StackMapType} object. + * @param stackMapType object to visit + * @since 6.7.1 + */ + @Override + public void visitStackMapType(final StackMapType stackMapType) { + super.visitStackMapType(stackMapType); + printWriter.print("new StackMapType((byte)"); + printWriter.print(stackMapType.getType()); + printWriter.print(", "); + if (stackMapType.hasIndex()) { + printWriter.print("_cp.addClass(\""); + printWriter.print(stackMapType.getClassName()); + printWriter.print("\")"); + } else { + printWriter.print("-1"); + } + printWriter.print(", _cp.getConstantPool())"); + } + + private void visitStackMapTypeArray(final StackMapType[] types) { + if (types == null || types.length == 0) { + printWriter.print("null"); // null translates to StackMapType.EMPTY_ARRAY + } else { + printWriter.print("new StackMapType[] {"); + for (int i = 0; i < types.length; i++) { + types[i].accept(this); + if (i < types.length - 1) { + printWriter.print(", "); + } else { + printWriter.print(" }"); + } + } + } + } } diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Class2HTML.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Class2HTML.java index f644c698b72..10d31974d47 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Class2HTML.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Class2HTML.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -53,7 +53,7 @@ import com.sun.org.apache.bcel.internal.classfile.Utility; * All subfiles reference each other appropriately, e.g. clicking on a method in the Method's frame will jump to the * appropriate method in the Code frame. * - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class Class2HTML { @@ -73,7 +73,7 @@ public class Class2HTML { basicTypes.add("float"); } - public static void _main(final String[] argv) throws IOException { + public static void main(final String[] argv) throws IOException { final String[] fileName = new String[argv.length]; int files = 0; ClassParser parser = null; @@ -89,7 +89,7 @@ public class Class2HTML { if (argv[i].equals("-d")) { // Specify target directory, default '.' dir = argv[++i]; if (!dir.endsWith("" + sep)) { - dir = dir + sep; + dir += sep; } final File store = new File(dir); if (!store.isDirectory()) { @@ -115,7 +115,7 @@ public class Class2HTML { if (zipFile == null) { parser = new ClassParser(fileName[i]); // Create parser object from file } else { - parser = new ClassParser(zipFile, fileName[i]); // Create parser object from zip file + parser = new ClassParser(zipFile, fileName[i]); // Create parser object from ZIP file } javaClass = parser.parse(); new Class2HTML(javaClass, dir); diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/ClassSet.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/ClassSet.java index 178ccc786b1..e95e8fbd0a0 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/ClassSet.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/ClassSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -30,7 +30,7 @@ import com.sun.org.apache.bcel.internal.classfile.JavaClass; * Utility class implementing a (type-safe) set of JavaClass objects. Since JavaClass has no equals() method, the name of the class is used for comparison. * * @see ClassStack - * @LastModified: Feb 2023 + * @LastModified: Sept 2025 */ public class ClassSet { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/CodeHTML.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/CodeHTML.java index 723fc07509b..1935d724b3b 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/CodeHTML.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/CodeHTML.java @@ -542,7 +542,7 @@ final class CodeHTML { final String str = codeToHTML(stream, methodNumber); String anchor = ""; /* - * Set an anchor mark if this line is targetted by a goto, jsr, etc. Defining an anchor for every line is very + * Sets an anchor mark if this line is targetted by a goto, jsr, etc. Defining an anchor for every line is very * inefficient! */ if (gotoSet.get(offset)) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/InstructionFinder.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/InstructionFinder.java index 5b045bf08cc..510fe4a03a6 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/InstructionFinder.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/InstructionFinder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -63,14 +63,13 @@ import com.sun.org.apache.bcel.internal.generic.InstructionList; * * @see com.sun.org.apache.bcel.internal.generic.Instruction * @see InstructionList - * @LastModified: May 2021 + * @LastModified: Sept 2025 */ public class InstructionFinder { /** * Code patterns found may be checked using an additional user-defined constraint object whether they really match the * needed criterion. I.e., check constraints that can not expressed with regular expressions. - * */ public interface CodeConstraint { @@ -374,7 +373,7 @@ public class InstructionFinder { // } // private static final String pattern2string( String pattern, boolean make_string ) { -// StringBuffer buf = new StringBuffer(); +// StringBuilder buf = new StringBuilder(); // for (int i = 0; i < pattern.length(); i++) { // char ch = pattern.charAt(i); // if (ch >= OFFSET) { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Repository.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Repository.java index 8d1ddf493cd..c44e36d0a81 100644 --- a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Repository.java +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/Repository.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. */ /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -25,8 +25,8 @@ import com.sun.org.apache.bcel.internal.classfile.JavaClass; * Abstract definition of a class repository. Instances may be used to load classes from different sources and may be * used in the Repository.setRepository method. * - * @see org.apache.bcel.Repository - * @LastModified: Feb 2023 + * @see com.sun.org.apache.bcel.internal.Repository + * @LastModified: Sept 2025 */ public interface Repository { diff --git a/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/package-info.java b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/package-info.java new file mode 100644 index 00000000000..2bd92f155bc --- /dev/null +++ b/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/util/package-info.java @@ -0,0 +1,32 @@ +/* + * reserved comment block + * DO NOT REMOVE OR ALTER! + */ +/* + * 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. + */ + +/** + * Utility classes for the Apache Byte Code Engineering Library (BCEL), namely: + *
          + *
        • Collection classes for JavaClass objects
        • + *
        • A converter for class files to HTML
        • + *
        • A tool to find instructions patterns via regular expressions
        • + *
        • A class to find classes as defined in the CLASSPATH
        • + *
        • A class loader that allows to create classes at run time
        • + *
        + */ +package com.sun.org.apache.bcel.internal.util; diff --git a/src/java.xml/share/classes/jdk/xml/internal/Utils.java b/src/java.xml/share/classes/jdk/xml/internal/Utils.java index f55ab95a58f..96d9b1b9521 100644 --- a/src/java.xml/share/classes/jdk/xml/internal/Utils.java +++ b/src/java.xml/share/classes/jdk/xml/internal/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,11 @@ package jdk.xml.internal; +import java.lang.reflect.Array; import java.util.Arrays; +import java.util.Objects; import java.util.function.Supplier; +import java.util.stream.Stream; /** * General utility. Use JdkXmlUtils for XML processing related functions. @@ -76,4 +79,59 @@ public class Utils { System.arraycopy(items, 0, result, original.length, items.length); return result; } + + /** + * Returns the original array, or an empty array if it is {@code null}. + * @param array the specified array + * @return the original array, or an empty array if it is {@code null} + */ + public static byte[] createEmptyArrayIfNull(byte[] array) { + return (array != null) ? array : new byte[0]; + } + + /** + * Returns the original array, or an empty array if it is {@code null}. + * @param array the specified array + * @return the original array, or an empty array if it is {@code null} + */ + public static int[] createEmptyArrayIfNull(int[] array) { + return (array != null) ? array : new int[0]; + } + + /** + * Returns the original array, or an empty array if it is {@code null}. + * @param the class type + * @param array the specified array + * @param type the type of the array + * @return the original array, or an empty array if it is {@code null} + */ + public static T[] createEmptyArrayIfNull(final T[] array, final Class type) { + Objects.requireNonNull(type, "The type argument should not be null."); + + return (array != null) ? array : type.cast(Array.newInstance(type.getComponentType(), 0)); + } + + /** + * Returns the new stream created by {@code Stream.of(values)} or an empty + * sequential stream created by {@code Stream.empty()} if values is null. + * + * @param the type of stream elements + * @param values the elements of the new stream + * @return the new stream created by {@code Stream.of(values)} or an empty + * sequential stream created by {@code Stream.empty()} if values is null. + */ + @SafeVarargs + @SuppressWarnings("varargs") // Creating a stream from an array is safe + public static Stream streamOfIfNonNull(final T... values) { + return values == null ? Stream.empty() : Stream.of(values); + } + + /** + * Checks if a CharSequence is empty ("") or null. + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is empty or null + */ + public static boolean isEmpty(final CharSequence cs) { + return cs == null || cs.length() == 0; + } } diff --git a/src/java.xml/share/legal/bcel.md b/src/java.xml/share/legal/bcel.md index 2c673d6b1af..b64fc3640d4 100644 --- a/src/java.xml/share/legal/bcel.md +++ b/src/java.xml/share/legal/bcel.md @@ -1,4 +1,4 @@ -## Apache Commons Byte Code Engineering Library (BCEL) Version 6.7.0 +## Apache Commons Byte Code Engineering Library (BCEL) Version 6.10.0 ### Apache Commons BCEL Notice
        
        From ebb6fd7d7865fd20ff2f90b4ef72c5ef6a3e5dea Mon Sep 17 00:00:00 2001
        From: Mikhail Yankelevich 
        Date: Fri, 3 Oct 2025 07:37:17 +0000
        Subject: [PATCH 341/556] 8360562: sun/security/tools/keytool/i18n.java add an
         ability to add comment for failures
        
        Reviewed-by: rhalade
        ---
         test/jdk/sun/security/tools/keytool/i18n.java | 46 ++++++++++++++++++-
         1 file changed, 44 insertions(+), 2 deletions(-)
        
        diff --git a/test/jdk/sun/security/tools/keytool/i18n.java b/test/jdk/sun/security/tools/keytool/i18n.java
        index 6eac0239eee..ab9da8b1d3e 100644
        --- a/test/jdk/sun/security/tools/keytool/i18n.java
        +++ b/test/jdk/sun/security/tools/keytool/i18n.java
        @@ -63,11 +63,21 @@
         
         import jdk.test.lib.UIBuilder;
         
        -import javax.swing.*;
        +import javax.swing.JDialog;
        +import javax.swing.SwingUtilities;
        +import javax.swing.JTextArea;
        +import javax.swing.JButton;
        +import javax.swing.JPanel;
        +import javax.swing.JScrollPane;
        +import javax.swing.JFrame;
        +import java.awt.FlowLayout;
        +import java.awt.BorderLayout;
         import java.io.ByteArrayOutputStream;
         import java.io.PrintStream;
         import java.util.Locale;
         
        +import static javax.swing.BorderFactory.createEmptyBorder;
        +
         public class i18n {
             private static final String[][] TABLE = new String[][]{
                     {"-help", "All the output in this test should be in ${LANG}. "
        @@ -234,11 +244,12 @@ public class i18n {
                             "Output in ${LANG}. Check keytool error: java.lang"
                                     + ".IllegalArgumentException: if -protected is "
                                     + "specified, then -storepass, -keypass, and -new "
        -                            + "must not be specified."},
        +                            + "must not be specified."}
             };
             private static String TEST_SRC = System.getProperty("test.src");
             private static int TIMEOUT_MS = 120000;
             private volatile boolean failed = false;
        +    private volatile String failureReason = "";
             private volatile boolean aborted = false;
             private Thread currentThread = null;
         
        @@ -330,6 +341,7 @@ public class i18n {
         
                     if (failed) {
                         System.out.println(command + ": TEST FAILED");
        +                System.out.println("REASON: " + failureReason);
                         System.out.println(message);
                     } else {
                         System.out.println(command + ": TEST PASSED");
        @@ -348,6 +360,7 @@ public class i18n {
         
             public void fail() {
                 failed = true;
        +        failureReason = requestFailDescription();
                 currentThread.interrupt();
             }
         
        @@ -355,4 +368,33 @@ public class i18n {
                 aborted = true;
                 currentThread.interrupt();
             }
        +
        +    /**
        +     * Opens a prompt to enter a failure reason to be filled by the tester
        +     */
        +     public static String requestFailDescription() {
        +
        +        final JDialog dialogWindow = new JDialog(new JFrame(), "Failure Description", true);
        +        final JTextArea reasonTextArea = new JTextArea(5, 20);
        +
        +        final JButton okButton = new JButton("OK");
        +        okButton.addActionListener(_ -> dialogWindow.setVisible(false));
        +
        +        final JPanel okayBtnPanel = new JPanel(
        +                new FlowLayout(FlowLayout.CENTER, 4, 0));
        +        okayBtnPanel.setBorder(createEmptyBorder(4, 0, 0, 0));
        +        okayBtnPanel.add(okButton);
        +
        +        final JPanel mainPanel = new JPanel(new BorderLayout());
        +        mainPanel.add(new JScrollPane(reasonTextArea), BorderLayout.CENTER);
        +        mainPanel.add(okayBtnPanel, BorderLayout.SOUTH);
        +
        +        dialogWindow.add(mainPanel);
        +        dialogWindow.pack();
        +        dialogWindow.setVisible(true);
        +
        +        dialogWindow.dispose();
        +
        +        return reasonTextArea.getText();
        +    }
         }
        
        From 2e783963d21c8edd88e79226ca473cfe0e41335b Mon Sep 17 00:00:00 2001
        From: =?UTF-8?q?Mar=C3=ADa=20Arias=20de=20Reyna=20Dom=C3=ADnguez?=
         
        Date: Fri, 3 Oct 2025 07:57:24 +0000
        Subject: [PATCH 342/556] 8369037: Identify owning method for MethodData and
         MethodCounters in AOT map output
        
        Reviewed-by: iklam, asmehra, adinn, macarte
        ---
         src/hotspot/share/cds/aotMapLogger.cpp | 20 ++++++++++++++++++++
         src/hotspot/share/cds/aotMapLogger.hpp |  4 ++++
         2 files changed, 24 insertions(+)
        
        diff --git a/src/hotspot/share/cds/aotMapLogger.cpp b/src/hotspot/share/cds/aotMapLogger.cpp
        index a15d7f670d4..ded3f05a9b8 100644
        --- a/src/hotspot/share/cds/aotMapLogger.cpp
        +++ b/src/hotspot/share/cds/aotMapLogger.cpp
        @@ -33,6 +33,8 @@
         #include "memory/metaspaceClosure.hpp"
         #include "memory/resourceArea.hpp"
         #include "oops/method.hpp"
        +#include "oops/methodCounters.hpp"
        +#include "oops/methodData.hpp"
         #include "oops/oop.inline.hpp"
         #include "runtime/fieldDescriptor.inline.hpp"
         #include "runtime/globals_extension.hpp"
        @@ -348,6 +350,12 @@ void AOTMapLogger::log_metaspace_objects_impl(address region_base, address regio
             case MetaspaceObj::MethodType:
               log_method((Method*)src, requested_addr, type_name, bytes, current);
               break;
        +    case MetaspaceObj::MethodCountersType:
        +      log_method_counters((MethodCounters*)src, requested_addr, type_name, bytes, current);
        +      break;
        +    case MetaspaceObj::MethodDataType:
        +      log_method_data((MethodData*)src, requested_addr, type_name, bytes, current);
        +      break;
             case MetaspaceObj::SymbolType:
               log_symbol((Symbol*)src, requested_addr, type_name, bytes, current);
               break;
        @@ -389,6 +397,18 @@ void AOTMapLogger::log_const_method(ConstMethod* cm, address requested_addr, con
           log_debug(aot, map)(_LOG_PREFIX " %s", p2i(requested_addr), type_name, bytes,  cm->method()->external_name());
         }
         
        +void AOTMapLogger::log_method_counters(MethodCounters* mc, address requested_addr, const char* type_name,
        +                                      int bytes, Thread* current) {
        +  ResourceMark rm(current);
        +  log_debug(aot, map)(_LOG_PREFIX " %s", p2i(requested_addr), type_name, bytes,  mc->method()->external_name());
        +}
        +
        +void AOTMapLogger::log_method_data(MethodData* md, address requested_addr, const char* type_name,
        +                                   int bytes, Thread* current) {
        +  ResourceMark rm(current);
        +  log_debug(aot, map)(_LOG_PREFIX " %s", p2i(requested_addr), type_name, bytes,  md->method()->external_name());
        +}
        +
         void AOTMapLogger::log_klass(Klass* k, address requested_addr, const char* type_name,
                                      int bytes, Thread* current) {
           ResourceMark rm(current);
        diff --git a/src/hotspot/share/cds/aotMapLogger.hpp b/src/hotspot/share/cds/aotMapLogger.hpp
        index 9cd67fb7ff6..f2ca4c407e3 100644
        --- a/src/hotspot/share/cds/aotMapLogger.hpp
        +++ b/src/hotspot/share/cds/aotMapLogger.hpp
        @@ -98,6 +98,10 @@ class AOTMapLogger : AllStatic {
           static void log_constant_pool_cache(ConstantPoolCache* cpc, address requested_addr,
                                               const char* type_name, int bytes, Thread* current);
           static void log_const_method(ConstMethod* cm, address requested_addr, const char* type_name, int bytes, Thread* current);
        +  static void log_method_counters(MethodCounters* mc, address requested_addr, const char* type_name, int bytes,
        +  Thread* current);
        +  static void log_method_data(MethodData* md, address requested_addr, const char* type_name, int bytes,
        +  Thread* current);
           static void log_klass(Klass* k, address requested_addr, const char* type_name, int bytes, Thread* current);
           static void log_method(Method* m, address requested_addr, const char* type_name, int bytes, Thread* current);
           static void log_symbol(Symbol* s, address requested_addr, const char* type_name, int bytes, Thread* current);
        
        From 134b63f0e8c4093f7ad0a528d6996898ab881d5c Mon Sep 17 00:00:00 2001
        From: Yuri Gaevsky 
        Date: Fri, 3 Oct 2025 09:44:56 +0000
        Subject: [PATCH 343/556] 8322174: RISC-V: C2 VectorizedHashCode RVV Version
        
        Reviewed-by: fyang, rehn
        ---
         .../cpu/riscv/c2_MacroAssembler_riscv.cpp     | 147 ++++++++++++++++--
         .../cpu/riscv/c2_MacroAssembler_riscv.hpp     |   8 +-
         src/hotspot/cpu/riscv/riscv.ad                |   1 +
         src/hotspot/cpu/riscv/riscv_v.ad              |  22 +++
         .../cpu/riscv/stubDeclarations_riscv.hpp      |   3 +
         src/hotspot/cpu/riscv/stubGenerator_riscv.cpp |  22 +++
         6 files changed, 185 insertions(+), 18 deletions(-)
        
        diff --git a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp
        index 1bdb7bc2f7c..154b62db47f 100644
        --- a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp
        +++ b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp
        @@ -1687,6 +1687,7 @@ void C2_MacroAssembler::arrays_hashcode(Register ary, Register cnt, Register res
                                                 Register tmp4, Register tmp5, Register tmp6,
                                                 BasicType eltype)
         {
        +  assert(!UseRVV, "sanity");
           assert_different_registers(ary, cnt, result, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, t0, t1);
         
           const int elsize = arrays_hashcode_elsize(eltype);
        @@ -1759,29 +1760,143 @@ void C2_MacroAssembler::arrays_hashcode(Register ary, Register cnt, Register res
           BLOCK_COMMENT("} // arrays_hashcode");
         }
         
        +void C2_MacroAssembler::arrays_hashcode_v(Register ary, Register cnt, Register result,
        +                                          Register tmp1, Register tmp2, Register tmp3,
        +                                          BasicType eltype)
        +{
        +  assert(UseRVV, "sanity");
        +  assert(StubRoutines::riscv::arrays_hashcode_powers_of_31() != nullptr, "sanity");
        +  assert_different_registers(ary, cnt, result, tmp1, tmp2, tmp3, t0, t1);
        +
        +  // The MaxVectorSize should have been set by detecting RVV max vector register
        +  // size when check UseRVV (i.e. MaxVectorSize == VM_Version::_initial_vector_length).
        +  // Let's use T_INT as all hashCode calculations eventually deal with ints.
        +  const int lmul = 2;
        +  const int stride = MaxVectorSize / sizeof(jint) * lmul;
        +
        +  const int elsize_bytes = arrays_hashcode_elsize(eltype);
        +  const int elsize_shift = exact_log2(elsize_bytes);
        +
        +  switch (eltype) {
        +    case T_BOOLEAN: BLOCK_COMMENT("arrays_hashcode_v(unsigned byte) {"); break;
        +    case T_CHAR:    BLOCK_COMMENT("arrays_hashcode_v(char) {");          break;
        +    case T_BYTE:    BLOCK_COMMENT("arrays_hashcode_v(byte) {");          break;
        +    case T_SHORT:   BLOCK_COMMENT("arrays_hashcode_v(short) {");         break;
        +    case T_INT:     BLOCK_COMMENT("arrays_hashcode_v(int) {");           break;
        +    default:
        +      ShouldNotReachHere();
        +  }
        +
        +  const Register pow31_highest = tmp1;
        +  const Register ary_end       = tmp2;
        +  const Register consumed      = tmp3;
        +
        +  const VectorRegister v_sum    = v2;
        +  const VectorRegister v_src    = v4;
        +  const VectorRegister v_coeffs = v6;
        +  const VectorRegister v_tmp    = v8;
        +
        +  const address adr_pows31 = StubRoutines::riscv::arrays_hashcode_powers_of_31()
        +                           + sizeof(jint);
        +  Label VEC_LOOP, DONE, SCALAR_TAIL, SCALAR_TAIL_LOOP;
        +
        +  // NB: at this point (a) 'result' already has some value,
        +  // (b) 'cnt' is not 0 or 1, see java code for details.
        +
        +  andi(t0, cnt, ~(stride - 1));
        +  beqz(t0, SCALAR_TAIL);
        +
        +  la(t1, ExternalAddress(adr_pows31));
        +  lw(pow31_highest, Address(t1, -1 * sizeof(jint)));
        +
        +  vsetvli(consumed, cnt, Assembler::e32, Assembler::m2);
        +  vle32_v(v_coeffs, t1); // 31^^(stride - 1) ... 31^^0
        +  vmv_v_x(v_sum, x0);
        +
        +  bind(VEC_LOOP);
        +  arrays_hashcode_elload_v(v_src, v_tmp, ary, eltype);
        +  vmul_vv(v_src, v_src, v_coeffs);
        +  vmadd_vx(v_sum, pow31_highest, v_src);
        +  mulw(result, result, pow31_highest);
        +  shadd(ary, consumed, ary, t0, elsize_shift);
        +  subw(cnt, cnt, consumed);
        +  andi(t1, cnt, ~(stride - 1));
        +  bnez(t1, VEC_LOOP);
        +
        +  vmv_s_x(v_tmp, x0);
        +  vredsum_vs(v_sum, v_sum, v_tmp);
        +  vmv_x_s(t0, v_sum);
        +  addw(result, result, t0);
        +  beqz(cnt, DONE);
        +
        +  bind(SCALAR_TAIL);
        +  shadd(ary_end, cnt, ary, t0, elsize_shift);
        +
        +  bind(SCALAR_TAIL_LOOP);
        +  arrays_hashcode_elload(t0, Address(ary), eltype);
        +  slli(t1, result, 5);      // optimize 31 * result
        +  subw(result, t1, result); // with result<<5 - result
        +  addw(result, result, t0);
        +  addi(ary, ary, elsize_bytes);
        +  bne(ary, ary_end, SCALAR_TAIL_LOOP);
        +
        +  bind(DONE);
        +  BLOCK_COMMENT("} // arrays_hashcode_v");
        +}
        +
         int C2_MacroAssembler::arrays_hashcode_elsize(BasicType eltype) {
           switch (eltype) {
        -  case T_BOOLEAN: return sizeof(jboolean);
        -  case T_BYTE:    return sizeof(jbyte);
        -  case T_SHORT:   return sizeof(jshort);
        -  case T_CHAR:    return sizeof(jchar);
        -  case T_INT:     return sizeof(jint);
        -  default:
        -    ShouldNotReachHere();
        -    return -1;
        +    case T_BOOLEAN: return sizeof(jboolean);
        +    case T_BYTE:    return sizeof(jbyte);
        +    case T_SHORT:   return sizeof(jshort);
        +    case T_CHAR:    return sizeof(jchar);
        +    case T_INT:     return sizeof(jint);
        +    default:
        +      ShouldNotReachHere();
        +      return -1;
           }
         }
         
         void C2_MacroAssembler::arrays_hashcode_elload(Register dst, Address src, BasicType eltype) {
           switch (eltype) {
        -  // T_BOOLEAN used as surrogate for unsigned byte
        -  case T_BOOLEAN: lbu(dst, src);   break;
        -  case T_BYTE:     lb(dst, src);   break;
        -  case T_SHORT:    lh(dst, src);   break;
        -  case T_CHAR:    lhu(dst, src);   break;
        -  case T_INT:      lw(dst, src);   break;
        -  default:
        -    ShouldNotReachHere();
        +    // T_BOOLEAN used as surrogate for unsigned byte
        +    case T_BOOLEAN: lbu(dst, src);   break;
        +    case T_BYTE:     lb(dst, src);   break;
        +    case T_SHORT:    lh(dst, src);   break;
        +    case T_CHAR:    lhu(dst, src);   break;
        +    case T_INT:      lw(dst, src);   break;
        +    default:
        +      ShouldNotReachHere();
        +  }
        +}
        +
        +void C2_MacroAssembler::arrays_hashcode_elload_v(VectorRegister vdst,
        +                                                 VectorRegister vtmp,
        +                                                 Register src,
        +                                                 BasicType eltype) {
        +  assert_different_registers(vdst, vtmp);
        +  switch (eltype) {
        +    case T_BOOLEAN:
        +      vle8_v(vtmp, src);
        +      vzext_vf4(vdst, vtmp);
        +      break;
        +    case T_BYTE:
        +      vle8_v(vtmp, src);
        +      vsext_vf4(vdst, vtmp);
        +      break;
        +    case T_CHAR:
        +      vle16_v(vtmp, src);
        +      vzext_vf2(vdst, vtmp);
        +      break;
        +    case T_SHORT:
        +      vle16_v(vtmp, src);
        +      vsext_vf2(vdst, vtmp);
        +      break;
        +    case T_INT:
        +      vle32_v(vdst, src);
        +      break;
        +    default:
        +      ShouldNotReachHere();
           }
         }
         
        diff --git a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp
        index 309ef8d9d5e..2d5339dc153 100644
        --- a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp
        +++ b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp
        @@ -92,11 +92,15 @@
                                Register tmp3, Register tmp4,
                                Register tmp5, Register tmp6,
                                BasicType eltype);
        -
        -  // helper function for arrays_hashcode
           int arrays_hashcode_elsize(BasicType eltype);
           void arrays_hashcode_elload(Register dst, Address src, BasicType eltype);
         
        +  void arrays_hashcode_v(Register ary, Register cnt, Register result,
        +                         Register tmp1, Register tmp2, Register tmp3,
        +                         BasicType eltype);
        +  void arrays_hashcode_elload_v(VectorRegister vdst, VectorRegister vtmp,
        +                                Register src, BasicType eltype);
        +
           void string_equals(Register r1, Register r2,
                              Register result, Register cnt1);
         
        diff --git a/src/hotspot/cpu/riscv/riscv.ad b/src/hotspot/cpu/riscv/riscv.ad
        index 1f14c499c34..009acd628a0 100644
        --- a/src/hotspot/cpu/riscv/riscv.ad
        +++ b/src/hotspot/cpu/riscv/riscv.ad
        @@ -10991,6 +10991,7 @@ instruct arrays_hashcode(iRegP_R11 ary, iRegI_R12 cnt, iRegI_R10 result, immI ba
                                  iRegLNoSp tmp3, iRegLNoSp tmp4,
                                  iRegLNoSp tmp5, iRegLNoSp tmp6, rFlagsReg cr)
         %{
        +  predicate(!UseRVV);
           match(Set result (VectorizedHashCode (Binary ary cnt) (Binary result basic_type)));
           effect(TEMP tmp1, TEMP tmp2, TEMP tmp3, TEMP tmp4, TEMP tmp5, TEMP tmp6,
                  USE_KILL ary, USE_KILL cnt, USE basic_type, KILL cr);
        diff --git a/src/hotspot/cpu/riscv/riscv_v.ad b/src/hotspot/cpu/riscv/riscv_v.ad
        index f2845ee2a6c..fe323474d60 100644
        --- a/src/hotspot/cpu/riscv/riscv_v.ad
        +++ b/src/hotspot/cpu/riscv/riscv_v.ad
        @@ -4080,6 +4080,28 @@ instruct varray_equalsC(iRegP_R11 ary1, iRegP_R12 ary2, iRegI_R10 result,
           ins_pipe(pipe_class_memory);
         %}
         
        +// fast ArraysSupport.vectorizedHashCode
        +instruct varrays_hashcode(iRegP_R11 ary, iRegI_R12 cnt, iRegI_R10 result, immI basic_type,
        +                          vReg_V2 v2, vReg_V3 v3, vReg_V4 v4, vReg_V5 v5,
        +                          vReg_V6 v6, vReg_V7 v7, vReg_V8 v8, vReg_V9 v9,
        +                          iRegLNoSp tmp1, iRegLNoSp tmp2, iRegLNoSp tmp3,
        +                          rFlagsReg cr)
        +%{
        +  predicate(UseRVV);
        +  match(Set result (VectorizedHashCode (Binary ary cnt) (Binary result basic_type)));
        +  effect(USE_KILL ary, USE_KILL cnt, USE basic_type,
        +         TEMP v2, TEMP v3, TEMP v4, TEMP v5, TEMP v6, TEMP v7, TEMP v8, TEMP v9,
        +         TEMP tmp1, TEMP tmp2, TEMP tmp3, KILL cr);
        +
        +  format %{ "Array HashCode array[] $ary,$cnt,$result,$basic_type -> $result   // KILL all" %}
        +  ins_encode %{
        +    __ arrays_hashcode_v($ary$$Register, $cnt$$Register, $result$$Register,
        +                         $tmp1$$Register, $tmp2$$Register, $tmp3$$Register,
        +                         (BasicType)$basic_type$$constant);
        +  %}
        +  ins_pipe(pipe_class_memory);
        +%}
        +
         instruct vstring_compareU_128b(iRegP_R11 str1, iRegI_R12 cnt1, iRegP_R13 str2, iRegI_R14 cnt2,
                                   iRegI_R10 result, vReg_V4 v4, vReg_V5 v5, vReg_V6 v6, vReg_V7 v7,
                                   vReg_V8 v8, vReg_V9 v9, vReg_V10 v10, vReg_V11 v11,
        diff --git a/src/hotspot/cpu/riscv/stubDeclarations_riscv.hpp b/src/hotspot/cpu/riscv/stubDeclarations_riscv.hpp
        index fe7f52884fa..f977d759d20 100644
        --- a/src/hotspot/cpu/riscv/stubDeclarations_riscv.hpp
        +++ b/src/hotspot/cpu/riscv/stubDeclarations_riscv.hpp
        @@ -73,6 +73,9 @@
           do_stub(compiler, string_indexof_linear_ul)                           \
           do_arch_entry(riscv, compiler, string_indexof_linear_ul,              \
                         string_indexof_linear_ul, string_indexof_linear_ul)     \
        +  do_stub(compiler, arrays_hashcode_powers_of_31)                       \
        +  do_arch_entry(riscv, compiler, arrays_hashcode_powers_of_31,          \
        +            arrays_hashcode_powers_of_31, arrays_hashcode_powers_of_31) \
         
         
         #define STUBGEN_FINAL_BLOBS_ARCH_DO(do_stub,                            \
        diff --git a/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp b/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp
        index 88961ccd5a4..ec268d9bb65 100644
        --- a/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp
        +++ b/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp
        @@ -6624,6 +6624,24 @@ static const int64_t right_3_bits = right_n_bits(3);
             return start;
           }
         
        +  address generate_arrays_hashcode_powers_of_31() {
        +    assert(UseRVV, "sanity");
        +    const int lmul = 2;
        +    const int stride = MaxVectorSize / sizeof(jint) * lmul;
        +    __ align(CodeEntryAlignment);
        +    StubCodeMark mark(this, "StubRoutines", "arrays_hashcode_powers_of_31");
        +    address start = __ pc();
        +    for (int i = stride; i >= 0; i--) {
        +        jint power_of_31 = 1;
        +        for (int j = i; j > 0; j--) {
        +          power_of_31 = java_multiply(power_of_31, 31);
        +        }
        +        __ emit_int32(power_of_31);
        +    }
        +
        +    return start;
        +  }
        +
         #endif // COMPILER2
         
           /**
        @@ -6818,6 +6836,10 @@ static const int64_t right_3_bits = right_n_bits(3);
               StubRoutines::_bigIntegerRightShiftWorker = generate_bigIntegerRightShift();
             }
         
        +    if (UseVectorizedHashCodeIntrinsic && UseRVV) {
        +      StubRoutines::riscv::_arrays_hashcode_powers_of_31 = generate_arrays_hashcode_powers_of_31();
        +    }
        +
             if (UseSHA256Intrinsics) {
               Sha2Generator sha2(_masm, this);
               StubRoutines::_sha256_implCompress   = sha2.generate_sha256_implCompress(StubId::stubgen_sha256_implCompress_id);
        
        From 72319167543a28295276f11178c17bef6680c32f Mon Sep 17 00:00:00 2001
        From: =?UTF-8?q?Beno=C3=AEt=20Maillard?= 
        Date: Fri, 3 Oct 2025 10:40:50 +0000
        Subject: [PATCH 344/556] 8364757: Missing Store nodes caused by bad wiring in
         PhaseIdealLoop::insert_post_loop
        
        Reviewed-by: mhaessig, roland
        ---
         src/hotspot/share/opto/loopTransform.cpp      |  44 ++++++
         src/hotspot/share/opto/loopnode.hpp           |   3 +
         .../MissingStoreAfterOuterStripMinedLoop.java | 136 ++++++++++++++++++
         3 files changed, 183 insertions(+)
         create mode 100644 test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java
        
        diff --git a/src/hotspot/share/opto/loopTransform.cpp b/src/hotspot/share/opto/loopTransform.cpp
        index 5f5e0520e7e..f92833e9e1c 100644
        --- a/src/hotspot/share/opto/loopTransform.cpp
        +++ b/src/hotspot/share/opto/loopTransform.cpp
        @@ -1668,6 +1668,30 @@ void PhaseIdealLoop::insert_vector_post_loop(IdealLoopTree *loop, Node_List &old
           loop->record_for_igvn();
         }
         
        +Node* PhaseIdealLoop::find_last_store_in_outer_loop(Node* store, const IdealLoopTree* outer_loop) {
        +  assert(store != nullptr && store->is_Store(), "starting point should be a store node");
        +  // Follow the memory uses until we get out of the loop.
        +  // Store nodes in the outer loop body were moved by PhaseIdealLoop::try_move_store_after_loop.
        +  // Because of the conditions in try_move_store_after_loop (no other usage in the loop body
        +  // except for the phi node associated with the loop head), we have the guarantee of a
        +  // linear memory subgraph within the outer loop body.
        +  Node* last = store;
        +  Node* unique_next = store;
        +  do {
        +    last = unique_next;
        +    for (DUIterator_Fast imax, l = last->fast_outs(imax); l < imax; l++) {
        +      Node* use = last->fast_out(l);
        +      if (use->is_Store() && use->in(MemNode::Memory) == last) {
        +        if (is_member(outer_loop, get_ctrl(use))) {
        +          assert(unique_next == last, "memory node should only have one usage in the loop body");
        +          unique_next = use;
        +        }
        +      }
        +    }
        +  } while (last != unique_next);
        +  return last;
        +}
        +
         //------------------------------insert_post_loop-------------------------------
         // Insert post loops.  Add a post loop to the given loop passed.
         Node *PhaseIdealLoop::insert_post_loop(IdealLoopTree* loop, Node_List& old_new,
        @@ -1758,6 +1782,26 @@ Node *PhaseIdealLoop::insert_post_loop(IdealLoopTree* loop, Node_List& old_new,
               cur_phi->set_req(LoopNode::EntryControl, fallnew);
             }
           }
        +  // Store nodes that were moved to the outer loop by PhaseIdealLoop::try_move_store_after_loop
        +  // do not have an associated Phi node. Such nodes are attached to the false projection of the CountedLoopEnd node,
        +  // right after the execution of the inner CountedLoop.
        +  // We have to make sure that such stores in the post loop have the right memory inputs from the main loop
        +  // The moved store node is always attached right after the inner loop exit, and just before the safepoint
        +  const Node* if_false = main_end->proj_out(false);
        +  for (DUIterator j = if_false->outs(); if_false->has_out(j); j++) {
        +    Node* store = if_false->out(j);
        +    if (store->is_Store()) {
        +      // We only make changes if the memory input of the store is outside the outer loop body,
        +      // as this is when we would normally expect a Phi as input. If the memory input
        +      // is in the loop body as well, then we can safely assume it is still correct as the entire
        +      // body was cloned as a unit
        +      if (!is_member(outer_loop, get_ctrl(store->in(MemNode::Memory)))) {
        +        Node* mem_out = find_last_store_in_outer_loop(store, outer_loop);
        +        Node* store_new = old_new[store->_idx];
        +        store_new->set_req(MemNode::Memory, mem_out);
        +      }
        +    }
        +  }
         
           DEBUG_ONLY(ensure_zero_trip_guard_proj(post_head->in(LoopNode::EntryControl), false);)
           initialize_assertion_predicates_for_post_loop(main_head, post_head, first_node_index_in_cloned_loop_body);
        diff --git a/src/hotspot/share/opto/loopnode.hpp b/src/hotspot/share/opto/loopnode.hpp
        index bdccffa18b2..2645df86d96 100644
        --- a/src/hotspot/share/opto/loopnode.hpp
        +++ b/src/hotspot/share/opto/loopnode.hpp
        @@ -1380,6 +1380,9 @@ public:
           // during RCE, unrolling and aligning loops.
           void insert_pre_post_loops( IdealLoopTree *loop, Node_List &old_new, bool peel_only );
         
        +  // Find the last store in the body of an OuterStripMinedLoop when following memory uses
        +  Node *find_last_store_in_outer_loop(Node* store, const IdealLoopTree* outer_loop);
        +
           // Add post loop after the given loop.
           Node *insert_post_loop(IdealLoopTree* loop, Node_List& old_new,
                                  CountedLoopNode* main_head, CountedLoopEndNode* main_end,
        diff --git a/test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java b/test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java
        new file mode 100644
        index 00000000000..8fa6cae12e6
        --- /dev/null
        +++ b/test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java
        @@ -0,0 +1,136 @@
        +/*
        + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
        + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
        + *
        + * This code is free software; you can redistribute it and/or modify it
        + * under the terms of the GNU General Public License version 2 only, as
        + * published by the Free Software Foundation.
        + *
        + * This code is distributed in the hope that it will be useful, but WITHOUT
        + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
        + * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
        + * version 2 for more details (a copy is included in the LICENSE file that
        + * accompanied this code).
        + *
        + * You should have received a copy of the GNU General Public License version
        + * 2 along with this work; if not, write to the Free Software Foundation,
        + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
        + *
        + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
        + * or visit www.oracle.com if you need additional information or have any
        + * questions.
        + */
        +
        +/**
        + * @test
        + * @bug 8364757
        + * @summary Moving Store nodes from the main CountedLoop to the OuterStripMinedLoop causes
        + *          subsequent Store nodes to be eventually removed because of missing Phi nodes,
        + *          leading to wrong results.
        + *
        + * @run main/othervm -XX:+IgnoreUnrecognizedVMOptions -XX:-TieredCompilation
        + *      -Xcomp -XX:-UseLoopPredicate -XX:-UseAutoVectorizationPredicate
        + *      -XX:CompileCommand=compileonly,compiler.loopstripmining.MissingStoreAfterOuterStripMinedLoop::test*
        + *      compiler.loopstripmining.MissingStoreAfterOuterStripMinedLoop
        + * @run main compiler.loopstripmining.MissingStoreAfterOuterStripMinedLoop
        + *
        + */
        +
        +package compiler.loopstripmining;
        +
        +public class MissingStoreAfterOuterStripMinedLoop {
        +    public static int x = 0;
        +    public static int y = 0;
        +
        +    static class A {
        +        int field;
        +    }
        +
        +    // The store node in the loop body is moved to the OuterStripLoop.
        +    // When making the post loop the new store node
        +    // should have the moved store node as memory input, and not the
        +    // initial x = 0 store.
        +    //
        +    // store (x = 0)
        +    //  |
        +    // store (x += 1, exit of CountedLoop main)
        +    //  | <-- additional rewiring due to absence of phi node
        +    // store (x += 1, exit of CountedLoop post)
        +    //  |
        +    // store (x = 0)
        +    static public void test1() {
        +        x = 0;
        +        for (int i = 0; i < 20000; i++) {
        +            x += i;
        +        }
        +        x = 0;
        +    }
        +
        +    // Two independent stores
        +    // They should be wired independently in the post loop, no aliasing
        +    static public void test2() {
        +        x = 0;
        +        y = 0;
        +        for (int i = 0; i < 20000; i++) {
        +            x += i;
        +            y += i;
        +        }
        +        x = 0;
        +        y = 0;
        +    }
        +
        +    // Chain of stores with potential aliasing.
        +    // The entire chain is moved to the OuterStripLoop, between the
        +    // inner loop exit and the safepoint.
        +    // The chain should be preserved when cloning the main loop body
        +    // to create the post loop. Only the first store of the post loop
        +    // should be rewired to have the last store of the main loop
        +    // as memory input.
        +    //
        +    // ...
        +    //  |
        +    // store (a1.field = v, exit of CountedLoop main)
        +    //  |
        +    // store (a2.field = v, exit of CountedLoop main)
        +    //  |
        +    // store (a3.field = v, exit of CountedLoop main)
        +    //  | <-- only additional rewiring needed
        +    // store (a1.field = v, exit of CountedLoop post)
        +    //  |
        +    // store (a2.field = v, exit of CountedLoop post)
        +    //  |
        +    // store (a3.field = v, exit of CountedLoop post)
        +    static public void test3(A a1, A a2, A a3) {
        +        a1.field = 0;
        +        a2.field = 0;
        +        a3.field = 0;
        +        int v = 0;
        +        for (int i = 0; i < 20000; i++) {
        +            v++;
        +            a1.field = v;
        +            a2.field = v;
        +            a3.field = v;
        +        }
        +    }
        +
        +    public static void main(String[] strArr) {
        +        A a1 = new A();
        +        A a2 = new A();
        +        A a3 = new A();
        +
        +        test1();
        +        if (x != 0) {
        +            throw new RuntimeException("unexpected value: " + x);
        +        }
        +
        +        test2();
        +        if (x != 0 || y != 0) {
        +            throw new RuntimeException("unexpected value: " + x + " " + y);
        +        }
        +
        +        test3(a1, a2, a3);
        +        if (a1.field != 20000 || a2.field != 20000 || a3.field != 20000) {
        +            throw new RuntimeException("unexpected value: " + a1.field + " " + a2.field + " " + a3.field);
        +        }
        +    }
        +}
        \ No newline at end of file
        
        From f81c7c592bbc9f5575ed41e41d12f54cbfc5e4aa Mon Sep 17 00:00:00 2001
        From: =?UTF-8?q?Hannes=20Walln=C3=B6fer?= 
        Date: Fri, 3 Oct 2025 15:53:37 +0000
        Subject: [PATCH 345/556] 8276966: Improve diagnostic output for the
         mismatching parts of a hybrid snippet
        
        Reviewed-by: prappo
        ---
         .../formats/html/taglets/SnippetTaglet.java    |  1 +
         .../doclet/testSnippetTag/TestSnippetTag.java  | 18 ++++++++++++++----
         2 files changed, 15 insertions(+), 4 deletions(-)
        
        diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/taglets/SnippetTaglet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/taglets/SnippetTaglet.java
        index 32bcab0eef5..d16c47dd59f 100644
        --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/taglets/SnippetTaglet.java
        +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/taglets/SnippetTaglet.java
        @@ -462,6 +462,7 @@ public class SnippetTaglet extends BaseTaglet {
                        %s
                        ----------------- external -----------------
                        %s
        +               --------------------------------------------
                        """.formatted(inline, external);
             }
         
        diff --git a/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java b/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java
        index 413de03d23b..26030629738 100644
        --- a/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java
        +++ b/test/langtools/jdk/javadoc/doclet/testSnippetTag/TestSnippetTag.java
        @@ -1,5 +1,5 @@
         /*
        - * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
        + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
          * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
          *
          * This code is free software; you can redistribute it and/or modify it
        @@ -23,7 +23,7 @@
         
         /*
          * @test
        - * @bug 8266666 8275788 8276964 8299080
        + * @bug 8266666 8275788 8276964 8299080 8276966
          * @summary Implementation for snippets
          * @library /tools/lib ../../lib
          * @modules jdk.compiler/com.sun.tools.javac.api
        @@ -2166,6 +2166,7 @@ public class TestSnippetTag extends SnippetTester {
                                 Hello, Snippet!
                                 ----------------- external -----------------
                                 Hello, Snippet!...more
        +                        --------------------------------------------
                                 
        """); @@ -2207,8 +2208,16 @@ public class TestSnippetTag extends SnippetTester { "pkg"); checkExit(Exit.ERROR); checkOutput(Output.OUT, true, - """ - A.java:4: error: contents mismatch"""); + """ + A.java:4: error: contents mismatch""", + """ + ----------------- inline ------------------- + Hello, Snippet! ...more + \s\s + ----------------- external ----------------- + Hello, Snippet! + \s\s + --------------------------------------------"""); checkOutput("pkg/A.html", true, """
        invalid @snippet @@ -2219,6 +2228,7 @@ public class TestSnippetTag extends SnippetTester { ----------------- external ----------------- Hello, Snippet! + --------------------------------------------
        """); From 23a65644ae63b271ca99c55a2a60a192c4e4dfb8 Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Fri, 3 Oct 2025 16:25:55 +0000 Subject: [PATCH 346/556] 8368981: Case Fold Locale Legacy Tags On Demand Reviewed-by: rriggs, naoto --- .../classes/sun/util/locale/LanguageTag.java | 158 ++++++++---------- .../util/Locale/CaseFoldLanguageTagTest.java | 68 ++++++-- 2 files changed, 118 insertions(+), 108 deletions(-) diff --git a/src/java.base/share/classes/sun/util/locale/LanguageTag.java b/src/java.base/share/classes/sun/util/locale/LanguageTag.java index 6036c1dd04f..0b2fee7f2cd 100644 --- a/src/java.base/share/classes/sun/util/locale/LanguageTag.java +++ b/src/java.base/share/classes/sun/util/locale/LanguageTag.java @@ -34,17 +34,21 @@ package sun.util.locale; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.IllformedLocaleException; import java.util.List; import java.util.Locale; -import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.StringJoiner; // List fields are unmodifiable -public record LanguageTag(String language, String script, String region, String privateuse, - List extlangs, List variants, List extensions) { +public record LanguageTag(String language, + String script, + String region, + String privateuse, + List extlangs, + List variants, + List extensions) { public static final String SEP = "-"; public static final String PRIVATEUSE = "x"; @@ -53,78 +57,6 @@ public record LanguageTag(String language, String script, String region, String private static final String EMPTY_SUBTAG = ""; private static final List EMPTY_SUBTAGS = List.of(); - // Map contains legacy language tags and its preferred mappings from - // http://www.ietf.org/rfc/rfc5646.txt - // Keys are lower-case strings. - private static final Map LEGACY; - - static { - // grandfathered = irregular ; non-redundant tags registered - // / regular ; during the RFC 3066 era - // - // irregular = "en-GB-oed" ; irregular tags do not match - // / "i-ami" ; the 'langtag' production and - // / "i-bnn" ; would not otherwise be - // / "i-default" ; considered 'well-formed' - // / "i-enochian" ; These tags are all valid, - // / "i-hak" ; but most are deprecated - // / "i-klingon" ; in favor of more modern - // / "i-lux" ; subtags or subtag - // / "i-mingo" ; combination - // / "i-navajo" - // / "i-pwn" - // / "i-tao" - // / "i-tay" - // / "i-tsu" - // / "sgn-BE-FR" - // / "sgn-BE-NL" - // / "sgn-CH-DE" - // - // regular = "art-lojban" ; these tags match the 'langtag' - // / "cel-gaulish" ; production, but their subtags - // / "no-bok" ; are not extended language - // / "no-nyn" ; or variant subtags: their meaning - // / "zh-guoyu" ; is defined by their registration - // / "zh-hakka" ; and all of these are deprecated - // / "zh-min" ; in favor of a more modern - // / "zh-min-nan" ; subtag or sequence of subtags - // / "zh-xiang" - - final String[][] entries = { - //{"tag", "preferred"}, - {"art-lojban", "jbo"}, - {"cel-gaulish", "xtg-x-cel-gaulish"}, // fallback - {"en-GB-oed", "en-GB-x-oed"}, // fallback - {"i-ami", "ami"}, - {"i-bnn", "bnn"}, - {"i-default", "en-x-i-default"}, // fallback - {"i-enochian", "und-x-i-enochian"}, // fallback - {"i-hak", "hak"}, - {"i-klingon", "tlh"}, - {"i-lux", "lb"}, - {"i-mingo", "see-x-i-mingo"}, // fallback - {"i-navajo", "nv"}, - {"i-pwn", "pwn"}, - {"i-tao", "tao"}, - {"i-tay", "tay"}, - {"i-tsu", "tsu"}, - {"no-bok", "nb"}, - {"no-nyn", "nn"}, - {"sgn-BE-FR", "sfb"}, - {"sgn-BE-NL", "vgt"}, - {"sgn-CH-DE", "sgg"}, - {"zh-guoyu", "cmn"}, - {"zh-hakka", "hak"}, - {"zh-min", "nan-x-zh-min"}, // fallback - {"zh-min-nan", "nan"}, - {"zh-xiang", "hsn"}, - }; - LEGACY = HashMap.newHashMap(entries.length); - for (String[] e : entries) { - LEGACY.put(LocaleUtils.toLowerString(e[0]), e); - } - } - /* * BNF in RFC5646 * @@ -175,14 +107,10 @@ public record LanguageTag(String language, String script, String region, String StringTokenIterator itr; var errorMsg = new StringBuilder(); - // Check if the tag is a legacy language tag - String[] gfmap = LEGACY.get(LocaleUtils.toLowerString(languageTag)); - if (gfmap != null) { - // use preferred mapping - itr = new StringTokenIterator(gfmap[1], SEP); - } else { - itr = new StringTokenIterator(languageTag, SEP); - } + // Check if the tag is a legacy tag + var pref = legacyToPreferred(LocaleUtils.toLowerString(languageTag)); + // If legacy use preferred mapping, otherwise use the tag as is + itr = new StringTokenIterator(Objects.requireNonNullElse(pref, languageTag), SEP); String language = parseLanguage(itr, pp); List extlangs; @@ -400,15 +328,24 @@ public record LanguageTag(String language, String script, String region, String public static String caseFoldTag(String tag) { parse(tag, new ParsePosition(0), false); + StringBuilder bldr = new StringBuilder(tag.length()); + String[] subtags = tag.split(SEP); // Legacy tags - String potentialLegacy = tag.toLowerCase(Locale.ROOT); - if (LEGACY.containsKey(potentialLegacy)) { - return LEGACY.get(potentialLegacy)[0]; + if (legacyToPreferred(tag.toLowerCase(Locale.ROOT)) != null) { + // Fold the legacy tag + for (int i = 0; i < subtags.length ; i++) { + // 2 ALPHA Region subtag(s) are upper, all other subtags are lower + if (i > 0 && subtags[i].length() == 2) { + bldr.append(LocaleUtils.toUpperString(subtags[i])).append(SEP); + } else { + bldr.append(LocaleUtils.toLowerString(subtags[i])).append(SEP); + } + } + bldr.setLength(bldr.length() - 1); // Remove trailing '-' + return bldr.toString(); } // Non-legacy tags - StringBuilder bldr = new StringBuilder(tag.length()); - String[] subtags = tag.split("-"); boolean privateFound = false; boolean singletonFound = false; boolean privUseVarFound = false; @@ -435,7 +372,7 @@ public record LanguageTag(String language, String script, String region, String bldr.append(subtag.toLowerCase(Locale.ROOT)); } if (i != subtags.length-1) { - bldr.append("-"); + bldr.append(SEP); } } return bldr.substring(0); @@ -567,6 +504,47 @@ public record LanguageTag(String language, String script, String region, String return new LanguageTag(language, script, region, privateuse, EMPTY_SUBTAGS, variants, extensions); } + /* + * Converts a legacy tag to its preferred mapping if it exists, otherwise null. + * The keys are mapped and stored as lower case. (Folded on demand). + * See http://www.ietf.org/rfc/rfc5646.txt Section 2.1 and 2.2.8 for the + * full syntax and case accurate legacy tags. + */ + private static String legacyToPreferred(String tag) { + if (tag.length() < 5) { + return null; + } + return switch (tag) { + case "art-lojban" -> "jbo"; + case "cel-gaulish" -> "xtg-x-cel-gaulish"; // fallback + case "en-gb-oed" -> "en-GB-x-oed"; // fallback + case "i-ami" -> "ami"; + case "i-bnn" -> "bnn"; + case "i-default" -> "en-x-i-default"; // fallback + case "i-enochian" -> "und-x-i-enochian"; // fallback + case "i-hak", + "zh-hakka" -> "hak"; + case "i-klingon" -> "tlh"; + case "i-lux" -> "lb"; + case "i-mingo" -> "see-x-i-mingo"; // fallback + case "i-navajo" -> "nv"; + case "i-pwn" -> "pwn"; + case "i-tao" -> "tao"; + case "i-tay" -> "tay"; + case "i-tsu" -> "tsu"; + case "no-bok" -> "nb"; + case "no-nyn" -> "nn"; + case "sgn-be-fr" -> "sfb"; + case "sgn-be-nl" -> "vgt"; + case "sgn-ch-de" -> "sgg"; + case "zh-guoyu" -> "cmn"; + case "zh-min" -> "nan-x-zh-min"; // fallback + case "zh-min-nan" -> "nan"; + case "zh-xiang" -> "hsn"; + default -> null; + }; + } + // // Language subtag syntax checking methods // diff --git a/test/jdk/java/util/Locale/CaseFoldLanguageTagTest.java b/test/jdk/java/util/Locale/CaseFoldLanguageTagTest.java index fdee5075229..f3babb7e4c2 100644 --- a/test/jdk/java/util/Locale/CaseFoldLanguageTagTest.java +++ b/test/jdk/java/util/Locale/CaseFoldLanguageTagTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,12 +23,11 @@ /* * @test - * @bug 8159337 + * @bug 8159337 8368981 * @summary Test Locale.caseFoldLanguageTag(String languageTag) * @run junit CaseFoldLanguageTagTest */ -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -54,24 +53,67 @@ public class CaseFoldLanguageTagTest { @ParameterizedTest @MethodSource("wellFormedTags") - public void wellFormedTags(String tag, String foldedTag) { + void wellFormedTagsTest(String tag, String foldedTag) { assertEquals(foldedTag, Locale.caseFoldLanguageTag(tag), String.format("Folded %s", tag)); } + @ParameterizedTest + @MethodSource("legacyTags") + void legacyTagsTest(String tag) { + var lowerTag = tag.toLowerCase(Locale.ROOT); + var upperTag = tag.toUpperCase(Locale.ROOT); + assertEquals(tag, Locale.caseFoldLanguageTag(lowerTag), + String.format("Folded %s", lowerTag)); + assertEquals(tag, Locale.caseFoldLanguageTag(upperTag), + String.format("Folded %s", upperTag)); + } + @ParameterizedTest @MethodSource("illFormedTags") - public void illFormedTags(String tag) { + void illFormedTagsTest(String tag) { assertThrows(IllformedLocaleException.class, () -> Locale.caseFoldLanguageTag(tag)); } @Test - public void throwNPE() { + void throwNPETest() { assertThrows(NullPointerException.class, () -> Locale.caseFoldLanguageTag(null)); } - private static Stream wellFormedTags() { + // Well-formed legacy tags in expected case + static Stream legacyTags() { + return Stream.of( + "art-lojban", + "cel-gaulish", + "en-GB-oed", + "i-ami", + "i-bnn", + "i-default", + "i-enochian", + "i-hak", + "i-klingon", + "i-lux", + "i-mingo", + "i-navajo", + "i-pwn", + "i-tao", + "i-tay", + "i-tsu", + "no-bok", + "no-nyn", + "sgn-BE-FR", + "sgn-BE-NL", + "sgn-CH-DE", + "zh-guoyu", + "zh-hakka", + "zh-min", + "zh-min-nan", + "zh-xiang" + ); + } + + static Stream wellFormedTags() { return Stream.of( // langtag tests // language @@ -124,16 +166,6 @@ public class CaseFoldLanguageTagTest { Arguments.of("X-A-ABC", "x-a-abc"), // private w/ extended (incl. 1) Arguments.of("X-A-AB-Abcd", "x-a-ab-abcd"), // private w/ extended (incl. 1, 2, 4) - // Legacy tests - // irregular - Arguments.of("I-AMI", "i-ami"), - Arguments.of("EN-gb-OED", "en-GB-oed"), - Arguments.of("SGN-be-fr", "sgn-BE-FR"), - // regular - Arguments.of("NO-BOK", "no-bok"), - Arguments.of("CEL-GAULISH", "cel-gaulish"), - Arguments.of("ZH-MIN-NAN", "zh-min-nan"), - // Special JDK Cases (Variant and x-lvariant) Arguments.of("de-POSIX-x-URP-lvariant-Abc-Def", "de-POSIX-x-urp-lvariant-Abc-Def"), Arguments.of("JA-JPAN-JP-U-CA-JAPANESE-x-RANDOM-lvariant-JP", @@ -150,7 +182,7 @@ public class CaseFoldLanguageTagTest { ); } - private static Stream illFormedTags() { + static Stream illFormedTags() { return Stream.of( // Starts with non-language Arguments.of("xabadadoo-me"), From aee73d3568fbcb2fe7293f92154e6677c080d20c Mon Sep 17 00:00:00 2001 From: Harshitha Onkar Date: Fri, 3 Oct 2025 17:32:51 +0000 Subject: [PATCH 347/556] 8365424: [macos26] java/awt/Frame/DisposeTest.java fails on macOS 26 Reviewed-by: serb, azvegint --- test/jdk/java/awt/Frame/DisposeTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/jdk/java/awt/Frame/DisposeTest.java b/test/jdk/java/awt/Frame/DisposeTest.java index 08c0def638e..ea1b2428f5c 100644 --- a/test/jdk/java/awt/Frame/DisposeTest.java +++ b/test/jdk/java/awt/Frame/DisposeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -44,6 +44,7 @@ import java.io.IOException; public class DisposeTest { private static Frame backgroundFrame; private static Frame testedFrame; + private static final int PIXEL_OFFSET = 4; private static final Rectangle backgroundFrameBounds = new Rectangle(100, 100, 200, 200); @@ -74,8 +75,8 @@ public class DisposeTest { BufferedImage bi = robot.createScreenCapture(backgroundFrameBounds); int redPix = Color.RED.getRGB(); - for (int x = 0; x < bi.getWidth(); x++) { - for (int y = 0; y < bi.getHeight(); y++) { + for (int x = PIXEL_OFFSET; x < bi.getWidth() - PIXEL_OFFSET; x++) { + for (int y = PIXEL_OFFSET; y < bi.getHeight() - PIXEL_OFFSET; y++) { if (bi.getRGB(x, y) != redPix) { try { ImageIO.write(bi, "png", From 0935b76c6bd1fbfa0a431eedb54c51f6fe4d194e Mon Sep 17 00:00:00 2001 From: Mikael Vidstedt Date: Fri, 3 Oct 2025 17:40:37 +0000 Subject: [PATCH 348/556] 8369080: Use uname -m for devkit cpu detection Reviewed-by: iris, erikj --- make/devkit/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/make/devkit/Makefile b/make/devkit/Makefile index d2167bf33fa..ffa23508a13 100644 --- a/make/devkit/Makefile +++ b/make/devkit/Makefile @@ -1,5 +1,5 @@ # -# Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -58,7 +58,7 @@ COMMA := , os := $(shell uname -o) -cpu := $(shell uname -p) +cpu := $(shell uname -m) # Figure out what platform this is building on. me := $(cpu)-$(if $(findstring Linux,$(os)),linux-gnu) From 0e98ec36623d5d83172209058574a97bab1d6038 Mon Sep 17 00:00:00 2001 From: Sergey Bylokhov Date: Fri, 3 Oct 2025 17:49:59 +0000 Subject: [PATCH 349/556] 8367384: The ICC_Profile class may throw exceptions during serialization Reviewed-by: honkar, prr --- .../classes/java/awt/color/ICC_Profile.java | 41 +++----- .../SerializationSpecTest.java | 99 ++++++++++++++++++ .../SerializationSpecTest/empty.ser | Bin 0 -> 130 bytes .../SerializationSpecTest/invalid.ser | Bin 0 -> 141 bytes .../SerializationSpecTest/invalid_invalid.ser | Bin 0 -> 142 bytes .../SerializationSpecTest/invalid_null.ser | Bin 0 -> 142 bytes .../SerializationSpecTest/invalid_valid.ser | Bin 0 -> 7040 bytes .../invalid_wrongType.ser | Bin 0 -> 218 bytes .../SerializationSpecTest/null.ser | Bin 0 -> 131 bytes .../SerializationSpecTest/null_invalid.ser | Bin 0 -> 7030 bytes .../SerializationSpecTest/null_null.ser | Bin 0 -> 132 bytes .../SerializationSpecTest/null_valid.ser | Bin 0 -> 7030 bytes .../SerializationSpecTest/null_wrongType.ser | Bin 0 -> 208 bytes .../SerializationSpecTest/valid.ser | Bin 0 -> 140 bytes .../SerializationSpecTest/valid_invalid.ser | Bin 0 -> 141 bytes .../SerializationSpecTest/valid_null.ser | Bin 0 -> 141 bytes .../SerializationSpecTest/valid_valid.ser | Bin 0 -> 7039 bytes .../SerializationSpecTest/valid_wrongType.ser | Bin 0 -> 217 bytes .../SerializationSpecTest/wrongType.ser | Bin 0 -> 207 bytes .../wrongType_invalid.ser | Bin 0 -> 7106 bytes .../SerializationSpecTest/wrongType_null.ser | Bin 0 -> 208 bytes .../SerializationSpecTest/wrongType_valid.ser | Bin 0 -> 208 bytes .../wrongType_wrongType.ser | Bin 0 -> 217 bytes .../StandardProfilesRoundTrip.java | 73 +++++++++++++ .../ValidateICCHeaderData.java | 10 +- 25 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/SerializationSpecTest.java create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/empty.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_invalid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_null.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_valid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_wrongType.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_invalid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_null.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_valid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_wrongType.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_invalid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_null.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_valid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_wrongType.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType_invalid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType_null.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType_valid.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType_wrongType.ser create mode 100644 test/jdk/java/awt/color/ICC_Profile/Serialization/StandardProfilesRoundTrip.java diff --git a/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java b/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java index 8bf09195627..0c2902ba74a 100644 --- a/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java +++ b/src/java.desktop/share/classes/java/awt/color/ICC_Profile.java @@ -41,6 +41,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; @@ -1549,33 +1550,19 @@ public sealed class ICC_Profile implements Serializable private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); - - String csName = (String) s.readObject(); - byte[] data = (byte[]) s.readObject(); - - int cspace = 0; // ColorSpace.CS_* constant if known - boolean isKnownPredefinedCS = false; - if (csName != null) { - isKnownPredefinedCS = true; - if (csName.equals("CS_sRGB")) { - cspace = ColorSpace.CS_sRGB; - } else if (csName.equals("CS_CIEXYZ")) { - cspace = ColorSpace.CS_CIEXYZ; - } else if (csName.equals("CS_PYCC")) { - cspace = ColorSpace.CS_PYCC; - } else if (csName.equals("CS_GRAY")) { - cspace = ColorSpace.CS_GRAY; - } else if (csName.equals("CS_LINEAR_RGB")) { - cspace = ColorSpace.CS_LINEAR_RGB; - } else { - isKnownPredefinedCS = false; - } - } - - if (isKnownPredefinedCS) { - resolvedDeserializedProfile = getInstance(cspace); - } else { - resolvedDeserializedProfile = getInstance(data); + try { + String csName = (String) s.readObject(); + byte[] data = (byte[]) s.readObject(); + resolvedDeserializedProfile = switch (csName) { + case "CS_sRGB" -> getInstance(ColorSpace.CS_sRGB); + case "CS_CIEXYZ" -> getInstance(ColorSpace.CS_CIEXYZ); + case "CS_PYCC" -> getInstance(ColorSpace.CS_PYCC); + case "CS_GRAY" -> getInstance(ColorSpace.CS_GRAY); + case "CS_LINEAR_RGB" -> getInstance(ColorSpace.CS_LINEAR_RGB); + case null, default -> getInstance(data); + }; + } catch (ClassCastException | IllegalArgumentException e) { + throw new InvalidObjectException("Invalid ICC Profile Data", e); } } diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/SerializationSpecTest.java b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/SerializationSpecTest.java new file mode 100644 index 00000000000..aa50d814264 --- /dev/null +++ b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/SerializationSpecTest.java @@ -0,0 +1,99 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.OptionalDataException; + +/** + * @test + * @bug 8367384 + * @summary Verify ICC_Profile serialization per spec, all name/data cases + */ +public final class SerializationSpecTest { + + public static void main(String[] args) throws Exception { + // Serialization form for ICC_Profile includes version, profile name, + // and profile data. If the name is invalid or does not match a standard + // profile, the data is used. An exception is thrown only if both the + // name and the data are invalid, or if one of them is missing or is of + // the wrong type. + + // Naming conventions used in test file names: + // null : null reference + // valid : valid standard profile name or valid profile data (byte[]) + // invalid : unrecognized name or data with incorrect ICC header + // wrongType: incorrect type, e.g., int[] instead of String or byte[] + + // No name or data + test("empty", OptionalDataException.class); + + // Cases where only the profile name is present (no profile data) + test("null", OptionalDataException.class); + test("valid", OptionalDataException.class); + test("invalid", OptionalDataException.class); + test("wrongType", InvalidObjectException.class); + + // The test files are named as _.ser + test("null_null", InvalidObjectException.class); + test("null_valid", null); // valid data is enough if name is null + test("null_invalid", InvalidObjectException.class); + test("null_wrongType", InvalidObjectException.class); + + test("invalid_null", InvalidObjectException.class); + test("invalid_valid", null); // valid data is enough if name is invalid + test("invalid_invalid", InvalidObjectException.class); + test("invalid_wrongType", InvalidObjectException.class); + + test("wrongType_null", InvalidObjectException.class); + test("wrongType_valid", InvalidObjectException.class); + test("wrongType_invalid", InvalidObjectException.class); + test("wrongType_wrongType", InvalidObjectException.class); + + test("valid_null", null); // the valid name is enough + test("valid_valid", null); // the valid name is enough + test("valid_invalid", null); // the valid name is enough + test("valid_wrongType", InvalidObjectException.class); + } + + private static void test(String test, Class expected) { + String fileName = test + ".ser"; + File file = new File(System.getProperty("test.src", "."), fileName); + Class actual = null; + try (var fis = new FileInputStream(file); + var ois = new ObjectInputStream(fis)) + { + ois.readObject(); + } catch (Exception e) { + actual = e.getClass(); + } + if (actual != expected) { + System.err.println("Test: " + test); + System.err.println("Expected: " + expected); + System.err.println("Actual: " + actual); + throw new RuntimeException("Test failed"); + } + } +} diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/empty.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/empty.ser new file mode 100644 index 0000000000000000000000000000000000000000..3fd70024d656af1d2f6370bdc63c5c863c32c8e6 GIT binary patch literal 130 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM GRsaCnJ}zni literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid.ser new file mode 100644 index 0000000000000000000000000000000000000000..dfe071e69dc6bfc8f50eea4dc91759c21baf0133 GIT binary patch literal 141 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM RmN0NQ2gip8`TMz7008G^F=zk) literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_invalid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_invalid.ser new file mode 100644 index 0000000000000000000000000000000000000000..60fb02f77836caff458594904ffba51da4445be9 GIT binary patch literal 142 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM SmN0NQ2gip8`TMySQ~&@OMKRa_ literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_null.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_null.ser new file mode 100644 index 0000000000000000000000000000000000000000..60fb02f77836caff458594904ffba51da4445be9 GIT binary patch literal 142 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM SmN0NQ2gip8`TMySQ~&@OMKRa_ literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_valid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/invalid_valid.ser new file mode 100644 index 0000000000000000000000000000000000000000..e01b5766f62f3b75de13f54d014c55168a7d2dd6 GIT binary patch literal 7040 zcmeHMXHXQ`8ok{;IS^)G$T@>V2?I#ZNpcj0VTLe-0fs1MWpM>p5D`g=AcBMiSC_bo ziYTIpf&vC~2U%3qRb0h{%A3JgwQpa&_iyX{dDB(h_w?znzVEx;x2jK_+x9y^XK;Z+ zA|sn&z{troV5TOgat%D3oTB`=sd4P&*Z?=jY@B#+)ld@#1|XLUWdD2YaA?`d8^f<0 z5de7rB{q}!&m4iVTs9+_ofpe;W@Iu#V!0XYR8DRh007DaIH$m<&;Z}HZdqIauXAjB zBlQ7;3*rfi@groX1!podB?C4BAcd335I-Xj z#0tj09`7arBw%jo|L5m_jsCYgA}fW%;x~)q^NFn34CYGX03@(dSOT8kBS^@}Ov?lS zcIIOVF26xO4in&*Z#Y?i20g2mmo{!6cN~oXl7*hmowaLjIkXf2k#sA$Z|2S?uIYeKzOsEW*$Azp5ixFW*{! zW3#djAX>$rTq^$?&|d<8l>Eu59|k~l7XV%Te{wO$0q8CPK>8swi<|u|A4)Kw0Re~t z2_OR$ff~>T`oIKO0Bc|mT!1I=1wkMjYyeD<08#)K|5FN6GTp({K01Ahq zp?HV`WkH*vT~HBJ3ROXMP&4!s)C*mJhM_U&E;IwpLw`V@U<4+?60icS2^+$6*a7x} zgW&aWJe&^a!TIn$xB{+++u$zv0(=dgfO+t9co6{z5uqU}h(1C`oDg3m0*OPo$X28f zDM#v&cBBs(LT({EK1ASwSfAH zCZpxidT1-O2RaNLkIq3Cpv%#X=uY${^aOei{SJe{&@fsU3yeD^9FvIIgxQNZjOoCf z$BbcSF^gC%RtBqwwZZyg8Q4s00k#s`hCPQJ$If9t;D|USoC(eq7lGs8cH+u$Ex3N% zIPM8<2``4%z+2&c@htpCd@;TO--o|}pTjQ^#0gpi8$u8vk+6eML1-sjB1{wB5Q#)p zq9xIv$R=(lRuE4TuMlU5A4wFFF3E`$Ny;MaBQ=rElkSqwuw}Wbcu|MycETYYKS_BZV=5AEf+m4dPDSu7+y?M%t?$PwpFZJ ztXFJGY*CylZY=IAo+7?iyhVIi{3!)X(WJOgSd@IqG0H{CEES=uQJttv>Mm*n^%8YX z0wbX%;USSIQ6%xB#HhrABt_CxGDI?0vRd-2B#(yBG-)2RWLh!p6m63BNlHP=K`K^i zk5s$VxYT=TIca<8Sm{FP4(SQ$Pcq6fE;30n`(?Ui9>}6(b!Gi!b7YUmUXpzwCn0Ao z$CN9SJ0*8d9+uaY50Kv^e@uQveo;YD!A&7ep;F6x;mvV(H6a)t7s@`8%Iin~gdN}bB6%95&bnxUGzHM=#RYDsH(Xys|OYE5gCwe7Uiwd=KS z>tJ-~I!QW*b#Cgyx@Niwy4AWj^k6-6J+|Iqy|Gp3RhFwbtLj%x>67#w^t1I_^=Axd z2Hplc4Z00p8mb#c816S5GW=>}W|VByV07P@V(e+W)40$04-;Jzrb)HQ?bYPfuB*4L z?pgiXRL?Zl^oZ#lGpd=7*>1A|voGcr=3Mi3^LYz(i)f1)iz&JU-H*PPe#H`P>1esl z@{Hw2D|4$%t5a65to5yvtXr&~uF+Z(yQX2ytc|J-!=}!LXRB-*ZF|&~XQyJvu&cLw zWUpq=vTwA1;-KS@=+Nfy($UC~>)7e|-pR^oi_>{$#M#Zc(0SB_;u7pq;pAYQg>2ttm zdacG<&f0EY;OpUg!1sZlmS4KxFa8*RKmTg~#{tFxn*)Xdsew^}ErIWX9D|C3?gwiJ zX9W+0h=xRlG>5zobq*~J<%JoBZ3(*;E*s7c?}@-hghVt(EUt4|cW~WYq*-J^C-f%WbG>Q>*IvO1v8r>58nc>T*XDr5e#MHzrFrAo(n9o`EtTNVotW9ib z?2|Z~xYD?%@iy@X;^!0W63P=^u$|ad?AM9ziM5IElYEn!l9rQ0lRHvyDbXpt9116i zGnlHFnwvV7rk7Tb#!I(KKbXG2_2M>WKpE>ZdNL*WKjx7v?X2&zX0mOwtFu4kgyfvg z70>15j%?K3xM$;&JlDMAn^2pWn+7(kY~HbXW{bm?x~1ZwE1f;ybD*nCxva44 z?ZK#n!{w&swTB3YvJUYoyeoPt)hkOXKUXDGO;kHpchtz&?5TNwn00vki2afFT7}x8 z+D}K>NAJ|R)pgZt)|Vf{9LqlTxFMur=s5j&bE9lyQR9~;P807$z=^?Ti{|DQ`Ih2V zxHYSFzAdut=8w)l_O$D_*LBc3icW%)Stp{{qf=;rl=_l)&=_73z}_jUea@JrJfl{3|6CC?W36Z-S}m(S&$TRfk3{>6p(3$p`J z1NSe6UA#T$KR9~H>(Z6Wu9q(kISiefr+9Z*cb?u&zWe%K_PwS1 z+o#dfg%89Yls}YzSpTceuN^#d-q{)FnUP02r{^qlF&*q+|JzsiJ@Vof$H7~Va{`|`R)yP8F!rW`_>*YW8zL9y;^w#w4z@p#c z!*?m~mfjbBkoj=pBmLvhr_fJNmU2F0KUaLw`O^2*>+AG#%JQ;M3kbD3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM zmN0NQ2gip8`TMy8-604zJ0~$OUC%SGBsD#?Na)dmCEpv{Eto(?vX&+0l%@jRAb?Qk bSDKrYTGX~?sx0@E2i+hi7I;o$x>NxG?Kn+0 literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null.ser new file mode 100644 index 0000000000000000000000000000000000000000..1fc2022f8683b572573642900988316676339a5c GIT binary patch literal 131 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM H7E}NL2_G)n literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_invalid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_invalid.ser new file mode 100644 index 0000000000000000000000000000000000000000..b2847de4f46feabb8c141b730916d33e04d3dac5 GIT binary patch literal 7030 zcmeHMXHXQ`8ok{;IS^)G$T@>V2?L1aEP`YZg<*y;gaL*qU_usGa1{{|0Yy+j!h)+y zTqB~0A_@u^&>dtEP*!mjQBim^`l@_YulC>8`}3x&y6>rTzxuxKcHgQ#b#C*Y0G-MO z3UQ1Kh5;ip&48Jdki<1`cXSN*;U-116QX=w9Wrp@T@`)x7#M&oE|C5E*#4l>Gk5x5 zJ0JjZ2TE)v^WQoAqPT2E0y{g3<-|y11V(XF*-4zNWB>q^oX!RCMu+CNQXeolf1aQO zpO+mM+zHIYRM-fBL{1u)--dEq z^6y*{{~OR>0e}>L=hTk^Ai4{H_KV-S$Ws7x6aygrl$p-W_?8bP7|?(KM1cg50g6Bk zXajv<0xWCWr-zfD1A~F4zeQKoKYghd>3W1}8xSXaQ$I2RIL| zfa~BExC=(WW55Gb;3b#`@4*rTL0E_kQ6U*f3DSV{AQOlVt%aPSb&xL<0!2VE5C=+! za-m&N5mW+IKs8VU^b6DlU4ihjH z0%ybd@IJU4u7#W7cK8Z>3m$=a@Jo0B0SFPHAu5PILPs1CZzL3nM!3i}q!1}XYLOPC z8|g#tAv|Ob`G~@xs3>KWKFS*9h6+G2Q7NcxsJ*BP)M->Zsuy(+HG!H(Eu+b3d9)td z3hj;#M#rEt(FN!-bRD`4eGNT=obO--6$dKZ)Kxwutc_tREo5V42!%H#fxf)+KX-$%@!>aJtulcbWRK}rYYtq#t_>kRw>pcHYT

        (VCi1a5JWf^Cgc$ot-9Wqa3QL?(S8)P$OkI7z>os*N0vzBAZ70R8J zdn6Cb>&pAe=g6OsACO;AP*iYLNLDzaa8+SWk)~*;7^hgOcu{dyiK?_#iLF$sbV=!j zvZS)Ta)NTXaJv4B8eJ_~tyHZ?ZC+hT-Ag@J zy-EF%23~`%5vy@nqfcW|Q%f^QbGK%P=8TrKmb+HAR+HAaHd)(NJ4L%z`@RlFhprQ^ zb5!T9F05;&8>?HXdq)q}GuLD59n~9Jg-(EjMze#_>fM&4HAkUz~ z;FY1eVW{B&!#=}hBQv7}qmxFDjVZ<+#(Bow#($aUnlMc&P42HIuXb6zeRb#RH>P@~ zQKrXCADB_iyv%l+^_YDzw=n0Lx0ugbs9Qu>R9TGCCFnl%z4RNFXiEpn?Uv^)KU$ev zrCFV|dTp(59dF%eJ+nq@P1Kr`YbI?}Z5TE+HoUdUYa`YkU(2&qv1QoS+CH;Wvt!xS z*-hK)*vHv7+rM%!a^O0&IV?I_Ic{~l?1VVEIu$w%I#ZkjoGYB4x~RIuxHP%UyPCP? zxL$TcyLq}Da2s=1aA&$VxX*i7cx?6P^(1-vc~*EnTc^8@yRO>{_VVyL=rz7xV?Aem zhd1zc_de+T#7D~~#pkyTm<>J~DmOg$HTKQ*?enAhh5I%7z4v$U-|zo8Ksz8kpeIl? zFf6bka52a!s3eFNY#6*X_*RH)2s@-R6dM{CS{J&o(Rt&cjZcbuM)`&14phndrptCG7jcVesk)|ze5w#aQg+ts$`Z=c%Xy5sat!p``exATnh zO7hf~zXCHr1 z<66^Rt65ui0&^nc#PgGZC;Lv(Pc_uZ))m!#spr)5PWztjZLnx)Xq0c<-vl?MH_bMO zHQ)W&>F3TC{g#?mT5Hi6a3=lCi(evs89nQHw&xuET+6Q-zgD$T+X~yE_ATx69kCs} z&XCTbE|0FBZtL#0-wb}MKd*AW@`B`r{TB%r^DnMk%D%L4Ir;M3m6$7&J>fl%uLfVe z-@BoA@S5kf8`oW~U+uH+yL7|m#`%7${;r$!n;o~zZ?zAY4Yb`hyWKWuKG=T8;!fvX z%e&n}Ylbck+YVp3=XCG-efRsfM%IrEjRuT97z-QYJzzbUd6@9<&7+J*OOJPqqsI%M zh&?HLD*v?hcb(r`dFH$e6HXHY&-|V}o@7qG{3G>`&r|u&$3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM I78Fzf061YUN&o-= literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_valid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/null_valid.ser new file mode 100644 index 0000000000000000000000000000000000000000..b32c838c80ad94b311dcd5af73d74c1ad0ec9dd6 GIT binary patch literal 7030 zcmeHMXHXQ`8ok{;IS^)G2$FLU1SAX~IVZ_cRE8PC5C$BgU_=&IUxKzEQuMOnpFL`CJz=&RbduipE&_5Qr+s_uLG^jF{a-R@h}r_OEt9iUUV zKt7I<&d_IMr0O%15|X(3ZVnEceYi={?1U&k7yERacu(bEBL)T_lM7`3d+bnf>8YDT zuj~;3xdBBslljja{!v^uBY~Y2#d2h%GJ>MGDeNRpW-1Yfc!PcG8>#mgTmVl{ zg3rrN3C;v&VhU^kKq4oV%Wp#|Bs5G3I{`>Q4p0CV&=|~=WG{c`0Dc}Ih!u=~J>E?K zNWk3I`_Iq+8vSo~L{=h)#cvVE=i^vWDNH_2<>OdZB1^#Ydjzo=smXi}cI0CTF26xO z4iVtUZ#Y4KW4_^CUtM*xUv z2_~V$=A=e(IgAADujJo(`IlNEDS{UsnZ`~?)njx1&LaF=|EoHJ_42LtH&$QQ0YoeR zlS|@%1Nut;`$D02G5mpc2%8de8*gz-iD0&Vq~J z3b+n#fpPEv@W3p10T#eJuna*E79vAbNCr}b)FEBS2%9P$DlLNdFV291G)o!89k1kMZd#fFf@!N#th?%3B|-=vN462Bbau~1SgMz1V%zr!c8Jh zqCnzDi4lnfNs6SgWRPU0WR>JONgfTMY0%tg3A7^GY1#zsqm;aqom7<6KB+dTF{vf# zRnoT7QPTO+?b74YA7zweoMhr<4#;%LJd{Pr>d5-aX2>3uy(~MwN@A7eD(0&ERi{_o zmxJYWlAl+gDcCB+DU>SoDaC@m<aul@=#H*NuQp%JSzWh!Qjer(ryfl^BqT=C{BS+l}?YGRh(m-TbvhMOkA>E zF1VsyJzNjCPP)mvG2NQn7TnF;x4RE`kUac7Dm@;1>UeTJd%a*Ucdvt9Q|r~&bJllx z18+C)gWeB)G<~-C{Nju8_3^Fped1^6x6N@9ZDPw}=h+VIO7`nG*SMOvrFif7#`u+l;Dq)>Tw+9G4~N2u=L{q%BxNRz zChI2WCG)meY&o=Lf$PC-NP$u|rgW!D@PEw1Xr&p!F&j`vmlPR9b$sFFQ zvvuFrr&-QfC$dr5%y>^ixduseSDjU0oV zlAO1Dg7@_1%IEINo%zn?yOunWJZ|3PUdz2T-=n@~e?PKs?Y@eAU-DV`*9r^@$_hRe zG7GQ$VE9A%4=ekl_m32r79B0d6myCv4%i$xSt4GNRWftX>tJW8QfYqa+e4cV4V4*} z)f^@qPCLvi_bl(OP^&1e_*5BNIbLO7)m|-Ey{~%d2~7O*t8J&X7n}m8(oQ}9DdMMz(;lb$&(P1b{jC0TbqBR0zZ2@*+PTmb+r{e+ z?H=uM@9FQg?CtnP|Ch$I%4e(2NuDd}BlP9=t(?y~zjz_}!u-XUi!=S3`yX5ixpZg1 zcVOhQ$K|V6oUdFOv>QBs)#~cmA&a4&YxHYf*G;c?4x0>j+%UP(F=9H>dDHA>_bu~V zy`$?!`^Id>F5Y&$edUhZo$KT4$44guC+<#$Px9`v?moMhaPRf~^!v*Xc1@wD@*j#l zEPEvPsP0$oU)y=6ymQlz)5DMbA3vC3&b;_7<+o3>xlhPX%AP7dZF*+(?ChNL+^y#u zp3lBWez82C_q+J-)i1SP{`|`J)$l^d!t87A>yslKG5F)t<&00*PvxJrKlgs|_%gMUxUwSD0zxey)B-{+Ak+dvEg;kaLM3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM z765G*1e=(Xn3t~SnOBmUo?0aIXu*>2jqMgpAcI)T5_3vZfi?>u)cKX>CZ!g&t(hvz RJ>@|+$YBMZ)0i$*002rXN~r(< literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid.ser new file mode 100644 index 0000000000000000000000000000000000000000..a6512d244cf246a938ee37fe376ee7c92bd57160 GIT binary patch literal 140 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM PmN2k82ger!ZK(hNwt_Jz literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_invalid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_invalid.ser new file mode 100644 index 0000000000000000000000000000000000000000..dbe11f2c6d651974009b5c4d96db2f45923a47dc GIT binary patch literal 141 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM QmN2k82ger!Z7HY#0O%4im;e9( literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_null.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_null.ser new file mode 100644 index 0000000000000000000000000000000000000000..dbe11f2c6d651974009b5c4d96db2f45923a47dc GIT binary patch literal 141 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM QmN2k82ger!Z7HY#0O%4im;e9( literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_valid.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_valid.ser new file mode 100644 index 0000000000000000000000000000000000000000..fbe90d0eb6ea8059d4cde7ff1b8e31d7531919b7 GIT binary patch literal 7039 zcmeHMXHXQ`8ok{;IS^)G$T@>V2?I#ZNpcj0VTLe-0fs1;WpM>p5D`g=AcBMiSC_bo ziUBd8pnw70K^75p6<0B#@@DW=?b}!H{o8tf-gH&>J^l5m@B41uTh*t|ZT}shGq^w@ zk&(?XVB};PFjJFLxdt9iPEmf`)Hrr>Y=E0%Hcq^+dZ-x#1CYxFvj3es99n+j=I|>= z1VA1@iOppGGe=-7m(56K=f$#|8JUcbSZ)S8m6MwW0Dv+9)+sP5gI^_!3*dE*?Qf($ zU~oY^K`lN(c1mz3GgC5PBLGr3nOr_oIV?Ov89M<;Kps#47SI^Xj5MD>*C2jEAm9qd zzh3Vq03?Xq*8k7n|H}TiHzF&A!{YafY*0sC#VlP4-G@3&^>4xnuq>?KEVh~f+b)DSQ9pc>97Or1qZ?F z;dnS5&Vvi!y>KPm0Jp>4@Ok(eJPz~V=kOu|5F$cDR1kfHjyNH{NCXmxaFH!Y5mJFP zARS0QGKAblc*qOnBMO6}qLfkkC~K5EDj3B?rK7f>_Moa!$5Gv=i>TYEY19JhE1Ha! zN9&=j&>rY8bUZo-U5KtgH=(=Gm(b(rS@b&$21CPWVJtB2m~c!YCLgm0a|qLkIfog= z%wQI=SgZ_I4{L+<#WJv&*g|X-wjFyGJBFRbe!vlNN;ngoD=q@Z!R^3R;97A5xG~%u zZV4}j*T7rheeo>(Mtljr5#Nu$iJ!$U5yS~v1RFvSA(611P)X<@Tp~;n-VljIRiY)) zpU5U|BUTbm5U&uYi62Q6k}k=K6iLb=?Iksn&XMks-jK;;O|m^Xf}Ba-OKu@wAU`61 z5TS|~hA+|-VMyyY4 zQfyJ2DsC+9E1n|0N4!;hSo|pkOVOmbP*{`#%2CP%$_y2us!^S&OzKW*BlQw>RstiT zCE+2FC{Zl&qr`~Bf+R)KR5C;|SF%R(j3keS&@^ctv}9Td?Idl2_DM=X%0VhtYPVE} z)R@$JX*p?o=~(F^=}zf!=}$7sGA=SnGW%qDWFE?*WOZfzWpiW?%U+UwAtxbcEyt8A zk~=APUmlj%l@E~5mp>|hU4BtPQNc|iO`%HRg2D?$nxeg8qGGw?fa1IoRmoO~tyHdb zR_U3tq_Tr@vT~*JMdbw*c@=k+ER}kd5tSuXE!AMv9jcwG57h{2bhS9Oa}tz6Lf2IZ|cE%=6Y{E>K4%!wHA|f3A!JB5B-WI+S1W-tL16S zk5=YZnN}yQURmo~Ct0^zKV74>CU#BZni(5a8-`824bN8DHrn=xEzeHHj$zke_t;*| zo@L);Kj)z1km%6v@Y2!9k?Yvy_}-OGKS`=p102h*d)W5LtHbF=3~FOpZFSGCt;Z(VP$cfSwpf*gZNf*u5G2WJHj zhKPnlhO~sd4|NVL3+06whHVbJ7A_mk4)2Y?MubE(MJ%p!S$AOFY@}IaVdTVmt@XL< zhd0P>NZxQJN;HZQbt)Pi9U9#l{h8s*XkaYHc*NAkEHIsz2bs@V_N;Q&e5_4uS?pY# zO;FW64(YWC|y_r$uy_es7<%}LA2p~;;oxRmIWJ`RPG#JQNN zn3|h9nx>aln8r)DNX4hnY$O*|gl`Edh z$-TZ&cjNAjb9t_L$MRA6%>2PkDx0=%n%?ZNxqb_@C1%UuR<*4KTW7bqZ9Bf5usvz} zjU7fi%67cn8M?E-K%rn;!OVAV-?bHr6mknEcUkYM`yTZ@`}>jIt9MuJ{#wK;x>{^h zTv7aa4|C7eAB=yf{9$=-+}@EA^OD1*m{LyZ#6G)y$IHaa^2%oR`|R&7S1vCqe|sS6 zz;K0WMcqNd!K{P4O7F_vD)p+;s?XI4)#EjeHJ!EcwYzKIA7ULEJ8XZrqfVi&xbD*t z_K~~wZuQ*_nhh04F-NnHK4}bT96Cln*3u-~RNVBXnbXWW9&r3(i$zOIt9)xo8{C%F zHs2oEe(Oi)AA39WJL)@Woy8}>iL4XPev1BS;-uHf!Bg~89Y1UQT-!zMD(Z&1H+C=d zB=qol!+S^jJo^Uwt^2!vG5DqVw94t4Gm>XY1_%QM1IuUg&MuxyJNM#z{P~%|sKEyp z!Y0ztkzN_@BJ=e^ybze8T-gU$5M%Re>NcT;P zo4vOzZ}pF^866n28#{m7`S#^I9(S&duN@zq2%fk*89B+j%ewpYUh=)y_p|RWJ=ivd zo+^4M_ORlS{G*0nb$;#Sne)y}J5OJK9QgRb40GoBZyCRRo-KGneo`@~G}rRfeW_61hXZun{d5h#MAlK=$e1K@oFfU0T$G~@W^ I3z51108jusz5oCK literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_wrongType.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/valid_wrongType.ser new file mode 100644 index 0000000000000000000000000000000000000000..3538676276fd1b9cb42cfdd37378f05967885d59 GIT binary patch literal 217 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM zmN2k82ger!Z2>w%5Nvc#VqUtQXI@EadTNo-qXkR8H?~_aflOpAOUx-v1v)_hq0X-~ ZHz~EKZOv3!?kNwtK`t!toW^vi0syHLO!oi) literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType.ser new file mode 100644 index 0000000000000000000000000000000000000000..3469f871ef6042f9d8d244c3ec9388cc3063423a GIT binary patch literal 207 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM z0xcH=8<>-rm#*iTSCX2ZS|s#n!IJNd?G{WRb6Cp~b4pWz77HNM`IY7-r53fVnJUXY QM2_;L?iH?cY!43iv&;V3`1#}iWGb1S6H$u`Akj6^K z@7I?v0K6Lnkd(P=@!#{`BmV{wIcWlpWVtxWd>3w@0`J20OU6T(0cwmmv9(>w(S6@JYkE3+28V^qyridfILtH zsz3|q0~25YY=I+i0q(#D1b|==0b;;Pzy`@64G2LF$Oi?W7?gtTU>B$X`$0Wu0L|be zXai@#C2$qo0C&Iucm%{?47>o-;2oHQAP5VQAsVC#X+wsPIb;hlpv906v;+!+VxV{^ z2@*geC?DDcl|toE4RjD{fPR6_K$oB%s1JGojX;yoU(iPwfl06;tO*;zRxkthfCJ$O zcmH zB6UbJ(t&g%cM&l%g?vC^P&AYd$^zwr@<%O0u~Dm0>rrK>8q_gVE2;~17d3*KMtwn( z(HdxTv@@ECjz%Y;bI`@;O7s!*Y4jEJ0D27l4uiqaF~%52j6Ws@lY&``DZ}i=v|uh` z`Y@xI87vm7iZ#c&VuP_PY!5NM5L{xqoj+Z2c$P-GTDgiPL3sKk++f?$d}1a z$nWK7a+Y#TIgZ>0xmvkaxqi8q@_2bec@O!O@_F)=@~7l)%TFob6^s1gE zC=4mgP-qkzN-!mjQbsvW>7h(eu~Z|f50yhLq8_4NrjF7Onm)~o#-?qe)zhxf#uPD% z#)?eE6va}-pA~x*r-xk|N4=aj^BglTw#(RkQ|Y~p6J+N93po+-wZ zVVY{X*Yu7VY-VqkY*uS_+Z;A`Fz1=?HSb%1Uf{Gqu%K?ikOj%Y!y?sLYoeoziiEH*|xQ|_ZE^D`YznCuzlfc zJ99g(-9Eef_B8t-`_1;9_MaUb9fS_e4wH@sj`5Cr9ETW+j1Wc{hU%k`kEc(Kmn_{9eni`{hHSZ;N0Pu=z1 zIqpZ?$309vQaqYGUV2)43O!GI&U!g}t@FC*jd=Tcmw5O3P<@v9)c8E{)$>jAZS(*ceF>jJt0NrB;kHGxl;m@N@5=?H>@0)lo14KFoZ zDp=YU41$@#JA)sG7>BG5`7IO^8WLI?`Yg;QEI+I}oEE++{CN1g2+xRZ5s#LcEE6s3 zjFgXzi)@ITjq;8vj}k{)MX!s#5u+Bvi)oL=#zw{-iJe*QvwYX`u{is<;<&*T#w&7H z^sH1{$zOSHmHaB!s#Edk_^9~f@t;`1tUA_A0yAMx!Zh29y_@}lzT4exb|f6D%p+0@|Fqp4r{QT&!PTv~kE839$0D(Fhr zO3zL2%P`L<&JeG5UcGDev@lS3BooS9k=dT5DETqp6q$&A5RGIn&aTaVpA(sLDwmQg z$i2D7Y|WN6<9WV$hu5Okve$Oz>*g2akF4`pcW^zlK4E?52K^018^$*JZ9G;$C`c{1 zRcKvUUifxX)TWLi&7zG(qd)ll&{!;2EG!<{?6P_PkEkDcKlW}}xTSi_ml96N^-}B7 z%F<6|?6T`W+5A-X)7PzuTYI-TY}>aTvt6)#aEIHDW95|cyz~ zfOp{jLBE5obw+iShcJh-4?U}otnWU|INWeV?MUg7&qoDE#mB;qbu~CPG#uABzO4~% z6g5sZ#Wmge+56}AW{c*7E%cVs6X1mC#PeU`e;GU(c(U^p<5csnhQIDPO*>uE3bn3j zoo-8R6Sv2-_niqi)7jzDar!sQ-;SQuJzIND>D;#Sg!4t`zh20@Fmo~E;?$+2OQW5u zIv-t*zI?AMw5#_@;FW7veXm~b_UOKF&Gp*Z9_OAj*BRH_ZaCa%y=i~*^ey{ar+XcG zTW>qwZolJnr=xFC-}!#G{!4eg?_Ryfymw<@=|JD$vcda9aYN$!ocj|G_zzw`%zilc zXyY(?xa6_I?F)c*BJ4 z#Mw#T$ve-NKOcLM@nUYO_)p58dtREn{PmUltDDo&(_^oNufP6P_D1#1(YJPQJ7+>> zp1ezYH#b}IUiJO44~!4pAEQ2w&*gl=eyaLx`nlsv;FsaAXd;ys+AoB%ezJSaZkof{KU*P}P7nnc0k*EPgst67Z0T8hkfF-v8sICFP LC{c305SRNe4T)CE literal 0 HcmV?d00001 diff --git a/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType_null.ser b/test/jdk/java/awt/color/ICC_Profile/Serialization/SerializationSpecTest/wrongType_null.ser new file mode 100644 index 0000000000000000000000000000000000000000..2c1536cc09423dfa9117647e3d5206ba5763c15b GIT binary patch literal 208 zcmZ4UmVvdnh(R_hu`E$9vAjetIX@@ANYB&RIX<8$KP@vSHOSqmj6-netmDhsm>3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM z0xcH=8<>-rm#*iTSCX2ZS|s#n!IJNd?G{WRb6Cp~b4pWz77HNM`IY7-r53fVnJUXY R3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM z0xcH=8<>-rm#*iTSCX2ZS|s#n!IJNd?G{WRb6Cp~b4pWz77HNM`IY7-r53fVnJUXY R3u; ziWsDDt34S$WyATC@12+#7(E%}Gn12{W(21eWhUliR;8x6B$gzGr4|)u=I2!uFfcGM z0xcH=8<>-rm#*iTSCX2ZS|s#n!IJNd?G{WRb6Cp~b4pWz77HNM`IY7-r53fVnJUXY X Date: Fri, 3 Oct 2025 18:45:34 +0000 Subject: [PATCH 350/556] 8356202: Cleanup Source code in String Implementation Classes Reviewed-by: jpai, rgiulietti, liach --- .../share/classes/java/lang/StringLatin1.java | 124 +++++--------- .../share/classes/java/lang/StringUTF16.java | 161 +++++++++--------- 2 files changed, 127 insertions(+), 158 deletions(-) diff --git a/src/java.base/share/classes/java/lang/StringLatin1.java b/src/java.base/share/classes/java/lang/StringLatin1.java index da3acbe5f0a..61c62d049bc 100644 --- a/src/java.base/share/classes/java/lang/StringLatin1.java +++ b/src/java.base/share/classes/java/lang/StringLatin1.java @@ -41,61 +41,49 @@ import static java.lang.String.checkIndex; import static java.lang.String.checkOffset; final class StringLatin1 { - public static char charAt(byte[] value, int index) { + static char charAt(byte[] value, int index) { checkIndex(index, value.length); return (char)(value[index] & 0xff); } - public static boolean canEncode(char cp) { + static boolean canEncode(char cp) { return cp <= 0xff; } - public static boolean canEncode(int cp) { + static boolean canEncode(int cp) { return cp >=0 && cp <= 0xff; } - public static byte coderFromChar(char cp) { + static byte coderFromChar(char cp) { return (byte)((0xff - cp) >>> (Integer.SIZE - 1)); } - public static int length(byte[] value) { + static int length(byte[] value) { return value.length; } - public static int codePointAt(byte[] value, int index, int end) { - return value[index] & 0xff; - } - - public static int codePointBefore(byte[] value, int index) { - return value[index - 1] & 0xff; - } - - public static int codePointCount(byte[] value, int beginIndex, int endIndex) { - return endIndex - beginIndex; - } - - public static char[] toChars(byte[] value) { + static char[] toChars(byte[] value) { char[] dst = new char[value.length]; inflate(value, 0, dst, 0, value.length); return dst; } - public static byte[] inflate(byte[] value, int off, int len) { + static byte[] inflate(byte[] value, int off, int len) { byte[] ret = StringUTF16.newBytesFor(len); inflate(value, off, ret, 0, len); return ret; } - public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { + static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { inflate(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } - public static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { + static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); } @IntrinsicCandidate - public static boolean equals(byte[] value, byte[] other) { + static boolean equals(byte[] value, byte[] other) { if (value.length == other.length) { for (int i = 0; i < value.length; i++) { if (value[i] != other[i]) { @@ -108,20 +96,20 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int compareTo(byte[] value, byte[] other) { + static int compareTo(byte[] value, byte[] other) { int len1 = value.length; int len2 = other.length; return compareTo(value, other, len1, len2); } - public static int compareTo(byte[] value, byte[] other, int len1, int len2) { + static int compareTo(byte[] value, byte[] other, int len1, int len2) { int lim = Math.min(len1, len2); int k = ArraysSupport.mismatch(value, other, lim); return (k < 0) ? len1 - len2 : getChar(value, k) - getChar(other, k); } @IntrinsicCandidate - public static int compareToUTF16(byte[] value, byte[] other) { + static int compareToUTF16(byte[] value, byte[] other) { int len1 = length(value); int len2 = StringUTF16.length(other); return compareToUTF16Values(value, other, len1, len2); @@ -130,7 +118,7 @@ final class StringLatin1 { /* * Checks the boundary and then compares the byte arrays. */ - public static int compareToUTF16(byte[] value, byte[] other, int len1, int len2) { + static int compareToUTF16(byte[] value, byte[] other, int len1, int len2) { checkOffset(len1, length(value)); checkOffset(len2, StringUTF16.length(other)); @@ -149,7 +137,7 @@ final class StringLatin1 { return len1 - len2; } - public static int compareToCI(byte[] value, byte[] other) { + static int compareToCI(byte[] value, byte[] other) { int len1 = value.length; int len2 = other.length; int lim = Math.min(len1, len2); @@ -169,7 +157,7 @@ final class StringLatin1 { return len1 - len2; } - public static int compareToCI_UTF16(byte[] value, byte[] other) { + static int compareToCI_UTF16(byte[] value, byte[] other) { int len1 = length(value); int len2 = StringUTF16.length(other); int lim = Math.min(len1, len2); @@ -191,12 +179,12 @@ final class StringLatin1 { return len1 - len2; } - public static int hashCode(byte[] value) { + static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUnsigned(value, 0, value.length, 0); } // Caller must ensure that from- and toIndex are within bounds - public static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { + static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { if (!canEncode(ch)) { return -1; } @@ -215,7 +203,7 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int indexOf(byte[] value, byte[] str) { + static int indexOf(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -226,7 +214,7 @@ final class StringLatin1 { } @IntrinsicCandidate - public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { + static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { byte first = str[0]; int max = (valueCount - strCount); for (int i = fromIndex; i <= max; i++) { @@ -248,8 +236,8 @@ final class StringLatin1 { return -1; } - public static int lastIndexOf(byte[] src, int srcCount, - byte[] tgt, int tgtCount, int fromIndex) { + static int lastIndexOf(byte[] src, int srcCount, + byte[] tgt, int tgtCount, int fromIndex) { int min = tgtCount - 1; int i = min + fromIndex; int strLastIndex = tgtCount - 1; @@ -276,7 +264,7 @@ final class StringLatin1 { } } - public static int lastIndexOf(final byte[] value, int ch, int fromIndex) { + static int lastIndexOf(final byte[] value, int ch, int fromIndex) { if (!canEncode(ch)) { return -1; } @@ -289,7 +277,7 @@ final class StringLatin1 { return -1; } - public static String replace(byte[] value, char oldChar, char newChar) { + static String replace(byte[] value, char oldChar, char newChar) { if (canEncode(oldChar)) { int len = value.length; int i = -1; @@ -326,8 +314,8 @@ final class StringLatin1 { return null; // for string to return this; } - public static String replace(byte[] value, int valLen, byte[] targ, - int targLen, byte[] repl, int replLen) + static String replace(byte[] value, int valLen, byte[] targ, + int targLen, byte[] repl, int replLen) { assert targLen > 0; int i, j, p = 0; @@ -377,8 +365,8 @@ final class StringLatin1 { } // case insensitive - public static boolean regionMatchesCI(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI(byte[] value, int toffset, + byte[] other, int ooffset, int len) { int last = toffset + len; while (toffset < last) { byte b1 = value[toffset++]; @@ -391,8 +379,8 @@ final class StringLatin1 { return true; } - public static boolean regionMatchesCI_UTF16(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI_UTF16(byte[] value, int toffset, + byte[] other, int ooffset, int len) { int last = toffset + len; while (toffset < last) { char c1 = (char)(value[toffset++] & 0xff); @@ -413,7 +401,7 @@ final class StringLatin1 { return true; } - public static String toLowerCase(String str, byte[] value, Locale locale) { + static String toLowerCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -480,7 +468,7 @@ final class StringLatin1 { return StringUTF16.newString(result, 0, resultOffset); } - public static String toUpperCase(String str, byte[] value, Locale locale) { + static String toUpperCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -560,7 +548,7 @@ final class StringLatin1 { return StringUTF16.newString(result, 0, resultOffset); } - public static String trim(byte[] value) { + static String trim(byte[] value) { int len = value.length; int st = 0; while ((st < len) && ((value[st] & 0xff) <= ' ')) { @@ -573,7 +561,7 @@ final class StringLatin1 { newString(value, st, len - st) : null; } - public static int indexOfNonWhitespace(byte[] value) { + static int indexOfNonWhitespace(byte[] value) { int length = value.length; int left = 0; while (left < length) { @@ -586,9 +574,8 @@ final class StringLatin1 { return left; } - public static int lastIndexOfNonWhitespace(byte[] value) { - int length = value.length; - int right = length; + static int lastIndexOfNonWhitespace(byte[] value) { + int right = value.length; while (0 < right) { char ch = getChar(value, right - 1); if (ch != ' ' && ch != '\t' && !CharacterDataLatin1.instance.isWhitespace(ch)) { @@ -599,7 +586,7 @@ final class StringLatin1 { return right; } - public static String strip(byte[] value) { + static String strip(byte[] value) { int left = indexOfNonWhitespace(value); if (left == value.length) { return ""; @@ -609,12 +596,12 @@ final class StringLatin1 { return ifChanged ? newString(value, left, right - left) : null; } - public static String stripLeading(byte[] value) { + static String stripLeading(byte[] value) { int left = indexOfNonWhitespace(value); return (left != 0) ? newString(value, left, value.length - left) : null; } - public static String stripTrailing(byte[] value) { + static String stripTrailing(byte[] value) { int right = lastIndexOfNonWhitespace(value); return (right != value.length) ? newString(value, 0, right) : null; } @@ -713,14 +700,14 @@ final class StringLatin1 { return StreamSupport.stream(LinesSpliterator.spliterator(value), false); } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { value[i] = (byte)c1; value[i + 1] = (byte)c2; value[i + 2] = (byte)c3; value[i + 3] = (byte)c4; } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { value[i] = (byte)c1; value[i + 1] = (byte)c2; value[i + 2] = (byte)c3; @@ -728,32 +715,15 @@ final class StringLatin1 { value[i + 4] = (byte)c5; } - public static void putChar(byte[] val, int index, int c) { - //assert (canEncode(c)); - val[index] = (byte)(c); - } - - public static char getChar(byte[] val, int index) { + static char getChar(byte[] val, int index) { return (char)(val[index] & 0xff); } - public static byte[] toBytes(int[] val, int off, int len) { - byte[] ret = new byte[len]; - for (int i = 0; i < len; i++) { - int cp = val[off++]; - if (!canEncode(cp)) { - return null; - } - ret[i] = (byte)cp; - } - return ret; - } - - public static byte[] toBytes(char c) { + static byte[] toBytes(char c) { return new byte[] { (byte)c }; } - public static String newString(byte[] val, int index, int len) { + static String newString(byte[] val, int index, int len) { if (len == 0) { return ""; } @@ -763,7 +733,7 @@ final class StringLatin1 { // inflatedCopy byte[] -> char[] @IntrinsicCandidate - public static void inflate(byte[] src, int srcOff, char[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, char[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { dst[dstOff++] = (char)(src[srcOff++] & 0xff); } @@ -771,7 +741,7 @@ final class StringLatin1 { // inflatedCopy byte[] -> byte[] @IntrinsicCandidate - public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { StringUTF16.inflate(src, srcOff, dst, dstOff, len); } @@ -824,7 +794,7 @@ final class StringLatin1 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { diff --git a/src/java.base/share/classes/java/lang/StringUTF16.java b/src/java.base/share/classes/java/lang/StringUTF16.java index 08b4072fc35..4e31c9728e9 100644 --- a/src/java.base/share/classes/java/lang/StringUTF16.java +++ b/src/java.base/share/classes/java/lang/StringUTF16.java @@ -54,13 +54,13 @@ final class StringUTF16 { // Return a new byte array for a UTF16-coded string for len chars // Throw an exception if out of range - public static byte[] newBytesFor(int len) { + static byte[] newBytesFor(int len) { return new byte[newBytesLength(len)]; } // Check the size of a UTF16-coded string // Throw an exception if out of range - public static int newBytesLength(int len) { + static int newBytesLength(int len) { if (len < 0) { throw new NegativeArraySizeException(); } @@ -89,7 +89,7 @@ final class StringUTF16 { ((val[index] & 0xff) << LO_BYTE_SHIFT)); } - public static int length(byte[] value) { + static int length(byte[] value) { return value.length >> 1; } @@ -111,7 +111,7 @@ final class StringUTF16 { return c1; } - public static int codePointAt(byte[] value, int index, int end) { + static int codePointAt(byte[] value, int index, int end) { return codePointAt(value, index, end, false /* unchecked */); } @@ -134,7 +134,7 @@ final class StringUTF16 { return c2; } - public static int codePointBefore(byte[] value, int index) { + static int codePointBefore(byte[] value, int index) { return codePointBefore(value, index, false /* unchecked */); } @@ -155,11 +155,11 @@ final class StringUTF16 { return count; } - public static int codePointCount(byte[] value, int beginIndex, int endIndex) { + static int codePointCount(byte[] value, int beginIndex, int endIndex) { return codePointCount(value, beginIndex, endIndex, false /* unchecked */); } - public static char[] toChars(byte[] value) { + static char[] toChars(byte[] value) { char[] dst = new char[value.length >> 1]; getChars(value, 0, dst.length, dst, 0); return dst; @@ -173,7 +173,7 @@ final class StringUTF16 { * @param len a length */ @IntrinsicCandidate - public static byte[] toBytes(char[] value, int off, int len) { + static byte[] toBytes(char[] value, int off, int len) { byte[] val = newBytesFor(len); for (int i = 0; i < len; i++) { putChar(val, i, value[off]); @@ -218,7 +218,7 @@ final class StringUTF16 { * @param count count of chars to be compressed, {@code count} > 0 */ @ForceInline - public static byte[] compress(final char[] val, final int off, final int count) { + static byte[] compress(final char[] val, final int off, final int count) { byte[] latin1 = new byte[count]; int ndx = compress(val, off, latin1, 0, count); if (ndx != count) { @@ -245,7 +245,7 @@ final class StringUTF16 { * @param off starting offset * @param count count of chars to be compressed, {@code count} > 0 */ - public static byte[] compress(final byte[] val, final int off, final int count) { + static byte[] compress(final byte[] val, final int off, final int count) { byte[] latin1 = new byte[count]; int ndx = compress(val, off, latin1, 0, count); if (ndx != count) {// Switch to UTF16 @@ -279,7 +279,7 @@ final class StringUTF16 { * @param off starting offset * @param count length of code points to be compressed, length > 0 */ - public static byte[] compress(final int[] val, int off, final int count) { + static byte[] compress(final int[] val, int off, final int count) { // Optimistically copy all latin1 code points to the destination byte[] latin1 = new byte[count]; final int end = off + count; @@ -389,7 +389,7 @@ final class StringUTF16 { // compressedCopy char[] -> byte[] @IntrinsicCandidate - public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { + static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { char c = src[srcOff]; if (c > 0xff) { @@ -404,7 +404,7 @@ final class StringUTF16 { // compressedCopy byte[] -> byte[] @IntrinsicCandidate - public static int compress(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static int compress(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { // We need a range check here because 'getChar' has no checks checkBoundsOffCount(srcOff, len, src); for (int i = 0; i < len; i++) { @@ -420,7 +420,7 @@ final class StringUTF16 { } // Create the UTF16 buffer for !COMPACT_STRINGS - public static byte[] toBytes(int[] val, int index, int len) { + static byte[] toBytes(int[] val, int index, int len) { final int end = index + len; int n = computeCodePointSize(val, index, end); @@ -428,7 +428,7 @@ final class StringUTF16 { return extractCodepoints(val, index, end, buf, 0); } - public static byte[] toBytes(char c) { + static byte[] toBytes(char c) { byte[] result = new byte[2]; putChar(result, 0, c); return result; @@ -442,7 +442,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { + static void getChars(byte[] value, int srcBegin, int srcEnd, char[] dst, int dstBegin) { // We need a range check here because 'getChar' has no checks if (srcBegin < srcEnd) { checkBoundsOffCount(srcBegin, srcEnd - srcBegin, value); @@ -453,7 +453,7 @@ final class StringUTF16 { } /* @see java.lang.String.getBytes(int, int, byte[], int) */ - public static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { + static void getBytes(byte[] value, int srcBegin, int srcEnd, byte[] dst, int dstBegin) { srcBegin <<= 1; srcEnd <<= 1; for (int i = srcBegin + (1 >> LO_BYTE_SHIFT); i < srcEnd; i += 2) { @@ -462,7 +462,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int compareTo(byte[] value, byte[] other) { + static int compareTo(byte[] value, byte[] other) { int len1 = length(value); int len2 = length(other); return compareValues(value, other, len1, len2); @@ -471,7 +471,7 @@ final class StringUTF16 { /* * Checks the boundary and then compares the byte arrays. */ - public static int compareTo(byte[] value, byte[] other, int len1, int len2) { + static int compareTo(byte[] value, byte[] other, int len1, int len2) { checkOffset(len1, value); checkOffset(len2, other); @@ -491,15 +491,15 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int compareToLatin1(byte[] value, byte[] other) { + static int compareToLatin1(byte[] value, byte[] other) { return -StringLatin1.compareToUTF16(other, value); } - public static int compareToLatin1(byte[] value, byte[] other, int len1, int len2) { + static int compareToLatin1(byte[] value, byte[] other, int len1, int len2) { return -StringLatin1.compareToUTF16(other, value, len2, len1); } - public static int compareToCI(byte[] value, byte[] other) { + static int compareToCI(byte[] value, byte[] other) { return compareToCIImpl(value, 0, length(value), other, 0, length(other)); } @@ -512,8 +512,8 @@ final class StringUTF16 { assert olast <= length(other); for (int k1 = toffset, k2 = ooffset; k1 < tlast && k2 < olast; k1++, k2++) { - int cp1 = (int)getChar(value, k1); - int cp2 = (int)getChar(other, k2); + int cp1 = getChar(value, k1); + int cp2 = getChar(other, k2); if (cp1 == cp2 || compareCodePointCI(cp1, cp2) == 0) { continue; @@ -588,16 +588,16 @@ final class StringUTF16 { return cp; } - public static int compareToCI_Latin1(byte[] value, byte[] other) { + static int compareToCI_Latin1(byte[] value, byte[] other) { return -StringLatin1.compareToCI_UTF16(other, value); } - public static int hashCode(byte[] value) { + static int hashCode(byte[] value) { return ArraysSupport.hashCodeOfUTF16(value, 0, value.length >> 1, 0); } // Caller must ensure that from- and toIndex are within bounds - public static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { + static int indexOf(byte[] value, int ch, int fromIndex, int toIndex) { if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) @@ -608,7 +608,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOf(byte[] value, byte[] str) { + static int indexOf(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -619,7 +619,7 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { + static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) { checkBoundsBeginEnd(fromIndex, valueCount, value); checkBoundsBeginEnd(0, strCount, str); return indexOfUnsafe(value, valueCount, str, strCount, fromIndex); @@ -657,7 +657,7 @@ final class StringUTF16 { * Handles indexOf Latin1 substring in UTF16 string. */ @IntrinsicCandidate - public static int indexOfLatin1(byte[] value, byte[] str) { + static int indexOfLatin1(byte[] value, byte[] str) { if (str.length == 0) { return 0; } @@ -668,13 +668,13 @@ final class StringUTF16 { } @IntrinsicCandidate - public static int indexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { + static int indexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { checkBoundsBeginEnd(fromIndex, srcCount, src); String.checkBoundsBeginEnd(0, tgtCount, tgt.length); return indexOfLatin1Unsafe(src, srcCount, tgt, tgtCount, fromIndex); } - public static int indexOfLatin1Unsafe(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { + static int indexOfLatin1Unsafe(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; assert tgtCount <= tgt.length; @@ -730,8 +730,8 @@ final class StringUTF16 { } // srcCoder == UTF16 && tgtCoder == UTF16 - public static int lastIndexOf(byte[] src, int srcCount, - byte[] tgt, int tgtCount, int fromIndex) { + static int lastIndexOf(byte[] src, int srcCount, + byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; assert tgtCount <= length(tgt); @@ -765,7 +765,7 @@ final class StringUTF16 { } } - public static int lastIndexOf(byte[] value, int ch, int fromIndex) { + static int lastIndexOf(byte[] value, int ch, int fromIndex) { if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) { // handle most cases here (ch is a BMP code point or a // negative value (invalid code point)) @@ -798,7 +798,7 @@ final class StringUTF16 { return -1; } - public static String replace(byte[] value, char oldChar, char newChar) { + static String replace(byte[] value, char oldChar, char newChar) { int len = value.length >> 1; int i = -1; while (++i < len) { @@ -829,9 +829,9 @@ final class StringUTF16 { return null; } - public static String replace(byte[] value, int valLen, boolean valLat1, - byte[] targ, int targLen, boolean targLat1, - byte[] repl, int replLen, boolean replLat1) + static String replace(byte[] value, int valLen, boolean valLat1, + byte[] targ, int targLen, boolean targLat1, + byte[] repl, int replLen, boolean replLat1) { assert targLen > 0; assert !valLat1 || !targLat1 || !replLat1; @@ -944,18 +944,18 @@ final class StringUTF16 { return new String(result, UTF16); } - public static boolean regionMatchesCI(byte[] value, int toffset, - byte[] other, int ooffset, int len) { + static boolean regionMatchesCI(byte[] value, int toffset, + byte[] other, int ooffset, int len) { return compareToCIImpl(value, toffset, len, other, ooffset, len) == 0; } - public static boolean regionMatchesCI_Latin1(byte[] value, int toffset, - byte[] other, int ooffset, - int len) { + static boolean regionMatchesCI_Latin1(byte[] value, int toffset, + byte[] other, int ooffset, + int len) { return StringLatin1.regionMatchesCI_UTF16(other, ooffset, value, toffset, len); } - public static String toLowerCase(String str, byte[] value, Locale locale) { + static String toLowerCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -965,7 +965,7 @@ final class StringUTF16 { // Now check if there are any characters that need to be changed, or are surrogate for (first = 0 ; first < len; first++) { - int cp = (int)getChar(value, first); + int cp = getChar(value, first); if (Character.isSurrogate((char)cp)) { hasSurr = true; break; @@ -988,7 +988,7 @@ final class StringUTF16 { } int bits = 0; for (int i = first; i < len; i++) { - int cp = (int)getChar(value, i); + int cp = getChar(value, i); if (cp == '\u03A3' || // GREEK CAPITAL LETTER SIGMA Character.isSurrogate((char)cp)) { return toLowerCaseEx(str, value, result, i, locale, false); @@ -1003,7 +1003,7 @@ final class StringUTF16 { bits |= cp; putChar(result, i, cp); } - if (bits < 0 || bits > 0xff) { + if (bits > 0xff) { return new String(result, UTF16); } else { return newString(result, 0, len); @@ -1059,7 +1059,7 @@ final class StringUTF16 { return newString(result, 0, resultOffset); } - public static String toUpperCase(String str, byte[] value, Locale locale) { + static String toUpperCase(String str, byte[] value, Locale locale) { if (locale == null) { throw new NullPointerException(); } @@ -1069,7 +1069,7 @@ final class StringUTF16 { // Now check if there are any characters that need to be changed, or are surrogate for (first = 0 ; first < len; first++) { - int cp = (int)getChar(value, first); + int cp = getChar(value, first); if (Character.isSurrogate((char)cp)) { hasSurr = true; break; @@ -1093,7 +1093,7 @@ final class StringUTF16 { } int bits = 0; for (int i = first; i < len; i++) { - int cp = (int)getChar(value, i); + int cp = getChar(value, i); if (Character.isSurrogate((char)cp)) { return toUpperCaseEx(str, value, result, i, locale, false); } @@ -1104,7 +1104,7 @@ final class StringUTF16 { bits |= cp; putChar(result, i, cp); } - if (bits < 0 || bits > 0xff) { + if (bits > 0xff) { return new String(result, UTF16); } else { return newString(result, 0, len); @@ -1164,7 +1164,7 @@ final class StringUTF16 { return newString(result, 0, resultOffset); } - public static String trim(byte[] value) { + static String trim(byte[] value) { int length = value.length >> 1; int len = length; int st = 0; @@ -1179,7 +1179,7 @@ final class StringUTF16 { null; } - public static int indexOfNonWhitespace(byte[] value) { + static int indexOfNonWhitespace(byte[] value) { int length = value.length >> 1; int left = 0; while (left < length) { @@ -1192,9 +1192,8 @@ final class StringUTF16 { return left; } - public static int lastIndexOfNonWhitespace(byte[] value) { - int length = value.length >>> 1; - int right = length; + static int lastIndexOfNonWhitespace(byte[] value) { + int right = value.length >>> 1; while (0 < right) { int codepoint = codePointBefore(value, right); if (codepoint != ' ' && codepoint != '\t' && !Character.isWhitespace(codepoint)) { @@ -1205,7 +1204,7 @@ final class StringUTF16 { return right; } - public static String strip(byte[] value) { + static String strip(byte[] value) { int length = value.length >>> 1; int left = indexOfNonWhitespace(value); if (left == length) { @@ -1216,13 +1215,13 @@ final class StringUTF16 { return ifChanged ? newString(value, left, right - left) : null; } - public static String stripLeading(byte[] value) { + static String stripLeading(byte[] value) { int length = value.length >>> 1; int left = indexOfNonWhitespace(value); return (left != 0) ? newString(value, left, length - left) : null; } - public static String stripTrailing(byte[] value) { + static String stripTrailing(byte[] value) { int length = value.length >>> 1; int right = lastIndexOfNonWhitespace(value); return (right != length) ? newString(value, 0, right) : null; @@ -1322,7 +1321,7 @@ final class StringUTF16 { return StreamSupport.stream(LinesSpliterator.spliterator(value), false); } - public static String newString(byte[] val, int index, int len) { + static String newString(byte[] val, int index, int len) { if (len == 0) { return ""; } @@ -1388,7 +1387,7 @@ final class StringUTF16 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { @@ -1473,7 +1472,7 @@ final class StringUTF16 { } @Override - public long estimateSize() { return (long)(fence - index); } + public long estimateSize() { return fence - index; } @Override public int characteristics() { @@ -1483,12 +1482,12 @@ final class StringUTF16 { //////////////////////////////////////////////////////////////// - public static void putCharSB(byte[] val, int index, int c) { + static void putCharSB(byte[] val, int index, int c) { checkIndex(index, val); putChar(val, index, c); } - public static void putCharsSB(byte[] val, int index, char[] ca, int off, int end) { + static void putCharsSB(byte[] val, int index, char[] ca, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); String.checkBoundsBeginEnd(off, end, ca.length); Unsafe.getUnsafe().copyMemory( @@ -1499,26 +1498,26 @@ final class StringUTF16 { (long) (end - off) << 1); } - public static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { + static void putCharsSB(byte[] val, int index, CharSequence s, int off, int end) { checkBoundsBeginEnd(index, index + end - off, val); for (int i = off; i < end; i++) { putChar(val, index++, s.charAt(i)); } } - public static int codePointAtSB(byte[] val, int index, int end) { + static int codePointAtSB(byte[] val, int index, int end) { return codePointAt(val, index, end, true /* checked */); } - public static int codePointBeforeSB(byte[] val, int index) { + static int codePointBeforeSB(byte[] val, int index) { return codePointBefore(val, index, true /* checked */); } - public static int codePointCountSB(byte[] val, int beginIndex, int endIndex) { + static int codePointCountSB(byte[] val, int beginIndex, int endIndex) { return codePointCount(val, beginIndex, endIndex, true /* checked */); } - public static boolean contentEquals(byte[] v1, byte[] v2, int len) { + static boolean contentEquals(byte[] v1, byte[] v2, int len) { checkBoundsOffCount(0, len, v2); for (int i = 0; i < len; i++) { if ((char)(v1[i] & 0xff) != getChar(v2, i)) { @@ -1528,7 +1527,7 @@ final class StringUTF16 { return true; } - public static boolean contentEquals(byte[] value, CharSequence cs, int len) { + static boolean contentEquals(byte[] value, CharSequence cs, int len) { checkOffset(len, value); for (int i = 0; i < len; i++) { if (getChar(value, i) != cs.charAt(i)) { @@ -1538,7 +1537,7 @@ final class StringUTF16 { return true; } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4) { int end = i + 4; checkBoundsBeginEnd(i, end, value); putChar(value, i, c1); @@ -1547,7 +1546,7 @@ final class StringUTF16 { putChar(value, i + 3, c4); } - public static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { + static void putCharsAt(byte[] value, int i, char c1, char c2, char c3, char c4, char c5) { int end = i + 5; checkBoundsBeginEnd(i, end, value); putChar(value, i, c1); @@ -1557,12 +1556,12 @@ final class StringUTF16 { putChar(value, i + 4, c5); } - public static char charAt(byte[] value, int index) { + static char charAt(byte[] value, int index) { checkIndex(index, value); return getChar(value, index); } - public static void reverse(byte[] val, int count) { + static void reverse(byte[] val, int count) { checkOffset(count, val); int n = count - 1; boolean hasSurrogates = false; @@ -1597,7 +1596,7 @@ final class StringUTF16 { } // inflatedCopy byte[] -> byte[] - public static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { + static void inflate(byte[] src, int srcOff, byte[] dst, int dstOff, int len) { // We need a range check here because 'putChar' has no checks checkBoundsOffCount(dstOff, len, dst); for (int i = 0; i < len; i++) { @@ -1606,7 +1605,7 @@ final class StringUTF16 { } // srcCoder == UTF16 && tgtCoder == LATIN1 - public static int lastIndexOfLatin1(byte[] src, int srcCount, + static int lastIndexOfLatin1(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) { assert fromIndex >= 0; assert tgtCount > 0; @@ -1659,19 +1658,19 @@ final class StringUTF16 { static final int MAX_LENGTH = Integer.MAX_VALUE >> 1; - public static void checkIndex(int off, byte[] val) { + static void checkIndex(int off, byte[] val) { String.checkIndex(off, length(val)); } - public static void checkOffset(int off, byte[] val) { + static void checkOffset(int off, byte[] val) { String.checkOffset(off, length(val)); } - public static void checkBoundsBeginEnd(int begin, int end, byte[] val) { + static void checkBoundsBeginEnd(int begin, int end, byte[] val) { String.checkBoundsBeginEnd(begin, end, length(val)); } - public static void checkBoundsOffCount(int offset, int count, byte[] val) { + static void checkBoundsOffCount(int offset, int count, byte[] val) { String.checkBoundsOffCount(offset, count, length(val)); } From 837f634bf29fd877dd62a2e0f7d7a1bd383372d3 Mon Sep 17 00:00:00 2001 From: "Daniel D. Daugherty" Date: Fri, 3 Oct 2025 21:11:33 +0000 Subject: [PATCH 351/556] 8369128: ProblemList jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java in Xcomp configs 8369132: Disable vmTestbase/gc/vector/CircularListLow and LinearListLow with SerialGC 8369133: Disable gc/g1/TestShrinkAuxiliaryDataRunner.java with UseLargePages option Reviewed-by: ayang, dholmes --- test/hotspot/jtreg/gc/g1/TestShrinkAuxiliaryDataRunner.java | 1 + .../vmTestbase/gc/vector/CircularListLow/TestDescription.java | 1 + .../vmTestbase/gc/vector/LinearListLow/TestDescription.java | 1 + test/jdk/ProblemList-Xcomp.txt | 1 + 4 files changed, 4 insertions(+) diff --git a/test/hotspot/jtreg/gc/g1/TestShrinkAuxiliaryDataRunner.java b/test/hotspot/jtreg/gc/g1/TestShrinkAuxiliaryDataRunner.java index 309ba722787..d478f811490 100644 --- a/test/hotspot/jtreg/gc/g1/TestShrinkAuxiliaryDataRunner.java +++ b/test/hotspot/jtreg/gc/g1/TestShrinkAuxiliaryDataRunner.java @@ -29,6 +29,7 @@ package gc.g1; * @bug 8038423 8061715 * @summary Checks that decommitment occurs for JVM with different ObjectAlignmentInBytes values * @requires vm.gc.G1 + * @requires !vm.opt.final.UseLargePages * @library /test/lib * @library / * @modules java.base/jdk.internal.misc diff --git a/test/hotspot/jtreg/vmTestbase/gc/vector/CircularListLow/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/vector/CircularListLow/TestDescription.java index 68fe6779e99..252e3a2d40f 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/vector/CircularListLow/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/vector/CircularListLow/TestDescription.java @@ -31,5 +31,6 @@ * * @library /vmTestbase * /test/lib + * @requires vm.gc != "Serial" * @run main/othervm/timeout=480 gc.vector.SimpleGC.SimpleGC -ms low -gp circularList(low) */ diff --git a/test/hotspot/jtreg/vmTestbase/gc/vector/LinearListLow/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/vector/LinearListLow/TestDescription.java index 8ae86af035d..bd094045abf 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/vector/LinearListLow/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/vector/LinearListLow/TestDescription.java @@ -31,5 +31,6 @@ * * @library /vmTestbase * /test/lib + * @requires vm.gc != "Serial" * @run main/othervm gc.vector.SimpleGC.SimpleGC -ms low -gp linearList(low) */ diff --git a/test/jdk/ProblemList-Xcomp.txt b/test/jdk/ProblemList-Xcomp.txt index 5ed171a1fea..8e37e6e15d0 100644 --- a/test/jdk/ProblemList-Xcomp.txt +++ b/test/jdk/ProblemList-Xcomp.txt @@ -29,3 +29,4 @@ java/lang/invoke/MethodHandles/CatchExceptionTest.java 8146623 generic-all java/lang/reflect/callerCache/ReflectionCallerCacheTest.java 8332028 generic-all +jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java 8367302 linux-all From e6868c624851d5c6bd182e45ba908cb06b731e8c Mon Sep 17 00:00:00 2001 From: "Daniel D. Daugherty" Date: Fri, 3 Oct 2025 22:17:01 +0000 Subject: [PATCH 352/556] 8369138: New test compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java fails Reviewed-by: kvn --- .../MissingStoreAfterOuterStripMinedLoop.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java b/test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java index 8fa6cae12e6..24e3c4a5d30 100644 --- a/test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java +++ b/test/hotspot/jtreg/compiler/loopstripmining/MissingStoreAfterOuterStripMinedLoop.java @@ -29,7 +29,8 @@ * leading to wrong results. * * @run main/othervm -XX:+IgnoreUnrecognizedVMOptions -XX:-TieredCompilation - * -Xcomp -XX:-UseLoopPredicate -XX:-UseAutoVectorizationPredicate + * -Xcomp -XX:-UseLoopPredicate + * -XX:+UnlockDiagnosticVMOptions -XX:-UseAutoVectorizationPredicate * -XX:CompileCommand=compileonly,compiler.loopstripmining.MissingStoreAfterOuterStripMinedLoop::test* * compiler.loopstripmining.MissingStoreAfterOuterStripMinedLoop * @run main compiler.loopstripmining.MissingStoreAfterOuterStripMinedLoop @@ -133,4 +134,4 @@ public class MissingStoreAfterOuterStripMinedLoop { throw new RuntimeException("unexpected value: " + a1.field + " " + a2.field + " " + a3.field); } } -} \ No newline at end of file +} From c3fbbfabcc9a9535a3b422c1c9afaa8e092a5da0 Mon Sep 17 00:00:00 2001 From: Sergey Bylokhov Date: Fri, 3 Oct 2025 23:16:41 +0000 Subject: [PATCH 353/556] 8369027: Apply java.io.Serial annotations in java.scripting Reviewed-by: rriggs --- .../share/classes/javax/script/ScriptException.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/java.scripting/share/classes/javax/script/ScriptException.java b/src/java.scripting/share/classes/javax/script/ScriptException.java index 1037ccde27f..48fb3221914 100644 --- a/src/java.scripting/share/classes/javax/script/ScriptException.java +++ b/src/java.scripting/share/classes/javax/script/ScriptException.java @@ -25,6 +25,8 @@ package javax.script; +import java.io.Serial; + /** * The generic Exception class for the Scripting APIs. Checked * exception types thrown by underlying scripting implementations must be wrapped in instances of @@ -36,6 +38,7 @@ package javax.script; */ public class ScriptException extends Exception { + @Serial private static final long serialVersionUID = 8265071037049225001L; /** @serial */ From 76dba201fa1a525780677e4d3dee8e9ffafd1cd7 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Sat, 4 Oct 2025 08:09:09 +0000 Subject: [PATCH 354/556] 8368821: Test java/net/httpclient/http3/GetHTTP3Test.java intermittently fails with java.io.IOException: QUIC endpoint closed Reviewed-by: dfuchs --- test/jdk/java/net/httpclient/http3/GetHTTP3Test.java | 12 ++++++------ .../jdk/java/net/httpclient/http3/PostHTTP3Test.java | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java b/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java index a67710c485f..14842d401fd 100644 --- a/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java +++ b/test/jdk/java/net/httpclient/http3/GetHTTP3Test.java @@ -33,7 +33,6 @@ import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -47,6 +46,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.SSLContext; +import jdk.test.lib.Utils; import jdk.test.lib.net.SimpleSSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.http2.Http2TestServer; @@ -74,8 +74,9 @@ import static java.lang.System.out; * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.Utils * @compile ../ReferenceTracker.java - * @run testng/othervm/timeout=60 -Djdk.internal.httpclient.debug=true + * @run testng/othervm -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.HttpClient.log=requests,responses,errors * GetHTTP3Test * @summary Basic HTTP/3 GET test @@ -216,7 +217,6 @@ public class GetHTTP3Test implements HttpServerAdapters { .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) - .connectTimeout(Duration.ofSeconds(10)) .build(); return TRACKER.track(client); } @@ -348,7 +348,7 @@ public class GetHTTP3Test implements HttpServerAdapters { var tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker, 1000); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1000)); if (error != null) throw error; } System.out.println("test: DONE"); @@ -394,7 +394,7 @@ public class GetHTTP3Test implements HttpServerAdapters { var tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker, 1000); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1000)); if (error != null) throw error; } @@ -423,7 +423,7 @@ public class GetHTTP3Test implements HttpServerAdapters { sharedClient == null ? null : sharedClient.toString(); sharedClient = null; Thread.sleep(100); - AssertionError fail = TRACKER.check(500); + AssertionError fail = TRACKER.check(Utils.adjustTimeout(1000)); try { h3TestServer.stop(); } finally { diff --git a/test/jdk/java/net/httpclient/http3/PostHTTP3Test.java b/test/jdk/java/net/httpclient/http3/PostHTTP3Test.java index 0e7a57ca699..8bb6515d4ff 100644 --- a/test/jdk/java/net/httpclient/http3/PostHTTP3Test.java +++ b/test/jdk/java/net/httpclient/http3/PostHTTP3Test.java @@ -38,7 +38,6 @@ import java.net.http.HttpOption.Http3DiscoveryMode; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,6 +52,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import javax.net.ssl.SSLContext; +import jdk.test.lib.Utils; import jdk.test.lib.net.SimpleSSLContext; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.http2.Http2TestServer; @@ -79,6 +79,7 @@ import static java.lang.System.out; * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.HttpServerAdapters + * jdk.test.lib.Utils * @compile ../ReferenceTracker.java * @run testng/othervm -Djdk.internal.httpclient.debug=true * -Djdk.httpclient.HttpClient.log=requests,responses,errors @@ -221,7 +222,6 @@ public class PostHTTP3Test implements HttpServerAdapters { .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) - .connectTimeout(Duration.ofSeconds(10)) .build(); return TRACKER.track(client); } @@ -375,7 +375,7 @@ public class PostHTTP3Test implements HttpServerAdapters { var tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker, 1000); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1000)); if (error != null) throw error; } System.out.println("test: DONE"); @@ -437,7 +437,7 @@ public class PostHTTP3Test implements HttpServerAdapters { var tracker = TRACKER.getTracker(client); client = null; System.gc(); - AssertionError error = TRACKER.check(tracker, 1000); + AssertionError error = TRACKER.check(tracker, Utils.adjustTimeout(1000)); if (error != null) throw error; } @@ -466,7 +466,7 @@ public class PostHTTP3Test implements HttpServerAdapters { sharedClient == null ? null : sharedClient.toString(); sharedClient = null; Thread.sleep(100); - AssertionError fail = TRACKER.check(500); + AssertionError fail = TRACKER.check(Utils.adjustTimeout(1000)); try { h3TestServer.stop(); } finally { From f740cd2aad43a008da1ed1ff15ebe2c790f893a0 Mon Sep 17 00:00:00 2001 From: Chad Rakoczy Date: Sat, 4 Oct 2025 21:17:26 +0000 Subject: [PATCH 355/556] 8316694: Implement relocation of nmethod within CodeCache Reviewed-by: kvn, eosterlund, never, eastigeevich, bulasevich --- src/hotspot/cpu/aarch64/relocInfo_aarch64.cpp | 1 - src/hotspot/share/code/codeBehaviours.cpp | 11 +- src/hotspot/share/code/codeBehaviours.hpp | 12 +- src/hotspot/share/code/codeCache.hpp | 2 +- src/hotspot/share/code/compiledIC.cpp | 4 +- src/hotspot/share/code/compiledIC.hpp | 2 +- src/hotspot/share/code/nmethod.cpp | 311 +++++++++++++++++- src/hotspot/share/code/nmethod.hpp | 24 +- src/hotspot/share/code/relocInfo.cpp | 3 +- src/hotspot/share/compiler/oopMap.cpp | 6 + src/hotspot/share/compiler/oopMap.hpp | 2 + .../share/gc/shenandoah/shenandoahUnload.cpp | 2 +- src/hotspot/share/gc/z/zUnload.cpp | 2 +- src/hotspot/share/jvmci/jvmciRuntime.hpp | 5 + src/hotspot/share/prims/whitebox.cpp | 58 +++- src/hotspot/share/runtime/globals.hpp | 3 + .../classes/sun/jvm/hotspot/code/NMethod.java | 43 +-- .../whitebox/CompilerWhiteBoxTest.java | 56 +++- .../whitebox/DeoptimizeRelocatedNMethod.java | 157 +++++++++ .../compiler/whitebox/RelocateNMethod.java | 146 ++++++++ .../RelocateNMethodMultiplePaths.java | 261 +++++++++++++++ .../whitebox/StressNMethodRelocation.java | 239 ++++++++++++++ .../NMethodRelocationTest.java | 178 ++++++++++ .../libNMethodRelocationTest.cpp | 114 +++++++ test/lib/jdk/test/whitebox/WhiteBox.java | 6 + test/lib/jdk/test/whitebox/code/CodeBlob.java | 7 +- 26 files changed, 1591 insertions(+), 64 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/whitebox/DeoptimizeRelocatedNMethod.java create mode 100644 test/hotspot/jtreg/compiler/whitebox/RelocateNMethod.java create mode 100644 test/hotspot/jtreg/compiler/whitebox/RelocateNMethodMultiplePaths.java create mode 100644 test/hotspot/jtreg/compiler/whitebox/StressNMethodRelocation.java create mode 100644 test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java create mode 100644 test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp diff --git a/src/hotspot/cpu/aarch64/relocInfo_aarch64.cpp b/src/hotspot/cpu/aarch64/relocInfo_aarch64.cpp index f5d7d9e4387..94694b58d2f 100644 --- a/src/hotspot/cpu/aarch64/relocInfo_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/relocInfo_aarch64.cpp @@ -90,7 +90,6 @@ void Relocation::pd_set_call_destination(address x) { void trampoline_stub_Relocation::pd_fix_owner_after_move() { NativeCall* call = nativeCall_at(owner()); - assert(call->raw_destination() == owner(), "destination should be empty"); address trampoline = addr(); address dest = nativeCallTrampolineStub_at(trampoline)->destination(); if (!Assembler::reachable_from_branch_at(owner(), dest)) { diff --git a/src/hotspot/share/code/codeBehaviours.cpp b/src/hotspot/share/code/codeBehaviours.cpp index 1f9eb0e2914..bc5a81c95b3 100644 --- a/src/hotspot/share/code/codeBehaviours.cpp +++ b/src/hotspot/share/code/codeBehaviours.cpp @@ -23,23 +23,24 @@ */ #include "code/codeBehaviours.hpp" +#include "code/nmethod.hpp" #include "runtime/mutexLocker.hpp" #include "runtime/safepoint.hpp" CompiledICProtectionBehaviour* CompiledICProtectionBehaviour::_current = nullptr; -bool DefaultICProtectionBehaviour::lock(nmethod* method) { - if (is_safe(method)) { +bool DefaultICProtectionBehaviour::lock(nmethod* nm) { + if (is_safe(nm)) { return false; } CompiledIC_lock->lock_without_safepoint_check(); return true; } -void DefaultICProtectionBehaviour::unlock(nmethod* method) { +void DefaultICProtectionBehaviour::unlock(nmethod* nm) { CompiledIC_lock->unlock(); } -bool DefaultICProtectionBehaviour::is_safe(nmethod* method) { - return SafepointSynchronize::is_at_safepoint() || CompiledIC_lock->owned_by_self(); +bool DefaultICProtectionBehaviour::is_safe(nmethod* nm) { + return SafepointSynchronize::is_at_safepoint() || CompiledIC_lock->owned_by_self() || (NMethodState_lock->owned_by_self() && nm->is_not_installed()); } diff --git a/src/hotspot/share/code/codeBehaviours.hpp b/src/hotspot/share/code/codeBehaviours.hpp index 0350f5752f6..96a38fffc30 100644 --- a/src/hotspot/share/code/codeBehaviours.hpp +++ b/src/hotspot/share/code/codeBehaviours.hpp @@ -33,18 +33,18 @@ class CompiledICProtectionBehaviour { static CompiledICProtectionBehaviour* _current; public: - virtual bool lock(nmethod* method) = 0; - virtual void unlock(nmethod* method) = 0; - virtual bool is_safe(nmethod* method) = 0; + virtual bool lock(nmethod* nm) = 0; + virtual void unlock(nmethod* nm) = 0; + virtual bool is_safe(nmethod* nm) = 0; static CompiledICProtectionBehaviour* current() { return _current; } static void set_current(CompiledICProtectionBehaviour* current) { _current = current; } }; class DefaultICProtectionBehaviour: public CompiledICProtectionBehaviour, public CHeapObj { - virtual bool lock(nmethod* method); - virtual void unlock(nmethod* method); - virtual bool is_safe(nmethod* method); + virtual bool lock(nmethod* nm); + virtual void unlock(nmethod* nm); + virtual bool is_safe(nmethod* nm); }; #endif // SHARE_CODE_CODEBEHAVIOURS_HPP diff --git a/src/hotspot/share/code/codeCache.hpp b/src/hotspot/share/code/codeCache.hpp index 3e446ab8430..dc9e5d7dc20 100644 --- a/src/hotspot/share/code/codeCache.hpp +++ b/src/hotspot/share/code/codeCache.hpp @@ -259,7 +259,7 @@ class CodeCache : AllStatic { static bool heap_available(CodeBlobType code_blob_type); // Returns the CodeBlobType for the given nmethod - static CodeBlobType get_code_blob_type(nmethod* nm) { + static CodeBlobType get_code_blob_type(const nmethod* nm) { return get_code_heap(nm)->code_blob_type(); } diff --git a/src/hotspot/share/code/compiledIC.cpp b/src/hotspot/share/code/compiledIC.cpp index 03d9adc8e24..5f5c9711441 100644 --- a/src/hotspot/share/code/compiledIC.cpp +++ b/src/hotspot/share/code/compiledIC.cpp @@ -55,8 +55,8 @@ CompiledICLocker::~CompiledICLocker() { } } -bool CompiledICLocker::is_safe(nmethod* method) { - return CompiledICProtectionBehaviour::current()->is_safe(method); +bool CompiledICLocker::is_safe(nmethod* nm) { + return CompiledICProtectionBehaviour::current()->is_safe(nm); } bool CompiledICLocker::is_safe(address code) { diff --git a/src/hotspot/share/code/compiledIC.hpp b/src/hotspot/share/code/compiledIC.hpp index 624c1b428de..db8a0860b39 100644 --- a/src/hotspot/share/code/compiledIC.hpp +++ b/src/hotspot/share/code/compiledIC.hpp @@ -50,7 +50,7 @@ class CompiledICLocker: public StackObj { public: CompiledICLocker(nmethod* method); ~CompiledICLocker(); - static bool is_safe(nmethod* method); + static bool is_safe(nmethod* nm); static bool is_safe(address code); }; diff --git a/src/hotspot/share/code/nmethod.cpp b/src/hotspot/share/code/nmethod.cpp index 1540fa0333a..7274b627f3e 100644 --- a/src/hotspot/share/code/nmethod.cpp +++ b/src/hotspot/share/code/nmethod.cpp @@ -754,7 +754,7 @@ Method* nmethod::attached_method_before_pc(address pc) { } void nmethod::clear_inline_caches() { - assert(SafepointSynchronize::is_at_safepoint(), "clearing of IC's only allowed at safepoint"); + assert(SafepointSynchronize::is_at_safepoint() || (NMethodState_lock->owned_by_self() && is_not_installed()), "clearing of IC's only allowed at safepoint or when not installed"); RelocIterator iter(this); while (iter.next()) { iter.reloc()->clear_inline_cache(); @@ -1146,7 +1146,8 @@ nmethod* nmethod::new_nmethod(const methodHandle& method, #if INCLUDE_JVMCI + align_up(speculations_len , oopSize) #endif - + align_up(debug_info->data_size() , oopSize); + + align_up(debug_info->data_size() , oopSize) + + align_up(ImmutableDataReferencesCounterSize, oopSize); // First, allocate space for immutable data in C heap. address immutable_data = nullptr; @@ -1371,10 +1372,266 @@ nmethod::nmethod( } } + +nmethod::nmethod(const nmethod &nm) : CodeBlob(nm._name, nm._kind, nm._size, nm._header_size) +{ + + if (nm._oop_maps != nullptr) { + _oop_maps = nm._oop_maps->clone(); + } else { + _oop_maps = nullptr; + } + + _size = nm._size; + _relocation_size = nm._relocation_size; + _content_offset = nm._content_offset; + _code_offset = nm._code_offset; + _data_offset = nm._data_offset; + _frame_size = nm._frame_size; + + S390_ONLY( _ctable_offset = nm._ctable_offset; ) + + _header_size = nm._header_size; + _frame_complete_offset = nm._frame_complete_offset; + + _kind = nm._kind; + + _caller_must_gc_arguments = nm._caller_must_gc_arguments; + +#ifndef PRODUCT + _asm_remarks.share(nm._asm_remarks); + _dbg_strings.share(nm._dbg_strings); +#endif + + // Allocate memory and copy mutable data to C heap + _mutable_data_size = nm._mutable_data_size; + if (_mutable_data_size > 0) { + _mutable_data = (address)os::malloc(_mutable_data_size, mtCode); + if (_mutable_data == nullptr) { + vm_exit_out_of_memory(_mutable_data_size, OOM_MALLOC_ERROR, "nmethod: no space for mutable data"); + } + memcpy(mutable_data_begin(), nm.mutable_data_begin(), nm.mutable_data_size()); + } else { + _mutable_data = nullptr; + } + + _deoptimization_generation = 0; + _gc_epoch = CodeCache::gc_epoch(); + _method = nm._method; + _osr_link = nullptr; + + // Increment number of references to immutable data to share it between nmethods + _immutable_data_size = nm._immutable_data_size; + if (_immutable_data_size > 0) { + _immutable_data = nm._immutable_data; + set_immutable_data_references_counter(get_immutable_data_references_counter() + 1); + } else { + _immutable_data = blob_end(); + } + + _exception_cache = nullptr; + _gc_data = nullptr; + _oops_do_mark_nmethods = nullptr; + _oops_do_mark_link = nullptr; + _compiled_ic_data = nullptr; + + if (nm._osr_entry_point != nullptr) { + _osr_entry_point = (nm._osr_entry_point - (address) &nm) + (address) this; + } else { + _osr_entry_point = nullptr; + } + + _entry_offset = nm._entry_offset; + _verified_entry_offset = nm._verified_entry_offset; + _entry_bci = nm._entry_bci; + + _skipped_instructions_size = nm._skipped_instructions_size; + _stub_offset = nm._stub_offset; + _exception_offset = nm._exception_offset; + _deopt_handler_offset = nm._deopt_handler_offset; + _unwind_handler_offset = nm._unwind_handler_offset; + _num_stack_arg_slots = nm._num_stack_arg_slots; + _oops_size = nm._oops_size; +#if INCLUDE_JVMCI + _metadata_size = nm._metadata_size; +#endif + _nul_chk_table_offset = nm._nul_chk_table_offset; + _handler_table_offset = nm._handler_table_offset; + _scopes_pcs_offset = nm._scopes_pcs_offset; + _scopes_data_offset = nm._scopes_data_offset; +#if INCLUDE_JVMCI + _speculations_offset = nm._speculations_offset; +#endif + + _orig_pc_offset = nm._orig_pc_offset; + _compile_id = nm._compile_id; + _comp_level = nm._comp_level; + _compiler_type = nm._compiler_type; + _is_unloading_state = nm._is_unloading_state; + _state = not_installed; + + _has_unsafe_access = nm._has_unsafe_access; + _has_wide_vectors = nm._has_wide_vectors; + _has_monitors = nm._has_monitors; + _has_scoped_access = nm._has_scoped_access; + _has_flushed_dependencies = nm._has_flushed_dependencies; + _is_unlinked = nm._is_unlinked; + _load_reported = nm._load_reported; + + _deoptimization_status = nm._deoptimization_status; + + if (nm._pc_desc_container != nullptr) { + _pc_desc_container = new PcDescContainer(scopes_pcs_begin()); + } else { + _pc_desc_container = nullptr; + } + + // Copy nmethod contents excluding header + // - Constant part (doubles, longs and floats used in nmethod) + // - Code part: + // - Code body + // - Exception handler + // - Stub code + // - OOP table + memcpy(consts_begin(), nm.consts_begin(), nm.data_end() - nm.consts_begin()); + + post_init(); +} + +nmethod* nmethod::relocate(CodeBlobType code_blob_type) { + assert(NMethodRelocation, "must enable use of function"); + + // Locks required to be held by caller to ensure the nmethod + // is not modified or purged from code cache during relocation + assert_lock_strong(CodeCache_lock); + assert_lock_strong(Compile_lock); + assert(CompiledICLocker::is_safe(this), "mt unsafe call"); + + if (!is_relocatable()) { + return nullptr; + } + + run_nmethod_entry_barrier(); + nmethod* nm_copy = new (size(), code_blob_type) nmethod(*this); + + if (nm_copy == nullptr) { + return nullptr; + } + + // Fix relocation + RelocIterator iter(nm_copy); + CodeBuffer src(this); + CodeBuffer dst(nm_copy); + while (iter.next()) { +#ifdef USE_TRAMPOLINE_STUB_FIX_OWNER + // Direct calls may no longer be in range and the use of a trampoline may now be required. + // Instead, allow trampoline relocations to update their owners and perform the necessary checks. + if (iter.reloc()->is_call()) { + address trampoline = trampoline_stub_Relocation::get_trampoline_for(iter.reloc()->addr(), nm_copy); + if (trampoline != nullptr) { + continue; + } + } +#endif + + iter.reloc()->fix_relocation_after_move(&src, &dst); + } + + // To make dependency checking during class loading fast, record + // the nmethod dependencies in the classes it is dependent on. + // This allows the dependency checking code to simply walk the + // class hierarchy above the loaded class, checking only nmethods + // which are dependent on those classes. The slow way is to + // check every nmethod for dependencies which makes it linear in + // the number of methods compiled. For applications with a lot + // classes the slow way is too slow. + for (Dependencies::DepStream deps(nm_copy); deps.next(); ) { + if (deps.type() == Dependencies::call_site_target_value) { + // CallSite dependencies are managed on per-CallSite instance basis. + oop call_site = deps.argument_oop(0); + MethodHandles::add_dependent_nmethod(call_site, nm_copy); + } else { + InstanceKlass* ik = deps.context_type(); + if (ik == nullptr) { + continue; // ignore things like evol_method + } + // record this nmethod as dependent on this klass + ik->add_dependent_nmethod(nm_copy); + } + } + + MutexLocker ml_NMethodState_lock(NMethodState_lock, Mutex::_no_safepoint_check_flag); + + // Verify the nm we copied from is still valid + if (!is_marked_for_deoptimization() && is_in_use()) { + assert(method() != nullptr && method()->code() == this, "should be if is in use"); + + nm_copy->clear_inline_caches(); + + // Attempt to start using the copy + if (nm_copy->make_in_use()) { + ICache::invalidate_range(nm_copy->code_begin(), nm_copy->code_size()); + + methodHandle mh(Thread::current(), nm_copy->method()); + nm_copy->method()->set_code(mh, nm_copy); + + make_not_used(); + + nm_copy->post_compiled_method_load_event(); + + nm_copy->log_relocated_nmethod(this); + + return nm_copy; + } + } + + nm_copy->make_not_used(); + + return nullptr; +} + +bool nmethod::is_relocatable() { + if (!is_java_method()) { + return false; + } + + if (!is_in_use()) { + return false; + } + + if (is_osr_method()) { + return false; + } + + if (is_marked_for_deoptimization()) { + return false; + } + +#if INCLUDE_JVMCI + if (jvmci_nmethod_data() != nullptr && jvmci_nmethod_data()->has_mirror()) { + return false; + } +#endif + + if (is_unloading()) { + return false; + } + + if (has_evol_metadata()) { + return false; + } + + return true; +} + void* nmethod::operator new(size_t size, int nmethod_size, int comp_level) throw () { return CodeCache::allocate(nmethod_size, CodeCache::get_code_blob_type(comp_level)); } +void* nmethod::operator new(size_t size, int nmethod_size, CodeBlobType code_blob_type) throw () { + return CodeCache::allocate(nmethod_size, code_blob_type); +} + void* nmethod::operator new(size_t size, int nmethod_size, bool allow_NonNMethod_space) throw () { // Try MethodNonProfiled and MethodProfiled. void* return_value = CodeCache::allocate(nmethod_size, CodeBlobType::MethodNonProfiled); @@ -1494,9 +1751,9 @@ nmethod::nmethod( #if INCLUDE_JVMCI _speculations_offset = _scopes_data_offset + align_up(debug_info->data_size(), oopSize); - DEBUG_ONLY( int immutable_data_end_offset = _speculations_offset + align_up(speculations_len, oopSize); ) + DEBUG_ONLY( int immutable_data_end_offset = _speculations_offset + align_up(speculations_len, oopSize) + align_up(ImmutableDataReferencesCounterSize, oopSize); ) #else - DEBUG_ONLY( int immutable_data_end_offset = _scopes_data_offset + align_up(debug_info->data_size(), oopSize); ) + DEBUG_ONLY( int immutable_data_end_offset = _scopes_data_offset + align_up(debug_info->data_size(), oopSize) + align_up(ImmutableDataReferencesCounterSize, oopSize); ) #endif assert(immutable_data_end_offset <= immutable_data_size, "wrong read-only data size: %d > %d", immutable_data_end_offset, immutable_data_size); @@ -1529,6 +1786,7 @@ nmethod::nmethod( memcpy(speculations_begin(), speculations, speculations_len); } #endif + set_immutable_data_references_counter(1); post_init(); @@ -1595,6 +1853,40 @@ void nmethod::log_new_nmethod() const { } } + +void nmethod::log_relocated_nmethod(nmethod* original) const { + if (LogCompilation && xtty != nullptr) { + ttyLocker ttyl; + xtty->begin_elem("relocated nmethod"); + log_identity(xtty); + xtty->print(" entry='" INTPTR_FORMAT "' size='%d'", p2i(code_begin()), size()); + + const char* original_code_heap_name = CodeCache::get_code_heap_name(CodeCache::get_code_blob_type(original)); + xtty->print(" original_address='" INTPTR_FORMAT "'", p2i(original)); + xtty->print(" original_code_heap='%s'", original_code_heap_name); + + const char* new_code_heap_name = CodeCache::get_code_heap_name(CodeCache::get_code_blob_type(this)); + xtty->print(" new_address='" INTPTR_FORMAT "'", p2i(this)); + xtty->print(" new_code_heap='%s'", new_code_heap_name); + + LOG_OFFSET(xtty, relocation); + LOG_OFFSET(xtty, consts); + LOG_OFFSET(xtty, insts); + LOG_OFFSET(xtty, stub); + LOG_OFFSET(xtty, scopes_data); + LOG_OFFSET(xtty, scopes_pcs); + LOG_OFFSET(xtty, dependencies); + LOG_OFFSET(xtty, handler_table); + LOG_OFFSET(xtty, nul_chk_table); + LOG_OFFSET(xtty, oops); + LOG_OFFSET(xtty, metadata); + + xtty->method(method()); + xtty->stamp(); + xtty->end_elem(); + } +} + #undef LOG_OFFSET @@ -2127,9 +2419,18 @@ void nmethod::purge(bool unregister_nmethod) { delete[] _compiled_ic_data; if (_immutable_data != blob_end()) { - os::free(_immutable_data); + int reference_count = get_immutable_data_references_counter(); + assert(reference_count > 0, "immutable data has no references"); + + set_immutable_data_references_counter(reference_count - 1); + // Free memory if this is the last nmethod referencing immutable data + if (reference_count == 0) { + os::free(_immutable_data); + } + _immutable_data = blob_end(); // Valid not null address } + if (unregister_nmethod) { Universe::heap()->unregister_nmethod(this); } diff --git a/src/hotspot/share/code/nmethod.hpp b/src/hotspot/share/code/nmethod.hpp index 1e876657098..2332766a47c 100644 --- a/src/hotspot/share/code/nmethod.hpp +++ b/src/hotspot/share/code/nmethod.hpp @@ -154,6 +154,7 @@ public: // - Scopes data array // - Scopes pcs array // - JVMCI speculations array +// - Nmethod reference counter #if INCLUDE_JVMCI class FailedSpeculation; @@ -167,6 +168,8 @@ class nmethod : public CodeBlob { friend class JVMCINMethodData; friend class DeoptimizationScope; + #define ImmutableDataReferencesCounterSize ((int)sizeof(int)) + private: // Used to track in which deoptimize handshake this method will be deoptimized. @@ -330,8 +333,11 @@ class nmethod : public CodeBlob { #endif ); + nmethod(const nmethod &nm); + // helper methods void* operator new(size_t size, int nmethod_size, int comp_level) throw(); + void* operator new(size_t size, int nmethod_size, CodeBlobType code_blob_type) throw(); // For method handle intrinsics: Try MethodNonProfiled, MethodProfiled and NonNMethod. // Attention: Only allow NonNMethod space for special nmethods which don't need to be @@ -564,6 +570,12 @@ public: #endif ); + // Relocate the nmethod to the code heap identified by code_blob_type. + // Returns nullptr if the code heap does not have enough space, the + // nmethod is unrelocatable, or the nmethod is invalidated during relocation, + // otherwise the relocated nmethod. The original nmethod will be marked not entrant. + nmethod* relocate(CodeBlobType code_blob_type); + static nmethod* new_native_nmethod(const methodHandle& method, int compile_id, CodeBuffer *code_buffer, @@ -580,6 +592,8 @@ public: bool is_java_method () const { return _method != nullptr && !_method->is_native(); } bool is_osr_method () const { return _entry_bci != InvocationEntryBci; } + bool is_relocatable(); + // Compiler task identification. Note that all OSR methods // are numbered in an independent sequence if CICountOSR is true, // and native method wrappers are also numbered independently if @@ -632,11 +646,13 @@ public: #if INCLUDE_JVMCI address scopes_data_end () const { return _immutable_data + _speculations_offset ; } address speculations_begin () const { return _immutable_data + _speculations_offset ; } - address speculations_end () const { return immutable_data_end(); } + address speculations_end () const { return immutable_data_end() - ImmutableDataReferencesCounterSize ; } #else - address scopes_data_end () const { return immutable_data_end(); } + address scopes_data_end () const { return immutable_data_end() - ImmutableDataReferencesCounterSize ; } #endif + address immutable_data_references_counter_begin () const { return immutable_data_end() - ImmutableDataReferencesCounterSize ; } + // Sizes int immutable_data_size() const { return _immutable_data_size; } int consts_size () const { return int( consts_end () - consts_begin ()); } @@ -946,6 +962,9 @@ public: bool load_reported() const { return _load_reported; } void set_load_reported() { _load_reported = true; } + inline int get_immutable_data_references_counter() { return *((int*)immutable_data_references_counter_begin()); } + inline void set_immutable_data_references_counter(int count) { *((int*)immutable_data_references_counter_begin()) = count; } + public: // ScopeDesc retrieval operation PcDesc* pc_desc_at(address pc) { return find_pc_desc(pc, false); } @@ -1014,6 +1033,7 @@ public: // Logging void log_identity(xmlStream* log) const; void log_new_nmethod() const; + void log_relocated_nmethod(nmethod* original) const; void log_state_change(InvalidationReason invalidation_reason) const; // Prints block-level comments, including nmethod specific block labels: diff --git a/src/hotspot/share/code/relocInfo.cpp b/src/hotspot/share/code/relocInfo.cpp index 8fc22596d01..02a1e5faf16 100644 --- a/src/hotspot/share/code/relocInfo.cpp +++ b/src/hotspot/share/code/relocInfo.cpp @@ -406,11 +406,12 @@ void CallRelocation::fix_relocation_after_move(const CodeBuffer* src, CodeBuffer pd_set_call_destination(callee); } - #ifdef USE_TRAMPOLINE_STUB_FIX_OWNER void trampoline_stub_Relocation::fix_relocation_after_move(const CodeBuffer* src, CodeBuffer* dest) { // Finalize owner destination only for nmethods if (dest->blob() != nullptr) return; + // We either relocate a nmethod residing in CodeCache or just generated code from CodeBuffer + assert(src->blob() == nullptr || nativeCall_at(owner())->raw_destination() == owner(), "destination should be empty"); pd_fix_owner_after_move(); } #endif diff --git a/src/hotspot/share/compiler/oopMap.cpp b/src/hotspot/share/compiler/oopMap.cpp index aa010872975..87467d06400 100644 --- a/src/hotspot/share/compiler/oopMap.cpp +++ b/src/hotspot/share/compiler/oopMap.cpp @@ -862,6 +862,12 @@ ImmutableOopMapSet* ImmutableOopMapSet::build_from(const OopMapSet* oopmap_set) return builder.build(); } +ImmutableOopMapSet* ImmutableOopMapSet::clone() const { + address buffer = NEW_C_HEAP_ARRAY(unsigned char, _size, mtCode); + memcpy(buffer, (address)this, _size); + return (ImmutableOopMapSet*)buffer; +} + void ImmutableOopMapSet::operator delete(void* p) { FREE_C_HEAP_ARRAY(unsigned char, p); } diff --git a/src/hotspot/share/compiler/oopMap.hpp b/src/hotspot/share/compiler/oopMap.hpp index ef8845ac9aa..f7a8cd8496c 100644 --- a/src/hotspot/share/compiler/oopMap.hpp +++ b/src/hotspot/share/compiler/oopMap.hpp @@ -348,6 +348,8 @@ public: static ImmutableOopMapSet* build_from(const OopMapSet* oopmap_set); + ImmutableOopMapSet* clone() const; + int find_slot_for_offset(int pc_offset) const; const ImmutableOopMap* find_map_at_offset(int pc_offset) const; const ImmutableOopMap* find_map_at_slot(int slot, int pc_offset) const; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahUnload.cpp b/src/hotspot/share/gc/shenandoah/shenandoahUnload.cpp index 83151313f75..b248fab7958 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahUnload.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahUnload.cpp @@ -103,7 +103,7 @@ public: } virtual bool is_safe(nmethod* nm) { - if (SafepointSynchronize::is_at_safepoint() || nm->is_unloading()) { + if (SafepointSynchronize::is_at_safepoint() || nm->is_unloading() || (NMethodState_lock->owned_by_self() && nm->is_not_installed())) { return true; } diff --git a/src/hotspot/share/gc/z/zUnload.cpp b/src/hotspot/share/gc/z/zUnload.cpp index c8b32385fcd..5c50b3077dd 100644 --- a/src/hotspot/share/gc/z/zUnload.cpp +++ b/src/hotspot/share/gc/z/zUnload.cpp @@ -100,7 +100,7 @@ public: } virtual bool is_safe(nmethod* nm) { - if (SafepointSynchronize::is_at_safepoint() || nm->is_unloading()) { + if (SafepointSynchronize::is_at_safepoint() || nm->is_unloading() || (NMethodState_lock->owned_by_self() && nm->is_not_installed())) { return true; } diff --git a/src/hotspot/share/jvmci/jvmciRuntime.hpp b/src/hotspot/share/jvmci/jvmciRuntime.hpp index f4c322e831c..885ff0dbf9b 100644 --- a/src/hotspot/share/jvmci/jvmciRuntime.hpp +++ b/src/hotspot/share/jvmci/jvmciRuntime.hpp @@ -137,6 +137,11 @@ public: // Gets the JVMCI name of the nmethod (which may be null). const char* name() { return has_name() ? (char*)(((address) this) + sizeof(JVMCINMethodData)) : nullptr; } + // Returns true if this nmethod has a mirror + bool has_mirror() const { + return _nmethod_mirror_index != -1; + } + // Clears the HotSpotNmethod.address field in the mirror. If nm // is dead, the HotSpotNmethod.entryPoint field is also cleared. void invalidate_nmethod_mirror(nmethod* nm, nmethod::InvalidationReason invalidation_reason); diff --git a/src/hotspot/share/prims/whitebox.cpp b/src/hotspot/share/prims/whitebox.cpp index f77b648ba95..1ecd105f218 100644 --- a/src/hotspot/share/prims/whitebox.cpp +++ b/src/hotspot/share/prims/whitebox.cpp @@ -39,6 +39,7 @@ #include "classfile/systemDictionary.hpp" #include "classfile/vmSymbols.hpp" #include "code/codeCache.hpp" +#include "code/compiledIC.hpp" #include "compiler/compilationPolicy.hpp" #include "compiler/compilerOracle.hpp" #include "compiler/directivesParser.hpp" @@ -1548,19 +1549,23 @@ struct CodeBlobStub { name(os::strdup(blob->name())), size(blob->size()), blob_type(static_cast(WhiteBox::get_blob_type(blob))), - address((jlong) blob) { } + address((jlong) blob), + code_begin((jlong) blob->code_begin()), + is_nmethod((jboolean) blob->is_nmethod()) { } ~CodeBlobStub() { os::free((void*) name); } const char* const name; const jint size; const jint blob_type; const jlong address; + const jlong code_begin; + const jboolean is_nmethod; }; static jobjectArray codeBlob2objectArray(JavaThread* thread, JNIEnv* env, CodeBlobStub* cb) { ResourceMark rm; jclass clazz = env->FindClass(vmSymbols::java_lang_Object()->as_C_string()); CHECK_JNI_EXCEPTION_(env, nullptr); - jobjectArray result = env->NewObjectArray(4, clazz, nullptr); + jobjectArray result = env->NewObjectArray(6, clazz, nullptr); jstring name = env->NewStringUTF(cb->name); CHECK_JNI_EXCEPTION_(env, nullptr); @@ -1578,6 +1583,14 @@ static jobjectArray codeBlob2objectArray(JavaThread* thread, JNIEnv* env, CodeBl CHECK_JNI_EXCEPTION_(env, nullptr); env->SetObjectArrayElement(result, 3, obj); + obj = longBox(thread, env, cb->code_begin); + CHECK_JNI_EXCEPTION_(env, nullptr); + env->SetObjectArrayElement(result, 4, obj); + + obj = booleanBox(thread, env, cb->is_nmethod); + CHECK_JNI_EXCEPTION_(env, nullptr); + env->SetObjectArrayElement(result, 5, obj); + return result; } @@ -1627,6 +1640,44 @@ WB_ENTRY(jobjectArray, WB_GetNMethod(JNIEnv* env, jobject o, jobject method, jbo return result; WB_END +WB_ENTRY(void, WB_RelocateNMethodFromMethod(JNIEnv* env, jobject o, jobject method, jint blob_type)) + ResourceMark rm(THREAD); + jmethodID jmid = reflected_method_to_jmid(thread, env, method); + CHECK_JNI_EXCEPTION(env); + methodHandle mh(THREAD, Method::checked_resolve_jmethod_id(jmid)); + nmethod* code = mh->code(); + if (code != nullptr) { + MutexLocker ml_Compile_lock(Compile_lock); + CompiledICLocker ic_locker(code); + MutexLocker ml_CodeCache_lock(CodeCache_lock, Mutex::_no_safepoint_check_flag); + code->relocate(static_cast(blob_type)); + } +WB_END + +WB_ENTRY(void, WB_RelocateNMethodFromAddr(JNIEnv* env, jobject o, jlong addr, jint blob_type)) + ResourceMark rm(THREAD); + CHECK_JNI_EXCEPTION(env); + void* address = (void*) addr; + + if (address == nullptr) { + return; + } + + MutexLocker ml_Compile_lock(Compile_lock); + MutexLocker ml_CompiledIC_lock(CompiledIC_lock, Mutex::_no_safepoint_check_flag); + MutexLocker ml_CodeCache_lock(CodeCache_lock, Mutex::_no_safepoint_check_flag); + + // Verify that nmethod address is still valid + CodeBlob* blob = CodeCache::find_blob(address); + if (blob != nullptr && blob->is_nmethod()) { + nmethod* code = blob->as_nmethod(); + if (code->is_in_use()) { + CompiledICLocker ic_locker(code); + code->relocate(static_cast(blob_type)); + } + } +WB_END + CodeBlob* WhiteBox::allocate_code_blob(int size, CodeBlobType blob_type) { guarantee(WhiteBoxAPI, "internal testing API :: WhiteBox has to be enabled"); BufferBlob* blob; @@ -2916,6 +2967,9 @@ static JNINativeMethod methods[] = { {CC"getCPUFeatures", CC"()Ljava/lang/String;", (void*)&WB_GetCPUFeatures }, {CC"getNMethod0", CC"(Ljava/lang/reflect/Executable;Z)[Ljava/lang/Object;", (void*)&WB_GetNMethod }, + {CC"relocateNMethodFromMethod0", CC"(Ljava/lang/reflect/Executable;I)V", + (void*)&WB_RelocateNMethodFromMethod }, + {CC"relocateNMethodFromAddr", CC"(JI)V", (void*)&WB_RelocateNMethodFromAddr }, {CC"allocateCodeBlob", CC"(II)J", (void*)&WB_AllocateCodeBlob }, {CC"freeCodeBlob", CC"(J)V", (void*)&WB_FreeCodeBlob }, {CC"getCodeHeapEntries", CC"(I)[Ljava/lang/Object;",(void*)&WB_GetCodeHeapEntries }, diff --git a/src/hotspot/share/runtime/globals.hpp b/src/hotspot/share/runtime/globals.hpp index dac01d018bf..513edaf6588 100644 --- a/src/hotspot/share/runtime/globals.hpp +++ b/src/hotspot/share/runtime/globals.hpp @@ -1565,6 +1565,9 @@ const int ObjectAlignmentInBytes = 8; "Start aggressive sweeping if less than X[%] of the total code cache is free.")\ range(0, 100) \ \ + product(bool, NMethodRelocation, false, EXPERIMENTAL, \ + "Enables use of experimental function nmethod::relocate()") \ + \ /* interpreter debugging */ \ develop(intx, BinarySwitchThreshold, 5, \ "Minimal number of lookupswitch entries for rewriting to binary " \ diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java index c8ba2e8b5af..939b47fdd2a 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/NMethod.java @@ -37,6 +37,7 @@ import sun.jvm.hotspot.utilities.Observer; public class NMethod extends CodeBlob { private static long pcDescSize; + private static long immutableDataReferencesCounterSize; private static AddressField methodField; /** != InvocationEntryBci if this nmethod is an on-stack replacement method */ private static CIntegerField entryBCIField; @@ -78,24 +79,25 @@ public class NMethod extends CodeBlob { private static void initialize(TypeDataBase db) { Type type = db.lookupType("nmethod"); - methodField = type.getAddressField("_method"); - entryBCIField = type.getCIntegerField("_entry_bci"); - osrLinkField = type.getAddressField("_osr_link"); - immutableDataField = type.getAddressField("_immutable_data"); - immutableDataSizeField = type.getCIntegerField("_immutable_data_size"); - exceptionOffsetField = type.getCIntegerField("_exception_offset"); - deoptHandlerOffsetField = type.getCIntegerField("_deopt_handler_offset"); - origPCOffsetField = type.getCIntegerField("_orig_pc_offset"); - stubOffsetField = type.getCIntegerField("_stub_offset"); - scopesPCsOffsetField = type.getCIntegerField("_scopes_pcs_offset"); - scopesDataOffsetField = type.getCIntegerField("_scopes_data_offset"); - handlerTableOffsetField = new CIntField(type.getCIntegerField("_handler_table_offset"), 0); - nulChkTableOffsetField = new CIntField(type.getCIntegerField("_nul_chk_table_offset"), 0); - entryOffsetField = new CIntField(type.getCIntegerField("_entry_offset"), 0); - verifiedEntryOffsetField = new CIntField(type.getCIntegerField("_verified_entry_offset"), 0); - osrEntryPointField = type.getAddressField("_osr_entry_point"); - compLevelField = new CIntField(type.getCIntegerField("_comp_level"), 0); - pcDescSize = db.lookupType("PcDesc").getSize(); + methodField = type.getAddressField("_method"); + entryBCIField = type.getCIntegerField("_entry_bci"); + osrLinkField = type.getAddressField("_osr_link"); + immutableDataField = type.getAddressField("_immutable_data"); + immutableDataSizeField = type.getCIntegerField("_immutable_data_size"); + exceptionOffsetField = type.getCIntegerField("_exception_offset"); + deoptHandlerOffsetField = type.getCIntegerField("_deopt_handler_offset"); + origPCOffsetField = type.getCIntegerField("_orig_pc_offset"); + stubOffsetField = type.getCIntegerField("_stub_offset"); + scopesPCsOffsetField = type.getCIntegerField("_scopes_pcs_offset"); + scopesDataOffsetField = type.getCIntegerField("_scopes_data_offset"); + handlerTableOffsetField = new CIntField(type.getCIntegerField("_handler_table_offset"), 0); + nulChkTableOffsetField = new CIntField(type.getCIntegerField("_nul_chk_table_offset"), 0); + entryOffsetField = new CIntField(type.getCIntegerField("_entry_offset"), 0); + verifiedEntryOffsetField = new CIntField(type.getCIntegerField("_verified_entry_offset"), 0); + osrEntryPointField = type.getAddressField("_osr_entry_point"); + compLevelField = new CIntField(type.getCIntegerField("_comp_level"), 0); + pcDescSize = db.lookupType("PcDesc").getSize(); + immutableDataReferencesCounterSize = VM.getVM().getIntSize(); } public NMethod(Address addr) { @@ -139,7 +141,7 @@ public class NMethod extends CodeBlob { public Address scopesDataBegin() { return immutableDataBegin().addOffsetTo(getScopesDataOffset()); } public Address scopesDataEnd() { return immutableDataBegin().addOffsetTo(getScopesPCsOffset()); } public Address scopesPCsBegin() { return immutableDataBegin().addOffsetTo(getScopesPCsOffset()); } - public Address scopesPCsEnd() { return immutableDataEnd(); } + public Address scopesPCsEnd() { return immutableDataEnd().addOffsetTo(-immutableDataReferencesCounterSize); } public Address metadataBegin() { return mutableDataBegin().addOffsetTo(getRelocationSize()); } public Address metadataEnd() { return mutableDataEnd(); } @@ -169,7 +171,8 @@ public class NMethod extends CodeBlob { scopesPCsSize() + dependenciesSize() + handlerTableSize() + - nulChkTableSize(); + nulChkTableSize() + + (int) immutableDataReferencesCounterSize; } public boolean constantsContains (Address addr) { return constantsBegin() .lessThanOrEqual(addr) && constantsEnd() .greaterThan(addr); } diff --git a/test/hotspot/jtreg/compiler/whitebox/CompilerWhiteBoxTest.java b/test/hotspot/jtreg/compiler/whitebox/CompilerWhiteBoxTest.java index f87292be019..eb0f70af5c4 100644 --- a/test/hotspot/jtreg/compiler/whitebox/CompilerWhiteBoxTest.java +++ b/test/hotspot/jtreg/compiler/whitebox/CompilerWhiteBoxTest.java @@ -221,15 +221,28 @@ public abstract class CompilerWhiteBoxTest { * compilation level. */ protected final void checkNotCompiled(boolean isOsr) { - if (WHITE_BOX.isMethodQueuedForCompilation(method)) { - throw new RuntimeException(method + " must not be in queue"); + checkNotCompiled(method, isOsr); + } + + /** + * Checks, that the specified executable is not (OSR-)compiled. + * + * @param executable The method or constructor to check. + * @param isOsr Check for OSR compilation if true + * @throws RuntimeException if {@linkplain #method} is in compiler queue or + * is compiled, or if {@linkplain #method} has zero + * compilation level. + */ + protected static final void checkNotCompiled(Executable executable, boolean isOsr) { + if (WHITE_BOX.isMethodQueuedForCompilation(executable)) { + throw new RuntimeException(executable + " must not be in queue"); } - if (WHITE_BOX.isMethodCompiled(method, isOsr)) { - throw new RuntimeException(method + " must not be " + + if (WHITE_BOX.isMethodCompiled(executable, isOsr)) { + throw new RuntimeException(executable + " must not be " + (isOsr ? "osr_" : "") + "compiled"); } - if (WHITE_BOX.getMethodCompilationLevel(method, isOsr) != 0) { - throw new RuntimeException(method + (isOsr ? " osr_" : " ") + + if (WHITE_BOX.getMethodCompilationLevel(executable, isOsr) != 0) { + throw new RuntimeException(executable + (isOsr ? " osr_" : " ") + "comp_level must be == 0"); } } @@ -242,21 +255,34 @@ public abstract class CompilerWhiteBoxTest { * has nonzero compilation level */ protected final void checkCompiled() { + checkCompiled(method, testCase.isOsr()); + } + + /** + * Checks, that the specified executable is compiled. + * + * @param executable The method or constructor to check. + * @param isOsr Check for OSR compilation if true + * @throws RuntimeException if {@linkplain #method} isn't in compiler queue + * and isn't compiled, or if {@linkplain #method} + * has nonzero compilation level + */ + protected static final void checkCompiled(Executable executable, boolean isOsr) { final long start = System.currentTimeMillis(); - waitBackgroundCompilation(); - if (WHITE_BOX.isMethodQueuedForCompilation(method)) { + waitBackgroundCompilation(executable); + if (WHITE_BOX.isMethodQueuedForCompilation(executable)) { System.err.printf("Warning: %s is still in queue after %dms%n", - method, System.currentTimeMillis() - start); + executable, System.currentTimeMillis() - start); return; } - if (!WHITE_BOX.isMethodCompiled(method, testCase.isOsr())) { - throw new RuntimeException(method + " must be " - + (testCase.isOsr() ? "osr_" : "") + "compiled"); + if (!WHITE_BOX.isMethodCompiled(executable, isOsr)) { + throw new RuntimeException(executable + " must be " + + (isOsr ? "osr_" : "") + "compiled"); } - if (WHITE_BOX.getMethodCompilationLevel(method, testCase.isOsr()) + if (WHITE_BOX.getMethodCompilationLevel(executable, isOsr) == 0) { - throw new RuntimeException(method - + (testCase.isOsr() ? " osr_" : " ") + throw new RuntimeException(executable + + (isOsr ? " osr_" : " ") + "comp_level must be != 0"); } } diff --git a/test/hotspot/jtreg/compiler/whitebox/DeoptimizeRelocatedNMethod.java b/test/hotspot/jtreg/compiler/whitebox/DeoptimizeRelocatedNMethod.java new file mode 100644 index 00000000000..25314051fdd --- /dev/null +++ b/test/hotspot/jtreg/compiler/whitebox/DeoptimizeRelocatedNMethod.java @@ -0,0 +1,157 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +/* + * @test id=Serial + * @bug 8316694 + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Serial + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache -XX:+UseSerialGC + * -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.DeoptimizeRelocatedNMethod + */ + +/* + * @test id=Parallel + * @bug 8316694 + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Parallel + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache -XX:+UseParallelGC + * -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.DeoptimizeRelocatedNMethod + */ + +/* + * @test id=G1 + * @bug 8316694 + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.G1 + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache -XX:+UseG1GC + * -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.DeoptimizeRelocatedNMethod + */ + +/* + * @test id=Shenandoah + * @bug 8316694 + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Shenandoah + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache -XX:+UseShenandoahGC + * -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.DeoptimizeRelocatedNMethod + */ + +/* + * @test id=ZGC + * @bug 8316694 + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Z + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache -XX:+UseZGC + * -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.DeoptimizeRelocatedNMethod + */ + +package compiler.whitebox; + +import compiler.whitebox.CompilerWhiteBoxTest; +import java.lang.reflect.Method; +import jdk.test.whitebox.WhiteBox; +import jdk.test.whitebox.code.BlobType; +import jdk.test.whitebox.code.NMethod; + +public class DeoptimizeRelocatedNMethod { + + private static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox(); + public static double FUNCTION_RESULT = 0; + + public static void main(String [] args) throws Exception { + // Get method that will be relocated + Method method = DeoptimizeRelocatedNMethod.class.getMethod("function"); + WHITE_BOX.testSetDontInlineMethod(method, true); + + // Verify not initially compiled + CompilerWhiteBoxTest.checkNotCompiled(method, false); + + // Call function enough to compile + callFunction(); + + // Verify now compiled + CompilerWhiteBoxTest.checkCompiled(method, false); + + // Get newly created nmethod + NMethod origNmethod = NMethod.get(method, false); + + // Relocate nmethod and mark old for cleanup + WHITE_BOX.relocateNMethodFromMethod(method, BlobType.MethodProfiled.id); + + // Trigger GC to clean up old nmethod + WHITE_BOX.fullGC(); + + // Verify function still compiled after old was cleaned up + CompilerWhiteBoxTest.checkCompiled(method, false); + + // Get new nmethod and verify it's actually new + NMethod newNmethod = NMethod.get(method, false); + if (origNmethod.entry_point == newNmethod.entry_point) { + throw new RuntimeException("Did not create new nmethod"); + } + + // Call to verify everything still works + function(); + + // Deoptimized method + WHITE_BOX.deoptimizeMethod(method); + + CompilerWhiteBoxTest.checkNotCompiled(method, false); + + // Call to verify everything still works + function(); + } + + // Call function multiple times to trigger compilation + private static void callFunction() { + for (int i = 0; i < CompilerWhiteBoxTest.THRESHOLD; i++) { + function(); + } + } + + public static void function() { + FUNCTION_RESULT = Math.random(); + } +} diff --git a/test/hotspot/jtreg/compiler/whitebox/RelocateNMethod.java b/test/hotspot/jtreg/compiler/whitebox/RelocateNMethod.java new file mode 100644 index 00000000000..c18a8afa400 --- /dev/null +++ b/test/hotspot/jtreg/compiler/whitebox/RelocateNMethod.java @@ -0,0 +1,146 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +/* + * @test id=Serial + * @bug 8316694 + * @summary test that nmethod::relocate() correctly creates a new nmethod + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Serial + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache + * -XX:+UseSerialGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.RelocateNMethod + */ + +/* + * @test id=Parallel + * @bug 8316694 + * @summary test that nmethod::relocate() correctly creates a new nmethod + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Parallel + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache + * -XX:+UseParallelGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.RelocateNMethod + */ + +/* + * @test id=G1 + * @bug 8316694 + * @summary test that nmethod::relocate() correctly creates a new nmethod + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.G1 + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache + * -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.RelocateNMethod + */ + +/* + * @test id=Shenandoah + * @bug 8316694 + * @summary test that nmethod::relocate() correctly creates a new nmethod + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Shenandoah + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache + * -XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.RelocateNMethod + */ + +/* + * @test id=ZGC + * @bug 8316694 + * @summary test that nmethod::relocate() correctly creates a new nmethod + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @requires vm.opt.DeoptimizeALot != true + * @requires vm.gc.Z + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+SegmentedCodeCache + * -XX:+UseZGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation compiler.whitebox.RelocateNMethod + */ + +package compiler.whitebox; + +import java.lang.reflect.Method; +import jdk.test.whitebox.code.BlobType; +import jdk.test.whitebox.code.NMethod; +import jdk.test.whitebox.WhiteBox; + +import compiler.whitebox.CompilerWhiteBoxTest; + +public class RelocateNMethod extends CompilerWhiteBoxTest { + + public static void main(String[] args) throws Exception { + CompilerWhiteBoxTest.main(RelocateNMethod::new, new String[] {"CONSTRUCTOR_TEST", "METHOD_TEST", "STATIC_TEST"}); + } + + private RelocateNMethod(TestCase testCase) { + super(testCase); + // to prevent inlining of #method + WHITE_BOX.testSetDontInlineMethod(method, true); + } + + @Override + protected void test() throws Exception { + checkNotCompiled(); + + compile(); + + checkCompiled(); + NMethod origNmethod = NMethod.get(method, false); + + WHITE_BOX.relocateNMethodFromMethod(method, BlobType.MethodProfiled.id); + + WHITE_BOX.fullGC(); + + checkCompiled(); + + NMethod newNmethod = NMethod.get(method, false); + if (origNmethod.entry_point == newNmethod.entry_point) { + throw new RuntimeException("Did not create new nmethod"); + } + } +} diff --git a/test/hotspot/jtreg/compiler/whitebox/RelocateNMethodMultiplePaths.java b/test/hotspot/jtreg/compiler/whitebox/RelocateNMethodMultiplePaths.java new file mode 100644 index 00000000000..49be3eff8c2 --- /dev/null +++ b/test/hotspot/jtreg/compiler/whitebox/RelocateNMethodMultiplePaths.java @@ -0,0 +1,261 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +/* + * @test id=SerialC1 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Serial + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation -XX:TieredStopAtLevel=1 + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseSerialGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=SerialC2 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Serial + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseSerialGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=ParallelC1 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Parallel + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation -XX:TieredStopAtLevel=1 + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseParallelGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=ParallelC2 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Parallel + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseParallelGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=G1C1 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.G1 + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation -XX:TieredStopAtLevel=1 + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=G1C2 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.G1 + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=ShenandoahC1 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Shenandoah + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation -XX:TieredStopAtLevel=1 + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=ShenandoahC2 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Shenandoah + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=ZGCC1 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Z + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation -XX:TieredStopAtLevel=1 + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseZGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +/* + * @test id=ZGCC2 + * @bug 8316694 + * @requires vm.debug == true + * @requires vm.gc.Z + * @summary test that relocated nmethod is correctly deoptimized + * @library /test/lib / + * @modules java.base/jdk.internal.misc java.management + * + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbatch -XX:+TieredCompilation + * -XX:+SegmentedCodeCache -XX:-DeoptimizeRandom -XX:+DeoptimizeALot -XX:+UseZGC -XX:+UnlockExperimentalVMOptions -XX:+NMethodRelocation + * compiler.whitebox.RelocateNMethodMultiplePaths + */ + +package compiler.whitebox; + +import compiler.whitebox.CompilerWhiteBoxTest; +import java.lang.reflect.Method; +import jdk.test.whitebox.WhiteBox; +import jdk.test.whitebox.code.BlobType; +import jdk.test.whitebox.code.NMethod; + +public class RelocateNMethodMultiplePaths { + + private static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox(); + + private static final int PATH_ONE_RESULT = 1; + private static final int PATH_TWO_RESULT = 2; + + public static void main(String [] args) throws Exception { + // Get method that will be relocated + Method method = RelocateNMethodMultiplePaths.class.getMethod("function", boolean.class); + WHITE_BOX.testSetDontInlineMethod(method, true); + + // Verify not initially compiled + CompilerWhiteBoxTest.checkNotCompiled(method, false); + + // Call function enough to compile + callFunction(true); + + // Verify now compiled + CompilerWhiteBoxTest.checkCompiled(method, false); + + // Get newly created nmethod + NMethod origNmethod = NMethod.get(method, false); + + // Relocate nmethod and mark old for cleanup + WHITE_BOX.relocateNMethodFromMethod(method, BlobType.MethodNonProfiled.id); + + // Trigger GC to clean up old nmethod + WHITE_BOX.fullGC(); + + // Verify function still compiled after old was cleaned up + CompilerWhiteBoxTest.checkCompiled(method, false); + + // Get new nmethod and verify it's actually new + NMethod newNmethod = NMethod.get(method, false); + if (origNmethod.entry_point == newNmethod.entry_point) { + throw new RuntimeException("Did not create new nmethod"); + } + + // Verify function still produces correct result + if (function(true) != PATH_ONE_RESULT) { + throw new RuntimeException("Relocated function produced incorrect result in path one"); + } + + // Call function again with different path and verify result + if (function(false) != PATH_TWO_RESULT) { + throw new RuntimeException("Relocated function produced incorrect result in path two"); + } + + // Verify function can be correctly deoptimized + WHITE_BOX.deoptimizeMethod(method); + CompilerWhiteBoxTest.checkNotCompiled(method, false); + } + + // Call function multiple times to trigger compilation + private static void callFunction(boolean pathOne) { + for (int i = 0; i < CompilerWhiteBoxTest.THRESHOLD; i++) { + function(pathOne); + } + } + + public static int function(boolean pathOne) { + if (pathOne) { + return PATH_ONE_RESULT; + } else { + return PATH_TWO_RESULT; + } + } +} diff --git a/test/hotspot/jtreg/compiler/whitebox/StressNMethodRelocation.java b/test/hotspot/jtreg/compiler/whitebox/StressNMethodRelocation.java new file mode 100644 index 00000000000..3b397f48306 --- /dev/null +++ b/test/hotspot/jtreg/compiler/whitebox/StressNMethodRelocation.java @@ -0,0 +1,239 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +/* + * @test StressNMethodRelocation + * @summary Call and relocate methods concurrently + * @library /test/lib / + * @modules java.base/jdk.internal.misc + * java.management + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI + * -XX:+SegmentedCodeCache -XX:+UnlockExperimentalVMOptions + * -XX:+NMethodRelocation compiler.whitebox.StressNMethodRelocation + */ + +package compiler.whitebox; + +import jdk.test.whitebox.WhiteBox; +import jdk.test.whitebox.code.BlobType; +import jdk.test.whitebox.code.CodeBlob; +import jdk.test.whitebox.code.NMethod; + +import jdk.test.lib.compiler.InMemoryJavaCompiler; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Random; + +public class StressNMethodRelocation { + private static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox(); + private static final int C2_LEVEL = 4; + private static final int ACTIVE_METHODS = 1024; + + private static TestMethod[] methods; + private static byte[] num1; + private static byte[] num2; + + private static long DURATION = 60_000; + + public static void main(String[] args) throws Exception { + // Initialize defaults + initNums(); + + // Generate compiled code + methods = new TestMethod[ACTIVE_METHODS]; + generateCode(methods); + + // Create thread that runs compiled methods + RunMethods runMethods = new RunMethods(); + Thread runMethodsThread = new Thread(runMethods); + + // Create thread that relocates compiled methods + RelocateNMethods relocate = new RelocateNMethods(); + Thread relocateThread = new Thread(relocate); + + // Start theads + runMethodsThread.start(); + relocateThread.start(); + + // Wait for threads to finish + runMethodsThread.join(); + relocateThread.join(); + } + + private static byte[] genNum(Random random, int digitCount) { + byte[] num = new byte[digitCount]; + int d; + do { + d = random.nextInt(10); + } while (d == 0); + + num[0] = (byte)d; + for (int i = 1; i < digitCount; ++i) { + num[i] = (byte)random.nextInt(10); + } + return num; + } + + private static void initNums() { + final long seed = 8374592837465123L; + Random random = new Random(seed); + + final int digitCount = 40; + num1 = genNum(random, digitCount); + num2 = genNum(random, digitCount); + } + + private static void generateCode(TestMethod[] m) throws Exception { + byte[] result = new byte[num1.length + 1]; + + for (int i = 0; i < ACTIVE_METHODS; ++i) { + m[i] = new TestMethod(); + m[i].profile(num1, num2, result); + m[i].compileWithC2(); + } + } + + private static final class TestMethod { + private static final String CLASS_NAME = "A"; + private static final String METHOD_TO_COMPILE = "sum"; + private static final String JAVA_CODE = """ + public class A { + + public static void sum(byte[] n1, byte[] n2, byte[] out) { + final int digitCount = n1.length; + int carry = 0; + for (int i = digitCount - 1; i >= 0; --i) { + int sum = n1[i] + n2[i] + carry; + out[i] = (byte)(sum % 10); + carry = sum / 10; + } + if (carry != 0) { + for (int i = digitCount; i > 0; --i) { + out[i] = out[i - 1]; + } + out[0] = (byte)carry; + } + } + }"""; + + private static final byte[] BYTE_CODE; + + static { + BYTE_CODE = InMemoryJavaCompiler.compile(CLASS_NAME, JAVA_CODE); + } + + private final Method method; + + private static ClassLoader createClassLoaderFor() { + return new ClassLoader() { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (!name.equals(CLASS_NAME)) { + return super.loadClass(name); + } + + return defineClass(name, BYTE_CODE, 0, BYTE_CODE.length); + } + }; + } + + public TestMethod() throws Exception { + var cl = createClassLoaderFor().loadClass(CLASS_NAME); + method = cl.getMethod(METHOD_TO_COMPILE, byte[].class, byte[].class, byte[].class); + WHITE_BOX.testSetDontInlineMethod(method, true); + } + + public void profile(byte[] num1, byte[] num2, byte[] result) throws Exception { + method.invoke(null, num1, num2, result); + WHITE_BOX.markMethodProfiled(method); + } + + public void invoke(byte[] num1, byte[] num2, byte[] result) throws Exception { + method.invoke(null, num1, num2, result); + } + + public void compileWithC2() throws Exception { + WHITE_BOX.enqueueMethodForCompilation(method, C2_LEVEL); + while (WHITE_BOX.isMethodQueuedForCompilation(method)) { + Thread.onSpinWait(); + } + if (WHITE_BOX.getMethodCompilationLevel(method) != C2_LEVEL) { + throw new IllegalStateException("Method " + method + " is not compiled by C2."); + } + } + } + + private static final class RelocateNMethods implements Runnable { + public RelocateNMethods() {} + + // Move nmethod back and forth between NonProfiled and Profiled code heaps + public void run() { + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < DURATION) { + // Relocate NonProfiled to Profiled + CodeBlob[] nonProfiledBlobs = CodeBlob.getCodeBlobs(BlobType.MethodNonProfiled); + for (CodeBlob blob : nonProfiledBlobs) { + if (blob.isNMethod) { + WHITE_BOX.relocateNMethodFromAddr(blob.address, BlobType.MethodProfiled.id); + } + } + + // Relocate Profiled to NonProfiled + CodeBlob[] profiledBlobs = CodeBlob.getCodeBlobs(BlobType.MethodProfiled); + for (CodeBlob blob : nonProfiledBlobs) { + if (blob.isNMethod) { + WHITE_BOX.relocateNMethodFromAddr(blob.address, BlobType.MethodNonProfiled.id); + } + } + } + } + } + + private static final class RunMethods implements Runnable { + public RunMethods() {} + + public void run() { + try { + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < DURATION) { + callMethods(); + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + private void callMethods() throws Exception { + for (var m : methods) { + byte[] result = new byte[num1.length + 1]; + m.invoke(num1, num2, result); + } + } + } + +} diff --git a/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java new file mode 100644 index 00000000000..b12e2c3455c --- /dev/null +++ b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java @@ -0,0 +1,178 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * + * @bug 8316694 + * @summary Verify that nmethod relocation posts the correct JVMTI events + * @requires vm.jvmti + * @library /test/lib /test/hotspot/jtreg + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm/native NMethodRelocationTest + */ + +import static compiler.whitebox.CompilerWhiteBoxTest.COMP_LEVEL_FULL_OPTIMIZATION; + +import java.lang.reflect.Executable; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jdk.test.lib.Asserts; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; +import jdk.test.whitebox.WhiteBox; +import jdk.test.whitebox.code.BlobType; +import jdk.test.whitebox.code.NMethod; + + +public class NMethodRelocationTest { + public static void main(String[] args) throws Exception { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder( + "-agentlib:NMethodRelocationTest", + "--enable-native-access=ALL-UNNAMED", + "-Xbootclasspath/a:.", + "-XX:+UseSerialGC", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+WhiteBoxAPI", + "-XX:+SegmentedCodeCache", + "-XX:-TieredCompilation", + "-XX:+UnlockExperimentalVMOptions", + "-XX:+NMethodRelocation", + "DoWork"); + + OutputAnalyzer oa = new OutputAnalyzer(pb.start()); + String output = oa.getOutput(); + if (oa.getExitValue() != 0) { + System.err.println(oa.getOutput()); + throw new RuntimeException("Non-zero exit code returned from the test"); + } + Asserts.assertTrue(oa.getExitValue() == 0); + + Pattern pattern = Pattern.compile("(?m)^Relocated nmethod from (0x[0-9a-f]{16}) to (0x[0-9a-f]{16})$"); + Matcher matcher = pattern.matcher(output); + + if (matcher.find()) { + String fromAddr = matcher.group(1); + String toAddr = matcher.group(2); + + // Confirm events sent for both original and relocated nmethod + oa.shouldContain(": name: compiledMethod, code: " + fromAddr); + oa.shouldContain(": name: compiledMethod, code: " + toAddr); + oa.shouldContain(": name: compiledMethod, code: " + fromAddr); + oa.shouldContain(": name: compiledMethod, code: " + toAddr); + } else { + System.err.println(oa.getOutput()); + throw new RuntimeException("Unable to find relocation information"); + } + } +} + +class DoWork { + + protected static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox(); + + /** Load native library if required. */ + static { + try { + System.loadLibrary("NMethodRelocationTest"); + } catch (UnsatisfiedLinkError ule) { + System.err.println("Could not load NMethodRelocationTest library"); + System.err.println("java.library.path: " + + System.getProperty("java.library.path")); + throw ule; + } + } + + /** + * Returns value of VM option. + * + * @param name option's name + * @return value of option or {@code null}, if option doesn't exist + * @throws NullPointerException if name is null + */ + protected static String getVMOption(String name) { + Objects.requireNonNull(name); + return Objects.toString(WHITE_BOX.getVMFlag(name), null); + } + + /** + * Returns value of VM option or default value. + * + * @param name option's name + * @param defaultValue default value + * @return value of option or {@code defaultValue}, if option doesn't exist + * @throws NullPointerException if name is null + * @see #getVMOption(String) + */ + protected static String getVMOption(String name, String defaultValue) { + String result = getVMOption(name); + return result == null ? defaultValue : result; + } + + public static void main(String argv[]) throws Exception { + run(); + } + + public static void run() throws Exception { + Executable method = DoWork.class.getDeclaredMethod("compiledMethod"); + WHITE_BOX.testSetDontInlineMethod(method, true); + + WHITE_BOX.enqueueMethodForCompilation(method, COMP_LEVEL_FULL_OPTIMIZATION); + while (WHITE_BOX.isMethodQueuedForCompilation(method)) { + Thread.onSpinWait(); + } + + NMethod originalNMethod = NMethod.get(method, false); + if (originalNMethod == null) { + throw new AssertionError("Could not find original nmethod"); + } + + WHITE_BOX.relocateNMethodFromMethod(method, BlobType.MethodNonProfiled.id); + + NMethod relocatedNMethod = NMethod.get(method, false); + if (relocatedNMethod == null) { + throw new AssertionError("Could not find relocated nmethod"); + } + + if (originalNMethod.address == relocatedNMethod.address) { + throw new AssertionError("Relocated nmethod same as original"); + } + + WHITE_BOX.deoptimizeAll(); + + WHITE_BOX.fullGC(); + WHITE_BOX.fullGC(); + + WHITE_BOX.lockCompilation(); + + System.out.printf("Relocated nmethod from 0x%016x to 0x%016x%n", originalNMethod.code_begin, relocatedNMethod.code_begin); + System.out.flush(); + } + + public static long compiledMethod() { + return 0; + } +} diff --git a/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp new file mode 100644 index 00000000000..41ba6b10608 --- /dev/null +++ b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp @@ -0,0 +1,114 @@ +/* + * Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include +#include + +/** + * Callback for COMPILED_METHOD_LOAD event. + */ +JNIEXPORT void JNICALL +callbackCompiledMethodLoad(jvmtiEnv* jvmti, jmethodID method, + jint code_size, const void* code_addr, + jint map_length, const jvmtiAddrLocationMap* map, + const void* compile_info) { + char* name = nullptr; + char* sig = nullptr; + + if (jvmti->GetMethodName(method, &name, &sig, nullptr) != JVMTI_ERROR_NONE) { + printf(" [Could not retrieve method name]\n"); + fflush(stdout); + return; + } + + printf(": name: %s, code: 0x%016" PRIxPTR "\n", + name, (uintptr_t)code_addr); + fflush(stdout); +} + +/** + * Callback for COMPILED_METHOD_UNLOAD event. + */ +JNIEXPORT void JNICALL +callbackCompiledMethodUnload(jvmtiEnv* jvmti, jmethodID method, + const void* code_addr) { + char* name = nullptr; + char* sig = nullptr; + + if (jvmti->GetMethodName(method, &name, &sig, nullptr) != JVMTI_ERROR_NONE) { + printf(" [Could not retrieve method name]\n"); + fflush(stdout); + return; + } + printf(": name: %s, code: 0x%016" PRIxPTR "\n", + name, (uintptr_t)code_addr); + fflush(stdout); +} + +JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) { + jvmtiEnv* jvmti = nullptr; + jvmtiError error; + + if (jvm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) { + printf("Unable to access JVMTI!\n"); + return JNI_ERR; + } + + // Add required capabilities + jvmtiCapabilities caps; + memset(&caps, 0, sizeof(caps)); + caps.can_generate_compiled_method_load_events = 1; + error = jvmti->AddCapabilities(&caps); + if (error != JVMTI_ERROR_NONE) { + printf("ERROR: Unable to add capabilities, error=%d\n", error); + return JNI_ERR; + } + + // Set event callbacks + jvmtiEventCallbacks eventCallbacks; + memset(&eventCallbacks, 0, sizeof(eventCallbacks)); + eventCallbacks.CompiledMethodLoad = callbackCompiledMethodLoad; + eventCallbacks.CompiledMethodUnload = callbackCompiledMethodUnload; + error = jvmti->SetEventCallbacks(&eventCallbacks, sizeof(eventCallbacks)); + if (error != JVMTI_ERROR_NONE) { + printf("ERROR: Unable to set event callbacks, error=%d\n", error); + return JNI_ERR; + } + + // Enable events + error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_COMPILED_METHOD_LOAD, nullptr); + if (error != JVMTI_ERROR_NONE) { + printf("ERROR: Unable to enable COMPILED_METHOD_LOAD event, error=%d\n", error); + return JNI_ERR; + } + + error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_COMPILED_METHOD_UNLOAD, nullptr); + if (error != JVMTI_ERROR_NONE) { + printf("ERROR: Unable to enable COMPILED_METHOD_UNLOAD event, error=%d\n", error); + return JNI_ERR; + } + + return JNI_OK; +} diff --git a/test/lib/jdk/test/whitebox/WhiteBox.java b/test/lib/jdk/test/whitebox/WhiteBox.java index 5adb7bf5127..669ec48b619 100644 --- a/test/lib/jdk/test/whitebox/WhiteBox.java +++ b/test/lib/jdk/test/whitebox/WhiteBox.java @@ -490,6 +490,12 @@ public class WhiteBox { Objects.requireNonNull(method); return getNMethod0(method, isOsr); } + private native void relocateNMethodFromMethod0(Executable method, int type); + public void relocateNMethodFromMethod(Executable method, int type) { + Objects.requireNonNull(method); + relocateNMethodFromMethod0(method, type); + } + public native void relocateNMethodFromAddr(long address, int type); public native long allocateCodeBlob(int size, int type); public long allocateCodeBlob(long size, int type) { int intSize = (int) size; diff --git a/test/lib/jdk/test/whitebox/code/CodeBlob.java b/test/lib/jdk/test/whitebox/code/CodeBlob.java index c6c23fdff0c..fd95b5a7e7d 100644 --- a/test/lib/jdk/test/whitebox/code/CodeBlob.java +++ b/test/lib/jdk/test/whitebox/code/CodeBlob.java @@ -46,18 +46,22 @@ public class CodeBlob { return new CodeBlob(obj); } protected CodeBlob(Object[] obj) { - assert obj.length == 4; + assert obj.length == 6; name = (String) obj[0]; size = (Integer) obj[1]; int blob_type_index = (Integer) obj[2]; code_blob_type = BlobType.values()[blob_type_index]; assert code_blob_type.id == (Integer) obj[2]; address = (Long) obj[3]; + code_begin = (Long) obj[4]; + isNMethod = (Boolean) obj[5]; } public final String name; public final int size; public final BlobType code_blob_type; public final long address; + public final long code_begin; + public final boolean isNMethod; @Override public String toString() { return "CodeBlob{" @@ -65,6 +69,7 @@ public class CodeBlob { + ", size=" + size + ", code_blob_type=" + code_blob_type + ", address=" + address + + ", code_begin=" + code_begin + '}'; } } From 5d9f94e05e1527745271d0167a418741607619e2 Mon Sep 17 00:00:00 2001 From: Vladimir Kozlov Date: Sun, 5 Oct 2025 16:20:53 +0000 Subject: [PATCH 356/556] 8369152: Problem list new tests from JDK-8316694 Reviewed-by: jpai, dholmes, serb --- test/hotspot/jtreg/ProblemList.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/hotspot/jtreg/ProblemList.txt b/test/hotspot/jtreg/ProblemList.txt index d061236c957..f02ba70ba87 100644 --- a/test/hotspot/jtreg/ProblemList.txt +++ b/test/hotspot/jtreg/ProblemList.txt @@ -79,6 +79,17 @@ compiler/c2/TestVerifyConstraintCasts.java 8355574 generic-all compiler/c2/aarch64/TestStaticCallStub.java 8359963 linux-aarch64,macosx-aarch64 +compiler/whitebox/DeoptimizeRelocatedNMethod.java#G1 8369147 generic-all +compiler/whitebox/DeoptimizeRelocatedNMethod.java#Parallel 8369147 generic-all +compiler/whitebox/DeoptimizeRelocatedNMethod.java#Serial 8369147 generic-all +compiler/whitebox/DeoptimizeRelocatedNMethod.java#ZGC 8369147 generic-all +compiler/whitebox/RelocateNMethod.java#G1 8369147 generic-all +compiler/whitebox/RelocateNMethod.java#Parallel 8369147 generic-all +compiler/whitebox/RelocateNMethod.java#Serial 8369147 generic-all +compiler/whitebox/RelocateNMethod.java#ZGC 8369147 generic-all +compiler/whitebox/StressNMethodRelocation.java 8369147,8369148,8369149 generic-all +serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java 8369150,8369151 generic-all + ############################################################################# # :hotspot_gc From ba7bf43c76c94bea85dbbd865794184b7ee0cc86 Mon Sep 17 00:00:00 2001 From: Vladimir Ivanov Date: Sun, 5 Oct 2025 23:55:53 +0000 Subject: [PATCH 357/556] 8365290: [perf] x86 ArrayFill intrinsic generates SPLIT_STORE for unaligned arrays Reviewed-by: sviswanathan, vpaprotski, kvn --- src/hotspot/cpu/x86/macroAssembler_x86.cpp | 47 ++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/hotspot/cpu/x86/macroAssembler_x86.cpp b/src/hotspot/cpu/x86/macroAssembler_x86.cpp index c1319b2ef7f..77ee71c0382 100644 --- a/src/hotspot/cpu/x86/macroAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/macroAssembler_x86.cpp @@ -5847,7 +5847,7 @@ void MacroAssembler::generate_fill(BasicType t, bool aligned, orl(value, rtmp); } - cmpptr(count, 2< Date: Mon, 6 Oct 2025 06:17:48 +0000 Subject: [PATCH 358/556] 8355354: C2 crashed: assert(_callee == nullptr || _callee == m) failed: repeated inline attempt with different callee Reviewed-by: vlivanov, dlong --- src/hotspot/share/opto/callGenerator.cpp | 4 ++ src/hotspot/share/opto/callGenerator.hpp | 1 + src/hotspot/share/opto/callnode.cpp | 52 +++++++++++++----------- src/hotspot/share/opto/compile.cpp | 6 +++ src/hotspot/share/opto/compile.hpp | 3 +- 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/hotspot/share/opto/callGenerator.cpp b/src/hotspot/share/opto/callGenerator.cpp index 03225044f5e..483cb731103 100644 --- a/src/hotspot/share/opto/callGenerator.cpp +++ b/src/hotspot/share/opto/callGenerator.cpp @@ -465,6 +465,10 @@ class LateInlineVirtualCallGenerator : public VirtualCallGenerator { // Convert the CallDynamicJava into an inline virtual void do_late_inline(); + virtual ciMethod* callee_method() { + return _callee; + } + virtual void set_callee_method(ciMethod* m) { assert(_callee == nullptr || _callee == m, "repeated inline attempt with different callee"); _callee = m; diff --git a/src/hotspot/share/opto/callGenerator.hpp b/src/hotspot/share/opto/callGenerator.hpp index 82b195e0c76..e24ea5e5356 100644 --- a/src/hotspot/share/opto/callGenerator.hpp +++ b/src/hotspot/share/opto/callGenerator.hpp @@ -88,6 +88,7 @@ class CallGenerator : public ArenaObj { virtual void set_unique_id(jlong id) { fatal("unique id only for late inlines"); }; virtual jlong unique_id() const { fatal("unique id only for late inlines"); return 0; }; + virtual ciMethod* callee_method() { ShouldNotReachHere(); } virtual void set_callee_method(ciMethod* callee) { ShouldNotReachHere(); } // Note: It is possible for a CG to be both inline and virtual. diff --git a/src/hotspot/share/opto/callnode.cpp b/src/hotspot/share/opto/callnode.cpp index 995208ba24f..ef1ebc5cef9 100644 --- a/src/hotspot/share/opto/callnode.cpp +++ b/src/hotspot/share/opto/callnode.cpp @@ -1228,33 +1228,37 @@ Node* CallDynamicJavaNode::Ideal(PhaseGVN* phase, bool can_reshape) { assert(IncrementalInlineVirtual, "required"); assert(cg->call_node() == this, "mismatch"); - // Recover symbolic info for method resolution. - ciMethod* caller = jvms()->method(); - ciBytecodeStream iter(caller); - iter.force_bci(jvms()->bci()); + if (cg->callee_method() == nullptr) { + // Recover symbolic info for method resolution. + ciMethod* caller = jvms()->method(); + ciBytecodeStream iter(caller); + iter.force_bci(jvms()->bci()); - bool not_used1; - ciSignature* not_used2; - ciMethod* orig_callee = iter.get_method(not_used1, ¬_used2); // callee in the bytecode - ciKlass* holder = iter.get_declared_method_holder(); - if (orig_callee->is_method_handle_intrinsic()) { - assert(_override_symbolic_info, "required"); - orig_callee = method(); - holder = method()->holder(); + bool not_used1; + ciSignature* not_used2; + ciMethod* orig_callee = iter.get_method(not_used1, ¬_used2); // callee in the bytecode + ciKlass* holder = iter.get_declared_method_holder(); + if (orig_callee->is_method_handle_intrinsic()) { + assert(_override_symbolic_info, "required"); + orig_callee = method(); + holder = method()->holder(); + } + + ciInstanceKlass* klass = ciEnv::get_instance_klass_for_declared_method_holder(holder); + + Node* receiver_node = in(TypeFunc::Parms); + const TypeOopPtr* receiver_type = phase->type(receiver_node)->isa_oopptr(); + + int not_used3; + bool call_does_dispatch; + ciMethod* callee = phase->C->optimize_virtual_call(caller, klass, holder, orig_callee, receiver_type, true /*is_virtual*/, + call_does_dispatch, not_used3); // out-parameters + if (!call_does_dispatch) { + cg->set_callee_method(callee); + } } - - ciInstanceKlass* klass = ciEnv::get_instance_klass_for_declared_method_holder(holder); - - Node* receiver_node = in(TypeFunc::Parms); - const TypeOopPtr* receiver_type = phase->type(receiver_node)->isa_oopptr(); - - int not_used3; - bool call_does_dispatch; - ciMethod* callee = phase->C->optimize_virtual_call(caller, klass, holder, orig_callee, receiver_type, true /*is_virtual*/, - call_does_dispatch, not_used3); // out-parameters - if (!call_does_dispatch) { + if (cg->callee_method() != nullptr) { // Register for late inlining. - cg->set_callee_method(callee); register_for_late_inline(); // MH late inlining prepends to the list, so do the same } } else { diff --git a/src/hotspot/share/opto/compile.cpp b/src/hotspot/share/opto/compile.cpp index 47de5acc2f2..7f63efe9e5e 100644 --- a/src/hotspot/share/opto/compile.cpp +++ b/src/hotspot/share/opto/compile.cpp @@ -2104,6 +2104,12 @@ bool Compile::inline_incrementally_one() { bool is_scheduled_for_igvn_before = C->igvn_worklist()->member(cg->call_node()); bool does_dispatch = cg->is_virtual_late_inline() || cg->is_mh_late_inline(); if (inlining_incrementally() || does_dispatch) { // a call can be either inlined or strength-reduced to a direct call + if (should_stress_inlining()) { + // randomly add repeated inline attempt if stress-inlining + cg->call_node()->set_generator(cg); + C->igvn_worklist()->push(cg->call_node()); + continue; + } cg->do_late_inline(); assert(_late_inlines.at(i) == cg, "no insertions before current position allowed"); if (failing()) { diff --git a/src/hotspot/share/opto/compile.hpp b/src/hotspot/share/opto/compile.hpp index 05c5f22dad9..a68da644d82 100644 --- a/src/hotspot/share/opto/compile.hpp +++ b/src/hotspot/share/opto/compile.hpp @@ -1094,7 +1094,8 @@ public: bool inline_incrementally_one(); void inline_incrementally_cleanup(PhaseIterGVN& igvn); void inline_incrementally(PhaseIterGVN& igvn); - bool should_delay_inlining() { return AlwaysIncrementalInline || (StressIncrementalInlining && (random() % 2) == 0); } + bool should_stress_inlining() { return StressIncrementalInlining && (random() % 2) == 0; } + bool should_delay_inlining() { return AlwaysIncrementalInline || should_stress_inlining(); } void inline_string_calls(bool parse_time); void inline_boxing_calls(PhaseIterGVN& igvn); bool optimize_loops(PhaseIterGVN& igvn, LoopOptsMode mode); From 069c569a710f50bc715f523c6c4c7aa087694af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Sj=C3=B6len?= Date: Mon, 6 Oct 2025 07:48:45 +0000 Subject: [PATCH 359/556] 8368097: [asan] heap-buffer-overflow reported in ClassFileParser::skip_over_field_signature Reviewed-by: dholmes, mbaesken --- src/hotspot/share/classfile/classFileParser.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/hotspot/share/classfile/classFileParser.cpp b/src/hotspot/share/classfile/classFileParser.cpp index fddd9df726b..87f2da91288 100644 --- a/src/hotspot/share/classfile/classFileParser.cpp +++ b/src/hotspot/share/classfile/classFileParser.cpp @@ -4678,11 +4678,15 @@ const char* ClassFileParser::skip_over_field_signature(const char* signature, return signature + 1; case JVM_SIGNATURE_CLASS: { if (_major_version < JAVA_1_5_VERSION) { + signature++; + length--; // Skip over the class name if one is there - const char* const p = skip_over_field_name(signature + 1, true, --length); - + const char* const p = skip_over_field_name(signature, true, length); + assert(p == nullptr || p > signature, "must parse one character at least"); // The next character better be a semicolon - if (p && (p - signature) > 1 && p[0] == JVM_SIGNATURE_ENDCLASS) { + if (p != nullptr && // Parse of field name succeeded. + p - signature < static_cast(length) && // There is at least one character left to parse. + p[0] == JVM_SIGNATURE_ENDCLASS) { return p + 1; } } From e6781fd9497723a7baab38d6bfb958ba1b1c24ff Mon Sep 17 00:00:00 2001 From: Fredrik Bredberg Date: Mon, 6 Oct 2025 08:10:11 +0000 Subject: [PATCH 360/556] 8367601: Remove held_monitor_count Reviewed-by: mdoerr, pchilanomate, fyang --- .../cpu/aarch64/globalDefinitions_aarch64.hpp | 2 - .../cpu/aarch64/macroAssembler_aarch64.cpp | 32 ---------- .../cpu/aarch64/macroAssembler_aarch64.hpp | 3 - .../cpu/aarch64/sharedRuntime_aarch64.cpp | 47 --------------- src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp | 4 +- src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp | 58 +------------------ .../cpu/riscv/globalDefinitions_riscv.hpp | 4 +- .../cpu/riscv/macroAssembler_riscv.cpp | 30 ---------- .../cpu/riscv/macroAssembler_riscv.hpp | 5 +- src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp | 47 --------------- src/hotspot/cpu/x86/globalDefinitions_x86.hpp | 4 +- src/hotspot/cpu/x86/macroAssembler_x86.cpp | 8 --- src/hotspot/cpu/x86/macroAssembler_x86.hpp | 3 - src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp | 46 --------------- src/hotspot/share/jvmci/vmStructs_jvmci.cpp | 1 - src/hotspot/share/runtime/continuation.hpp | 9 ++- .../share/runtime/continuationEntry.cpp | 1 - .../share/runtime/continuationEntry.hpp | 7 --- .../share/runtime/continuationFreezeThaw.cpp | 14 +---- src/hotspot/share/runtime/javaThread.cpp | 43 -------------- src/hotspot/share/runtime/javaThread.hpp | 12 ---- src/hotspot/share/runtime/objectMonitor.cpp | 1 - src/hotspot/share/runtime/sharedRuntime.cpp | 15 ----- src/hotspot/share/runtime/sharedRuntime.hpp | 3 - src/hotspot/share/runtime/synchronizer.cpp | 8 +-- .../share/runtime/synchronizer.inline.hpp | 2 - .../classes/jdk/internal/vm/Continuation.java | 9 +-- 27 files changed, 17 insertions(+), 401 deletions(-) diff --git a/src/hotspot/cpu/aarch64/globalDefinitions_aarch64.hpp b/src/hotspot/cpu/aarch64/globalDefinitions_aarch64.hpp index 948ba97aa22..1e788590b64 100644 --- a/src/hotspot/cpu/aarch64/globalDefinitions_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/globalDefinitions_aarch64.hpp @@ -35,8 +35,6 @@ const bool CCallingConventionRequiresIntsAsLongs = false; #define SUPPORTS_NATIVE_CX8 -#define SUPPORT_MONITOR_COUNT - // Aarch64 was not originally defined to be multi-copy-atomic, but now // is. See: "Simplifying ARM Concurrency: Multicopy-atomic Axiomatic // and Operational Models for ARMv8" diff --git a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp index 3999beeec2b..2622bda1d0b 100644 --- a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp @@ -5630,38 +5630,6 @@ void MacroAssembler::tlab_allocate(Register obj, bs->tlab_allocate(this, obj, var_size_in_bytes, con_size_in_bytes, t1, t2, slow_case); } -void MacroAssembler::inc_held_monitor_count(Register tmp) { - Address dst(rthread, JavaThread::held_monitor_count_offset()); -#ifdef ASSERT - ldr(tmp, dst); - increment(tmp); - str(tmp, dst); - Label ok; - tbz(tmp, 63, ok); - STOP("assert(held monitor count underflow)"); - should_not_reach_here(); - bind(ok); -#else - increment(dst); -#endif -} - -void MacroAssembler::dec_held_monitor_count(Register tmp) { - Address dst(rthread, JavaThread::held_monitor_count_offset()); -#ifdef ASSERT - ldr(tmp, dst); - decrement(tmp); - str(tmp, dst); - Label ok; - tbz(tmp, 63, ok); - STOP("assert(held monitor count underflow)"); - should_not_reach_here(); - bind(ok); -#else - decrement(dst); -#endif -} - void MacroAssembler::verify_tlab() { #ifdef ASSERT if (UseTLAB && VerifyOops) { diff --git a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp index 0570fad5b8d..705bd19093c 100644 --- a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp @@ -983,9 +983,6 @@ public: void push_cont_fastpath(Register java_thread = rthread); void pop_cont_fastpath(Register java_thread = rthread); - void inc_held_monitor_count(Register tmp); - void dec_held_monitor_count(Register tmp); - // Round up to a power of two void round_to(Register reg, int modulus); diff --git a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp index 70af8dd91d8..39609cbe0ac 100644 --- a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp @@ -985,11 +985,8 @@ static void fill_continuation_entry(MacroAssembler* masm) { __ ldr(rscratch1, Address(rthread, JavaThread::cont_fastpath_offset())); __ str(rscratch1, Address(sp, ContinuationEntry::parent_cont_fastpath_offset())); - __ ldr(rscratch1, Address(rthread, JavaThread::held_monitor_count_offset())); - __ str(rscratch1, Address(sp, ContinuationEntry::parent_held_monitor_count_offset())); __ str(zr, Address(rthread, JavaThread::cont_fastpath_offset())); - __ str(zr, Address(rthread, JavaThread::held_monitor_count_offset())); } // on entry, sp points to the ContinuationEntry @@ -1005,50 +1002,6 @@ static void continuation_enter_cleanup(MacroAssembler* masm) { #endif __ ldr(rscratch1, Address(sp, ContinuationEntry::parent_cont_fastpath_offset())); __ str(rscratch1, Address(rthread, JavaThread::cont_fastpath_offset())); - - if (CheckJNICalls) { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ ldrw(rscratch1, Address(sp, ContinuationEntry::flags_offset())); - __ cbzw(rscratch1, L_skip_vthread_code); - - // If the held monitor count is > 0 and this vthread is terminating then - // it failed to release a JNI monitor. So we issue the same log message - // that JavaThread::exit does. - __ ldr(rscratch1, Address(rthread, JavaThread::jni_monitor_count_offset())); - __ cbz(rscratch1, L_skip_vthread_code); - - // Save return value potentially containing the exception oop in callee-saved R19. - __ mov(r19, r0); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::log_jni_monitor_still_held)); - // Restore potential return value. - __ mov(r0, r19); - - // For vthreads we have to explicitly zero the JNI monitor count of the carrier - // on termination. The held count is implicitly zeroed below when we restore from - // the parent held count (which has to be zero). - __ str(zr, Address(rthread, JavaThread::jni_monitor_count_offset())); - - __ bind(L_skip_vthread_code); - } -#ifdef ASSERT - else { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ ldrw(rscratch1, Address(sp, ContinuationEntry::flags_offset())); - __ cbzw(rscratch1, L_skip_vthread_code); - - // See comment just above. If not checking JNI calls the JNI count is only - // needed for assertion checking. - __ str(zr, Address(rthread, JavaThread::jni_monitor_count_offset())); - - __ bind(L_skip_vthread_code); - } -#endif - - __ ldr(rscratch1, Address(sp, ContinuationEntry::parent_held_monitor_count_offset())); - __ str(rscratch1, Address(rthread, JavaThread::held_monitor_count_offset())); - __ ldr(rscratch2, Address(sp, ContinuationEntry::parent_offset())); __ str(rscratch2, Address(rthread, JavaThread::cont_entry_offset())); __ add(rfp, sp, (int)ContinuationEntry::size()); diff --git a/src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp b/src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp index f8f15741301..6c41e56b20b 100644 --- a/src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp +++ b/src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2012, 2016 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -43,8 +43,6 @@ const bool CCallingConventionRequiresIntsAsLongs = true; #define SUPPORTS_NATIVE_CX8 -#define SUPPORT_MONITOR_COUNT - // PPC64 is not specified as multi-copy-atomic // So we must not #define CPU_MULTI_COPY_ATOMIC diff --git a/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp b/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp index aec36b3f3f9..9fe7e1f22ff 100644 --- a/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp +++ b/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp @@ -1639,7 +1639,6 @@ static void fill_continuation_entry(MacroAssembler* masm, Register reg_cont_obj, assert_different_registers(reg_cont_obj, reg_flags); Register zero = R8_ARG6; Register tmp2 = R9_ARG7; - Register tmp3 = R10_ARG8; DEBUG_ONLY(__ block_comment("fill {")); #ifdef ASSERT @@ -1655,12 +1654,9 @@ static void fill_continuation_entry(MacroAssembler* masm, Register reg_cont_obj, __ stw(zero, in_bytes(ContinuationEntry::pin_count_offset()), R1_SP); __ ld_ptr(tmp2, JavaThread::cont_fastpath_offset(), R16_thread); - __ ld(tmp3, in_bytes(JavaThread::held_monitor_count_offset()), R16_thread); __ st_ptr(tmp2, ContinuationEntry::parent_cont_fastpath_offset(), R1_SP); - __ std(tmp3, in_bytes(ContinuationEntry::parent_held_monitor_count_offset()), R1_SP); __ st_ptr(zero, JavaThread::cont_fastpath_offset(), R16_thread); - __ std(zero, in_bytes(JavaThread::held_monitor_count_offset()), R16_thread); DEBUG_ONLY(__ block_comment("} fill")); } @@ -1681,7 +1677,6 @@ static void fill_continuation_entry(MacroAssembler* masm, Register reg_cont_obj, static void continuation_enter_cleanup(MacroAssembler* masm) { Register tmp1 = R8_ARG6; Register tmp2 = R9_ARG7; - Register tmp3 = R10_ARG8; #ifdef ASSERT __ block_comment("clean {"); @@ -1692,57 +1687,8 @@ static void continuation_enter_cleanup(MacroAssembler* masm) { __ ld_ptr(tmp1, ContinuationEntry::parent_cont_fastpath_offset(), R1_SP); __ st_ptr(tmp1, JavaThread::cont_fastpath_offset(), R16_thread); - - if (CheckJNICalls) { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ lwz(R0, in_bytes(ContinuationEntry::flags_offset()), R1_SP); - __ cmpwi(CR0, R0, 0); - __ beq(CR0, L_skip_vthread_code); - - // If the held monitor count is > 0 and this vthread is terminating then - // it failed to release a JNI monitor. So we issue the same log message - // that JavaThread::exit does. - __ ld(R0, in_bytes(JavaThread::jni_monitor_count_offset()), R16_thread); - __ cmpdi(CR0, R0, 0); - __ beq(CR0, L_skip_vthread_code); - - // Save return value potentially containing the exception oop - Register ex_oop = R15_esp; // nonvolatile register - __ mr(ex_oop, R3_RET); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::log_jni_monitor_still_held)); - // Restore potental return value - __ mr(R3_RET, ex_oop); - - // For vthreads we have to explicitly zero the JNI monitor count of the carrier - // on termination. The held count is implicitly zeroed below when we restore from - // the parent held count (which has to be zero). - __ li(tmp1, 0); - __ std(tmp1, in_bytes(JavaThread::jni_monitor_count_offset()), R16_thread); - - __ bind(L_skip_vthread_code); - } -#ifdef ASSERT - else { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ lwz(R0, in_bytes(ContinuationEntry::flags_offset()), R1_SP); - __ cmpwi(CR0, R0, 0); - __ beq(CR0, L_skip_vthread_code); - - // See comment just above. If not checking JNI calls the JNI count is only - // needed for assertion checking. - __ li(tmp1, 0); - __ std(tmp1, in_bytes(JavaThread::jni_monitor_count_offset()), R16_thread); - - __ bind(L_skip_vthread_code); - } -#endif - - __ ld(tmp2, in_bytes(ContinuationEntry::parent_held_monitor_count_offset()), R1_SP); - __ ld_ptr(tmp3, ContinuationEntry::parent_offset(), R1_SP); - __ std(tmp2, in_bytes(JavaThread::held_monitor_count_offset()), R16_thread); - __ st_ptr(tmp3, JavaThread::cont_entry_offset(), R16_thread); + __ ld_ptr(tmp2, ContinuationEntry::parent_offset(), R1_SP); + __ st_ptr(tmp2, JavaThread::cont_entry_offset(), R16_thread); DEBUG_ONLY(__ block_comment("} clean")); } diff --git a/src/hotspot/cpu/riscv/globalDefinitions_riscv.hpp b/src/hotspot/cpu/riscv/globalDefinitions_riscv.hpp index 407017ee1c0..57223cf4390 100644 --- a/src/hotspot/cpu/riscv/globalDefinitions_riscv.hpp +++ b/src/hotspot/cpu/riscv/globalDefinitions_riscv.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, 2015, Red Hat Inc. All rights reserved. * Copyright (c) 2020, 2022, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -44,8 +44,6 @@ const bool CCallingConventionRequiresIntsAsLongs = false; #define SUPPORTS_NATIVE_CX8 -#define SUPPORT_MONITOR_COUNT - #define SUPPORT_RESERVED_STACK_AREA #define USE_POINTERS_TO_REGISTER_IMPL_ARRAY diff --git a/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp b/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp index 5c85cc13bed..115b90a0087 100644 --- a/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp +++ b/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp @@ -225,36 +225,6 @@ void MacroAssembler::pop_cont_fastpath(Register java_thread) { bind(done); } -void MacroAssembler::inc_held_monitor_count(Register tmp) { - Address dst(xthread, JavaThread::held_monitor_count_offset()); - ld(tmp, dst); - addi(tmp, tmp, 1); - sd(tmp, dst); -#ifdef ASSERT - Label ok; - test_bit(tmp, tmp, 63); - beqz(tmp, ok); - STOP("assert(held monitor count overflow)"); - should_not_reach_here(); - bind(ok); -#endif -} - -void MacroAssembler::dec_held_monitor_count(Register tmp) { - Address dst(xthread, JavaThread::held_monitor_count_offset()); - ld(tmp, dst); - subi(tmp, tmp, 1); - sd(tmp, dst); -#ifdef ASSERT - Label ok; - test_bit(tmp, tmp, 63); - beqz(tmp, ok); - STOP("assert(held monitor count underflow)"); - should_not_reach_here(); - bind(ok); -#endif -} - int MacroAssembler::align(int modulus, int extra_offset) { CompressibleScope scope(this); intptr_t before = offset(); diff --git a/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp b/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp index 13b70d5dbd7..9e713e90270 100644 --- a/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp +++ b/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, 2020, Red Hat Inc. All rights reserved. * Copyright (c) 2020, 2024, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -849,9 +849,6 @@ public: void push_cont_fastpath(Register java_thread = xthread); void pop_cont_fastpath(Register java_thread = xthread); - void inc_held_monitor_count(Register tmp); - void dec_held_monitor_count(Register tmp); - // if heap base register is used - reinit it with the correct value void reinit_heapbase(); diff --git a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp index 94506e9f19d..b303178a666 100644 --- a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp +++ b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp @@ -885,11 +885,8 @@ static void fill_continuation_entry(MacroAssembler* masm) { __ ld(t0, Address(xthread, JavaThread::cont_fastpath_offset())); __ sd(t0, Address(sp, ContinuationEntry::parent_cont_fastpath_offset())); - __ ld(t0, Address(xthread, JavaThread::held_monitor_count_offset())); - __ sd(t0, Address(sp, ContinuationEntry::parent_held_monitor_count_offset())); __ sd(zr, Address(xthread, JavaThread::cont_fastpath_offset())); - __ sd(zr, Address(xthread, JavaThread::held_monitor_count_offset())); } // on entry, sp points to the ContinuationEntry @@ -905,50 +902,6 @@ static void continuation_enter_cleanup(MacroAssembler* masm) { __ ld(t0, Address(sp, ContinuationEntry::parent_cont_fastpath_offset())); __ sd(t0, Address(xthread, JavaThread::cont_fastpath_offset())); - - if (CheckJNICalls) { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ lwu(t0, Address(sp, ContinuationEntry::flags_offset())); - __ beqz(t0, L_skip_vthread_code); - - // If the held monitor count is > 0 and this vthread is terminating then - // it failed to release a JNI monitor. So we issue the same log message - // that JavaThread::exit does. - __ ld(t0, Address(xthread, JavaThread::jni_monitor_count_offset())); - __ beqz(t0, L_skip_vthread_code); - - // Save return value potentially containing the exception oop in callee-saved x9 - __ mv(x9, x10); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::log_jni_monitor_still_held)); - // Restore potential return value - __ mv(x10, x9); - - // For vthreads we have to explicitly zero the JNI monitor count of the carrier - // on termination. The held count is implicitly zeroed below when we restore from - // the parent held count (which has to be zero). - __ sd(zr, Address(xthread, JavaThread::jni_monitor_count_offset())); - - __ bind(L_skip_vthread_code); - } -#ifdef ASSERT - else { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ lwu(t0, Address(sp, ContinuationEntry::flags_offset())); - __ beqz(t0, L_skip_vthread_code); - - // See comment just above. If not checking JNI calls the JNI count is only - // needed for assertion checking. - __ sd(zr, Address(xthread, JavaThread::jni_monitor_count_offset())); - - __ bind(L_skip_vthread_code); - } -#endif - - __ ld(t0, Address(sp, ContinuationEntry::parent_held_monitor_count_offset())); - __ sd(t0, Address(xthread, JavaThread::held_monitor_count_offset())); - __ ld(t0, Address(sp, ContinuationEntry::parent_offset())); __ sd(t0, Address(xthread, JavaThread::cont_entry_offset())); __ add(fp, sp, (int)ContinuationEntry::size() + 2 * wordSize /* 2 extra words to match up with leave() */); diff --git a/src/hotspot/cpu/x86/globalDefinitions_x86.hpp b/src/hotspot/cpu/x86/globalDefinitions_x86.hpp index 3c1474ae861..abbeb66a1ca 100644 --- a/src/hotspot/cpu/x86/globalDefinitions_x86.hpp +++ b/src/hotspot/cpu/x86/globalDefinitions_x86.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,8 +34,6 @@ const bool CCallingConventionRequiresIntsAsLongs = false; #define SUPPORTS_NATIVE_CX8 -#define SUPPORT_MONITOR_COUNT - #define CPU_MULTI_COPY_ATOMIC // The expected size in bytes of a cache line. diff --git a/src/hotspot/cpu/x86/macroAssembler_x86.cpp b/src/hotspot/cpu/x86/macroAssembler_x86.cpp index 77ee71c0382..4f19b30b832 100644 --- a/src/hotspot/cpu/x86/macroAssembler_x86.cpp +++ b/src/hotspot/cpu/x86/macroAssembler_x86.cpp @@ -2431,14 +2431,6 @@ void MacroAssembler::pop_cont_fastpath() { bind(L_done); } -void MacroAssembler::inc_held_monitor_count() { - incrementq(Address(r15_thread, JavaThread::held_monitor_count_offset())); -} - -void MacroAssembler::dec_held_monitor_count() { - decrementq(Address(r15_thread, JavaThread::held_monitor_count_offset())); -} - #ifdef ASSERT void MacroAssembler::stop_if_in_cont(Register cont, const char* name) { Label no_cont; diff --git a/src/hotspot/cpu/x86/macroAssembler_x86.hpp b/src/hotspot/cpu/x86/macroAssembler_x86.hpp index 1c0dbaaefbe..ed1343d9c8c 100644 --- a/src/hotspot/cpu/x86/macroAssembler_x86.hpp +++ b/src/hotspot/cpu/x86/macroAssembler_x86.hpp @@ -472,9 +472,6 @@ class MacroAssembler: public Assembler { void push_cont_fastpath(); void pop_cont_fastpath(); - void inc_held_monitor_count(); - void dec_held_monitor_count(); - DEBUG_ONLY(void stop_if_in_cont(Register cont_reg, const char* name);) // Round up to a power of two diff --git a/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp b/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp index e2a8f36b050..e702b587edd 100644 --- a/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp +++ b/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp @@ -1352,11 +1352,8 @@ static void fill_continuation_entry(MacroAssembler* masm, Register reg_cont_obj, __ movptr(rax, Address(r15_thread, JavaThread::cont_fastpath_offset())); __ movptr(Address(rsp, ContinuationEntry::parent_cont_fastpath_offset()), rax); - __ movq(rax, Address(r15_thread, JavaThread::held_monitor_count_offset())); - __ movq(Address(rsp, ContinuationEntry::parent_held_monitor_count_offset()), rax); __ movptr(Address(r15_thread, JavaThread::cont_fastpath_offset()), 0); - __ movq(Address(r15_thread, JavaThread::held_monitor_count_offset()), 0); } //---------------------------- continuation_enter_cleanup --------------------------- @@ -1380,49 +1377,6 @@ static void continuation_enter_cleanup(MacroAssembler* masm) { #endif __ movptr(rbx, Address(rsp, ContinuationEntry::parent_cont_fastpath_offset())); __ movptr(Address(r15_thread, JavaThread::cont_fastpath_offset()), rbx); - - if (CheckJNICalls) { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ cmpl(Address(rsp, ContinuationEntry::flags_offset()), 0); - __ jcc(Assembler::equal, L_skip_vthread_code); - - // If the held monitor count is > 0 and this vthread is terminating then - // it failed to release a JNI monitor. So we issue the same log message - // that JavaThread::exit does. - __ cmpptr(Address(r15_thread, JavaThread::jni_monitor_count_offset()), 0); - __ jcc(Assembler::equal, L_skip_vthread_code); - - // rax may hold an exception oop, save it before the call - __ push(rax); - __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::log_jni_monitor_still_held)); - __ pop(rax); - - // For vthreads we have to explicitly zero the JNI monitor count of the carrier - // on termination. The held count is implicitly zeroed below when we restore from - // the parent held count (which has to be zero). - __ movq(Address(r15_thread, JavaThread::jni_monitor_count_offset()), 0); - - __ bind(L_skip_vthread_code); - } -#ifdef ASSERT - else { - // Check if this is a virtual thread continuation - Label L_skip_vthread_code; - __ cmpl(Address(rsp, ContinuationEntry::flags_offset()), 0); - __ jcc(Assembler::equal, L_skip_vthread_code); - - // See comment just above. If not checking JNI calls the JNI count is only - // needed for assertion checking. - __ movq(Address(r15_thread, JavaThread::jni_monitor_count_offset()), 0); - - __ bind(L_skip_vthread_code); - } -#endif - - __ movq(rbx, Address(rsp, ContinuationEntry::parent_held_monitor_count_offset())); - __ movq(Address(r15_thread, JavaThread::held_monitor_count_offset()), rbx); - __ movptr(rbx, Address(rsp, ContinuationEntry::parent_offset())); __ movptr(Address(r15_thread, JavaThread::cont_entry_offset()), rbx); __ addptr(rsp, checked_cast(ContinuationEntry::size())); diff --git a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp index b4f033a4f9a..7ef16f6e32c 100644 --- a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp +++ b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp @@ -255,7 +255,6 @@ nonstatic_field(JavaThread, _should_post_on_exceptions_flag, int) \ nonstatic_field(JavaThread, _jni_environment, JNIEnv) \ nonstatic_field(JavaThread, _stack_overflow_state._reserved_stack_activation, address) \ - nonstatic_field(JavaThread, _held_monitor_count, intx) \ nonstatic_field(JavaThread, _lock_stack, LockStack) \ nonstatic_field(JavaThread, _om_cache, OMCache) \ nonstatic_field(JavaThread, _cont_entry, ContinuationEntry*) \ diff --git a/src/hotspot/share/runtime/continuation.hpp b/src/hotspot/share/runtime/continuation.hpp index e678e0bd42b..0cfd484361d 100644 --- a/src/hotspot/share/runtime/continuation.hpp +++ b/src/hotspot/share/runtime/continuation.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -53,10 +53,9 @@ enum freeze_result { freeze_ok_bottom = 1, freeze_pinned_cs = 2, freeze_pinned_native = 3, - freeze_pinned_monitor = 4, - freeze_exception = 5, - freeze_not_mounted = 6, - freeze_unsupported = 7 + freeze_exception = 4, + freeze_not_mounted = 5, + freeze_unsupported = 6 }; class Continuation : AllStatic { diff --git a/src/hotspot/share/runtime/continuationEntry.cpp b/src/hotspot/share/runtime/continuationEntry.cpp index 4551bfa7cc8..69a20808798 100644 --- a/src/hotspot/share/runtime/continuationEntry.cpp +++ b/src/hotspot/share/runtime/continuationEntry.cpp @@ -107,7 +107,6 @@ void ContinuationEntry::describe(FrameValues& values, int frame_no) const { values.describe(frame_no, (intptr_t*)(usp + in_bytes(ContinuationEntry::argsize_offset())), "argsize"); values.describe(frame_no, (intptr_t*)(usp + in_bytes(ContinuationEntry::pin_count_offset())), "pin_count"); values.describe(frame_no, (intptr_t*)(usp + in_bytes(ContinuationEntry::parent_cont_fastpath_offset())), "parent fastpath"); - values.describe(frame_no, (intptr_t*)(usp + in_bytes(ContinuationEntry::parent_held_monitor_count_offset())), "parent held monitor count"); } #endif diff --git a/src/hotspot/share/runtime/continuationEntry.hpp b/src/hotspot/share/runtime/continuationEntry.hpp index 3c8532b9e87..8361f2f912b 100644 --- a/src/hotspot/share/runtime/continuationEntry.hpp +++ b/src/hotspot/share/runtime/continuationEntry.hpp @@ -79,11 +79,6 @@ class ContinuationEntry { // The caller (if there is one) is the still frozen top frame in the StackChunk. int _argsize; intptr_t* _parent_cont_fastpath; -#ifdef _LP64 - int64_t _parent_held_monitor_count; -#else - int32_t _parent_held_monitor_count; -#endif uint32_t _pin_count; public: @@ -94,7 +89,6 @@ class ContinuationEntry { static ByteSize argsize_offset() { return byte_offset_of(ContinuationEntry, _argsize); } static ByteSize pin_count_offset(){ return byte_offset_of(ContinuationEntry, _pin_count); } static ByteSize parent_cont_fastpath_offset() { return byte_offset_of(ContinuationEntry, _parent_cont_fastpath); } - static ByteSize parent_held_monitor_count_offset() { return byte_offset_of(ContinuationEntry, _parent_held_monitor_count); } static address return_pc() { return _return_pc; } static address return_pc_address() { return (address)&_return_pc; } @@ -103,7 +97,6 @@ class ContinuationEntry { static size_t size() { return align_up((int)sizeof(ContinuationEntry), 2*wordSize); } ContinuationEntry* parent() const { return _parent; } - int64_t parent_held_monitor_count() const { return (int64_t)_parent_held_monitor_count; } static address entry_pc() { return _return_pc; } intptr_t* entry_sp() const { return (intptr_t*)this; } diff --git a/src/hotspot/share/runtime/continuationFreezeThaw.cpp b/src/hotspot/share/runtime/continuationFreezeThaw.cpp index 024b69c765f..33b4f2bf488 100644 --- a/src/hotspot/share/runtime/continuationFreezeThaw.cpp +++ b/src/hotspot/share/runtime/continuationFreezeThaw.cpp @@ -1736,13 +1736,10 @@ static inline freeze_result freeze_internal(JavaThread* current, intptr_t* const assert(entry->is_virtual_thread() == (entry->scope(current) == java_lang_VirtualThread::vthread_scope()), ""); - assert((current->held_monitor_count() == 0 && current->jni_monitor_count() == 0), - "Held monitor count should not be used for lightweight locking: " INT64_FORMAT " JNI: " INT64_FORMAT, (int64_t)current->held_monitor_count(), (int64_t)current->jni_monitor_count()); - - if (entry->is_pinned() || current->held_monitor_count() > 0) { - log_develop_debug(continuations)("PINNED due to critical section/hold monitor"); + if (entry->is_pinned()) { + log_develop_debug(continuations)("PINNED due to critical section"); verify_continuation(cont.continuation()); - freeze_result res = entry->is_pinned() ? freeze_pinned_cs : freeze_pinned_monitor; + const freeze_result res = freeze_pinned_cs; if (!preempt) { JFR_ONLY(current->set_last_freeze_fail_result(res);) } @@ -1799,8 +1796,6 @@ static freeze_result is_pinned0(JavaThread* thread, oop cont_scope, bool safepoi } if (entry->is_pinned()) { return freeze_pinned_cs; - } else if (thread->held_monitor_count() > 0) { - return freeze_pinned_monitor; } RegisterMap map(thread, @@ -1836,15 +1831,12 @@ static freeze_result is_pinned0(JavaThread* thread, oop cont_scope, bool safepoi if (scope == cont_scope) { break; } - intx monitor_count = entry->parent_held_monitor_count(); entry = entry->parent(); if (entry == nullptr) { break; } if (entry->is_pinned()) { return freeze_pinned_cs; - } else if (monitor_count > 0) { - return freeze_pinned_monitor; } } } diff --git a/src/hotspot/share/runtime/javaThread.cpp b/src/hotspot/share/runtime/javaThread.cpp index e5af8d7bedd..8bb8095878f 100644 --- a/src/hotspot/share/runtime/javaThread.cpp +++ b/src/hotspot/share/runtime/javaThread.cpp @@ -488,8 +488,6 @@ JavaThread::JavaThread(MemTag mem_tag) : _cont_entry(nullptr), _cont_fastpath(nullptr), _cont_fastpath_thread_state(1), - _held_monitor_count(0), - _jni_monitor_count(0), _unlocked_inflated_monitor(nullptr), _preempt_alternate_return(nullptr), @@ -927,27 +925,6 @@ void JavaThread::exit(bool destroy_vm, ExitType exit_type) { "should not have a Java frame when detaching or exiting"); ObjectSynchronizer::release_monitors_owned_by_thread(this); assert(!this->has_pending_exception(), "release_monitors should have cleared"); - // Check for monitor counts being out of sync. - assert(held_monitor_count() == jni_monitor_count(), - "held monitor count should be equal to jni: %zd != %zd", - held_monitor_count(), jni_monitor_count()); - // All in-use monitors, including JNI-locked ones, should have been released above. - assert(held_monitor_count() == 0, "Failed to unlock %zd object monitors", - held_monitor_count()); - } else { - // Check for monitor counts being out of sync. - assert(held_monitor_count() == jni_monitor_count(), - "held monitor count should be equal to jni: %zd != %zd", - held_monitor_count(), jni_monitor_count()); - // It is possible that a terminating thread failed to unlock monitors it locked - // via JNI so we don't assert the count is zero. - } - - if (CheckJNICalls && jni_monitor_count() > 0) { - // We would like a fatal here, but due to we never checked this before there - // is a lot of tests which breaks, even with an error log. - log_debug(jni)("JavaThread %s (tid: %zu) with Objects still locked by JNI MonitorEnter.", - exit_type == JavaThread::normal_exit ? "exiting" : "detaching", os::current_thread_id()); } // These things needs to be done while we are still a Java Thread. Make sure that thread @@ -1988,26 +1965,6 @@ void JavaThread::trace_stack() { #endif // PRODUCT -// Slow-path increment of the held monitor counts. JNI locking is always -// this slow-path. -void JavaThread::inc_held_monitor_count(intx i, bool jni) { -#ifdef SUPPORT_MONITOR_COUNT - // Nothing to do. Just do some sanity check. - assert(_held_monitor_count == 0, "counter should not be used"); - assert(_jni_monitor_count == 0, "counter should not be used"); -#endif // SUPPORT_MONITOR_COUNT -} - -// Slow-path decrement of the held monitor counts. JNI unlocking is always -// this slow-path. -void JavaThread::dec_held_monitor_count(intx i, bool jni) { -#ifdef SUPPORT_MONITOR_COUNT - // Nothing to do. Just do some sanity check. - assert(_held_monitor_count == 0, "counter should not be used"); - assert(_jni_monitor_count == 0, "counter should not be used"); -#endif // SUPPORT_MONITOR_COUNT -} - frame JavaThread::vthread_last_frame() { assert (is_vthread_mounted(), "Virtual thread not mounted"); return last_frame(); diff --git a/src/hotspot/share/runtime/javaThread.hpp b/src/hotspot/share/runtime/javaThread.hpp index 89c3191669a..c8be1594a69 100644 --- a/src/hotspot/share/runtime/javaThread.hpp +++ b/src/hotspot/share/runtime/javaThread.hpp @@ -476,9 +476,6 @@ class JavaThread: public Thread { // frame inside the continuation that we know about int _cont_fastpath_thread_state; // whether global thread state allows continuation fastpath (JVMTI) - // It's signed for error detection. - intx _held_monitor_count; // used by continuations for fast lock detection - intx _jni_monitor_count; ObjectMonitor* _unlocked_inflated_monitor; // This is the field we poke in the interpreter and native @@ -662,13 +659,6 @@ private: bool cont_fastpath() const { return _cont_fastpath == nullptr && _cont_fastpath_thread_state != 0; } bool cont_fastpath_thread_state() const { return _cont_fastpath_thread_state != 0; } - void inc_held_monitor_count(intx i = 1, bool jni = false); - void dec_held_monitor_count(intx i = 1, bool jni = false); - - intx held_monitor_count() { return _held_monitor_count; } - intx jni_monitor_count() { return _jni_monitor_count; } - void clear_jni_monitor_count() { _jni_monitor_count = 0; } - // Support for SharedRuntime::monitor_exit_helper() ObjectMonitor* unlocked_inflated_monitor() const { return _unlocked_inflated_monitor; } void clear_unlocked_inflated_monitor() { @@ -897,8 +887,6 @@ public: static ByteSize cont_entry_offset() { return byte_offset_of(JavaThread, _cont_entry); } static ByteSize cont_fastpath_offset() { return byte_offset_of(JavaThread, _cont_fastpath); } - static ByteSize held_monitor_count_offset() { return byte_offset_of(JavaThread, _held_monitor_count); } - static ByteSize jni_monitor_count_offset() { return byte_offset_of(JavaThread, _jni_monitor_count); } static ByteSize preemption_cancelled_offset() { return byte_offset_of(JavaThread, _preemption_cancelled); } static ByteSize preempt_alternate_return_offset() { return byte_offset_of(JavaThread, _preempt_alternate_return); } static ByteSize unlocked_inflated_monitor_offset() { return byte_offset_of(JavaThread, _unlocked_inflated_monitor); } diff --git a/src/hotspot/share/runtime/objectMonitor.cpp b/src/hotspot/share/runtime/objectMonitor.cpp index 8859f6e7f5f..4c00ecac697 100644 --- a/src/hotspot/share/runtime/objectMonitor.cpp +++ b/src/hotspot/share/runtime/objectMonitor.cpp @@ -1952,7 +1952,6 @@ void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { int relock_count = JvmtiDeferredUpdates::get_and_reset_relock_count_after_wait(current); _recursions = save // restore the old recursion count + relock_count; // increased by the deferred relock count - current->inc_held_monitor_count(relock_count); // Deopt never entered these counts. _waiters--; // decrement the number of waiters // Verify a few postconditions diff --git a/src/hotspot/share/runtime/sharedRuntime.cpp b/src/hotspot/share/runtime/sharedRuntime.cpp index cf949fe1e7c..85aeba361ff 100644 --- a/src/hotspot/share/runtime/sharedRuntime.cpp +++ b/src/hotspot/share/runtime/sharedRuntime.cpp @@ -1980,7 +1980,6 @@ void SharedRuntime::monitor_exit_helper(oopDesc* obj, BasicLock* lock, JavaThrea if (!m->try_enter(current, /*check_for_recursion*/ false)) { // Some other thread acquired the lock (or the monitor was // deflated). Either way we are done. - current->dec_held_monitor_count(); return; } } @@ -2002,20 +2001,6 @@ JRT_LEAF(void, SharedRuntime::complete_monitor_unlocking_C(oopDesc* obj, BasicLo SharedRuntime::monitor_exit_helper(obj, lock, current); JRT_END -// This is only called when CheckJNICalls is true, and only -// for virtual thread termination. -JRT_LEAF(void, SharedRuntime::log_jni_monitor_still_held()) - assert(CheckJNICalls, "Only call this when checking JNI usage"); - if (log_is_enabled(Debug, jni)) { - JavaThread* current = JavaThread::current(); - int64_t vthread_id = java_lang_Thread::thread_id(current->vthread()); - int64_t carrier_id = java_lang_Thread::thread_id(current->threadObj()); - log_debug(jni)("VirtualThread (tid: " INT64_FORMAT ", carrier id: " INT64_FORMAT - ") exiting with Objects still locked by JNI MonitorEnter.", - vthread_id, carrier_id); - } -JRT_END - #ifndef PRODUCT void SharedRuntime::print_statistics() { diff --git a/src/hotspot/share/runtime/sharedRuntime.hpp b/src/hotspot/share/runtime/sharedRuntime.hpp index 6544e380d99..2a19b80c3b5 100644 --- a/src/hotspot/share/runtime/sharedRuntime.hpp +++ b/src/hotspot/share/runtime/sharedRuntime.hpp @@ -404,9 +404,6 @@ class SharedRuntime: AllStatic { static void monitor_exit_helper(oopDesc* obj, BasicLock* lock, JavaThread* current); - // Issue UL warning for unlocked JNI monitor on virtual thread termination - static void log_jni_monitor_still_held(); - private: static Handle find_callee_info(Bytecodes::Code& bc, CallInfo& callinfo, TRAPS); static Handle find_callee_info_helper(vframeStream& vfst, Bytecodes::Code& bc, CallInfo& callinfo, TRAPS); diff --git a/src/hotspot/share/runtime/synchronizer.cpp b/src/hotspot/share/runtime/synchronizer.cpp index ff4e09e741f..e513c57fe06 100644 --- a/src/hotspot/share/runtime/synchronizer.cpp +++ b/src/hotspot/share/runtime/synchronizer.cpp @@ -452,7 +452,6 @@ void ObjectSynchronizer::jni_enter(Handle obj, JavaThread* current) { while (true) { BasicLock lock; if (LightweightSynchronizer::inflate_and_enter(obj(), &lock, inflate_cause_jni_enter, current, current) != nullptr) { - current->inc_held_monitor_count(1, true); break; } } @@ -470,7 +469,6 @@ void ObjectSynchronizer::jni_exit(oop obj, TRAPS) { // monitor even if an exception was already pending. if (monitor->check_owner(THREAD)) { monitor->exit(current); - current->dec_held_monitor_count(1, true); } } @@ -1263,8 +1261,7 @@ class ReleaseJavaMonitorsClosure: public MonitorClosure { public: ReleaseJavaMonitorsClosure(JavaThread* thread) : _thread(thread) {} void do_monitor(ObjectMonitor* mid) { - intx rec = mid->complete_exit(_thread); - _thread->dec_held_monitor_count(rec + 1); + mid->complete_exit(_thread); } }; @@ -1290,9 +1287,6 @@ void ObjectSynchronizer::release_monitors_owned_by_thread(JavaThread* current) { ObjectSynchronizer::owned_monitors_iterate(&rjmc, current); assert(!current->has_pending_exception(), "Should not be possible"); current->clear_pending_exception(); - assert(current->held_monitor_count() == 0, "Should not be possible"); - // All monitors (including entered via JNI) have been unlocked above, so we need to clear jni count. - current->clear_jni_monitor_count(); } const char* ObjectSynchronizer::inflate_cause_name(const InflateCause cause) { diff --git a/src/hotspot/share/runtime/synchronizer.inline.hpp b/src/hotspot/share/runtime/synchronizer.inline.hpp index 6a850e5c8ca..cdbeb1daf5b 100644 --- a/src/hotspot/share/runtime/synchronizer.inline.hpp +++ b/src/hotspot/share/runtime/synchronizer.inline.hpp @@ -61,8 +61,6 @@ inline bool ObjectSynchronizer::quick_enter(oop obj, BasicLock* lock, JavaThread } inline void ObjectSynchronizer::exit(oop object, BasicLock* lock, JavaThread* current) { - current->dec_held_monitor_count(); - LightweightSynchronizer::exit(object, lock, current); } diff --git a/src/java.base/share/classes/jdk/internal/vm/Continuation.java b/src/java.base/share/classes/jdk/internal/vm/Continuation.java index a97f9ac9ea4..a7eb3ea6a9f 100644 --- a/src/java.base/share/classes/jdk/internal/vm/Continuation.java +++ b/src/java.base/share/classes/jdk/internal/vm/Continuation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -57,7 +57,6 @@ public class Continuation { /** Reason for pinning */ public enum Pinned { /** Native frame on stack */ NATIVE, - /** Monitor held */ MONITOR, /** In critical section */ CRITICAL_SECTION, /** Exception (OOME/SOE) */ EXCEPTION } @@ -69,8 +68,7 @@ public class Continuation { /** Permanent failure: continuation already yielding */ PERM_FAIL_YIELDING(null), /** Permanent failure: continuation not mounted on the thread */ PERM_FAIL_NOT_MOUNTED(null), /** Transient failure: continuation pinned due to a held CS */ TRANSIENT_FAIL_PINNED_CRITICAL_SECTION(Pinned.CRITICAL_SECTION), - /** Transient failure: continuation pinned due to native frame */ TRANSIENT_FAIL_PINNED_NATIVE(Pinned.NATIVE), - /** Transient failure: continuation pinned due to a held monitor */ TRANSIENT_FAIL_PINNED_MONITOR(Pinned.MONITOR); + /** Transient failure: continuation pinned due to native frame */ TRANSIENT_FAIL_PINNED_NATIVE(Pinned.NATIVE); final Pinned pinned; private PreemptStatus(Pinned reason) { this.pinned = reason; } @@ -85,8 +83,7 @@ public class Continuation { return switch (reason) { case 2 -> Pinned.CRITICAL_SECTION; case 3 -> Pinned.NATIVE; - case 4 -> Pinned.MONITOR; - case 5 -> Pinned.EXCEPTION; + case 4 -> Pinned.EXCEPTION; default -> throw new AssertionError("Unknown pinned reason: " + reason); }; } From 59e87437b4f9259121710dca5e595ca714c3e71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Casta=C3=B1eda=20Lozano?= Date: Mon, 6 Oct 2025 08:14:24 +0000 Subject: [PATCH 361/556] 8368753: IGV: improve CFG view of difference graphs Reviewed-by: chagedorn, mhaessig, dfenacci --- .../com/sun/hotspot/igv/data/InputBlock.java | 2 +- .../hotspot/igv/data/services/Scheduler.java | 10 ++-- .../hotspot/igv/difference/Difference.java | 9 ++++ .../ServerCompilerScheduler.java | 54 +++++++++++++------ 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputBlock.java b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputBlock.java index 6670470b5e8..a5b282f9de1 100644 --- a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputBlock.java +++ b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/InputBlock.java @@ -135,7 +135,7 @@ public class InputBlock { successors.add(b); } - void setArtificial() { + public void setArtificial() { this.artificial = true; } diff --git a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/services/Scheduler.java b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/services/Scheduler.java index 1fe6ac3342a..4ff960f7c3d 100644 --- a/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/services/Scheduler.java +++ b/src/utils/IdealGraphVisualizer/Data/src/main/java/com/sun/hotspot/igv/data/services/Scheduler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,9 +24,7 @@ */ package com.sun.hotspot.igv.data.services; -import com.sun.hotspot.igv.data.InputBlock; import com.sun.hotspot.igv.data.InputGraph; -import java.util.Collection; /** * @@ -34,5 +32,9 @@ import java.util.Collection; */ public interface Scheduler { - public Collection schedule(InputGraph graph); + // Compute a set of scheduled blocks for the given graph, creating new + // blocks if these are not found in the graph. + public void schedule(InputGraph graph); + // Schedule locally the set of blocks in the given graph. + public void scheduleLocally(InputGraph graph); } diff --git a/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java b/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java index 89b1434663f..e3888be31be 100644 --- a/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java +++ b/src/utils/IdealGraphVisualizer/Difference/src/main/java/com/sun/hotspot/igv/difference/Difference.java @@ -114,6 +114,9 @@ public class Difference { Map blocksMap = new HashMap<>(); for (InputBlock blk : a.getBlocks()) { InputBlock diffblk = graph.addBlock(blk.getName()); + if (blk.isArtificial()) { + diffblk.setArtificial(); + } blocksMap.put(blk, diffblk); } for (InputBlock blk : b.getBlocks()) { @@ -121,6 +124,9 @@ public class Difference { if (diffblk == null) { diffblk = graph.addBlock(blk.getName()); } + if (blk.isArtificial()) { + diffblk.setArtificial(); + } blocksMap.put(blk, diffblk); } @@ -249,6 +255,9 @@ public class Difference { } } + Scheduler s = Lookup.getDefault().lookup(Scheduler.class); + s.scheduleLocally(graph); + return graph; } diff --git a/src/utils/IdealGraphVisualizer/ServerCompiler/src/main/java/com/sun/hotspot/igv/servercompiler/ServerCompilerScheduler.java b/src/utils/IdealGraphVisualizer/ServerCompiler/src/main/java/com/sun/hotspot/igv/servercompiler/ServerCompilerScheduler.java index 0a069c53a71..93199ea35a1 100644 --- a/src/utils/IdealGraphVisualizer/ServerCompiler/src/main/java/com/sun/hotspot/igv/servercompiler/ServerCompilerScheduler.java +++ b/src/utils/IdealGraphVisualizer/ServerCompiler/src/main/java/com/sun/hotspot/igv/servercompiler/ServerCompilerScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -296,10 +296,25 @@ public class ServerCompilerScheduler implements Scheduler { return n.getProperties().get("block"); } + private boolean initialize(InputGraph graph) { + nodes = new ArrayList<>(); + inputNodeToNode = new HashMap<>(graph.getNodes().size()); + this.graph = graph; + if (!hasCategoryInformation()) { + ErrorManager.getDefault().log(ErrorManager.WARNING, + "Cannot find node category information in the input graph. " + + "The control-flow graph will not be approximated."); + return false; + } + buildUpGraph(); + markCFGNodes(); + return true; + } + @Override - public Collection schedule(InputGraph graph) { + public void schedule(InputGraph graph) { if (graph.getNodes().isEmpty()) { - return Collections.emptyList(); + return; } if (graph.getBlocks().size() > 0) { @@ -311,20 +326,11 @@ public class ServerCompilerScheduler implements Scheduler { assert graph.getBlock(n) != null; } } - return graph.getBlocks(); + return; } else { - nodes = new ArrayList<>(); - inputNodeToNode = new HashMap<>(graph.getNodes().size()); - - this.graph = graph; - if (!hasCategoryInformation()) { - ErrorManager.getDefault().log(ErrorManager.WARNING, - "Cannot find node category information in the input graph. " + - "The control-flow graph will not be approximated."); - return null; + if (!initialize(graph)) { + return; } - buildUpGraph(); - markCFGNodes(); buildBlocks(); schedulePinned(); buildDominators(); @@ -333,10 +339,26 @@ public class ServerCompilerScheduler implements Scheduler { check(); reportWarnings(); - return blocks; + return; } } + @Override + public void scheduleLocally(InputGraph graph) { + if (!initialize(graph)) { + return; + } + // Import global schedule from the given graph. + blocks = new Vector<>(); + for (InputBlock block : graph.getBlocks()) { + blocks.add(block); + for (InputNode in : block.getNodes()) { + inputNodeToNode.get(in).block = block; + } + } + scheduleLocal(); + } + private void scheduleLocal() { // Leave only local predecessors and successors. for (InputBlock b : blocks) { From baf8bc5701c43425e3345f82d4318b134b26d7c9 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Mon, 6 Oct 2025 08:14:44 +0000 Subject: [PATCH 362/556] 8369038: Parallel: Use NMethodMarkingScope and ThreadsClaimTokenScope in psParallelCompact Reviewed-by: ayang, shade --- src/hotspot/share/gc/parallel/psParallelCompact.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psParallelCompact.cpp b/src/hotspot/share/gc/parallel/psParallelCompact.cpp index af812c652a6..5affa1a3e35 100644 --- a/src/hotspot/share/gc/parallel/psParallelCompact.cpp +++ b/src/hotspot/share/gc/parallel/psParallelCompact.cpp @@ -28,6 +28,7 @@ #include "classfile/symbolTable.hpp" #include "classfile/systemDictionary.hpp" #include "code/codeCache.hpp" +#include "code/nmethod.hpp" #include "compiler/oopMap.hpp" #include "gc/parallel/objectStartArray.inline.hpp" #include "gc/parallel/parallelArguments.hpp" @@ -61,7 +62,6 @@ #include "gc/shared/referenceProcessor.hpp" #include "gc/shared/referenceProcessorPhaseTimes.hpp" #include "gc/shared/spaceDecorator.hpp" -#include "gc/shared/strongRootsScope.hpp" #include "gc/shared/taskTerminator.hpp" #include "gc/shared/weakProcessor.inline.hpp" #include "gc/shared/workerPolicy.hpp" @@ -1085,7 +1085,8 @@ void steal_marking_work(TaskTerminator& terminator, uint worker_id) { } class MarkFromRootsTask : public WorkerTask { - StrongRootsScope _strong_roots_scope; // needed for Threads::possibly_parallel_threads_do + NMethodMarkingScope _nmethod_marking_scope; + ThreadsClaimTokenScope _threads_claim_token_scope; OopStorageSetStrongParState _oop_storage_set_par_state; TaskTerminator _terminator; uint _active_workers; @@ -1093,7 +1094,8 @@ class MarkFromRootsTask : public WorkerTask { public: MarkFromRootsTask(uint active_workers) : WorkerTask("MarkFromRootsTask"), - _strong_roots_scope(active_workers), + _nmethod_marking_scope(), + _threads_claim_token_scope(), _terminator(active_workers, ParCompactionManager::marking_stacks()), _active_workers(active_workers) {} From 2c114d676d9904094dd6058d15f06d801ec7a3d6 Mon Sep 17 00:00:00 2001 From: SendaoYan Date: Mon, 6 Oct 2025 09:26:51 +0000 Subject: [PATCH 363/556] 8367899: compiler/c2/gvn/TestBitCompressValueTransform.java intermittent timed out Reviewed-by: dfenacci, chagedorn --- .../c2/gvn/TestBitCompressValueTransform.java | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/test/hotspot/jtreg/compiler/c2/gvn/TestBitCompressValueTransform.java b/test/hotspot/jtreg/compiler/c2/gvn/TestBitCompressValueTransform.java index 98f26f120b1..3f32f78871c 100644 --- a/test/hotspot/jtreg/compiler/c2/gvn/TestBitCompressValueTransform.java +++ b/test/hotspot/jtreg/compiler/c2/gvn/TestBitCompressValueTransform.java @@ -76,10 +76,7 @@ public class TestBitCompressValueTransform { @Run(test = "test1") public void run1(RunInfo info) { - long res = 0; - for (int i = 0; i < 10000; i++) { - res |= test1(field_L); - } + long res = test1(field_L); Asserts.assertEQ(res, gold_L); } @@ -92,10 +89,7 @@ public class TestBitCompressValueTransform { @Run(test = "test2") public void run2(RunInfo info) { - int res = 0; - for (int i = 0; i < 10000; i++) { - res |= test2(field_I); - } + int res = test2(field_I); Asserts.assertEQ(res, gold_I); } @@ -113,7 +107,7 @@ public class TestBitCompressValueTransform { @Run(test = "test3") public void run3(RunInfo info) { int res = 0; - for (int i = 1; i < 10000; i++) { + for (int i = 1; i < 100; i++) { res |= test3(i); } Asserts.assertLTE(0, res); @@ -133,7 +127,7 @@ public class TestBitCompressValueTransform { @Run(test = "test4") public void run4(RunInfo info) { long res = 0; - for (long i = 1; i < 10000; i++) { + for (long i = 1; i < 100; i++) { res |= test4(i); } Asserts.assertLTE(0L, res); @@ -151,7 +145,7 @@ public class TestBitCompressValueTransform { @Run(test = "test5") public void run5(RunInfo info) { long res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test5((long)i); } Asserts.assertEQ(-1L, res); @@ -169,7 +163,7 @@ public class TestBitCompressValueTransform { @Run(test = "test6") public void run6(RunInfo info) { long res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test6((long)i); } Asserts.assertLTE(0L, res); @@ -188,7 +182,7 @@ public class TestBitCompressValueTransform { @Run(test = "test7") public void run7(RunInfo info) { long res = Long.MIN_VALUE; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res = Long.max(test7((long)i), res); } Asserts.assertGTE(10000L, res); @@ -206,7 +200,7 @@ public class TestBitCompressValueTransform { @Run(test = "test8") public void run8(RunInfo info) { int res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test8(i); } Asserts.assertEQ(-1, res); @@ -224,7 +218,7 @@ public class TestBitCompressValueTransform { @Run(test = "test9") public void run9(RunInfo info) { int res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test9(i); } Asserts.assertLTE(0, res); @@ -243,10 +237,10 @@ public class TestBitCompressValueTransform { @Run(test = "test10") public void run10(RunInfo info) { int res = Integer.MIN_VALUE; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res = Integer.max(test10(i), res); } - Asserts.assertGTE(10000, res); + Asserts.assertGTE(100, res); } @Test @@ -260,7 +254,7 @@ public class TestBitCompressValueTransform { @Run(test = "test11") public void run11(RunInfo info) { int res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test11(i); } Asserts.assertEQ(0, res); @@ -277,7 +271,7 @@ public class TestBitCompressValueTransform { @Run(test = "test12") public void run12(RunInfo info) { long res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test12(i); } Asserts.assertEQ(0L, res); @@ -294,7 +288,7 @@ public class TestBitCompressValueTransform { @Run(test = "test13") public void run13(RunInfo info) { int res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test13(i); } Asserts.assertEQ(0, res); @@ -311,7 +305,7 @@ public class TestBitCompressValueTransform { @Run(test = "test14") public void run14(RunInfo info) { long res = 0; - for (int i = -10000; i < 10000; i++) { + for (int i = -100; i < 100; i++) { res |= test14(i); } Asserts.assertEQ(0L, res); @@ -327,10 +321,7 @@ public class TestBitCompressValueTransform { @Run (test = "test15") public void run15(RunInfo info) { - int res = 0; - for (int i = 0; i < 10000; i++) { - res |= test15(0, 0); - } + int res = test15(0, 0); Asserts.assertEQ(0, res); } @@ -408,7 +399,7 @@ public class TestBitCompressValueTransform { int actual = 0; int expected = 0; - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < 100; i++) { int arg1 = GEN_I.next(); int arg2 = GEN_I.next(); @@ -492,7 +483,7 @@ public class TestBitCompressValueTransform { int actual = 0; int expected = 0; - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < 100; i++) { int arg1 = GEN_I.next(); int arg2 = GEN_I.next(); @@ -576,7 +567,7 @@ public class TestBitCompressValueTransform { long actual = 0; long expected = 0; - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < 100; i++) { long arg1 = GEN_L.next(); long arg2 = GEN_L.next(); @@ -660,7 +651,7 @@ public class TestBitCompressValueTransform { long actual = 0; long expected = 0; - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < 100; i++) { long arg1 = GEN_L.next(); long arg2 = GEN_L.next(); From 2bfada3f58df6c041d948267368cbc4db915cac3 Mon Sep 17 00:00:00 2001 From: jonghoonpark Date: Mon, 6 Oct 2025 11:53:14 +0000 Subject: [PATCH 364/556] 8364927: Add @requires annotation to TestReclaimStringsLeaksMemory.java Reviewed-by: tschatzl, stefank, ayang --- .../stress/TestReclaimStringsLeaksMemory.java | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/test/hotspot/jtreg/gc/stress/TestReclaimStringsLeaksMemory.java b/test/hotspot/jtreg/gc/stress/TestReclaimStringsLeaksMemory.java index 80ab9e21667..1f19222fb8e 100644 --- a/test/hotspot/jtreg/gc/stress/TestReclaimStringsLeaksMemory.java +++ b/test/hotspot/jtreg/gc/stress/TestReclaimStringsLeaksMemory.java @@ -24,19 +24,60 @@ package gc.stress; /* - * @test TestReclaimStringsLeaksMemory + * @test id=Serial * @bug 8180048 - * @summary Ensure that during a Full GC interned string memory is reclaimed completely. - * @requires vm.gc == "null" + * @summary Ensure that during a Full GC interned string memory is reclaimed completely with SerialGC. + * @requires vm.gc.Serial * @requires !vm.debug * @library /test/lib * @modules java.base/jdk.internal.misc - * @run driver/timeout=480 gc.stress.TestReclaimStringsLeaksMemory * @run driver/timeout=480 gc.stress.TestReclaimStringsLeaksMemory -XX:+UseSerialGC + */ + +/* + * @test id=Parallel + * @bug 8180048 + * @summary Ensure that during a Full GC interned string memory is reclaimed completely with ParallelGC. + * @requires vm.gc.Parallel + * @requires !vm.debug + * @library /test/lib + * @modules java.base/jdk.internal.misc * @run driver/timeout=480 gc.stress.TestReclaimStringsLeaksMemory -XX:+UseParallelGC + */ + +/* + * @test id=G1 + * @bug 8180048 + * @summary Ensure that during a Full GC interned string memory is reclaimed completely with G1GC. + * @requires vm.gc.G1 + * @requires !vm.debug + * @library /test/lib + * @modules java.base/jdk.internal.misc * @run driver/timeout=480 gc.stress.TestReclaimStringsLeaksMemory -XX:+UseG1GC */ +/* + * @test id=Shenandoah + * @bug 8180048 + * @summary Ensure that during a Full GC interned string memory is reclaimed completely with ShenandoahGC. + * @requires vm.gc.Shenandoah + * @requires !vm.debug + * @library /test/lib + * @modules java.base/jdk.internal.misc + * @run driver/timeout=480 gc.stress.TestReclaimStringsLeaksMemory -XX:+UseShenandoahGC + */ + +/* + * @test id=Z + * @bug 8180048 + * @summary Ensure that during a Full GC interned string memory is reclaimed completely with ZGC. + * @requires vm.gc.Z + * @requires !vm.debug + * @library /test/lib + * @modules java.base/jdk.internal.misc + * @run driver/timeout=480 gc.stress.TestReclaimStringsLeaksMemory -XX:+UseZGC + */ + import java.util.Arrays; import java.util.ArrayList; import java.util.regex.Pattern; From e3320a9df592a06c466ae9158d8f173921679952 Mon Sep 17 00:00:00 2001 From: Nizar Benalla Date: Mon, 6 Oct 2025 13:32:46 +0000 Subject: [PATCH 365/556] 8367610: Test tools/sincechecker/modules/java.base/JavaBaseCheckSince.java timed out on Windows Reviewed-by: liach --- .../sincechecker/modules/java.base/JavaBaseCheckSince.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/jdk/tools/sincechecker/modules/java.base/JavaBaseCheckSince.java b/test/jdk/tools/sincechecker/modules/java.base/JavaBaseCheckSince.java index 64d5bf2465f..b75b6d9401a 100644 --- a/test/jdk/tools/sincechecker/modules/java.base/JavaBaseCheckSince.java +++ b/test/jdk/tools/sincechecker/modules/java.base/JavaBaseCheckSince.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,8 +23,8 @@ /* * @test - * @bug 8331051 + * @bug 8331051 8367610 * @summary Test for `@since` in java.base module * @library /test/lib /test/jdk/tools/sincechecker - * @run main SinceChecker java.base --exclude java.lang.classfile + * @run main/timeout=480 SinceChecker java.base --exclude java.lang.classfile */ From b6a4cfecb731615b6ef70828ac10fae4b2264cdc Mon Sep 17 00:00:00 2001 From: Mahendra Chhipa Date: Mon, 6 Oct 2025 15:26:59 +0000 Subject: [PATCH 366/556] 8367114: Update jdk.test.lib.net.SimpleHttpServer to use SimpleFileServer Reviewed-by: dfuchs, vyazici --- .../catalog/CatalogFileInputTest.java | 27 +++- .../sun/net/httpserver/SimpleFileServer.java | 71 -------- .../mrjar/MultiReleaseJarHttpProperties.java | 27 ++-- .../jar/MultiReleaseJarURLConnection.java | 27 ++-- .../jdk/test/lib/net/SimpleHttpServer.java | 152 ------------------ 5 files changed, 53 insertions(+), 251 deletions(-) delete mode 100644 test/jdk/com/sun/net/httpserver/SimpleFileServer.java delete mode 100644 test/lib/jdk/test/lib/net/SimpleHttpServer.java diff --git a/test/jaxp/javax/xml/jaxp/unittest/catalog/CatalogFileInputTest.java b/test/jaxp/javax/xml/jaxp/unittest/catalog/CatalogFileInputTest.java index 66ddad86785..9a19c0237d7 100644 --- a/test/jaxp/javax/xml/jaxp/unittest/catalog/CatalogFileInputTest.java +++ b/test/jaxp/javax/xml/jaxp/unittest/catalog/CatalogFileInputTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -38,6 +38,8 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import javax.xml.catalog.Catalog; import javax.xml.catalog.CatalogException; @@ -49,6 +51,9 @@ import static java.nio.file.StandardOpenOption.APPEND; import static java.nio.file.StandardOpenOption.CREATE; import static jaxp.library.JAXPTestUtilities.getSystemProperty; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.SimpleFileServer; +import jdk.test.lib.net.URIBuilder; import jdk.test.lib.util.JarUtils; import org.testng.Assert; import org.testng.annotations.AfterClass; @@ -56,13 +61,11 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.xml.sax.InputSource; -import jdk.test.lib.net.SimpleHttpServer; /* * @test * @bug 8151154 8171243 * @library /javax/xml/jaxp/libs /javax/xml/jaxp/unittest /test/lib - * @build jdk.test.lib.net.SimpleHttpServer * @run testng/othervm catalog.CatalogFileInputTest * @summary Verifies that the Catalog API accepts valid URIs only; * Verifies that the CatalogFeatures' builder throws @@ -81,9 +84,9 @@ public class CatalogFileInputTest extends CatalogSupportBase { final static String SCHEME_JARFILE = "jar:"; static final String REMOTE_FILE_LOCATION = "/jar/META-INF"; static final String DOCROOT = SRC_DIR; - static final String TESTCONTEXT = REMOTE_FILE_LOCATION; //mapped to local file path - private SimpleHttpServer httpserver; + private HttpServer httpserver; private String remoteFilePath; + private ExecutorService executor; /* * Initializing fields @@ -92,15 +95,23 @@ public class CatalogFileInputTest extends CatalogSupportBase { public void setUpClass() throws Exception { super.setUp(); // set up HttpServer - httpserver = new SimpleHttpServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), TESTCONTEXT, DOCROOT); + httpserver = SimpleFileServer.createFileServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), + Path.of(DOCROOT), SimpleFileServer.OutputLevel.INFO); + executor = Executors.newCachedThreadPool(); + httpserver.setExecutor(executor); httpserver.start(); - remoteFilePath = httpserver.getAddress() + REMOTE_FILE_LOCATION; + remoteFilePath = URIBuilder.newBuilder() + .scheme("http") + .host(httpserver.getAddress().getAddress()) + .port(httpserver.getAddress().getPort()) + .build().toString() + REMOTE_FILE_LOCATION; } @AfterClass protected void tearDown() { if (httpserver != null) { - httpserver.stop(); + httpserver.stop(0); + executor.shutdown(); } } diff --git a/test/jdk/com/sun/net/httpserver/SimpleFileServer.java b/test/jdk/com/sun/net/httpserver/SimpleFileServer.java deleted file mode 100644 index bff415c0b98..00000000000 --- a/test/jdk/com/sun/net/httpserver/SimpleFileServer.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -import java.util.concurrent.*; -import java.util.logging.*; -import java.io.*; -import java.net.*; - -import com.sun.net.httpserver.*; - -/** - * Implements a basic static content HTTP server - * which understands text/html, text/plain content types - * - * Must be given an abs pathname to the document root. - * Directory listings together with text + html files - * can be served. - * - * File Server created on files sub-path - * - * Echo server created on echo sub-path - */ -public class SimpleFileServer { - - public static void main (String[] args) throws Exception { - if (args.length != 3) { - System.out.println ("usage: java FileServerHandler rootDir port logfilename"); - System.exit(1); - } - Logger logger = Logger.getLogger("com.sun.net.httpserver"); - ConsoleHandler ch = new ConsoleHandler(); - logger.setLevel(Level.ALL); - ch.setLevel(Level.ALL); - logger.addHandler(ch); - - String rootDir = args[0]; - int port = Integer.parseInt (args[1]); - String logfile = args[2]; - HttpServer server = HttpServer.create (new InetSocketAddress (port), 0); - HttpHandler h = new FileServerHandler (rootDir); - HttpHandler h1 = new EchoHandler (); - - HttpContext c = server.createContext ("/files", h); - c.getFilters().add (new LogFilter (new File (logfile))); - HttpContext c1 = server.createContext ("/echo", h1); - c.getFilters().add (new LogFilter (new File (logfile))); - c1.getFilters().add (new LogFilter (new File (logfile))); - server.setExecutor (Executors.newCachedThreadPool()); - server.start (); - } -} diff --git a/test/jdk/java/util/jar/JarFile/mrjar/MultiReleaseJarHttpProperties.java b/test/jdk/java/util/jar/JarFile/mrjar/MultiReleaseJarHttpProperties.java index 93cf0e0165d..16f764c6674 100644 --- a/test/jdk/java/util/jar/JarFile/mrjar/MultiReleaseJarHttpProperties.java +++ b/test/jdk/java/util/jar/JarFile/mrjar/MultiReleaseJarHttpProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,9 +28,7 @@ * @library /lib/testlibrary/java/util/jar /test/lib * @modules jdk.jartool * jdk.compiler - * jdk.httpserver * @build CreateMultiReleaseTestJars - * jdk.test.lib.net.SimpleHttpServer * jdk.test.lib.compiler.Compiler * jdk.test.lib.util.JarBuilder * @run testng MultiReleaseJarHttpProperties @@ -51,21 +49,28 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; -import jdk.test.lib.net.SimpleHttpServer; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.SimpleFileServer; import jdk.test.lib.net.URIBuilder; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class MultiReleaseJarHttpProperties extends MultiReleaseJarProperties { - private SimpleHttpServer server; + private HttpServer server; + private ExecutorService executor; static final String TESTCONTEXT = "/multi-release.jar"; //mapped to local file path @BeforeClass public void initialize() throws Exception { - server = new SimpleHttpServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), TESTCONTEXT, - System.getProperty("user.dir", ".")); + server = SimpleFileServer.createFileServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), + Path.of(System.getProperty("user.dir", ".")), SimpleFileServer.OutputLevel.INFO); + executor = Executors.newCachedThreadPool(); + server.setExecutor(executor); server.start(); super.initialize(); } @@ -73,7 +78,7 @@ public class MultiReleaseJarHttpProperties extends MultiReleaseJarProperties { @Override protected void initializeClassLoader() throws Exception { URL[] urls = new URL[]{ - URIBuilder.newBuilder().scheme("http").port(server.getPort()).loopback() + URIBuilder.newBuilder().scheme("http").port(server.getAddress().getPort()).loopback() .path(TESTCONTEXT).toURL(), }; cldr = new URLClassLoader(urls); @@ -84,8 +89,10 @@ public class MultiReleaseJarHttpProperties extends MultiReleaseJarProperties { @AfterClass public void close() throws IOException { // Windows requires server to stop before file is deleted - if (server != null) - server.stop(); + if (server != null) { + server.stop(0); + executor.shutdown(); + } super.close(); } diff --git a/test/jdk/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java b/test/jdk/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java index 2d5ba81194f..f113e7d3fcd 100644 --- a/test/jdk/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java +++ b/test/jdk/sun/net/www/protocol/jar/MultiReleaseJarURLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,10 +27,8 @@ * @summary Test that URL connections to multi-release jars can be runtime versioned * @library /lib/testlibrary/java/util/jar /test/lib * @modules jdk.compiler - * jdk.httpserver * jdk.jartool * @build CreateMultiReleaseTestJars - * jdk.test.lib.net.SimpleHttpServer * jdk.test.lib.util.JarBuilder * jdk.test.lib.compiler.Compiler * @run testng MultiReleaseJarURLConnection @@ -51,11 +49,15 @@ import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.jar.JarFile; -import jdk.test.lib.net.SimpleHttpServer; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.SimpleFileServer; import jdk.test.lib.net.URIBuilder; import org.testng.Assert; import org.testng.annotations.AfterClass; @@ -68,8 +70,8 @@ public class MultiReleaseJarURLConnection { String unversioned = userdir + "/unversioned.jar"; String unsigned = userdir + "/multi-release.jar"; String signed = userdir + "/signed-multi-release.jar"; - static final String TESTCONTEXT = "/multi-release.jar"; - SimpleHttpServer server; + HttpServer server; + ExecutorService executor; @BeforeClass public void initialize() throws Exception { @@ -78,7 +80,10 @@ public class MultiReleaseJarURLConnection { creator.buildUnversionedJar(); creator.buildMultiReleaseJar(); creator.buildSignedMultiReleaseJar(); - server = new SimpleHttpServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), TESTCONTEXT, System.getProperty("user.dir", ".")); + server = SimpleFileServer.createFileServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), + Path.of(System.getProperty("user.dir", ".")), SimpleFileServer.OutputLevel.INFO); + executor = Executors.newCachedThreadPool(); + server.setExecutor(executor); server.start(); } @@ -86,7 +91,9 @@ public class MultiReleaseJarURLConnection { public void close() throws IOException { // Windows requires server to stop before file is deleted if (server != null) - server.stop(); + server.stop(0); + executor.shutdown(); + Files.delete(Paths.get(unversioned)); Files.delete(Paths.get(unsigned)); Files.delete(Paths.get(signed)); @@ -176,8 +183,8 @@ public class MultiReleaseJarURLConnection { {"unsigned", new URL("jar:file:" + unsigned + "!/")}, {"signed", new URL("jar:file:" + signed + "!/")}, // external jar received via http protocol - {"http", toHttpJarURL(server.getPort(), "/multi-release.jar", "!/")}, - {"http", URIBuilder.newBuilder().scheme("http").port(server.getPort()) + {"http", toHttpJarURL(server.getAddress().getPort(), "/multi-release.jar", "!/")}, + {"http", URIBuilder.newBuilder().scheme("http").port(server.getAddress().getPort()) .loopback().path("/multi-release.jar").toURL()}, }; } diff --git a/test/lib/jdk/test/lib/net/SimpleHttpServer.java b/test/lib/jdk/test/lib/net/SimpleHttpServer.java deleted file mode 100644 index 1905091eac6..00000000000 --- a/test/lib/jdk/test/lib/net/SimpleHttpServer.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ -package jdk.test.lib.net; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; - -/** - * A simple HTTP Server. - **/ -public class SimpleHttpServer { - private final HttpServer httpServer; - private ExecutorService executor; - private String address; - private final String context; - private final String docRoot; - private final InetSocketAddress inetSocketAddress; - - public SimpleHttpServer(final InetSocketAddress inetSocketAddress, final String context, final String docRoot) - throws IOException { - this.inetSocketAddress = inetSocketAddress; - this.context = context; - this.docRoot = docRoot; - httpServer = HttpServer.create(); - } - - public void start() throws IOException, URISyntaxException { - MyHttpHandler handler = new MyHttpHandler(docRoot); - httpServer.bind(inetSocketAddress, 0); - httpServer.createContext(context, handler); - executor = Executors.newCachedThreadPool(); - httpServer.setExecutor(executor); - httpServer.start(); - address = "http:" + URIBuilder.newBuilder().host(httpServer.getAddress().getAddress()). - port(httpServer.getAddress().getPort()).build().toString(); - } - - public void stop() { - httpServer.stop(0); - executor.shutdown(); - } - - public String getAddress() { - return address; - } - - public int getPort() { - return httpServer.getAddress().getPort(); - } - - class MyHttpHandler implements HttpHandler { - private final URI rootUri; - - MyHttpHandler(final String docroot) { - rootUri = Path.of(docroot).toUri().normalize(); - } - - public void handle(final HttpExchange t) throws IOException { - try (InputStream is = t.getRequestBody()) { - is.readAllBytes(); - Headers rMap = t.getResponseHeaders(); - try (OutputStream os = t.getResponseBody()) { - URI uri = t.getRequestURI(); - String path = uri.getRawPath(); - assert path.isEmpty() || path.startsWith("/"); - Path fPath; - try { - uri = URI.create("file://" + rootUri.getRawPath() + path).normalize(); - fPath = Path.of(uri); - } catch (IllegalArgumentException | FileSystemNotFoundException ex) { - ex.printStackTrace(); - notfound(t, path); - return; - } - byte[] bytes = Files.readAllBytes(fPath); - String method = t.getRequestMethod(); - if (method.equals("HEAD")) { - rMap.set("Content-Length", Long.toString(bytes.length)); - t.sendResponseHeaders(200, -1); - t.close(); - } else if (!method.equals("GET")) { - t.sendResponseHeaders(405, -1); - t.close(); - return; - } - if (path.endsWith(".html") || path.endsWith(".htm")) { - rMap.set("Content-Type", "text/html"); - } else { - rMap.set("Content-Type", "text/plain"); - } - t.sendResponseHeaders(200, bytes.length); - os.write(bytes); - } - } - } - void moved(final HttpExchange t) throws IOException { - Headers req = t.getRequestHeaders(); - Headers map = t.getResponseHeaders(); - URI uri = t.getRequestURI(); - String host = req.getFirst("Host"); - String location = "http://" + host + uri.getPath() + "/"; - map.set("Content-Type", "text/html"); - map.set("Location", location); - t.sendResponseHeaders(301, -1); - t.close(); - } - void notfound(final HttpExchange t, final String p) throws IOException { - t.getResponseHeaders().set("Content-Type", "text/html"); - t.sendResponseHeaders(404, 0); - try (OutputStream os = t.getResponseBody()) { - String s = "

        File not found

        "; - s = s + p + "

        "; - os.write(s.getBytes()); - } - t.close(); - } - } -} From 596af0a7cc37e359d54689be20f855a86ae46567 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Mon, 6 Oct 2025 15:44:13 +0000 Subject: [PATCH 367/556] 8369041: Release memory after testing in ThreadsRunner.java Reviewed-by: shade, tschatzl --- .../jtreg/vmTestbase/nsk/share/runner/ThreadsRunner.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/hotspot/jtreg/vmTestbase/nsk/share/runner/ThreadsRunner.java b/test/hotspot/jtreg/vmTestbase/nsk/share/runner/ThreadsRunner.java index 0401fea13fc..39e7e2c2237 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/share/runner/ThreadsRunner.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/share/runner/ThreadsRunner.java @@ -310,6 +310,10 @@ public class ThreadsRunner implements MultiRunner, LogAware, RunParamsAware { log.info("Unexpected exception during the run."); log.info(t); successful = false; + } finally { + // Finished testing; release memory to avoid OOM. + runnables.clear(); + threads.clear(); } } From 0f406c420e35f7a4358dc99711fd23d162f21777 Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Mon, 6 Oct 2025 16:11:59 +0000 Subject: [PATCH 368/556] 8369078: Fix faulty test conversion in IllegalCharsetName.java Reviewed-by: naoto, alanb --- test/jdk/java/nio/charset/Charset/IllegalCharsetName.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/jdk/java/nio/charset/Charset/IllegalCharsetName.java b/test/jdk/java/nio/charset/Charset/IllegalCharsetName.java index 266801cf52b..9f2879a3ff0 100644 --- a/test/jdk/java/nio/charset/Charset/IllegalCharsetName.java +++ b/test/jdk/java/nio/charset/Charset/IllegalCharsetName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -50,7 +50,7 @@ public class IllegalCharsetName { assertThrows(IllegalCharsetNameException.class, () -> Charset.forName(name)); assertThrows(IllegalCharsetNameException.class, - () -> Charset.forName(name)); + () -> Charset.isSupported(name)); } // Charset.forName, Charset.isSupported, and the Charset constructor should @@ -60,7 +60,7 @@ public class IllegalCharsetName { assertThrows(IllegalCharsetNameException.class, () -> Charset.forName("")); assertThrows(IllegalCharsetNameException.class, - () -> Charset.forName("")); + () -> Charset.isSupported("")); assertThrows(IllegalCharsetNameException.class, () -> new Charset("", new String[]{}) { @Override From 2376a9e9727e9cb3020dd3f57584950a4cdcdab6 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Mon, 6 Oct 2025 17:30:42 +0000 Subject: [PATCH 369/556] 8365630: jdk/jfr/tool/TestPrintContextual.java fails with wrong spanId Reviewed-by: shade --- test/jdk/jdk/jfr/tool/TestPrintContextual.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/jdk/jdk/jfr/tool/TestPrintContextual.java b/test/jdk/jdk/jfr/tool/TestPrintContextual.java index 15486148b9c..e6df9c7c729 100644 --- a/test/jdk/jdk/jfr/tool/TestPrintContextual.java +++ b/test/jdk/jdk/jfr/tool/TestPrintContextual.java @@ -25,6 +25,7 @@ package jdk.jfr.tool; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; @@ -264,7 +265,8 @@ public class TestPrintContextual { } } - private static void span(int depth) { + private static void span(int depth) throws InterruptedException { + awaitUniqueTimestamp(); SpanEvent span = new SpanEvent(); span.name = "span"; span.spanId = depth; @@ -277,6 +279,13 @@ public class TestPrintContextual { span.commit(); } + private static void awaitUniqueTimestamp() throws InterruptedException { + Instant timestamp = Instant.now(); + while (timestamp.equals(Instant.now())) { + Thread.sleep(1); + } + } + // Tests that context values are only inhjected into events in the same thread. private static void testThreadedContext() throws Exception { try (Recording r = new Recording()) { From eb34a117934951af075a425ce2cf8d3b1ced9700 Mon Sep 17 00:00:00 2001 From: Mikael Vidstedt Date: Tue, 7 Oct 2025 00:52:38 +0000 Subject: [PATCH 370/556] 8369242: Rename URL variables in devkit/Tools.gmk Reviewed-by: erikj --- make/devkit/Tools.gmk | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/make/devkit/Tools.gmk b/make/devkit/Tools.gmk index f27d47b822c..6241674071c 100644 --- a/make/devkit/Tools.gmk +++ b/make/devkit/Tools.gmk @@ -117,13 +117,13 @@ dependencies := gcc binutils ccache mpfr gmp mpc gdb $(foreach dep,$(dependencies),$(eval $(dep)_ver := $(dep)-$($(dep)_ver_only))) -GCC := http://ftp.gnu.org/pub/gnu/gcc/$(gcc_ver)/$(gcc_ver).tar.xz -BINUTILS := http://ftp.gnu.org/pub/gnu/binutils/$(binutils_ver).tar.gz -CCACHE := https://github.com/ccache/ccache/releases/download/v$(ccache_ver_only)/$(ccache_ver).tar.xz -MPFR := https://www.mpfr.org/$(mpfr_ver)/$(mpfr_ver).tar.bz2 -GMP := http://ftp.gnu.org/pub/gnu/gmp/$(gmp_ver).tar.bz2 -MPC := http://ftp.gnu.org/pub/gnu/mpc/$(mpc_ver).tar.gz -GDB := http://ftp.gnu.org/gnu/gdb/$(gdb_ver).tar.xz +GCC_URL := http://ftp.gnu.org/pub/gnu/gcc/$(gcc_ver)/$(gcc_ver).tar.xz +BINUTILS_URL := http://ftp.gnu.org/pub/gnu/binutils/$(binutils_ver).tar.gz +CCACHE_URL := https://github.com/ccache/ccache/releases/download/v$(ccache_ver_only)/$(ccache_ver).tar.xz +MPFR_URL := https://www.mpfr.org/$(mpfr_ver)/$(mpfr_ver).tar.bz2 +GMP_URL := http://ftp.gnu.org/pub/gnu/gmp/$(gmp_ver).tar.bz2 +MPC_URL := http://ftp.gnu.org/pub/gnu/mpc/$(mpc_ver).tar.gz +GDB_URL := http://ftp.gnu.org/gnu/gdb/$(gdb_ver).tar.xz REQUIRED_MIN_MAKE_MAJOR_VERSION := 4 ifneq ($(REQUIRED_MIN_MAKE_MAJOR_VERSION),) @@ -201,7 +201,7 @@ download-rpms: # Generate downloading + unpacking of sources. define Download # Allow override - $(1)_DIRNAME ?= $(basename $(basename $(notdir $($(1))))) + $(1)_DIRNAME ?= $(basename $(basename $(notdir $($(1)_URL)))) $(1)_DIR = $(abspath $(SRCDIR)/$$($(1)_DIRNAME)) ifeq ($$($(1)_CMAKE_BASED),) $(1)_CFG = $$($(1)_DIR)/configure @@ -212,7 +212,7 @@ define Download $(1)_SRC_MARKER = $$($(1)_DIR)/CMakeLists.txt $(1)_CONFIG = $$(CMAKE_CONFIG) $$($(1)_DIR) endif - $(1)_FILE = $(DOWNLOAD)/$(notdir $($(1))) + $(1)_FILE = $(DOWNLOAD)/$(notdir $($(1)_URL)) $$($(1)_SRC_MARKER) : $$($(1)_FILE) mkdir -p $$(SRCDIR) @@ -224,7 +224,7 @@ define Download touch $$@ $$($(1)_FILE) : - wget -P $(DOWNLOAD) $$($(1)) + wget -P $(DOWNLOAD) $$($(1)_URL) endef # Download and unpack all source packages From e783c524c17e1d1a3fff4b6370e222134e66edc8 Mon Sep 17 00:00:00 2001 From: Prasanta Sadhukhan Date: Tue, 7 Oct 2025 04:08:32 +0000 Subject: [PATCH 371/556] 8368185: Test javax/swing/plaf/synth/SynthButtonUI/6276188/bug6276188.java failed: Synth ButtonUI does not handle PRESSED & MOUSE_OVER state Reviewed-by: tr, aivanov --- .../SynthButtonUI/6276188/bug6276188.java | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/test/jdk/javax/swing/plaf/synth/SynthButtonUI/6276188/bug6276188.java b/test/jdk/javax/swing/plaf/synth/SynthButtonUI/6276188/bug6276188.java index be9c9457fff..9b689d2fe2c 100644 --- a/test/jdk/javax/swing/plaf/synth/SynthButtonUI/6276188/bug6276188.java +++ b/test/jdk/javax/swing/plaf/synth/SynthButtonUI/6276188/bug6276188.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -21,40 +21,53 @@ * questions. */ /** - * @test 1.4 08/08/05 + * @test * @key headful * @bug 6276188 * @library ../../../../regtesthelpers * @build Util - * @author Romain Guy * @summary Tests PRESSED and MOUSE_OVER and FOCUSED state for buttons with Synth. * @run main/othervm -Dsun.java2d.uiScale=1 bug6276188 */ -import java.awt.*; -import java.awt.image.*; -import java.awt.event.*; -import javax.swing.*; -import javax.swing.plaf.synth.*; +import java.io.File; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.awt.event.InputEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.imageio.ImageIO; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.plaf.synth.SynthLookAndFeel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import java.util.concurrent.CountDownLatch; public class bug6276188 { private static JButton button; - private static Point p; private static JFrame testFrame; // move away from cursor - private final static int OFFSET_X = -20; - private final static int OFFSET_Y = -20; + private final static int OFFSET_X = 20; + private final static int OFFSET_Y = 20; public static void main(String[] args) throws Throwable { + Robot robot = new Robot(); try { - Robot robot = new Robot(); robot.setAutoDelay(100); SynthLookAndFeel lookAndFeel = new SynthLookAndFeel(); lookAndFeel.load(bug6276188.class.getResourceAsStream("bug6276188.xml"), bug6276188.class); UIManager.setLookAndFeel(lookAndFeel); + CountDownLatch latch = new CountDownLatch(1); SwingUtilities.invokeAndWait(new Runnable() { public void run() { @@ -62,6 +75,13 @@ public class bug6276188 { testFrame.setLayout(new BorderLayout()); testFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); testFrame.add(BorderLayout.CENTER, button = new JButton()); + button.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + System.out.println("Mouse pressed"); + latch.countDown(); + } + }); testFrame.setSize(new Dimension(320, 200)); testFrame.setLocationRelativeTo(null); @@ -72,13 +92,14 @@ public class bug6276188 { robot.waitForIdle(); robot.delay(1000); - p = Util.getCenterPoint(button); + Point p = Util.getCenterPoint(button); System.out.println("Button center point: " + p); robot.mouseMove(p.x , p.y); robot.waitForIdle(); robot.mousePress(InputEvent.BUTTON1_DOWN_MASK); - robot.waitForIdle(); + latch.await(); + robot.delay(1000); Color color = robot.getPixelColor(p.x - OFFSET_X, p.y - OFFSET_Y); robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK); @@ -89,7 +110,7 @@ public class bug6276188 { Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Rectangle screen = new Rectangle(0, 0, (int) screenSize.getWidth(), (int) screenSize.getHeight()); BufferedImage img = robot.createScreenCapture(screen); - javax.imageio.ImageIO.write(img, "png", new java.io.File("image.png")); + ImageIO.write(img, "png", new File("image.png")); throw new RuntimeException("Synth ButtonUI does not handle PRESSED & MOUSE_OVER state"); } } finally { From 07549f3e1539a2dd491a4f9ffe9df8580d7d7dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Maillard?= Date: Tue, 7 Oct 2025 07:43:43 +0000 Subject: [PATCH 372/556] 8360389: Support printing from C2 compiled code Reviewed-by: kvn, thartmann, mhaessig --- src/hotspot/share/opto/compile.cpp | 155 ++++++++++++++++++++ src/hotspot/share/opto/compile.hpp | 22 +++ src/hotspot/share/opto/runtime.cpp | 56 +++++++ src/hotspot/share/opto/runtime.hpp | 10 ++ src/hotspot/share/runtime/sharedRuntime.cpp | 40 +++++ src/hotspot/share/runtime/sharedRuntime.hpp | 34 +++++ 6 files changed, 317 insertions(+) diff --git a/src/hotspot/share/opto/compile.cpp b/src/hotspot/share/opto/compile.cpp index 7f63efe9e5e..6babc13e1b3 100644 --- a/src/hotspot/share/opto/compile.cpp +++ b/src/hotspot/share/opto/compile.cpp @@ -5378,3 +5378,158 @@ Node* Compile::narrow_value(BasicType bt, Node* value, const Type* type, PhaseGV void Compile::record_method_not_compilable_oom() { record_method_not_compilable(CompilationMemoryStatistic::failure_reason_memlimit()); } + +#ifndef PRODUCT +// Collects all the control inputs from nodes on the worklist and from their data dependencies +static void find_candidate_control_inputs(Unique_Node_List& worklist, Unique_Node_List& candidates) { + // Follow non-control edges until we reach CFG nodes + for (uint i = 0; i < worklist.size(); i++) { + const Node* n = worklist.at(i); + for (uint j = 0; j < n->req(); j++) { + Node* in = n->in(j); + if (in == nullptr || in->is_Root()) { + continue; + } + if (in->is_CFG()) { + if (in->is_Call()) { + // The return value of a call is only available if the call did not result in an exception + Node* control_proj_use = in->as_Call()->proj_out(TypeFunc::Control)->unique_out(); + if (control_proj_use->is_Catch()) { + Node* fall_through = control_proj_use->as_Catch()->proj_out(CatchProjNode::fall_through_index); + candidates.push(fall_through); + continue; + } + } + + if (in->is_Multi()) { + // We got here by following data inputs so we should only have one control use + // (no IfNode, etc) + assert(!n->is_MultiBranch(), "unexpected node type: %s", n->Name()); + candidates.push(in->as_Multi()->proj_out(TypeFunc::Control)); + } else { + candidates.push(in); + } + } else { + worklist.push(in); + } + } + } +} + +// Returns the candidate node that is a descendant to all the other candidates +static Node* pick_control(Unique_Node_List& candidates) { + Unique_Node_List worklist; + worklist.copy(candidates); + + // Traverse backwards through the CFG + for (uint i = 0; i < worklist.size(); i++) { + const Node* n = worklist.at(i); + if (n->is_Root()) { + continue; + } + for (uint j = 0; j < n->req(); j++) { + // Skip backedge of loops to avoid cycles + if (n->is_Loop() && j == LoopNode::LoopBackControl) { + continue; + } + + Node* pred = n->in(j); + if (pred != nullptr && pred != n && pred->is_CFG()) { + worklist.push(pred); + // if pred is an ancestor of n, then pred is an ancestor to at least one candidate + candidates.remove(pred); + } + } + } + + assert(candidates.size() == 1, "unexpected control flow"); + return candidates.at(0); +} + +// Initialize a parameter input for a debug print call, using a placeholder for jlong and jdouble +static void debug_print_init_parm(Node* call, Node* parm, Node* half, int* pos) { + call->init_req((*pos)++, parm); + const BasicType bt = parm->bottom_type()->basic_type(); + if (bt == T_LONG || bt == T_DOUBLE) { + call->init_req((*pos)++, half); + } +} + +Node* Compile::make_debug_print_call(const char* str, address call_addr, PhaseGVN* gvn, + Node* parm0, Node* parm1, + Node* parm2, Node* parm3, + Node* parm4, Node* parm5, + Node* parm6) const { + Node* str_node = gvn->transform(new ConPNode(TypeRawPtr::make(((address) str)))); + const TypeFunc* type = OptoRuntime::debug_print_Type(parm0, parm1, parm2, parm3, parm4, parm5, parm6); + Node* call = new CallLeafNode(type, call_addr, "debug_print", TypeRawPtr::BOTTOM); + + // find the most suitable control input + Unique_Node_List worklist, candidates; + if (parm0 != nullptr) { worklist.push(parm0); + if (parm1 != nullptr) { worklist.push(parm1); + if (parm2 != nullptr) { worklist.push(parm2); + if (parm3 != nullptr) { worklist.push(parm3); + if (parm4 != nullptr) { worklist.push(parm4); + if (parm5 != nullptr) { worklist.push(parm5); + if (parm6 != nullptr) { worklist.push(parm6); + /* close each nested if ===> */ } } } } } } } + find_candidate_control_inputs(worklist, candidates); + Node* control = nullptr; + if (candidates.size() == 0) { + control = C->start()->proj_out(TypeFunc::Control); + } else { + control = pick_control(candidates); + } + + // find all the previous users of the control we picked + GrowableArray users_of_control; + for (DUIterator_Fast kmax, i = control->fast_outs(kmax); i < kmax; i++) { + Node* use = control->fast_out(i); + if (use->is_CFG() && use != control) { + users_of_control.push(use); + } + } + + // we do not actually care about IO and memory as it uses neither + call->init_req(TypeFunc::Control, control); + call->init_req(TypeFunc::I_O, top()); + call->init_req(TypeFunc::Memory, top()); + call->init_req(TypeFunc::FramePtr, C->start()->proj_out(TypeFunc::FramePtr)); + call->init_req(TypeFunc::ReturnAdr, top()); + + int pos = TypeFunc::Parms; + call->init_req(pos++, str_node); + if (parm0 != nullptr) { debug_print_init_parm(call, parm0, top(), &pos); + if (parm1 != nullptr) { debug_print_init_parm(call, parm1, top(), &pos); + if (parm2 != nullptr) { debug_print_init_parm(call, parm2, top(), &pos); + if (parm3 != nullptr) { debug_print_init_parm(call, parm3, top(), &pos); + if (parm4 != nullptr) { debug_print_init_parm(call, parm4, top(), &pos); + if (parm5 != nullptr) { debug_print_init_parm(call, parm5, top(), &pos); + if (parm6 != nullptr) { debug_print_init_parm(call, parm6, top(), &pos); + /* close each nested if ===> */ } } } } } } } + assert(call->in(call->req()-1) != nullptr, "must initialize all parms"); + + call = gvn->transform(call); + Node* call_control_proj = gvn->transform(new ProjNode(call, TypeFunc::Control)); + + // rewire previous users to have the new call as control instead + PhaseIterGVN* igvn = gvn->is_IterGVN(); + for (int i = 0; i < users_of_control.length(); i++) { + Node* use = users_of_control.at(i); + for (uint j = 0; j < use->req(); j++) { + if (use->in(j) == control) { + if (igvn != nullptr) { + igvn->replace_input_of(use, j, call_control_proj); + } else { + gvn->hash_delete(use); + use->set_req(j, call_control_proj); + gvn->hash_insert(use); + } + } + } + } + + return call; +} +#endif // !PRODUCT diff --git a/src/hotspot/share/opto/compile.hpp b/src/hotspot/share/opto/compile.hpp index a68da644d82..66a5497a7ad 100644 --- a/src/hotspot/share/opto/compile.hpp +++ b/src/hotspot/share/opto/compile.hpp @@ -1316,6 +1316,28 @@ public: BasicType out_bt, BasicType in_bt); static Node* narrow_value(BasicType bt, Node* value, const Type* type, PhaseGVN* phase, bool transform_res); + +#ifndef PRODUCT +private: + // getting rid of the template makes things easier + Node* make_debug_print_call(const char* str, address call_addr, PhaseGVN* gvn, + Node* parm0 = nullptr, Node* parm1 = nullptr, + Node* parm2 = nullptr, Node* parm3 = nullptr, + Node* parm4 = nullptr, Node* parm5 = nullptr, + Node* parm6 = nullptr) const; + +public: + // Creates a CallLeafNode for a runtime call that prints a static string and the values of the + // nodes passed as arguments. + // This function also takes care of doing the necessary wiring, including finding a suitable control + // based on the nodes that need to be printed. Note that passing nodes that have incompatible controls + // is undefined behavior. + template + Node* make_debug_print(const char* str, PhaseGVN* gvn, NN... in) { + address call_addr = CAST_FROM_FN_PTR(address, SharedRuntime::debug_print); + return make_debug_print_call(str, call_addr, gvn, in...); + } +#endif }; #endif // SHARE_OPTO_COMPILE_HPP diff --git a/src/hotspot/share/opto/runtime.cpp b/src/hotspot/share/opto/runtime.cpp index 072b1384921..9de9fe5da03 100644 --- a/src/hotspot/share/opto/runtime.cpp +++ b/src/hotspot/share/opto/runtime.cpp @@ -1780,6 +1780,62 @@ static const TypeFunc* make_osr_end_Type() { return TypeFunc::make(domain, range); } +#ifndef PRODUCT +static void debug_print_convert_type(const Type** fields, int* argp, Node *parm) { + const BasicType bt = parm->bottom_type()->basic_type(); + fields[(*argp)++] = Type::get_const_basic_type(bt); + if (bt == T_LONG || bt == T_DOUBLE) { + fields[(*argp)++] = Type::HALF; + } +} + +static void update_arg_cnt(const Node* parm, int* arg_cnt) { + (*arg_cnt)++; + const BasicType bt = parm->bottom_type()->basic_type(); + if (bt == T_LONG || bt == T_DOUBLE) { + (*arg_cnt)++; + } +} + +const TypeFunc* OptoRuntime::debug_print_Type(Node* parm0, Node* parm1, + Node* parm2, Node* parm3, + Node* parm4, Node* parm5, + Node* parm6) { + int argcnt = 1; + if (parm0 != nullptr) { update_arg_cnt(parm0, &argcnt); + if (parm1 != nullptr) { update_arg_cnt(parm1, &argcnt); + if (parm2 != nullptr) { update_arg_cnt(parm2, &argcnt); + if (parm3 != nullptr) { update_arg_cnt(parm3, &argcnt); + if (parm4 != nullptr) { update_arg_cnt(parm4, &argcnt); + if (parm5 != nullptr) { update_arg_cnt(parm5, &argcnt); + if (parm6 != nullptr) { update_arg_cnt(parm6, &argcnt); + /* close each nested if ===> */ } } } } } } } + + // create input type (domain) + const Type** fields = TypeTuple::fields(argcnt); + int argp = TypeFunc::Parms; + fields[argp++] = TypePtr::NOTNULL; // static string pointer + + if (parm0 != nullptr) { debug_print_convert_type(fields, &argp, parm0); + if (parm1 != nullptr) { debug_print_convert_type(fields, &argp, parm1); + if (parm2 != nullptr) { debug_print_convert_type(fields, &argp, parm2); + if (parm3 != nullptr) { debug_print_convert_type(fields, &argp, parm3); + if (parm4 != nullptr) { debug_print_convert_type(fields, &argp, parm4); + if (parm5 != nullptr) { debug_print_convert_type(fields, &argp, parm5); + if (parm6 != nullptr) { debug_print_convert_type(fields, &argp, parm6); + /* close each nested if ===> */ } } } } } } } + + assert(argp == TypeFunc::Parms+argcnt, "correct decoding"); + const TypeTuple* domain = TypeTuple::make(TypeFunc::Parms+argcnt, fields); + + // no result type needed + fields = TypeTuple::fields(1); + fields[TypeFunc::Parms+0] = nullptr; // void + const TypeTuple* range = TypeTuple::make(TypeFunc::Parms, fields); + return TypeFunc::make(domain, range); +} +#endif // PRODUCT + //------------------------------------------------------------------------------------- // register policy diff --git a/src/hotspot/share/opto/runtime.hpp b/src/hotspot/share/opto/runtime.hpp index 40e436c0d5c..76e69fd9d36 100644 --- a/src/hotspot/share/opto/runtime.hpp +++ b/src/hotspot/share/opto/runtime.hpp @@ -737,6 +737,16 @@ private: return _dtrace_object_alloc_Type; } +#ifndef PRODUCT + // Signature for runtime calls in debug printing nodes, which depends on which nodes are actually passed + // Note: we do not allow more than 7 node arguments as GraphKit::make_runtime_call only allows 8, and we need + // one for the static string + static const TypeFunc* debug_print_Type(Node* parm0 = nullptr, Node* parm1 = nullptr, + Node* parm2 = nullptr, Node* parm3 = nullptr, + Node* parm4 = nullptr, Node* parm5 = nullptr, + Node* parm6 = nullptr); +#endif // PRODUCT + private: static NamedCounter * volatile _named_counters; diff --git a/src/hotspot/share/runtime/sharedRuntime.cpp b/src/hotspot/share/runtime/sharedRuntime.cpp index 85aeba361ff..a8c9a64d63a 100644 --- a/src/hotspot/share/runtime/sharedRuntime.cpp +++ b/src/hotspot/share/runtime/sharedRuntime.cpp @@ -264,6 +264,46 @@ void SharedRuntime::print_ic_miss_histogram() { tty->print_cr("Total IC misses: %7d", tot_misses); } } + +#ifdef COMPILER2 +// Runtime methods for printf-style debug nodes (same printing format as fieldDescriptor::print_on_for) +void SharedRuntime::debug_print_value(jboolean x) { + tty->print_cr("boolean %d", x); +} + +void SharedRuntime::debug_print_value(jbyte x) { + tty->print_cr("byte %d", x); +} + +void SharedRuntime::debug_print_value(jshort x) { + tty->print_cr("short %d", x); +} + +void SharedRuntime::debug_print_value(jchar x) { + tty->print_cr("char %c %d", isprint(x) ? x : ' ', x); +} + +void SharedRuntime::debug_print_value(jint x) { + tty->print_cr("int %d", x); +} + +void SharedRuntime::debug_print_value(jlong x) { + tty->print_cr("long " JLONG_FORMAT, x); +} + +void SharedRuntime::debug_print_value(jfloat x) { + tty->print_cr("float %f", x); +} + +void SharedRuntime::debug_print_value(jdouble x) { + tty->print_cr("double %lf", x); +} + +void SharedRuntime::debug_print_value(oopDesc* x) { + x->print(); +} +#endif // COMPILER2 + #endif // PRODUCT diff --git a/src/hotspot/share/runtime/sharedRuntime.hpp b/src/hotspot/share/runtime/sharedRuntime.hpp index 2a19b80c3b5..93cd92b3a32 100644 --- a/src/hotspot/share/runtime/sharedRuntime.hpp +++ b/src/hotspot/share/runtime/sharedRuntime.hpp @@ -32,6 +32,7 @@ #include "memory/allStatic.hpp" #include "memory/metaspaceClosure.hpp" #include "memory/resourceArea.hpp" +#include "runtime/safepointVerifiers.hpp" #include "runtime/stubInfo.hpp" #include "utilities/macros.hpp" @@ -635,6 +636,39 @@ class SharedRuntime: AllStatic { static void print_call_statistics(uint64_t comp_total); static void print_ic_miss_histogram(); +#ifdef COMPILER2 + // Runtime methods for printf-style debug nodes + static void debug_print_value(jboolean x); + static void debug_print_value(jbyte x); + static void debug_print_value(jshort x); + static void debug_print_value(jchar x); + static void debug_print_value(jint x); + static void debug_print_value(jlong x); + static void debug_print_value(jfloat x); + static void debug_print_value(jdouble x); + static void debug_print_value(oopDesc* x); + + template + static void debug_print_rec(T arg, Rest... args) { + debug_print_value(arg); + debug_print_rec(args...); + } + + static void debug_print_rec() {} + + // template is required here as we need to know the exact signature at compile-time + template + static void debug_print(const char *str, TT... args) { + // these three lines are the manual expansion of JRT_LEAF ... JRT_END, does not work well with templates + DEBUG_ONLY(NoHandleMark __hm;) + os::verify_stack_alignment(); + DEBUG_ONLY(NoSafepointVerifier __nsv;) + + tty->print_cr("%s", str); + debug_print_rec(args...); + } +#endif // COMPILER2 + #endif // PRODUCT static void print_statistics() PRODUCT_RETURN; From c06d6805aae3af2e6175f3f43deea46c9ce08bc6 Mon Sep 17 00:00:00 2001 From: Daniel Skantz Date: Tue, 7 Oct 2025 09:04:39 +0000 Subject: [PATCH 373/556] 8362394: C2: Repeated stacked string concatenation fails with "Hit MemLimit" and other resourcing errors Reviewed-by: chagedorn, rcastanedalo --- src/hotspot/share/opto/stringopts.cpp | 22 ++++- .../stringopts/TestStackedConcatsMany.java | 95 +++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 test/hotspot/jtreg/compiler/stringopts/TestStackedConcatsMany.java diff --git a/src/hotspot/share/opto/stringopts.cpp b/src/hotspot/share/opto/stringopts.cpp index 25aa82870c3..420423dd246 100644 --- a/src/hotspot/share/opto/stringopts.cpp +++ b/src/hotspot/share/opto/stringopts.cpp @@ -53,6 +53,11 @@ class StringConcat : public ResourceObj { Node_List _uncommon_traps; // Uncommon traps that needs to be rewritten // to restart at the initial JVMState. + static constexpr uint STACKED_CONCAT_UPPER_BOUND = 256; // argument limit for a merged concat. + // The value 256 was derived by measuring + // compilation time on variable length sequences + // of stackable concatenations and chosen to keep + // a safe margin to any critical point. public: // Mode for converting arguments to Strings enum { @@ -295,6 +300,8 @@ StringConcat* StringConcat::merge(StringConcat* other, Node* arg) { } assert(result->_control.contains(other->_end), "what?"); assert(result->_control.contains(_begin), "what?"); + + uint arguments_appended = 0; for (int x = 0; x < num_arguments(); x++) { Node* argx = argument_uncast(x); if (argx == arg) { @@ -303,8 +310,21 @@ StringConcat* StringConcat::merge(StringConcat* other, Node* arg) { for (int y = 0; y < other->num_arguments(); y++) { result->append(other->argument(y), other->mode(y)); } + arguments_appended += other->num_arguments(); } else { result->append(argx, mode(x)); + arguments_appended++; + } + // Check if this concatenation would result in an excessive number of arguments + // -- leading to high memory use, compilation time, and later, a large number of IR nodes + // -- and bail out in that case. + if (arguments_appended > STACKED_CONCAT_UPPER_BOUND) { +#ifndef PRODUCT + if (PrintOptimizeStringConcat) { + tty->print_cr("Merge candidate of length %d exceeds argument limit", arguments_appended); + } +#endif + return nullptr; } } result->set_allocation(other->_begin); @@ -680,7 +700,7 @@ PhaseStringOpts::PhaseStringOpts(PhaseGVN* gvn): #endif StringConcat* merged = sc->merge(other, arg); - if (merged->validate_control_flow() && merged->validate_mem_flow()) { + if (merged != nullptr && merged->validate_control_flow() && merged->validate_mem_flow()) { #ifndef PRODUCT AtomicAccess::inc(&_stropts_merged); if (PrintOptimizeStringConcat) { diff --git a/test/hotspot/jtreg/compiler/stringopts/TestStackedConcatsMany.java b/test/hotspot/jtreg/compiler/stringopts/TestStackedConcatsMany.java new file mode 100644 index 00000000000..dbc6e495594 --- /dev/null +++ b/test/hotspot/jtreg/compiler/stringopts/TestStackedConcatsMany.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8362394 + * @summary Test that repeated stacked string concatenations do not + * consume too many compilation resources. + * @requires vm.compiler2.enabled + * @library /test/lib / + * @run main/othervm -XX:-OptoScheduling compiler.stringopts.TestStackedConcatsMany + * @run main/othervm -XX:-TieredCompilation -Xcomp -XX:-OptoScheduling + * -XX:CompileOnly=compiler.stringopts.TestStackedConcatsMany::f + * compiler.stringopts.TestStackedConcatsMany + */ + +// The test uses -XX:-OptoScheduling to avoid the assert "too many D-U pinch points" on aarch64 (JDK-8328078). + +package compiler.stringopts; + +import jdk.test.lib.Asserts; + +public class TestStackedConcatsMany { + + public static void main (String... args) { + new StringBuilder(); // Trigger loading of the StringBuilder class. + String s = f(); + String z = "xy"; + for (int i = 0; i < 24; i++) { + z = z + z; + } + Asserts.assertEQ(s, z); + } + + static String f() { + String s = "xy"; + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + s = new StringBuilder().append(s).append(s).toString(); + s = new StringBuilder().append(s).append(s).toString(); + + return s; + } +} From aed9485bbb1d93063e5e5f60ed84bfb36053bdd1 Mon Sep 17 00:00:00 2001 From: Andrew Haley Date: Tue, 7 Oct 2025 10:09:23 +0000 Subject: [PATCH 374/556] 8368303: AlwaysAtomicAccesses is excessively strict Reviewed-by: shade, vlivanov, dlong --- .../share/gc/shared/c1/barrierSetC1.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/hotspot/share/gc/shared/c1/barrierSetC1.cpp b/src/hotspot/share/gc/shared/c1/barrierSetC1.cpp index ac640fb88d2..a31078f7e67 100644 --- a/src/hotspot/share/gc/shared/c1/barrierSetC1.cpp +++ b/src/hotspot/share/gc/shared/c1/barrierSetC1.cpp @@ -38,6 +38,16 @@ #define __ gen->lir()-> #endif +// Return true iff an access to bt is single-copy atomic. + +// The JMM requires atomicity for all accesses to fields of primitive +// types other than double and long. In practice, HotSpot assumes that +// on all processors, accesses to memory operands of wordSize and +// smaller are atomic. +static bool access_is_atomic(BasicType bt) { + return type2aelembytes(bt) <= wordSize; +} + LIR_Opr BarrierSetC1::resolve_address(LIRAccess& access, bool resolve_in_register) { DecoratorSet decorators = access.decorators(); bool is_array = (decorators & IS_ARRAY) != 0; @@ -140,7 +150,7 @@ LIR_Opr BarrierSetC1::atomic_add_at(LIRAccess& access, LIRItem& value) { void BarrierSetC1::store_at_resolved(LIRAccess& access, LIR_Opr value) { DecoratorSet decorators = access.decorators(); bool is_volatile = (decorators & MO_SEQ_CST) != 0; - bool is_atomic = is_volatile || AlwaysAtomicAccesses; + bool needs_atomic = AlwaysAtomicAccesses && !access_is_atomic(value->type()); bool needs_patching = (decorators & C1_NEEDS_PATCHING) != 0; bool mask_boolean = (decorators & C1_MASK_BOOLEAN) != 0; LIRGenerator* gen = access.gen(); @@ -154,7 +164,7 @@ void BarrierSetC1::store_at_resolved(LIRAccess& access, LIR_Opr value) { } LIR_PatchCode patch_code = needs_patching ? lir_patch_normal : lir_patch_none; - if (is_atomic && !needs_patching) { + if ((is_volatile || needs_atomic) && !needs_patching) { gen->volatile_field_store(value, access.resolved_addr()->as_address_ptr(), access.access_emit_info()); } else { __ store(value, access.resolved_addr()->as_address_ptr(), access.access_emit_info(), patch_code); @@ -169,7 +179,7 @@ void BarrierSetC1::load_at_resolved(LIRAccess& access, LIR_Opr result) { LIRGenerator *gen = access.gen(); DecoratorSet decorators = access.decorators(); bool is_volatile = (decorators & MO_SEQ_CST) != 0; - bool is_atomic = is_volatile || AlwaysAtomicAccesses; + bool needs_atomic = AlwaysAtomicAccesses && !access_is_atomic(result->type()); bool needs_patching = (decorators & C1_NEEDS_PATCHING) != 0; bool mask_boolean = (decorators & C1_MASK_BOOLEAN) != 0; bool in_native = (decorators & IN_NATIVE) != 0; @@ -181,7 +191,7 @@ void BarrierSetC1::load_at_resolved(LIRAccess& access, LIR_Opr result) { LIR_PatchCode patch_code = needs_patching ? lir_patch_normal : lir_patch_none; if (in_native) { __ move_wide(access.resolved_addr()->as_address_ptr(), result); - } else if (is_atomic && !needs_patching) { + } else if ((is_volatile || needs_atomic) && !needs_patching) { gen->volatile_field_load(access.resolved_addr()->as_address_ptr(), result, access.access_emit_info()); } else { __ load(access.resolved_addr()->as_address_ptr(), result, access.access_emit_info(), patch_code); From 6bec42adcc1d99e16ddd5148bb4012c74a0c3090 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Tue, 7 Oct 2025 10:21:33 +0000 Subject: [PATCH 375/556] 8368892: Make JEditorPane/TestBrowserBGColor.java headless Reviewed-by: serb, azvegint --- .../swing/JEditorPane/TestBrowserBGColor.java | 139 ++++++++---------- 1 file changed, 62 insertions(+), 77 deletions(-) diff --git a/test/jdk/javax/swing/JEditorPane/TestBrowserBGColor.java b/test/jdk/javax/swing/JEditorPane/TestBrowserBGColor.java index bff0f7c4aaa..9abe286a75d 100644 --- a/test/jdk/javax/swing/JEditorPane/TestBrowserBGColor.java +++ b/test/jdk/javax/swing/JEditorPane/TestBrowserBGColor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,94 +23,79 @@ /* * @test - * @key headful * @bug 8213781 * @summary Verify webpage background color renders correctly in JEditorPane */ -import java.awt.Toolkit; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.IOException; -import java.net.MalformedURLException; -import javax.swing.JDialog; -import javax.swing.JEditorPane; -import javax.swing.JFrame; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; -import javax.swing.event.HyperlinkEvent; -import javax.swing.event.HyperlinkListener; -import javax.swing.text.html.HTMLFrameHyperlinkEvent; -import javax.swing.text.html.HTMLDocument; import java.awt.Color; -import java.awt.Insets; -import java.awt.Point; -import java.awt.Robot; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import java.util.stream.IntStream; -public class TestBrowserBGColor extends JFrame implements HyperlinkListener { +import javax.imageio.ImageIO; +import javax.swing.JEditorPane; +import javax.swing.text.StyleConstants; +import javax.swing.text.View; - private static TestBrowserBGColor b; - private static JEditorPane browser; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; +import static java.lang.Integer.toHexString; + +public final class TestBrowserBGColor { + + private static final String HTML_DOC = + "" + + "" + + "" + + "Title" + + " "; + + private static final int SIZE = 300; public static void main(final String[] args) throws Exception { - Robot r = new Robot(); - SwingUtilities.invokeAndWait(() -> { - try { - b = new TestBrowserBGColor(); - } catch (Exception e) { - throw new RuntimeException(e); - } - b.setSize(Toolkit.getDefaultToolkit().getScreenSize()); - b.setVisible(true); - b.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - b.addWindowListener(new WindowAdapter() { - public void windowClosing(WindowEvent e) { - b.dispose(); - b = null; - } - }); - }); - - r.waitForIdle(); - r.delay(500); - - SwingUtilities.invokeAndWait(() -> { - Insets insets = browser.getInsets(); - Point loc = browser.getLocationOnScreen(); - Color c = r.getPixelColor( loc.x + insets.left+100, - loc.y + insets.top + 100); - b.dispose(); - if (!c.equals(Color.WHITE)) { - throw new RuntimeException("webpage background color wrong"); - } - }); - } - - - String htmlDoc = " Title "; - - public TestBrowserBGColor() throws IOException, MalformedURLException { - browser = new JEditorPane("text/html", htmlDoc); + JEditorPane browser = new JEditorPane("text/html", HTML_DOC); browser.setEditable(false); - browser.addHyperlinkListener(this); - JScrollPane scroll = new JScrollPane(browser); - getContentPane().add(scroll); + browser.setSize(SIZE, SIZE); + + BufferedImage image = new BufferedImage(SIZE, SIZE, TYPE_INT_RGB); + Graphics g = image.getGraphics(); + browser.paint(g); + g.dispose(); + + Color bgColor = StyleConstants.getBackground( + getBodyView(browser.getUI() + .getRootView(browser)) + .getAttributes()); + if (!bgColor.equals(Color.WHITE)) { + saveImage(image); + throw new RuntimeException("Wrong background color: " + + toHexString(bgColor.getRGB()) + + " vs " + + toHexString(Color.WHITE.getRGB())); + } } - public void hyperlinkUpdate(final HyperlinkEvent e) { - if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - JEditorPane pane = (JEditorPane) e.getSource(); - if (e instanceof HTMLFrameHyperlinkEvent) { - HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent) e; - HTMLDocument doc = (HTMLDocument) pane.getDocument(); - doc.processHTMLFrameHyperlinkEvent(evt); - } else { - try { - pane.setPage(e.getURL()); - } catch (Throwable t) { - t.printStackTrace(); - } - } + private static View getBodyView(final View view) { + if ("body".equals(view.getElement() + .getName())) { + return view; + } + + return IntStream.range(0, view.getViewCount()) + .mapToObj(view::getView) + .map(TestBrowserBGColor::getBodyView) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private static void saveImage(BufferedImage image) { + try { + ImageIO.write(image, "png", + new File("html-rendering.png")); + } catch (IOException ignored) { } } } From 9c46febcac01b9f1831f5f3e2a68dd1f1612a01f Mon Sep 17 00:00:00 2001 From: Yasumasa Suenaga Date: Tue, 7 Oct 2025 12:47:40 +0000 Subject: [PATCH 376/556] 8245234: Still seeing missing mixed stack traces, even after JDK-8234624 Reviewed-by: kevinw, cjplummer --- .../linux/amd64/LinuxAMD64CFrame.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/debugger/linux/amd64/LinuxAMD64CFrame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/debugger/linux/amd64/LinuxAMD64CFrame.java index 5f3c9786d6e..3dfb83c9f5a 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/debugger/linux/amd64/LinuxAMD64CFrame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/debugger/linux/amd64/LinuxAMD64CFrame.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -145,17 +145,12 @@ public final class LinuxAMD64CFrame extends BasicCFrame { } DwarfParser nextDwarf = null; - - if ((dwarf != null) && dwarf.isIn(nextPC)) { - nextDwarf = dwarf; - } else { - Address libptr = dbg.findLibPtrByAddress(nextPC); - if (libptr != null) { - try { - nextDwarf = new DwarfParser(libptr); - } catch (DebuggerException e) { - // Bail out to Java frame - } + Address libptr = dbg.findLibPtrByAddress(nextPC); + if (libptr != null) { + try { + nextDwarf = new DwarfParser(libptr); + } catch (DebuggerException e) { + // Bail out to Java frame } } From 4b4d0cd35a32448e4b056109c502af2765766432 Mon Sep 17 00:00:00 2001 From: Johny Jose Date: Tue, 7 Oct 2025 13:13:42 +0000 Subject: [PATCH 377/556] 8365398: TEST_BUG: java/rmi/transport/checkLeaseInfoLeak/CheckLeaseLeak.java failing intermittently Reviewed-by: msheppar, smarks, jpai --- test/jdk/ProblemList.txt | 1 - .../checkLeaseInfoLeak/CheckLeaseLeak.java | 25 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 8d13670805f..48a495bc238 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -608,7 +608,6 @@ java/rmi/transport/rapidExportUnexport/RapidExportUnexport.java 7146541 linux-al java/rmi/registry/readTest/CodebaseTest.java 8173324 windows-all java/rmi/registry/multipleRegistries/MultipleRegistries.java 8268182 macosx-all -java/rmi/transport/checkLeaseInfoLeak/CheckLeaseLeak.java 8365398 generic-all java/rmi/Naming/DefaultRegistryPort.java 8005619 windows-all java/rmi/Naming/legalRegistryNames/LegalRegistryNames.java 8005619 windows-all diff --git a/test/jdk/java/rmi/transport/checkLeaseInfoLeak/CheckLeaseLeak.java b/test/jdk/java/rmi/transport/checkLeaseInfoLeak/CheckLeaseLeak.java index 4de6598a0f4..f40502601d2 100644 --- a/test/jdk/java/rmi/transport/checkLeaseInfoLeak/CheckLeaseLeak.java +++ b/test/jdk/java/rmi/transport/checkLeaseInfoLeak/CheckLeaseLeak.java @@ -59,10 +59,13 @@ import java.io.*; import java.lang.reflect.*; import java.rmi.registry.*; import sun.rmi.transport.*; +import java.util.concurrent.CountDownLatch; public class CheckLeaseLeak extends UnicastRemoteObject implements LeaseLeak { public CheckLeaseLeak() throws RemoteException { } - public void ping () throws RemoteException { } + public void ping () throws RemoteException { + remoteCallsComplete.countDown(); + } /** * Id to fake the DGC_ID, so we can later get a reference to the @@ -74,6 +77,9 @@ public class CheckLeaseLeak extends UnicastRemoteObject implements LeaseLeak { private final static int numberPingCalls = 0; private final static int CHECK_INTERVAL = 400; private final static int LEASE_VALUE = 20; + private static final int NO_OF_CLIENTS = ITERATIONS; + private static final int GOOD_LUCK_FACTOR = 2; + private static CountDownLatch remoteCallsComplete = new CountDownLatch(NO_OF_CLIENTS); public static void main (String[] args) { CheckLeaseLeak leakServer = null; @@ -113,8 +119,14 @@ public class CheckLeaseLeak extends UnicastRemoteObject implements LeaseLeak { jvm.destroy(); } } + try { + remoteCallsComplete.await(); + System.out.println("remoteCallsComplete . . . "); + } catch (InterruptedException intEx) { + System.out.println("remoteCallsComplete.await interrupted . . . "); + } + Thread.sleep(NO_OF_CLIENTS * LEASE_VALUE * GOOD_LUCK_FACTOR); numLeft = getDGCLeaseTableSize(); - Thread.sleep(3000); } catch(Exception e) { TestLibrary.bomb("CheckLeaseLeak Error: ", e); @@ -125,8 +137,8 @@ public class CheckLeaseLeak extends UnicastRemoteObject implements LeaseLeak { } } - /* numLeft should be 4 - if 11 there is a problem. */ - if (numLeft > 4) { + /* numLeft should not be greater than 2 - if 11 there is a problem. */ + if (numLeft > 2) { TestLibrary.bomb("Too many objects in DGCImpl.leaseTable: "+ numLeft); } else { @@ -204,8 +216,9 @@ public class CheckLeaseLeak extends UnicastRemoteObject implements LeaseLeak { * objects if the LeaseInfo memory leak is not fixed. */ leaseTable = (Map) f.get(dgcImpl[0]); - - numLeaseInfosLeft = leaseTable.size(); + synchronized (leaseTable) { + numLeaseInfosLeft = leaseTable.size(); + } } catch(Exception e) { TestLibrary.bomb(e); From a9c93f865bb5438420bc4df278d211ff3af9a0ad Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Tue, 7 Oct 2025 13:40:19 +0000 Subject: [PATCH 378/556] 8369263: Parallel: Inline PSPromotionManager::push_depth Reviewed-by: iwalulya, shade, fandreuzzi --- src/hotspot/share/gc/parallel/psPromotionManager.hpp | 2 -- src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/hotspot/share/gc/parallel/psPromotionManager.hpp b/src/hotspot/share/gc/parallel/psPromotionManager.hpp index 7d3a1682519..9808a55335d 100644 --- a/src/hotspot/share/gc/parallel/psPromotionManager.hpp +++ b/src/hotspot/share/gc/parallel/psPromotionManager.hpp @@ -102,8 +102,6 @@ class PSPromotionManager { void process_array_chunk(PartialArrayState* state, bool stolen); void push_objArray(oop old_obj, oop new_obj); - void push_depth(ScannerTask task); - inline void promotion_trace_event(oop new_obj, Klass* klass, size_t obj_size, uint age, bool tenured, const PSPromotionLAB* lab); diff --git a/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp b/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp index 4c12a4c357f..fb58c22cf29 100644 --- a/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp +++ b/src/hotspot/share/gc/parallel/psPromotionManager.inline.hpp @@ -50,10 +50,6 @@ inline PSPromotionManager* PSPromotionManager::manager_array(uint index) { return &_manager_array[index]; } -inline void PSPromotionManager::push_depth(ScannerTask task) { - claimed_stack_depth()->push(task); -} - template inline void PSPromotionManager::claim_or_forward_depth(T* p) { assert(ParallelScavengeHeap::heap()->is_in(p), "pointer outside heap"); @@ -62,7 +58,7 @@ inline void PSPromotionManager::claim_or_forward_depth(T* p) { oop obj = CompressedOops::decode_not_null(heap_oop); assert(!PSScavenge::is_obj_in_to_space(obj), "revisiting object?"); Prefetch::write(obj->base_addr(), oopDesc::mark_offset_in_bytes()); - push_depth(ScannerTask(p)); + claimed_stack_depth()->push(ScannerTask(p)); } } From 0f2a95c15d7c1e3796660d786c9a72497dab5ab1 Mon Sep 17 00:00:00 2001 From: jonghoonpark Date: Tue, 7 Oct 2025 15:13:23 +0000 Subject: [PATCH 379/556] 8365782: Remove unnecessary inclusion of in jfrOSInterface.cpp Reviewed-by: ayang, tschatzl --- src/hotspot/share/jfr/periodic/jfrOSInterface.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/hotspot/share/jfr/periodic/jfrOSInterface.cpp b/src/hotspot/share/jfr/periodic/jfrOSInterface.cpp index 2d4b99d59ab..18b2d7c5785 100644 --- a/src/hotspot/share/jfr/periodic/jfrOSInterface.cpp +++ b/src/hotspot/share/jfr/periodic/jfrOSInterface.cpp @@ -32,8 +32,6 @@ #include "runtime/vm_version.hpp" #include "utilities/ostream.hpp" -#include // for environment variables - static JfrOSInterface* _instance = nullptr; JfrOSInterface& JfrOSInterface::instance() { @@ -81,10 +79,7 @@ class JfrOSInterface::JfrOSInterfaceImpl : public JfrCHeapObj { // os information int os_version(char** os_version) const; - // environment information - void generate_environment_variables_events(); - - // system processes information + // system processes information int system_processes(SystemProcess** system_processes, int* no_of_sys_processes); int network_utilization(NetworkInterface** network_interfaces); From 8a20656ed03aa26806c7b4a4e361999dea62aa79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Walln=C3=B6fer?= Date: Tue, 7 Oct 2025 15:16:08 +0000 Subject: [PATCH 380/556] 8367321: Fix CSS bugs in dark theme 8366942: Dark mode pages briefly blink before going dark Reviewed-by: nbenalla, liach --- .../doclets/formats/html/markup/Head.java | 3 +- .../formats/html/resources/script.js.template | 60 +++++++++++-------- .../formats/html/resources/stylesheet.css | 58 +++++++++--------- .../javadoc/doclet/testSearch/TestSearch.java | 3 +- 4 files changed, 69 insertions(+), 55 deletions(-) diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java index 2b6bfa77951..cda4bc9a5be 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java @@ -378,7 +378,8 @@ public class Head extends Content { mainBodyScript.append("const pathtoroot = ") .appendStringLiteral(ptrPath + "/") .append(";\n") - .append("loadScripts(document, 'script');"); + .append("loadScripts();\n") + .append("initTheme();\n"); } addScriptElement(head, DocPaths.JQUERY_JS); addScriptElement(head, DocPaths.JQUERY_UI_JS); diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template index b275323b2f5..b91f99b2c42 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template @@ -11,12 +11,13 @@ var typeSearchIndex; var memberSearchIndex; var tagSearchIndex; -var oddRowColor = "odd-row-color"; -var evenRowColor = "even-row-color"; -var sortAsc = "sort-asc"; -var sortDesc = "sort-desc"; -var tableTab = "table-tab"; -var activeTableTab = "active-table-tab"; +const oddRowColor = "odd-row-color"; +const evenRowColor = "even-row-color"; +const sortAsc = "sort-asc"; +const sortDesc = "sort-desc"; +const tableTab = "table-tab"; +const activeTableTab = "active-table-tab"; +const THEMES = Object.freeze(["theme-light", "theme-dark", "theme-os"]); const linkIcon = "##REPLACE:doclet.Link_icon##"; const linkToSection = "##REPLACE:doclet.Link_to_section##"; @@ -30,21 +31,20 @@ if (typeof hljs !== "undefined") { } } -function loadScripts(doc, tag) { - createElem(doc, tag, 'script-files/search.js'); - - createElem(doc, tag, 'module-search-index.js'); - createElem(doc, tag, 'package-search-index.js'); - createElem(doc, tag, 'type-search-index.js'); - createElem(doc, tag, 'member-search-index.js'); - createElem(doc, tag, 'tag-search-index.js'); +function loadScripts() { + createScript('script-files/search.js'); + createScript('module-search-index.js'); + createScript('package-search-index.js'); + createScript('type-search-index.js'); + createScript('member-search-index.js'); + createScript('tag-search-index.js'); } -function createElem(doc, tag, path) { - var script = doc.createElement(tag); - var scriptElement = doc.getElementsByTagName(tag)[0]; +function createScript(path) { + var script = document.createElement("script"); script.src = pathtoroot + path; - scriptElement.parentNode.insertBefore(script, scriptElement); + var firstScript = document.getElementsByTagName("script")[0]; + firstScript.parentNode.insertBefore(script, firstScript); } // Helper for making content containing release names comparable lexicographically @@ -312,21 +312,31 @@ function makeFilterWidget(sidebar, updateToc) { return sidebar; } +function getTheme() { + return localStorage.getItem('theme') || THEMES[0]; +} + +function initTheme() { + document.body.classList.add(getTheme()); +} + function setTopMargin() { // Dynamically set scroll margin to accomodate for draft header var headerHeight = Math.ceil(document.querySelector("header").offsetHeight); document.querySelector(":root") .style.setProperty("--nav-height", headerHeight + "px"); } + document.addEventListener("readystatechange", (e) => { if (document.readyState === "interactive") { setTopMargin(); - } - if (sessionStorage.getItem("sidebar") === "hidden") { - const sidebar = document.querySelector(".main-grid nav.toc"); - if (sidebar) sidebar.classList.add("hide-sidebar"); + if (sessionStorage.getItem("sidebar") === "hidden") { + const sidebar = document.querySelector(".main-grid nav.toc"); + if (sidebar) sidebar.classList.add("hide-sidebar"); + } } }); + document.addEventListener("DOMContentLoaded", function(e) { setTopMargin(); const subnav = document.querySelector("ol.sub-nav-list"); @@ -375,13 +385,16 @@ document.addEventListener("DOMContentLoaded", function(e) { themePanelVisible = false; } } + var currentTheme = getTheme(); themePanel.querySelectorAll("input").forEach(input => { input.removeAttribute("disabled"); + if (input.id === currentTheme) { + input.checked = true; + } input.addEventListener("change", e => { setTheme(e.target.value); }) }); - const THEMES = ["theme-light", "theme-dark", "theme-os"]; function setTheme(theme) { THEMES.forEach(t => { if (t !== theme) document.body.classList.remove(t); @@ -390,7 +403,6 @@ document.addEventListener("DOMContentLoaded", function(e) { localStorage.setItem("theme", theme); document.getElementById(theme).checked = true; } - setTheme(localStorage.getItem("theme") || THEMES[0]); makeFilterWidget(sidebar, updateToc); if (tocMenu) { navbar.appendChild(tocMenu); diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css index d61283dc677..4bb0fad4306 100644 --- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css +++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css @@ -73,7 +73,7 @@ body { --selected-link-color: #4a698a; /* Background colors for generated tables */ --table-header-color: #ebeff4; - --even-row-color: #ffffff; + --even-row-color: #fdfdfe; --odd-row-color: #f0f0f2; /* Text color for page title */ --title-color: #2c4557; @@ -109,17 +109,19 @@ body { /* Colors for invalid tag notifications */ --invalid-tag-background-color: #ffe6e6; --invalid-tag-text-color: #000000; + --icon-filter: none; + --caption-link-color: var(--subnav-link-color); } body.theme-dark { --body-text-color: #e8e8e8; --block-text-color: #e8e8e8; - --body-background-color: #222528; + --body-background-color: #1f2124; --section-background-color: var(--body-background-color); --detail-background-color: var(--body-background-color); --code-background-color: #303940; --mark-background-color: #313131; - --detail-block-color: #f4f4f4; + --detail-block-color: #31363c; --navbar-background-color: #395A6F; --navbar-text-color: #ffffff; --subnav-background-color: #3d454d; @@ -133,11 +135,11 @@ body.theme-dark { --odd-row-color: #2d3135; --title-color: #fff; --link-color: #94badb; - --link-color-active: #ffb45b; - --toc-background-color: #31363c; + --link-color-active: #e8a351; + --toc-background-color: #2f3439; --toc-highlight-color: var(--subnav-background-color); --toc-hover-color: #3f4146; - --snippet-background-color: #2d363c; + --snippet-background-color: #2c353b; --snippet-text-color: var(--block-text-color); --snippet-highlight-color: #f7c590; --pre-background-color: var(--snippet-background-color); @@ -155,10 +157,8 @@ body.theme-dark { --button-focus-filter: brightness(104%); --invalid-tag-background-color: #ffe6e6; --invalid-tag-text-color: #000000; - div.main-grid img, - .inherited-list h3 > button { - filter: invert(100%) brightness(160%); - } + --icon-filter: invert(100%) brightness(160%); + --caption-link-color: var(--link-color); } /* @@ -168,12 +168,12 @@ body.theme-dark { body { --body-text-color: #e8e8e8; --block-text-color: #e8e8e8; - --body-background-color: #222528; + --body-background-color: #1f2124; --section-background-color: var(--body-background-color); --detail-background-color: var(--body-background-color); --code-background-color: #303940; --mark-background-color: #313131; - --detail-block-color: #f4f4f4; + --detail-block-color: #31363c; --navbar-background-color: #395A6F; --navbar-text-color: #ffffff; --subnav-background-color: #3d454d; @@ -187,11 +187,11 @@ body.theme-dark { --odd-row-color: #2d3135; --title-color: #fff; --link-color: #94badb; - --link-color-active: #ffb45b; - --toc-background-color: #31363c; + --link-color-active: #e8a351; + --toc-background-color: #2f3439; --toc-highlight-color: var(--subnav-background-color); --toc-hover-color: #3f4146; - --snippet-background-color: #2d363c; + --snippet-background-color: #2c353b; --snippet-text-color: var(--block-text-color); --snippet-highlight-color: #f7c590; --pre-background-color: var(--snippet-background-color); @@ -209,15 +209,13 @@ body.theme-dark { --button-focus-filter: brightness(104%); --invalid-tag-background-color: #ffe6e6; --invalid-tag-text-color: #000000; - div.main-grid img, - .inherited-list h3 > button { - filter: invert(100%) brightness(160%); - } + --icon-filter: invert(100%) brightness(160%); + --caption-link-color: var(--link-color); } body.theme-light { - --body-text-color: #282828; - --block-text-color: #282828; + --body-text-color: #181818; + --block-text-color: #181818; --body-background-color: #ffffff; --section-background-color: var(--body-background-color); --detail-background-color: var(--body-background-color); @@ -233,7 +231,7 @@ body.theme-dark { --selected-text-color: #253441; --selected-link-color: #4a698a; --table-header-color: #ebeff4; - --even-row-color: #ffffff; + --even-row-color: #fdfdfe; --odd-row-color: #f0f0f2; --title-color: #2c4557; --link-color: #437291; @@ -259,10 +257,8 @@ body.theme-dark { --button-focus-filter: brightness(104%); --invalid-tag-background-color: #ffe6e6; --invalid-tag-text-color: #000000; - div.main-grid img, - .inherited-list h3 > button { - filter: none; - } + --icon-filter: none; + --caption-link-color: var(--subnav-link-color); } } /* @@ -288,6 +284,9 @@ div.main-grid { max-width: var(--max-content-width); margin: var(--content-margin); } +div.main-grid img { + filter: var(--icon-filter); +} a:link, a:visited { text-decoration:none; color:var(--link-color); @@ -909,12 +908,12 @@ ul.preview-feature-list input { .caption a:visited, .inherited-list h3 a:link, .inherited-list h3 a:visited { - color:var(--subnav-link-color); + color:var(--caption-link-color); } .caption a:hover, .caption a:active, -.inherited-list.expanded h3 a:hover, -.inherited-list.expanded h3 a:active { +.inherited-list h3 a:hover, +.inherited-list h3 a:active { color: var(--link-color-active); } div.table-tabs { @@ -1539,6 +1538,7 @@ section[class$="-details"] .detail > div { height: 1.6em; vertical-align: middle; top: -2px; + filter: var(--icon-filter); } .inherited-list h3:has(button) { padding-left: 2px; diff --git a/test/langtools/jdk/javadoc/doclet/testSearch/TestSearch.java b/test/langtools/jdk/javadoc/doclet/testSearch/TestSearch.java index 8615d9f1e64..00c2f017909 100644 --- a/test/langtools/jdk/javadoc/doclet/testSearch/TestSearch.java +++ b/test/langtools/jdk/javadoc/doclet/testSearch/TestSearch.java @@ -427,7 +427,8 @@ public class TestSearch extends JavadocTester { """, """ const pathtoroot = "./"; - loadScripts(document, 'script');""", + loadScripts(); + initTheme();""", "

        ", """
      • Search
      • """, From eb729f0aaa2297c3b3dbadadf40a502d2d9ed124 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Tue, 7 Oct 2025 15:38:58 +0000 Subject: [PATCH 381/556] 8247776: JFR: TestThreadContextSwitches.java failed "RuntimeException: No events: expected false, was true" Reviewed-by: mgronlun --- .../event/os/TestThreadContextSwitches.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/test/jdk/jdk/jfr/event/os/TestThreadContextSwitches.java b/test/jdk/jdk/jfr/event/os/TestThreadContextSwitches.java index 7b3dfc79ce9..acfaecdde7d 100644 --- a/test/jdk/jdk/jfr/event/os/TestThreadContextSwitches.java +++ b/test/jdk/jdk/jfr/event/os/TestThreadContextSwitches.java @@ -28,6 +28,7 @@ import jdk.jfr.Recording; import jdk.jfr.consumer.RecordedEvent; import jdk.test.lib.jfr.EventNames; import jdk.test.lib.jfr.Events; +import jdk.test.lib.Platform; /** * @test @@ -40,15 +41,25 @@ public class TestThreadContextSwitches { private final static String EVENT_NAME = EventNames.ThreadContextSwitchRate; public static void main(String[] args) throws Throwable { - Recording recording = new Recording(); - recording.enable(EVENT_NAME); - recording.start(); - recording.stop(); - List events = Events.fromRecording(recording); - Events.hasEvents(events); - for (RecordedEvent event : events) { - System.out.println("Event: " + event); - Events.assertField(event, "switchRate").atLeast(0.0f); + while (true) { + try (Recording recording = new Recording()) { + recording.enable(EVENT_NAME); + recording.start(); + recording.stop(); + List events = Events.fromRecording(recording); + if (!events.isEmpty()) { + for (RecordedEvent event : events) { + System.out.println("Event: " + event); + Events.assertField(event, "switchRate").atLeast(0.0f); + } + return; + } + // Thread context switch rate is unreliable on Windows because + // the way processes are identified with performance counters. + if (!Platform.isWindows()) { + Events.hasEvents(events); + } + } } } } From eb835e05f9cf8a65d804b733b382ecfba5b12907 Mon Sep 17 00:00:00 2001 From: Volkan Yazici Date: Tue, 7 Oct 2025 15:57:31 +0000 Subject: [PATCH 382/556] 8366040: Change URL.lookupViaProviders to use ScopedValue to detect recursive lookup Reviewed-by: alanb, dfuchs --- src/java.base/share/classes/java/net/URL.java | 22 ++------- .../spi/URLStreamHandlerProvider/Basic.java | 18 ++++++- .../circular.provider.template | 48 +++++++++++++++++++ 3 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 test/jdk/java/net/spi/URLStreamHandlerProvider/circular.provider.template diff --git a/src/java.base/share/classes/java/net/URL.java b/src/java.base/share/classes/java/net/URL.java index 1435d851f41..c82236b5b85 100644 --- a/src/java.base/share/classes/java/net/URL.java +++ b/src/java.base/share/classes/java/net/URL.java @@ -41,7 +41,6 @@ import java.util.ServiceLoader; import jdk.internal.access.JavaNetURLAccess; import jdk.internal.access.SharedSecrets; -import jdk.internal.misc.ThreadTracker; import jdk.internal.misc.VM; import jdk.internal.vm.annotation.AOTRuntimeSetup; import jdk.internal.vm.annotation.AOTSafeClassInitializer; @@ -1394,24 +1393,13 @@ public final class URL implements java.io.Serializable { return handler; } - private static class ThreadTrackHolder { - static final ThreadTracker TRACKER = new ThreadTracker(); - } - - private static Object tryBeginLookup() { - return ThreadTrackHolder.TRACKER.tryBegin(); - } - - private static void endLookup(Object key) { - ThreadTrackHolder.TRACKER.end(key); - } + private static final ScopedValue IN_LOOKUP = ScopedValue.newInstance(); private static URLStreamHandler lookupViaProviders(final String protocol) { - Object key = tryBeginLookup(); - if (key == null) { + if (IN_LOOKUP.isBound()) { throw new Error("Circular loading of URL stream handler providers detected"); } - try { + return ScopedValue.where(IN_LOOKUP, true).call(() -> { final ClassLoader cl = ClassLoader.getSystemClassLoader(); final ServiceLoader sl = ServiceLoader.load(URLStreamHandlerProvider.class, cl); @@ -1423,9 +1411,7 @@ public final class URL implements java.io.Serializable { return h; } return null; - } finally { - endLookup(key); - } + }); } /** diff --git a/test/jdk/java/net/spi/URLStreamHandlerProvider/Basic.java b/test/jdk/java/net/spi/URLStreamHandlerProvider/Basic.java index 9f8381a92c8..a98bf8e129e 100644 --- a/test/jdk/java/net/spi/URLStreamHandlerProvider/Basic.java +++ b/test/jdk/java/net/spi/URLStreamHandlerProvider/Basic.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -77,6 +77,7 @@ public class Basic { viaProvider("bert", KNOWN); viaBadProvider("tom", SCE); viaBadProvider("jerry", SCE); + viaCircularProvider("circular", CIRCULAR); } private static String withoutWarning(String in) { @@ -99,6 +100,12 @@ public class Basic { throw new RuntimeException("exitValue: "+ r.exitValue + ", output:[" +r.output +"]"); } }; + static final Consumer CIRCULAR = r -> { + if (r.exitValue == 0 || + !r.output.contains("Circular loading of URL stream handler providers detected")) { + throw new RuntimeException("exitValue: " + r.exitValue + ", output:[" + r.output + "]"); + } + }; static void unknownProtocol(String protocol, Consumer resultChecker) { System.out.println("\nTesting " + protocol); @@ -125,6 +132,15 @@ public class Basic { sysProps); } + static void viaCircularProvider(String protocol, Consumer resultChecker, + String... sysProps) + throws Exception + { + viaProviderWithTemplate(protocol, resultChecker, + TEST_SRC.resolve("circular.provider.template"), + sysProps); + } + static void viaProviderWithTemplate(String protocol, Consumer resultChecker, Path template, String... sysProps) diff --git a/test/jdk/java/net/spi/URLStreamHandlerProvider/circular.provider.template b/test/jdk/java/net/spi/URLStreamHandlerProvider/circular.provider.template new file mode 100644 index 00000000000..b846cc4fec6 --- /dev/null +++ b/test/jdk/java/net/spi/URLStreamHandlerProvider/circular.provider.template @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package $package; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.spi.URLStreamHandlerProvider; + +public final class Provider extends URLStreamHandlerProvider { + + private static final String PROTOCOL = "$protocol"; + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + try { + // Trigger circular lookup + URI.create("bogus://path/to/nothing").toURL(); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + throw new AssertionError("Should not have reached here!"); + } + +} From 4ca3ab62759b366fd3e0b2267925f1fa70f057b7 Mon Sep 17 00:00:00 2001 From: Joe Darcy Date: Tue, 7 Oct 2025 16:41:45 +0000 Subject: [PATCH 383/556] 8369123: Still more small Float16 refactorings Reviewed-by: rgiulietti --- .../classes/jdk/incubator/vector/Float16.java | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java index a564cdfed0f..fe2a6bf5580 100644 --- a/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java +++ b/src/jdk.incubator.vector/share/classes/jdk/incubator/vector/Float16.java @@ -35,6 +35,8 @@ import static jdk.incubator.vector.Float16Consts.SIGN_BIT_MASK; import static jdk.incubator.vector.Float16Consts.EXP_BIT_MASK; import static jdk.incubator.vector.Float16Consts.SIGNIF_BIT_MASK; import static jdk.incubator.vector.Float16Consts.MAG_BIT_MASK; +import static jdk.incubator.vector.Float16Consts.EXP_BIAS; +import static jdk.incubator.vector.Float16Consts.SIGNIFICAND_WIDTH; import static java.lang.Float.float16ToFloat; import static java.lang.Float.floatToFloat16; @@ -95,19 +97,26 @@ import jdk.internal.vm.vector.Float16Math; * IEEE Standard for Floating-Point Arithmetic */ -// Currently Float16 is a value-based class and in future it is +// Currently Float16 is a value-based class and in the future it is // expected to be aligned with Value Classes and Object as described in // JEP-401 (https://openjdk.org/jeps/401). @jdk.internal.ValueBased public final class Float16 extends Number implements Comparable { - /** @serial */ + + /** + * Primitive {@code short} field to hold the bits of the {@code Float16}. + * @serial + */ private final short value; + private static final long serialVersionUID = 16; // May not be needed when a value class? // Functionality for future consideration: - // IEEEremainder / remainder operator remainder + // IEEEremainder and separate % operator remainder (which are + // defined to use different rounding modes, see JLS sections 15.4 + // and 15.17.3). // Do *not* define any public constructors /** @@ -147,8 +156,14 @@ public final class Float16 */ public static final Float16 NaN = valueOf(Float.NaN); + /** + * A constant holding a zero (0.0) of type {@code Float16}. + */ private static final Float16 ZERO = valueOf(0); + /** + * A constant holding a one (1.0) of type {@code Float16}. + */ private static final Float16 ONE = valueOf(1); /** @@ -316,10 +331,10 @@ public final class Float16 * @param value a {@code long} value. */ public static Float16 valueOf(long value) { - if (value <= -65_520L) { // -(Float16.MAX_VALUE + Float16.ulp(Float16.MAX_VALUE) / 2) + if (value <= -65_520L) { // -(MAX_VALUE + ulp(MAX_VALUE) / 2) return NEGATIVE_INFINITY; } else { - if (value >= 65_520L) { // Float16.MAX_VALUE + Float16.ulp(Float16.MAX_VALUE) / 2 + if (value >= 65_520L) { // MAX_VALUE + ulp(MAX_VALUE) / 2 return POSITIVE_INFINITY; } // Remaining range of long, the integers in approx. +/- @@ -427,9 +442,8 @@ public final class Float16 // to implement a carry out from rounding the significand. assert (0xf800 & signif_bits) == 0x0; - // Exponent bias adjust in the representation is equal to MAX_EXPONENT. return new Float16((short)(sign_bit | - ( ((exp + MAX_EXPONENT) << (PRECISION - 1)) + signif_bits ) )); + ( ((exp + EXP_BIAS) << (PRECISION - 1)) + signif_bits) )); } /** @@ -468,7 +482,7 @@ public final class Float16 // characters rather than codepoints. if (trialResult == 0.0 // handles signed zeros - || Math.abs(trialResult) > (65504.0 + 32.0) || // Float.MAX_VALUE + ulp(MAX_VALUE), + || Math.abs(trialResult) > (65504.0 + 32.0) || // MAX_VALUE + ulp(MAX_VALUE), // handles infinities too Double.isNaN(trialResult) || noDoubleRoundingToFloat16(trialResult)) { @@ -899,7 +913,7 @@ public final class Float16 */ public static int hashCode(Float16 value) { // Use bit-pattern of canonical NaN for hashing. - Float16 f16 = isNaN(value) ? Float16.NaN : value; + Float16 f16 = isNaN(value) ? NaN : value; return (int)float16ToRawShortBits(f16); } @@ -946,7 +960,7 @@ public final class Float16 */ public static short float16ToShortBits(Float16 f16) { if (isNaN(f16)) { - return Float16.NaN.value; + return NaN.value; } return f16.value; } @@ -1531,8 +1545,8 @@ public final class Float16 */ /*package*/ static int getExponent0(short bits) { // package private to be usable in java.lang.Float. - int bin16ExpBits = 0x0000_7c00 & bits; // Five exponent bits. - return (bin16ExpBits >> (PRECISION - 1)) - 15; + int bin16ExpBits = EXP_BIT_MASK & bits; // Five exponent bits. + return (bin16ExpBits >> (PRECISION - 1)) - EXP_BIAS; } /** @@ -1563,10 +1577,10 @@ public final class Float16 int exp = getExponent(f16); return switch(exp) { - case MAX_EXPONENT + 1 -> abs(f16); // NaN or infinity - case MIN_EXPONENT - 1 -> Float16.MIN_VALUE; // zero or subnormal + case MAX_EXPONENT + 1 -> abs(f16); // NaN or infinity + case MIN_EXPONENT - 1 -> MIN_VALUE; // zero or subnormal default -> { - assert exp <= MAX_EXPONENT && exp >= MIN_EXPONENT; + assert exp <= MAX_EXPONENT && exp >= MIN_EXPONENT: "Out of range exponent"; // ulp(x) is usually 2^(SIGNIFICAND_WIDTH-1)*(2^ilogb(x)) // Let float -> float16 conversion handle encoding issues. yield scalb(ONE, exp - (PRECISION - 1)); @@ -1687,8 +1701,7 @@ public final class Float16 // nonzero value by it would be guaranteed to over or // underflow; due to rounding, scaling down takes an // additional power of two which is reflected here - final int MAX_SCALE = Float16.MAX_EXPONENT + -Float16.MIN_EXPONENT + - Float16Consts.SIGNIFICAND_WIDTH + 1; + final int MAX_SCALE = MAX_EXPONENT + -MIN_EXPONENT + SIGNIFICAND_WIDTH + 1; // Make sure scaling factor is in a reasonable range scaleFactor = Math.clamp(scaleFactor, -MAX_SCALE, MAX_SCALE); @@ -1725,9 +1738,8 @@ public final class Float16 * @see Math#copySign(double, double) */ public static Float16 copySign(Float16 magnitude, Float16 sign) { - return shortBitsToFloat16((short) ((float16ToRawShortBits(sign) & SIGN_BIT_MASK) | - (float16ToRawShortBits(magnitude) & - (EXP_BIT_MASK | SIGNIF_BIT_MASK) ))); + return shortBitsToFloat16((short)((float16ToRawShortBits(sign) & SIGN_BIT_MASK) | + (float16ToRawShortBits(magnitude) & MAG_BIT_MASK))); } /** From ebeb77baaeb6d9098d7462f5ddf61d8583b1e493 Mon Sep 17 00:00:00 2001 From: Phil Race Date: Tue, 7 Oct 2025 16:47:43 +0000 Subject: [PATCH 384/556] 8358058: sun/java2d/OpenGL/DrawImageBg.java Test fails intermittently Reviewed-by: azvegint, serb, psadhukhan --- test/jdk/ProblemList.txt | 1 + .../sun/java2d/OpenGL/DrawBitmaskImage.java | 173 ++++++ test/jdk/sun/java2d/OpenGL/DrawBufImgOp.java | 514 ++++++++++++++++++ test/jdk/sun/java2d/OpenGL/DrawImageBg.java | 145 +++++ test/jdk/sun/java2d/OpenGL/LargeOps.java | 138 +++++ test/jdk/sun/java2d/OpenGL/OpaqueDest.java | 180 ++++++ .../jdk/sun/java2d/OpenGL/ScaleParamsOOB.java | 198 +++++++ test/jdk/sun/java2d/OpenGL/ShapeClip.java | 140 +++++ test/jdk/sun/java2d/OpenGL/SrcMaskOps.java | 188 +++++++ .../sun/java2d/OpenGL/VolatileSubRegion.java | 166 ++++++ test/jdk/sun/java2d/OpenGL/XformVolatile.java | 146 +++++ 11 files changed, 1989 insertions(+) create mode 100644 test/jdk/sun/java2d/OpenGL/DrawBitmaskImage.java create mode 100644 test/jdk/sun/java2d/OpenGL/DrawBufImgOp.java create mode 100644 test/jdk/sun/java2d/OpenGL/DrawImageBg.java create mode 100644 test/jdk/sun/java2d/OpenGL/LargeOps.java create mode 100644 test/jdk/sun/java2d/OpenGL/OpaqueDest.java create mode 100644 test/jdk/sun/java2d/OpenGL/ScaleParamsOOB.java create mode 100644 test/jdk/sun/java2d/OpenGL/ShapeClip.java create mode 100644 test/jdk/sun/java2d/OpenGL/SrcMaskOps.java create mode 100644 test/jdk/sun/java2d/OpenGL/VolatileSubRegion.java create mode 100644 test/jdk/sun/java2d/OpenGL/XformVolatile.java diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 48a495bc238..fce7fe85069 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -248,6 +248,7 @@ sun/awt/datatransfer/SuplementaryCharactersTransferTest.java 8011371 generic-all sun/awt/shell/ShellFolderMemoryLeak.java 8197794 windows-all sun/java2d/DirectX/OverriddenInsetsTest/OverriddenInsetsTest.java 8196102 generic-all sun/java2d/DirectX/RenderingToCachedGraphicsTest/RenderingToCachedGraphicsTest.java 8196180 windows-all,macosx-all +sun/java2d/OpenGL/OpaqueDest.java#id1 8367574 macosx-all sun/java2d/SunGraphics2D/EmptyClipRenderingTest.java 8144029 macosx-all,linux-all sun/java2d/SunGraphics2D/DrawImageBilinear.java 8297175 linux-all sun/java2d/SunGraphics2D/PolyVertTest.java 6986565 generic-all diff --git a/test/jdk/sun/java2d/OpenGL/DrawBitmaskImage.java b/test/jdk/sun/java2d/OpenGL/DrawBitmaskImage.java new file mode 100644 index 00000000000..d2730593b5b --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/DrawBitmaskImage.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 6248561 6264014 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that bitmask image copies work properly with the + * OGL pipeline when a SrcOver composite with extra alpha is involved. + * @run main/othervm -Dsun.java2d.opengl=True DrawBitmaskImage + * @run main/othervm DrawBitmaskImage + */ + +/* + * @test + * @bug 6248561 6264014 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that bitmask image copies work properly with the + * OGL pipeline when a SrcOver composite with extra alpha is involved. + * @run main/othervm -Dsun.java2d.opengl=True DrawBitmaskImage + * @run main/othervm DrawBitmaskImage + */ + +import java.awt.AlphaComposite; +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Transparency; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.File; +import javax.imageio.ImageIO; + +public class DrawBitmaskImage extends Panel { + + static final int TESTW = 200, TESTH = 200; + private static volatile DrawBitmaskImage test; + private static volatile Frame frame; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + g2d.setColor(Color.black); + g2d.fillRect(0, 0, getWidth(), getHeight()); + g2d.setComposite(AlphaComposite.SrcOver.derive(0.50f)); + + BufferedImage img = getGraphicsConfiguration().createCompatibleImage(50, 50, + Transparency.BITMASK); + Graphics2D gimg = img.createGraphics(); + gimg.setComposite(AlphaComposite.Src); + gimg.setColor(new Color(0, 0, 0, 0)); + gimg.fillRect(0, 0, 50, 50); + gimg.setColor(Color.red); + gimg.fillRect(10, 10, 30, 30); + gimg.dispose(); + + + g2d.drawImage(img, 10, 10, null); + + // draw a second time to ensure that the cached copy is used + g2d.drawImage(img, 80, 10, null); + } + + public Dimension getPreferredSize() { + return new Dimension(TESTW, TESTH); + } + + static void createUI() { + test = new DrawBitmaskImage(); + frame = new Frame("OpenGL DrawBitmaskImage Test"); + Panel p = new Panel(); + p.add(test); + frame.add(p); + frame.setSize(TESTW+100, TESTH+100); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + Robot robot = new Robot(); + + EventQueue.invokeAndWait(DrawBitmaskImage::createUI); + + robot.waitForIdle(); + robot.delay(2000); + + BufferedImage capture = null; + try { + GraphicsConfiguration gc = frame.getGraphicsConfiguration(); + if (gc.getColorModel() instanceof IndexColorModel) { + System.out.println("IndexColorModel detected: " + + "test considered PASSED"); + return; + } + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, TESTW, TESTH); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + // Test background color + int pixel = capture.getRGB(5, 10); + if (pixel != 0xff000000) { + saveImage(capture); + throw new RuntimeException("Failed: Incorrect color for " + + "background (actual=" + + Integer.toHexString(pixel) + ")"); + } + + // Test pixels (allow for small error in the actual red value) + pixel = capture.getRGB(25, 25); + System.out.println("pixel1 is " + Integer.toHexString(pixel)); + + if ((pixel < 0xff7e0000) || (pixel > 0xff900000)) { + saveImage(capture); + throw new RuntimeException("Failed: Incorrect color for " + + "first pixel (actual=" + + Integer.toHexString(pixel) + ")"); + } + + pixel = capture.getRGB(95, 25); + System.out.println("pixel2 is " + Integer.toHexString(pixel)); + if ((pixel < 0xff7e0000) || (pixel > 0xff900000)) { + saveImage(capture); + throw new RuntimeException("Failed: Incorrect color for " + + "second pixel (actual=" + + Integer.toHexString(pixel) + ")"); + } + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/DrawBufImgOp.java b/test/jdk/sun/java2d/OpenGL/DrawBufImgOp.java new file mode 100644 index 00000000000..5d60eb7e792 --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/DrawBufImgOp.java @@ -0,0 +1,514 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 6514990 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that calling + * Graphics2D.drawImage(BufferedImage, BufferedImageOp, x, y) to an + * OpenGL-accelerated destination produces the same results when performed + * in software via BufferedImageOp.filter(). + * @run main/othervm -Dsun.java2d.opengl=True DrawBufImgOp -ignore + */ + +/* + * @test + * @bug 6514990 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that calling + * Graphics2D.drawImage(BufferedImage, BufferedImageOp, x, y) to an + * OpenGL-accelerated destination produces the same results when performed + * in software via BufferedImageOp.filter(). + * @run main/othervm -Dsun.java2d.opengl=True DrawBufImgOp -ignore + */ + +import java.awt.AlphaComposite; +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.awt.image.ByteLookupTable; +import java.awt.image.ColorModel; +import java.awt.image.ConvolveOp; +import java.awt.image.IndexColorModel; +import java.awt.image.Kernel; +import java.awt.image.LookupOp; +import java.awt.image.RescaleOp; +import java.awt.image.ShortLookupTable; +import java.awt.image.VolatileImage; +import java.io.File; +import javax.imageio.ImageIO; + +/** + * REMIND: This testcase was originally intended to automatically compare + * the results of the software BufferedImageOp implementations against + * the OGL-accelerated codepaths. However, there are just too many open + * bugs in the mediaLib-based codepaths (see below), which means that + * creating the reference image may cause crashes or exceptions, + * and even if we work around those cases using the "-ignore" flag, + * the visual results of the reference image are often buggy as well + * (so the comparison will fail even though the OGL results are correct). + * Therefore, for now we will run the testcase with the "-ignore" flag + * but without the "-compare" flag, so at least it will be checking for + * any exceptions/crashes in the OGL code. When we fix all of the + * outstanding bugs with the software codepaths, we can remove the + * "-ignore" flag and maybe even restore the "-compare" flag. In the + * meantime, it also functions well as a manual testcase (with either + * the "-show" or "-dump" options). + */ +public class DrawBufImgOp extends Canvas { + + private static final int TESTW = 600; + private static final int TESTH = 500; + + private static volatile DrawBufImgOp test; + private static volatile Frame frame; + + /* + * If true, skips tests that are known to trigger bugs (which in + * turn may cause crashes, exceptions, or other artifacts). + */ + private static boolean ignore; + + // Test both pow2 and non-pow2 sized images + private static final int[] srcSizes = { 32, 17 }; + private static final int[] srcTypes = { + BufferedImage.TYPE_INT_RGB, + BufferedImage.TYPE_INT_ARGB, + BufferedImage.TYPE_INT_ARGB_PRE, + BufferedImage.TYPE_INT_BGR, + BufferedImage.TYPE_3BYTE_BGR, + BufferedImage.TYPE_4BYTE_ABGR, + BufferedImage.TYPE_USHORT_565_RGB, + BufferedImage.TYPE_BYTE_GRAY, + BufferedImage.TYPE_USHORT_GRAY, + }; + + private static final RescaleOp + rescale1band, rescale3band, rescale4band; + private static final LookupOp + lookup1bandbyte, lookup3bandbyte, lookup4bandbyte; + private static final LookupOp + lookup1bandshort, lookup3bandshort, lookup4bandshort; + private static final ConvolveOp + convolve3x3zero, convolve5x5zero, convolve7x7zero; + private static final ConvolveOp + convolve3x3noop, convolve5x5noop, convolve7x7noop; + + static { + rescale1band = new RescaleOp(0.5f, 10.0f, null); + rescale3band = new RescaleOp( + new float[] { 0.6f, 0.4f, 0.6f }, + new float[] { 10.0f, -3.0f, 5.0f }, + null); + rescale4band = new RescaleOp( + new float[] { 0.6f, 0.4f, 0.6f, 0.9f }, + new float[] { -1.0f, 5.0f, 3.0f, 1.0f }, + null); + + // REMIND: we should probably test non-zero offsets, but that + // would require massaging the source image data to avoid going + // outside the lookup table array bounds + int offset = 0; + { + byte invert[] = new byte[256]; + byte halved[] = new byte[256]; + for (int j = 0; j < 256 ; j++) { + invert[j] = (byte) (255-j); + halved[j] = (byte) (j / 2); + } + ByteLookupTable lut1 = new ByteLookupTable(offset, invert); + lookup1bandbyte = new LookupOp(lut1, null); + ByteLookupTable lut3 = + new ByteLookupTable(offset, + new byte[][] {invert, halved, invert}); + lookup3bandbyte = new LookupOp(lut3, null); + ByteLookupTable lut4 = + new ByteLookupTable(offset, + new byte[][] {invert, halved, invert, halved}); + lookup4bandbyte = new LookupOp(lut4, null); + } + + { + short invert[] = new short[256]; + short halved[] = new short[256]; + for (int j = 0; j < 256 ; j++) { + invert[j] = (short) ((255-j) * 255); + halved[j] = (short) ((j / 2) * 255); + } + ShortLookupTable lut1 = new ShortLookupTable(offset, invert); + lookup1bandshort = new LookupOp(lut1, null); + ShortLookupTable lut3 = + new ShortLookupTable(offset, + new short[][] {invert, halved, invert}); + lookup3bandshort = new LookupOp(lut3, null); + ShortLookupTable lut4 = + new ShortLookupTable(offset, + new short[][] {invert, halved, invert, halved}); + lookup4bandshort = new LookupOp(lut4, null); + } + + // 3x3 blur + float[] data3 = { + 0.1f, 0.1f, 0.1f, + 0.1f, 0.2f, 0.1f, + 0.1f, 0.1f, 0.1f, + }; + Kernel k3 = new Kernel(3, 3, data3); + + // 5x5 edge + float[] data5 = { + -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, + -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, + -1.0f, -1.0f, 24.0f, -1.0f, -1.0f, + -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, + -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, + }; + Kernel k5 = new Kernel(5, 5, data5); + + // 7x7 blur + float[] data7 = { + 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, + 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, + 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, + 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, + 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, + 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, + 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, 0.02f, + }; + Kernel k7 = new Kernel(7, 7, data7); + + convolve3x3zero = new ConvolveOp(k3, ConvolveOp.EDGE_ZERO_FILL, null); + convolve5x5zero = new ConvolveOp(k5, ConvolveOp.EDGE_ZERO_FILL, null); + convolve7x7zero = new ConvolveOp(k7, ConvolveOp.EDGE_ZERO_FILL, null); + + convolve3x3noop = new ConvolveOp(k3, ConvolveOp.EDGE_NO_OP, null); + convolve5x5noop = new ConvolveOp(k5, ConvolveOp.EDGE_NO_OP, null); + convolve7x7noop = new ConvolveOp(k7, ConvolveOp.EDGE_NO_OP, null); + } + + public void paint(Graphics g) { + + VolatileImage vimg = createVolatileImage(TESTW, TESTH); + vimg.validate(getGraphicsConfiguration()); + + Graphics2D g2d = vimg.createGraphics(); + renderTest(g2d); + g2d.dispose(); + + g.drawImage(vimg, 0, 0, null); + } + + /* + * foreach source image size (once with pow2, once with non-pow2) + * + * foreach BufferedImage type + * + * RescaleOp (1 band) + * RescaleOp (3 bands, if src has 3 bands) + * RescaleOp (4 bands, if src has 4 bands) + * + * foreach LookupTable type (once with ByteLUT, once with ShortLUT) + * LookupOp (1 band) + * LookupOp (3 bands, if src has 3 bands) + * LookupOp (4 bands, if src has 4 bands) + * + * foreach edge condition (once with ZERO_FILL, once with EDGE_NO_OP) + * ConvolveOp (3x3) + * ConvolveOp (5x5) + * ConvolveOp (7x7) + */ + private void renderTest(Graphics2D g2d) { + g2d.setColor(Color.white); + g2d.fillRect(0, 0, TESTW, TESTH); + + int yorig = 2; + int xinc = 34; + int yinc = srcSizes[0] + srcSizes[1] + 2 + 2; + + for (int srcType : srcTypes) { + int y = yorig; + + for (int srcSize : srcSizes) { + int x = 2; + System.out.printf("type=%d size=%d\n", srcType, srcSize); + + BufferedImage srcImg = makeSourceImage(srcSize, srcType); + ColorModel srcCM = srcImg.getColorModel(); + + // RescaleOp + g2d.drawImage(srcImg, rescale1band, x, y); + x += xinc; + // REMIND: 3-band RescaleOp.filter() throws IAE for images + // that contain an alpha channel (bug to be filed) + if (srcCM.getNumColorComponents() == 3 && + !(ignore && srcCM.hasAlpha())) + { + g2d.drawImage(srcImg, rescale3band, x, y); + } + x += xinc; + if (srcCM.getNumComponents() == 4) { + g2d.drawImage(srcImg, rescale4band, x, y); + } + x += xinc; + + // LookupOp + // REMIND: Our LUTs are only 256 elements long, so won't + // currently work with USHORT_GRAY data + if (srcType != BufferedImage.TYPE_USHORT_GRAY) { + g2d.drawImage(srcImg, lookup1bandbyte, x, y); + x += xinc; + if (srcCM.getNumColorComponents() == 3) { + g2d.drawImage(srcImg, lookup3bandbyte, x, y); + } + x += xinc; + if (srcCM.getNumComponents() == 4) { + g2d.drawImage(srcImg, lookup4bandbyte, x, y); + } + x += xinc; + + // REMIND: LookupOp.createCompatibleDestImage() throws + // IAE for 3BYTE_BGR/4BYTE_ABGR (bug to be filed) + if (!(ignore && + (srcType == BufferedImage.TYPE_3BYTE_BGR || + srcType == BufferedImage.TYPE_4BYTE_ABGR))) + { + g2d.drawImage(srcImg, lookup1bandshort, x, y); + x += xinc; + // REMIND: 3-band LookupOp.filter() throws IAE for + // images that contain an alpha channel + // (bug to be filed) + if (srcCM.getNumColorComponents() == 3 && + !(ignore && srcCM.hasAlpha())) + { + g2d.drawImage(srcImg, lookup3bandshort, x, y); + } + x += xinc; + if (srcCM.getNumComponents() == 4) { + g2d.drawImage(srcImg, lookup4bandshort, x, y); + } + x += xinc; + } else { + x += 3*xinc; + } + } else { + x += 6*xinc; + } + + // ConvolveOp + // REMIND: ConvolveOp.filter() throws ImagingOpException + // for 3BYTE_BGR (see 4957775) + if (srcType != BufferedImage.TYPE_3BYTE_BGR) { + g2d.drawImage(srcImg, convolve3x3zero, x, y); + x += xinc; + g2d.drawImage(srcImg, convolve5x5zero, x, y); + x += xinc; + g2d.drawImage(srcImg, convolve7x7zero, x, y); + x += xinc; + + g2d.drawImage(srcImg, convolve3x3noop, x, y); + x += xinc; + g2d.drawImage(srcImg, convolve5x5noop, x, y); + x += xinc; + g2d.drawImage(srcImg, convolve7x7noop, x, y); + x += xinc; + } else { + x += 6*xinc; + } + + y += srcSize + 2; + } + + yorig += yinc; + } + } + + private BufferedImage makeSourceImage(int size, int type) { + int s2 = size/2; + BufferedImage img = new BufferedImage(size, size, type); + Graphics2D g2d = img.createGraphics(); + g2d.setComposite(AlphaComposite.Src); + g2d.setColor(Color.orange); + g2d.fillRect(0, 0, size, size); + g2d.setColor(Color.red); + g2d.fillRect(0, 0, s2, s2); + g2d.setColor(Color.green); + g2d.fillRect(s2, 0, s2, s2); + g2d.setColor(Color.blue); + g2d.fillRect(0, s2, s2, s2); + g2d.setColor(new Color(255, 255, 0, 128)); + g2d.fillRect(s2, s2, s2, s2); + g2d.setColor(Color.pink); + g2d.fillOval(s2-3, s2-3, 6, 6); + g2d.dispose(); + return img; + } + + public BufferedImage makeReferenceImage() { + BufferedImage img = new BufferedImage(TESTW, TESTH, + BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = img.createGraphics(); + renderTest(g2d); + g2d.dispose(); + return img; + } + + public Dimension getPreferredSize() { + return new Dimension(TESTW, TESTH); + } + + private static void compareImages(BufferedImage refImg, + BufferedImage testImg, + int tolerance) + { + int x1 = 0; + int y1 = 0; + int x2 = refImg.getWidth(); + int y2 = refImg.getHeight(); + + for (int y = y1; y < y2; y++) { + for (int x = x1; x < x2; x++) { + Color expected = new Color(refImg.getRGB(x, y)); + Color actual = new Color(testImg.getRGB(x, y)); + if (!isSameColor(expected, actual, tolerance)) { + saveImage("referenceimage", refImg); + saveImage("testimage", testImg); + throw new RuntimeException("Test failed at x="+x+" y="+y+ + " (expected="+expected+ + " actual="+actual+ + ")"); + } + } + } + } + + private static boolean isSameColor(Color c1, Color c2, int e) { + int r1 = c1.getRed(); + int g1 = c1.getGreen(); + int b1 = c1.getBlue(); + int r2 = c2.getRed(); + int g2 = c2.getGreen(); + int b2 = c2.getBlue(); + int rmin = Math.max(r2-e, 0); + int gmin = Math.max(g2-e, 0); + int bmin = Math.max(b2-e, 0); + int rmax = Math.min(r2+e, 255); + int gmax = Math.min(g2+e, 255); + int bmax = Math.min(b2+e, 255); + if (r1 >= rmin && r1 <= rmax && + g1 >= gmin && g1 <= gmax && + b1 >= bmin && b1 <= bmax) + { + return true; + } + return false; + } + + + static void createUI() { + test = new DrawBufImgOp(); + Panel panel = new Panel(); + panel.add(test); + frame = new Frame("OpenGL DrawBufImgOp Test"); + frame.add(panel); + frame.setSize(TESTW+100, TESTH+100); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + + boolean show = false; + boolean dump = false; + boolean compare = false; + + for (String arg : args) { + if (arg.equals("-show")) { + show = true; + } else if (arg.equals("-dump")) { + dump = true; + } else if (arg.equals("-compare")) { + compare = true; + } else if (arg.equals("-ignore")) { + ignore = true; + } + } + + Robot robot = new Robot(); + + EventQueue.invokeAndWait(DrawBufImgOp::createUI); + + robot.waitForIdle(); + robot.delay(2000); + + BufferedImage capture = null; + try { + GraphicsConfiguration gc = frame.getGraphicsConfiguration(); + if (gc.getColorModel() instanceof IndexColorModel) { + System.out.println("IndexColorModel detected: " + + "test considered PASSED"); + return; + } + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, TESTW, TESTH); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + // Compare the images (allow for +/- 1 bit differences in color comps) + if (dump || compare) { + BufferedImage ref = test.makeReferenceImage(); + if (dump) { + saveImage("DrawBufImgOp_ref", ref); + saveImage("DrawBufImgOp_cap", capture); + } + if (compare) { + test.compareImages(ref, capture, 1); + } + } + } + + static void saveImage(String name, BufferedImage img) { + try { + File file = new File(name + ".png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/DrawImageBg.java b/test/jdk/sun/java2d/OpenGL/DrawImageBg.java new file mode 100644 index 00000000000..7fc38d91b06 --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/DrawImageBg.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 4993274 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that managed image copies and transforms work properly + * with the OGL pipeline when a background color is specified. + * @run main/othervm -Dsun.java2d.opengl=True DrawImageBg + * @run main/othervm DrawImageBg + */ + +/* + * @test + * @bug 4993274 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that managed image copies and transforms work properly + * with the OGL pipeline when a background color is specified. + * @run main/othervm -Dsun.java2d.opengl=True DrawImageBg + * @run main/othervm DrawImageBg + */ + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class DrawImageBg extends Panel { + + static volatile Frame frame; + static volatile DrawImageBg test; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + g2d.setColor(Color.black); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + BufferedImage img = getGraphicsConfiguration().createCompatibleImage(50, 50, + Transparency.BITMASK); + Graphics2D gimg = img.createGraphics(); + gimg.setComposite(AlphaComposite.Src); + gimg.setColor(new Color(0, 0, 0, 0)); + gimg.fillRect(0, 0, 50, 50); + gimg.setColor(Color.red); + gimg.fillRect(10, 10, 30, 30); + gimg.dispose(); + + g2d.drawImage(img, 10, 10, Color.blue, null); + + // draw a second time to ensure that the cached copy is used + g2d.drawImage(img, 80, 10, Color.blue, null); + } + + static void createUI() { + frame = new Frame("OpenGL DrawImageBg Test"); + test = new DrawImageBg(); + frame.add(test); + frame.setSize(300, 300); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + + BufferedImage capture = null; + Robot robot = new Robot(); + try { + EventQueue.invokeAndWait(DrawImageBg::createUI); + robot.waitForIdle(); + robot.delay(3000); + + // Grab the screen region + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x+80, pt1.y, 80, 80); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + if (capture == null) { + throw new RuntimeException("Screen capture is null"); + } + + // Test inner and outer pixels + int pixel1 = capture.getRGB(5, 10); + if (pixel1 != 0xff0000ff) { + saveImage(capture); + throw new RuntimeException(getMsg("outer", pixel1)); + } + int pixel2 = capture.getRGB(25, 25); + if (pixel2 != 0xffff0000) { + saveImage(capture); + throw new RuntimeException(getMsg("inner", pixel2)); + } + } + + static String getMsg(String r, int p1) { + return "Failed: Incorrect color for " + r + " pixel: got " + Integer.toHexString(p1); + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/LargeOps.java b/test/jdk/sun/java2d/OpenGL/LargeOps.java new file mode 100644 index 00000000000..ace60b7a3c4 --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/LargeOps.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 6219284 6358147 6274813 6578452 + * @key headful + * @summary Verifies that OGLRenderer.drawPoly(), + * OGLTextRenderer.drawGlyphList(), and OGLMaskFill work properly when the + * operation parameters exceed the capacity of the render queue. With the + * single-threaded OpenGL pipeline, there are some operations that require + * a separate buffer to be spawned if the parameters cannot fit entirely on + * the standard buffer. This test exercises this special case. + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.lcdshader=true LargeOps + */ + +import java.awt.Canvas; +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Font; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Robot; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class LargeOps extends Canvas { + + private static final int NUM_POINTS = 8000; + private int[] xPoints, yPoints; + private String str; + + public LargeOps() { + xPoints = new int[NUM_POINTS]; + yPoints = new int[NUM_POINTS]; + for (int i = 0; i < NUM_POINTS; i++) { + xPoints[i] = (i % 2 == 0) ? 10 : 400; + yPoints[i] = (i % 2 == 1) ? i+3 : i; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < NUM_POINTS; i+=11) { + sb.append("ThisIsATest"); + } + str = sb.toString(); + } + + public void paint(Graphics g) { + Graphics2D g2d = (Graphics2D)g; + g2d.setColor(Color.white); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + // draw large polyline + g2d.setColor(Color.green); + g2d.drawPolyline(xPoints, yPoints, NUM_POINTS); + + // draw long string + g2d.setColor(Color.blue); + g2d.drawString(str, 10, 100); + + // draw long string with larger pt size + Font font = g2d.getFont(); + g2d.setFont(font.deriveFont(40.0f)); + g2d.drawString(str, 10, 150); + + // do the same with LCD hints enabled + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + g2d.setFont(font); + g2d.drawString(str, 10, 200); + g2d.setFont(font.deriveFont(43.0f)); + g2d.drawString(str, 10, 250); + + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR); + g2d.setFont(font); + g2d.drawString(str, 10, 300); + g2d.setFont(font.deriveFont(37.0f)); + g2d.drawString(str, 10, 350); + } + + static volatile Frame frame; + static volatile LargeOps test; + + static void createUI() { + frame = new Frame("OpenGL LargeOps Test"); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + frame.dispose(); + } + }); + test = new LargeOps(); + frame.add(test); + frame.setSize(600, 600); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + try { + Robot robot = new Robot(); + EventQueue.invokeAndWait(LargeOps::createUI); + robot.waitForIdle(); + robot.delay(6000); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/OpaqueDest.java b/test/jdk/sun/java2d/OpenGL/OpaqueDest.java new file mode 100644 index 00000000000..50896316109 --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/OpaqueDest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 6277977 6319663 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that blending operations do not inadvertantly leave + * non-opaque alpha values in the framebuffer. Note that this test is + * intended to run on GraphicsConfigs that support a stored alpha channel + * (to verify the bug at hand), but it is also a useful for testing the + * compositing results on any configuration. + * @run main/othervm -Dsun.java2d.opengl=True OpaqueDest + * @run main/othervm OpaqueDest + */ + +/* + * @test + * @bug 6277977 6319663 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that blending operations do not inadvertantly leave + * non-opaque alpha values in the framebuffer. Note that this test is + * intended to run on GraphicsConfigs that support a stored alpha channel + * (to verify the bug at hand), but it is also a useful for testing the + * compositing results on any configuration. + * @run main/othervm -Dsun.java2d.opengl=True OpaqueDest + * @run main/othervm OpaqueDest + */ + +import java.awt.AlphaComposite; +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.File; +import javax.imageio.ImageIO; + +public class OpaqueDest extends Canvas { + + private static volatile Frame frame; + private static volatile OpaqueDest test; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + + g2d.setColor(Color.red); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + // This will clear the rectangle to black + g2d.setComposite(AlphaComposite.Clear); + g2d.fillRect(10, 10, 80, 80); + + // If everything is working properly, then this will fill the + // rectangle with red again. Before this bug was fixed, the previous + // Clear operation would leave zero values in the destination's + // alpha channel (if present), and therefore a SrcIn operation + // would result in all-black. + g2d.setComposite(AlphaComposite.SrcIn); + g2d.fillRect(10, 10, 80, 80); + } + + public Dimension getPreferredSize() { + return new Dimension(100, 100); + } + + static void createUI() { + test = new OpaqueDest(); + frame = new Frame("OpenGL OpaqueDest Test"); + Panel p = new Panel(); + p.add(test); + frame.add(p); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + Robot robot = new Robot(); + + EventQueue.invokeAndWait(OpaqueDest::createUI); + + robot.waitForIdle(); + robot.delay(2000); + + BufferedImage capture = null; + try { + GraphicsConfiguration gc = frame.getGraphicsConfiguration(); + if (gc.getColorModel() instanceof IndexColorModel) { + System.out.println("IndexColorModel detected: " + + "test considered PASSED"); + return; + } + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, 100, 100); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + + // Test all pixels (every one should be red) + for (int y = 0; y < 100; y++) { + for (int x = 0; x < 100; x++) { + int actual = capture.getRGB(x, y); + int expected = 0xffff0000; + if (!similar(actual, expected)) { + saveImage(capture); + throw new RuntimeException("Test failed at x="+x+" y="+y+ + " (expected="+ + Integer.toHexString(expected) + + " actual="+ + Integer.toHexString(actual) + + ")"); + } + } + } + } + + static boolean similar(int p1, int p2) { + int a1 = (p1 >> 24) & 0xff; + int r1 = (p1 >> 16) & 0xff; + int g1 = (p1 >> 8) & 0xff; + int b1 = p1 & 0xff; + int a2 = (p2 >> 24) & 0xff; + int r2 = (p2 >> 16) & 0xff; + int g2 = (p2 >> 8) & 0xff; + int b2 = p2 & 0xff; + + int allowedDiff = 0x01; // tiny rounding error allowed. + return + (Math.abs(a1 - a2) <= allowedDiff) && + (Math.abs(r1 - r2) <= allowedDiff) && + (Math.abs(g1 - g2) <= allowedDiff) && + (Math.abs(b1 - b2) <= allowedDiff); + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/ScaleParamsOOB.java b/test/jdk/sun/java2d/OpenGL/ScaleParamsOOB.java new file mode 100644 index 00000000000..b3d866cfd75 --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/ScaleParamsOOB.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 5104584 8237244 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that scaling an image works properly when the + * source parameters are outside the source bounds. + * @run main/othervm -Dsun.java2d.opengl=True ScaleParamsOOB + * @run main/othervm ScaleParamsOOB + */ + +/* + * @test + * @bug 5104584 8237244 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that scaling an image works properly when the + * source parameters are outside the source bounds. + * @run main/othervm -Dsun.java2d.opengl=True ScaleParamsOOB + * @run main/othervm ScaleParamsOOB + */ + + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class ScaleParamsOOB extends Panel { + + private static final int TOLERANCE = 12; + + private static volatile ScaleParamsOOB test; + private static volatile Frame frame; + + private BufferedImage img; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + g2d.setColor(Color.black); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + BufferedImage img = getGraphicsConfiguration().createCompatibleImage(40, 40); + Graphics2D gimg = img.createGraphics(); + gimg.setColor(Color.red); + gimg.fillRect(0, 0, 40, 40); + gimg.dispose(); + + // first time will be a sw->surface blit + g2d.drawImage(img, + 10, 10, 90, 90, + -60, -60, 100, 100, + null); + + // second time will be a texture->surface blit + g2d.drawImage(img, + 110, 10, 190, 90, + -60, -60, 100, 100, + null); + } + + public Dimension getPreferredSize() { + return new Dimension(300, 200); + } + + private static void testRegion(BufferedImage bi, + Rectangle wholeRegion, + Rectangle affectedRegion) + { + int x1 = wholeRegion.x; + int y1 = wholeRegion.y; + int x2 = x1 + wholeRegion.width; + int y2 = y1 + wholeRegion.height; + + for (int y = y1; y < y2; y++) { + for (int x = x1; x < x2; x++) { + int actual = bi.getRGB(x, y); + int expected = 0; + if (affectedRegion.contains(x, y)) { + expected = Color.red.getRGB(); + } else { + expected = Color.black.getRGB(); + } + int alpha = (actual >> 24) & 0xFF; + int red = (actual >> 16) & 0xFF; + int green = (actual >> 8) & 0xFF; + int blue = (actual) & 0xFF; + + int standardAlpha = (expected >> 24) & 0xFF; + int standardRed = (expected >> 16) & 0xFF; + int standardGreen = (expected >> 8) & 0xFF; + int standardBlue = (expected) & 0xFF; + + if ((Math.abs(alpha - standardAlpha) > TOLERANCE) || + (Math.abs(red - standardRed) > TOLERANCE) || + (Math.abs(green - standardGreen) > TOLERANCE) || + (Math.abs(blue - standardBlue) > TOLERANCE)) { + saveImage(bi); + throw new RuntimeException("Test failed at x="+x+" y="+y+ + " (expected="+ + Integer.toHexString(expected) + + " actual="+ + Integer.toHexString(actual) + + ")"); + } + } + } + } + + private static void createAndShowGUI() { + test = new ScaleParamsOOB(); + frame = new Frame("OpenGL ScaleParamsOOB Test"); + frame.setAlwaysOnTop(true); + frame.add(test); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + Robot robot = new Robot(); + + EventQueue.invokeAndWait(() -> createAndShowGUI()); + + robot.waitForIdle(); + robot.delay(2000); + + // Grab the screen region + BufferedImage capture = null; + try { + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, 200, 200); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + // Test background color + int pixel = capture.getRGB(5, 5); + if (pixel != 0xff000000) { + saveImage(capture); + throw new RuntimeException("Failed: Incorrect color for " + + "background: " + Integer.toHexString(pixel)); + } + + // Test pixels + testRegion(capture, + new Rectangle(5, 5, 90, 90), + new Rectangle(40, 40, 20, 20)); + testRegion(capture, + new Rectangle(105, 5, 90, 90), + new Rectangle(140, 40, 20, 20)); + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/ShapeClip.java b/test/jdk/sun/java2d/OpenGL/ShapeClip.java new file mode 100644 index 00000000000..f50b7aff5a6 --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/ShapeClip.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 5002133 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that the OpenGL pipeline does not affect the color + * buffer when setting up a complex (shape) clip region. The test fails if + * the circular clip region is filled with a green color (the green region + * should not be visible at all). + * @run main/othervm -Dsun.java2d.opengl=True ShapeClip + * @run main/othervm ShapeClip + */ + +/* + * @test + * @bug 5002133 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that the OpenGL pipeline does not affect the color + * buffer when setting up a complex (shape) clip region. The test fails if + * the circular clip region is filled with a green color (the green region + * should not be visible at all). + * @run main/othervm -Dsun.java2d.opengl=True ShapeClip + * @run main/othervm ShapeClip + */ + +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.geom.Ellipse2D; +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class ShapeClip extends Panel { + + private static volatile Frame frame; + private static volatile ShapeClip test; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + + int width = getWidth(); + int height = getHeight(); + + g2d.setColor(Color.black); + g2d.fillRect(0, 0, width, height); + + g2d.setColor(Color.green); + g2d.fillRect(0, 0, 1, 1); + g2d.setClip(new Ellipse2D.Double(10, 10, 100, 100)); + g2d.setColor(Color.blue); + g2d.fillRect(30, 30, 20, 20); + } + + static void createUI() { + test = new ShapeClip(); + frame = new Frame("OpenGL ShapeClip Test"); + frame.add(test); + frame.setSize(200, 200); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + Robot robot = new Robot(); + + EventQueue.invokeAndWait(ShapeClip::createUI); + + robot.waitForIdle(); + robot.delay(2000); + + // Grab the screen region + BufferedImage capture = null; + try { + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, 80, 80); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + // Test blue rectangle + int pixel1 = capture.getRGB(40, 40); + if (pixel1 != 0xff0000ff) { + saveImage(capture); + throw new RuntimeException("Failed: Incorrect color for " + + "rectangle " + Integer.toHexString(pixel1)); + } + + // Test clip region (should be same color as background) + int pixel2 = capture.getRGB(60, 40); + if (pixel2 != 0xff000000) { + saveImage(capture); + throw new RuntimeException("Failed: Incorrect color for " + + "clip region " + Integer.toHexString(pixel2)); + } + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/SrcMaskOps.java b/test/jdk/sun/java2d/OpenGL/SrcMaskOps.java new file mode 100644 index 00000000000..9908cffdefb --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/SrcMaskOps.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 4942939 4970674 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that OGLMaskFill, OGLMaskBlit, and OGLTextRenderer + * operations work properly for non-SrcOver composites. + * @run main/othervm -Dsun.java2d.opengl=True SrcMaskOps + * @run main/othervm SrcMaskOps + */ + +/* + * @test + * @bug 4942939 4970674 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that OGLMaskFill, OGLMaskBlit, and OGLTextRenderer + * operations work properly for non-SrcOver composites. + * @run main/othervm -Dsun.java2d.opengl=True SrcMaskOps + * @run main/othervm SrcMaskOps + */ + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Font; +import java.awt.Frame; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class SrcMaskOps extends Panel { + + static volatile Frame frame; + static volatile SrcMaskOps test; + + static final int SRX = 50; + static final int SRY = 50; + static final int GPX = 90; + static final int GPY = 50; + static final int DTX = 120; + static final int DTY = 70; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + + g2d.setColor(Color.white); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setComposite(AlphaComposite.Src); + + g2d.setColor(Color.blue); + g2d.drawRect(SRX, SRY, 20, 20); + + g2d.setPaint(new GradientPaint(0.0f, 0.0f, Color.red, + 100.0f, 100.f, Color.red, true)); + g2d.drawRect(GPX, GPY, 20, 20); + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_OFF); + + g2d.setColor(Color.red); + Font font = new Font(Font.DIALOG, Font.PLAIN, 20); + g2d.setFont(font); + g2d.drawString("HELLO", DTX, DTY); + } + + static void createUI() { + frame = new Frame("OpenGL SrcMaskOps Test"); + test = new SrcMaskOps(); + frame.add(test); + frame.setSize(300, 300); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + + Robot robot = new Robot(); + BufferedImage capture = null; + try { + EventQueue.invokeAndWait(SrcMaskOps::createUI); + robot.waitForIdle(); + robot.delay(3000); + + // Grab the screen region + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, 300, 300); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + // Test solid rectangle + int pixel1, pixel2; + pixel1 = capture.getRGB(SRX, SRY); + pixel2 = capture.getRGB(SRX+2, SRY+2); + if (!similar(pixel1, 0xff0000ff) || !similar(pixel2, 0xffffffff)) { + saveImage(capture); + throw new RuntimeException(getMsg("solid rectangle", pixel1, pixel2)); + } + + // Test GradientPaint rectangle + pixel1 = capture.getRGB(GPX, GPY); + pixel2 = capture.getRGB(GPX+2, GPY+2); + if (!similar(pixel1, 0xffff0000) || !similar(pixel2, 0xffffffff)) { + saveImage(capture); + throw new RuntimeException(getMsg("GradientPaint rectangle", pixel1, pixel2)); + } + + // Test solid text + pixel1 = capture.getRGB(DTX+2, DTY-5); + pixel2 = capture.getRGB(DTX+5, DTY-5); + if (!similar(pixel1, 0xffff0000) || !similar(pixel2, 0xffffffff)) { + saveImage(capture); + throw new RuntimeException(getMsg("solid text", pixel1, pixel2)); + } + + } + + static boolean similar(int p1, int p2) { + int a1 = (p1 >> 24) & 0xff; + int r1 = (p1 >> 16) & 0xff; + int g1 = (p1 >> 8) & 0xff; + int b1 = p1 & 0xff; + int a2 = (p2 >> 24) & 0xff; + int r2 = (p2 >> 16) & 0xff; + int g2 = (p2 >> 8) & 0xff; + int b2 = p2 & 0xff; + + int allowedDiff = 0x10; + return + (Math.abs(a1 - a2) <= allowedDiff) && + (Math.abs(r1 - r2) <= allowedDiff) && + (Math.abs(g1 - g2) <= allowedDiff) && + (Math.abs(b1 - b2) <= allowedDiff); + } + + static String getMsg(String r, int p1, int p2) { + return "Failed: Incorrect color[s] for " + r + " got " + + Integer.toHexString(p1) + " and " + Integer.toHexString(p2); + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test/jdk/sun/java2d/OpenGL/VolatileSubRegion.java b/test/jdk/sun/java2d/OpenGL/VolatileSubRegion.java new file mode 100644 index 00000000000..7ec350bc958 --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/VolatileSubRegion.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 6244071 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that copying a subregion from a VolatileImage works + * properly with the OGL pipeline. + * @run main/othervm VolatileSubRegion + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=true VolatileSubRegion + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=false VolatileSubRegion + */ + +/* + * @test + * @bug 6244071 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that copying a subregion from a VolatileImage works + * properly with the OGL pipeline. + * @run main/othervm VolatileSubRegion + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=true VolatileSubRegion + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=false VolatileSubRegion + */ + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.awt.image.VolatileImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class VolatileSubRegion extends Panel { + + private VolatileImage img; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + + if (img == null) { + img = createVolatileImage(200, 200); + Graphics2D goff = img.createGraphics(); + goff.setColor(Color.green); + goff.fillRect(50, 0, 100, 50); + goff.setColor(Color.blue); + goff.fillRect(0, 0, 200, 200); + goff.setColor(Color.red); + goff.fillRect(50, 50, 100, 100); + goff.setColor(Color.yellow); + goff.fillRect(50, 150, 100, 50); + goff.dispose(); + } + + g2d.setColor(Color.white); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + g2d.drawImage(img, + 50, 50, 200, 200, + 50, 50, 200, 200, + null); + + } + + + private static volatile VolatileSubRegion test; + private static volatile Frame frame; + + static void createUI() { + test = new VolatileSubRegion(); + frame = new Frame("OpenGL VolatileSubRegion Test"); + frame.add(test); + frame.setSize(300, 300); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + Robot robot = new Robot(); + + EventQueue.invokeAndWait(VolatileSubRegion::createUI); + + robot.waitForIdle(); + robot.delay(2000); + + BufferedImage capture = null; + try { + GraphicsConfiguration gc = frame.getGraphicsConfiguration(); + if (gc.getColorModel() instanceof IndexColorModel) { + System.out.println("IndexColorModel detected: " + + "test considered PASSED"); + return; + } + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, 200, 200); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + // Test pixels + int pixel1 = capture.getRGB(49, 50); + if (pixel1 != 0xffffffff) { + saveImage(capture); + throw new RuntimeException(getMsg("background pixel", pixel1)); + } + int pixel2 = capture.getRGB(50, 50); + if (pixel2 != 0xffff0000) { + saveImage(capture); + throw new RuntimeException(getMsg("red region", pixel2)); + } + int pixel3 = capture.getRGB(50, 150); + if (pixel3 != 0xffffff00) { + saveImage(capture); + throw new RuntimeException(getMsg("yellow region", pixel3)); + } + } + + static String getMsg(String r, int p1) { + return "Failed: Incorrect color for " + r + " : got " + Integer.toHexString(p1); + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/test/jdk/sun/java2d/OpenGL/XformVolatile.java b/test/jdk/sun/java2d/OpenGL/XformVolatile.java new file mode 100644 index 00000000000..44e7c7ee8ba --- /dev/null +++ b/test/jdk/sun/java2d/OpenGL/XformVolatile.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 4970836 + * @key headful + * @requires (os.family != "mac") + * @summary Verifies that transformed VolatileImage copies work properly with + * the OGL pipeline. + * @run main/othervm XformVolatile + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=true XformVolatile + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=false XformVolatile + */ + +/* + * @test + * @bug 4970836 + * @key headful + * @requires (os.family == "mac") + * @summary Verifies that transformed VolatileImage copies work properly with + * the OGL pipeline. + * @run main/othervm XformVolatile + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=true XformVolatile + * @run main/othervm -Dsun.java2d.opengl=True -Dsun.java2d.opengl.fbobject=false XformVolatile + */ + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Panel; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.awt.image.VolatileImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class XformVolatile extends Panel { + + private static volatile Frame frame; + private static volatile XformVolatile test; + private volatile VolatileImage img; + + public void paint(Graphics g) { + + Graphics2D g2d = (Graphics2D)g; + + if (img == null) { + img = createVolatileImage(200, 200); + Graphics2D goff = img.createGraphics(); + goff.setColor(Color.blue); + goff.fillRect(0, 0, 200, 200); + goff.setColor(Color.red); + goff.fillPolygon(new int[] {10, 100, 190}, + new int[] {190, 10, 190}, 3); + goff.dispose(); + } + + g2d.setColor(Color.black); + g2d.fillRect(0, 0, getWidth(), getHeight()); + + g2d.rotate(Math.toRadians(3.0)); + g2d.drawImage(img, 0, 0, null); + } + + static void createUI() { + test = new XformVolatile(); + frame = new Frame("OpenGL XformVolatile Test"); + frame.add(test); + frame.setSize(300, 300); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + + Robot robot = new Robot(); + + EventQueue.invokeAndWait(XformVolatile::createUI); + + robot.waitForIdle(); + robot.delay(2000); + + // Grab the screen region + BufferedImage capture = null; + try { + Point pt1 = test.getLocationOnScreen(); + Rectangle rect = new Rectangle(pt1.x, pt1.y, 200, 200); + capture = robot.createScreenCapture(rect); + } finally { + if (frame != null) { + EventQueue.invokeAndWait(frame::dispose); + } + } + + // Test inner and outer pixels + int pixel1 = capture.getRGB(5, 175); + if (pixel1 != 0xff0000ff) { + saveImage(capture); + throw new RuntimeException(getMsg("inner", pixel1)); + } + int pixel2 = capture.getRGB(5, 188); + if (pixel2 != 0xffff0000) { + saveImage(capture); + throw new RuntimeException(getMsg("inner", pixel2)); + } + } + + static String getMsg(String r, int p1) { + return "Failed: Incorrect color for " + r + " pixel: got " + Integer.toHexString(p1); + } + + static void saveImage(BufferedImage img) { + try { + File file = new File("capture.png"); + ImageIO.write(img, "png", file); + } catch (Exception e) { + e.printStackTrace(); + } + } +} From 1ea8cfa6dc8e6f96fd87553331abaae17ec173ea Mon Sep 17 00:00:00 2001 From: Aleksey Shipilev Date: Tue, 7 Oct 2025 16:54:36 +0000 Subject: [PATCH 385/556] 8369226: GHA: Switch to MacOS 15 Reviewed-by: erikj, ayang, sgehwolf --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d8663fab1a..4d1e8a8be3d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -327,8 +327,8 @@ jobs: uses: ./.github/workflows/build-macos.yml with: platform: macos-x64 - runs-on: 'macos-13' - xcode-toolset-version: '14.3.1' + runs-on: 'macos-15-intel' + xcode-toolset-version: '16.4' configure-arguments: ${{ github.event.inputs.configure-arguments }} make-arguments: ${{ github.event.inputs.make-arguments }} dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} @@ -340,8 +340,8 @@ jobs: uses: ./.github/workflows/build-macos.yml with: platform: macos-aarch64 - runs-on: 'macos-14' - xcode-toolset-version: '15.4' + runs-on: 'macos-15' + xcode-toolset-version: '16.4' configure-arguments: ${{ github.event.inputs.configure-arguments }} make-arguments: ${{ github.event.inputs.make-arguments }} dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} @@ -432,9 +432,9 @@ jobs: with: platform: macos-aarch64 bootjdk-platform: macos-aarch64 - runs-on: macos-14 + runs-on: macos-15 dry-run: ${{ needs.prepare.outputs.dry-run == 'true' }} - xcode-toolset-version: '15.4' + xcode-toolset-version: '16.4' debug-suffix: -debug test-windows-x64: From 6b3162620bd808227ec7b4331ae6fc32ceb909e8 Mon Sep 17 00:00:00 2001 From: Naoto Sato Date: Tue, 7 Oct 2025 17:21:13 +0000 Subject: [PATCH 386/556] 8368845: x-IBM930 uses incorrect character for Hex 42 60 Reviewed-by: sherman, rriggs, iris --- make/data/charsetmapping/IBM930.c2b | 5 ----- make/data/charsetmapping/IBM930.map | 10 +--------- test/jdk/sun/nio/cs/mapping/CoderTest.java | 4 ++-- test/jdk/sun/nio/cs/mapping/ConverterTest.java | 5 +++-- test/jdk/sun/nio/cs/mapping/Cp930.b2c | 2 +- test/jdk/sun/nio/cs/mapping/TestConv.java | 4 ++-- 6 files changed, 9 insertions(+), 21 deletions(-) diff --git a/make/data/charsetmapping/IBM930.c2b b/make/data/charsetmapping/IBM930.c2b index 88754763fe3..72107424104 100644 --- a/make/data/charsetmapping/IBM930.c2b +++ b/make/data/charsetmapping/IBM930.c2b @@ -32,11 +32,6 @@ 547d 92ca 53da 9b7e 446e f86f -# -# we should use this one instead of the 4260<-ff0d -#4260 2212 -4260 ff0d -# 426A 00A6 43A1 301C 444A 2014 diff --git a/make/data/charsetmapping/IBM930.map b/make/data/charsetmapping/IBM930.map index 4b9dad9526b..7939e795bdf 100644 --- a/make/data/charsetmapping/IBM930.map +++ b/make/data/charsetmapping/IBM930.map @@ -25,13 +25,6 @@ # 4260 <--> 2212 # 426A <--> 00A6 # -# Warning: -# "our old" implementation seems agree with above "new" mappings -# except the entries 4260 <-> 2212. To keep the "compatbility" -# with the "old" implementation, I changed the entries "temporarily" -# 4260 <-> 2212 -# 4260 <- ff0d -# 00 0000 01 0001 02 0002 @@ -407,8 +400,7 @@ FF 009F 425D FF09 425E FF1B 425F FFE2 -#4260 FF0D -4260 2212 +4260 FF0D 4261 FF0F 426A FFE4 426B FF0C diff --git a/test/jdk/sun/nio/cs/mapping/CoderTest.java b/test/jdk/sun/nio/cs/mapping/CoderTest.java index 05913a40535..2f766736743 100644 --- a/test/jdk/sun/nio/cs/mapping/CoderTest.java +++ b/test/jdk/sun/nio/cs/mapping/CoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,7 +22,7 @@ */ /* @test - @bug 4691554 6221056 6380723 6404504 6419565 6529796 8301119 + @bug 4691554 6221056 6380723 6404504 6419565 6529796 8301119 8368845 @summary Test the supported New I/O coders @modules jdk.charsets @run main CoderTest diff --git a/test/jdk/sun/nio/cs/mapping/ConverterTest.java b/test/jdk/sun/nio/cs/mapping/ConverterTest.java index be8df03230c..8d33670b0a5 100644 --- a/test/jdk/sun/nio/cs/mapping/ConverterTest.java +++ b/test/jdk/sun/nio/cs/mapping/ConverterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,7 +26,8 @@ * @summary test Bug 4199484 * @modules jdk.charsets * @run main ConverterTest - * @bug 4199484 4199599 4199601 4199602 4159519 4201529 4199604 4201532 4947038 6217210 + * @bug 4199484 4199599 4199601 4199602 4159519 4201529 4199604 4201532 4947038 + * 6217210 8368845 */ import java.util.*; diff --git a/test/jdk/sun/nio/cs/mapping/Cp930.b2c b/test/jdk/sun/nio/cs/mapping/Cp930.b2c index 67cc93fd628..3cd45375e2d 100644 --- a/test/jdk/sun/nio/cs/mapping/Cp930.b2c +++ b/test/jdk/sun/nio/cs/mapping/Cp930.b2c @@ -340,7 +340,7 @@ F9 0039 0E425D0F FF09 0E425E0F FF1B 0E425F0F FFE2 -0E42600F 2212 +0E42600F FF0D 0E42610F FF0F 0E426A0F FFE4 0E426B0F FF0C diff --git a/test/jdk/sun/nio/cs/mapping/TestConv.java b/test/jdk/sun/nio/cs/mapping/TestConv.java index 2f4ea424f6c..3bd0b15a7d3 100644 --- a/test/jdk/sun/nio/cs/mapping/TestConv.java +++ b/test/jdk/sun/nio/cs/mapping/TestConv.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -22,7 +22,7 @@ */ /* @test - @bug 4179153 4652234 6529796 + @bug 4179153 4652234 6529796 8368845 @summary Read code mapping table and check code conversion @modules jdk.charsets */ From 7f070d356c479ae30fe84fcf4d322c0b693fa15a Mon Sep 17 00:00:00 2001 From: Mikael Vidstedt Date: Tue, 7 Oct 2025 17:37:31 +0000 Subject: [PATCH 387/556] 8369246: Use https in make/devkit scripts Reviewed-by: ayang, erikj --- make/devkit/Tools.gmk | 10 +++++----- make/devkit/createAutoconfBundle.sh | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/make/devkit/Tools.gmk b/make/devkit/Tools.gmk index 6241674071c..f6ea6749f48 100644 --- a/make/devkit/Tools.gmk +++ b/make/devkit/Tools.gmk @@ -117,13 +117,13 @@ dependencies := gcc binutils ccache mpfr gmp mpc gdb $(foreach dep,$(dependencies),$(eval $(dep)_ver := $(dep)-$($(dep)_ver_only))) -GCC_URL := http://ftp.gnu.org/pub/gnu/gcc/$(gcc_ver)/$(gcc_ver).tar.xz -BINUTILS_URL := http://ftp.gnu.org/pub/gnu/binutils/$(binutils_ver).tar.gz +GCC_URL := https://ftp.gnu.org/pub/gnu/gcc/$(gcc_ver)/$(gcc_ver).tar.xz +BINUTILS_URL := https://ftp.gnu.org/pub/gnu/binutils/$(binutils_ver).tar.gz CCACHE_URL := https://github.com/ccache/ccache/releases/download/v$(ccache_ver_only)/$(ccache_ver).tar.xz MPFR_URL := https://www.mpfr.org/$(mpfr_ver)/$(mpfr_ver).tar.bz2 -GMP_URL := http://ftp.gnu.org/pub/gnu/gmp/$(gmp_ver).tar.bz2 -MPC_URL := http://ftp.gnu.org/pub/gnu/mpc/$(mpc_ver).tar.gz -GDB_URL := http://ftp.gnu.org/gnu/gdb/$(gdb_ver).tar.xz +GMP_URL := https://ftp.gnu.org/pub/gnu/gmp/$(gmp_ver).tar.bz2 +MPC_URL := https://ftp.gnu.org/pub/gnu/mpc/$(mpc_ver).tar.gz +GDB_URL := https://ftp.gnu.org/gnu/gdb/$(gdb_ver).tar.xz REQUIRED_MIN_MAKE_MAJOR_VERSION := 4 ifneq ($(REQUIRED_MIN_MAKE_MAJOR_VERSION),) diff --git a/make/devkit/createAutoconfBundle.sh b/make/devkit/createAutoconfBundle.sh index ebe9c427f76..4697e4eb1e3 100644 --- a/make/devkit/createAutoconfBundle.sh +++ b/make/devkit/createAutoconfBundle.sh @@ -93,7 +93,7 @@ elif test "x$TARGET_PLATFORM" = xlinux_x64; then rpm2cpio $OUTPUT_ROOT/m4-$M4_VERSION.el6.x86_64.rpm | cpio -d -i elif test "x$TARGET_PLATFORM" = xlinux_x86; then M4_VERSION=1.4.13-5 - wget http://yum.oracle.com/repo/OracleLinux/OL6/latest/i386/getPackage/m4-$M4_VERSION.el6.i686.rpm + wget https://yum.oracle.com/repo/OracleLinux/OL6/latest/i386/getPackage/m4-$M4_VERSION.el6.i686.rpm cd $IMAGE_DIR rpm2cpio $OUTPUT_ROOT/m4-$M4_VERSION.el6.i686.rpm | cpio -d -i else From 6bfd018beaf187940ebafc71885045b4aabca673 Mon Sep 17 00:00:00 2001 From: Phil Race Date: Tue, 7 Oct 2025 19:08:22 +0000 Subject: [PATCH 388/556] 8366002: Beans.instantiate needs to describe the lookup procedure Reviewed-by: serb, aivanov --- .../share/classes/java/beans/Beans.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/java.desktop/share/classes/java/beans/Beans.java b/src/java.desktop/share/classes/java/beans/Beans.java index 313bfe98515..a95aeb45cbb 100644 --- a/src/java.desktop/share/classes/java/beans/Beans.java +++ b/src/java.desktop/share/classes/java/beans/Beans.java @@ -64,6 +64,22 @@ public class Beans { *

        * Instantiate a JavaBean. *

        + * The bean is created based on a name relative to a class-loader. + * This name should be a {@linkplain ClassLoader##binary-name binary name} of a class such as "a.b.C". + *

        + * The given name can indicate either a serialized object or a class. + * We first try to treat the {@code beanName} as a serialized object + * name then as a class name. + *

        + * When using the {@code beanName} as a serialized object name we convert the + * given {@code beanName} to a resource pathname and add a trailing ".ser" suffix. + * We then try to load a serialized object from that resource. + *

        + * For example, given a {@code beanName} of "x.y", {@code Beans.instantiate} would first + * try to read a serialized object from the resource "x/y.ser" and if + * that failed it would try to load the class "x.y" and create an + * instance of that class. + * * @return a JavaBean * @param cls the class-loader from which we should create * the bean. If this is null, then the system @@ -84,6 +100,22 @@ public class Beans { *

        * Instantiate a JavaBean. *

        + * The bean is created based on a name relative to a class-loader. + * This name should be a {@linkplain ClassLoader##binary-name binary name} of a class such as "a.b.C". + *

        + * The given name can indicate either a serialized object or a class. + * We first try to treat the {@code beanName} as a serialized object + * name then as a class name. + *

        + * When using the {@code beanName} as a serialized object name we convert the + * given {@code beanName} to a resource pathname and add a trailing ".ser" suffix. + * We then try to load a serialized object from that resource. + *

        + * For example, given a {@code beanName} of "x.y", {@code Beans.instantiate} would first + * try to read a serialized object from the resource "x/y.ser" and if + * that failed it would try to load the class "x.y" and create an + * instance of that class. + * * @return a JavaBean * * @param cls the class-loader from which we should create From 910bb68e5191f830ff6f3dff5753e4e5f6214a7b Mon Sep 17 00:00:00 2001 From: Archie Cobbs Date: Tue, 7 Oct 2025 19:32:08 +0000 Subject: [PATCH 389/556] 8349847: Support configuring individual lint categories as errors Reviewed-by: vromero --- .../com/sun/tools/javac/code/Lint.java | 29 +---- .../sun/tools/javac/main/JavaCompiler.java | 23 +++- .../com/sun/tools/javac/main/Option.java | 18 +++ .../JavacProcessingEnvironment.java | 2 +- .../tools/javac/resources/javac.properties | 10 +- .../classes/com/sun/tools/javac/util/Log.java | 30 ++++- .../com/sun/tools/javac/util/Options.java | 113 ++++++++++++++---- src/jdk.compiler/share/man/javac.md | 13 +- .../tools/javac/warnings/WerrorLint.e1.out | 4 + .../tools/javac/warnings/WerrorLint.e2.out | 5 + .../tools/javac/warnings/WerrorLint.java | 23 ++++ .../tools/javac/warnings/WerrorLint.w1.out | 2 + .../tools/javac/warnings/WerrorLint.w2.out | 3 + 13 files changed, 214 insertions(+), 61 deletions(-) create mode 100644 test/langtools/tools/javac/warnings/WerrorLint.e1.out create mode 100644 test/langtools/tools/javac/warnings/WerrorLint.e2.out create mode 100644 test/langtools/tools/javac/warnings/WerrorLint.java create mode 100644 test/langtools/tools/javac/warnings/WerrorLint.w1.out create mode 100644 test/langtools/tools/javac/warnings/WerrorLint.w2.out diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java index 88c9da5d9e8..3a8b4e5dbea 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java @@ -147,33 +147,10 @@ public class Lint { // Process command line options on demand to allow use of root Lint early during startup private void initializeRootIfNeeded() { - - // Already initialized? - if (values != null) - return; - - // Initialize enabled categories based on "-Xlint" flags - if (options.isSet(Option.XLINT) || options.isSet(Option.XLINT_CUSTOM, Option.LINT_CUSTOM_ALL)) { - // If -Xlint or -Xlint:all is given, enable all categories by default - values = EnumSet.allOf(LintCategory.class); - } else if (options.isSet(Option.XLINT_CUSTOM, Option.LINT_CUSTOM_NONE)) { - // if -Xlint:none is given, disable all categories by default - values = LintCategory.newEmptySet(); - } else { - // otherwise, enable on-by-default categories - values = getDefaults(); + if (values == null) { + values = options.getLintCategoriesOf(Option.XLINT, this::getDefaults); + suppressedValues = LintCategory.newEmptySet(); } - - // Look for specific overrides - for (LintCategory lc : LintCategory.values()) { - if (options.isLintExplicitlyEnabled(lc)) { - values.add(lc); - } else if (options.isLintExplicitlyDisabled(lc)) { - values.remove(lc); - } - } - - suppressedValues = LintCategory.newEmptySet(); } // Obtain the set of on-by-default categories. Note that for a few categories, diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java index ee11304dce9..2469dc9e031 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/JavaCompiler.java @@ -31,6 +31,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.ReadOnlyFileSystemException; import java.util.Collection; import java.util.Comparator; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -55,6 +56,7 @@ import javax.tools.StandardLocation; import com.sun.source.util.TaskEvent; import com.sun.tools.javac.api.MultiTaskListener; import com.sun.tools.javac.code.*; +import com.sun.tools.javac.code.Lint.LintCategory; import com.sun.tools.javac.code.Source.Feature; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.CompletionFailure; @@ -440,7 +442,8 @@ public class JavaCompiler { context.get(DiagnosticListener.class) != null; devVerbose = options.isSet("dev"); processPcks = options.isSet("process.packages"); - werror = options.isSet(WERROR); + werrorAny = options.isSet(WERROR) || options.isSet(WERROR_CUSTOM, Option.LINT_CUSTOM_ALL); + werrorLint = options.getLintCategoriesOf(WERROR, LintCategory::newEmptySet); verboseCompilePolicy = options.isSet("verboseCompilePolicy"); @@ -513,9 +516,13 @@ public class JavaCompiler { */ protected boolean processPcks; - /** Switch: treat warnings as errors + /** Switch: treat any kind of warning (lint or non-lint) as an error. */ - protected boolean werror; + protected boolean werrorAny; + + /** Switch: treat lint warnings in the specified {@link LintCategory}s as errors. + */ + protected EnumSet werrorLint; /** Switch: is annotation processing requested explicitly via * CompilationTask.setProcessors? @@ -581,12 +588,20 @@ public class JavaCompiler { */ public int errorCount() { log.reportOutstandingWarnings(); - if (werror && log.nerrors == 0 && log.nwarnings > 0) { + if (log.nerrors == 0 && log.nwarnings > 0 && + (werrorAny || werrorLint.clone().removeAll(log.lintWarnings))) { log.error(Errors.WarningsAndWerror); } return log.nerrors; } + /** + * Should warnings in the given lint category be treated as errors due to a {@code -Werror} flag? + */ + public boolean isWerror(LintCategory lc) { + return werrorAny || werrorLint.contains(lc); + } + protected final Queue stopIfError(CompileState cs, Queue queue) { return shouldStop(cs) ? new ListBuffer() : queue; } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java index 8d5ad4c4d78..c14767a7a8c 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java @@ -563,6 +563,8 @@ public enum Option { // treat warnings as errors WERROR("-Werror", "opt.Werror", STANDARD, BASIC), + WERROR_CUSTOM("-Werror:", "opt.arg.Werror", "opt.Werror.custom", STANDARD, BASIC, ANYOF, getXLintChoices()), + // prompt after each error // new Option("-prompt", "opt.prompt"), PROMPT("-prompt", null, HIDDEN, BASIC), @@ -1132,6 +1134,22 @@ public enum Option { return Option.valueOf(name() + "_CUSTOM"); } + /** + * Like {@link #getCustom} but also requires that the custom option supports lint categories. + * + *

        + * In practice, that means {@code option} must be {@link Option#LINT} or {@link Option#WERROR}. + * + * @param option regular option + * @return corresponding lint custom option + * @throws IllegalArgumentException if no such option exists + */ + public Option getLintCustom() { + if (this == XLINT || this == WERROR) + return getCustom(); + throw new IllegalArgumentException(); + } + public boolean isInBasicOptionGroup() { return group == BASIC; } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/JavacProcessingEnvironment.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/JavacProcessingEnvironment.java index b28f19bd3af..74d082d4b64 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/JavacProcessingEnvironment.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/JavacProcessingEnvironment.java @@ -211,7 +211,7 @@ public class JavacProcessingEnvironment implements ProcessingEnvironment, Closea } fatalErrors = options.isSet("fatalEnterError"); showResolveErrors = options.isSet("showResolveErrors"); - werror = options.isSet(Option.WERROR); + werror = compiler.isWerror(PROCESSING); fileManager = context.get(JavaFileManager.class); platformAnnotations = initPlatformAnnotations(); diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties index 15a63da06eb..6d4276c794b 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties @@ -95,7 +95,13 @@ javac.opt.source=\ Provide source compatibility with the specified Java SE release.\n\ Supported releases: \n {0} javac.opt.Werror=\ - Terminate compilation if warnings occur + Terminate compilation if any warnings occur +javac.opt.arg.Werror=\ + (,)* +javac.opt.Werror.custom=\ + Specify lint categories for which warnings should terminate compilation,\n\ + separated by comma. Precede a key by ''-'' to exclude the specified category.\n\ + Use --help-lint to see the supported keys. javac.opt.A=\ Options to pass to annotation processors javac.opt.implicit=\ @@ -330,7 +336,7 @@ javac.opt.X=\ javac.opt.help=\ Print this help message javac.opt.help.lint=\ - Print the supported keys for -Xlint + Print the supported keys for -Xlint and -Werror javac.opt.help.lint.header=\ The supported keys for -Xlint are: javac.opt.help.lint.enabled.by.default=\ diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Log.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Log.java index 95458f339a1..24a77a751fd 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Log.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Log.java @@ -171,7 +171,7 @@ public class Log extends AbstractLog { lint.isEnabled(category) : // then emit if the category is enabled category.annotationSuppression ? // else emit if the category is not suppressed, where !lint.isSuppressed(category) : // ...suppression happens via @SuppressWarnings - !options.isLintDisabled(category); // ...suppression happens via -Xlint:-category + !options.isDisabled(Option.XLINT, category); // ...suppression happens via -Xlint:-category if (!emit) return; } @@ -553,10 +553,14 @@ public class Log extends AbstractLog { */ public int nerrors = 0; - /** The number of warnings encountered so far. + /** The total number of warnings encountered so far. */ public int nwarnings = 0; + /** Tracks whether any warnings have been encountered in each {@link LintCategory}. + */ + public final EnumSet lintWarnings = LintCategory.newEmptySet(); + /** The number of errors encountered after MaxErrors was reached. */ public int nsuppressederrors = 0; @@ -885,6 +889,7 @@ public class Log extends AbstractLog { public void clear() { recorded.clear(); sourceMap.clear(); + lintWarnings.clear(); nerrors = 0; nwarnings = 0; nsuppressederrors = 0; @@ -940,7 +945,6 @@ public class Log extends AbstractLog { // Strict warnings are always emitted if (diagnostic.isFlagSet(STRICT)) { writeDiagnostic(diagnostic); - nwarnings++; return; } @@ -948,7 +952,6 @@ public class Log extends AbstractLog { if (emitWarnings || diagnostic.isMandatory()) { if (nwarnings < MaxWarnings) { writeDiagnostic(diagnostic); - nwarnings++; } else { nsuppressedwarns++; } @@ -959,7 +962,6 @@ public class Log extends AbstractLog { if (diagnostic.isFlagSet(API) || shouldReport(diagnostic)) { if (nerrors < MaxErrors) { writeDiagnostic(diagnostic); - nerrors++; } else { nsuppressederrors++; } @@ -973,9 +975,25 @@ public class Log extends AbstractLog { } /** - * Write out a diagnostic. + * Write out a diagnostic and bump the warning and error counters as needed. */ protected void writeDiagnostic(JCDiagnostic diag) { + + // Increment counter(s) + switch (diag.getType()) { + case WARNING: + nwarnings++; + Optional.of(diag) + .map(JCDiagnostic::getLintCategory) + .ifPresent(lintWarnings::add); + break; + case ERROR: + nerrors++; + break; + default: + break; + } + if (diagListener != null) { diagListener.report(diag); return; diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Options.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Options.java index 32a31028b68..030e5b21758 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Options.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Options.java @@ -173,66 +173,139 @@ public class Options { /** * Determine if a specific {@link LintCategory} is enabled via a custom - * option flag of the form {@code -Xlint}, {@code -Xlint:all}, or {@code -Xlint:key}. + * option flag of the form {@code -Flag}, {@code -Flag:all}, or {@code -Flag:key}. + * + *

        + * The given {@code option} must have a custom lint variant (available via {@link Option#getLintCustom}). * *

        * Note: It's possible the category was also disabled; this method does not check that. * + * @param option the plain (non-custom) version of the option (e.g., {@link Option#XLINT}) * @param lc the {@link LintCategory} in question - * @return true if {@code lc} has been enabled + * @return true if {@code lc} is enabled via {@code option}'s lint custom variant (e.g., {@link Option#XLINT_CUSTOM}) + * @throws IllegalArgumentException if there is no lint custom variant of {@code option} */ - public boolean isLintEnabled(LintCategory lc) { - return isLintExplicitlyEnabled(lc) || - isSet(Option.XLINT_CUSTOM) || - isSet(Option.XLINT_CUSTOM, Option.LINT_CUSTOM_ALL); + public boolean isEnabled(Option option, LintCategory lc) { + Option custom = option.getLintCustom(); + return isExplicitlyEnabled(option, lc) || isSet(custom) || isSet(custom, Option.LINT_CUSTOM_ALL); } /** * Determine if a specific {@link LintCategory} is disabled via a custom - * option flag of the form {@code -Xlint:none} or {@code -Xlint:-key}. + * option flag of the form {@code -Flag:none} or {@code -Flag:-key}. + * + *

        + * The given {@code option} must have a custom lint variant (available via {@link Option#getLintCustom}). * *

        * Note: It's possible the category was also enabled; this method does not check that. * + * @param option the plain (non-custom) version of the option (e.g., {@link Option#XLINT}) * @param lc the {@link LintCategory} in question - * @return true if {@code lc} has been disabled + * @return true if {@code lc} is disabled via {@code option}'s lint custom variant (e.g., {@link Option#XLINT_CUSTOM}) + * @throws IllegalArgumentException if there is no lint custom variant of {@code option} */ - public boolean isLintDisabled(LintCategory lc) { - return isLintExplicitlyDisabled(lc) || isSet(Option.XLINT_CUSTOM, Option.LINT_CUSTOM_NONE); + public boolean isDisabled(Option option, LintCategory lc) { + return isExplicitlyDisabled(option, lc) || isSet(option.getLintCustom(), Option.LINT_CUSTOM_NONE); } /** * Determine if a specific {@link LintCategory} is explicitly enabled via a custom - * option flag of the form {@code -Xlint:key}. + * option flag of the form {@code -Flag:key}. * *

        - * Note: This does not check for option flags of the form {@code -Xlint} or {@code -Xlint:all}. + * The given {@code option} must have a custom lint variant (available via {@link Option#getLintCustom}). + * + *

        + * Note: This does not check for option flags of the form {@code -Flag} or {@code -Flag:all}. * *

        * Note: It's possible the category was also disabled; this method does not check that. * + * @param option the plain (non-custom) version of the option (e.g., {@link Option#XLINT}) * @param lc the {@link LintCategory} in question - * @return true if {@code lc} has been explicitly enabled + * @return true if {@code lc} is explicitly enabled via {@code option}'s lint custom variant (e.g., {@link Option#XLINT_CUSTOM}) + * @throws IllegalArgumentException if there is no lint custom variant of {@code option} */ - public boolean isLintExplicitlyEnabled(LintCategory lc) { - return lc.optionList.stream().anyMatch(alias -> isSet(Option.XLINT_CUSTOM, alias)); + public boolean isExplicitlyEnabled(Option option, LintCategory lc) { + Option customOption = option.getLintCustom(); + return lc.optionList.stream().anyMatch(alias -> isSet(customOption, alias)); } /** * Determine if a specific {@link LintCategory} is explicitly disabled via a custom - * option flag of the form {@code -Xlint:-key}. + * option flag of the form {@code -Flag:-key}. * *

        - * Note: This does not check for an option flag of the form {@code -Xlint:none}. + * The given {@code option} must have a custom lint variant (available via {@link Option#getLintCustom}). + * + *

        + * Note: This does not check for an option flag of the form {@code -Flag:none}. * *

        * Note: It's possible the category was also enabled; this method does not check that. * + * @param option the plain (non-custom) version of the option (e.g., {@link Option#XLINT}) * @param lc the {@link LintCategory} in question - * @return true if {@code lc} has been explicitly disabled + * @return true if {@code lc} is explicitly disabled via {@code option}'s lint custom variant (e.g., {@link Option#XLINT_CUSTOM}) + * @throws IllegalArgumentException if there is no lint custom variant of {@code option} */ - public boolean isLintExplicitlyDisabled(LintCategory lc) { - return lc.optionList.stream().anyMatch(alias -> isSet(Option.XLINT_CUSTOM, "-" + alias)); + public boolean isExplicitlyDisabled(Option option, LintCategory lc) { + Option customOption = option.getLintCustom(); + return lc.optionList.stream().anyMatch(alias -> isSet(customOption, "-" + alias)); + } + + /** + * Collect the set of {@link LintCategory}s specified by option flag(s) of the form + * {@code -Flag} and/or {@code -Flag:[-]key,[-]key,...}. + * + *

        + * The given {@code option} must have a custom lint variant (available via {@link Option#getLintCustom}). + * + *

        + * The set of categories is calculated as follows. First, an initial set is created: + *

          + *
        • If {@code -Flag} or {@code -Flag:all} appears, the initial set contains all categories; otherwise, + *
        • If {@code -Flag:none} appears, the initial set is empty; otherwise, + *
        • The {@code defaults} parameter is invoked to construct an initial set. + *
        + * Next, for each lint category key {@code key}: + *
          + *
        • If {@code -Flag:key} flag appears, the corresponding category is added to the set; otherwise + *
        • If {@code -Flag:-key} flag appears, the corresponding category is removed to the set + *
        + * Unrecognized {@code key}s are ignored. + * + * @param option the plain (non-custom) version of the option (e.g., {@link Option#XLINT}) + * @param defaults populates the default set, or null for an empty default set + * @return the specified set of categories + * @throws IllegalArgumentException if there is no lint custom variant of {@code option} + */ + public EnumSet getLintCategoriesOf(Option option, Supplier> defaults) { + + // Create the initial set + EnumSet categories; + Option customOption = option.getLintCustom(); + if (isSet(option) || isSet(customOption, Option.LINT_CUSTOM_ALL)) { + categories = EnumSet.allOf(LintCategory.class); + } else if (isSet(customOption, Option.LINT_CUSTOM_NONE)) { + categories = EnumSet.noneOf(LintCategory.class); + } else { + categories = defaults.get(); + } + + // Apply specific overrides + for (LintCategory category : LintCategory.values()) { + if (isExplicitlyEnabled(option, category)) { + categories.add(category); + } else if (isExplicitlyDisabled(option, category)) { + categories.remove(category); + } + } + + // Done + return categories; } public void put(String name, String value) { diff --git a/src/jdk.compiler/share/man/javac.md b/src/jdk.compiler/share/man/javac.md index b8243cc78fb..46246624e53 100644 --- a/src/jdk.compiler/share/man/javac.md +++ b/src/jdk.compiler/share/man/javac.md @@ -1,5 +1,5 @@ --- -# Copyright (c) 1994, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 1994, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -448,7 +448,16 @@ file system locations may be directories, JAR files or JMOD files. : Prints version information. `-Werror` -: Terminates compilation when warnings occur. +: Terminates compilation when any warnings occur; this includes warnings in all lint + categories, as well as non-lint warnings. + +`-Werror:`\[`-`\]*key*(`,`\[`-`\]*key*)\* +: Specify lint categories for which warnings should terminate compilation. The keys + `all` and `none` include or exclude all categories (respectively); other keys include + the corresponding category, or exclude it if preceded by a hyphen (`-`). By default, + no categories are included. In order to terminate compilation, the category must also + be enabled (via [`-Xlint`](#option-Xlint-custom), if necessary). + See [`-Xlint`](#option-Xlint-custom) below for the list of lint category keys. ### Extra Options diff --git a/test/langtools/tools/javac/warnings/WerrorLint.e1.out b/test/langtools/tools/javac/warnings/WerrorLint.e1.out new file mode 100644 index 00000000000..ae99cfa5056 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.e1.out @@ -0,0 +1,4 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +- compiler.err.warnings.and.werror +1 error +1 warning diff --git a/test/langtools/tools/javac/warnings/WerrorLint.e2.out b/test/langtools/tools/javac/warnings/WerrorLint.e2.out new file mode 100644 index 00000000000..1c9bd4d54f8 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.e2.out @@ -0,0 +1,5 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +WerrorLint.java:21:30: compiler.warn.empty.if +- compiler.err.warnings.and.werror +1 error +2 warnings diff --git a/test/langtools/tools/javac/warnings/WerrorLint.java b/test/langtools/tools/javac/warnings/WerrorLint.java new file mode 100644 index 00000000000..3331a664d55 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.java @@ -0,0 +1,23 @@ +/* + * @test /nodynamiccopyright/ + * @bug 8349847 + * + * @compile -XDrawDiagnostics -Xlint:none WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror:empty WerrorLint.java + * @compile -XDrawDiagnostics -Xlint:none -Werror:strictfp WerrorLint.java + * @compile/ref=WerrorLint.w2.out -XDrawDiagnostics -Xlint:all WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror:empty WerrorLint.java + * @compile/fail/ref=WerrorLint.e2.out -XDrawDiagnostics -Xlint:all -Werror:strictfp WerrorLint.java + * @compile/ref=WerrorLint.w1.out -XDrawDiagnostics WerrorLint.java + * @compile/fail/ref=WerrorLint.e1.out -XDrawDiagnostics -Werror WerrorLint.java + * @compile/ref=WerrorLint.w1.out -XDrawDiagnostics -Werror:empty WerrorLint.java + * @compile/fail/ref=WerrorLint.e1.out -XDrawDiagnostics -Werror:strictfp WerrorLint.java + */ + +class WerrorLint { + strictfp void m() { // [strictfp] - this category is enabled by default + if (hashCode() == 1) ; // [empty] - this category is disabled by default + } +} diff --git a/test/langtools/tools/javac/warnings/WerrorLint.w1.out b/test/langtools/tools/javac/warnings/WerrorLint.w1.out new file mode 100644 index 00000000000..3e19de51033 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.w1.out @@ -0,0 +1,2 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +1 warning diff --git a/test/langtools/tools/javac/warnings/WerrorLint.w2.out b/test/langtools/tools/javac/warnings/WerrorLint.w2.out new file mode 100644 index 00000000000..bac258706a6 --- /dev/null +++ b/test/langtools/tools/javac/warnings/WerrorLint.w2.out @@ -0,0 +1,3 @@ +WerrorLint.java:20:19: compiler.warn.strictfp +WerrorLint.java:21:30: compiler.warn.empty.if +2 warnings From 4ee6079b11034e7de8be72cd2832fb717c2f140d Mon Sep 17 00:00:00 2001 From: Mikael Vidstedt Date: Wed, 8 Oct 2025 02:05:20 +0000 Subject: [PATCH 390/556] 8369328: Use uppercase variable names in the devkit makefiles Reviewed-by: erikj --- make/devkit/Makefile | 50 +++++++-------- make/devkit/Tools.gmk | 146 +++++++++++++++++++++--------------------- 2 files changed, 98 insertions(+), 98 deletions(-) diff --git a/make/devkit/Makefile b/make/devkit/Makefile index ffa23508a13..30e0dce0839 100644 --- a/make/devkit/Makefile +++ b/make/devkit/Makefile @@ -57,61 +57,61 @@ COMMA := , -os := $(shell uname -o) -cpu := $(shell uname -m) +OS := $(shell uname -o) +CPU := $(shell uname -m) # Figure out what platform this is building on. -me := $(cpu)-$(if $(findstring Linux,$(os)),linux-gnu) +ME := $(CPU)-$(if $(findstring Linux,$(OS)),linux-gnu) -$(info Building on platform $(me)) +$(info Building on platform $(ME)) # # By default just build for the current platform, which is assumed to be Linux # ifeq ($(TARGETS), ) - platforms := $(me) - host_platforms := $(platforms) + PLATFORMS := $(ME) + HOST_PLATFORMS := $(PLATFORMS) else - platforms := $(subst $(COMMA), , $(TARGETS)) - host_platforms := $(me) + PLATFORMS := $(subst $(COMMA), , $(TARGETS)) + HOST_PLATFORMS := $(ME) endif -target_platforms := $(platforms) -$(info host_platforms $(host_platforms)) -$(info target_platforms $(target_platforms)) +TARGET_PLATFORMS := $(PLATFORMS) +$(info HOST_PLATFORMS $(HOST_PLATFORMS)) +$(info TARGET_PLATFORMS $(TARGET_PLATFORMS)) -all compile : $(platforms) +all compile : $(PLATFORMS) ifeq ($(SKIP_ME), ) - $(foreach p,$(filter-out $(me),$(platforms)),$(eval $(p) : $$(me))) + $(foreach p,$(filter-out $(ME),$(PLATFORMS)),$(eval $(p) : $$(ME))) endif OUTPUT_ROOT = $(abspath ../../build/devkit) RESULT = $(OUTPUT_ROOT)/result -submakevars = HOST=$@ BUILD=$(me) RESULT=$(RESULT) OUTPUT_ROOT=$(OUTPUT_ROOT) +SUBMAKEVARS = HOST=$@ BUILD=$(ME) RESULT=$(RESULT) OUTPUT_ROOT=$(OUTPUT_ROOT) -$(host_platforms) : +$(HOST_PLATFORMS) : @echo 'Building compilers for $@' - @echo 'Targets: $(target_platforms)' - for p in $(filter $@, $(target_platforms)) $(filter-out $@, $(target_platforms)); do \ - $(MAKE) -f Tools.gmk download-rpms $(submakevars) \ + @echo 'Targets: $(TARGET_PLATFORMS)' + for p in $(filter $@, $(TARGET_PLATFORMS)) $(filter-out $@, $(TARGET_PLATFORMS)); do \ + $(MAKE) -f Tools.gmk download-rpms $(SUBMAKEVARS) \ TARGET=$$p PREFIX=$(RESULT)/$@-to-$$p && \ - $(MAKE) -f Tools.gmk all $(submakevars) \ + $(MAKE) -f Tools.gmk all $(SUBMAKEVARS) \ TARGET=$$p PREFIX=$(RESULT)/$@-to-$$p && \ - $(MAKE) -f Tools.gmk ccache $(submakevars) \ + $(MAKE) -f Tools.gmk ccache $(SUBMAKEVARS) \ TARGET=$@ PREFIX=$(RESULT)/$@-to-$$p || exit 1 ; \ done @echo 'All done"' -today := $(shell date +%Y%m%d) +TODAY := $(shell date +%Y%m%d) define Mktar - $(1)-to-$(2)_tar = $$(RESULT)/sdk-$(1)-to-$(2)-$$(today).tar.gz + $(1)-to-$(2)_tar = $$(RESULT)/sdk-$(1)-to-$(2)-$$(TODAY).tar.gz $$($(1)-to-$(2)_tar) : PLATFORM = $(1)-to-$(2) TARFILES += $$($(1)-to-$(2)_tar) endef -$(foreach p,$(host_platforms),$(foreach t,$(target_platforms),$(eval $(call Mktar,$(p),$(t))))) +$(foreach p,$(HOST_PLATFORMS),$(foreach t,$(TARGET_PLATFORMS),$(eval $(call Mktar,$(p),$(t))))) tars : all $(TARFILES) onlytars : $(TARFILES) @@ -119,9 +119,9 @@ onlytars : $(TARFILES) $(MAKE) -r -f Tars.gmk SRC_DIR=$(RESULT)/$(PLATFORM) TAR_FILE=$@ clean : - rm -rf $(addprefix ../../build/devkit/, result $(host_platforms)) + rm -rf $(addprefix ../../build/devkit/, result $(HOST_PLATFORMS)) dist-clean: clean rm -rf $(addprefix ../../build/devkit/, src download) FORCE : -.PHONY : all compile tars $(configs) $(host_platforms) clean dist-clean +.PHONY : all compile tars $(HOST_PLATFORMS) clean dist-clean diff --git a/make/devkit/Tools.gmk b/make/devkit/Tools.gmk index f6ea6749f48..77a201d0c38 100644 --- a/make/devkit/Tools.gmk +++ b/make/devkit/Tools.gmk @@ -39,7 +39,7 @@ # Fix this... # -uppercase = $(shell echo $1 | tr a-z A-Z) +lowercase = $(shell echo $1 | tr A-Z a-z) $(info TARGET=$(TARGET)) $(info HOST=$(HOST)) @@ -104,26 +104,26 @@ endif ################################################################################ # Define external dependencies -gcc_ver_only := 14.2.0 -binutils_ver_only := 2.43 -ccache_ver_only := 4.10.2 +GCC_VER_ONLY := 14.2.0 +BINUTILS_VER_ONLY := 2.43 +CCACHE_VER_ONLY := 4.10.2 CCACHE_CMAKE_BASED := 1 -mpfr_ver_only := 4.2.1 -gmp_ver_only := 6.3.0 -mpc_ver_only := 1.3.1 -gdb_ver_only := 15.2 +MPFR_VER_ONLY := 4.2.1 +GMP_VER_ONLY := 6.3.0 +MPC_VER_ONLY := 1.3.1 +GDB_VER_ONLY := 15.2 -dependencies := gcc binutils ccache mpfr gmp mpc gdb +DEPENDENCIES := GCC BINUTILS CCACHE MPFR GMP MPC GDB -$(foreach dep,$(dependencies),$(eval $(dep)_ver := $(dep)-$($(dep)_ver_only))) +$(foreach dep,$(DEPENDENCIES),$(eval $(dep)_VER := $(call lowercase,$(dep)-$($(dep)_VER_ONLY)))) -GCC_URL := https://ftp.gnu.org/pub/gnu/gcc/$(gcc_ver)/$(gcc_ver).tar.xz -BINUTILS_URL := https://ftp.gnu.org/pub/gnu/binutils/$(binutils_ver).tar.gz -CCACHE_URL := https://github.com/ccache/ccache/releases/download/v$(ccache_ver_only)/$(ccache_ver).tar.xz -MPFR_URL := https://www.mpfr.org/$(mpfr_ver)/$(mpfr_ver).tar.bz2 -GMP_URL := https://ftp.gnu.org/pub/gnu/gmp/$(gmp_ver).tar.bz2 -MPC_URL := https://ftp.gnu.org/pub/gnu/mpc/$(mpc_ver).tar.gz -GDB_URL := https://ftp.gnu.org/gnu/gdb/$(gdb_ver).tar.xz +GCC_URL := https://ftp.gnu.org/pub/gnu/gcc/$(GCC_VER)/$(GCC_VER).tar.xz +BINUTILS_URL := https://ftp.gnu.org/pub/gnu/binutils/$(BINUTILS_VER).tar.gz +CCACHE_URL := https://github.com/ccache/ccache/releases/download/v$(CCACHE_VER_ONLY)/$(CCACHE_VER).tar.xz +MPFR_URL := https://www.mpfr.org/$(MPFR_VER)/$(MPFR_VER).tar.bz2 +GMP_URL := https://ftp.gnu.org/pub/gnu/gmp/$(GMP_VER).tar.bz2 +MPC_URL := https://ftp.gnu.org/pub/gnu/mpc/$(MPC_VER).tar.gz +GDB_URL := https://ftp.gnu.org/gnu/gdb/$(GDB_VER).tar.xz REQUIRED_MIN_MAKE_MAJOR_VERSION := 4 ifneq ($(REQUIRED_MIN_MAKE_MAJOR_VERSION),) @@ -180,10 +180,10 @@ DOWNLOAD_RPMS := $(DOWNLOAD)/rpms/$(TARGET)-$(LINUX_VERSION) SRCDIR := $(OUTPUT_ROOT)/src # Marker file for unpacking rpms -rpms := $(SYSROOT)/rpms_unpacked +RPMS := $(SYSROOT)/rpms_unpacked # Need to patch libs that are linker scripts to use non-absolute paths -libs := $(SYSROOT)/libs_patched +LIBS := $(SYSROOT)/libs_patched ################################################################################ # Download RPMs @@ -228,7 +228,7 @@ define Download endef # Download and unpack all source packages -$(foreach dep,$(dependencies),$(eval $(call Download,$(call uppercase,$(dep))))) +$(foreach dep,$(DEPENDENCIES),$(eval $(call Download,$(dep)))) ################################################################################ # Unpack RPMS @@ -250,7 +250,7 @@ RPM_FILE_LIST := $(sort $(foreach a, $(RPM_ARCHS), \ # Note. For building linux you should install rpm2cpio. define unrpm $(SYSROOT)/$(notdir $(1)).unpacked : $(1) - $$(rpms) : $(SYSROOT)/$(notdir $(1)).unpacked + $$(RPMS) : $(SYSROOT)/$(notdir $(1)).unpacked endef %.unpacked : @@ -277,7 +277,7 @@ $(foreach p,$(RPM_FILE_LIST),$(eval $(call unrpm,$(p)))) # have it anyway, but just to make sure... # Patch libc.so and libpthread.so to force linking against libraries in sysroot # and not the ones installed on the build machine. -$(libs) : $(rpms) +$(LIBS) : $(RPMS) @echo Patching libc and pthreads @(for f in `find $(SYSROOT) -name libc.so -o -name libpthread.so`; do \ (cat $$f | sed -e 's|/usr/lib64/||g' \ @@ -293,10 +293,10 @@ $(libs) : $(rpms) # Create links for ffi header files so that they become visible by default when using the # devkit. ifeq ($(ARCH), x86_64) - $(SYSROOT)/usr/include/ffi.h: $(rpms) + $(SYSROOT)/usr/include/ffi.h: $(RPMS) cd $(@D) && rm -f $(@F) && ln -s ../lib/libffi-*/include/$(@F) . - $(SYSROOT)/usr/include/ffitarget.h: $(rpms) + $(SYSROOT)/usr/include/ffitarget.h: $(RPMS) cd $(@D) && rm -f $(@F) && ln -s ../lib/libffi-*/include/$(@F) . SYSROOT_LINKS += $(SYSROOT)/usr/include/ffi.h $(SYSROOT)/usr/include/ffitarget.h @@ -305,7 +305,7 @@ endif ################################################################################ # Define marker files for each source package to be compiled -$(foreach dep,$(dependencies),$(eval $(dep) = $(TARGETDIR)/$($(dep)_ver).done)) +$(foreach dep,$(DEPENDENCIES),$(eval $(dep) = $(TARGETDIR)/$($(dep)_VER).done)) ################################################################################ @@ -345,48 +345,48 @@ TOOLS ?= $(call declare_tools,_FOR_TARGET,$(TARGET)-) # CFLAG_ to most likely -m32. define mk_bfd $$(info Libs for $(1)) - $$(BUILDDIR)/$$(binutils_ver)-$(subst /,-,$(1))/Makefile \ + $$(BUILDDIR)/$$(BINUTILS_VER)-$(subst /,-,$(1))/Makefile \ : CFLAGS += $$(CFLAGS_$(1)) - $$(BUILDDIR)/$$(binutils_ver)-$(subst /,-,$(1))/Makefile \ + $$(BUILDDIR)/$$(BINUTILS_VER)-$(subst /,-,$(1))/Makefile \ : LIBDIRS = --libdir=$(TARGETDIR)/$(1) - bfdlib += $$(TARGETDIR)/$$(binutils_ver)-$(subst /,-,$(1)).done - bfdmakes += $$(BUILDDIR)/$$(binutils_ver)-$(subst /,-,$(1))/Makefile + BFDLIB += $$(TARGETDIR)/$$(BINUTILS_VER)-$(subst /,-,$(1)).done + BFDMAKES += $$(BUILDDIR)/$$(BINUTILS_VER)-$(subst /,-,$(1))/Makefile endef # Create one set of bfds etc for each multilib arch $(foreach l,$(LIBDIRS),$(eval $(call mk_bfd,$(l)))) # Only build these two libs. -$(bfdlib) : MAKECMD = all-libiberty all-bfd -$(bfdlib) : INSTALLCMD = install-libiberty install-bfd +$(BFDLIB) : MAKECMD = all-libiberty all-bfd +$(BFDLIB) : INSTALLCMD = install-libiberty install-bfd # Building targets libbfd + libiberty. HOST==TARGET, i.e not # for a cross env. -$(bfdmakes) : CONFIG = --target=$(TARGET) \ +$(BFDMAKES) : CONFIG = --target=$(TARGET) \ --host=$(TARGET) --build=$(BUILD) \ --prefix=$(TARGETDIR) \ --with-sysroot=$(SYSROOT) \ $(LIBDIRS) -$(bfdmakes) : TOOLS = $(call declare_tools,_FOR_TARGET,$(TARGET)-) $(call declare_tools,,$(TARGET)-) +$(BFDMAKES) : TOOLS = $(call declare_tools,_FOR_TARGET,$(TARGET)-) $(call declare_tools,,$(TARGET)-) ################################################################################ -$(gcc) \ - $(binutils) \ - $(gmp) \ - $(mpfr) \ - $(mpc) \ - $(bfdmakes) \ - $(ccache) : ENVS += $(TOOLS) +$(GCC) \ + $(BINUTILS) \ + $(GMP) \ + $(MPFR) \ + $(MPC) \ + $(BFDMAKES) \ + $(CCACHE) : ENVS += $(TOOLS) # libdir to work around hateful bfd stuff installing into wrong dirs... # ensure we have 64 bit bfd support in the HOST library. I.e our # compiler on i686 will know 64 bit symbols, BUT later # we build just the libs again for TARGET, then with whatever the arch # wants. -$(BUILDDIR)/$(binutils_ver)/Makefile : CONFIG += --enable-64-bit-bfd --libdir=$(PREFIX)/$(word 1,$(LIBDIRS)) +$(BUILDDIR)/$(BINUTILS_VER)/Makefile : CONFIG += --enable-64-bit-bfd --libdir=$(PREFIX)/$(word 1,$(LIBDIRS)) ifeq ($(filter $(ARCH), s390x riscv64 ppc64le), ) # gold compiles but cannot link properly on s390x @ gcc 13.2 and Fedore 41 @@ -397,8 +397,8 @@ endif # Makefile creation. Simply run configure in build dir. # Setting CFLAGS to -O2 generates a much faster ld. -$(bfdmakes) \ -$(BUILDDIR)/$(binutils_ver)/Makefile \ +$(BFDMAKES) \ +$(BUILDDIR)/$(BINUTILS_VER)/Makefile \ : $(BINUTILS_CFG) $(info Configuring $@. Log in $(@D)/log.config) @mkdir -p $(@D) @@ -417,7 +417,7 @@ $(BUILDDIR)/$(binutils_ver)/Makefile \ ) > $(@D)/log.config 2>&1 @echo 'done' -$(BUILDDIR)/$(mpfr_ver)/Makefile \ +$(BUILDDIR)/$(MPFR_VER)/Makefile \ : $(MPFR_CFG) $(info Configuring $@. Log in $(@D)/log.config) @mkdir -p $(@D) @@ -432,7 +432,7 @@ $(BUILDDIR)/$(mpfr_ver)/Makefile \ ) > $(@D)/log.config 2>&1 @echo 'done' -$(BUILDDIR)/$(gmp_ver)/Makefile \ +$(BUILDDIR)/$(GMP_VER)/Makefile \ : $(GMP_CFG) $(info Configuring $@. Log in $(@D)/log.config) @mkdir -p $(@D) @@ -449,7 +449,7 @@ $(BUILDDIR)/$(gmp_ver)/Makefile \ ) > $(@D)/log.config 2>&1 @echo 'done' -$(BUILDDIR)/$(mpc_ver)/Makefile \ +$(BUILDDIR)/$(MPC_VER)/Makefile \ : $(MPC_CFG) $(info Configuring $@. Log in $(@D)/log.config) @mkdir -p $(@D) @@ -468,11 +468,11 @@ $(BUILDDIR)/$(mpc_ver)/Makefile \ # Only valid if glibc target -> linux # proper destructor handling for c++ ifneq (,$(findstring linux,$(TARGET))) - $(BUILDDIR)/$(gcc_ver)/Makefile : CONFIG += --enable-__cxa_atexit + $(BUILDDIR)/$(GCC_VER)/Makefile : CONFIG += --enable-__cxa_atexit endif ifeq ($(ARCH), armhfp) - $(BUILDDIR)/$(gcc_ver)/Makefile : CONFIG += --with-float=hard + $(BUILDDIR)/$(GCC_VER)/Makefile : CONFIG += --with-float=hard endif ifneq ($(filter riscv64 ppc64le s390x, $(ARCH)), ) @@ -487,7 +487,7 @@ endif # skip native language. # and link and assemble with the binutils we created # earlier, so --with-gnu* -$(BUILDDIR)/$(gcc_ver)/Makefile \ +$(BUILDDIR)/$(GCC_VER)/Makefile \ : $(GCC_CFG) $(info Configuring $@. Log in $(@D)/log.config) mkdir -p $(@D) @@ -509,17 +509,17 @@ $(BUILDDIR)/$(gcc_ver)/Makefile \ @echo 'done' # need binutils for gcc -$(gcc) : $(binutils) +$(GCC) : $(BINUTILS) # as of 4.3 or so need these for doing config -$(BUILDDIR)/$(gcc_ver)/Makefile : $(gmp) $(mpfr) $(mpc) -$(mpfr) : $(gmp) -$(mpc) : $(gmp) $(mpfr) +$(BUILDDIR)/$(GCC_VER)/Makefile : $(GMP) $(MPFR) $(MPC) +$(MPFR) : $(GMP) +$(MPC) : $(GMP) $(MPFR) ################################################################################ # Build gdb but only where host and target match ifeq ($(HOST), $(TARGET)) - $(BUILDDIR)/$(gdb_ver)/Makefile: $(GDB_CFG) + $(BUILDDIR)/$(GDB_VER)/Makefile: $(GDB_CFG) $(info Configuring $@. Log in $(@D)/log.config) mkdir -p $(@D) ( \ @@ -532,9 +532,9 @@ ifeq ($(HOST), $(TARGET)) ) > $(@D)/log.config 2>&1 @echo 'done' - $(gdb): $(gcc) + $(GDB): $(GCC) else - $(BUILDDIR)/$(gdb_ver)/Makefile: + $(BUILDDIR)/$(GDB_VER)/Makefile: $(info Faking $@, not used when cross-compiling) mkdir -p $(@D) echo "install:" > $@ @@ -543,7 +543,7 @@ endif ################################################################################ # very straightforward. just build a ccache. it is only for host. -$(BUILDDIR)/$(ccache_ver)/Makefile \ +$(BUILDDIR)/$(CCACHE_VER)/Makefile \ : $(CCACHE_SRC_MARKER) $(info Configuring $@. Log in $(@D)/log.config) @mkdir -p $(@D) @@ -554,12 +554,12 @@ $(BUILDDIR)/$(ccache_ver)/Makefile \ ) > $(@D)/log.config 2>&1 @echo 'done' -gccpatch = $(TARGETDIR)/gcc-patched +GCC_PATCHED = $(TARGETDIR)/gcc-patched ################################################################################ # For some reason cpp is not created as a target-compiler ifeq ($(HOST),$(TARGET)) - $(gccpatch) : $(gcc) link_libs + $(GCC_PATCHED) : $(GCC) link_libs @echo -n 'Creating compiler symlinks...' @for f in cpp; do \ if [ ! -e $(PREFIX)/bin/$(TARGET)-$$f ]; \ @@ -587,7 +587,7 @@ ifeq ($(HOST),$(TARGET)) done;) @echo 'done' else - $(gccpatch) : + $(GCC_PATCHED) : @echo 'done' endif @@ -615,7 +615,7 @@ $(PREFIX)/devkit.info: echo '# This file describes to configure how to interpret the contents of this' >> $@ echo '# devkit' >> $@ echo '' >> $@ - echo 'DEVKIT_NAME="$(gcc_ver) - $(LINUX_VERSION)"' >> $@ + echo 'DEVKIT_NAME="$(GCC_VER) - $(LINUX_VERSION)"' >> $@ echo 'DEVKIT_TOOLCHAIN_PATH="$$DEVKIT_ROOT/bin"' >> $@ echo 'DEVKIT_SYSROOT="$$DEVKIT_ROOT/$(TARGET)/sysroot"' >> $@ echo 'DEVKIT_EXTRA_PATH="$$DEVKIT_ROOT/bin"' >> $@ @@ -651,32 +651,32 @@ ifeq ($(TARGET), $(HOST)) @echo 'Creating missing $* soft link' ln -s $(TARGET)-$* $@ - missing-links := $(addprefix $(PREFIX)/bin/, \ - addr2line ar as c++ c++filt dwp elfedit g++ gcc gcc-$(gcc_ver_only) gprof ld ld.bfd \ + MISSING_LINKS := $(addprefix $(PREFIX)/bin/, \ + addr2line ar as c++ c++filt dwp elfedit g++ gcc gcc-$(GCC_VER_ONLY) gprof ld ld.bfd \ ld.gold nm objcopy objdump ranlib readelf size strings strip) endif # Add link to work around "plugin needed to handle lto object" (JDK-8344272) -$(PREFIX)/lib/bfd-plugins/liblto_plugin.so: $(PREFIX)/libexec/gcc/$(TARGET)/$(gcc_ver_only)/liblto_plugin.so +$(PREFIX)/lib/bfd-plugins/liblto_plugin.so: $(PREFIX)/libexec/gcc/$(TARGET)/$(GCC_VER_ONLY)/liblto_plugin.so @echo 'Creating missing $(@F) soft link' @mkdir -p $(@D) ln -s $$(realpath -s --relative-to=$(@D) $<) $@ -missing-links += $(PREFIX)/lib/bfd-plugins/liblto_plugin.so +MISSING_LINKS += $(PREFIX)/lib/bfd-plugins/liblto_plugin.so ################################################################################ -bfdlib : $(bfdlib) -binutils : $(binutils) -rpms : $(rpms) -libs : $(libs) +bfdlib : $(BFDLIB) +binutils : $(BINUTILS) +rpms : $(RPMS) +libs : $(LIBS) sysroot : rpms libs -gcc : sysroot $(gcc) $(gccpatch) -gdb : $(gdb) -all : binutils gcc bfdlib $(PREFIX)/devkit.info $(missing-links) $(SYSROOT_LINKS) \ +gcc : sysroot $(GCC) $(GCC_PATCHED) +gdb : $(GDB) +all : binutils gcc bfdlib $(PREFIX)/devkit.info $(MISSING_LINKS) $(SYSROOT_LINKS) \ $(THESE_MAKEFILES) gdb # this is only built for host. so separate. -ccache : $(ccache) +ccache : $(CCACHE) .PHONY : gcc all binutils bfdlib link_libs rpms libs sysroot From 650fd35b3b30bf16e8caad968bd335d423c87b7d Mon Sep 17 00:00:00 2001 From: Prasanta Sadhukhan Date: Wed, 8 Oct 2025 03:00:30 +0000 Subject: [PATCH 391/556] 8335646: Nimbus : JLabel not painted with LAF defined foreground color on Ubuntu 24.04 Reviewed-by: aivanov, dnguyen, serb --- test/jdk/javax/swing/plaf/basic/BasicHTML/bug4248210.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jdk/javax/swing/plaf/basic/BasicHTML/bug4248210.java b/test/jdk/javax/swing/plaf/basic/BasicHTML/bug4248210.java index fddfbb28384..54f7744ee90 100644 --- a/test/jdk/javax/swing/plaf/basic/BasicHTML/bug4248210.java +++ b/test/jdk/javax/swing/plaf/basic/BasicHTML/bug4248210.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -71,7 +71,7 @@ public class bug4248210 { UIManager.getDefaults().put("Label.foreground", labelColor); } - JLabel label = new JLabel("Can You Read This?"); + JLabel label = new JLabel("\u2588 \u2588 \u2588 \u2588"); label.setSize(150, 30); BufferedImage img = paintToImage(label); From 2ac24bf1bac9c32704ebd72b93a75819b9404063 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Wed, 8 Oct 2025 03:06:29 +0000 Subject: [PATCH 392/556] 8367389: C2 SuperWord: refactor VTransform to model the whole loop instead of just the basic block Reviewed-by: roland, mhaessig --- src/hotspot/share/opto/phasetype.hpp | 7 +- src/hotspot/share/opto/superword.cpp | 220 +++++------------- src/hotspot/share/opto/superword.hpp | 6 +- .../share/opto/superwordVTransformBuilder.cpp | 60 +++-- .../share/opto/superwordVTransformBuilder.hpp | 2 + src/hotspot/share/opto/vectorization.cpp | 74 +++++- src/hotspot/share/opto/vectorization.hpp | 102 +++++--- src/hotspot/share/opto/vtransform.cpp | 149 ++++++++---- src/hotspot/share/opto/vtransform.hpp | 147 +++++++----- .../lib/ir_framework/CompilePhase.java | 6 +- 10 files changed, 447 insertions(+), 326 deletions(-) diff --git a/src/hotspot/share/opto/phasetype.hpp b/src/hotspot/share/opto/phasetype.hpp index 5c733c7dc0a..f24938b51c1 100644 --- a/src/hotspot/share/opto/phasetype.hpp +++ b/src/hotspot/share/opto/phasetype.hpp @@ -89,10 +89,9 @@ flags(PHASEIDEALLOOP2, "PhaseIdealLoop 2") \ flags(PHASEIDEALLOOP3, "PhaseIdealLoop 3") \ flags(AUTO_VECTORIZATION1_BEFORE_APPLY, "AutoVectorization 1, before Apply") \ - flags(AUTO_VECTORIZATION2_AFTER_REORDER, "AutoVectorization 2, after Apply Memop Reordering") \ - flags(AUTO_VECTORIZATION3_AFTER_ADJUST_LIMIT, "AutoVectorization 3, after Adjusting Pre-loop Limit") \ - flags(AUTO_VECTORIZATION4_AFTER_SPECULATIVE_RUNTIME_CHECKS, "AutoVectorization 4, after Adding Speculative Runtime Checks") \ - flags(AUTO_VECTORIZATION5_AFTER_APPLY, "AutoVectorization 5, after Apply") \ + flags(AUTO_VECTORIZATION3_AFTER_ADJUST_LIMIT, "AutoVectorization 2, after Adjusting Pre-loop Limit") \ + flags(AUTO_VECTORIZATION4_AFTER_SPECULATIVE_RUNTIME_CHECKS, "AutoVectorization 3, after Adding Speculative Runtime Checks") \ + flags(AUTO_VECTORIZATION5_AFTER_APPLY, "AutoVectorization 4, after Apply") \ flags(BEFORE_CCP1, "Before PhaseCCP 1") \ flags(CCP1, "PhaseCCP 1") \ flags(ITER_GVN2, "Iter GVN 2") \ diff --git a/src/hotspot/share/opto/superword.cpp b/src/hotspot/share/opto/superword.cpp index 2b3928781b8..41a4339e4c9 100644 --- a/src/hotspot/share/opto/superword.cpp +++ b/src/hotspot/share/opto/superword.cpp @@ -40,7 +40,7 @@ SuperWord::SuperWord(const VLoopAnalyzer &vloop_analyzer) : NOT_PRODUCT(COMMA is_trace_superword_packset()) NOT_PRODUCT(COMMA is_trace_superword_rejections()) ), - _mem_ref_for_main_loop_alignment(nullptr), + _vpointer_for_main_loop_alignment(nullptr), _aw_for_main_loop_alignment(0), _do_vector_loop(phase()->C->do_vector_loop()), // whether to do vectorization/simd style _num_work_vecs(0), // amount of vector work we have @@ -455,11 +455,16 @@ bool SuperWord::transform_loop() { // // 8) The pairs are combined into vector sized packs. // -// 9) Reorder the memory slices to co-locate members of the memory packs. +// 9) The packs are split and filtered, to ensure correctness and that +// all packs have corresponding vector nodes implemented in the backend. // -// 10) Generate ideal vector nodes for the final set of packs and where necessary, -// inserting scalar promotion, vector creation from multiple scalars, and -// extraction of scalar values from vectors. +// 10) VTransform (see vtransform.hpp) +// - construct from PackSet +// - schedule (detect circles) +// - apply +// - align main loop +// - add runtime checks (aliasing and alignment) +// - build new loop with vector C2 nodes // // Runtime Checks: // Some required properties cannot be proven statically, and require a @@ -498,7 +503,7 @@ bool SuperWord::SLP_extract() { DEBUG_ONLY(verify_packs();) DEBUG_ONLY(verify_no_extract()); - return schedule_and_apply(); + return do_vtransform(); } int SuperWord::MemOp::cmp_by_group(MemOp* a, MemOp* b) { @@ -660,39 +665,9 @@ void SuperWord::create_adjacent_memop_pairs_in_one_group(const GrowableArrayfast_outs(imax); i < imax; i++) { - PhiNode* phi = cl->fast_out(i)->isa_Phi(); - if (phi != nullptr && _vloop.in_bb(phi) && phi->is_memory_phi()) { - Node* phi_tail = phi->in(LoopNode::LoopBackControl); - if (phi_tail != phi->in(LoopNode::EntryControl)) { - _heads.push(phi); - _tails.push(phi_tail->as_Mem()); - } - } - } - - NOT_PRODUCT( if (_vloop.is_trace_memory_slices()) { print(); } ) -} - -#ifndef PRODUCT -void VLoopMemorySlices::print() const { - tty->print_cr("\nVLoopMemorySlices::print: %s", - heads().length() > 0 ? "" : "NONE"); - for (int m = 0; m < heads().length(); m++) { - tty->print("%6d ", m); heads().at(m)->dump(); - tty->print(" "); tails().at(m)->dump(); - } -} -#endif - // Get all memory nodes of a slice, in reverse order void VLoopMemorySlices::get_slice_in_reverse_order(PhiNode* head, MemNode* tail, GrowableArray &slice) const { + assert(head != nullptr && tail != nullptr, "must be slice with memory state loop"); assert(slice.is_empty(), "start empty"); Node* n = tail; Node* prev = nullptr; @@ -1576,7 +1551,7 @@ void SuperWord::filter_packs_for_alignment() { MemNode const* mem = current->as_constrained()->mem_ref(); Node_List* pack = get_pack(mem); assert(pack != nullptr, "memop of final solution must still be packed"); - _mem_ref_for_main_loop_alignment = mem; + _vpointer_for_main_loop_alignment = &vpointer(mem); _aw_for_main_loop_alignment = pack->size() * mem->memory_size(); } } @@ -1946,7 +1921,9 @@ void PackSet::verify() const { } #endif -bool SuperWord::schedule_and_apply() const { +// Build VTransform from SuperWord Packset, and eventually apply it (create new vectorized C2 loop). +// See description at top of "vtransform.hpp". +bool SuperWord::do_vtransform() const { if (_packset.is_empty()) { return false; } // Make an empty transform. @@ -1959,7 +1936,7 @@ bool SuperWord::schedule_and_apply() const { is_trace_superword_info()); #endif VTransform vtransform(_vloop_analyzer, - _mem_ref_for_main_loop_alignment, + _vpointer_for_main_loop_alignment, _aw_for_main_loop_alignment NOT_PRODUCT(COMMA trace) ); @@ -1988,6 +1965,7 @@ bool SuperWord::schedule_and_apply() const { // Apply the vectorization, i.e. we irreversibly edit the C2 graph. At this point, all // correctness and profitability checks have passed, and the graph was successfully scheduled. +// See description at top of "vtransform.hpp". void VTransform::apply() { #ifndef PRODUCT if (_trace._info || TraceLoopOpts) { @@ -2002,9 +1980,6 @@ void VTransform::apply() { Compile* C = phase()->C; C->print_method(PHASE_AUTO_VECTORIZATION1_BEFORE_APPLY, 4, cl()); - _graph.apply_memops_reordering_with_schedule(); - C->print_method(PHASE_AUTO_VECTORIZATION2_AFTER_REORDER, 4, cl()); - adjust_pre_loop_limit_to_align_main_loop_vectors(); C->print_method(PHASE_AUTO_VECTORIZATION3_AFTER_ADJUST_LIMIT, 4, cl()); @@ -2016,102 +1991,11 @@ void VTransform::apply() { C->print_method(PHASE_AUTO_VECTORIZATION5_AFTER_APPLY, 4, cl()); } -// We prepare the memory graph for the replacement of scalar memops with vector memops. -// We reorder all slices in parallel, ensuring that the memops inside each slice are -// ordered according to the _schedule. This means that all packed memops are consecutive -// in the memory graph after the reordering. -void VTransformGraph::apply_memops_reordering_with_schedule() const { -#ifndef PRODUCT - assert(is_scheduled(), "must be already scheduled"); - if (_trace._info) { - print_memops_schedule(); - } -#endif - - ResourceMark rm; - int max_slices = phase()->C->num_alias_types(); - // When iterating over the schedule, we keep track of the current memory state, - // which is the Phi or a store in the loop. - GrowableArray current_state_in_slice(max_slices, max_slices, nullptr); - // The memory state after the loop is the last store inside the loop. If we reorder the - // loop we may have a different last store, and we need to adjust the uses accordingly. - GrowableArray old_last_store_in_slice(max_slices, max_slices, nullptr); - - const GrowableArray& mem_slice_head = _vloop_analyzer.memory_slices().heads(); - - // (1) Set up the initial memory state from Phi. And find the old last store. - for (int i = 0; i < mem_slice_head.length(); i++) { - Node* phi = mem_slice_head.at(i); - assert(phi->is_Phi(), "must be phi"); - int alias_idx = phase()->C->get_alias_index(phi->adr_type()); - current_state_in_slice.at_put(alias_idx, phi); - - // If we have a memory phi, we have a last store in the loop, find it over backedge. - StoreNode* last_store = phi->in(2)->as_Store(); - old_last_store_in_slice.at_put(alias_idx, last_store); - } - - // (2) Walk over schedule, append memops to the current state - // of that slice. If it is a Store, we take it as the new state. - for_each_memop_in_schedule([&] (MemNode* n) { - assert(n->is_Load() || n->is_Store(), "only loads or stores"); - int alias_idx = phase()->C->get_alias_index(n->adr_type()); - Node* current_state = current_state_in_slice.at(alias_idx); - if (current_state == nullptr) { - // If there are only loads in a slice, we never update the memory - // state in the loop, hence there is no phi for the memory state. - // We just keep the old memory state that was outside the loop. - assert(n->is_Load() && !in_bb(n->in(MemNode::Memory)), - "only loads can have memory state from outside loop"); - } else { - igvn().replace_input_of(n, MemNode::Memory, current_state); - if (n->is_Store()) { - current_state_in_slice.at_put(alias_idx, n); - } - } - }); - - // (3) For each slice, we add the current state to the backedge - // in the Phi. Further, we replace uses of the old last store - // with uses of the new last store (current_state). - GrowableArray uses_after_loop; - for (int i = 0; i < mem_slice_head.length(); i++) { - Node* phi = mem_slice_head.at(i); - int alias_idx = phase()->C->get_alias_index(phi->adr_type()); - Node* current_state = current_state_in_slice.at(alias_idx); - assert(current_state != nullptr, "slice is mapped"); - assert(current_state != phi, "did some work in between"); - assert(current_state->is_Store(), "sanity"); - igvn().replace_input_of(phi, 2, current_state); - - // Replace uses of old last store with current_state (new last store) - // Do it in two loops: first find all the uses, and change the graph - // in as second loop so that we do not break the iterator. - Node* last_store = old_last_store_in_slice.at(alias_idx); - assert(last_store != nullptr, "we have a old last store"); - uses_after_loop.clear(); - for (DUIterator_Fast kmax, k = last_store->fast_outs(kmax); k < kmax; k++) { - Node* use = last_store->fast_out(k); - if (!in_bb(use)) { - uses_after_loop.push(use); - } - } - for (int k = 0; k < uses_after_loop.length(); k++) { - Node* use = uses_after_loop.at(k); - for (uint j = 0; j < use->req(); j++) { - Node* def = use->in(j); - if (def == last_store) { - igvn().replace_input_of(use, j, current_state); - } - } - } - } -} - void VTransformGraph::apply_vectorization_for_each_vtnode(uint& max_vector_length, uint& max_vector_width) const { ResourceMark rm; VTransformApplyState apply_state(_vloop_analyzer, _vtnodes.length()); + // Apply: transform the node and connect with inputs (no backedges). for (int i = 0; i < _schedule.length(); i++) { VTransformNode* vtn = _schedule.at(i); VTransformApplyResult result = vtn->apply(apply_state); @@ -2121,6 +2005,16 @@ void VTransformGraph::apply_vectorization_for_each_vtnode(uint& max_vector_lengt max_vector_length = MAX2(max_vector_length, result.vector_length()); max_vector_width = MAX2(max_vector_width, result.vector_width()); } + + // Cleanup: connect backedges + for (int i = 0; i < _schedule.length(); i++) { + VTransformNode* vtn = _schedule.at(i); + vtn->apply_backedge(apply_state); + } + + // Memory uses after the loop: used to connect to old last store, + // now need to connect to new last store. + apply_state.fix_memory_state_uses_after_loop(); } // We call "apply" on every VTransformNode, which replaces the packed scalar nodes with vector nodes. @@ -2774,10 +2668,10 @@ bool VLoopMemorySlices::same_memory_slice(MemNode* m1, MemNode* m2) const { _vloop.phase()->C->get_alias_index(m2->adr_type()); } -LoadNode::ControlDependency VTransformLoadVectorNode::control_dependency() const { +LoadNode::ControlDependency SuperWordVTransformBuilder::load_control_dependency(const Node_List* pack) const { LoadNode::ControlDependency dep = LoadNode::DependsOnlyOnTest; - for (int i = 0; i < nodes().length(); i++) { - Node* n = nodes().at(i); + for (uint i = 0; i < pack->size(); i++) { + Node* n = pack->at(i); assert(n->is_Load(), "only meaningful for loads"); if (!n->depends_only_on_test()) { if (n->as_Load()->has_unknown_control_dependency() && @@ -2795,22 +2689,24 @@ LoadNode::ControlDependency VTransformLoadVectorNode::control_dependency() const // Find the memop pack with the maximum vector width, unless they were already // determined (e.g. by SuperWord::filter_packs_for_alignment()). -void VTransform::determine_mem_ref_and_aw_for_main_loop_alignment() { - if (_mem_ref_for_main_loop_alignment != nullptr) { - assert(VLoop::vectors_should_be_aligned(), "mem_ref only set if filtered for alignment"); +void VTransform::determine_vpointer_and_aw_for_main_loop_alignment() { + if (_vpointer_for_main_loop_alignment != nullptr) { + assert(VLoop::vectors_should_be_aligned(), "vpointer_for_main_loop_alignment only set if filtered for alignment"); return; } - MemNode const* mem_ref = nullptr; + VPointer const* vpointer = nullptr; int max_aw = 0; + bool vpointer_is_load = false; const GrowableArray& vtnodes = _graph.vtnodes(); for (int i = 0; i < vtnodes.length(); i++) { VTransformMemVectorNode* vtn = vtnodes.at(i)->isa_MemVector(); if (vtn == nullptr) { continue; } - MemNode* p0 = vtn->nodes().at(0)->as_Mem(); - int vw = p0->memory_size() * vtn->nodes().length(); + int vw = vtn->vpointer().size(); + bool vtn_is_load = vtn->is_load_in_loop(); + // Generally, we prefer to align with the largest memory op (load or store). // If there are multiple, then SuperWordAutomaticAlignment determines if we // prefer loads or stores. @@ -2820,15 +2716,16 @@ void VTransform::determine_mem_ref_and_aw_for_main_loop_alignment() { // it is worse if a store is split, and less bad if a load is split. // By default, we have SuperWordAutomaticAlignment=1, i.e. we align with a // store if possible, to avoid splitting that store. - bool prefer_store = mem_ref != nullptr && SuperWordAutomaticAlignment == 1 && mem_ref->is_Load() && p0->is_Store(); - bool prefer_load = mem_ref != nullptr && SuperWordAutomaticAlignment == 2 && mem_ref->is_Store() && p0->is_Load(); + bool prefer_store = SuperWordAutomaticAlignment == 1 && vpointer_is_load && !vtn_is_load; + bool prefer_load = SuperWordAutomaticAlignment == 2 && !vpointer_is_load && vtn_is_load; if (vw > max_aw || (vw == max_aw && (prefer_load || prefer_store))) { + vpointer = &vtn->vpointer(); max_aw = vw; - mem_ref = p0; + vpointer_is_load = vtn_is_load; } } - assert(mem_ref != nullptr && max_aw > 0, "found mem_ref and aw"); - _mem_ref_for_main_loop_alignment = mem_ref; + assert(vpointer != nullptr && max_aw > 0, "found vpointer and aw"); + _vpointer_for_main_loop_alignment = vpointer; _aw_for_main_loop_alignment = max_aw; } @@ -2842,13 +2739,17 @@ void VTransform::determine_mem_ref_and_aw_for_main_loop_alignment() { } \ // Ensure that the main loop vectors are aligned by adjusting the pre loop limit. We memory-align -// the address of "_mem_ref_for_main_loop_alignment" to "_aw_for_main_loop_alignment", which is a +// the address of "_vpointer_for_main_loop_alignment" to "_aw_for_main_loop_alignment", which is a // sufficiently large alignment width. We adjust the pre-loop iteration count by adjusting the // pre-loop limit. void VTransform::adjust_pre_loop_limit_to_align_main_loop_vectors() { - determine_mem_ref_and_aw_for_main_loop_alignment(); - const MemNode* align_to_ref = _mem_ref_for_main_loop_alignment; - const int aw = _aw_for_main_loop_alignment; + determine_vpointer_and_aw_for_main_loop_alignment(); + + assert(cl()->is_main_loop(), "can only do alignment for main loop"); + assert(_vpointer_for_main_loop_alignment != nullptr && + _vpointer_for_main_loop_alignment->is_valid() && + _aw_for_main_loop_alignment > 0, + "must have alignment reference and aw"); if (!VLoop::vectors_should_be_aligned() && SuperWordAutomaticAlignment == 0) { #ifdef ASSERT @@ -2859,8 +2760,8 @@ void VTransform::adjust_pre_loop_limit_to_align_main_loop_vectors() { return; } - assert(align_to_ref != nullptr && aw > 0, "must have alignment reference and aw"); - assert(cl()->is_main_loop(), "can only do alignment for main loop"); + const VPointer& p = *_vpointer_for_main_loop_alignment; + const int aw = _aw_for_main_loop_alignment; // The opaque node for the limit, where we adjust the input Opaque1Node* pre_opaq = _vloop.pre_loop_end()->limit()->as_Opaque1(); @@ -2875,10 +2776,7 @@ void VTransform::adjust_pre_loop_limit_to_align_main_loop_vectors() { Node* orig_limit = pre_opaq->original_loop_limit(); assert(orig_limit != nullptr && igvn().type(orig_limit) != Type::TOP, ""); - const VPointer& p = vpointer(align_to_ref); - assert(p.is_valid(), "sanity"); - - // For the main-loop, we want the address of align_to_ref to be memory aligned + // For the main-loop, we want the address of vpointer p to be memory aligned // with some alignment width (aw, a power of 2). When we enter the main-loop, // we know that iv is equal to the pre-loop limit. If we adjust the pre-loop // limit by executing adjust_pre_iter many extra iterations, we can change the @@ -3013,9 +2911,7 @@ void VTransform::adjust_pre_loop_limit_to_align_main_loop_vectors() { #ifdef ASSERT if (_trace._align_vector) { tty->print_cr("\nVTransform::adjust_pre_loop_limit_to_align_main_loop_vectors:"); - tty->print(" align_to_ref:"); - align_to_ref->dump(); - tty->print(" "); + tty->print(" vpointer_for_main_loop_alignment"); p.print_on(tty); tty->print_cr(" aw: %d", aw); tty->print_cr(" iv_stride: %d", iv_stride); diff --git a/src/hotspot/share/opto/superword.hpp b/src/hotspot/share/opto/superword.hpp index 0940e752f85..118e0aa042c 100644 --- a/src/hotspot/share/opto/superword.hpp +++ b/src/hotspot/share/opto/superword.hpp @@ -410,9 +410,9 @@ class SuperWord : public ResourceObj { PairSet _pairset; PackSet _packset; - // Memory reference, and the alignment width (aw) for which we align the main-loop, + // VPointer, and the alignment width (aw) for which we align the main-loop, // by adjusting the pre-loop limit. - MemNode const* _mem_ref_for_main_loop_alignment; + VPointer const* _vpointer_for_main_loop_alignment; int _aw_for_main_loop_alignment; public: @@ -657,7 +657,7 @@ private: bool is_velt_basic_type_compatible_use_def(Node* use, Node* def) const; - bool schedule_and_apply() const; + bool do_vtransform() const; }; #endif // SHARE_OPTO_SUPERWORD_HPP diff --git a/src/hotspot/share/opto/superwordVTransformBuilder.cpp b/src/hotspot/share/opto/superwordVTransformBuilder.cpp index dbc96c234a9..45c919ccffa 100644 --- a/src/hotspot/share/opto/superwordVTransformBuilder.cpp +++ b/src/hotspot/share/opto/superwordVTransformBuilder.cpp @@ -37,6 +37,10 @@ void SuperWordVTransformBuilder::build() { VectorSet vtn_memory_dependencies; // Shared, but cleared for every vtnode. build_inputs_for_vector_vtnodes(vtn_memory_dependencies); build_inputs_for_scalar_vtnodes(vtn_memory_dependencies); + + // Build vtnodes for all uses of nodes from the loop, and connect them + // as outputs to the nodes in the loop. + build_uses_after_loop(); } void SuperWordVTransformBuilder::build_vector_vtnodes_for_packed_nodes() { @@ -50,8 +54,8 @@ void SuperWordVTransformBuilder::build_vector_vtnodes_for_packed_nodes() { } void SuperWordVTransformBuilder::build_scalar_vtnodes_for_non_packed_nodes() { - for (int i = 0; i < _vloop_analyzer.body().body().length(); i++) { - Node* n = _vloop_analyzer.body().body().at(i); + for (uint i = 0; i < _vloop.lpt()->_body.size(); i++) { + Node* n = _vloop.lpt()->_body.at(i); if (_packset.get_pack(n) != nullptr) { continue; } VTransformNode* vtn = nullptr; @@ -61,6 +65,8 @@ void SuperWordVTransformBuilder::build_scalar_vtnodes_for_non_packed_nodes() { vtn = new (_vtransform.arena()) VTransformMemopScalarNode(_vtransform, mem, mem_p); } else if (n->is_Phi()) { vtn = new (_vtransform.arena()) VTransformLoopPhiNode(_vtransform, n->as_Phi()); + } else if (n->is_CountedLoop()) { + vtn = new (_vtransform.arena()) VTransformCountedLoopNode(_vtransform, n->as_CountedLoop()); } else if (n->is_CFG()) { vtn = new (_vtransform.arena()) VTransformCFGNode(_vtransform, n); } else { @@ -121,8 +127,8 @@ void SuperWordVTransformBuilder::build_inputs_for_vector_vtnodes(VectorSet& vtn_ } void SuperWordVTransformBuilder::build_inputs_for_scalar_vtnodes(VectorSet& vtn_memory_dependencies) { - for (int i = 0; i < _vloop_analyzer.body().body().length(); i++) { - Node* n = _vloop_analyzer.body().body().at(i); + for (uint i = 0; i < _vloop.lpt()->_body.size(); i++) { + Node* n = _vloop.lpt()->_body.at(i); VTransformNode* vtn = get_vtnode(n); if (vtn->isa_Vector() != nullptr) { continue; } vtn_memory_dependencies.clear(); // Add every dependency only once per vtn. @@ -135,18 +141,41 @@ void SuperWordVTransformBuilder::build_inputs_for_scalar_vtnodes(VectorSet& vtn_ init_req_with_scalar(n, vtn, MemNode::ValueIn); add_memory_dependencies_of_node_to_vtnode(n, vtn, vtn_memory_dependencies); } else if (n->is_CountedLoop()) { - continue; // Is "root", has no dependency. - } else if (n->is_Phi()) { - // CountedLoop Phi's: ignore backedge (and entry value). - assert(n->in(0) == _vloop.cl(), "only Phi's from the CountedLoop allowed"); - init_req_with_scalar(n, vtn, 0); - continue; + // Avoid self-loop, it only creates unnecessary issues in scheduling. + init_req_with_scalar(n, vtn, LoopNode::EntryControl); + init_req_with_scalar(n, vtn, LoopNode::LoopBackControl); } else { init_all_req_with_scalars(n, vtn); } } } +// Build vtnodes for all uses of nodes from the loop, and connect them +// as outputs to the nodes in the loop. +void SuperWordVTransformBuilder::build_uses_after_loop() { + for (uint i = 0; i < _vloop.lpt()->_body.size(); i++) { + Node* n = _vloop.lpt()->_body.at(i); + VTransformNode* vtn = get_vtnode(n); + + for (DUIterator_Fast imax, i = n->fast_outs(imax); i < imax; i++) { + Node* use = n->fast_out(i); + + if (!_vloop.in_bb(use)) { + VTransformNode* vtn_use = get_vtnode_or_wrap_as_outer(use); + + // Set all edges + for (uint j = 0; j < use->req(); j++) { + Node* def = use->in(j); + if (n == def && vtn_use->in_req(j) != vtn) { + assert(vtn_use->in_req(j) == nullptr, "should not yet be set"); + vtn_use->init_req(j, vtn); + } + } + } + } + } +} + // Create a vtnode for each pack. No in/out edges set yet. VTransformVectorNode* SuperWordVTransformBuilder::make_vector_vtnode_for_pack(const Node_List* pack) const { Node* p0 = pack->at(0); @@ -159,7 +188,8 @@ VTransformVectorNode* SuperWordVTransformBuilder::make_vector_vtnode_for_pack(co if (p0->is_Load()) { const VPointer& scalar_p = _vloop_analyzer.vpointers().vpointer(p0->as_Load()); const VPointer vector_p(scalar_p.make_with_size(scalar_p.size() * vlen)); - vtn = new (_vtransform.arena()) VTransformLoadVectorNode(_vtransform, properties, vector_p, p0->adr_type()); + const LoadNode::ControlDependency control_dependency = load_control_dependency(pack); + vtn = new (_vtransform.arena()) VTransformLoadVectorNode(_vtransform, properties, vector_p, p0->adr_type(), control_dependency); } else if (p0->is_Store()) { const VPointer& scalar_p = _vloop_analyzer.vpointers().vpointer(p0->as_Store()); const VPointer vector_p(scalar_p.make_with_size(scalar_p.size() * vlen)); @@ -209,7 +239,6 @@ VTransformVectorNode* SuperWordVTransformBuilder::make_vector_vtnode_for_pack(co int vopc = VectorNode::opcode(sopc, bt); vtn = new (_vtransform.arena()) VTransformElementWiseVectorNode(_vtransform, p0->req(), properties, vopc); } - vtn->set_nodes(pack); return vtn; } @@ -276,15 +305,15 @@ VTransformNode* SuperWordVTransformBuilder::get_or_make_vtnode_vector_input_at_i // case of a ConvL2I, it can be int or some narrower type such // as short etc. But given we replicate the input of the Convert // node, we have to use the input type instead. - BasicType element_type = p0->is_Convert() ? p0->in(1)->bottom_type()->basic_type() : _vloop_analyzer.types().velt_basic_type(p0); - if (index == 2 && VectorNode::is_scalar_rotate(p0) && element_type == T_LONG) { + BasicType element_bt = p0->is_Convert() ? p0->in(1)->bottom_type()->basic_type() : _vloop_analyzer.types().velt_basic_type(p0); + if (index == 2 && VectorNode::is_scalar_rotate(p0) && element_bt == T_LONG) { // Scalar rotate has int rotation value, but the scalar rotate expects longs. assert(same_input->bottom_type()->isa_int(), "scalar rotate expects int rotation"); VTransformNode* conv = new (_vtransform.arena()) VTransformConvI2LNode(_vtransform); conv->init_req(1, same_input_vtn); same_input_vtn = conv; } - VTransformNode* replicate = new (_vtransform.arena()) VTransformReplicateNode(_vtransform, pack->size(), element_type); + VTransformNode* replicate = new (_vtransform.arena()) VTransformReplicateNode(_vtransform, pack->size(), element_bt); replicate->init_req(1, same_input_vtn); return replicate; } @@ -307,6 +336,7 @@ VTransformNode* SuperWordVTransformBuilder::get_vtnode_or_wrap_as_outer(Node* n) assert(!_vloop.in_bb(n), "only nodes outside the loop can be input nodes to the loop"); vtn = new (_vtransform.arena()) VTransformOuterNode(_vtransform, n); map_node_to_vtnode(n, vtn); + assert(vtn == get_vtnode_or_null(n), "consistency"); return vtn; } diff --git a/src/hotspot/share/opto/superwordVTransformBuilder.hpp b/src/hotspot/share/opto/superwordVTransformBuilder.hpp index 6ed8480209a..cc9dd225f01 100644 --- a/src/hotspot/share/opto/superwordVTransformBuilder.hpp +++ b/src/hotspot/share/opto/superwordVTransformBuilder.hpp @@ -56,6 +56,7 @@ private: void build_scalar_vtnodes_for_non_packed_nodes(); void build_inputs_for_vector_vtnodes(VectorSet& vtn_memory_dependencies); void build_inputs_for_scalar_vtnodes(VectorSet& vtn_memory_dependencies); + void build_uses_after_loop(); // Helper methods for building VTransform. VTransformNode* get_vtnode_or_null(Node* n) const { @@ -82,6 +83,7 @@ private: void init_all_req_with_scalars(Node* n, VTransformNode* vtn); void init_all_req_with_vectors(const Node_List* pack, VTransformNode* vtn); void add_memory_dependencies_of_node_to_vtnode(Node* n, VTransformNode* vtn, VectorSet& vtn_memory_dependencies); + LoadNode::ControlDependency load_control_dependency(const Node_List* pack) const; }; #endif // SHARE_OPTO_SUPERWORD_VTRANSFORM_BUILDER_HPP diff --git a/src/hotspot/share/opto/vectorization.cpp b/src/hotspot/share/opto/vectorization.cpp index 8e0f0980ff7..5c4e15fdbb9 100644 --- a/src/hotspot/share/opto/vectorization.cpp +++ b/src/hotspot/share/opto/vectorization.cpp @@ -182,6 +182,11 @@ VStatus VLoopAnalyzer::setup_submodules_helper() { _reductions.mark_reductions(); } + VStatus body_status = _body.construct(); + if (!body_status.is_success()) { + return body_status; + } + _memory_slices.find_memory_slices(); // If there is no memory slice detected, it means there is no store. @@ -192,11 +197,6 @@ VStatus VLoopAnalyzer::setup_submodules_helper() { return VStatus::make_failure(VLoopAnalyzer::FAILURE_NO_REDUCTION_OR_STORE); } - VStatus body_status = _body.construct(); - if (!body_status.is_success()) { - return body_status; - } - _types.compute_vector_element_type(); _vpointers.compute_vpointers(); @@ -206,6 +206,64 @@ VStatus VLoopAnalyzer::setup_submodules_helper() { return VStatus::make_success(); } +// There are 2 kinds of slices: +// - No memory phi: only loads. All have the same input memory state from before the loop. +// - With memory phi. Chain of memory operations inside the loop. +void VLoopMemorySlices::find_memory_slices() { + Compile* C = _vloop.phase()->C; + // We iterate over the body, which is topologically sorted. Hence, if there is a phi + // in a slice, we will find it first, and the loads and stores afterwards. + for (int i = 0; i < _body.body().length(); i++) { + Node* n = _body.body().at(i); + if (n->is_memory_phi()) { + // Memory slice with stores (and maybe loads) + PhiNode* phi = n->as_Phi(); + int alias_idx = C->get_alias_index(phi->adr_type()); + assert(_inputs.at(alias_idx) == nullptr, "did not yet touch this slice"); + _inputs.at_put(alias_idx, phi->in(1)); + _heads.at_put(alias_idx, phi); + } else if (n->is_Load()) { + LoadNode* load = n->as_Load(); + int alias_idx = C->get_alias_index(load->adr_type()); + PhiNode* head = _heads.at(alias_idx); + if (head == nullptr) { + // We did not find a phi on this slice yet -> must be a slice with only loads. + assert(_inputs.at(alias_idx) == nullptr || _inputs.at(alias_idx) == load->in(1), + "not yet touched or the same input"); + _inputs.at_put(alias_idx, load->in(1)); + } // else: the load belongs to a slice with a phi that already set heads and inputs. +#ifdef ASSERT + } else if (n->is_Store()) { + // Found a store. Make sure it is in a slice with a Phi. + StoreNode* store = n->as_Store(); + int alias_idx = C->get_alias_index(store->adr_type()); + PhiNode* head = _heads.at(alias_idx); + assert(head != nullptr, "should have found a mem phi for this slice"); +#endif + } + } + NOT_PRODUCT( if (_vloop.is_trace_memory_slices()) { print(); } ) +} + +#ifndef PRODUCT +void VLoopMemorySlices::print() const { + tty->print_cr("\nVLoopMemorySlices::print: %s", + heads().length() > 0 ? "" : "NONE"); + for (int i = 0; i < _inputs.length(); i++) { + Node* input = _inputs.at(i); + PhiNode* head = _heads.at(i); + if (input != nullptr) { + tty->print("%3d input", i); input->dump(); + if (head == nullptr) { + tty->print_cr(" load only"); + } else { + tty->print(" head "); head->dump(); + } + } + } +} +#endif + void VLoopVPointers::compute_vpointers() { count_vpointers(); allocate_vpointers_array(); @@ -267,7 +325,6 @@ void VLoopVPointers::print() const { // the edge, i.e. spaw the order. void VLoopDependencyGraph::construct() { const GrowableArray& mem_slice_heads = _memory_slices.heads(); - const GrowableArray& mem_slice_tails = _memory_slices.tails(); ResourceMark rm; GrowableArray slice_nodes; @@ -277,7 +334,10 @@ void VLoopDependencyGraph::construct() { // For each memory slice, create the memory subgraph for (int i = 0; i < mem_slice_heads.length(); i++) { PhiNode* head = mem_slice_heads.at(i); - MemNode* tail = mem_slice_tails.at(i); + // If there is no head (memory-phi) for this slice, then we have either no memops + // in the loop, or only loads. We do not need to add any memory edges in that case. + if (head == nullptr) { continue; } + MemNode* tail = head->in(2)->as_Mem(); _memory_slices.get_slice_in_reverse_order(head, tail, slice_nodes); diff --git a/src/hotspot/share/opto/vectorization.hpp b/src/hotspot/share/opto/vectorization.hpp index b39e46cbf35..e006589cce9 100644 --- a/src/hotspot/share/opto/vectorization.hpp +++ b/src/hotspot/share/opto/vectorization.hpp @@ -379,37 +379,6 @@ private: static Node* original_input(const Node* n, uint i); }; -// Submodule of VLoopAnalyzer. -// Find the memory slices in the loop. -class VLoopMemorySlices : public StackObj { -private: - const VLoop& _vloop; - - GrowableArray _heads; - GrowableArray _tails; - -public: - VLoopMemorySlices(Arena* arena, const VLoop& vloop) : - _vloop(vloop), - _heads(arena, 8, 0, nullptr), - _tails(arena, 8, 0, nullptr) {}; - NONCOPYABLE(VLoopMemorySlices); - - void find_memory_slices(); - - const GrowableArray& heads() const { return _heads; } - const GrowableArray& tails() const { return _tails; } - - // Get all memory nodes of a slice, in reverse order - void get_slice_in_reverse_order(PhiNode* head, MemNode* tail, GrowableArray& slice) const; - - bool same_memory_slice(MemNode* m1, MemNode* m2) const; - -#ifndef PRODUCT - void print() const; -#endif -}; - // Submodule of VLoopAnalyzer. // Finds all nodes in the body, and creates a mapping node->_idx to a body_idx. // This mapping is used so that subsequent datastructures sizes only grow with @@ -461,6 +430,73 @@ private: } }; +// Submodule of VLoopAnalyzer. +// Find the memory slices in the loop. There are 3 kinds of slices: +// 1. no use in loop: inputs(i) = nullptr, heads(i) = nullptr +// 2. stores in loop: inputs(i) = entry_mem, heads(i) = phi_mem +// +// = entry_mem +// | +// CountedLoop | +-----------------------+ +// | v v | +// phi_mem | +// | | +// | +// | | +// +---------------------------+ +// | +// +// +// Note: the mem uses after the loop are dependent on the last store in the loop. +// Once we vectorize, we may reorder the loads and stores, and replace +// scalar mem ops with vector mem ops. We will have to make sure that all +// uses after the loop use the new last store. +// See: VTransformApplyState::fix_memory_state_uses_after_loop +// +// 3. only loads but no stores in loop: inputs(i) = entry_mem, heads(i) = nullptr +// +// = entry_mem +// | | +// | CountedLoop | +// | | | +// | +// | +// +// +// Note: the mem uses after the loop are NOT dependent any mem ops in the loop, +// since there are no stores. +// +class VLoopMemorySlices : public StackObj { +private: + const VLoop& _vloop; + const VLoopBody& _body; + + GrowableArray _inputs; + GrowableArray _heads; + +public: + VLoopMemorySlices(Arena* arena, const VLoop& vloop, const VLoopBody& body) : + _vloop(vloop), + _body(body), + _inputs(arena, num_slices(), num_slices(), nullptr), + _heads(arena, num_slices(), num_slices(), nullptr) {}; + NONCOPYABLE(VLoopMemorySlices); + + const GrowableArray& inputs() const { return _inputs; } + const GrowableArray& heads() const { return _heads; } + + void find_memory_slices(); + void get_slice_in_reverse_order(PhiNode* head, MemNode* tail, GrowableArray& slice) const; + bool same_memory_slice(MemNode* m1, MemNode* m2) const; + +private: +#ifndef PRODUCT + void print() const; +#endif + + int num_slices() const { return _vloop.phase()->C->num_alias_types(); } +}; + // Submodule of VLoopAnalyzer. // Compute the vector element type for every node in the loop body. // We need to do this to be able to vectorize the narrower integer @@ -737,8 +773,8 @@ private: // Submodules VLoopReductions _reductions; - VLoopMemorySlices _memory_slices; VLoopBody _body; + VLoopMemorySlices _memory_slices; VLoopTypes _types; VLoopVPointers _vpointers; VLoopDependencyGraph _dependency_graph; @@ -749,8 +785,8 @@ public: _arena(mtCompiler, Arena::Tag::tag_superword), _success(false), _reductions (&_arena, vloop), - _memory_slices (&_arena, vloop), _body (&_arena, vloop, vshared), + _memory_slices (&_arena, vloop, _body), _types (&_arena, vloop, _body), _vpointers (&_arena, vloop, _body), _dependency_graph(&_arena, vloop, _body, _memory_slices, _vpointers) diff --git a/src/hotspot/share/opto/vtransform.cpp b/src/hotspot/share/opto/vtransform.cpp index 8c1210a5a09..27c541c2732 100644 --- a/src/hotspot/share/opto/vtransform.cpp +++ b/src/hotspot/share/opto/vtransform.cpp @@ -78,6 +78,10 @@ bool VTransformGraph::schedule() { // runtime check, see VTransform::apply_speculative_aliasing_runtime_checks. for (uint i = 0; i < vtn->out_strong_edges(); i++) { VTransformNode* use = vtn->out_strong_edge(i); + + // Skip LoopPhi backedge. + if ((use->isa_LoopPhi() != nullptr || use->isa_CountedLoop() != nullptr) && use->in_req(2) == vtn) { continue; } + if (post_visited.test(use->_idx)) { continue; } if (pre_visited.test(use->_idx)) { // Cycle detected! @@ -120,6 +124,11 @@ void VTransformGraph::collect_nodes_without_strong_in_edges(GrowableArrayhas_strong_in_edge()) { stack.push(vtn); } + // If an Outer node has both inputs and outputs, we will most likely have cycles in the final graph. + // This is not a correctness problem, but it just will prevent vectorization. If this ever happens + // try to find a way to avoid the cycle somehow. + assert(vtn->isa_Outer() == nullptr || (vtn->has_strong_in_edge() != (vtn->out_strong_edges() > 0)), + "Outer nodes should either be inputs or outputs, but not both, otherwise we may get cycles"); } } @@ -717,28 +726,111 @@ Node* VTransformApplyState::transformed_node(const VTransformNode* vtn) const { return n; } +void VTransformApplyState::init_memory_states_and_uses_after_loop() { + const GrowableArray& inputs = _vloop_analyzer.memory_slices().inputs(); + const GrowableArray& heads = _vloop_analyzer.memory_slices().heads(); + for (int i = 0; i < inputs.length(); i++) { + PhiNode* head = heads.at(i); + if (head != nullptr) { + // Slice with Phi (i.e. with stores) -> start with the phi (phi_mem) + _memory_states.at_put(i, head); + + // Remember uses outside the loop of the last memory state (store). + StoreNode* last_store = head->in(2)->as_Store(); + assert(vloop().in_bb(last_store), "backedge store should be in the loop"); + for (DUIterator_Fast jmax, j = last_store->fast_outs(jmax); j < jmax; j++) { + Node* use = last_store->fast_out(j); + if (!vloop().in_bb(use)) { + for (uint k = 0; k < use->req(); k++) { + if (use->in(k) == last_store) { + _memory_state_uses_after_loop.push(MemoryStateUseAfterLoop(use, k, i)); + } + } + } + } + } else { + // Slice without Phi (i.e. only loads) -> use the input state (entry_mem) + _memory_states.at_put(i, inputs.at(i)); + } + } +} + +// We may have reordered the scalar stores, or replaced them with vectors. Now +// the last memory state in the loop may have changed. Thus, we need to change +// the uses of the old last memory state the new last memory state. +void VTransformApplyState::fix_memory_state_uses_after_loop() { + for (int i = 0; i < _memory_state_uses_after_loop.length(); i++) { + MemoryStateUseAfterLoop& use = _memory_state_uses_after_loop.at(i); + Node* last_state = memory_state(use._alias_idx); + phase()->igvn().replace_input_of(use._use, use._in_idx, last_state); + } +} + +void VTransformNode::apply_vtn_inputs_to_node(Node* n, VTransformApplyState& apply_state) const { + PhaseIdealLoop* phase = apply_state.phase(); + for (uint i = 0; i < req(); i++) { + VTransformNode* vtn_def = in_req(i); + if (vtn_def != nullptr) { + Node* def = apply_state.transformed_node(vtn_def); + phase->igvn().replace_input_of(n, i, def); + } + } +} + VTransformApplyResult VTransformMemopScalarNode::apply(VTransformApplyState& apply_state) const { - // This was just wrapped. Now we simply unwrap without touching the inputs. + apply_vtn_inputs_to_node(_node, apply_state); + // The memory state has to be applied separately: the vtn does not hold it. This allows reordering. + Node* mem = apply_state.memory_state(_node->adr_type()); + apply_state.phase()->igvn().replace_input_of(_node, 1, mem); + if (_node->is_Store()) { + apply_state.set_memory_state(_node->adr_type(), _node); + } + return VTransformApplyResult::make_scalar(_node); } VTransformApplyResult VTransformDataScalarNode::apply(VTransformApplyState& apply_state) const { - // This was just wrapped. Now we simply unwrap without touching the inputs. + apply_vtn_inputs_to_node(_node, apply_state); return VTransformApplyResult::make_scalar(_node); } VTransformApplyResult VTransformLoopPhiNode::apply(VTransformApplyState& apply_state) const { - // This was just wrapped. Now we simply unwrap without touching the inputs. + PhaseIdealLoop* phase = apply_state.phase(); + Node* in0 = apply_state.transformed_node(in_req(0)); + Node* in1 = apply_state.transformed_node(in_req(1)); + phase->igvn().replace_input_of(_node, 0, in0); + phase->igvn().replace_input_of(_node, 1, in1); + // Note: the backedge is hooked up later. return VTransformApplyResult::make_scalar(_node); } +// Cleanup backedges. In the schedule, the backedges come after their phis. Hence, +// we only have the transformed backedges after the phis are already transformed. +// We hook the backedges into the phis now, during cleanup. +void VTransformLoopPhiNode::apply_backedge(VTransformApplyState& apply_state) const { + PhaseIdealLoop* phase = apply_state.phase(); + if (_node->is_memory_phi()) { + // Memory phi/backedge + // The last memory state of that slice is the backedge. + Node* last_state = apply_state.memory_state(_node->adr_type()); + phase->igvn().replace_input_of(_node, 2, last_state); + } else { + // Data phi/backedge + Node* in2 = apply_state.transformed_node(in_req(2)); + phase->igvn().replace_input_of(_node, 2, in2); + } +} + VTransformApplyResult VTransformCFGNode::apply(VTransformApplyState& apply_state) const { - // This was just wrapped. Now we simply unwrap without touching the inputs. + // We do not modify the inputs of the CountedLoop (and certainly not its backedge) + if (!_node->is_CountedLoop()) { + apply_vtn_inputs_to_node(_node, apply_state); + } return VTransformApplyResult::make_scalar(_node); } VTransformApplyResult VTransformOuterNode::apply(VTransformApplyState& apply_state) const { - // This was just wrapped. Now we simply unwrap without touching the inputs. + apply_vtn_inputs_to_node(_node, apply_state); return VTransformApplyResult::make_scalar(_node); } @@ -797,7 +889,7 @@ VTransformApplyResult VTransformElementWiseVectorNode::apply(VTransformApplyStat vn = VectorNode::make(_vector_opcode, in1, in2, in3, vt); // ternary } - register_new_node_from_vectorization_and_replace_scalar_nodes(apply_state, vn); + register_new_node_from_vectorization(apply_state, vn); return VTransformApplyResult::make_vector(vn); } @@ -812,7 +904,7 @@ VTransformApplyResult VTransformElementWiseLongOpWithCastToIntVectorNode::apply( register_new_node_from_vectorization(apply_state, long_vn); // Cast long -> int, to mimic the scalar long -> int operation. VectorNode* vn = VectorCastNode::make(Op_VectorCastL2X, long_vn, T_INT, vlen); - register_new_node_from_vectorization_and_replace_scalar_nodes(apply_state, vn); + register_new_node_from_vectorization(apply_state, vn); return VTransformApplyResult::make_vector(vn); } @@ -824,7 +916,7 @@ VTransformApplyResult VTransformReinterpretVectorNode::apply(VTransformApplyStat Node* in1 = apply_state.transformed_node(in_req(1)); VectorNode* vn = new VectorReinterpretNode(in1, src_vt, dst_vt); - register_new_node_from_vectorization_and_replace_scalar_nodes(apply_state, vn); + register_new_node_from_vectorization(apply_state, vn); return VTransformApplyResult::make_vector(vn); } @@ -843,7 +935,7 @@ VTransformApplyResult VTransformBoolVectorNode::apply(VTransformApplyState& appl PhaseIdealLoop* phase = apply_state.phase(); ConINode* mask_node = phase->intcon((int)mask); VectorNode* vn = new VectorMaskCmpNode(mask, cmp_in1, cmp_in2, mask_node, vt); - register_new_node_from_vectorization_and_replace_scalar_nodes(apply_state, vn); + register_new_node_from_vectorization(apply_state, vn); return VTransformApplyResult::make_vector(vn); } @@ -852,7 +944,7 @@ VTransformApplyResult VTransformReductionVectorNode::apply(VTransformApplyState& Node* vec = apply_state.transformed_node(in_req(2)); ReductionNode* vn = ReductionNode::make(scalar_opcode(), nullptr, init, vec, element_basic_type()); - register_new_node_from_vectorization_and_replace_scalar_nodes(apply_state, vn); + register_new_node_from_vectorization(apply_state, vn); return VTransformApplyResult::make_vector(vn, vn->vect_type()); } @@ -861,10 +953,9 @@ VTransformApplyResult VTransformLoadVectorNode::apply(VTransformApplyState& appl uint vlen = vector_length(); BasicType bt = element_basic_type(); - LoadNode* first = nodes().at(0)->as_Load(); + // The memory state has to be applied separately: the vtn does not hold it. This allows reordering. Node* ctrl = apply_state.transformed_node(in_req(MemNode::Control)); - // first has the correct memory state, determined by VTransformGraph::apply_memops_reordering_with_schedule - Node* mem = first->in(MemNode::Memory); + Node* mem = apply_state.memory_state(_adr_type); Node* adr = apply_state.transformed_node(in_req(MemNode::Address)); // Set the memory dependency of the LoadVector as early as possible. @@ -880,10 +971,9 @@ VTransformApplyResult VTransformLoadVectorNode::apply(VTransformApplyState& appl } } - LoadVectorNode* vn = LoadVectorNode::make(sopc, ctrl, mem, adr, _adr_type, vlen, bt, - control_dependency()); + LoadVectorNode* vn = LoadVectorNode::make(sopc, ctrl, mem, adr, _adr_type, vlen, bt, _control_dependency); DEBUG_ONLY( if (VerifyAlignVector) { vn->set_must_verify_alignment(); } ) - register_new_node_from_vectorization_and_replace_scalar_nodes(apply_state, vn); + register_new_node_from_vectorization(apply_state, vn); return VTransformApplyResult::make_vector(vn, vn->vect_type()); } @@ -891,27 +981,17 @@ VTransformApplyResult VTransformStoreVectorNode::apply(VTransformApplyState& app int sopc = scalar_opcode(); uint vlen = vector_length(); - StoreNode* first = nodes().at(0)->as_Store(); + // The memory state has to be applied separately: the vtn does not hold it. This allows reordering. Node* ctrl = apply_state.transformed_node(in_req(MemNode::Control)); - // first has the correct memory state, determined by VTransformGraph::apply_memops_reordering_with_schedule - Node* mem = first->in(MemNode::Memory); + Node* mem = apply_state.memory_state(_adr_type); Node* adr = apply_state.transformed_node(in_req(MemNode::Address)); Node* value = apply_state.transformed_node(in_req(MemNode::ValueIn)); StoreVectorNode* vn = StoreVectorNode::make(sopc, ctrl, mem, adr, _adr_type, value, vlen); DEBUG_ONLY( if (VerifyAlignVector) { vn->set_must_verify_alignment(); } ) - register_new_node_from_vectorization_and_replace_scalar_nodes(apply_state, vn); - return VTransformApplyResult::make_vector(vn, vn->vect_type()); -} - -void VTransformVectorNode::register_new_node_from_vectorization_and_replace_scalar_nodes(VTransformApplyState& apply_state, Node* vn) const { - PhaseIdealLoop* phase = apply_state.phase(); register_new_node_from_vectorization(apply_state, vn); - - for (int i = 0; i < _nodes.length(); i++) { - Node* n = _nodes.at(i); - phase->igvn().replace_node(n, vn); - } + apply_state.set_memory_state(_adr_type, vn); + return VTransformApplyResult::make_vector(vn, vn->vect_type()); } void VTransformNode::register_new_node_from_vectorization(VTransformApplyState& apply_state, Node* vn) const { @@ -944,15 +1024,6 @@ void VTransformGraph::print_schedule() const { } } -void VTransformGraph::print_memops_schedule() const { - tty->print_cr("\nVTransformGraph::print_memops_schedule:"); - int i = 0; - for_each_memop_in_schedule([&] (MemNode* mem) { - tty->print(" %3d: ", i++); - mem->dump(); - }); -} - void VTransformNode::print() const { tty->print("%3d %s (", _idx, name()); for (uint i = 0; i < _req; i++) { diff --git a/src/hotspot/share/opto/vtransform.hpp b/src/hotspot/share/opto/vtransform.hpp index 9a4e4de01a2..a004962eea7 100644 --- a/src/hotspot/share/opto/vtransform.hpp +++ b/src/hotspot/share/opto/vtransform.hpp @@ -39,7 +39,7 @@ // // This is the life-cycle of a VTransform: // - Construction: -// - From SuperWord, with the SuperWordVTransformBuilder. +// - From SuperWord PackSet, with the SuperWordVTransformBuilder. // // - Future Plans: optimize, if-conversion, etc. // @@ -49,8 +49,16 @@ // // - Apply: // - Changes to the C2 IR are only made once the "apply" method is called. +// - Align the main loop, by adjusting pre loop limit. +// - Add speculative runtime checks (alignment and aliasing). // - Each vtnode generates its corresponding scalar and vector C2 nodes, -// possibly replacing old scalar C2 nodes. +// possibly replacing old scalar C2 nodes. We apply each vtnode in order +// of the schedule, so that all input vtnodes are already applied, i.e. +// all input vtnodes have already generated the transformed C2 nodes. +// - We also build the new memory graph on the fly. The schedule may have +// reordered the memory operations, and so we cannot use the old memory +// graph, but must build it from the scheduled order. We keep track of +// the current memory state in VTransformApplyState. // // Future Plans with VTransform: // - Cost model: estimate if vectorization is profitable. @@ -65,6 +73,7 @@ class VTransformMemopScalarNode; class VTransformDataScalarNode; class VTransformLoopPhiNode; class VTransformCFGNode; +class VTransformCountedLoopNode; class VTransformOuterNode; class VTransformVectorNode; class VTransformElementWiseVectorNode; @@ -176,7 +185,6 @@ public: bool schedule(); bool has_store_to_load_forwarding_failure(const VLoopAnalyzer& vloop_analyzer) const; - void apply_memops_reordering_with_schedule() const; void apply_vectorization_for_each_vtnode(uint& max_vector_length, uint& max_vector_width) const; private: @@ -187,13 +195,9 @@ private: void collect_nodes_without_strong_in_edges(GrowableArray& stack) const; - template - void for_each_memop_in_schedule(Callback callback) const; - #ifndef PRODUCT void print_vtnodes() const; void print_schedule() const; - void print_memops_schedule() const; void trace_schedule_cycle(const GrowableArray& stack, const VectorSet& pre_visited, const VectorSet& post_visited) const; @@ -215,14 +219,14 @@ private: VTransformGraph _graph; - // Memory reference, and the alignment width (aw) for which we align the main-loop, + // VPointer, and the alignment width (aw) for which we align the main-loop, // by adjusting the pre-loop limit. - MemNode const* _mem_ref_for_main_loop_alignment; + VPointer const* _vpointer_for_main_loop_alignment; int _aw_for_main_loop_alignment; public: VTransform(const VLoopAnalyzer& vloop_analyzer, - MemNode const* mem_ref_for_main_loop_alignment, + VPointer const* vpointer_for_main_loop_alignment, int aw_for_main_loop_alignment NOT_PRODUCT( COMMA const VTransformTrace trace) ) : @@ -231,7 +235,7 @@ public: NOT_PRODUCT(_trace(trace) COMMA) _arena(mtCompiler, Arena::Tag::tag_superword), _graph(_vloop_analyzer, _arena NOT_PRODUCT(COMMA _trace)), - _mem_ref_for_main_loop_alignment(mem_ref_for_main_loop_alignment), + _vpointer_for_main_loop_alignment(vpointer_for_main_loop_alignment), _aw_for_main_loop_alignment(aw_for_main_loop_alignment) {} const VLoopAnalyzer& vloop_analyzer() const { return _vloop_analyzer; } @@ -257,7 +261,7 @@ private: } // Ensure that the main loop vectors are aligned by adjusting the pre loop limit. - void determine_mem_ref_and_aw_for_main_loop_alignment(); + void determine_vpointer_and_aw_for_main_loop_alignment(); void adjust_pre_loop_limit_to_align_main_loop_vectors(); void apply_speculative_alignment_runtime_checks(); @@ -271,7 +275,7 @@ private: }; // Keeps track of the state during "VTransform::apply" -// -> keep track of the already transformed nodes +// -> keep track of the already transformed nodes and the memory state. class VTransformApplyState : public StackObj { private: const VLoopAnalyzer& _vloop_analyzer; @@ -281,11 +285,35 @@ private: // generated def (input) nodes when we are generating the use nodes in "apply". GrowableArray _vtnode_idx_to_transformed_node; + // We keep track of the current memory state in each slice. If the slice has only + // loads (and no phi), then this is always the input memory state from before the + // loop. If there is a memory phi, this is initially the memory phi, and each time + // a store is processed, it is updated to that store. + GrowableArray _memory_states; + + // We need to keep track of the memory uses after the loop, for the slices that + // have a memory phi. + // use->in(in_idx) = + class MemoryStateUseAfterLoop : public StackObj { + public: + Node* _use; + int _in_idx; + int _alias_idx; + + MemoryStateUseAfterLoop(Node* use, int in_idx, int alias_idx) : + _use(use), _in_idx(in_idx), _alias_idx(alias_idx) {} + MemoryStateUseAfterLoop() : MemoryStateUseAfterLoop(nullptr, 0, 0) {} + }; + + GrowableArray _memory_state_uses_after_loop; + public: VTransformApplyState(const VLoopAnalyzer& vloop_analyzer, int num_vtnodes) : _vloop_analyzer(vloop_analyzer), - _vtnode_idx_to_transformed_node(num_vtnodes, num_vtnodes, nullptr) + _vtnode_idx_to_transformed_node(num_vtnodes, num_vtnodes, nullptr), + _memory_states(num_slices(), num_slices(), nullptr) { + init_memory_states_and_uses_after_loop(); } const VLoop& vloop() const { return _vloop_analyzer.vloop(); } @@ -294,6 +322,25 @@ public: void set_transformed_node(VTransformNode* vtn, Node* n); Node* transformed_node(const VTransformNode* vtn) const; + + Node* memory_state(int alias_idx) const { return _memory_states.at(alias_idx); } + void set_memory_state(int alias_idx, Node* n) { _memory_states.at_put(alias_idx, n); } + + Node* memory_state(const TypePtr* adr_type) const { + int alias_idx = phase()->C->get_alias_index(adr_type); + return memory_state(alias_idx); + } + + void set_memory_state(const TypePtr* adr_type, Node* n) { + int alias_idx = phase()->C->get_alias_index(adr_type); + return set_memory_state(alias_idx, n); + } + + void fix_memory_state_uses_after_loop(); + +private: + int num_slices() const { return _vloop_analyzer.memory_slices().heads().length(); } + void init_memory_states_and_uses_after_loop(); }; // The vtnodes (VTransformNode) resemble the C2 IR Nodes, and model a part of the @@ -433,6 +480,8 @@ public: } virtual VTransformMemopScalarNode* isa_MemopScalar() { return nullptr; } + virtual VTransformLoopPhiNode* isa_LoopPhi() { return nullptr; } + virtual VTransformCountedLoopNode* isa_CountedLoop() { return nullptr; } virtual VTransformOuterNode* isa_Outer() { return nullptr; } virtual VTransformVectorNode* isa_Vector() { return nullptr; } virtual VTransformElementWiseVectorNode* isa_ElementWiseVector() { return nullptr; } @@ -448,9 +497,8 @@ public: virtual const VPointer& vpointer() const { ShouldNotReachHere(); } virtual VTransformApplyResult apply(VTransformApplyState& apply_state) const = 0; - - Node* find_transformed_input(int i, const GrowableArray& vnode_idx_to_transformed_node) const; - + virtual void apply_backedge(VTransformApplyState& apply_state) const {}; + void apply_vtn_inputs_to_node(Node* n, VTransformApplyState& apply_state) const; void register_new_node_from_vectorization(VTransformApplyState& apply_state, Node* vn) const; NOT_PRODUCT(virtual const char* name() const = 0;) @@ -510,7 +558,9 @@ public: assert(_node->in(0)->is_Loop(), "phi ctrl must be Loop: %s", _node->in(0)->Name()); } + virtual VTransformLoopPhiNode* isa_LoopPhi() override { return this; } virtual VTransformApplyResult apply(VTransformApplyState& apply_state) const override; + virtual void apply_backedge(VTransformApplyState& apply_state) const override; NOT_PRODUCT(virtual const char* name() const override { return "LoopPhi"; };) NOT_PRODUCT(virtual void print_spec() const override;) }; @@ -531,6 +581,16 @@ public: NOT_PRODUCT(virtual void print_spec() const override;) }; +// Identity transform for CountedLoop, the only CFG node with a backedge. +class VTransformCountedLoopNode : public VTransformCFGNode { +public: + VTransformCountedLoopNode(VTransform& vtransform, CountedLoopNode* n) : + VTransformCFGNode(vtransform, n) {} + + virtual VTransformCountedLoopNode* isa_CountedLoop() override { return this; } + NOT_PRODUCT(virtual const char* name() const override { return "CountedLoop"; };) +}; + // Wrapper node for nodes outside the loop that are inputs to nodes in the loop. // Since we want the loop-internal nodes to be able to reference all inputs as vtnodes, // we must wrap the inputs that are outside the loop into special vtnodes, too. @@ -632,22 +692,9 @@ public: class VTransformVectorNode : public VTransformNode { private: const VTransformVectorNodeProperties _properties; -protected: - GrowableArray _nodes; public: VTransformVectorNode(VTransform& vtransform, const uint req, const VTransformVectorNodeProperties properties) : - VTransformNode(vtransform, req), - _properties(properties), - _nodes(vtransform.arena(), - properties.vector_length(), - properties.vector_length(), - nullptr) {} - - void set_nodes(const Node_List* pack) { - for (uint k = 0; k < pack->size(); k++) { - _nodes.at_put(k, pack->at(k)); - } - } + VTransformNode(vtransform, req), _properties(properties) {} virtual VTransformVectorNode* isa_Vector() override { return this; } void register_new_node_from_vectorization_and_replace_scalar_nodes(VTransformApplyState& apply_state, Node* vn) const; @@ -749,17 +796,23 @@ public: _vpointer(vpointer), _adr_type(adr_type) {} - const GrowableArray& nodes() const { return _nodes; } virtual VTransformMemVectorNode* isa_MemVector() override { return this; } virtual bool is_load_or_store_in_loop() const override { return true; } virtual const VPointer& vpointer() const override { return _vpointer; } }; class VTransformLoadVectorNode : public VTransformMemVectorNode { +private: + const LoadNode::ControlDependency _control_dependency; + public: // req = 3 -> [ctrl, mem, adr] - VTransformLoadVectorNode(VTransform& vtransform, const VTransformVectorNodeProperties properties, const VPointer& vpointer, const TypePtr* adr_type) : - VTransformMemVectorNode(vtransform, 3, properties, vpointer, adr_type) {} + VTransformLoadVectorNode(VTransform& vtransform, + const VTransformVectorNodeProperties properties, + const VPointer& vpointer, + const TypePtr* adr_type, + const LoadNode::ControlDependency control_dependency) : + VTransformMemVectorNode(vtransform, 3, properties, vpointer, adr_type), _control_dependency(control_dependency) {} LoadNode::ControlDependency control_dependency() const; virtual VTransformLoadVectorNode* isa_LoadVector() override { return this; } virtual bool is_load_in_loop() const override { return true; } @@ -777,30 +830,4 @@ public: virtual VTransformApplyResult apply(VTransformApplyState& apply_state) const override; NOT_PRODUCT(virtual const char* name() const override { return "StoreVector"; };) }; - -// Invoke callback on all memops, in the order of the schedule. -template -void VTransformGraph::for_each_memop_in_schedule(Callback callback) const { - assert(_schedule.length() == _vtnodes.length(), "schedule was computed"); - - for (int i = 0; i < _schedule.length(); i++) { - VTransformNode* vtn = _schedule.at(i); - - // We must ignore nodes outside the loop. - if (vtn->isa_Outer() != nullptr) { continue; } - - VTransformMemopScalarNode* scalar = vtn->isa_MemopScalar(); - if (scalar != nullptr) { - callback(scalar->node()); - } - - VTransformMemVectorNode* vector = vtn->isa_MemVector(); - if (vector != nullptr) { - for (int j = 0; j < vector->nodes().length(); j++) { - callback(vector->nodes().at(j)->as_Mem()); - } - } - } -} - #endif // SHARE_OPTO_VTRANSFORM_HPP diff --git a/test/hotspot/jtreg/compiler/lib/ir_framework/CompilePhase.java b/test/hotspot/jtreg/compiler/lib/ir_framework/CompilePhase.java index 8b794e13e3f..06b2afa8a67 100644 --- a/test/hotspot/jtreg/compiler/lib/ir_framework/CompilePhase.java +++ b/test/hotspot/jtreg/compiler/lib/ir_framework/CompilePhase.java @@ -98,9 +98,9 @@ public enum CompilePhase { PHASEIDEALLOOP2( "PhaseIdealLoop 2"), PHASEIDEALLOOP3( "PhaseIdealLoop 3"), AUTO_VECTORIZATION1_BEFORE_APPLY( "AutoVectorization 1, before Apply"), - AUTO_VECTORIZATION2_AFTER_REORDER( "AutoVectorization 2, after Apply Memop Reordering"), - AUTO_VECTORIZATION3_AFTER_ADJUST_LIMIT( "AutoVectorization 3, after Adjusting Pre-loop Limit"), - AUTO_VECTORIZATION4_AFTER_SPECULATIVE_RUNTIME_CHECKS("AutoVectorization 4, after Adding Speculative Runtime Checks"), + AUTO_VECTORIZATION3_AFTER_ADJUST_LIMIT( "AutoVectorization 2, after Adjusting Pre-loop Limit"), + AUTO_VECTORIZATION4_AFTER_SPECULATIVE_RUNTIME_CHECKS("AutoVectorization 3, after Adding Speculative Runtime Checks"), + AUTO_VECTORIZATION5_AFTER_APPLY( "AutoVectorization 4, after Apply"), BEFORE_CCP1( "Before PhaseCCP 1"), CCP1( "PhaseCCP 1"), ITER_GVN2( "Iter GVN 2"), From 862119565db311fe0e02e383fd3493601ed23ea8 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 8 Oct 2025 05:32:51 +0000 Subject: [PATCH 393/556] 8363917: SwitchBootstraps.enumSwitch() args not checked as documented Reviewed-by: liach --- .../java/lang/runtime/SwitchBootstraps.java | 37 ++++++++------ .../lang/runtime/SwitchBootstrapsTest.java | 49 +++++++++++++++++-- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java b/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java index f4d82595842..99716baf439 100644 --- a/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java +++ b/src/java.base/share/classes/java/lang/runtime/SwitchBootstraps.java @@ -165,22 +165,22 @@ public final class SwitchBootstraps { * @param lookup Represents a lookup context with the accessibility * privileges of the caller. When used with {@code invokedynamic}, * this is stacked automatically by the VM. - * @param invocationName unused + * @param invocationName unused, {@code null} is permitted * @param invocationType The invocation type of the {@code CallSite} with two parameters, * a reference type, an {@code int}, and {@code int} as a return type. * @param labels case labels - {@code String} and {@code Integer} constants * and {@code Class} and {@code EnumDesc} instances, in any combination * @return a {@code CallSite} returning the first matching element as described above * - * @throws NullPointerException if any argument is {@code null} + * @throws NullPointerException if any argument is {@code null}, unless noted otherwise * @throws IllegalArgumentException if any element in the labels array is null * @throws IllegalArgumentException if the invocation type is not a method type of first parameter of a reference type, - * second parameter of type {@code int} and with {@code int} as its return type, + * second parameter of type {@code int} and with {@code int} as its return type * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code String}, * {@code Integer}, {@code Long}, {@code Float}, {@code Double}, {@code Boolean}, - * {@code Class} or {@code EnumDesc}. + * {@code Class} or {@code EnumDesc} * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code Boolean} - * when {@code target} is a {@code Boolean.class}. + * when {@code target} is a {@code Boolean.class} * @jvms 4.4.6 The CONSTANT_NameAndType_info Structure * @jvms 4.4.10 The CONSTANT_Dynamic_info and CONSTANT_InvokeDynamic_info Structures */ @@ -255,29 +255,36 @@ public final class SwitchBootstraps { * enum constant's {@link Enum#name()}. * *

        - * If no element in the {@code labels} array matches the target, then - * the method of the call site return the length of the {@code labels} array. + * If for a given {@code target} there is no element in the {@code labels} + * fulfilling one of the above conditions, then the method of the call + * site returns the length of the {@code labels} array. *

        * The value of the {@code restart} index must be between {@code 0} (inclusive) and * the length of the {@code labels} array (inclusive), - * both or an {@link IndexOutOfBoundsException} is thrown. + * or an {@link IndexOutOfBoundsException} is thrown. + * + * @apiNote It is permissible for the {@code labels} array to contain {@code String} + * values that do not represent any enum constants at runtime. * * @param lookup Represents a lookup context with the accessibility * privileges of the caller. When used with {@code invokedynamic}, * this is stacked automatically by the VM. - * @param invocationName unused + * @param invocationName unused, {@code null} is permitted * @param invocationType The invocation type of the {@code CallSite} with two parameters, * an enum type, an {@code int}, and {@code int} as a return type. * @param labels case labels - {@code String} constants and {@code Class} instances, * in any combination * @return a {@code CallSite} returning the first matching element as described above * - * @throws NullPointerException if any argument is {@code null} - * @throws IllegalArgumentException if any element in the labels array is null, if the - * invocation type is not a method type whose first parameter type is an enum type, - * second parameter of type {@code int} and whose return type is {@code int}, - * or if {@code labels} contains an element that is not of type {@code String} or - * {@code Class} of the target enum type. + * @throws NullPointerException if any argument is {@code null}, unless noted otherwise + * @throws IllegalArgumentException if any element in the labels array is null + * @throws IllegalArgumentException if any element in the labels array is an empty {@code String} + * @throws IllegalArgumentException if the invocation type is not a method type + * whose first parameter type is an enum type, + * second parameter of type {@code int} and + * whose return type is {@code int} + * @throws IllegalArgumentException if {@code labels} contains an element that is not of type {@code String} or + * {@code Class} equal to the target enum type * @jvms 4.4.6 The CONSTANT_NameAndType_info Structure * @jvms 4.4.10 The CONSTANT_Dynamic_info and CONSTANT_InvokeDynamic_info Structures */ diff --git a/test/jdk/java/lang/runtime/SwitchBootstrapsTest.java b/test/jdk/java/lang/runtime/SwitchBootstrapsTest.java index a231501894f..8c6132b2815 100644 --- a/test/jdk/java/lang/runtime/SwitchBootstrapsTest.java +++ b/test/jdk/java/lang/runtime/SwitchBootstrapsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -155,23 +155,60 @@ public class SwitchBootstrapsTest { testEnum(E1.B, 0, 0, "B", "C", "A", E1.class); testEnum(E1.B, 1, 3, "B", "C", "A", E1.class); try { - testEnum(E1.B, 1, 3, "B", "C", "A", E2.class); + testEnum(E1.B, 0, -1, E2.class); fail("Didn't get the expected exception."); } catch (IllegalArgumentException ex) { //OK } try { - testEnum(E1.B, 1, 3, "B", "C", "A", String.class); + testEnum(E1.B, 0, -1, String.class); fail("Didn't get the expected exception."); } catch (IllegalArgumentException ex) { //OK } + try { + testEnum(E1.B, 0, -1, 10); + fail("Didn't get the expected exception."); + } catch (IllegalArgumentException ex) { + //OK + } + try { + testEnum(E1.B, 0, -1, new Object()); + fail("Didn't get the expected exception."); + } catch (IllegalArgumentException ex) { + //OK + } + try { + testEnum(E1.B, 0, -1, new Object[] { null }); + fail("Didn't get the expected exception."); + } catch (IllegalArgumentException ex) { + //OK + } + try { + testEnum(E1.B, 0, -1, ""); + fail("Didn't get the expected exception."); + } catch (IllegalArgumentException ex) { + //OK + } + try { + testEnum(E1.B, 0, -1, (Object[]) null); + fail("Didn't get the expected exception."); + } catch (NullPointerException ex) { + //OK + } testEnum(E1.B, 0, 0, "B", "A"); testEnum(E1.A, 0, 1, "B", "A"); testEnum(E1.A, 0, 0, "A", "A", "B"); testEnum(E1.A, 1, 1, "A", "A", "B"); testEnum(E1.A, 2, 3, "A", "A", "B"); testEnum(E1.A, 0, 0); + testEnum(E1.B, 0, 2, "A", "OLD_REMOVED_CONSTANT", "B", E1.class); + testEnum(E1.B, 1, 2, "A", "OLD_REMOVED_CONSTANT", "B", E1.class); + + //null invocation name: + MethodType switchType = MethodType.methodType(int.class, E1.class, int.class); + MethodHandle indy = ((CallSite) BSM_ENUM_SWITCH.invoke(MethodHandles.lookup(), null, switchType)).dynamicInvoker(); + assertEquals((int) indy.invoke(E1.A, 0), 0); } public void testEnumsWithConstants() throws Throwable { @@ -197,6 +234,9 @@ public class SwitchBootstrapsTest { testEnum(E.class, E.A, 0, 0, "A", "B", "C"); testEnum(E.class, E.B, 0, 1, "A", "B", "C"); testEnum(E.class, E.C, 0, 2, "A", "B", "C"); + testEnum(E.class, E.C, 0, 2, "A", "B"); + testEnum(E.class, E.C, 1, 2, "A", "B"); + testEnum(E.class, E.C, 2, 2, "A", "B"); } public void testWrongSwitchTypes() throws Throwable { @@ -279,6 +319,9 @@ public class SwitchBootstrapsTest { } catch (IllegalArgumentException ex) { //OK } + //null invocationName is OK: + BSM_TYPE_SWITCH.invoke(MethodHandles.lookup(), null, switchType, + new Object[] {Object.class}); } private static AtomicBoolean enumInitialized = new AtomicBoolean(); From bd25db1fb8573fc908f7a8a96bca417b1d44689a Mon Sep 17 00:00:00 2001 From: Matthias Baesken Date: Wed, 8 Oct 2025 07:02:34 +0000 Subject: [PATCH 394/556] 8368960: Adjust java UL logging in the build Reviewed-by: erikj, dholmes --- make/ToolsJdk.gmk | 2 +- make/autoconf/boot-jdk.m4 | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/make/ToolsJdk.gmk b/make/ToolsJdk.gmk index ae0dd069c80..629cadbf83a 100644 --- a/make/ToolsJdk.gmk +++ b/make/ToolsJdk.gmk @@ -63,7 +63,7 @@ TOOL_GENERATECURRENCYDATA = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_ TOOL_TZDB = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ build.tools.tzdb.TzdbZoneRulesCompiler -TOOL_BLOCKED_CERTS = $(JAVA_SMALL) -Xlog:disable -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ +TOOL_BLOCKED_CERTS = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ --add-exports java.base/sun.security.util=ALL-UNNAMED \ build.tools.blockedcertsconverter.BlockedCertsConverter diff --git a/make/autoconf/boot-jdk.m4 b/make/autoconf/boot-jdk.m4 index 1dd768b2ae1..adc9afc349d 100644 --- a/make/autoconf/boot-jdk.m4 +++ b/make/autoconf/boot-jdk.m4 @@ -444,6 +444,9 @@ AC_DEFUN_ONCE([BOOTJDK_SETUP_BOOT_JDK_ARGUMENTS], # Force en-US environment UTIL_ADD_JVM_ARG_IF_OK([-Duser.language=en -Duser.country=US],boot_jdk_jvmargs,[$JAVA]) + UTIL_ADD_JVM_ARG_IF_OK([-Xlog:all=off:stdout],boot_jdk_jvmargs,[$JAVA]) + UTIL_ADD_JVM_ARG_IF_OK([-Xlog:all=warning:stderr],boot_jdk_jvmargs,[$JAVA]) + if test "x$BOOTJDK_USE_LOCAL_CDS" = xtrue; then # Use our own CDS archive UTIL_ADD_JVM_ARG_IF_OK([$boot_jdk_cds_args -Xshare:auto],boot_jdk_jvmargs,[$JAVA]) From d27649fe22a5bed9db72ac6c2595ac91f1fa28f8 Mon Sep 17 00:00:00 2001 From: Johannes Bechberger Date: Wed, 8 Oct 2025 08:03:32 +0000 Subject: [PATCH 395/556] 8367302: New test jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java from JDK-8366082 is failing Reviewed-by: dholmes, apangin --- .../sampling/jfrCPUTimeThreadSampler.cpp | 23 +-- .../sampling/jfrCPUTimeThreadSampler.hpp | 7 +- src/hotspot/share/prims/whitebox.cpp | 16 +-- test/jdk/ProblemList-Xcomp.txt | 1 - .../TestCPUTimeSampleQueueAutoSizes.java | 131 +++++++++++------- test/lib/jdk/test/whitebox/WhiteBox.java | 5 +- 6 files changed, 92 insertions(+), 91 deletions(-) diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp index 2ce1a93455b..7507b9c994e 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.cpp @@ -230,8 +230,7 @@ class JfrCPUSamplerThread : public NonJavaThread { volatile bool _is_async_processing_of_cpu_time_jfr_requests_triggered; volatile bool _warned_about_timer_creation_failure; volatile bool _signal_handler_installed; - DEBUG_ONLY(volatile bool _out_of_stack_walking_enabled;) - DEBUG_ONLY(volatile u8 _out_of_stack_walking_iterations;) + DEBUG_ONLY(volatile bool _out_of_stack_walking_enabled = true;) static const u4 STOP_SIGNAL_BIT = 0x80000000; @@ -283,10 +282,6 @@ public: void set_out_of_stack_walking_enabled(bool runnable) { AtomicAccess::release_store(&_out_of_stack_walking_enabled, runnable); } - - u8 out_of_stack_walking_iterations() const { - return AtomicAccess::load(&_out_of_stack_walking_iterations); - } #endif }; @@ -394,7 +389,6 @@ void JfrCPUSamplerThread::run() { } DEBUG_ONLY(if (AtomicAccess::load_acquire(&_out_of_stack_walking_enabled)) {) if (AtomicAccess::cmpxchg(&_is_async_processing_of_cpu_time_jfr_requests_triggered, true, false)) { - DEBUG_ONLY(AtomicAccess::inc(&_out_of_stack_walking_iterations);) stackwalk_threads_in_native(); } DEBUG_ONLY(}) @@ -588,18 +582,14 @@ void JfrCPUTimeThreadSampling::handle_timer_signal(siginfo_t* info, void* contex } #ifdef ASSERT -void JfrCPUTimeThreadSampling::set_out_of_stack_walking_enabled(bool runnable) { +bool JfrCPUTimeThreadSampling::set_out_of_stack_walking_enabled(bool runnable) { if (_instance != nullptr && _instance->_sampler != nullptr) { _instance->_sampler->set_out_of_stack_walking_enabled(runnable); + return true; + } else { + return false; } } - -u8 JfrCPUTimeThreadSampling::out_of_stack_walking_iterations() { - if (_instance != nullptr && _instance->_sampler != nullptr) { - return _instance->_sampler->out_of_stack_walking_iterations(); - } - return 0; -} #endif void JfrCPUSamplerThread::sample_thread(JfrSampleRequest& request, void* ucontext, JavaThread* jt, JfrThreadLocal* tl, JfrTicks& now) { @@ -872,8 +862,9 @@ void JfrCPUTimeThreadSampling::on_javathread_terminate(JavaThread* thread) { } #ifdef ASSERT -static void set_out_of_stack_walking_enabled(bool runnable) { +bool JfrCPUTimeThreadSampling::set_out_of_stack_walking_enabled(bool runnable) { warn(); + return false; } #endif diff --git a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp index e17e63fc3ed..e7c915fc8be 100644 --- a/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp +++ b/src/hotspot/share/jfr/periodic/sampling/jfrCPUTimeThreadSampler.hpp @@ -139,9 +139,7 @@ class JfrCPUTimeThreadSampling : public JfrCHeapObj { static void trigger_async_processing_of_cpu_time_jfr_requests(); - DEBUG_ONLY(static void set_out_of_stack_walking_enabled(bool runnable);) - - DEBUG_ONLY(static u8 out_of_stack_walking_iterations();) + DEBUG_ONLY(static bool set_out_of_stack_walking_enabled(bool runnable);) }; #else @@ -162,8 +160,7 @@ private: static void on_javathread_create(JavaThread* thread); static void on_javathread_terminate(JavaThread* thread); - DEBUG_ONLY(static void set_out_of_stack_walking_enabled(bool runnable)); - DEBUG_ONLY(static u8 out_of_stack_walking_iterations();) + DEBUG_ONLY(static bool set_out_of_stack_walking_enabled(bool runnable)); }; #endif // defined(LINUX) diff --git a/src/hotspot/share/prims/whitebox.cpp b/src/hotspot/share/prims/whitebox.cpp index 1ecd105f218..b4d100341e0 100644 --- a/src/hotspot/share/prims/whitebox.cpp +++ b/src/hotspot/share/prims/whitebox.cpp @@ -2710,7 +2710,7 @@ WB_ENTRY(void, WB_WaitUnsafe(JNIEnv* env, jobject wb, jint time)) os::naked_short_sleep(time); WB_END -WB_ENTRY(void, WB_BusyWait(JNIEnv* env, jobject wb, jint time)) +WB_ENTRY(void, WB_BusyWaitCPUTime(JNIEnv* env, jobject wb, jint time)) ThreadToNativeFromVM ttn(thread); u8 start = os::current_thread_cpu_time(); u8 target_duration = time * (u8)1000000; @@ -2721,21 +2721,12 @@ WB_END WB_ENTRY(jboolean, WB_CPUSamplerSetOutOfStackWalking(JNIEnv* env, jobject wb, jboolean enable)) #if defined(ASSERT) && INCLUDE_JFR && defined(LINUX) - JfrCPUTimeThreadSampling::set_out_of_stack_walking_enabled(enable == JNI_TRUE); - return JNI_TRUE; + return JfrCPUTimeThreadSampling::set_out_of_stack_walking_enabled(enable == JNI_TRUE) ? JNI_TRUE : JNI_FALSE; #else return JNI_FALSE; #endif WB_END -WB_ENTRY(jlong, WB_CPUSamplerOutOfStackWalkingIterations(JNIEnv* env, jobject wb)) - #if defined(ASSERT) && INCLUDE_JFR && defined(LINUX) - return (jlong)JfrCPUTimeThreadSampling::out_of_stack_walking_iterations(); - #else - return 0; - #endif -WB_END - WB_ENTRY(jstring, WB_GetLibcName(JNIEnv* env, jobject o)) ThreadToNativeFromVM ttn(thread); jstring info_string = env->NewStringUTF(XSTR(LIBC)); @@ -3092,9 +3083,8 @@ static JNINativeMethod methods[] = { {CC"isJVMTIIncluded", CC"()Z", (void*)&WB_IsJVMTIIncluded}, {CC"waitUnsafe", CC"(I)V", (void*)&WB_WaitUnsafe}, - {CC"busyWait", CC"(I)V", (void*)&WB_BusyWait}, + {CC"busyWaitCPUTime", CC"(I)V", (void*)&WB_BusyWaitCPUTime}, {CC"cpuSamplerSetOutOfStackWalking", CC"(Z)Z", (void*)&WB_CPUSamplerSetOutOfStackWalking}, - {CC"cpuSamplerOutOfStackWalkingIterations", CC"()J",(void*)&WB_CPUSamplerOutOfStackWalkingIterations}, {CC"getLibcName", CC"()Ljava/lang/String;", (void*)&WB_GetLibcName}, {CC"pinObject", CC"(Ljava/lang/Object;)V", (void*)&WB_PinObject}, diff --git a/test/jdk/ProblemList-Xcomp.txt b/test/jdk/ProblemList-Xcomp.txt index 8e37e6e15d0..5ed171a1fea 100644 --- a/test/jdk/ProblemList-Xcomp.txt +++ b/test/jdk/ProblemList-Xcomp.txt @@ -29,4 +29,3 @@ java/lang/invoke/MethodHandles/CatchExceptionTest.java 8146623 generic-all java/lang/reflect/callerCache/ReflectionCallerCacheTest.java 8332028 generic-all -jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java 8367302 linux-all diff --git a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java index 1ea96e3bad3..819329aabf5 100644 --- a/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java +++ b/test/jdk/jdk/jfr/event/profiling/TestCPUTimeSampleQueueAutoSizes.java @@ -24,6 +24,7 @@ package jdk.jfr.event.profiling; import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.stream.Collectors; @@ -41,8 +42,15 @@ import jdk.test.whitebox.WhiteBox; /* * Tests the sample queues increase in size as needed, when loss is recorded. + * + * The test starts CPU time sampling with a short interval (1ms), disabling + * out-of-stack sample processing for the duration of the test. + * It now runs in native for one second, to cause queue overflows, + * then it comes back into Java to trigger the queue walking. + * Repeats the cycle 5 times and verifies that the loss decreases from the first + * to the last iteration. * @test - * @requires vm.hasJFR & os.family == "linux" & vm.debug + * @requires vm.hasJFR & os.family == "linux" & vm.debug & vm.flagless * @library /test/lib * @modules jdk.jfr/jdk.jfr.internal * @build jdk.test.whitebox.WhiteBox @@ -54,15 +62,13 @@ public class TestCPUTimeSampleQueueAutoSizes { private static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox(); - private static final String BURST_THREAD_NAME = "Burst-Thread-1"; - - static volatile boolean alive = true; - record LossEvent(long relativeTimeMillis, long lostSamples) {} /** A data collection from the CPUTimeSampleLost events for the burst thread */ static class LossEventCollection { private final List events = new ArrayList<>(); + private final List sampleEventsInTimeBox = new ArrayList<>(); + private final List timeBoxEnds = new ArrayList<>(); public synchronized void addEvent(LossEvent event) { events.add(event); @@ -74,81 +80,100 @@ public class TestCPUTimeSampleQueueAutoSizes { .collect(Collectors.toList()); } - public List getEventsPerInterval(long widthMillis, long stopTimeMillis) { + public synchronized List getEventsPerTimeBox() { List ret = new ArrayList<>(); - for (long start = 0; start < stopTimeMillis; start += widthMillis) { - long actualStart = Math.min(start, stopTimeMillis - widthMillis); + AtomicLong previousEnd = new AtomicLong(0); + for (Long timeBoxEnd : timeBoxEnds) { long lostSamples = events.stream() - .filter(e -> e.relativeTimeMillis >= actualStart && e.relativeTimeMillis < actualStart + widthMillis) + .filter(e -> e.relativeTimeMillis >= previousEnd.get() && e.relativeTimeMillis <= timeBoxEnd) .mapToLong(e -> e.lostSamples) .sum(); - ret.add(new LossEvent(actualStart, lostSamples)); + ret.add(new LossEvent(previousEnd.get(), lostSamples)); + previousEnd.set(timeBoxEnd); } return ret; } + public synchronized void addTimeBoxEnd(long timeBoxEnd, long sampleEvents) { + timeBoxEnds.add(timeBoxEnd); + sampleEventsInTimeBox.add(sampleEvents); + } + + public synchronized void print() { + System.out.println("Loss event information:"); + for (int i = 0; i < timeBoxEnds.size(); i++) { + System.out.println(" Time box end: " + timeBoxEnds.get(i) + ", sample events: " + sampleEventsInTimeBox.get(i)); + } + for (LossEvent e : events) { + System.out.println(" Lost samples event: " + e.lostSamples + " at " + e.relativeTimeMillis); + } + for (LossEvent e : getEventsPerTimeBox()) { + System.out.println(" Lost samples in time box ending at " + e.relativeTimeMillis + ": " + e.lostSamples); + } + } } public static void main(String[] args) throws Exception { try (RecordingStream rs = new RecordingStream()) { // setup recording - AtomicLong firstSampleTimeMillis = new AtomicLong(0); - AtomicLong lastSampleTimeMillis = new AtomicLong(0); + long burstThreadId = Thread.currentThread().threadId(); + final long startTimeMillis = Instant.now().toEpochMilli(); LossEventCollection lossEvents = new LossEventCollection(); + AtomicLong sampleEventCountInTimeBox = new AtomicLong(0); rs.enable(EventNames.CPUTimeSample).with("throttle", "1ms"); - rs.onEvent(EventNames.CPUTimeSample, e -> { - if (firstSampleTimeMillis.get() == 0 && e.getThread("eventThread").getJavaName().equals(BURST_THREAD_NAME)) { - firstSampleTimeMillis.set(e.getStartTime().toEpochMilli()); - } - if (e.getThread("eventThread").getJavaName().equals(BURST_THREAD_NAME)) { - lastSampleTimeMillis.set(e.getStartTime().toEpochMilli()); - } - }); rs.enable(EventNames.CPUTimeSamplesLost); rs.onEvent(EventNames.CPUTimeSamplesLost, e -> { - if (e.getThread("eventThread").getJavaName().equals(BURST_THREAD_NAME)) { + if (e.getThread("eventThread").getJavaThreadId() == burstThreadId) { long eventTime = e.getStartTime().toEpochMilli(); - long relativeTime = firstSampleTimeMillis.get() > 0 ? (eventTime - firstSampleTimeMillis.get()) : eventTime; - System.out.println("Lost samples: " + e.getLong("lostSamples") + " at " + relativeTime); + long relativeTime = eventTime - startTimeMillis; + System.out.println("Lost samples: " + e.getLong("lostSamples") + " at " + relativeTime + " start time " + startTimeMillis); lossEvents.addEvent(new LossEvent(relativeTime, e.getLong("lostSamples"))); } }); - WHITE_BOX.cpuSamplerSetOutOfStackWalking(false); + rs.onEvent(EventNames.CPUTimeSample, e -> { + if (e.getThread("eventThread").getJavaThreadId() == burstThreadId) { + sampleEventCountInTimeBox.incrementAndGet(); + } + }); rs.startAsync(); - // this thread runs all along - Thread burstThread = new Thread(() -> WHITE_BOX.busyWait(11000)); - burstThread.setName(BURST_THREAD_NAME); - burstThread.start(); - // now we toggle out-of-stack-walking off, wait 1 second and then turn it on for 500ms a few times + // we disable the out-of-stack walking so that the queue fills up and overflows + // while we are in native code + disableOutOfStackWalking(); + + for (int i = 0; i < 5; i++) { - boolean supported = WHITE_BOX.cpuSamplerSetOutOfStackWalking(false); - if (!supported) { - System.out.println("Out-of-stack-walking not supported, skipping test"); - Asserts.assertFalse(true); - return; - } - Thread.sleep(700); - long iterations = WHITE_BOX.cpuSamplerOutOfStackWalkingIterations(); - WHITE_BOX.cpuSamplerSetOutOfStackWalking(true); - Thread.sleep(300); - while (WHITE_BOX.cpuSamplerOutOfStackWalkingIterations() == iterations) { - Thread.sleep(50); // just to make sure the stack walking really ran - } + // run in native for one second + WHITE_BOX.busyWaitCPUTime(1000); + // going out-of-native at the end of the previous call should have triggered + // the safepoint handler, thereby also triggering the stack walking and creation + // of the loss event + WHITE_BOX.forceSafepoint(); // just to be sure + lossEvents.addTimeBoxEnd(Instant.now().toEpochMilli() - startTimeMillis, sampleEventCountInTimeBox.get()); + sampleEventCountInTimeBox.set(0); } + + rs.stop(); rs.close(); - checkThatLossDecreased(lossEvents, lastSampleTimeMillis.get() - firstSampleTimeMillis.get()); + + enableOutOfStackWalking(); + + checkThatLossDecreased(lossEvents); } } - static void checkThatLossDecreased(LossEventCollection lossEvents, long lastSampleTimeMillis) { - List intervalLosses = lossEvents.getEventsPerInterval(1000, lastSampleTimeMillis); - for (LossEvent interval : intervalLosses) { - System.out.println("Lost samples in interval " + interval.relativeTimeMillis + ": " + interval.lostSamples); - } - // check that there are at least 3 intervals - Asserts.assertTrue(intervalLosses.size() > 2); - // check that the second to last interval has far fewer lost samples than the first - Asserts.assertTrue(intervalLosses.get(intervalLosses.size() - 2).lostSamples < - intervalLosses.get(0).lostSamples / 2); + static void disableOutOfStackWalking() { + Asserts.assertTrue(WHITE_BOX.cpuSamplerSetOutOfStackWalking(false), "Out-of-stack-walking not supported"); + } + + static void enableOutOfStackWalking() { + WHITE_BOX.cpuSamplerSetOutOfStackWalking(true); + } + + static void checkThatLossDecreased(LossEventCollection lossEvents) { + lossEvents.print(); + List timeBoxedLosses = lossEvents.getEventsPerTimeBox(); + // check that the last time box has far fewer lost samples than the first + Asserts.assertTrue(timeBoxedLosses.get(timeBoxedLosses.size() - 1).lostSamples <= + timeBoxedLosses.get(0).lostSamples / 2); } } diff --git a/test/lib/jdk/test/whitebox/WhiteBox.java b/test/lib/jdk/test/whitebox/WhiteBox.java index 669ec48b619..d07dedd2a8c 100644 --- a/test/lib/jdk/test/whitebox/WhiteBox.java +++ b/test/lib/jdk/test/whitebox/WhiteBox.java @@ -847,13 +847,12 @@ public class WhiteBox { public native void waitUnsafe(int time_ms); - public native void busyWait(int cpuTimeMs); + public native void busyWaitCPUTime(int cpuTimeMs); + // returns true if supported, false if not public native boolean cpuSamplerSetOutOfStackWalking(boolean enable); - public native long cpuSamplerOutOfStackWalkingIterations(); - public native void pinObject(Object o); public native void unpinObject(Object o); From f58e17fd27e868e4a8816befc4c4bb8946c1f7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3n=20Seoane=20Ampudia?= Date: Wed, 8 Oct 2025 08:58:58 +0000 Subject: [PATCH 396/556] 8368780: IGV: Upgrade to Netbeans Platform 27 Reviewed-by: rcastanedalo, chagedorn --- src/utils/IdealGraphVisualizer/Filter/pom.xml | 4 ++-- src/utils/IdealGraphVisualizer/README.md | 2 +- src/utils/IdealGraphVisualizer/pom.xml | 12 ++++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/utils/IdealGraphVisualizer/Filter/pom.xml b/src/utils/IdealGraphVisualizer/Filter/pom.xml index 176f7a80180..c22ce274493 100644 --- a/src/utils/IdealGraphVisualizer/Filter/pom.xml +++ b/src/utils/IdealGraphVisualizer/Filter/pom.xml @@ -1,6 +1,6 @@ x << 64 = x << 0 = x (!= 2x << 63, for example for x = 1) + // According to the Java spec, chapter 15.19, we only consider the six lowest-order bits of the right-hand operand + // (i.e. "right-hand operand" & 0b111111). Therefore, x << 64 is the same as x << 0 (64 = 0b10000000 & 0b0111111 = 0). + return LShiftNode::make(add1->in(1), phase->intcon(con + 1), bt); } // Left input is an add of a constant? - const TypeInt *t12 = phase->type(add1->in(2))->isa_int(); - if( t12 && t12->is_con() ){ // Left input is an add of a con? + const TypeInteger* t12 = phase->type(add1->in(2))->isa_integer(bt); + if (t12 != nullptr && t12->is_con()) { // Left input is an add of a con? // Compute X << con0 - Node *lsh = phase->transform( new LShiftINode( add1->in(1), in(2) ) ); + Node* lsh = phase->transform(LShiftNode::make(add1->in(1), in(2), bt)); // Compute X<intcon(t12->get_con() << con)); + return AddNode::make(lsh, phase->integercon(java_shift_left(t12->get_con_as_long(bt), con, bt), bt), bt); } } } // Check for "(x >> C1) << C2" - if (add1_op == Op_RShiftI || add1_op == Op_URShiftI) { + if (add1_op == Op_RShift(bt) || add1_op == Op_URShift(bt)) { int add1Con = 0; const_shift_count(phase, add1, &add1Con); // Special case C1 == C2, which just masks off low bits - if (add1Con > 0 && con == add1Con) { + if (add1Con > 0 && con == (uint)add1Con) { // Convert to "(x & -(1 << C2))" - return new AndINode(add1->in(1), phase->intcon(java_negate(jint(1 << con)))); + return MulNode::make_and(add1->in(1), phase->integercon(java_negate(java_shift_left(1, con, bt), bt), bt), bt); } else { // Wait until the right shift has been sharpened to the correct count - if (add1Con > 0 && add1Con < BitsPerJavaInteger) { + if (add1Con > 0 && (uint)add1Con < bits_per_java_integer(bt)) { // As loop parsing can produce LShiftI nodes, we should wait until the graph is fully formed // to apply optimizations, otherwise we can inadvertently stop vectorization opportunities. if (phase->is_IterGVN()) { - if (con > add1Con) { + if (con > (uint)add1Con) { // Creates "(x << (C2 - C1)) & -(1 << C2)" - Node* lshift = phase->transform(new LShiftINode(add1->in(1), phase->intcon(con - add1Con))); - return new AndINode(lshift, phase->intcon(java_negate(jint(1 << con)))); + Node* lshift = phase->transform(LShiftNode::make(add1->in(1), phase->intcon(con - add1Con), bt)); + return MulNode::make_and(lshift, phase->integercon(java_negate(java_shift_left(1, con, bt), bt), bt), bt); } else { - assert(con < add1Con, "must be (%d < %d)", con, add1Con); + assert(con < (uint)add1Con, "must be (%d < %d)", con, add1Con); // Creates "(x >> (C1 - C2)) & -(1 << C2)" // Handle logical and arithmetic shifts Node* rshift; - if (add1_op == Op_RShiftI) { - rshift = phase->transform(new RShiftINode(add1->in(1), phase->intcon(add1Con - con))); + if (add1_op == Op_RShift(bt)) { + rshift = phase->transform(RShiftNode::make(add1->in(1), phase->intcon(add1Con - con), bt)); } else { - rshift = phase->transform(new URShiftINode(add1->in(1), phase->intcon(add1Con - con))); + rshift = phase->transform(URShiftNode::make(add1->in(1), phase->intcon(add1Con - con), bt)); } - return new AndINode(rshift, phase->intcon(java_negate(jint(1 << con)))); + return MulNode::make_and(rshift, phase->integercon(java_negate(java_shift_left(1, con, bt)), bt), bt); } } else { phase->record_for_igvn(this); @@ -1135,29 +1130,29 @@ Node *LShiftINode::Ideal(PhaseGVN *phase, bool can_reshape) { } // Check for "((x >> C1) & Y) << C2" - if (add1_op == Op_AndI) { - Node *add2 = add1->in(1); + if (add1_op == Op_And(bt)) { + Node* add2 = add1->in(1); int add2_op = add2->Opcode(); - if (add2_op == Op_RShiftI || add2_op == Op_URShiftI) { + if (add2_op == Op_RShift(bt) || add2_op == Op_URShift(bt)) { // Special case C1 == C2, which just masks off low bits if (add2->in(2) == in(2)) { // Convert to "(x & (Y << C2))" - Node* y_sh = phase->transform(new LShiftINode(add1->in(2), phase->intcon(con))); - return new AndINode(add2->in(1), y_sh); + Node* y_sh = phase->transform(LShiftNode::make(add1->in(2), phase->intcon(con), bt)); + return MulNode::make_and(add2->in(1), y_sh, bt); } int add2Con = 0; const_shift_count(phase, add2, &add2Con); - if (add2Con > 0 && add2Con < BitsPerJavaInteger) { + if (add2Con > 0 && (uint)add2Con < bits_per_java_integer(bt)) { if (phase->is_IterGVN()) { // Convert to "((x >> C1) << C2) & (Y << C2)" // Make "(x >> C1) << C2", which will get folded away by the rule above - Node* x_sh = phase->transform(new LShiftINode(add2, phase->intcon(con))); + Node* x_sh = phase->transform(LShiftNode::make(add2, phase->intcon(con), bt)); // Make "Y << C2", which will simplify when Y is a constant - Node* y_sh = phase->transform(new LShiftINode(add1->in(2), phase->intcon(con))); + Node* y_sh = phase->transform(LShiftNode::make(add1->in(2), phase->intcon(con), bt)); - return new AndINode(x_sh, y_sh); + return MulNode::make_and(x_sh, y_sh, bt); } else { phase->record_for_igvn(this); } @@ -1167,14 +1162,16 @@ Node *LShiftINode::Ideal(PhaseGVN *phase, bool can_reshape) { // Check for ((x & ((1<<(32-c0))-1)) << c0) which ANDs off high bits // before shifting them away. - const jint bits_mask = right_n_bits(BitsPerJavaInteger-con); - if( add1_op == Op_AndI && - phase->type(add1->in(2)) == TypeInt::make( bits_mask ) ) - return new LShiftINode( add1->in(1), in(2) ); + const jlong bits_mask = max_unsigned_integer(bt) >> con; + assert(bt != T_INT || bits_mask == right_n_bits(bits_per_java_integer(bt)-con), "inconsistent"); + if (add1_op == Op_And(bt) && + phase->type(add1->in(2)) == TypeInteger::make(bits_mask, bt)) { + return LShiftNode::make(add1->in(1), in(2), bt); + } - // Performs: + // Collapse nested left-shifts with constant rhs: // (X << con1) << con2 ==> X << (con1 + con2) - Node* doubleShift = collapse_nested_shift_left(phase, this, con, T_INT); + Node* doubleShift = collapse_nested_shift_left(phase, this, con, bt); if (doubleShift != nullptr) { return doubleShift; } @@ -1182,237 +1179,103 @@ Node *LShiftINode::Ideal(PhaseGVN *phase, bool can_reshape) { return nullptr; } -//------------------------------Value------------------------------------------ -// A LShiftINode shifts its input2 left by input1 amount. -const Type* LShiftINode::Value(PhaseGVN* phase) const { - const Type *t1 = phase->type( in(1) ); - const Type *t2 = phase->type( in(2) ); +//------------------------------Ideal------------------------------------------ +Node* LShiftINode::Ideal(PhaseGVN *phase, bool can_reshape) { + return IdealIL(phase, can_reshape, T_INT); +} + +const Type* LShiftNode::ValueIL(PhaseGVN* phase, BasicType bt) const { + const Type* t1 = phase->type(in(1)); + const Type* t2 = phase->type(in(2)); // Either input is TOP ==> the result is TOP - if( t1 == Type::TOP ) return Type::TOP; - if( t2 == Type::TOP ) return Type::TOP; + if (t1 == Type::TOP) { + return Type::TOP; + } + if (t2 == Type::TOP) { + return Type::TOP; + } // Left input is ZERO ==> the result is ZERO. - if( t1 == TypeInt::ZERO ) return TypeInt::ZERO; + if (t1 == TypeInteger::zero(bt)) { + return TypeInteger::zero(bt); + } // Shift by zero does nothing - if( t2 == TypeInt::ZERO ) return t1; + if (t2 == TypeInt::ZERO) { + return t1; + } // Either input is BOTTOM ==> the result is BOTTOM - if( (t1 == TypeInt::INT) || (t2 == TypeInt::INT) || - (t1 == Type::BOTTOM) || (t2 == Type::BOTTOM) ) - return TypeInt::INT; + if ((t1 == TypeInteger::bottom(bt)) || (t2 == TypeInt::INT) || + (t1 == Type::BOTTOM) || (t2 == Type::BOTTOM)) { + return TypeInteger::bottom(bt); + } - const TypeInt *r1 = t1->is_int(); // Handy access - const TypeInt *r2 = t2->is_int(); // Handy access + const TypeInteger* r1 = t1->is_integer(bt); // Handy access + const TypeInt* r2 = t2->is_int(); // Handy access - if (!r2->is_con()) - return TypeInt::INT; + if (!r2->is_con()) { + return TypeInteger::bottom(bt); + } uint shift = r2->get_con(); - shift &= BitsPerJavaInteger-1; // semantics of Java shifts - // Shift by a multiple of 32 does nothing: - if (shift == 0) return t1; + shift &= bits_per_java_integer(bt) - 1; // semantics of Java shifts + // Shift by a multiple of 32/64 does nothing: + if (shift == 0) { + return t1; + } // If the shift is a constant, shift the bounds of the type, // unless this could lead to an overflow. if (!r1->is_con()) { - jint lo = r1->_lo, hi = r1->_hi; - if (((lo << shift) >> shift) == lo && - ((hi << shift) >> shift) == hi) { - // No overflow. The range shifts up cleanly. - return TypeInt::make((jint)lo << (jint)shift, - (jint)hi << (jint)shift, - MAX2(r1->_widen,r2->_widen)); + jlong lo = r1->lo_as_long(), hi = r1->hi_as_long(); +#ifdef ASSERT + if (bt == T_INT) { + jint lo_int = r1->is_int()->_lo, hi_int = r1->is_int()->_hi; + assert((java_shift_right(java_shift_left(lo, shift, bt), shift, bt) == lo) == (((lo_int << shift) >> shift) == lo_int), "inconsistent"); + assert((java_shift_right(java_shift_left(hi, shift, bt), shift, bt) == hi) == (((hi_int << shift) >> shift) == hi_int), "inconsistent"); } - return TypeInt::INT; +#endif + if (java_shift_right(java_shift_left(lo, shift, bt), shift, bt) == lo && + java_shift_right(java_shift_left(hi, shift, bt), shift, bt) == hi) { + // No overflow. The range shifts up cleanly. + return TypeInteger::make(java_shift_left(lo, shift, bt), + java_shift_left(hi, shift, bt), + MAX2(r1->_widen, r2->_widen), bt); + } + return TypeInteger::bottom(bt); } - return TypeInt::make( (jint)r1->get_con() << (jint)shift ); + return TypeInteger::make(java_shift_left(r1->get_con_as_long(bt), shift, bt), bt); } -//============================================================================= -//------------------------------Identity--------------------------------------- -Node* LShiftLNode::Identity(PhaseGVN* phase) { +//------------------------------Value------------------------------------------ +const Type* LShiftINode::Value(PhaseGVN* phase) const { + return ValueIL(phase, T_INT); +} + +Node* LShiftNode::IdentityIL(PhaseGVN* phase, BasicType bt) { int count = 0; - if (const_shift_count(phase, this, &count) && (count & (BitsPerJavaLong - 1)) == 0) { - // Shift by a multiple of 64 does nothing + if (const_shift_count(phase, this, &count) && (count & (bits_per_java_integer(bt) - 1)) == 0) { + // Shift by a multiple of 32/64 does nothing return in(1); } return this; } +//============================================================================= +//------------------------------Identity--------------------------------------- +Node* LShiftLNode::Identity(PhaseGVN* phase) { + return IdentityIL(phase, T_LONG); +} + //------------------------------Ideal------------------------------------------ -// If the right input is a constant, and the left input is an add of a -// constant, flatten the tree: (X+con1)< X< X << (con1 + con2) -Node *LShiftLNode::Ideal(PhaseGVN *phase, bool can_reshape) { - int con = mask_and_replace_shift_amount(phase, this, BitsPerJavaLong); - if (con == 0) { - return nullptr; - } - - // Left input is an add? - Node *add1 = in(1); - int add1_op = add1->Opcode(); - if( add1_op == Op_AddL ) { // Left input is an add? - // Avoid dead data cycles from dead loops - assert( add1 != add1->in(1), "dead loop in LShiftLNode::Ideal" ); - - // Left input is an add of the same number? - if (con != (BitsPerJavaLong - 1) && add1->in(1) == add1->in(2)) { - // Convert "(x + x) << c0" into "x << (c0 + 1)" - // Can only be applied if c0 != 63 because: - // (x + x) << 63 = 2x << 63, while - // (x + x) << 63 --transform--> x << 64 = x << 0 = x (!= 2x << 63, for example for x = 1) - // According to the Java spec, chapter 15.19, we only consider the six lowest-order bits of the right-hand operand - // (i.e. "right-hand operand" & 0b111111). Therefore, x << 64 is the same as x << 0 (64 = 0b10000000 & 0b0111111 = 0). - return new LShiftLNode(add1->in(1), phase->intcon(con + 1)); - } - - // Left input is an add of a constant? - const TypeLong *t12 = phase->type(add1->in(2))->isa_long(); - if( t12 && t12->is_con() ){ // Left input is an add of a con? - // Compute X << con0 - Node *lsh = phase->transform( new LShiftLNode( add1->in(1), in(2) ) ); - // Compute X<longcon(t12->get_con() << con)); - } - } - - // Check for "(x >> C1) << C2" - if (add1_op == Op_RShiftL || add1_op == Op_URShiftL) { - int add1Con = 0; - const_shift_count(phase, add1, &add1Con); - - // Special case C1 == C2, which just masks off low bits - if (add1Con > 0 && con == add1Con) { - // Convert to "(x & -(1 << C2))" - return new AndLNode(add1->in(1), phase->longcon(java_negate(jlong(CONST64(1) << con)))); - } else { - // Wait until the right shift has been sharpened to the correct count - if (add1Con > 0 && add1Con < BitsPerJavaLong) { - // As loop parsing can produce LShiftI nodes, we should wait until the graph is fully formed - // to apply optimizations, otherwise we can inadvertently stop vectorization opportunities. - if (phase->is_IterGVN()) { - if (con > add1Con) { - // Creates "(x << (C2 - C1)) & -(1 << C2)" - Node* lshift = phase->transform(new LShiftLNode(add1->in(1), phase->intcon(con - add1Con))); - return new AndLNode(lshift, phase->longcon(java_negate(jlong(CONST64(1) << con)))); - } else { - assert(con < add1Con, "must be (%d < %d)", con, add1Con); - // Creates "(x >> (C1 - C2)) & -(1 << C2)" - - // Handle logical and arithmetic shifts - Node* rshift; - if (add1_op == Op_RShiftL) { - rshift = phase->transform(new RShiftLNode(add1->in(1), phase->intcon(add1Con - con))); - } else { - rshift = phase->transform(new URShiftLNode(add1->in(1), phase->intcon(add1Con - con))); - } - - return new AndLNode(rshift, phase->longcon(java_negate(jlong(CONST64(1) << con)))); - } - } else { - phase->record_for_igvn(this); - } - } - } - } - - // Check for "((x >> C1) & Y) << C2" - if (add1_op == Op_AndL) { - Node* add2 = add1->in(1); - int add2_op = add2->Opcode(); - if (add2_op == Op_RShiftL || add2_op == Op_URShiftL) { - // Special case C1 == C2, which just masks off low bits - if (add2->in(2) == in(2)) { - // Convert to "(x & (Y << C2))" - Node* y_sh = phase->transform(new LShiftLNode(add1->in(2), phase->intcon(con))); - return new AndLNode(add2->in(1), y_sh); - } - - int add2Con = 0; - const_shift_count(phase, add2, &add2Con); - if (add2Con > 0 && add2Con < BitsPerJavaLong) { - if (phase->is_IterGVN()) { - // Convert to "((x >> C1) << C2) & (Y << C2)" - - // Make "(x >> C1) << C2", which will get folded away by the rule above - Node* x_sh = phase->transform(new LShiftLNode(add2, phase->intcon(con))); - // Make "Y << C2", which will simplify when Y is a constant - Node* y_sh = phase->transform(new LShiftLNode(add1->in(2), phase->intcon(con))); - - return new AndLNode(x_sh, y_sh); - } else { - phase->record_for_igvn(this); - } - } - } - } - - // Check for ((x & ((CONST64(1)<<(64-c0))-1)) << c0) which ANDs off high bits - // before shifting them away. - const jlong bits_mask = jlong(max_julong >> con); - if( add1_op == Op_AndL && - phase->type(add1->in(2)) == TypeLong::make( bits_mask ) ) - return new LShiftLNode( add1->in(1), in(2) ); - - // Performs: - // (X << con1) << con2 ==> X << (con1 + con2) - Node* doubleShift = collapse_nested_shift_left(phase, this, con, T_LONG); - if (doubleShift != nullptr) { - return doubleShift; - } - - return nullptr; +Node* LShiftLNode::Ideal(PhaseGVN* phase, bool can_reshape) { + return IdealIL(phase, can_reshape, T_LONG); } //------------------------------Value------------------------------------------ -// A LShiftLNode shifts its input2 left by input1 amount. const Type* LShiftLNode::Value(PhaseGVN* phase) const { - const Type *t1 = phase->type( in(1) ); - const Type *t2 = phase->type( in(2) ); - // Either input is TOP ==> the result is TOP - if( t1 == Type::TOP ) return Type::TOP; - if( t2 == Type::TOP ) return Type::TOP; - - // Left input is ZERO ==> the result is ZERO. - if( t1 == TypeLong::ZERO ) return TypeLong::ZERO; - // Shift by zero does nothing - if( t2 == TypeInt::ZERO ) return t1; - - // Either input is BOTTOM ==> the result is BOTTOM - if( (t1 == TypeLong::LONG) || (t2 == TypeInt::INT) || - (t1 == Type::BOTTOM) || (t2 == Type::BOTTOM) ) - return TypeLong::LONG; - - const TypeLong *r1 = t1->is_long(); // Handy access - const TypeInt *r2 = t2->is_int(); // Handy access - - if (!r2->is_con()) - return TypeLong::LONG; - - uint shift = r2->get_con(); - shift &= BitsPerJavaLong - 1; // semantics of Java shifts - // Shift by a multiple of 64 does nothing: - if (shift == 0) return t1; - - // If the shift is a constant, shift the bounds of the type, - // unless this could lead to an overflow. - if (!r1->is_con()) { - jlong lo = r1->_lo, hi = r1->_hi; - if (((lo << shift) >> shift) == lo && - ((hi << shift) >> shift) == hi) { - // No overflow. The range shifts up cleanly. - return TypeLong::make((jlong)lo << (jint)shift, - (jlong)hi << (jint)shift, - MAX2(r1->_widen,r2->_widen)); - } - return TypeLong::LONG; - } - - return TypeLong::make( (jlong)r1->get_con() << (jint)shift ); + return ValueIL(phase, T_LONG); } RShiftNode* RShiftNode::make(Node* in1, Node* in2, BasicType bt) { @@ -1649,6 +1512,18 @@ const Type* RShiftLNode::Value(PhaseGVN* phase) const { return ValueIL(phase, T_LONG); } +URShiftNode* URShiftNode::make(Node* in1, Node* in2, BasicType bt) { + switch (bt) { + case T_INT: + return new URShiftINode(in1, in2); + case T_LONG: + return new URShiftLNode(in1, in2); + default: + fatal("Not implemented for %s", type2name(bt)); + } + return nullptr; +} + //============================================================================= //------------------------------Identity--------------------------------------- Node* URShiftINode::Identity(PhaseGVN* phase) { @@ -1684,7 +1559,7 @@ Node* URShiftINode::Identity(PhaseGVN* phase) { } //------------------------------Ideal------------------------------------------ -Node *URShiftINode::Ideal(PhaseGVN *phase, bool can_reshape) { +Node* URShiftINode::Ideal(PhaseGVN* phase, bool can_reshape) { int con = mask_and_replace_shift_amount(phase, this, BitsPerJavaInteger); if (con == 0) { return nullptr; @@ -1848,7 +1723,7 @@ Node* URShiftLNode::Identity(PhaseGVN* phase) { } //------------------------------Ideal------------------------------------------ -Node *URShiftLNode::Ideal(PhaseGVN *phase, bool can_reshape) { +Node* URShiftLNode::Ideal(PhaseGVN* phase, bool can_reshape) { int con = mask_and_replace_shift_amount(phase, this, BitsPerJavaLong); if (con == 0) { return nullptr; diff --git a/src/hotspot/share/opto/mulnode.hpp b/src/hotspot/share/opto/mulnode.hpp index b736c17b300..1e19e8ec5cd 100644 --- a/src/hotspot/share/opto/mulnode.hpp +++ b/src/hotspot/share/opto/mulnode.hpp @@ -260,10 +260,14 @@ inline Node* make_and(Node* a, Node* b) { class LShiftNode : public Node { public: - LShiftNode(Node *in1, Node *in2) : Node(nullptr,in1,in2) { + LShiftNode(Node* in1, Node* in2) : Node(nullptr,in1,in2) { init_class_id(Class_LShift); } + const Type* ValueIL(PhaseGVN* phase, BasicType bt) const; + Node* IdentityIL(PhaseGVN* phase, BasicType bt); + Node* IdealIL(PhaseGVN* phase, bool can_reshape, BasicType bt); + static LShiftNode* make(Node* in1, Node* in2, BasicType bt); }; @@ -271,12 +275,12 @@ public: // Logical shift left class LShiftINode : public LShiftNode { public: - LShiftINode(Node *in1, Node *in2) : LShiftNode(in1,in2) {} + LShiftINode(Node* in1, Node* in2) : LShiftNode(in1,in2) {} virtual int Opcode() const; virtual Node* Identity(PhaseGVN* phase); - virtual Node *Ideal(PhaseGVN *phase, bool can_reshape); + virtual Node* Ideal(PhaseGVN *phase, bool can_reshape); virtual const Type* Value(PhaseGVN* phase) const; - const Type *bottom_type() const { return TypeInt::INT; } + const Type* bottom_type() const { return TypeInt::INT; } virtual uint ideal_reg() const { return Op_RegI; } }; @@ -287,9 +291,9 @@ public: LShiftLNode(Node *in1, Node *in2) : LShiftNode(in1,in2) {} virtual int Opcode() const; virtual Node* Identity(PhaseGVN* phase); - virtual Node *Ideal(PhaseGVN *phase, bool can_reshape); + virtual Node* Ideal(PhaseGVN *phase, bool can_reshape); virtual const Type* Value(PhaseGVN* phase) const; - const Type *bottom_type() const { return TypeLong::LONG; } + const Type* bottom_type() const { return TypeLong::LONG; } virtual uint ideal_reg() const { return Op_RegL; } }; @@ -358,11 +362,17 @@ public: virtual uint ideal_reg() const { return Op_RegL; } }; +class URShiftNode : public Node { +public: + URShiftNode(Node* in1, Node* in2) : Node(nullptr, in1, in2) {} + static URShiftNode* make(Node* in1, Node* in2, BasicType bt); +}; + //------------------------------URShiftBNode----------------------------------- // Logical shift right -class URShiftBNode : public Node { +class URShiftBNode : public URShiftNode { public: - URShiftBNode( Node *in1, Node *in2 ) : Node(nullptr,in1,in2) { + URShiftBNode(Node* in1, Node* in2) : URShiftNode(in1,in2) { ShouldNotReachHere(); // only vector variant is used } virtual int Opcode() const; @@ -370,9 +380,9 @@ public: //------------------------------URShiftSNode----------------------------------- // Logical shift right -class URShiftSNode : public Node { +class URShiftSNode : public URShiftNode { public: - URShiftSNode( Node *in1, Node *in2 ) : Node(nullptr,in1,in2) { + URShiftSNode(Node* in1, Node* in2) : URShiftNode(in1,in2) { ShouldNotReachHere(); // only vector variant is used } virtual int Opcode() const; @@ -380,27 +390,27 @@ public: //------------------------------URShiftINode----------------------------------- // Logical shift right -class URShiftINode : public Node { +class URShiftINode : public URShiftNode { public: - URShiftINode( Node *in1, Node *in2 ) : Node(nullptr,in1,in2) {} + URShiftINode(Node* in1, Node* in2) : URShiftNode(in1,in2) {} virtual int Opcode() const; virtual Node* Identity(PhaseGVN* phase); - virtual Node *Ideal(PhaseGVN *phase, bool can_reshape); + virtual Node* Ideal(PhaseGVN* phase, bool can_reshape); virtual const Type* Value(PhaseGVN* phase) const; - const Type *bottom_type() const { return TypeInt::INT; } + const Type* bottom_type() const { return TypeInt::INT; } virtual uint ideal_reg() const { return Op_RegI; } }; //------------------------------URShiftLNode----------------------------------- // Logical shift right -class URShiftLNode : public Node { +class URShiftLNode : public URShiftNode { public: - URShiftLNode( Node *in1, Node *in2 ) : Node(nullptr,in1,in2) {} + URShiftLNode(Node* in1, Node* in2) : URShiftNode(in1,in2) {} virtual int Opcode() const; virtual Node* Identity(PhaseGVN* phase); - virtual Node *Ideal(PhaseGVN *phase, bool can_reshape); + virtual Node* Ideal(PhaseGVN* phase, bool can_reshape); virtual const Type* Value(PhaseGVN* phase) const; - const Type *bottom_type() const { return TypeLong::LONG; } + const Type* bottom_type() const { return TypeLong::LONG; } virtual uint ideal_reg() const { return Op_RegL; } }; diff --git a/src/hotspot/share/opto/node.hpp b/src/hotspot/share/opto/node.hpp index c2b3c4fb0ad..9ce9e705eec 100644 --- a/src/hotspot/share/opto/node.hpp +++ b/src/hotspot/share/opto/node.hpp @@ -2086,6 +2086,7 @@ Op_IL(Sub) Op_IL(Mul) Op_IL(URShift) Op_IL(LShift) +Op_IL(RShift) Op_IL(Xor) Op_IL(Cmp) Op_IL(Div) diff --git a/src/hotspot/share/utilities/globalDefinitions.hpp b/src/hotspot/share/utilities/globalDefinitions.hpp index 51ea80a0150..68900f8bc86 100644 --- a/src/hotspot/share/utilities/globalDefinitions.hpp +++ b/src/hotspot/share/utilities/globalDefinitions.hpp @@ -26,6 +26,7 @@ #define SHARE_UTILITIES_GLOBALDEFINITIONS_HPP #include "classfile_constants.h" +#include "utilities/checkedCast.hpp" #include "utilities/compilerWarnings.hpp" #include "utilities/debug.hpp" #include "utilities/forbiddenFunctions.hpp" @@ -1253,13 +1254,21 @@ JAVA_INTEGER_SHIFT_OP(>>, java_shift_right_unsigned, jlong, julong) #undef JAVA_INTEGER_SHIFT_OP +inline jlong java_negate(jlong v, BasicType bt) { + if (bt == T_INT) { + return java_negate(checked_cast(v)); + } + assert(bt == T_LONG, "int or long only"); + return java_negate(v); +} + // Some convenient bit shift operations that accepts a BasicType as the last // argument. These avoid potential mistakes with overloaded functions only // distinguished by lhs argument type. #define JAVA_INTEGER_SHIFT_BASIC_TYPE(FUNC) \ inline jlong FUNC(jlong lhs, jint rhs, BasicType bt) { \ if (bt == T_INT) { \ - return FUNC((jint) lhs, rhs); \ + return FUNC(checked_cast(lhs), rhs); \ } \ assert(bt == T_LONG, "unsupported basic type"); \ return FUNC(lhs, rhs); \ diff --git a/test/hotspot/jtreg/compiler/c2/irTests/LShiftINodeIdealizationTests.java b/test/hotspot/jtreg/compiler/c2/irTests/LShiftINodeIdealizationTests.java index 0b2a87fb2c5..7db56e2a878 100644 --- a/test/hotspot/jtreg/compiler/c2/irTests/LShiftINodeIdealizationTests.java +++ b/test/hotspot/jtreg/compiler/c2/irTests/LShiftINodeIdealizationTests.java @@ -54,6 +54,17 @@ public class LShiftINodeIdealizationTests { "testDoubleShift9", "testDoubleShiftSliceAndStore", "testRandom", + "testShiftValue", + "testShiftValueOverflow", + "testShiftMultiple32", + "testShiftOfAddSameInput", + "testLargeShiftOfAddSameInput", + "testShiftOfAddConstant", + "testLShiftOfAndOfRShiftSameCon", + "testLShiftOfAndOfURShiftSameCon", + "testLShiftOfAndOfRShift", + "testLShiftOfAndOfURShift", + "testLShiftOfAndOfCon", }) public void runMethod() { int a = RunInfo.getRandom().nextInt(); @@ -71,6 +82,29 @@ public class LShiftINodeIdealizationTests { assertResult(d); assertResult(min); assertResult(max); + + Asserts.assertEQ(42 << 1, testShiftValue(42)); + Asserts.assertEQ(Integer.MAX_VALUE << 1, testShiftValueOverflow(Integer.MAX_VALUE)); + Asserts.assertEQ((Integer.MAX_VALUE-1) << 1, testShiftValueOverflow(Integer.MAX_VALUE-1)); + + assertResult(a, b); + assertResult(c, d); + assertResult(a, min); + assertResult(a, max); + assertResult(min, a); + assertResult(max, a); + assertResult(min, max); + assertResult(max, min); + assertResult(min, min); + assertResult(max, max); + } + + private void assertResult(int a, int b) { + otherInput = b; + Asserts.assertEQ(((a >> 4) & b) << 4, testLShiftOfAndOfRShiftSameCon(a)); + Asserts.assertEQ(((a >>> 4) & b) << 4, testLShiftOfAndOfURShiftSameCon(a)); + Asserts.assertEQ(((a >> 4) & b) << 8, testLShiftOfAndOfRShift(a)); + Asserts.assertEQ(((a >>> 4) & b) << 8, testLShiftOfAndOfURShift(a)); } @DontCompile @@ -83,6 +117,11 @@ public class LShiftINodeIdealizationTests { Asserts.assertEQ((a >>> 8) << 4, test6(a)); Asserts.assertEQ(((a >> 4) & 0xFF) << 8, test7(a)); Asserts.assertEQ(((a >>> 4) & 0xFF) << 8, test8(a)); + Asserts.assertEQ(a, testShiftMultiple32(a)); + Asserts.assertEQ((a + a) << 1, testShiftOfAddSameInput(a)); + Asserts.assertEQ((a + a) << 31, testLargeShiftOfAddSameInput(a)); + Asserts.assertEQ(((a + 1) << 1) + 1, testShiftOfAddConstant(a)); + Asserts.assertEQ((a & ((1 << (32 - 10)) -1)) << 10, testLShiftOfAndOfCon(a)); assertDoubleShiftResult(a); } @@ -267,4 +306,107 @@ public class LShiftINodeIdealizationTests { public int testRandom(int x) { return (x << CON0) << CON1; } + + @Test + @IR(counts = {IRNode.LSHIFT, "1"}, failOn = { IRNode.IF } ) + public int testShiftValue(int x) { + x = Integer.min(Integer.max(x, 10), 100); + int shift = x << 1; + if (shift > 200 || shift < 20) { + throw new RuntimeException("never taken"); + } + return shift; + } + + @Test + @IR(counts = {IRNode.LSHIFT, "1", IRNode.IF, "2" } ) + public int testShiftValueOverflow(int x) { + x = Integer.max(x, Integer.MAX_VALUE - 1); + int shift = x << 1; + if (shift != -2 && shift != -4) { + throw new RuntimeException("never taken"); + } + return shift; + } + + @Test + @IR(failOn = { IRNode.LSHIFT_I } ) + public int testShiftMultiple32(int x) { + return x << 128; + } + + @Test + @IR(counts = { IRNode.LSHIFT_I, "1" }, failOn = { IRNode.ADD_I } ) + public int testShiftOfAddSameInput(int x) { + return (x + x) << 1; + } + + @Test + @IR(counts = { IRNode.LSHIFT_I, "1", IRNode.ADD_I, "1" } ) + public int testLargeShiftOfAddSameInput(int x) { + return (x + x) << 31; + } + + @Test + @IR(counts = { IRNode.LSHIFT_I, "1", IRNode.ADD_I, "1" } ) + public int testShiftOfAddConstant(int x) { + return ((x + 1) << 1) + 1; + } + + static short shortField; + static byte byteField; + + @Test + @IR(counts = { IRNode.ADD_I, "1"} , failOn = { IRNode.LSHIFT_I, IRNode.RSHIFT_I } ) + @Arguments( values = { Argument.NUMBER_42 }) + public void testStoreShort(int x) { + shortField = (short)(x + x); + } + + @Test + @IR(counts = { IRNode.ADD_I, "1"} , failOn = { IRNode.LSHIFT_I, IRNode.RSHIFT_I } ) + @Arguments( values = { Argument.NUMBER_42 }) + public void testStoreByte(int x) { + byteField = (byte)(x + x); + } + + static int otherInput; + + @Test + @IR(counts = { IRNode.AND_I, "1", IRNode.LSHIFT_I, "1" } , failOn = { IRNode.RSHIFT_I } ) + public int testLShiftOfAndOfRShiftSameCon(int x) { + int shift = x >> 4; + int y = otherInput; + return (shift & y) << 4; + } + + @Test + @IR(counts = { IRNode.AND_I, "1", IRNode.LSHIFT_I, "1" } , failOn = { IRNode.URSHIFT_I } ) + public int testLShiftOfAndOfURShiftSameCon(int x) { + int shift = x >>> 4; + int y = otherInput; + return (shift & y) << 4; + } + + @Test + @IR(counts = { IRNode.AND_I, "2", IRNode.LSHIFT_I, "2" } , failOn = { IRNode.RSHIFT_I } ) + public int testLShiftOfAndOfRShift(int x) { + int shift = x >> 4; + int y = otherInput; + return (shift & y) << 8; + } + + @Test + @IR(counts = { IRNode.AND_I, "2", IRNode.LSHIFT_I, "2" } , failOn = { IRNode.URSHIFT_I } ) + public int testLShiftOfAndOfURShift(int x) { + int shift = x >>> 4; + int y = otherInput; + return (shift & y) << 8; + } + + @Test + @IR(counts = { IRNode.LSHIFT_I, "1" } , failOn = { IRNode.AND_I } ) + public int testLShiftOfAndOfCon(int x) { + return (x & ((1 << (32 - 10)) -1)) << 10; + } } diff --git a/test/hotspot/jtreg/compiler/c2/irTests/LShiftLNodeIdealizationTests.java b/test/hotspot/jtreg/compiler/c2/irTests/LShiftLNodeIdealizationTests.java index 11eb928d564..f489a348eef 100644 --- a/test/hotspot/jtreg/compiler/c2/irTests/LShiftLNodeIdealizationTests.java +++ b/test/hotspot/jtreg/compiler/c2/irTests/LShiftLNodeIdealizationTests.java @@ -52,6 +52,17 @@ public class LShiftLNodeIdealizationTests { "testDoubleShift8", "testDoubleShift9", "testRandom", + "testShiftValue", + "testShiftValueOverflow", + "testShiftMultiple64", + "testShiftOfAddSameInput", + "testLargeShiftOfAddSameInput", + "testShiftOfAddConstant", + "testLShiftOfAndOfRShiftSameCon", + "testLShiftOfAndOfURShiftSameCon", + "testLShiftOfAndOfRShift", + "testLShiftOfAndOfURShift", + "testLShiftOfAndOfCon", }) public void runMethod() { long a = RunInfo.getRandom().nextLong(); @@ -69,6 +80,29 @@ public class LShiftLNodeIdealizationTests { assertResult(d); assertResult(min); assertResult(max); + + Asserts.assertEQ(42L << 1, testShiftValue(42)); + Asserts.assertEQ(Long.MAX_VALUE << 1, testShiftValueOverflow(Long.MAX_VALUE)); + Asserts.assertEQ((Long.MAX_VALUE-1) << 1, testShiftValueOverflow(Long.MAX_VALUE-1)); + + assertResult(a, b); + assertResult(c, d); + assertResult(a, min); + assertResult(a, max); + assertResult(min, a); + assertResult(max, a); + assertResult(min, max); + assertResult(max, min); + assertResult(min, min); + assertResult(max, max); + } + + private void assertResult(long a, long b) { + otherInput = b; + Asserts.assertEQ(((a >> 4) & b) << 4, testLShiftOfAndOfRShiftSameCon(a)); + Asserts.assertEQ(((a >>> 4) & b) << 4, testLShiftOfAndOfURShiftSameCon(a)); + Asserts.assertEQ(((a >> 4) & b) << 8, testLShiftOfAndOfRShift(a)); + Asserts.assertEQ(((a >>> 4) & b) << 8, testLShiftOfAndOfURShift(a)); } @DontCompile @@ -79,6 +113,11 @@ public class LShiftLNodeIdealizationTests { Asserts.assertEQ((a >>> 8L) << 4L, test6(a)); Asserts.assertEQ(((a >> 4L) & 0xFFL) << 8L, test7(a)); Asserts.assertEQ(((a >>> 4L) & 0xFFL) << 8L, test8(a)); + Asserts.assertEQ(a, testShiftMultiple64(a)); + Asserts.assertEQ((a + a) << 1, testShiftOfAddSameInput(a)); + Asserts.assertEQ((a + a) << 63, testLargeShiftOfAddSameInput(a)); + Asserts.assertEQ(((a + 1) << 1) + 1, testShiftOfAddConstant(a)); + Asserts.assertEQ((a & ((1L << (64 - 10)) -1)) << 10, testLShiftOfAndOfCon(a)); assertDoubleShiftResult(a); } @@ -233,4 +272,90 @@ public class LShiftLNodeIdealizationTests { public long testRandom(long x) { return (x << CON0) << CON1; } + + @Test + @IR(counts = {IRNode.LSHIFT, "1"}, failOn = { IRNode.IF } ) + public long testShiftValue(long x) { + x = Long.min(Long.max(x, 10), 100); + long shift = x << 1; + if (shift > 200 || shift < 20) { + throw new RuntimeException("never taken"); + } + return shift; + } + + @Test + @IR(counts = {IRNode.LSHIFT, "1", IRNode.IF, "2" } ) + public long testShiftValueOverflow(long x) { + x = Long.max(x, Long.MAX_VALUE - 1); + long shift = x << 1; + if (shift != -2 && shift != -4) { + throw new RuntimeException("never taken"); + } + return shift; + } + + @Test + @IR(failOn = { IRNode.LSHIFT_L } ) + public long testShiftMultiple64(long x) { + return x << 128; + } + + @Test + @IR(counts = { IRNode.LSHIFT_L, "1" }, failOn = { IRNode.ADD_L } ) + public long testShiftOfAddSameInput(long x) { + return (x + x) << 1; + } + + @Test + @IR(counts = { IRNode.LSHIFT_L, "1", IRNode.ADD_L, "1" } ) + public long testLargeShiftOfAddSameInput(long x) { + return (x + x) << 63; + } + + @Test + @IR(counts = { IRNode.LSHIFT_L, "1", IRNode.ADD_L, "1" } ) + public long testShiftOfAddConstant(long x) { + return ((x + 1) << 1) + 1; + } + + static long otherInput; + + @Test + @IR(counts = { IRNode.AND_L, "1", IRNode.LSHIFT_L, "1" } , failOn = { IRNode.RSHIFT_L } ) + public long testLShiftOfAndOfRShiftSameCon(long x) { + long shift = x >> 4; + long y = otherInput; + return (shift & y) << 4; + } + + @Test + @IR(counts = { IRNode.AND_L, "1", IRNode.LSHIFT_L, "1" } , failOn = { IRNode.URSHIFT_L } ) + public long testLShiftOfAndOfURShiftSameCon(long x) { + long shift = x >>> 4; + long y = otherInput; + return (shift & y) << 4; + } + + @Test + @IR(counts = { IRNode.AND_L, "2", IRNode.LSHIFT_L, "2" } , failOn = { IRNode.RSHIFT_L } ) + public long testLShiftOfAndOfRShift(long x) { + long shift = x >> 4; + long y = otherInput; + return (shift & y) << 8; + } + + @Test + @IR(counts = { IRNode.AND_L, "2", IRNode.LSHIFT_L, "2" } , failOn = { IRNode.URSHIFT_L } ) + public long testLShiftOfAndOfURShift(long x) { + long shift = x >>> 4; + long y = otherInput; + return (shift & y) << 8; + } + + @Test + @IR(counts = { IRNode.LSHIFT_L, "1" } , failOn = { IRNode.AND_L } ) + public long testLShiftOfAndOfCon(long x) { + return (x & ((1L << (64 - 10)) -1)) << 10; + } } From aed42a16bacb24753a536d07fedd736d64cde3be Mon Sep 17 00:00:00 2001 From: Artem Semenov Date: Thu, 16 Oct 2025 07:28:13 +0000 Subject: [PATCH 519/556] 8365609: Fix several potential NULL native pointer dereferences in the desktop module Found by Linux Verification Center (linuxtesting.org) with SVACE. Signed-off-by: Artem Semenov Artem Semenov Reviewed-by: azvegint, prr, serb --- .../share/native/libsplashscreen/splashscreen_gif.c | 4 +++- .../unix/native/libawt_xawt/awt/gtk3_interface.c | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c b/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c index cbdad61f78e..4f2cfca8dd0 100644 --- a/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c +++ b/src/java.desktop/share/native/libsplashscreen/splashscreen_gif.c @@ -279,7 +279,9 @@ SplashDecodeGif(Splash * splash, GifFileType * gif) ImageRect dstRect; rgbquad_t fillColor = 0; // 0 is transparent - if (transparentColor < 0) { + if (colorMap && + colorMap->Colors && + transparentColor < 0) { fillColor= MAKE_QUAD_GIF( colorMap->Colors[gif->SBackGroundColor], 0xff); } diff --git a/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c b/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c index 916880873c6..e5b2dfa6db9 100644 --- a/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c +++ b/src/java.desktop/unix/native/libawt_xawt/awt/gtk3_interface.c @@ -276,10 +276,7 @@ GtkApi* gtk3_load(JNIEnv *env, const char* lib_name) fp_gtk_check_version = dl_symbol("gtk_check_version"); /* GLib */ - fp_glib_check_version = dlsym(gtk3_libhandle, "glib_check_version"); - if (!fp_glib_check_version) { - dlerror(); - } + fp_glib_check_version = dl_symbol("glib_check_version"); fp_g_free = dl_symbol("g_free"); fp_g_object_unref = dl_symbol("g_object_unref"); From ff6a0170f0ab5cfb4af6d6a4a779451823c486d6 Mon Sep 17 00:00:00 2001 From: Roland Westrelin Date: Thu, 16 Oct 2025 07:35:41 +0000 Subject: [PATCH 520/556] 8369258: C2: enable ReassociateInvariants for all loop types Reviewed-by: epeter, qamai --- src/hotspot/share/opto/loopnode.cpp | 15 ++- .../loopopts/TestReassociateInvariants.java | 93 +++++++++++++++++++ ...MemorySegment_ReassociateInvariants1.java} | 15 +-- ...MemorySegment_ReassociateInvariants2.java} | 16 +--- 4 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/loopopts/TestReassociateInvariants.java rename test/hotspot/jtreg/compiler/loopopts/superword/{TestMemorySegment_8360204.java => TestMemorySegment_ReassociateInvariants1.java} (83%) rename test/hotspot/jtreg/compiler/loopopts/superword/{TestMemorySegment_8365982.java => TestMemorySegment_ReassociateInvariants2.java} (82%) diff --git a/src/hotspot/share/opto/loopnode.cpp b/src/hotspot/share/opto/loopnode.cpp index 4cb1862cbb9..e8058edb4e5 100644 --- a/src/hotspot/share/opto/loopnode.cpp +++ b/src/hotspot/share/opto/loopnode.cpp @@ -5176,21 +5176,20 @@ void PhaseIdealLoop::build_and_optimize() { continue; } Node* head = lpt->_head; - if (!head->is_BaseCountedLoop() || !lpt->is_innermost()) continue; + if (!lpt->is_innermost()) continue; // check for vectorized loops, any reassociation of invariants was already done - if (head->is_CountedLoop()) { - if (head->as_CountedLoop()->is_unroll_only()) { - continue; - } else { - AutoNodeBudget node_budget(this); - lpt->reassociate_invariants(this); - } + if (head->is_CountedLoop() && head->as_CountedLoop()->is_unroll_only()) { + continue; + } else { + AutoNodeBudget node_budget(this); + lpt->reassociate_invariants(this); } // Because RCE opportunities can be masked by split_thru_phi, // look for RCE candidates and inhibit split_thru_phi // on just their loop-phi's for this pass of loop opts if (SplitIfBlocks && do_split_ifs && + head->is_BaseCountedLoop() && head->as_BaseCountedLoop()->is_valid_counted_loop(head->as_BaseCountedLoop()->bt()) && (lpt->policy_range_check(this, true, T_LONG) || (head->is_CountedLoop() && lpt->policy_range_check(this, true, T_INT)))) { diff --git a/test/hotspot/jtreg/compiler/loopopts/TestReassociateInvariants.java b/test/hotspot/jtreg/compiler/loopopts/TestReassociateInvariants.java new file mode 100644 index 00000000000..d03cb4e8567 --- /dev/null +++ b/test/hotspot/jtreg/compiler/loopopts/TestReassociateInvariants.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 IBM Corporation. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8369258 + * @summary C2: enable ReassociateInvariants for all loop types + * @library /test/lib / + * @run driver compiler.loopopts.TestReassociateInvariants + */ + +package compiler.loopopts; + + +import compiler.lib.ir_framework.*; + +import java.util.Objects; + +public class TestReassociateInvariants { + private static long longStart = 0; + private static long longStop = 1000; + private static int intStart = 0; + private static int intStop = 1000; + + public static void main(String[] args) { + TestFramework.runWithFlags("-XX:-ShortRunningLongLoop"); + } + + // The IR framework is not powerful enough to directly check + // wether invariants are moved out of a loop so tests below rely on + // some side effect that can be observed by the IR framework. + + // Once a + (b + i) is transformed into i + (a + b), the a + b + // before the loop and the one from inside the loop common and one + // Add is removed. + @Test + @IR(counts = {IRNode.ADD_I, "3"}) + @Arguments(values = { Argument.NUMBER_42, Argument.NUMBER_42 }) + public int test1(int a, int b) { + int v = a + b; + for (int i = 1; i < 100; i *= 2) { + v += a + (b + i); + } + return v; + } + + // Range Check Elimination only happens once a + (b + i) is + // transformed into i + (a + b). With the range check eliminated, + // the loop can be removed. At this point, C2 doesn't support + // removal of long counted loop. The long counted loop is + // transformed into a loop nest with an inner int counted + // loop. That one is empty and is removed. + @Test + @IR(failOn = { IRNode.COUNTED_LOOP, IRNode.LONG_COUNTED_LOOP }) + @IR(counts = { IRNode.LOOP, "1" }) + @Arguments(values = { Argument.NUMBER_42, Argument.NUMBER_42 }) + public void test2(long a, long b) { + for (long i = longStart; i < longStop; i++) { + Objects.checkIndex(a + (b + i), Long.MAX_VALUE); + } + } + + // Same here for an int counted loop with long range checks + @Test + @IR(failOn = { IRNode.COUNTED_LOOP }) + @IR(counts = { IRNode.LOOP, "1" }) + @Arguments(values = { Argument.NUMBER_42, Argument.NUMBER_42 }) + public void test3(long a, long b) { + for (int i = intStart; i < intStop; i++) { + Objects.checkIndex(a + (b + i), Long.MAX_VALUE); + } + } +} diff --git a/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_8360204.java b/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_ReassociateInvariants1.java similarity index 83% rename from test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_8360204.java rename to test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_ReassociateInvariants1.java index ecefcec8afa..57864a09d69 100644 --- a/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_8360204.java +++ b/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_ReassociateInvariants1.java @@ -31,16 +31,16 @@ import compiler.lib.ir_framework.*; /* * @test - * @bug 8324751 + * @bug 8324751 8369258 * @summary Reported issue: JDK-8360204: C2 SuperWord: missing RCE with MemorySegment.getAtIndex * The examples are generated from TestAliasingFuzzer.java * So if you see something change here, you may want to investigate if we * can also tighten up the IR rules there. * @library /test/lib / - * @run driver compiler.loopopts.superword.TestMemorySegment_8360204 + * @run driver compiler.loopopts.superword.TestMemorySegment_ReassociateInvariants1 */ -public class TestMemorySegment_8360204 { +public class TestMemorySegment_ReassociateInvariants1 { public static MemorySegment a = Arena.ofAuto().allocate(10_000); public static MemorySegment b = Arena.ofAuto().allocate(10_000); @@ -67,20 +67,13 @@ public class TestMemorySegment_8360204 { @Arguments(setup = "setup") @IR(counts = {IRNode.LOAD_VECTOR_I, "= 0", IRNode.STORE_VECTOR, "= 0", - ".*multiversion.*", "> 0"}, // Sadly, we now multiversion + ".*multiversion.*", "= 0"}, phase = CompilePhase.PRINT_IDEAL, applyIfPlatform = {"64-bit", "true"}, applyIf = {"AlignVector", "false"}, applyIfCPUFeatureOr = {"avx", "true", "asimd", "true"}) - // There is no aliasing, so we should compile without multiversioning. - // But currently, there seems to be some issue with RCE, we peel and lose the predicate. - // Then we multiversion. // We could imagine that this would eventually vectorize, but since one counts up, and the other down, // we would have to implement shuffle first. - // - // If you see this IR rule fail: investigate JDK-8360204, possibly close it and fix this IR rule! - // Also: consider renaming the file to something more descriptive: what have you fixed with this? - // And: you may now be able to tighten IR rules in TestAliasingFuzzer.java public static void test(MemorySegment container_0, long invar0_0, MemorySegment container_1, long invar0_1, long ivLo, long ivHi) { for (long i = ivLo; i < ivHi; i+=1) { var v = container_0.getAtIndex(ValueLayout.JAVA_INT_UNALIGNED, 19125L + 1L * i + 1L * invar0_0 + 0L * invar0_1159 + 1L * invar1_1159); diff --git a/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_8365982.java b/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_ReassociateInvariants2.java similarity index 82% rename from test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_8365982.java rename to test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_ReassociateInvariants2.java index 65fd3861174..6f1590f5e19 100644 --- a/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_8365982.java +++ b/test/hotspot/jtreg/compiler/loopopts/superword/TestMemorySegment_ReassociateInvariants2.java @@ -31,16 +31,16 @@ import compiler.lib.ir_framework.*; /* * @test - * @bug 8324751 + * @bug 8324751 8369258 * @summary Reported issue: JDK-8365982: C2 SuperWord: missing RCE / strange Multiversioning with MemorySegment.set * The examples are generated from TestAliasingFuzzer.java * So if you see something change here, you may want to investigate if we * can also tighten up the IR rules there. * @library /test/lib / - * @run driver compiler.loopopts.superword.TestMemorySegment_8365982 + * @run driver compiler.loopopts.superword.TestMemorySegment_ReassociateInvariants2 */ -public class TestMemorySegment_8365982 { +public class TestMemorySegment_ReassociateInvariants2 { public static MemorySegment a = MemorySegment.ofArray(new short[100_000]); public static MemorySegment b = MemorySegment.ofArray(new short[100_000]); @@ -76,19 +76,11 @@ public class TestMemorySegment_8365982 { // @IR(counts = {IRNode.STORE_VECTOR, "> 0", IRNode.REPLICATE_S, "> 0", - ".*multiversion.*", "> 0"}, // Bad: Sadly, we now multiversion + ".*multiversion.*", "= 0"}, phase = CompilePhase.PRINT_IDEAL, applyIfPlatform = {"64-bit", "true"}, applyIfAnd = {"AlignVector", "false", "ShortRunningLongLoop", "true"}, applyIfCPUFeatureOr = {"avx", "true", "asimd", "true"}) - // Some but not all predicates are RCE'd at the beginning. After unrolling, we multiversion (why?). - // After PreMainPost, we can do more RangeCheck. Now the main-loop of the multiversion_fast loop - // does not have any range checks any more. - // Now it vectorizes. That's good, but we should be able to vectorize without multiversioning. - // - // If you see this IR rule fail: investigate JDK-8365982, possibly close it and fix this IR rule! - // Also: consider renaming the file to something more descriptive: what have you fixed with this? - // And: you may now be able to tighten IR rules in TestAliasingFuzzer.java public static void test(MemorySegment container_0, long invar0_0, MemorySegment container_1, long invar0_1, long ivLo, long ivHi) { for (long i = ivHi-1; i >= ivLo; i-=1) { container_0.set(ValueLayout.JAVA_CHAR_UNALIGNED, -47143L + -2L * i + -2L * invar0_0 + -1L * invar0_853 + -1L * invar1_853 + 0L * invar2_853, (char)0x0102030405060708L); From 17c13e53aff16b294c7c0286ccb6ea3054b1de91 Mon Sep 17 00:00:00 2001 From: Christoph Langer Date: Thu, 16 Oct 2025 07:54:23 +0000 Subject: [PATCH 521/556] 8369683: Exclude runtime/Monitor/MonitorWithDeadObjectTest.java#DumpThreadsBeforeDetach on Alpine Linux debug Reviewed-by: mbaesken, dholmes --- .../jtreg/runtime/Monitor/MonitorWithDeadObjectTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/runtime/Monitor/MonitorWithDeadObjectTest.java b/test/hotspot/jtreg/runtime/Monitor/MonitorWithDeadObjectTest.java index 7f9b44a4a76..b1e6d0aa8c7 100644 --- a/test/hotspot/jtreg/runtime/Monitor/MonitorWithDeadObjectTest.java +++ b/test/hotspot/jtreg/runtime/Monitor/MonitorWithDeadObjectTest.java @@ -39,7 +39,8 @@ /* * @test id=DumpThreadsBeforeDetach - * @requires os.family != "windows" & os.family != "aix" + * @comment Temporarily exclude on Musl-C debug until JDK-8366133 is fixed. + * @requires os.family != "windows" & os.family != "aix" & (!vm.musl | !vm.debug) * @run main/othervm/native MonitorWithDeadObjectTest 1 */ From b5b83247da9caea30c88b69543e350783663bc46 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Thu, 16 Oct 2025 08:28:22 +0000 Subject: [PATCH 522/556] 8369656: Calling CompletableFuture.join() could execute task in common pool Reviewed-by: alanb, dl --- .../util/concurrent/CompletableFuture.java | 8 ++-- .../concurrent/tck/CompletableFutureTest.java | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java b/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java index 7503c154ddb..1338f2fd804 100644 --- a/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java +++ b/src/java.base/share/classes/java/util/concurrent/CompletableFuture.java @@ -1904,8 +1904,8 @@ public class CompletableFuture implements Future, CompletionStage { while ((r = result) == null) { if (q == null) { q = new Signaller(interruptible, 0L, 0L); - if (Thread.currentThread() instanceof ForkJoinWorkerThread) - ForkJoinPool.helpAsyncBlocker(defaultExecutor(), q); + if (Thread.currentThread() instanceof ForkJoinWorkerThread wt) + ForkJoinPool.helpAsyncBlocker(wt.pool, q); } else if (!queued) queued = tryPushStack(q); @@ -1950,8 +1950,8 @@ public class CompletableFuture implements Future, CompletionStage { break; else if (q == null) { q = new Signaller(true, nanos, deadline); - if (Thread.currentThread() instanceof ForkJoinWorkerThread) - ForkJoinPool.helpAsyncBlocker(defaultExecutor(), q); + if (Thread.currentThread() instanceof ForkJoinWorkerThread wt) + ForkJoinPool.helpAsyncBlocker(wt.pool, q); } else if (!queued) queued = tryPushStack(q); diff --git a/test/jdk/java/util/concurrent/tck/CompletableFutureTest.java b/test/jdk/java/util/concurrent/tck/CompletableFutureTest.java index de3d1dd1050..fc86936d5da 100644 --- a/test/jdk/java/util/concurrent/tck/CompletableFutureTest.java +++ b/test/jdk/java/util/concurrent/tck/CompletableFutureTest.java @@ -58,6 +58,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.ForkJoinWorkerThread; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; @@ -5133,4 +5134,46 @@ public class CompletableFutureTest extends JSR166TestCase { checkCompletedWithWrappedException(g.toCompletableFuture(), r.ex); r.assertInvoked(); }} + + public void testOnlyHelpsIfInTheSamePool() throws Exception { + class Logic { + interface Extractor { ForkJoinPool pool(CompletableFuture cf) throws Exception; } + static final List executeInnerOuter( + ForkJoinPool outer, ForkJoinPool inner, Logic.Extractor extractor + ) throws Exception { + return CompletableFuture.supplyAsync(() -> + Stream.iterate(1, i -> i + 1) + .limit(64) + .map(i -> CompletableFuture.supplyAsync( + () -> Thread.currentThread() instanceof ForkJoinWorkerThread wt ? wt.getPool() : null, inner) + ) + .map(cf -> { + try { + return extractor.pool(cf); + } catch (Exception ex) { + throw new AssertionError("Unexpected", ex); + } + }) + .toList() + , outer).join(); + } + } + + List extractors = + List.of( + c -> c.get(60, SECONDS), + CompletableFuture::get, + CompletableFuture::join + ); + + try (var pool = new ForkJoinPool(2)) { + for (var extractor : extractors) { + for (var p : Logic.executeInnerOuter(pool, ForkJoinPool.commonPool(), extractor)) + assertTrue(p != pool); // The inners should have all been executed by commonPool + + for (var p : Logic.executeInnerOuter(pool, pool, extractor)) + assertTrue(p == pool); // The inners could have been helped by the outer + } + } + } } From 6e911d819efa0f14ab1f9009b5bf325d99edb26c Mon Sep 17 00:00:00 2001 From: Martin Doerr Date: Thu, 16 Oct 2025 09:40:55 +0000 Subject: [PATCH 523/556] 8368205: [TESTBUG] VectorMaskCompareNotTest.java crashes when MaxVectorSize=8 Reviewed-by: dzhang, epeter, rrich --- .../vectorapi/VectorMaskCompareNotTest.java | 2 +- test/jtreg-ext/requires/VMProps.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java index 235093cceed..09185f63c69 100644 --- a/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java +++ b/test/hotspot/jtreg/compiler/vectorapi/VectorMaskCompareNotTest.java @@ -35,7 +35,7 @@ import jdk.test.lib.Asserts; * @library /test/lib / * @summary test combining vector not operation with compare * @modules jdk.incubator.vector - * @requires (os.arch != "riscv64" | (os.arch == "riscv64" & vm.cpu.features ~= ".*rvv.*")) + * @requires vm.opt.final.MaxVectorSize == "null" | vm.opt.final.MaxVectorSize >= 16 * * @run driver compiler.vectorapi.VectorMaskCompareNotTest */ diff --git a/test/jtreg-ext/requires/VMProps.java b/test/jtreg-ext/requires/VMProps.java index 29057f24d3b..3c06c97b37a 100644 --- a/test/jtreg-ext/requires/VMProps.java +++ b/test/jtreg-ext/requires/VMProps.java @@ -149,6 +149,7 @@ public class VMProps implements Callable> { vmGC(map); // vm.gc.X = true/false vmGCforCDS(map); // may set vm.gc vmOptFinalFlags(map); + vmOptFinalIntxFlags(map); dump(map.map); log("Leaving call()"); @@ -389,6 +390,26 @@ public class VMProps implements Callable> { vmOptFinalFlag(map, "UseVectorizedMismatchIntrinsic"); } + /** + * Selected final flag of type intx. + * + * @param map - property-value pairs + * @param flagName - flag name + */ + private void vmOptFinalIntxFlag(SafeMap map, String flagName) { + map.put("vm.opt.final." + flagName, + () -> String.valueOf(WB.getIntxVMFlag(flagName))); + } + + /** + * Selected sets of final flags of type intx. + * + * @param map - property-value pairs + */ + protected void vmOptFinalIntxFlags(SafeMap map) { + vmOptFinalIntxFlag(map, "MaxVectorSize"); + } + /** * @return "true" if VM has a serviceability agent. */ From d6c122b3ff1ccd559ba9c310976a77eefaf09ece Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Thu, 16 Oct 2025 09:57:11 +0000 Subject: [PATCH 524/556] 8369982: ProblemList jdk/jfr/jvm/TestWaste.java Reviewed-by: tschatzl, dholmes --- test/jdk/ProblemList.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index c305bc0bbeb..0e69446ae35 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -750,6 +750,7 @@ jdk/incubator/vector/LoadJsvmlTest.java 8305390 windows- jdk/jfr/event/compiler/TestCodeSweeper.java 8338127 generic-all jdk/jfr/event/oldobject/TestShenandoah.java 8342951 generic-all jdk/jfr/event/runtime/TestResidentSetSizeEvent.java 8309846 aix-ppc64 +jdk/jfr/jvm/TestWaste.java 8369949 generic-all ############################################################################ From ead35a754bf3a545a1b68f28d3d939750f11af39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jeli=C5=84ski?= Date: Thu, 16 Oct 2025 11:05:13 +0000 Subject: [PATCH 525/556] 8358942: HttpClient adds Content-Length: 0 for a GET request with a BodyPublishers.noBody() Reviewed-by: dfuchs, vyazici --- .../jdk/internal/net/http/Http1Request.java | 11 +- .../httpclient/ContentLengthHeaderTest.java | 154 +++++++++++++++--- 2 files changed, 136 insertions(+), 29 deletions(-) diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java index 815b6bad20c..8d28b664036 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java @@ -290,7 +290,8 @@ class Http1Request { } String uriString = requestURI(); StringBuilder sb = new StringBuilder(64); - sb.append(request.method()) + String method = request.method(); + sb.append(method) .append(' ') .append(uriString) .append(" HTTP/1.1\r\n"); @@ -300,11 +301,15 @@ class Http1Request { systemHeadersBuilder.setHeader("Host", hostString()); } - // GET, HEAD and DELETE with no request body should not set the Content-Length header if (requestPublisher != null) { contentLength = requestPublisher.contentLength(); if (contentLength == 0) { - systemHeadersBuilder.setHeader("Content-Length", "0"); + // PUT and POST with no request body should set the Content-Length header + // even when the content is empty. + // Other methods defined in RFC 9110 should not send the header in that case. + if ("POST".equals(method) || "PUT".equals(method)) { + systemHeadersBuilder.setHeader("Content-Length", "0"); + } } else if (contentLength > 0) { systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength)); streaming = false; diff --git a/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java b/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java index f302de4ee48..d7c77d0690a 100644 --- a/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java +++ b/test/jdk/java/net/httpclient/ContentLengthHeaderTest.java @@ -29,8 +29,9 @@ * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext * jdk.httpclient.test.lib.common.HttpServerAdapters - * @bug 8283544 + * @bug 8283544 8358942 * @run testng/othervm + * -Djdk.httpclient.allowRestrictedHeaders=content-length * -Djdk.internal.httpclient.debug=true * ContentLengthHeaderTest */ @@ -95,8 +96,8 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { testContentLengthServerH2.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); testContentLengthServerH3.addHandler(new NoContentLengthHandler(), NO_BODY_PATH); testContentLengthServerH1.addHandler(new ContentLengthHandler(), BODY_PATH); - testContentLengthServerH2.addHandler(new OptionalContentLengthHandler(), BODY_PATH); - testContentLengthServerH3.addHandler(new OptionalContentLengthHandler(), BODY_PATH); + testContentLengthServerH2.addHandler(new ContentLengthHandler(), BODY_PATH); + testContentLengthServerH3.addHandler(new ContentLengthHandler(), BODY_PATH); testContentLengthURIH1 = URIBuilder.newBuilder() .scheme("http") .loopback() @@ -163,6 +164,13 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { }; } + @DataProvider(name = "h1body") + Object[][] h1body() { + return new Object[][]{ + {HTTP_1_1, URI.create(testContentLengthURIH1 + BODY_PATH)} + }; + } + @DataProvider(name = "nobodies") Object[][] nobodies() { return new Object[][]{ @@ -186,6 +194,35 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { assertEquals(resp.version(), version); } + @Test(dataProvider = "nobodies") + // A GET request with empty request body should have no Content-length header + public void getWithEmptyBody(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking GET with no request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .method("GET", HttpRequest.BodyPublishers.noBody()) + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + + @Test(dataProvider = "bodies") + // A GET request with empty request body and explicitly added Content-length header + public void getWithZeroContentLength(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking GET with no request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .method("GET", HttpRequest.BodyPublishers.noBody()) + .header("Content-length", "0") + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + @Test(dataProvider = "bodies") // A GET request with a request body should have a Content-length header // in HTTP/1.1 @@ -215,6 +252,20 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { assertEquals(resp.version(), version); } + @Test(dataProvider = "nobodies") + // A DELETE request with empty request body should have no Content-length header + public void deleteWithEmptyBody(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking DELETE with no request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .method("DELETE", HttpRequest.BodyPublishers.noBody()) + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + @Test(dataProvider = "bodies") // A DELETE request with a request body should have a Content-length header // in HTTP/1.1 @@ -244,6 +295,20 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { assertEquals(resp.version(), version); } + @Test(dataProvider = "nobodies") + // A HEAD request with empty request body should have no Content-length header + public void headWithEmptyBody(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking HEAD with no request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .method("HEAD", HttpRequest.BodyPublishers.noBody()) + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + @Test(dataProvider = "bodies") // A HEAD request with a request body should have a Content-length header // in HTTP/1.1 @@ -261,6 +326,66 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { assertEquals(resp.version(), version); } + @Test(dataProvider = "h1body") + // A POST request with empty request body should have a Content-length header + // in HTTP/1.1 + public void postWithEmptyBody(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking POST with request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .method("POST", HttpRequest.BodyPublishers.noBody()) + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + + @Test(dataProvider = "bodies") + // A POST request with a request body should have a Content-length header + // in HTTP/1.1 + public void postWithBody(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking POST with request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .POST(HttpRequest.BodyPublishers.ofString("POST Body")) + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + + @Test(dataProvider = "h1body") + // A PUT request with empty request body should have a Content-length header + // in HTTP/1.1 + public void putWithEmptyBody(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking PUT with request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .method("PUT", HttpRequest.BodyPublishers.noBody()) + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + + @Test(dataProvider = "bodies") + // A PUT request with a request body should have a Content-length header + // in HTTP/1.1 + public void putWithBody(Version version, URI uri) throws IOException, InterruptedException { + testLog.println(version + " Checking PUT with request body"); + HttpRequest req = HttpRequest.newBuilder() + .version(version) + .PUT(HttpRequest.BodyPublishers.ofString("PUT Body")) + .uri(uri) + .build(); + HttpResponse resp = hc.send(req, HttpResponse.BodyHandlers.ofString(UTF_8)); + assertEquals(resp.statusCode(), 200, resp.body()); + assertEquals(resp.version(), version); + } + public static void handleResponse(long expected, HttpTestExchange ex, String body, int rCode) throws IOException { try (InputStream is = ex.getRequestBody()) { byte[] reqBody = is.readAllBytes(); @@ -324,27 +449,4 @@ public class ContentLengthHeaderTest implements HttpServerAdapters { } } } - - /** - * A handler used for cases where the presence of a Content-Length - * header is optional. If present, its value must match the number of - * bytes sent in the request body. - */ - static class OptionalContentLengthHandler implements HttpTestHandler { - - @Override - public void handle(HttpTestExchange exchange) throws IOException { - testLog.println("OptionalContentLengthHandler: Received Headers " - + exchange.getRequestHeaders().entrySet() + - " from " + exchange.getRequestMethod() + " request."); - Optional contentLength = exchange.getRequestHeaders().firstValue("Content-Length"); - - // Check Content-length header was set - if (contentLength.isPresent()) { - handleResponse(Long.parseLong(contentLength.get()), exchange, "Request completed", 200); - } else { - handleResponse(-1, exchange, "Request completed, no content length", 200); - } - } - } } From 5fc3904bfe290625ed6cf9b41773b35b52bf72b7 Mon Sep 17 00:00:00 2001 From: Stefan Karlsson Date: Thu, 16 Oct 2025 11:16:05 +0000 Subject: [PATCH 526/556] 8369491: Temporarily revert default TIMEOUT_FACTOR back to 4 Reviewed-by: lkorinth, cstein, jpai, syan, serb, prr --- doc/testing.html | 2 +- doc/testing.md | 2 +- make/RunTests.gmk | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/testing.html b/doc/testing.html index 89a9b1b23b7..b9838735e4f 100644 --- a/doc/testing.html +++ b/doc/testing.html @@ -450,7 +450,7 @@ itself (-timeoutFactor). Also, some test cases that programmatically wait a certain amount of time will apply this factor. If we run in forced compilation mode (-Xcomp), the build system will automatically adjust this factor to compensate for less -performance. Defaults to 1.

        +performance. Defaults to 4.

        FAILURE_HANDLER_TIMEOUT

        Sets the argument -timeoutHandlerTimeout for JTReg. The default value is 0. This is only valid if the failure handler is diff --git a/doc/testing.md b/doc/testing.md index 324f9645c27..0144610a5bf 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -387,7 +387,7 @@ The `TIMEOUT_FACTOR` is forwarded to JTReg framework itself (`-timeoutFactor`). Also, some test cases that programmatically wait a certain amount of time will apply this factor. If we run in forced compilation mode (`-Xcomp`), the build system will automatically -adjust this factor to compensate for less performance. Defaults to 1. +adjust this factor to compensate for less performance. Defaults to 4. #### FAILURE_HANDLER_TIMEOUT diff --git a/make/RunTests.gmk b/make/RunTests.gmk index 7b05a0ba12f..947389f64f9 100644 --- a/make/RunTests.gmk +++ b/make/RunTests.gmk @@ -946,8 +946,8 @@ define SetupRunJtregTestBody JTREG_ALL_OPTIONS := $$(JTREG_JAVA_OPTIONS) $$(JTREG_VM_OPTIONS) JTREG_AUTO_PROBLEM_LISTS := - # Please reach consensus before changing this. It was not easy changing it to a `1`. - JTREG_AUTO_TIMEOUT_FACTOR := 1 + # Please reach consensus before changing this. + JTREG_AUTO_TIMEOUT_FACTOR := 4 ifneq ($$(findstring -Xcomp, $$(JTREG_ALL_OPTIONS)), ) JTREG_AUTO_PROBLEM_LISTS += ProblemList-Xcomp.txt From 1653999871c8d7b1e61b44f8525e09b2cd0bdb6b Mon Sep 17 00:00:00 2001 From: Yasumasa Suenaga Date: Thu, 16 Oct 2025 12:45:05 +0000 Subject: [PATCH 527/556] 8369505: jhsdb jstack cannot handle continuation stub Reviewed-by: cjplummer, pchilanomate --- .../share/runtime/continuationEntry.hpp | 1 + src/hotspot/share/runtime/vmStructs.cpp | 5 +- .../sun/jvm/hotspot/code/CodeBlob.java | 2 + .../hotspot/runtime/ContinuationEntry.java | 63 ++++++++++++++ .../sun/jvm/hotspot/runtime/JavaThread.java | 6 ++ .../hotspot/runtime/aarch64/AARCH64Frame.java | 18 +++- .../hotspot/runtime/riscv64/RISCV64Frame.java | 18 +++- .../sun/jvm/hotspot/runtime/x86/X86Frame.java | 18 +++- .../sa/LingeredAppWithVirtualThread.java | 87 +++++++++++++++++++ .../sa/TestJhsdbJstackWithVirtualThread.java | 83 ++++++++++++++++++ 10 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ContinuationEntry.java create mode 100644 test/hotspot/jtreg/serviceability/sa/LingeredAppWithVirtualThread.java create mode 100644 test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java diff --git a/src/hotspot/share/runtime/continuationEntry.hpp b/src/hotspot/share/runtime/continuationEntry.hpp index 8361f2f912b..490293f5b11 100644 --- a/src/hotspot/share/runtime/continuationEntry.hpp +++ b/src/hotspot/share/runtime/continuationEntry.hpp @@ -39,6 +39,7 @@ class RegisterMap; // Metadata stored in the continuation entry frame class ContinuationEntry { + friend class VMStructs; friend class JVMCIVMStructs; ContinuationEntryPD _pd; #ifdef ASSERT diff --git a/src/hotspot/share/runtime/vmStructs.cpp b/src/hotspot/share/runtime/vmStructs.cpp index dee0a5d4eb7..8dc4b660f91 100644 --- a/src/hotspot/share/runtime/vmStructs.cpp +++ b/src/hotspot/share/runtime/vmStructs.cpp @@ -616,6 +616,7 @@ nonstatic_field(JavaThread, _active_handles, JNIHandleBlock*) \ nonstatic_field(JavaThread, _monitor_owner_id, int64_t) \ volatile_nonstatic_field(JavaThread, _terminated, JavaThread::TerminatedTypes) \ + nonstatic_field(JavaThread, _cont_entry, ContinuationEntry*) \ nonstatic_field(Thread, _osthread, OSThread*) \ \ /************/ \ @@ -796,7 +797,8 @@ nonstatic_field(Mutex, _name, const char*) \ static_field(Mutex, _mutex_array, Mutex**) \ static_field(Mutex, _num_mutex, int) \ - volatile_nonstatic_field(Mutex, _owner, Thread*) + volatile_nonstatic_field(Mutex, _owner, Thread*) \ + static_field(ContinuationEntry, _return_pc, address) //-------------------------------------------------------------------------------- // VM_TYPES @@ -1270,6 +1272,7 @@ declare_toplevel_type(FileMapHeader) \ declare_toplevel_type(CDSFileMapRegion) \ declare_toplevel_type(UpcallStub::FrameData) \ + declare_toplevel_type(ContinuationEntry) \ \ /************/ \ /* GC types */ \ diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/CodeBlob.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/CodeBlob.java index 20c5fabf8bc..12469efc67e 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/CodeBlob.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/CodeBlob.java @@ -180,6 +180,8 @@ public class CodeBlob extends VMObject { public boolean isUpcallStub() { return getKind() == UpcallKind; } + public boolean isContinuationStub() { return getName().equals("StubRoutines (continuation stubs)"); } + public boolean isJavaMethod() { return false; } public boolean isNativeMethod() { return false; } diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ContinuationEntry.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ContinuationEntry.java new file mode 100644 index 00000000000..73152bdee84 --- /dev/null +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ContinuationEntry.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, NTT DATA. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +package sun.jvm.hotspot.runtime; + +import sun.jvm.hotspot.debugger.*; +import sun.jvm.hotspot.runtime.*; +import sun.jvm.hotspot.types.*; + + +public class ContinuationEntry extends VMObject { + private static long size; + private static Address returnPC; + + static { + VM.registerVMInitializedObserver((o, d) -> initialize(VM.getVM().getTypeDataBase())); + } + + private static synchronized void initialize(TypeDataBase db) throws WrongTypeException { + Type type = db.lookupType("ContinuationEntry"); + size = type.getSize(); + returnPC = type.getAddressField("_return_pc").getValue(); + } + + public ContinuationEntry(Address addr) { + super(addr); + } + + public Address getEntryPC() { + return returnPC; + } + + public Address getEntrySP(){ + return this.getAddress(); + } + + public Address getEntryFP(){ + return this.getAddress().addOffsetTo(size); + } + +} diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java index d92f464f0d2..826b5cecfd5 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java @@ -47,6 +47,7 @@ public class JavaThread extends Thread { private static AddressField stackBaseField; private static CIntegerField stackSizeField; private static CIntegerField terminatedField; + private static AddressField contEntryField; private static AddressField activeHandlesField; private static CIntegerField monitorOwnerIDField; private static long oopPtrSize; @@ -95,6 +96,7 @@ public class JavaThread extends Thread { stackBaseField = type.getAddressField("_stack_base"); stackSizeField = type.getCIntegerField("_stack_size"); terminatedField = type.getCIntegerField("_terminated"); + contEntryField = type.getAddressField("_cont_entry"); activeHandlesField = type.getAddressField("_active_handles"); monitorOwnerIDField = type.getCIntegerField("_monitor_owner_id"); @@ -340,6 +342,10 @@ public class JavaThread extends Thread { return (int) terminatedField.getValue(addr); } + public ContinuationEntry getContEntry() { + return VMObjectFactory.newObject(ContinuationEntry.class, contEntryField.getValue(addr)); + } + /** Gets the Java-side thread object for this JavaThread */ public Oop getThreadObj() { Oop obj = null; diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java index a5aa7ce4405..5ae4cb703b3 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/aarch64/AARCH64Frame.java @@ -270,7 +270,13 @@ public class AARCH64Frame extends Frame { } if (cb != null) { - return cb.isUpcallStub() ? senderForUpcallStub(map, (UpcallStub)cb) : senderForCompiledFrame(map, cb); + if (cb.isUpcallStub()) { + return senderForUpcallStub(map, (UpcallStub)cb); + } else if (cb.isContinuationStub()) { + return senderForContinuationStub(map, cb); + } else { + return senderForCompiledFrame(map, cb); + } } // Must be native-compiled frame, i.e. the marshaling code for native @@ -356,6 +362,16 @@ public class AARCH64Frame extends Frame { map.setLocation(fp, savedFPAddr); } + private Frame senderForContinuationStub(AARCH64RegisterMap map, CodeBlob cb) { + var contEntry = map.getThread().getContEntry(); + + Address senderSP = contEntry.getEntrySP(); + Address senderPC = contEntry.getEntryPC(); + Address senderFP = contEntry.getEntryFP(); + + return new AARCH64Frame(senderSP, senderFP, senderPC); + } + private Frame senderForCompiledFrame(AARCH64RegisterMap map, CodeBlob cb) { if (DEBUG) { System.out.println("senderForCompiledFrame"); diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java index e02e056f028..44c8f4c679c 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/riscv64/RISCV64Frame.java @@ -262,7 +262,13 @@ public class RISCV64Frame extends Frame { } if (cb != null) { - return cb.isUpcallStub() ? senderForUpcallStub(map, (UpcallStub)cb) : senderForCompiledFrame(map, cb); + if (cb.isUpcallStub()) { + return senderForUpcallStub(map, (UpcallStub)cb); + } else if (cb.isContinuationStub()) { + return senderForContinuationStub(map, cb); + } else { + return senderForCompiledFrame(map, cb); + } } // Must be native-compiled frame, i.e. the marshaling code for native @@ -348,6 +354,16 @@ public class RISCV64Frame extends Frame { map.setLocation(fp, savedFPAddr); } + private Frame senderForContinuationStub(RISCV64RegisterMap map, CodeBlob cb) { + var contEntry = map.getThread().getContEntry(); + + Address senderSP = contEntry.getEntrySP(); + Address senderPC = contEntry.getEntryPC(); + Address senderFP = contEntry.getEntryFP(); + + return new RISCV64Frame(senderSP, senderFP, senderPC); + } + private Frame senderForCompiledFrame(RISCV64RegisterMap map, CodeBlob cb) { if (DEBUG) { System.out.println("senderForCompiledFrame"); diff --git a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java index 3ee4f0a8158..2d972d3df17 100644 --- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java +++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/x86/X86Frame.java @@ -270,7 +270,13 @@ public class X86Frame extends Frame { } if (cb != null) { - return cb.isUpcallStub() ? senderForUpcallStub(map, (UpcallStub)cb) : senderForCompiledFrame(map, cb); + if (cb.isUpcallStub()) { + return senderForUpcallStub(map, (UpcallStub)cb); + } else if (cb.isContinuationStub()) { + return senderForContinuationStub(map, cb); + } else { + return senderForCompiledFrame(map, cb); + } } // Must be native-compiled frame, i.e. the marshaling code for native @@ -356,6 +362,16 @@ public class X86Frame extends Frame { map.setLocation(rbp, savedFPAddr); } + private Frame senderForContinuationStub(X86RegisterMap map, CodeBlob cb) { + var contEntry = map.getThread().getContEntry(); + + Address senderSP = contEntry.getEntrySP(); + Address senderPC = contEntry.getEntryPC(); + Address senderFP = contEntry.getEntryFP(); + + return new X86Frame(senderSP, senderFP, senderPC); + } + private Frame senderForCompiledFrame(X86RegisterMap map, CodeBlob cb) { if (DEBUG) { System.out.println("senderForCompiledFrame"); diff --git a/test/hotspot/jtreg/serviceability/sa/LingeredAppWithVirtualThread.java b/test/hotspot/jtreg/serviceability/sa/LingeredAppWithVirtualThread.java new file mode 100644 index 00000000000..ca98506e133 --- /dev/null +++ b/test/hotspot/jtreg/serviceability/sa/LingeredAppWithVirtualThread.java @@ -0,0 +1,87 @@ + +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, NTT DATA + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.lang.invoke.MethodHandle; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.util.concurrent.CountDownLatch; + +import jdk.test.lib.apps.LingeredApp; + +public class LingeredAppWithVirtualThread extends LingeredApp implements Runnable { + + private static final String THREAD_NAME = "target thread"; + + private static final MethodHandle hndSleep; + + private static final int sleepArg; + + private static final CountDownLatch signal = new CountDownLatch(1); + + static { + MemorySegment func; + if (System.getProperty("os.name").startsWith("Windows")) { + func = SymbolLookup.libraryLookup("Kernel32", Arena.global()) + .findOrThrow("Sleep"); + sleepArg = 3600_000; // 1h in milliseconds + } else { + func = Linker.nativeLinker() + .defaultLookup() + .findOrThrow("sleep"); + sleepArg = 3600; // 1h in seconds + } + + var desc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT); + hndSleep = Linker.nativeLinker().downcallHandle(func, desc); + } + + @Override + public void run() { + Thread.yield(); + signal.countDown(); + try { + hndSleep.invoke(sleepArg); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public static void main(String[] args) { + try { + Thread.ofVirtual() + .name(THREAD_NAME) + .start(new LingeredAppWithVirtualThread()); + + signal.await(); + LingeredApp.main(args); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java b/test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java new file mode 100644 index 00000000000..fce9906ca94 --- /dev/null +++ b/test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, NTT DATA + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jdk.test.lib.JDKToolLauncher; +import jdk.test.lib.SA.SATestUtils; +import jdk.test.lib.Utils; +import jdk.test.lib.apps.LingeredApp; +import jdk.test.lib.process.OutputAnalyzer; + +/** + * @test + * @bug 8369505 + * @requires vm.hasSA + * @requires (os.arch == "amd64" | os.arch == "x86_64" | os.arch == "aarch64" | os.arch == "riscv64") + * @library /test/lib + * @run driver TestJhsdbJstackWithVirtualThread + */ +public class TestJhsdbJstackWithVirtualThread { + + private static void runJstack(LingeredApp app) throws Exception { + JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jhsdb"); + launcher.addVMArgs(Utils.getTestJavaOpts()); + launcher.addToolArg("jstack"); + launcher.addToolArg("--pid"); + launcher.addToolArg(Long.toString(app.getPid())); + + ProcessBuilder pb = SATestUtils.createProcessBuilder(launcher); + Process jhsdb = pb.start(); + OutputAnalyzer out = new OutputAnalyzer(jhsdb); + + jhsdb.waitFor(); + + System.out.println(out.getStdout()); + System.err.println(out.getStderr()); + + out.stderrShouldBeEmptyIgnoreDeprecatedWarnings(); + out.shouldNotContain("must have non-zero frame size"); + } + + public static void main(String... args) throws Exception { + SATestUtils.skipIfCannotAttach(); // throws SkippedException if attach not expected to work. + LingeredApp app = null; + + try { + app = new LingeredAppWithVirtualThread(); + LingeredApp.startApp(app); + System.out.println("Started LingeredApp with pid " + app.getPid()); + runJstack(app); + System.out.println("Test Completed"); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } finally { + LingeredApp.stopApp(app); + } + } +} From f475eb8ee7c9a3e360b2f1210ed71b629243cd2a Mon Sep 17 00:00:00 2001 From: Hamlin Li Date: Thu, 16 Oct 2025 14:04:45 +0000 Subject: [PATCH 528/556] 8368950: RISC-V: fail to catch out of order declarations among dependent cpu extensions/flags Reviewed-by: fyang, luhenry --- src/hotspot/cpu/riscv/vm_version_riscv.hpp | 100 +++++++++++------- .../os_cpu/linux_riscv/riscv_hwprobe.cpp | 87 +++++++-------- 2 files changed, 108 insertions(+), 79 deletions(-) diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.hpp b/src/hotspot/cpu/riscv/vm_version_riscv.hpp index 346ca35dc1e..3d555d47e9f 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.hpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.hpp @@ -64,41 +64,12 @@ class VM_Version : public Abstract_VM_Version { virtual void disable_feature() { _value = -1; } - const char* pretty() { return _pretty; } - uint64_t feature_bit() { return _linux_feature_bit; } - bool feature_string() { return _feature_string; } - int64_t value() { return _value; } + const char* pretty() { return _pretty; } + uint64_t feature_bit() { return _linux_feature_bit; } + bool feature_string() { return _feature_string; } + int64_t value() { return _value; } virtual bool enabled() = 0; virtual void update_flag() = 0; - - protected: - bool deps_all_enabled(RVFeatureValue* dep0, ...) { - assert(dep0 != nullptr, "must not"); - - va_list va; - va_start(va, dep0); - RVFeatureValue* next = dep0; - bool enabled = true; - while (next != nullptr && enabled) { - enabled = next->enabled(); - next = va_arg(va, RVFeatureValue*); - } - va_end(va); - return enabled; - } - - void deps_string(stringStream& ss, RVFeatureValue* dep0, ...) { - assert(dep0 != nullptr, "must not"); - ss.print("%s (%s)", dep0->pretty(), dep0->enabled() ? "enabled" : "disabled"); - - va_list va; - va_start(va, dep0); - RVFeatureValue* next = nullptr; - while ((next = va_arg(va, RVFeatureValue*)) != nullptr) { - ss.print(", %s (%s)", next->pretty(), next->enabled() ? "enabled" : "disabled"); - } - va_end(va); - } }; #define UPDATE_DEFAULT(flag) \ @@ -117,8 +88,9 @@ class VM_Version : public Abstract_VM_Version { #define UPDATE_DEFAULT_DEP(flag, dep0, ...) \ void update_flag() { \ assert(enabled(), "Must be."); \ + DEBUG_ONLY(verify_deps(dep0, ##__VA_ARGS__)); \ if (FLAG_IS_DEFAULT(flag)) { \ - if (this->deps_all_enabled(dep0, ##__VA_ARGS__)) { \ + if (deps_all_enabled(dep0, ##__VA_ARGS__)) { \ FLAG_SET_DEFAULT(flag, true); \ } else { \ FLAG_SET_DEFAULT(flag, false); \ @@ -149,11 +121,16 @@ class VM_Version : public Abstract_VM_Version { class RVExtFeatureValue : public RVFeatureValue { const uint32_t _cpu_feature_index; + public: RVExtFeatureValue(const char* pretty, int linux_bit_num, uint32_t cpu_feature_index, bool fstring) : RVFeatureValue(pretty, linux_bit_num, fstring), _cpu_feature_index(cpu_feature_index) { } + int cpu_feature_index() { + // Can be used to check, for example, v is declared before Zvfh in RV_EXT_FEATURE_FLAGS. + return _cpu_feature_index; + } bool enabled() { return RVExtFeatures::current()->support_feature(_cpu_feature_index); } @@ -165,6 +142,57 @@ class VM_Version : public Abstract_VM_Version { RVFeatureValue::disable_feature(); RVExtFeatures::current()->clear_feature(_cpu_feature_index); } + + protected: + bool deps_all_enabled(RVExtFeatureValue* dep0, ...) { + assert(dep0 != nullptr, "must not"); + + va_list va; + va_start(va, dep0); + RVExtFeatureValue* next = dep0; + bool enabled = true; + while (next != nullptr && enabled) { + enabled = next->enabled(); + next = va_arg(va, RVExtFeatureValue*); + } + va_end(va); + return enabled; + } + + void deps_string(stringStream& ss, RVExtFeatureValue* dep0, ...) { + assert(dep0 != nullptr, "must not"); + ss.print("%s (%s)", dep0->pretty(), dep0->enabled() ? "enabled" : "disabled"); + + va_list va; + va_start(va, dep0); + RVExtFeatureValue* next = nullptr; + while ((next = va_arg(va, RVExtFeatureValue*)) != nullptr) { + ss.print(", %s (%s)", next->pretty(), next->enabled() ? "enabled" : "disabled"); + } + va_end(va); + } + +#ifdef ASSERT + void verify_deps(RVExtFeatureValue* dep0, ...) { + assert(dep0 != nullptr, "must not"); + assert(cpu_feature_index() >= 0, "must"); + + va_list va; + va_start(va, dep0); + RVExtFeatureValue* next = dep0; + while (next != nullptr) { + assert(next->cpu_feature_index() >= 0, "must"); + // We only need to check depenency relationship for extension flags. + // The dependant ones must be declared before this, for example, v must be declared + // before Zvfh in RV_EXT_FEATURE_FLAGS. The reason is in setup_cpu_available_features + // we need to make sure v is `update_flag`ed before Zvfh, so Zvfh is `update_flag`ed + // based on v. + assert(cpu_feature_index() > next->cpu_feature_index(), "Invalid"); + next = va_arg(va, RVExtFeatureValue*); + } + va_end(va); + } +#endif // ASSERT }; class RVNonExtFeatureValue : public RVFeatureValue { @@ -282,14 +310,14 @@ class VM_Version : public Abstract_VM_Version { decl(marchid , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ /* A unique encoding of the version of the processor implementation. */ \ decl(mimpid , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ + /* Manufactory JEDEC id encoded, ISA vol 2 3.1.2.. */ \ + decl(mvendorid , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ /* SATP bits (number of virtual addr bits) mbare, sv39, sv48, sv57, sv64 */ \ decl(satp_mode , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ /* Performance of misaligned scalar accesses (unknown, emulated, slow, fast, unsupported) */ \ decl(unaligned_scalar , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ /* Performance of misaligned vector accesses (unknown, unspported, slow, fast) */ \ decl(unaligned_vector , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ - /* Manufactory JEDEC id encoded, ISA vol 2 3.1.2.. */ \ - decl(mvendorid , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ decl(zicboz_block_size , RV_NO_FLAG_BIT, false, NO_UPDATE_DEFAULT) \ #define DECLARE_RV_NON_EXT_FEATURE(PRETTY, LINUX_BIT, FSTRING, FLAGF) \ diff --git a/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp b/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp index 017d8a43666..ec756c44fe6 100644 --- a/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp +++ b/src/hotspot/os_cpu/linux_riscv/riscv_hwprobe.cpp @@ -167,27 +167,20 @@ static bool is_set(int64_t key, uint64_t value_mask) { void RiscvHwprobe::add_features_from_query_result() { assert(rw_hwprobe_completed, "hwprobe not init yet."); - if (is_valid(RISCV_HWPROBE_KEY_MVENDORID)) { - VM_Version::mvendorid.enable_feature(query[RISCV_HWPROBE_KEY_MVENDORID].value); - } - if (is_valid(RISCV_HWPROBE_KEY_MARCHID)) { - VM_Version::marchid.enable_feature(query[RISCV_HWPROBE_KEY_MARCHID].value); - } - if (is_valid(RISCV_HWPROBE_KEY_MIMPID)) { - VM_Version::mimpid.enable_feature(query[RISCV_HWPROBE_KEY_MIMPID].value); - } + // ====== extensions ====== + // if (is_set(RISCV_HWPROBE_KEY_BASE_BEHAVIOR, RISCV_HWPROBE_BASE_BEHAVIOR_IMA)) { + VM_Version::ext_a.enable_feature(); VM_Version::ext_i.enable_feature(); VM_Version::ext_m.enable_feature(); - VM_Version::ext_a.enable_feature(); - } - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_IMA_FD)) { - VM_Version::ext_f.enable_feature(); - VM_Version::ext_d.enable_feature(); } if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_IMA_C)) { VM_Version::ext_c.enable_feature(); } + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_IMA_FD)) { + VM_Version::ext_d.enable_feature(); + VM_Version::ext_f.enable_feature(); + } if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_IMA_V)) { // Linux signal return bug when using vector with vlen > 128b in pre 6.8.5. long major, minor, patch; @@ -202,21 +195,29 @@ void RiscvHwprobe::add_features_from_query_result() { VM_Version::ext_v.enable_feature(); } } + +#ifndef PRODUCT + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZACAS)) { + VM_Version::ext_Zacas.enable_feature(); + } +#endif if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZBA)) { VM_Version::ext_Zba.enable_feature(); } if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZBB)) { VM_Version::ext_Zbb.enable_feature(); } +#ifndef PRODUCT + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZBKB)) { + VM_Version::ext_Zbkb.enable_feature(); + } +#endif if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZBS)) { VM_Version::ext_Zbs.enable_feature(); } #ifndef PRODUCT - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZICBOZ)) { - VM_Version::ext_Zicboz.enable_feature(); - } - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZBKB)) { - VM_Version::ext_Zbkb.enable_feature(); + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZFA)) { + VM_Version::ext_Zfa.enable_feature(); } #endif if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZFH)) { @@ -226,15 +227,28 @@ void RiscvHwprobe::add_features_from_query_result() { VM_Version::ext_Zfhmin.enable_feature(); } #ifndef PRODUCT + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZICBOZ)) { + VM_Version::ext_Zicboz.enable_feature(); + } + // Currently tests shows that cmove using Zicond instructions will bring + // performance regression, but to get a test coverage all the time, will + // still prefer to enabling it in debug version. + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZICOND)) { + VM_Version::ext_Zicond.enable_feature(); + } + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZTSO)) { + VM_Version::ext_Ztso.enable_feature(); + } if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZVBB)) { VM_Version::ext_Zvbb.enable_feature(); } -#endif -#ifndef PRODUCT if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZVBC)) { VM_Version::ext_Zvbc.enable_feature(); } #endif + if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZVFH)) { + VM_Version::ext_Zvfh.enable_feature(); + } #ifndef PRODUCT if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZVKNED) && is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZVKNHB) && @@ -243,30 +257,18 @@ void RiscvHwprobe::add_features_from_query_result() { VM_Version::ext_Zvkn.enable_feature(); } #endif - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZVFH)) { - VM_Version::ext_Zvfh.enable_feature(); + + // ====== non-extensions ====== + // + if (is_valid(RISCV_HWPROBE_KEY_MARCHID)) { + VM_Version::marchid.enable_feature(query[RISCV_HWPROBE_KEY_MARCHID].value); } -#ifndef PRODUCT - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZFA)) { - VM_Version::ext_Zfa.enable_feature(); + if (is_valid(RISCV_HWPROBE_KEY_MIMPID)) { + VM_Version::mimpid.enable_feature(query[RISCV_HWPROBE_KEY_MIMPID].value); } -#endif -#ifndef PRODUCT - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZTSO)) { - VM_Version::ext_Ztso.enable_feature(); + if (is_valid(RISCV_HWPROBE_KEY_MVENDORID)) { + VM_Version::mvendorid.enable_feature(query[RISCV_HWPROBE_KEY_MVENDORID].value); } -#endif -#ifndef PRODUCT - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZACAS)) { - VM_Version::ext_Zacas.enable_feature(); - } - // Currently tests shows that cmove using Zicond instructions will bring - // performance regression, but to get a test coverage all the time, will - // still prefer to enabling it in debug version. - if (is_set(RISCV_HWPROBE_KEY_IMA_EXT_0, RISCV_HWPROBE_EXT_ZICOND)) { - VM_Version::ext_Zicond.enable_feature(); - } -#endif // RISCV_HWPROBE_KEY_CPUPERF_0 is deprecated and returns similar values // to RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF. Keep it there for backward // compatibility with old kernels. @@ -277,7 +279,6 @@ void RiscvHwprobe::add_features_from_query_result() { VM_Version::unaligned_scalar.enable_feature( query[RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF].value); } - if (is_valid(RISCV_HWPROBE_KEY_MISALIGNED_VECTOR_PERF)) { VM_Version::unaligned_vector.enable_feature( query[RISCV_HWPROBE_KEY_MISALIGNED_VECTOR_PERF].value); From 5dfe115ce1fbcff67777518a3c23a7560ebec423 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 16 Oct 2025 14:10:14 +0000 Subject: [PATCH 529/556] 8369912: [TESTBUG] testlibrary_tests/template_framework/examples/TestExpressions.java fails with ArithmeticException: / by zero - forgot to respect Expression.info Reviewed-by: kvn, mhaessig --- .../examples/TestExpressions.java | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java index 6e11a705054..c21d2492fc7 100644 --- a/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java +++ b/test/hotspot/jtreg/testlibrary_tests/template_framework/examples/TestExpressions.java @@ -70,29 +70,52 @@ public class TestExpressions { var withConstantsTemplate = Template.make("expression", (Expression expression) -> { // Create a token: fill the expression with a fixed set of constants. // We then use the same token with the same constants, once compiled and once not compiled. + // + // Some expressions can throw Exceptions. We have to catch them. In such a case, we return + // the Exception instead of the value from the expression, and compare the Exceptions. + // + // Some Expressions do not have a deterministic result. For example, different NaN or + // precision results from some operators. We only compare the results if we know that the + // result is deterministically the same. TemplateToken expressionToken = expression.asToken(expression.argumentTypes.stream().map(t -> t.con()).toList()); return body( let("returnType", expression.returnType), """ @Test public static void $primitiveConTest() { - #returnType v0 = ${primitiveConTest}_compiled(); - #returnType v1 = ${primitiveConTest}_reference(); - Verify.checkEQ(v0, v1); + Object v0 = ${primitiveConTest}_compiled(); + Object v1 = ${primitiveConTest}_reference(); + """, + expression.info.isResultDeterministic ? "Verify.checkEQ(v0, v1);\n" : "", + """ } @DontInline - public static #returnType ${primitiveConTest}_compiled() { + public static Object ${primitiveConTest}_compiled() { + try { """, - "return ", expressionToken, ";\n", + "return ", expressionToken, ";\n", + expression.info.exceptions.stream().map(exception -> + "} catch (" + exception + " e) { return e;\n" + ).toList(), """ + } finally { + // Just so that javac is happy if there are no exceptions to catch. + } } @DontCompile - public static #returnType ${primitiveConTest}_reference() { + public static Object ${primitiveConTest}_reference() { + try { """, - "return ", expressionToken, ";\n", + "return ", expressionToken, ";\n", + expression.info.exceptions.stream().map(exception -> + "} catch (" + exception + " e) { return e;\n" + ).toList(), """ + } finally { + // Just so that javac is happy if there are no exceptions to catch. + } } """ ); From f2a998326a6bebd4a7d2d0a39f785b2e6dac68c4 Mon Sep 17 00:00:00 2001 From: Emanuel Peter Date: Thu, 16 Oct 2025 14:22:15 +0000 Subject: [PATCH 530/556] 8369804: TestGenerators.java fails with IllegalArgumentException: bound must be greater than origin Reviewed-by: chagedorn, thartmann --- .../generators/UniformDoubleGenerator.java | 3 +++ .../lib/generators/UniformFloatGenerator.java | 3 +++ .../generators/tests/TestGenerators.java | 22 ++++++++++++++----- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/test/hotspot/jtreg/compiler/lib/generators/UniformDoubleGenerator.java b/test/hotspot/jtreg/compiler/lib/generators/UniformDoubleGenerator.java index d160bf319d8..b5729aeec7d 100644 --- a/test/hotspot/jtreg/compiler/lib/generators/UniformDoubleGenerator.java +++ b/test/hotspot/jtreg/compiler/lib/generators/UniformDoubleGenerator.java @@ -35,6 +35,9 @@ final class UniformDoubleGenerator extends UniformIntersectionRestrictableGenera */ public UniformDoubleGenerator(Generators g, double lo, double hi) { super(g, lo, hi); + if (Double.compare(lo, hi) >= 0) { + throw new EmptyGeneratorException(); + } } @Override diff --git a/test/hotspot/jtreg/compiler/lib/generators/UniformFloatGenerator.java b/test/hotspot/jtreg/compiler/lib/generators/UniformFloatGenerator.java index 1b72ad5adc9..4405b120619 100644 --- a/test/hotspot/jtreg/compiler/lib/generators/UniformFloatGenerator.java +++ b/test/hotspot/jtreg/compiler/lib/generators/UniformFloatGenerator.java @@ -35,6 +35,9 @@ final class UniformFloatGenerator extends UniformIntersectionRestrictableGenerat */ public UniformFloatGenerator(Generators g, float lo, float hi) { super(g, lo, hi); + if (Float.compare(lo, hi) >= 0) { + throw new EmptyGeneratorException(); + } } @Override diff --git a/test/hotspot/jtreg/testlibrary_tests/generators/tests/TestGenerators.java b/test/hotspot/jtreg/testlibrary_tests/generators/tests/TestGenerators.java index 8ad0c17ba98..f949f99c035 100644 --- a/test/hotspot/jtreg/testlibrary_tests/generators/tests/TestGenerators.java +++ b/test/hotspot/jtreg/testlibrary_tests/generators/tests/TestGenerators.java @@ -391,13 +391,13 @@ public class TestGenerators { Asserts.assertThrows(EmptyGeneratorException.class, () -> G.uniformDoubles(1, 0)); Asserts.assertNotNull(G.uniformDoubles(0, 1)); - Asserts.assertNotNull(G.uniformDoubles(0, 0)); + Asserts.assertThrows(EmptyGeneratorException.class, () -> G.uniformDoubles(0, 0)); Asserts.assertThrows(EmptyGeneratorException.class, () -> G.uniformDoubles(0, 1).restricted(1.1d, 2.4d)); Asserts.assertNotNull(G.uniformDoubles(0, 1).restricted(0.9d, 2.4d)); Asserts.assertThrows(EmptyGeneratorException.class, () -> G.uniformFloats(1, 0)); Asserts.assertNotNull(G.uniformFloats(0, 1)); - Asserts.assertNotNull(G.uniformFloats(0, 0)); + Asserts.assertThrows(EmptyGeneratorException.class, () -> G.uniformFloats(0, 0)); Asserts.assertThrows(EmptyGeneratorException.class, () -> G.uniformFloats(0, 1).restricted(1.1f, 2.4f)); Asserts.assertNotNull(G.uniformFloats(0, 1).restricted(0.9f, 2.4f)); @@ -592,8 +592,13 @@ public class TestGenerators { var floatBoundGen = G.uniformFloats(); for (int j = 0; j < 500; j++) { - float a = floatBoundGen.next(), b = floatBoundGen.next(); - float lo = Math.min(a, b), hi = Math.max(a, b); + float lo = 1, hi = 0; + // Failure of a single round is very rare, repeated failure even rarer. + while (lo >= hi) { + float a = floatBoundGen.next(), b = floatBoundGen.next(); + lo = Math.min(a, b); + hi = Math.max(a, b); + } var gb = G.uniformFloats(lo, hi); for (int i = 0; i < 10_000; i++) { float x = gb.next(); @@ -604,8 +609,13 @@ public class TestGenerators { var doubleBoundGen = G.uniformDoubles(); for (int j = 0; j < 500; j++) { - double a = doubleBoundGen.next(), b = doubleBoundGen.next(); - double lo = Math.min(a, b), hi = Math.max(a, b); + double lo = 1, hi = 0; + // Failure of a single round is very rare, repeated failure even rarer. + while (lo >= hi) { + double a = doubleBoundGen.next(), b = doubleBoundGen.next(); + lo = Math.min(a, b); + hi = Math.max(a, b); + } var gb = G.uniformDoubles(lo, hi); for (int i = 0; i < 10_000; i++) { double x = gb.next(); From 303eb1096ccaf06106aa080b9ea0553c0f6912dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lund=C3=A9n?= Date: Thu, 16 Oct 2025 15:02:32 +0000 Subject: [PATCH 531/556] 8369573: Add missing compile commands help documentation for the signature part of method patterns Reviewed-by: rcastanedalo, aseoane, thartmann --- src/hotspot/share/compiler/compilerOracle.cpp | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/hotspot/share/compiler/compilerOracle.cpp b/src/hotspot/share/compiler/compilerOracle.cpp index 868ae8bfa41..23bb754f432 100644 --- a/src/hotspot/share/compiler/compilerOracle.cpp +++ b/src/hotspot/share/compiler/compilerOracle.cpp @@ -617,18 +617,44 @@ static void usage() { tty->cr(); print_commands(); tty->cr(); - tty->print_cr("Method patterns has the format:"); - tty->print_cr(" package/Class.method()"); + tty->print_cr("The has the format '.'."); + tty->cr(); + tty->print_cr("For example, the "); + tty->cr(); + tty->print_cr(" package/Class.method(Lpackage/Parameter;)Lpackage/Return;"); + tty->cr(); + tty->print_cr("matches the 'method' in 'package/Class' with "); + tty->print_cr("'(Lpackage/Parameter;)Lpackage/Return;'"); tty->cr(); tty->print_cr("For backward compatibility this form is also allowed:"); - tty->print_cr(" package.Class::method()"); tty->cr(); - tty->print_cr("The signature can be separated by an optional whitespace or comma:"); - tty->print_cr(" package/Class.method ()"); + tty->print_cr(" package.Class::method(Lpackage.Parameter;)Lpackage.Return;"); tty->cr(); - tty->print_cr("The class and method identifier can be used together with leading or"); - tty->print_cr("trailing *'s for wildcard matching:"); - tty->print_cr(" *ackage/Clas*.*etho*()"); + tty->print_cr("A whitespace or comma can optionally separate the from the"); + tty->print_cr(":"); + tty->cr(); + tty->print_cr(" package/Class.method (Lpackage/Parameter;)Lpackage/Return;"); + tty->print_cr(" package/Class.method,(Lpackage/Parameter;)Lpackage/Return;"); + tty->cr(); + tty->print_cr("The and accept leading and trailing '*' wildcards"); + tty->print_cr("matching:"); + tty->cr(); + tty->print_cr(" *ackage/Clas*.*etho*(Lpackage/Parameter;)Lpackage/Return;"); + tty->cr(); + tty->print_cr("The does not support explicit wildcards and"); + tty->print_cr("always has an implicit trailing wildcard. Therefore,"); + tty->cr(); + tty->print_cr(" package/Class.method(Lpackage/Parameter;)Lpackage/Return;"); + tty->cr(); + tty->print_cr("matches a subset of"); + tty->cr(); + tty->print_cr(" package/Class.method(Lpackage/Parameter;)"); + tty->cr(); + tty->print_cr("which matches a subset of"); + tty->cr(); + tty->print_cr(" package/Class.method"); + tty->cr(); + tty->print_cr("which matches all possible descriptors."); tty->cr(); tty->print_cr("It is possible to use more than one CompileCommand on the command line:"); tty->print_cr(" -XX:CompileCommand=exclude,java/*.* -XX:CompileCommand=log,java*.*"); From 87092ef1d97e00ddb6674b0e309f2f904d307604 Mon Sep 17 00:00:00 2001 From: Arno Zeller Date: Thu, 16 Oct 2025 15:15:19 +0000 Subject: [PATCH 532/556] 8183336: Better cleanup for jdk/test/java/lang/module/customfs/ModulesInCustomFileSystem.java Reviewed-by: alanb, syan --- .../java/lang/module/customfs/ModulesInCustomFileSystem.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jdk/java/lang/module/customfs/ModulesInCustomFileSystem.java b/test/jdk/java/lang/module/customfs/ModulesInCustomFileSystem.java index 801f2e5fca6..e023a3a5839 100644 --- a/test/jdk/java/lang/module/customfs/ModulesInCustomFileSystem.java +++ b/test/jdk/java/lang/module/customfs/ModulesInCustomFileSystem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,7 @@ * @library /test/lib * @build ModulesInCustomFileSystem m1/* m2/* * jdk.test.lib.util.JarUtils - * @run testng/othervm ModulesInCustomFileSystem + * @run testng/othervm -Djava.io.tmpdir=. ModulesInCustomFileSystem * @summary Test ModuleFinder to find modules in a custom file system */ From 95380e1ea5c3f531f82fb7c4b2f75726f3cd2fc2 Mon Sep 17 00:00:00 2001 From: Roger Riggs Date: Thu, 16 Oct 2025 15:54:22 +0000 Subject: [PATCH 533/556] 8362637: Convert java.nio.ByteOrder to an enum Reviewed-by: alanb, liach, bpb --- .../share/classes/java/nio/ByteOrder.java | 41 ++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/src/java.base/share/classes/java/nio/ByteOrder.java b/src/java.base/share/classes/java/nio/ByteOrder.java index 96f2317b956..ab6876448be 100644 --- a/src/java.base/share/classes/java/nio/ByteOrder.java +++ b/src/java.base/share/classes/java/nio/ByteOrder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,28 +35,19 @@ import jdk.internal.misc.Unsafe; * @since 1.4 */ -public final class ByteOrder { - - private final String name; - - private ByteOrder(String name) { - this.name = name; - } - - /** - * Constant denoting big-endian byte order. In this order, the bytes of a - * multibyte value are ordered from most significant to least significant. - */ - public static final ByteOrder BIG_ENDIAN - = new ByteOrder("BIG_ENDIAN"); - +public enum ByteOrder { /** * Constant denoting little-endian byte order. In this order, the bytes of * a multibyte value are ordered from least significant to most * significant. */ - public static final ByteOrder LITTLE_ENDIAN - = new ByteOrder("LITTLE_ENDIAN"); + LITTLE_ENDIAN, + /** + * Constant denoting big-endian byte order. In this order, the bytes of a + * multibyte value are ordered from most significant to least significant. + */ + BIG_ENDIAN; + // Retrieve the native byte order. It's used early during bootstrap, and // must be initialized after BIG_ENDIAN and LITTLE_ENDIAN. @@ -78,18 +69,4 @@ public final class ByteOrder { public static ByteOrder nativeOrder() { return NATIVE_ORDER; } - - /** - * Constructs a string describing this object. - * - *

        This method returns the string - * {@code "BIG_ENDIAN"} for {@link #BIG_ENDIAN} and - * {@code "LITTLE_ENDIAN"} for {@link #LITTLE_ENDIAN}. - * - * @return The specified string - */ - public String toString() { - return name; - } - } From e56db37734aa7cbc0f20ba3fc469f51224f288fa Mon Sep 17 00:00:00 2001 From: Christian Hagedorn Date: Thu, 16 Oct 2025 16:02:26 +0000 Subject: [PATCH 534/556] 8369232: testlibrary_tests/ir_framework/tests/TestScenariosCrossProduct.java timed out Reviewed-by: dfenacci, epeter --- .../lib/ir_framework/TestFramework.java | 27 +- .../tests/TestScenariosCrossProduct.java | 384 +++++++++++++----- 2 files changed, 305 insertions(+), 106 deletions(-) diff --git a/test/hotspot/jtreg/compiler/lib/ir_framework/TestFramework.java b/test/hotspot/jtreg/compiler/lib/ir_framework/TestFramework.java index 85c52ef33da..09e291ce5a4 100644 --- a/test/hotspot/jtreg/compiler/lib/ir_framework/TestFramework.java +++ b/test/hotspot/jtreg/compiler/lib/ir_framework/TestFramework.java @@ -327,8 +327,10 @@ public class TestFramework { for (Scenario scenario : scenarios) { int scenarioIndex = scenario.getIndex(); - TestFormat.checkNoThrow(scenarioIndices.add(scenarioIndex), - "Cannot define two scenarios with the same index " + scenarioIndex); + if (!scenarioIndices.add(scenarioIndex)) { + TestFormat.failNoThrow("Cannot define two scenarios with the same index " + scenarioIndex); + continue; + } this.scenarios.add(scenario); } TestFormat.throwIfAnyFailures(); @@ -336,9 +338,12 @@ public class TestFramework { } /** - * Add the cross-product (cartesian product) of sets of flags as Scenarios. Unlike when when constructing + * Add the cross-product (cartesian product) of sets of flags as Scenarios. Unlike when constructing * scenarios directly a string can contain multiple flags separated with a space. This allows grouping - * flags that have to be specified togeher. Further, an empty string in a set stands in for "no flag". + * flags that have to be specified together. Further, an empty string in a set stands in for "no flag". + *

        + * Passing a single set will create a scenario for each of the provided flags in the set (i.e. the same as + * passing an additional set with an empty string only). *

        * Example: *

        @@ -355,7 +360,7 @@ public class TestFramework {
              *     Scenario(5, "-Xbatch -XX:-TieredCompilation", "-XX:+UseNewCode2")
              * 
        * - * @param sets sets of flags to generate the cross product for. + * @param flagSets sets of flags to generate the cross product for. * @return the same framework instance. */ @SafeVarargs @@ -376,7 +381,7 @@ public class TestFramework { Stream> crossProduct = Arrays.stream(flagSets) .reduce( - Stream.of(Collections.emptyList()), // Initialize Stream> acc with a Stream containing an empty list of Strings. + Stream.of(Collections.emptyList()), // Initialize Stream> acc with a Stream containing an empty list of Strings. (Stream> acc, Set set) -> acc.flatMap(lAcc -> // For each List> lAcc in acc... set.stream().map(flag -> { // ...and each flag in the current set... @@ -384,19 +389,19 @@ public class TestFramework { newList.add(flag); // ...and append the flag. return newList; }) // This results in one List> for each lAcc... - ), // ...that get flattend into one big List>. - (a, b) -> Stream.concat(a, b)); // combiner; if any reduction steps are executed in parallel, just concat two streams. + ), // ...that get flattened into one big List>. + Stream::concat); // combiner; if any reduction steps are executed in parallel, just concat two streams. Scenario[] newScenarios = crossProduct .map(flags -> new Scenario( // For each List flags in crossProduct create a new Scenario. idx.getAndIncrement(), flags.stream() // Process flags - .map(s -> Set.of(s.split("[ ]"))) // Split muliple flags in the same string into separate strings. + .map(s -> Set.of(s.split("[ ]"))) // Split multiple flags in the same string into separate strings. .flatMap(Collection::stream) // Flatten the Stream> into Stream>. .filter(s -> !s.isEmpty()) // Remove empty string flags. - .collect(Collectors.toList()) + .toList() .toArray(new String[0]))) - .collect(Collectors.toList()).toArray(new Scenario[0]); + .toList().toArray(new Scenario[0]); return addScenarios(newScenarios); } diff --git a/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestScenariosCrossProduct.java b/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestScenariosCrossProduct.java index 496fcbddb0f..46813bbff78 100644 --- a/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestScenariosCrossProduct.java +++ b/test/hotspot/jtreg/testlibrary_tests/ir_framework/tests/TestScenariosCrossProduct.java @@ -23,15 +23,22 @@ package ir_framework.tests; -import java.util.Set; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import compiler.lib.ir_framework.*; -import compiler.lib.ir_framework.shared.TestRunException; import compiler.lib.ir_framework.shared.TestFormatException; +import compiler.lib.ir_framework.shared.TestRunException; import jdk.test.lib.Asserts; /* * @test + * @bug 8365262 8369232 * @requires vm.debug == true & vm.compMode != "Xint" & vm.compiler2.enabled & vm.flagless * @summary Test cross product scenarios with the framework. * @library /test/lib /testlibrary_tests / @@ -39,29 +46,20 @@ import jdk.test.lib.Asserts; */ public class TestScenariosCrossProduct { - static void hasNFailures(String s, int count) { - if (!s.matches("The following scenarios have failed: (#[0-9](, )?){" + count + "}. Please check stderr for more information.")) { - throw new RuntimeException("Expected " + count + " failures in \"" + s + "\""); - } - } public static void main(String[] args) { - // Test argument handling - try { - TestFramework t = new TestFramework(); - t.addCrossProductScenarios((Set[]) null); - Asserts.fail("Should have thrown exception"); - } catch (TestFormatException e) {} - try { - TestFramework t = new TestFramework(); - t.addCrossProductScenarios(Set.of("foo", "bar"), null); - Asserts.fail("Should have thrown exception"); - } catch (TestFormatException e) {} + expectFormatFailure((Set[]) null); + expectFormatFailure(Set.of("foo", "bar"), null); + try { TestFramework t = new TestFramework(); t.addCrossProductScenarios(Set.of("blub"), Set.of("foo", null)); - Asserts.fail("Should have thrown exception"); - } catch (NullPointerException e) {} // Set.of prevents null elements + shouldHaveThrown(); + } catch (NullPointerException _) { + // Expected: Set.of prevents null elements + } + + try { TestFramework t = new TestFramework(); t.addCrossProductScenarios(); @@ -70,95 +68,291 @@ public class TestScenariosCrossProduct { } // Single set should test all flags in the set by themselves. - try { - TestFramework t1 = new TestFramework(); - t1.addCrossProductScenarios(Set.of("-XX:TLABRefillWasteFraction=51", - "-XX:TLABRefillWasteFraction=53", - "-XX:TLABRefillWasteFraction=64")); - t1.start(); - Asserts.fail("Should have thrown exception"); - } catch (TestRunException e) { - hasNFailures(e.getMessage(), 3); - } + new TestCase() + .inputFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=51", + "-XX:TLABRefillWasteFraction=53", + "-XX:TLABRefillWasteFraction=64") + ) + ) + .expectedScenariosWithFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=51"), + Set.of("-XX:TLABRefillWasteFraction=53"), + Set.of("-XX:TLABRefillWasteFraction=64") + )) + .run(); // The cross product of a set with one element and a set with three elements is three sets. - try { - TestFramework t2 = new TestFramework(); - t2.addCrossProductScenarios(Set.of("-XX:TLABRefillWasteFraction=53"), - Set.of("-XX:+UseNewCode", "-XX:+UseNewCode2", "-XX:+UseNewCode3")); - t2.start(); - Asserts.fail("Should have thrown exception"); - } catch (TestRunException e) { - hasNFailures(e.getMessage(), 3); - } + new TestCase() + .inputFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=53"), + Set.of("-XX:+UseNewCode", "-XX:+UseNewCode2", "-XX:+UseNewCode3") + ) + ) + .expectedScenariosWithFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode3") + )) + .run(); + // The cross product of two sets with two elements is four sets. - try { - TestFramework t3 = new TestFramework(); - t3.addCrossProductScenarios(Set.of("-XX:TLABRefillWasteFraction=53", "-XX:TLABRefillWasteFraction=64"), - Set.of("-XX:+UseNewCode", "-XX:-UseNewCode")); - t3.start(); - Asserts.fail("Should have thrown exception"); - } catch (TestRunException e) { - hasNFailures(e.getMessage(), 4); - } + new TestCase() + .inputFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:TLABRefillWasteFraction=64"), + Set.of("-XX:+UseNewCode", "-XX:-UseNewCode") + ) + ) + .expectedScenariosWithFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:-UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=64", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=64", "-XX:-UseNewCode") + )) + .run(); + // Test with a pair of flags. - try { - TestFramework t4 = new TestFramework(); - t4.addCrossProductScenarios(Set.of("-XX:TLABRefillWasteFraction=50 -XX:+UseNewCode", "-XX:TLABRefillWasteFraction=40"), - Set.of("-XX:+UseNewCode2")); - t4.start(); - Asserts.fail("Should have thrown exception"); - } catch (TestRunException e) { - hasNFailures(e.getMessage(), 1); - } + new TestCase() + .inputFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=50 -XX:+UseNewCode", "-XX:TLABRefillWasteFraction=40"), + Set.of("-XX:+UseNewCode2") + ) + ) + .expectedScenariosWithFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=50", "-XX:+UseNewCode", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=40", "-XX:+UseNewCode2") + )) + .run(); - // Test with an empty string. All 6 scenarios fail because 64 is the default value for TLABRefillWasteFraction. - try { - TestFramework t5 = new TestFramework(); - t5.addCrossProductScenarios(Set.of("", "-XX:TLABRefillWasteFraction=51", "-XX:TLABRefillWasteFraction=53"), - Set.of("-XX:+UseNewCode", "-XX:+UseNewCode2")); - t5.start(); - Asserts.fail("Should have thrown exception"); - } catch (TestRunException e) { - hasNFailures(e.getMessage(), 6); - } + // Test with an empty string, resulting in 6 scenarios. + new TestCase() + .inputFlags(Set.of( + Set.of("", "-XX:TLABRefillWasteFraction=51", "-XX:TLABRefillWasteFraction=53"), + Set.of("-XX:+UseNewCode", "-XX:+UseNewCode2") + ) + ) + .expectedScenariosWithFlags(Set.of( + Set.of("-XX:+UseNewCode"), + Set.of("-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode2") + )) + .run(); + // Test with 3 input sets which equals to 2x2x2 = 8 scenarios. + new TestCase() + .inputFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=51", + "-XX:TLABRefillWasteFraction=53"), + Set.of("-XX:+UseNewCode", + "-XX:-UseNewCode"), + Set.of("-XX:+UseNewCode2", + "-XX:-UseNewCode2") + ) + ) + .expectedScenariosWithFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:+UseNewCode", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:-UseNewCode", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:-UseNewCode", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:+UseNewCode", "-XX:-UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode", "-XX:-UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:-UseNewCode", "-XX:-UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:-UseNewCode", "-XX:-UseNewCode2") + )) + .run(); + + TestFramework testFramework = new TestFramework(); + testFramework.addScenarios(new Scenario(0, "-XX:TLABRefillWasteFraction=50", "-XX:+UseNewCode")); + testFramework.addCrossProductScenarios(Set.of("-XX:TLABRefillWasteFraction=51", "-XX:TLABRefillWasteFraction=53"), + Set.of("-XX:+UseNewCode", "-XX:+UseNewCode2")); try { - TestFramework t6 = new TestFramework(); - t6.addScenarios(new Scenario(0, "-XX:TLABRefillWasteFraction=50", "-XX:+UseNewCode")); // failPair - t6.addCrossProductScenarios(Set.of("-XX:TLABRefillWasteFraction=51", "-XX:TLABRefillWasteFraction=53"), - Set.of("-XX:+UseNewCode", "-XX:+UseNewCode2")); - try { - t6.addScenarios(new Scenario(4, "-XX:+UseNewCode3")); // fails because index 4 is already used - Asserts.fail("Should have thrown exception"); - } catch (TestFormatException e) {} - t6.addScenarios(new Scenario(5, "-XX:+UseNewCode3")); // fail default - t6.start(); - Asserts.fail("Should have thrown exception"); - } catch (TestRunException e) { - hasNFailures(e.getMessage(), 6); + testFramework.addScenarios(new Scenario(4, "-XX:+UseNewCode3")); // fails because index 4 is already used + shouldHaveThrown(); + } catch (TestFormatException _) { + // Expected. + } + testFramework.addScenarios(new Scenario(5, "-XX:+UseNewCode3")); + + new TestCase() + .expectedScenariosWithFlags(Set.of( + Set.of("-XX:TLABRefillWasteFraction=50", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=51", "-XX:+UseNewCode2"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode"), + Set.of("-XX:TLABRefillWasteFraction=53", "-XX:+UseNewCode2"), + Set.of("-XX:+UseNewCode3") + )) + .runWithPreAddedScenarios(testFramework); + + runEndToEndTest(); + } + + private static void expectFormatFailure(Set... flagSets) { + TestFramework testFramework = new TestFramework(); + try { + testFramework.addCrossProductScenarios(flagSets); + shouldHaveThrown(); + } catch (TestFormatException _) { + // Expected. } } - @Test - @IR(applyIf = {"TLABRefillWasteFraction", "64"}, counts = {IRNode.CALL, "1"}) - public void failDefault() { + private static void shouldHaveThrown() { + Asserts.fail("Should have thrown exception"); + } + + static class TestCase { + private Set> inputFlags; + private Set> expectedScenariosWithFlags; + + public TestCase inputFlags(Set> inputFlags) { + this.inputFlags = inputFlags; + return this; + } + + public TestCase expectedScenariosWithFlags(Set> expectedScenariosWithFlags) { + this.expectedScenariosWithFlags = expectedScenariosWithFlags; + return this; + } + + public void run() { + TestFramework testFramework = new TestFramework(); + testFramework.addCrossProductScenarios(inputFlags.toArray(new Set[0])); + runWithPreAddedScenarios(testFramework); + } + + public void runWithPreAddedScenarios(TestFramework testFramework) { + List scenariosFromCrossProduct = getScenarios(testFramework); + assertScenarioCount(expectedScenariosWithFlags.size(), scenariosFromCrossProduct); + assertScenariosWithFlags(scenariosFromCrossProduct, expectedScenariosWithFlags); + assertSameResultWhenManuallyAdding(scenariosFromCrossProduct, expectedScenariosWithFlags); + } + + private static void assertScenarioCount(int expectedCount, List scenarios) { + Asserts.assertEQ(expectedCount, scenarios.size(), "Scenario count is off"); + } + + /** + * Check that the added scenarios to the IR framework with TestFramework.addCrossProductScenarios() + * (i.e. 'scenariosFromCrossProduct') match the expected flag combos (i.e. 'expectedScenariosWithFlags'). + */ + private static void assertScenariosWithFlags(List scenariosFromCrossProduct, + Set> expectedScenariosWithFlags) { + for (Set expectedScenarioFlags : expectedScenariosWithFlags) { + if (scenariosFromCrossProduct.stream() + .map(Scenario::getFlags) + .map(Set::copyOf) + .anyMatch(flags -> flags.equals(expectedScenarioFlags))) { + continue; + } + System.err.println("Scenarios from cross product:"); + for (Scenario s : scenariosFromCrossProduct) { + System.err.println(Arrays.toString(s.getFlags().toArray())); + } + throw new RuntimeException("Could not find a scenario with the provided flags: " + Arrays.toString(expectedScenarioFlags.toArray())); + } + } + + /** + * Add scenarios for the provided flag sets in 'expectedScenariosWithFlags' by using TestFramework.addScenarios(). + * We should end up with the same scenarios as if we added them with TestFramework.addCrossProductScenarios(). + * This is verified by this method by comparing the flags of the scenarios, ignoring scenario indices. + */ + private static void assertSameResultWhenManuallyAdding(List scenariosFromCrossProduct, + Set> expectedScenariosWithFlags) { + List expectedScenarios = getScenariosWithFlags(expectedScenariosWithFlags); + List fetchedScenarios = addScenariosAndFetchFromFramework(expectedScenarios); + assertSameScenarios(scenariosFromCrossProduct, fetchedScenarios); + } + + private static List getScenariosWithFlags(Set> expectedScenariosWithFlags) { + List expecedScenarioList = new ArrayList<>(); + int index = -1; // Use some different indices - should not matter what we choose. + for (Set expectedScenarioFlags : expectedScenariosWithFlags) { + expecedScenarioList.add(new Scenario(index--, expectedScenarioFlags.toArray(new String[0]))); + } + return expecedScenarioList; + } + + private static List addScenariosAndFetchFromFramework(List expecedScenarioList) { + TestFramework testFramework = new TestFramework(); + testFramework.addScenarios(expecedScenarioList.toArray(new Scenario[0])); + return getScenarios(testFramework); + } + + private static void assertSameScenarios(List scenariosFromCrossProduct, + List expectedScenarios) { + assertScenariosWithFlags(scenariosFromCrossProduct, fetchFlags(expectedScenarios)); + } + + private static Set> fetchFlags(List scenarios) { + return scenarios.stream() + .map(scenario -> new HashSet<>(scenario.getFlags())) + .collect(Collectors.toSet()); + } + } + + private static List getScenarios(TestFramework testFramework) { + Field field; + try { + field = TestFramework.class.getDeclaredField("scenarios"); + field.setAccessible(true); + return (List)field.get(testFramework); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Also run a simple end-to-end test to sanity check the API method. We capture the stderr to fetch the + * scenario flags. + */ + private static void runEndToEndTest() { + TestFramework testFramework = new TestFramework(); + + // Capture stderr + PrintStream originalErr = System.err; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outputStream); + System.setErr(printStream); + + try { + testFramework + .addCrossProductScenarios(Set.of("-XX:+UseNewCode", "-XX:-UseNewCode"), + Set.of("-XX:+UseNewCode2", "-XX:-UseNewCode2")) + .addFlags() + .start(); + shouldHaveThrown(); + } catch (TestRunException e) { + // Expected. + System.setErr(originalErr); + Asserts.assertTrue(e.getMessage().contains("The following scenarios have failed: #0, #1, #2, #3.")); + String stdErr = outputStream.toString(); + Asserts.assertTrue(stdErr.contains("Scenario flags: [-XX:+UseNewCode, -XX:+UseNewCode2]")); + Asserts.assertTrue(stdErr.contains("Scenario flags: [-XX:-UseNewCode, -XX:-UseNewCode2]")); + Asserts.assertTrue(stdErr.contains("Scenario flags: [-XX:+UseNewCode, -XX:-UseNewCode2]")); + Asserts.assertTrue(stdErr.contains("Scenario flags: [-XX:-UseNewCode, -XX:+UseNewCode2]")); + Asserts.assertEQ(4, scenarioCount(stdErr)); + } + } + + public static int scenarioCount(String stdErr) { + Pattern pattern = Pattern.compile("Scenario flags"); + Matcher matcher = pattern.matcher(stdErr); + int count = 0; + while (matcher.find()) { + count++; + } + return count; } @Test - @IR(applyIf = {"TLABRefillWasteFraction", "51"}, counts = {IRNode.CALL, "1"}) - public void fail1() { - } - - @Test - @IR(applyIf = {"TLABRefillWasteFraction", "53"}, counts = {IRNode.CALL, "1"}) - public void fail2() { - } - - @Test - @IR(applyIfAnd = {"TLABRefillWasteFraction", "50", "UseNewCode", "true"}, counts = {IRNode.CALL, "1"}) - public void failPair() { + public void endToEndTest() { + throw new RuntimeException("executed test"); } } From 7e03240974cd66c471f5d02e14fd77971fe6d173 Mon Sep 17 00:00:00 2001 From: Joe Darcy Date: Thu, 16 Oct 2025 16:38:18 +0000 Subject: [PATCH 535/556] 8369858: Remove darcy author tags from jdk tests Reviewed-by: rriggs, iris, lancea --- test/jdk/java/io/Serializable/cloneArray/CloneArray.java | 3 +-- test/jdk/java/lang/Byte/Decode.java | 4 +--- test/jdk/java/lang/Class/IsAnnotationType.java | 3 +-- test/jdk/java/lang/Class/IsEnum.java | 3 +-- test/jdk/java/lang/Class/IsSynthetic.java | 3 +-- .../getEnclosingConstructor/EnclosingConstructorTests.java | 3 +-- .../lang/Class/getEnclosingMethod/EnclosingMethodTests.java | 3 +-- test/jdk/java/lang/Double/BitwiseConversion.java | 3 +-- test/jdk/java/lang/Double/Constants.java | 3 +-- test/jdk/java/lang/Double/Extrema.java | 3 +-- test/jdk/java/lang/Double/NaNInfinityParsing.java | 3 +-- test/jdk/java/lang/Double/ParseHexFloatingPoint.java | 3 +-- test/jdk/java/lang/Double/ToHexString.java | 3 +-- test/jdk/java/lang/Float/BitwiseConversion.java | 3 +-- test/jdk/java/lang/Float/Constants.java | 3 +-- test/jdk/java/lang/Float/Extrema.java | 3 +-- test/jdk/java/lang/Float/NaNInfinityParsing.java | 3 +-- test/jdk/java/lang/Integer/Decode.java | 4 +--- test/jdk/java/lang/Integer/ParsingTest.java | 3 +-- test/jdk/java/lang/Integer/Unsigned.java | 3 +-- test/jdk/java/lang/Long/Decode.java | 4 +--- test/jdk/java/lang/Long/ParsingTest.java | 3 +-- test/jdk/java/lang/Long/Unsigned.java | 3 +-- test/jdk/java/lang/Short/Decode.java | 4 +--- test/jdk/java/lang/Throwable/SuppressedExceptions.java | 3 +-- test/jdk/java/lang/annotation/Missing/MissingTest.java | 3 +-- .../lang/annotation/TestIncompleteAnnotationExceptionNPE.java | 3 +-- .../AnnotatedElement/TestAnnotatedElementDefaults.java | 3 +-- test/jdk/java/lang/reflect/Constructor/GenericStringTest.java | 3 +-- .../lang/reflect/Constructor/TestParameterAnnotations.java | 3 +-- test/jdk/java/lang/reflect/DefaultAccessibility.java | 3 +-- test/jdk/java/lang/reflect/Field/GenericStringTest.java | 3 +-- test/jdk/java/lang/reflect/Generics/HashCodeTest.java | 3 +-- test/jdk/java/lang/reflect/Generics/Probe.java | 3 +-- test/jdk/java/lang/reflect/Generics/StringsAndBounds.java | 3 +-- .../jdk/java/lang/reflect/Generics/TestParameterizedType.java | 3 +-- test/jdk/java/lang/reflect/Generics/exceptionCauseTest.java | 3 +-- test/jdk/java/lang/reflect/Generics/getAnnotationTest.java | 3 +-- test/jdk/java/lang/reflect/Method/GenericStringTest.java | 3 +-- test/jdk/java/lang/reflect/Method/IsDefaultTest.java | 3 +-- .../Method/defaultMethodModeling/DefaultMethodModeling.java | 3 +-- .../java/lang/reflect/TypeVariable/TestAnnotatedElement.java | 3 +-- test/jdk/java/math/BigDecimal/AddTests.java | 3 +-- test/jdk/java/math/BigDecimal/CompareToTests.java | 3 +-- test/jdk/java/math/BigDecimal/DivideTests.java | 3 +-- test/jdk/java/math/BigDecimal/IntegralDivisionTests.java | 3 +-- test/jdk/java/math/BigDecimal/NegateTests.java | 3 +-- test/jdk/java/math/BigDecimal/PowTests.java | 3 +-- test/jdk/java/math/BigDecimal/PrecisionTests.java | 3 +-- test/jdk/java/math/BigDecimal/RoundingTests.java | 3 +-- test/jdk/java/math/BigDecimal/ScaleByPowerOfTenTests.java | 3 +-- test/jdk/java/math/BigDecimal/StrippingZerosTest.java | 3 +-- test/jdk/java/math/BigDecimal/ToPlainStringTests.java | 3 +-- test/jdk/java/math/BigDecimal/ZeroScalingTests.java | 3 +-- test/jdk/java/math/BigInteger/CompareToTests.java | 3 +-- test/jdk/java/math/BigInteger/ExtremeShiftingTests.java | 3 +-- test/jdk/java/math/BigInteger/OperatorNpeTests.java | 3 +-- test/jdk/java/math/BigInteger/StringConstructor.java | 3 +-- test/jdk/java/math/BigInteger/TestValueExact.java | 3 +-- test/jdk/java/math/RoundingMode/RoundingModeTests.java | 3 +-- test/jdk/tools/launcher/ChangeDataModel.java | 3 +-- test/jdk/tools/launcher/I18NTest.java | 3 +-- test/jdk/tools/launcher/UnresolvedExceptions.java | 3 +-- 63 files changed, 63 insertions(+), 130 deletions(-) diff --git a/test/jdk/java/io/Serializable/cloneArray/CloneArray.java b/test/jdk/java/io/Serializable/cloneArray/CloneArray.java index a8de4407912..0a427e29da5 100644 --- a/test/jdk/java/io/Serializable/cloneArray/CloneArray.java +++ b/test/jdk/java/io/Serializable/cloneArray/CloneArray.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ /* @test * @bug 6990094 * @summary Verify ObjectInputStream.cloneArray works on many kinds of arrays - * @author Stuart Marks, Joseph D. Darcy */ import java.io.ByteArrayInputStream; diff --git a/test/jdk/java/lang/Byte/Decode.java b/test/jdk/java/lang/Byte/Decode.java index b4ef798cb7e..590c35989f7 100644 --- a/test/jdk/java/lang/Byte/Decode.java +++ b/test/jdk/java/lang/Byte/Decode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2007, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,6 @@ * @test * @bug 4242173 5017980 6576055 * @summary Test Byte.decode method - * @author madbot - * @author Joseph D. Darcy */ /** diff --git a/test/jdk/java/lang/Class/IsAnnotationType.java b/test/jdk/java/lang/Class/IsAnnotationType.java index 2189e1c5715..e007201d95c 100644 --- a/test/jdk/java/lang/Class/IsAnnotationType.java +++ b/test/jdk/java/lang/Class/IsAnnotationType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4891872 4988155 * @summary Check isAnnotation() method - * @author Joseph D. Darcy */ import java.lang.annotation.*; diff --git a/test/jdk/java/lang/Class/IsEnum.java b/test/jdk/java/lang/Class/IsEnum.java index fc0b0f7632b..3d2d9103ddb 100644 --- a/test/jdk/java/lang/Class/IsEnum.java +++ b/test/jdk/java/lang/Class/IsEnum.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4891872 4989735 4990789 5020490 * @summary Check isEnum() method - * @author Joseph D. Darcy */ import java.lang.annotation.*; diff --git a/test/jdk/java/lang/Class/IsSynthetic.java b/test/jdk/java/lang/Class/IsSynthetic.java index d594f6303e1..1775b6bc11d 100644 --- a/test/jdk/java/lang/Class/IsSynthetic.java +++ b/test/jdk/java/lang/Class/IsSynthetic.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5012133 * @summary Check Class.isSynthetic method - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/Class/getEnclosingConstructor/EnclosingConstructorTests.java b/test/jdk/java/lang/Class/getEnclosingConstructor/EnclosingConstructorTests.java index ca128021e0c..c08f33af1e6 100644 --- a/test/jdk/java/lang/Class/getEnclosingConstructor/EnclosingConstructorTests.java +++ b/test/jdk/java/lang/Class/getEnclosingConstructor/EnclosingConstructorTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4962341 6832557 * @summary Check getEnclosingMethod method - * @author Joseph D. Darcy */ import java.lang.reflect.Constructor; diff --git a/test/jdk/java/lang/Class/getEnclosingMethod/EnclosingMethodTests.java b/test/jdk/java/lang/Class/getEnclosingMethod/EnclosingMethodTests.java index 3a2a0910fc7..e890bb75d1e 100644 --- a/test/jdk/java/lang/Class/getEnclosingMethod/EnclosingMethodTests.java +++ b/test/jdk/java/lang/Class/getEnclosingMethod/EnclosingMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4962341 * @summary Check getEnclosingMethod method - * @author Joseph D. Darcy */ import java.lang.reflect.Method; diff --git a/test/jdk/java/lang/Double/BitwiseConversion.java b/test/jdk/java/lang/Double/BitwiseConversion.java index 381a7e9aed6..06e15d95575 100644 --- a/test/jdk/java/lang/Double/BitwiseConversion.java +++ b/test/jdk/java/lang/Double/BitwiseConversion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ * @library ../Math * @build DoubleConsts * @run main BitwiseConversion - * @author Joseph D. Darcy */ import static java.lang.Double.*; diff --git a/test/jdk/java/lang/Double/Constants.java b/test/jdk/java/lang/Double/Constants.java index 676630e12b9..e7aa5cbceb9 100644 --- a/test/jdk/java/lang/Double/Constants.java +++ b/test/jdk/java/lang/Double/Constants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,7 +26,6 @@ * @compile Constants.java * @bug 4397405 4826652 * @summary Testing constant-ness of Double.{MIN_VALUE, MAX_VALUE}, etc. - * @author Joseph D. Darcy */ public class Constants { diff --git a/test/jdk/java/lang/Double/Extrema.java b/test/jdk/java/lang/Double/Extrema.java index fef17100e0a..db9a10fb279 100644 --- a/test/jdk/java/lang/Double/Extrema.java +++ b/test/jdk/java/lang/Double/Extrema.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4408489 4826652 * @summary Testing values of Double.{MIN_VALUE, MIN_NORMAL, MAX_VALUE} - * @author Joseph D. Darcy */ public class Extrema { diff --git a/test/jdk/java/lang/Double/NaNInfinityParsing.java b/test/jdk/java/lang/Double/NaNInfinityParsing.java index 846dcecd3b9..8b5d0e4888f 100644 --- a/test/jdk/java/lang/Double/NaNInfinityParsing.java +++ b/test/jdk/java/lang/Double/NaNInfinityParsing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4428772 * @summary Testing recognition of "NaN" and "Infinity" strings - * @author Joseph D. Darcy */ diff --git a/test/jdk/java/lang/Double/ParseHexFloatingPoint.java b/test/jdk/java/lang/Double/ParseHexFloatingPoint.java index a26a8b7a756..60fa13df75b 100644 --- a/test/jdk/java/lang/Double/ParseHexFloatingPoint.java +++ b/test/jdk/java/lang/Double/ParseHexFloatingPoint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ * @run main ParseHexFloatingPoint * @bug 4826774 8078672 * @summary Numerical tests for hexadecimal inputs to parse{Double, Float} (use -Dseed=X to set PRNG seed) - * @author Joseph D. Darcy * @key randomness */ diff --git a/test/jdk/java/lang/Double/ToHexString.java b/test/jdk/java/lang/Double/ToHexString.java index a9f07bba508..912835b7aeb 100644 --- a/test/jdk/java/lang/Double/ToHexString.java +++ b/test/jdk/java/lang/Double/ToHexString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ * @library ../Math * @build DoubleConsts * @run main ToHexString - * @author Joseph D. Darcy */ import java.util.regex.*; diff --git a/test/jdk/java/lang/Float/BitwiseConversion.java b/test/jdk/java/lang/Float/BitwiseConversion.java index 973e00e7008..9bb4a34a55b 100644 --- a/test/jdk/java/lang/Float/BitwiseConversion.java +++ b/test/jdk/java/lang/Float/BitwiseConversion.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ * @library ../Math * @build FloatConsts * @run main BitwiseConversion - * @author Joseph D. Darcy */ import static java.lang.Float.*; diff --git a/test/jdk/java/lang/Float/Constants.java b/test/jdk/java/lang/Float/Constants.java index b6ad85ca0c2..47463fc5916 100644 --- a/test/jdk/java/lang/Float/Constants.java +++ b/test/jdk/java/lang/Float/Constants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,7 +26,6 @@ * @compile Constants.java * @bug 4397405 4826652 * @summary Testing constant-ness of Float.{MIN_VALUE, MAX_VALUE}, etc. - * @author Joseph D. Darcy */ public class Constants { diff --git a/test/jdk/java/lang/Float/Extrema.java b/test/jdk/java/lang/Float/Extrema.java index 869f22072e7..46447bfcb2f 100644 --- a/test/jdk/java/lang/Float/Extrema.java +++ b/test/jdk/java/lang/Float/Extrema.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4408489 4826652 * @summary Testing values of Float.{MIN_VALUE, MIN_NORMAL, MAX_VALUE} - * @author Joseph D. Darcy */ public class Extrema { diff --git a/test/jdk/java/lang/Float/NaNInfinityParsing.java b/test/jdk/java/lang/Float/NaNInfinityParsing.java index 38a402a9775..6df42b376a5 100644 --- a/test/jdk/java/lang/Float/NaNInfinityParsing.java +++ b/test/jdk/java/lang/Float/NaNInfinityParsing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4428772 * @summary Testing recognition of "NaN" and "Infinity" strings - * @author Joseph D. Darcy */ diff --git a/test/jdk/java/lang/Integer/Decode.java b/test/jdk/java/lang/Integer/Decode.java index 42493221fe4..b5f881a0e9f 100644 --- a/test/jdk/java/lang/Integer/Decode.java +++ b/test/jdk/java/lang/Integer/Decode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,6 @@ * @test * @bug 4136371 5017980 6576055 * @summary Test Integer.decode method - * @author madbot - * @author Joseph D. Darcy */ /** diff --git a/test/jdk/java/lang/Integer/ParsingTest.java b/test/jdk/java/lang/Integer/ParsingTest.java index f5f64f70c84..24e1dd6f3aa 100644 --- a/test/jdk/java/lang/Integer/ParsingTest.java +++ b/test/jdk/java/lang/Integer/ParsingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2007, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5017980 6576055 8041972 8055251 * @summary Test parsing methods - * @author Joseph D. Darcy */ import java.lang.IndexOutOfBoundsException; diff --git a/test/jdk/java/lang/Integer/Unsigned.java b/test/jdk/java/lang/Integer/Unsigned.java index 6c3aecc70c1..911f3f8fe8f 100644 --- a/test/jdk/java/lang/Integer/Unsigned.java +++ b/test/jdk/java/lang/Integer/Unsigned.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4504839 4215269 6322074 * @summary Basic tests for unsigned operations. - * @author Joseph D. Darcy */ public class Unsigned { public static void main(String... args) { diff --git a/test/jdk/java/lang/Long/Decode.java b/test/jdk/java/lang/Long/Decode.java index d47f976e6fc..fc2762b1bc2 100644 --- a/test/jdk/java/lang/Long/Decode.java +++ b/test/jdk/java/lang/Long/Decode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,6 @@ * @test * @bug 4136371 5017980 6576055 * @summary Test Long.decode method - * @author madbot - * @author Joseph D. Darcy */ import java.math.BigInteger; diff --git a/test/jdk/java/lang/Long/ParsingTest.java b/test/jdk/java/lang/Long/ParsingTest.java index cbf83415d36..23d007dfd26 100644 --- a/test/jdk/java/lang/Long/ParsingTest.java +++ b/test/jdk/java/lang/Long/ParsingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2007, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5017980 6576055 8041972 8055251 * @summary Test parsing methods - * @author Joseph D. Darcy */ /** diff --git a/test/jdk/java/lang/Long/Unsigned.java b/test/jdk/java/lang/Long/Unsigned.java index f6eeed534b8..aba66c72a92 100644 --- a/test/jdk/java/lang/Long/Unsigned.java +++ b/test/jdk/java/lang/Long/Unsigned.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4504839 4215269 6322074 8030814 * @summary Basic tests for unsigned operations - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/lang/Short/Decode.java b/test/jdk/java/lang/Short/Decode.java index 8f76751b589..d63e911b710 100644 --- a/test/jdk/java/lang/Short/Decode.java +++ b/test/jdk/java/lang/Short/Decode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2007, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,8 +25,6 @@ * @test * @bug 4136371 5017980 6576055 * @summary Test Short.decode method - * @author madbot - * @author Joseph D. Darcy */ /** diff --git a/test/jdk/java/lang/Throwable/SuppressedExceptions.java b/test/jdk/java/lang/Throwable/SuppressedExceptions.java index f6fe09df4ef..157a4ffa99b 100644 --- a/test/jdk/java/lang/Throwable/SuppressedExceptions.java +++ b/test/jdk/java/lang/Throwable/SuppressedExceptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,6 @@ import java.util.*; * @test * @bug 6911258 6962571 6963622 6991528 7005628 8012044 * @summary Basic tests of suppressed exceptions - * @author Joseph D. Darcy */ public class SuppressedExceptions { diff --git a/test/jdk/java/lang/annotation/Missing/MissingTest.java b/test/jdk/java/lang/annotation/Missing/MissingTest.java index d8a1c44e7ee..10e3b436d12 100644 --- a/test/jdk/java/lang/annotation/Missing/MissingTest.java +++ b/test/jdk/java/lang/annotation/Missing/MissingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6322301 5041778 * @summary Verify when missing annotation classes cause exceptions - * @author Joseph D. Darcy * @compile MissingTest.java A.java B.java C.java D.java Marker.java Missing.java MissingWrapper.java MissingDefault.java * @clean Missing * @run main MissingTest diff --git a/test/jdk/java/lang/annotation/TestIncompleteAnnotationExceptionNPE.java b/test/jdk/java/lang/annotation/TestIncompleteAnnotationExceptionNPE.java index 9e9e7a7a60a..390f0fc96bd 100644 --- a/test/jdk/java/lang/annotation/TestIncompleteAnnotationExceptionNPE.java +++ b/test/jdk/java/lang/annotation/TestIncompleteAnnotationExceptionNPE.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 7021922 * @summary Test null handling of IncompleteAnnotationException constructor - * @author Joseph D. Darcy */ import java.lang.annotation.*; diff --git a/test/jdk/java/lang/reflect/AnnotatedElement/TestAnnotatedElementDefaults.java b/test/jdk/java/lang/reflect/AnnotatedElement/TestAnnotatedElementDefaults.java index 97e9a79e14b..34c4f840241 100644 --- a/test/jdk/java/lang/reflect/AnnotatedElement/TestAnnotatedElementDefaults.java +++ b/test/jdk/java/lang/reflect/AnnotatedElement/TestAnnotatedElementDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 8005294 * @summary Check behavior of default methods of AnnotatedElement - * @author Joseph D. Darcy */ import java.lang.annotation.*; diff --git a/test/jdk/java/lang/reflect/Constructor/GenericStringTest.java b/test/jdk/java/lang/reflect/Constructor/GenericStringTest.java index 56a781fce18..a8c6c671d86 100644 --- a/test/jdk/java/lang/reflect/Constructor/GenericStringTest.java +++ b/test/jdk/java/lang/reflect/Constructor/GenericStringTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5033583 6316717 6470106 8161500 8162539 6304578 * @summary Check toGenericString() and toString() methods - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Constructor/TestParameterAnnotations.java b/test/jdk/java/lang/reflect/Constructor/TestParameterAnnotations.java index 5773825a8e2..7512d36cb42 100644 --- a/test/jdk/java/lang/reflect/Constructor/TestParameterAnnotations.java +++ b/test/jdk/java/lang/reflect/Constructor/TestParameterAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6332964 * @summary Verify getParameterAnnotations doesn't throw spurious errors - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/DefaultAccessibility.java b/test/jdk/java/lang/reflect/DefaultAccessibility.java index 6dce3e249c2..69f8946e617 100644 --- a/test/jdk/java/lang/reflect/DefaultAccessibility.java +++ b/test/jdk/java/lang/reflect/DefaultAccessibility.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6648344 * @summary Test that default accessibility is false - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Field/GenericStringTest.java b/test/jdk/java/lang/reflect/Field/GenericStringTest.java index 65fbbc43381..5b5c321a3e1 100644 --- a/test/jdk/java/lang/reflect/Field/GenericStringTest.java +++ b/test/jdk/java/lang/reflect/Field/GenericStringTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5033583 8161500 * @summary Check toGenericString() method - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Generics/HashCodeTest.java b/test/jdk/java/lang/reflect/Generics/HashCodeTest.java index 53fbad758d0..00e1494d04a 100644 --- a/test/jdk/java/lang/reflect/Generics/HashCodeTest.java +++ b/test/jdk/java/lang/reflect/Generics/HashCodeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5097856 * @summary Computing hashCode of objects modeling generics shouldn't blow stack - * @author Joseph D. Darcy */ import java.util.*; diff --git a/test/jdk/java/lang/reflect/Generics/Probe.java b/test/jdk/java/lang/reflect/Generics/Probe.java index cbe94aef7a4..e14ed54f863 100644 --- a/test/jdk/java/lang/reflect/Generics/Probe.java +++ b/test/jdk/java/lang/reflect/Generics/Probe.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5003916 6704655 6873951 6476261 8004928 * @summary Testing parsing of signatures attributes of nested classes - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Generics/StringsAndBounds.java b/test/jdk/java/lang/reflect/Generics/StringsAndBounds.java index 6d7756b2163..5c44207bcc5 100644 --- a/test/jdk/java/lang/reflect/Generics/StringsAndBounds.java +++ b/test/jdk/java/lang/reflect/Generics/StringsAndBounds.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5015676 4987888 4997464 * @summary Testing upper bounds and availability of toString methods - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Generics/TestParameterizedType.java b/test/jdk/java/lang/reflect/Generics/TestParameterizedType.java index 6bb3aa47aef..99fc3db0c97 100644 --- a/test/jdk/java/lang/reflect/Generics/TestParameterizedType.java +++ b/test/jdk/java/lang/reflect/Generics/TestParameterizedType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5061485 * @summary Test sematics of ParameterizedType.equals - * @author Joseph D. Darcy */ import java.util.*; diff --git a/test/jdk/java/lang/reflect/Generics/exceptionCauseTest.java b/test/jdk/java/lang/reflect/Generics/exceptionCauseTest.java index dc8397aded6..485e8d3c51e 100644 --- a/test/jdk/java/lang/reflect/Generics/exceptionCauseTest.java +++ b/test/jdk/java/lang/reflect/Generics/exceptionCauseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4981727 * @summary - * @author Joseph D. Darcy */ import java.io.PrintStream; diff --git a/test/jdk/java/lang/reflect/Generics/getAnnotationTest.java b/test/jdk/java/lang/reflect/Generics/getAnnotationTest.java index 0e1317e8d2d..766543863d0 100644 --- a/test/jdk/java/lang/reflect/Generics/getAnnotationTest.java +++ b/test/jdk/java/lang/reflect/Generics/getAnnotationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4979440 * @summary Test for signature parsing corner case - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Method/GenericStringTest.java b/test/jdk/java/lang/reflect/Method/GenericStringTest.java index c1c8e141f79..e7fe873d3cc 100644 --- a/test/jdk/java/lang/reflect/Method/GenericStringTest.java +++ b/test/jdk/java/lang/reflect/Method/GenericStringTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 5033583 6316717 6470106 8004979 8161500 8162539 6304578 * @summary Check toGenericString() and toString() methods - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Method/IsDefaultTest.java b/test/jdk/java/lang/reflect/Method/IsDefaultTest.java index c48ebaf9e96..0c983a8b0ba 100644 --- a/test/jdk/java/lang/reflect/Method/IsDefaultTest.java +++ b/test/jdk/java/lang/reflect/Method/IsDefaultTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 8005042 * @summary Check behavior of Method.isDefault - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Method/defaultMethodModeling/DefaultMethodModeling.java b/test/jdk/java/lang/reflect/Method/defaultMethodModeling/DefaultMethodModeling.java index 30eca409cea..b91f73ee845 100644 --- a/test/jdk/java/lang/reflect/Method/defaultMethodModeling/DefaultMethodModeling.java +++ b/test/jdk/java/lang/reflect/Method/defaultMethodModeling/DefaultMethodModeling.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 8011590 * @summary Check modeling of default methods - * @author Joseph D. Darcy */ import java.util.Objects; diff --git a/test/jdk/java/lang/reflect/TypeVariable/TestAnnotatedElement.java b/test/jdk/java/lang/reflect/TypeVariable/TestAnnotatedElement.java index 94f8d459cf0..9788eab3060 100644 --- a/test/jdk/java/lang/reflect/TypeVariable/TestAnnotatedElement.java +++ b/test/jdk/java/lang/reflect/TypeVariable/TestAnnotatedElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 7086192 * @summary Verify functionality of AnnotatedElement methods on type variables - * @author Joseph D. Darcy */ import java.lang.reflect.*; diff --git a/test/jdk/java/math/BigDecimal/AddTests.java b/test/jdk/java/math/BigDecimal/AddTests.java index 8045e280435..d6f57366d2a 100644 --- a/test/jdk/java/math/BigDecimal/AddTests.java +++ b/test/jdk/java/math/BigDecimal/AddTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6362557 8200698 * @summary Some tests of add(BigDecimal, mc) - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/CompareToTests.java b/test/jdk/java/math/BigDecimal/CompareToTests.java index baefcc5b499..b6ae31bca80 100644 --- a/test/jdk/java/math/BigDecimal/CompareToTests.java +++ b/test/jdk/java/math/BigDecimal/CompareToTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6473768 * @summary Tests of BigDecimal.compareTo - * @author Joseph D. Darcy */ import java.math.*; import static java.math.BigDecimal.*; diff --git a/test/jdk/java/math/BigDecimal/DivideTests.java b/test/jdk/java/math/BigDecimal/DivideTests.java index fe0fea73ff6..140271dc000 100644 --- a/test/jdk/java/math/BigDecimal/DivideTests.java +++ b/test/jdk/java/math/BigDecimal/DivideTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4851776 4907265 6177836 6876282 8066842 * @summary Some tests for the divide methods. - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/IntegralDivisionTests.java b/test/jdk/java/math/BigDecimal/IntegralDivisionTests.java index 9fab5bb28ae..7df63b6329f 100644 --- a/test/jdk/java/math/BigDecimal/IntegralDivisionTests.java +++ b/test/jdk/java/math/BigDecimal/IntegralDivisionTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,7 +24,6 @@ * @test * @bug 4904082 4917089 6337226 6378503 * @summary Tests that integral division and related methods return the proper result and scale. - * @author Joseph D. Darcy */ import java.math.*; public class IntegralDivisionTests { diff --git a/test/jdk/java/math/BigDecimal/NegateTests.java b/test/jdk/java/math/BigDecimal/NegateTests.java index 5b570325e90..fc4ce82e286 100644 --- a/test/jdk/java/math/BigDecimal/NegateTests.java +++ b/test/jdk/java/math/BigDecimal/NegateTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6325535 * @summary Test for the rounding behavior of negate(MathContext) - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/PowTests.java b/test/jdk/java/math/BigDecimal/PowTests.java index 49fc74c8791..a552e981c2e 100644 --- a/test/jdk/java/math/BigDecimal/PowTests.java +++ b/test/jdk/java/math/BigDecimal/PowTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4916097 * @summary Some exponent over/undeflow tests for the pow method - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/PrecisionTests.java b/test/jdk/java/math/BigDecimal/PrecisionTests.java index 43df43e16bc..88d9843c724 100644 --- a/test/jdk/java/math/BigDecimal/PrecisionTests.java +++ b/test/jdk/java/math/BigDecimal/PrecisionTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 1234567 * @summary Test that precision() is computed properly. - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/RoundingTests.java b/test/jdk/java/math/BigDecimal/RoundingTests.java index 95d579e791a..6a12124fd62 100644 --- a/test/jdk/java/math/BigDecimal/RoundingTests.java +++ b/test/jdk/java/math/BigDecimal/RoundingTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6334849 * @summary Tests of dropping digits near the scale threshold - * @author Joseph D. Darcy */ import java.math.*; public class RoundingTests { diff --git a/test/jdk/java/math/BigDecimal/ScaleByPowerOfTenTests.java b/test/jdk/java/math/BigDecimal/ScaleByPowerOfTenTests.java index 638fce9ed54..93131b77be2 100644 --- a/test/jdk/java/math/BigDecimal/ScaleByPowerOfTenTests.java +++ b/test/jdk/java/math/BigDecimal/ScaleByPowerOfTenTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4899722 * @summary Basic tests of scaleByPowerOfTen - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/StrippingZerosTest.java b/test/jdk/java/math/BigDecimal/StrippingZerosTest.java index 083b4eabbf1..c79c26c0ce5 100644 --- a/test/jdk/java/math/BigDecimal/StrippingZerosTest.java +++ b/test/jdk/java/math/BigDecimal/StrippingZerosTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ * @summary A few tests of stripTrailingZeros * @run main StrippingZerosTest * @run main/othervm -XX:+IgnoreUnrecognizedVMOptions -XX:+EliminateAutoBox -XX:AutoBoxCacheMax=20000 StrippingZerosTest - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/ToPlainStringTests.java b/test/jdk/java/math/BigDecimal/ToPlainStringTests.java index 0a1f617c0e5..28d5cdcd89d 100644 --- a/test/jdk/java/math/BigDecimal/ToPlainStringTests.java +++ b/test/jdk/java/math/BigDecimal/ToPlainStringTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2004, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ * @summary Basic tests of toPlainString method * @run main ToPlainStringTests * @run main/othervm -XX:+IgnoreUnrecognizedVMOptions -XX:+EliminateAutoBox -XX:AutoBoxCacheMax=20000 ToPlainStringTests - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigDecimal/ZeroScalingTests.java b/test/jdk/java/math/BigDecimal/ZeroScalingTests.java index 05a59150b13..908487d68ef 100644 --- a/test/jdk/java/math/BigDecimal/ZeroScalingTests.java +++ b/test/jdk/java/math/BigDecimal/ZeroScalingTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ * @summary Tests that the scale of zero is propagated properly and has the * proper effect and that setting the scale to zero does not mutate the * BigDecimal. - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigInteger/CompareToTests.java b/test/jdk/java/math/BigInteger/CompareToTests.java index 4e549fa2a53..3d9ff40ce56 100644 --- a/test/jdk/java/math/BigInteger/CompareToTests.java +++ b/test/jdk/java/math/BigInteger/CompareToTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6473768 * @summary Tests of BigInteger.compareTo - * @author Joseph D. Darcy */ import java.math.*; import static java.math.BigInteger.*; diff --git a/test/jdk/java/math/BigInteger/ExtremeShiftingTests.java b/test/jdk/java/math/BigInteger/ExtremeShiftingTests.java index 853b88668a2..a173d1cf6dd 100644 --- a/test/jdk/java/math/BigInteger/ExtremeShiftingTests.java +++ b/test/jdk/java/math/BigInteger/ExtremeShiftingTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ * @summary Tests of shiftLeft and shiftRight on Integer.MIN_VALUE * @requires os.maxMemory >= 1g * @run main/othervm -Xmx512m ExtremeShiftingTests - * @author Joseph D. Darcy */ import java.math.BigInteger; import static java.math.BigInteger.*; diff --git a/test/jdk/java/math/BigInteger/OperatorNpeTests.java b/test/jdk/java/math/BigInteger/OperatorNpeTests.java index 2985ae00c8e..593e15445d5 100644 --- a/test/jdk/java/math/BigInteger/OperatorNpeTests.java +++ b/test/jdk/java/math/BigInteger/OperatorNpeTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6365176 * @summary Get NullPointerExceptions when expected - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigInteger/StringConstructor.java b/test/jdk/java/math/BigInteger/StringConstructor.java index c8fd9f83c6b..aa95fd4e346 100644 --- a/test/jdk/java/math/BigInteger/StringConstructor.java +++ b/test/jdk/java/math/BigInteger/StringConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2007, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4489146 5017980 * @summary tests String constructors of BigInteger - * @author Joseph D. Darcy */ import java.math.*; diff --git a/test/jdk/java/math/BigInteger/TestValueExact.java b/test/jdk/java/math/BigInteger/TestValueExact.java index 63ee1583527..fcd934653be 100644 --- a/test/jdk/java/math/BigInteger/TestValueExact.java +++ b/test/jdk/java/math/BigInteger/TestValueExact.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 6371401 * @summary Tests of fooValueExact methods - * @author Joseph D. Darcy */ import java.math.BigInteger; diff --git a/test/jdk/java/math/RoundingMode/RoundingModeTests.java b/test/jdk/java/math/RoundingMode/RoundingModeTests.java index 87ecad945b6..6edccb9277d 100644 --- a/test/jdk/java/math/RoundingMode/RoundingModeTests.java +++ b/test/jdk/java/math/RoundingMode/RoundingModeTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,6 @@ * @test * @bug 4851776 4891522 4905335 * @summary Basic tests for the RoundingMode class. - * @author Joseph D. Darcy */ import java.math.RoundingMode; diff --git a/test/jdk/tools/launcher/ChangeDataModel.java b/test/jdk/tools/launcher/ChangeDataModel.java index e6bc281ad62..14945045435 100644 --- a/test/jdk/tools/launcher/ChangeDataModel.java +++ b/test/jdk/tools/launcher/ChangeDataModel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ * @compile -XDignore.symbol.file ChangeDataModel.java * @run main ChangeDataModel * @summary Verify -d32, -d64 and -J prefixed data-model options are rejected on all platforms - * @author Joseph D. Darcy, ksrini */ import java.util.Arrays; diff --git a/test/jdk/tools/launcher/I18NTest.java b/test/jdk/tools/launcher/I18NTest.java index aa1ce24e798..83d90f56327 100644 --- a/test/jdk/tools/launcher/I18NTest.java +++ b/test/jdk/tools/launcher/I18NTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,7 +27,6 @@ * @compile -XDignore.symbol.file I18NTest.java * @run main I18NTest * @summary Test to see if class files with non-ASCII characters can be run - * @author Joseph D. Darcy, Kumar Srinivasan */ diff --git a/test/jdk/tools/launcher/UnresolvedExceptions.java b/test/jdk/tools/launcher/UnresolvedExceptions.java index ce14f405aed..6faa306c34c 100644 --- a/test/jdk/tools/launcher/UnresolvedExceptions.java +++ b/test/jdk/tools/launcher/UnresolvedExceptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2012, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,7 +29,6 @@ * @compile -XDignore.symbol.file UnresolvedExceptions.java * @run main UnresolvedExceptions * @summary Verifying jvm won't segv if exception not available - * @author Joseph D. Darcy, ksrini */ import java.io.File; From 873666d157340b3b953ad869576afd30d4304610 Mon Sep 17 00:00:00 2001 From: Chris Plummer Date: Thu, 16 Oct 2025 16:53:47 +0000 Subject: [PATCH 536/556] 8369451: Debug agent support for USE_ITERATE_THROUGH_HEAP is broken and should be removed Reviewed-by: sspitsyn, amenkov --- .../share/native/libjdwp/debugInit.c | 11 +- .../share/native/libjdwp/util.c | 102 ++++-------------- .../share/native/libjdwp/util.h | 8 +- 3 files changed, 22 insertions(+), 99 deletions(-) diff --git a/src/jdk.jdwp.agent/share/native/libjdwp/debugInit.c b/src/jdk.jdwp.agent/share/native/libjdwp/debugInit.c index 73ea9a295e6..2bed6b6bb28 100644 --- a/src/jdk.jdwp.agent/share/native/libjdwp/debugInit.c +++ b/src/jdk.jdwp.agent/share/native/libjdwp/debugInit.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -890,8 +890,6 @@ printUsage(void) " everything = 0xfff")); TTY_MESSAGE(( - "debugflags=flags debug flags (bitmask) none\n" - " USE_ITERATE_THROUGH_HEAP 0x01\n" "\n" "Environment Variables\n" "---------------------\n" @@ -1192,13 +1190,6 @@ parseOptions(char *options) } /*LINTED*/ logflags = (unsigned)strtol(current, NULL, 0); - } else if (strcmp(buf, "debugflags") == 0) { - /*LINTED*/ - if (!get_tok(&str, current, (int)(end - current), ',')) { - goto syntax_error; - } - /*LINTED*/ - gdata->debugflags = (unsigned)strtol(current, NULL, 0); } else if ( strcmp(buf, "suspend")==0 ) { if ( !get_boolean(&str, &suspendOnInit) ) { goto syntax_error; diff --git a/src/jdk.jdwp.agent/share/native/libjdwp/util.c b/src/jdk.jdwp.agent/share/native/libjdwp/util.c index 45de2ba7b7a..980c622cc48 100644 --- a/src/jdk.jdwp.agent/share/native/libjdwp/util.c +++ b/src/jdk.jdwp.agent/share/native/libjdwp/util.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -2730,10 +2730,6 @@ typedef struct ClassCountData { jvmtiError error; } ClassCountData; -/* Two different cbObjectCounter's, one for FollowReferences, one for - * IterateThroughHeap. Pick a card, any card. - */ - /* Callback for object count heap traversal (heap_reference_callback) */ static jint JNICALL cbObjectCounterFromRef(jvmtiHeapReferenceKind reference_kind, @@ -2795,38 +2791,6 @@ cbObjectCounterFromRef(jvmtiHeapReferenceKind reference_kind, return JVMTI_VISIT_OBJECTS; } -/* Callback for instance count heap traversal (heap_iteration_callback) */ -static jint JNICALL -cbObjectCounter(jlong class_tag, jlong size, jlong* tag_ptr, jint length, - void* user_data) -{ - ClassCountData *data; - int index; - - /* Check data structure */ - data = (ClassCountData*)user_data; - if (data == NULL) { - return JVMTI_VISIT_ABORT; - } - - /* Classes with no tag should be filtered out. */ - if ( class_tag == (jlong)0 ) { - data->error = AGENT_ERROR_INTERNAL; - return JVMTI_VISIT_ABORT; - } - - /* Class tag is actually an index into data arrays */ - index = CLASSTAG2INDEX(class_tag); - if (index < 0 || index >= data->classCount) { - data->error = AGENT_ERROR_ILLEGAL_ARGUMENT; - return JVMTI_VISIT_ABORT; - } - - /* Bump instance count on this class */ - data->counts[index]++; - return JVMTI_VISIT_OBJECTS; -} - /* Get instance counts for a set of classes */ jvmtiError classInstanceCounts(jint classCount, jclass *classes, jlong *counts) @@ -2879,53 +2843,27 @@ classInstanceCounts(jint classCount, jclass *classes, jlong *counts) /* Clear out callbacks structure */ (void)memset(&heap_callbacks,0,sizeof(heap_callbacks)); - /* Check debug flags to see how to do this. */ - if ( (gdata->debugflags & USE_ITERATE_THROUGH_HEAP) == 0 ) { + /* Using FollowReferences only gives us live objects, but we + * need to tag the objects to avoid counting them twice since + * the callback is per reference. + * The jclass objects have been tagged with their index in the + * supplied list, and that tag may flip to negative if it + * is also an object of interest. + * All other objects being counted that weren't in the + * supplied classes list will have a negative classCount + * tag value. So all objects counted will have negative tags. + * If the absolute tag value is an index in the supplied + * list, then it's one of the supplied classes. + */ + data.negObjTag = -INDEX2CLASSTAG(classCount); - /* Using FollowReferences only gives us live objects, but we - * need to tag the objects to avoid counting them twice since - * the callback is per reference. - * The jclass objects have been tagged with their index in the - * supplied list, and that tag may flip to negative if it - * is also an object of interest. - * All other objects being counted that weren't in the - * supplied classes list will have a negative classCount - * tag value. So all objects counted will have negative tags. - * If the absolute tag value is an index in the supplied - * list, then it's one of the supplied classes. - */ - data.negObjTag = -INDEX2CLASSTAG(classCount); + /* Setup callbacks, only using object reference callback */ + heap_callbacks.heap_reference_callback = &cbObjectCounterFromRef; - /* Setup callbacks, only using object reference callback */ - heap_callbacks.heap_reference_callback = &cbObjectCounterFromRef; - - /* Follow references, no initiating object, tagged classes only */ - error = JVMTI_FUNC_PTR(jvmti,FollowReferences) - (jvmti, JVMTI_HEAP_FILTER_CLASS_UNTAGGED, - NULL, NULL, &heap_callbacks, &data); - - } else { - - /* Using IterateThroughHeap means that we will visit each object - * once, so no special tag tricks here. Just simple counting. - * However in this case the object might not be live, so we do - * a GC beforehand to make sure we minimize this. - */ - - /* FIXUP: Need some kind of trigger here to avoid excessive GC's? */ - error = JVMTI_FUNC_PTR(jvmti,ForceGarbageCollection)(jvmti); - if ( error != JVMTI_ERROR_NONE ) { - - /* Setup callbacks, just need object callback */ - heap_callbacks.heap_iteration_callback = &cbObjectCounter; - - /* Iterate through entire heap, tagged classes only */ - error = JVMTI_FUNC_PTR(jvmti,IterateThroughHeap) - (jvmti, JVMTI_HEAP_FILTER_CLASS_UNTAGGED, - NULL, &heap_callbacks, &data); - - } - } + /* Follow references, no initiating object, tagged classes only */ + error = JVMTI_FUNC_PTR(jvmti,FollowReferences) + (jvmti, JVMTI_HEAP_FILTER_CLASS_UNTAGGED, + NULL, NULL, &heap_callbacks, &data); /* Use data error if needed */ if ( error == JVMTI_ERROR_NONE ) { diff --git a/src/jdk.jdwp.agent/share/native/libjdwp/util.h b/src/jdk.jdwp.agent/share/native/libjdwp/util.h index 3d499d7d569..a48c8ba2c09 100644 --- a/src/jdk.jdwp.agent/share/native/libjdwp/util.h +++ b/src/jdk.jdwp.agent/share/native/libjdwp/util.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -92,12 +92,6 @@ typedef struct { jboolean quiet; jboolean jvmti_data_dump; /* If true, then support JVMTI DATA_DUMP_REQUEST events. */ - /* Debug flags (bit mask) */ - int debugflags; - - /* Possible debug flags */ - #define USE_ITERATE_THROUGH_HEAP 0X001 - char * options; jclass classClass; From d7b525ab9980743cf0cab3e3daaa4ccb725bfea8 Mon Sep 17 00:00:00 2001 From: Phil Race Date: Thu, 16 Oct 2025 16:58:38 +0000 Subject: [PATCH 537/556] 8364673: Remove duplicate font mapping for itcavantgarde in psfontj2d.properties Reviewed-by: azvegint, kizune --- src/java.desktop/share/conf/psfontj2d.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/src/java.desktop/share/conf/psfontj2d.properties b/src/java.desktop/share/conf/psfontj2d.properties index 9efe8864428..8030a82bc4f 100644 --- a/src/java.desktop/share/conf/psfontj2d.properties +++ b/src/java.desktop/share/conf/psfontj2d.properties @@ -59,7 +59,6 @@ avantgarde_book_oblique=avantgarde_book_oblique avantgarde_demi_oblique=avantgarde_demi_oblique # itcavantgarde=avantgarde_book -itcavantgarde=avantgarde_book itcavantgarde_demi=avantgarde_demi itcavantgarde_oblique=avantgarde_book_oblique itcavantgarde_demi_oblique=avantgarde_demi_oblique From 844118a9d854459778f88d299b148c2288131344 Mon Sep 17 00:00:00 2001 From: Phil Race Date: Thu, 16 Oct 2025 16:58:56 +0000 Subject: [PATCH 538/556] 8369146: java/awt/PrintJob/GetGraphicsTest.java: Parse Exception: Invalid or unrecognized bugid: 50510568367702 Reviewed-by: syan, azvegint, kizune, jdv --- test/jdk/java/awt/PrintJob/GetGraphicsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jdk/java/awt/PrintJob/GetGraphicsTest.java b/test/jdk/java/awt/PrintJob/GetGraphicsTest.java index 61abe37b66b..2fe82e57c5a 100644 --- a/test/jdk/java/awt/PrintJob/GetGraphicsTest.java +++ b/test/jdk/java/awt/PrintJob/GetGraphicsTest.java @@ -23,7 +23,7 @@ /* @test - @bug 50510568367702 + @bug 5051056 8367702 @key headful printer @summary PrintJob.getGraphics() should return null after PrintJob.end() is called. @run main GetGraphicsTest From d4472979c43d9825ed2d008dbaed26dbf6d36180 Mon Sep 17 00:00:00 2001 From: William Kemper Date: Thu, 16 Oct 2025 17:49:08 +0000 Subject: [PATCH 539/556] 8367709: GenShen: Dirty cards for objects that get promoted by safepoint that intervenes between allocation and stores Reviewed-by: ysr --- .../share/gc/shenandoah/shenandoahBarrierSet.cpp | 15 +++++++++++++-- .../gc/shenandoah/shenandoahMarkingContext.cpp | 8 ++++---- .../shenandoahMarkingContext.inline.hpp | 4 ++-- .../gc/shenandoah/shenandoahScanRemembered.cpp | 6 +++--- .../shenandoahScanRemembered.inline.hpp | 6 +++--- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp b/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp index 5d19a6a34e3..f6733d4a923 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp @@ -89,8 +89,19 @@ bool ShenandoahBarrierSet::need_keep_alive_barrier(DecoratorSet decorators, Basi void ShenandoahBarrierSet::on_slowpath_allocation_exit(JavaThread* thread, oop new_obj) { #if COMPILER2_OR_JVMCI - assert(!ReduceInitialCardMarks || !ShenandoahCardBarrier || ShenandoahGenerationalHeap::heap()->is_in_young(new_obj), - "Allocating new object outside of young generation: " INTPTR_FORMAT, p2i(new_obj)); + if (ReduceInitialCardMarks && ShenandoahCardBarrier && !ShenandoahHeap::heap()->is_in_young(new_obj)) { + log_debug(gc)("Newly allocated object (" PTR_FORMAT ") is not in the young generation", p2i(new_obj)); + // This can happen when an object is newly allocated, but we come to a safepoint before returning + // the object. If the safepoint runs a degenerated cycle that is upgraded to a full GC, this object + // will have survived two GC cycles. If the tenuring age is very low (1), this object may be promoted. + // In this case, we have an allocated object, but it has received no stores yet. If card marking barriers + // have been elided, we could end up with an object in old holding pointers to young that won't be in + // the remembered set. The solution here is conservative, but this problem should be rare, and it will + // correct itself on subsequent cycles when the remembered set is updated. + ShenandoahGenerationalHeap::heap()->old_generation()->card_scan()->mark_range_as_dirty( + cast_from_oop(new_obj), new_obj->size() + ); + } #endif // COMPILER2_OR_JVMCI assert(thread->deferred_card_mark().is_empty(), "We don't use this"); } diff --git a/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.cpp b/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.cpp index 0babeaffd3e..40eee8c342b 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.cpp @@ -74,8 +74,8 @@ void ShenandoahMarkingContext::initialize_top_at_mark_start(ShenandoahHeapRegion _top_at_mark_starts_base[idx] = bottom; _top_bitmaps[idx] = bottom; - log_debug(gc)("SMC:initialize_top_at_mark_start for Region %zu, TAMS: " PTR_FORMAT ", TopOfBitMap: " PTR_FORMAT, - r->index(), p2i(bottom), p2i(r->end())); + log_debug(gc, mark)("SMC:initialize_top_at_mark_start for Region %zu, TAMS: " PTR_FORMAT ", TopOfBitMap: " PTR_FORMAT, + r->index(), p2i(bottom), p2i(r->end())); } HeapWord* ShenandoahMarkingContext::top_bitmap(ShenandoahHeapRegion* r) { @@ -86,8 +86,8 @@ void ShenandoahMarkingContext::clear_bitmap(ShenandoahHeapRegion* r) { HeapWord* bottom = r->bottom(); HeapWord* top_bitmap = _top_bitmaps[r->index()]; - log_debug(gc)("SMC:clear_bitmap for %s Region %zu, top_bitmap: " PTR_FORMAT, - r->affiliation_name(), r->index(), p2i(top_bitmap)); + log_debug(gc, mark)("SMC:clear_bitmap for %s Region %zu, top_bitmap: " PTR_FORMAT, + r->affiliation_name(), r->index(), p2i(top_bitmap)); if (top_bitmap > bottom) { _mark_bit_map.clear_range_large(MemRegion(bottom, top_bitmap)); diff --git a/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.inline.hpp b/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.inline.hpp index e3ba774283c..bff4afc9ce9 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.inline.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahMarkingContext.inline.hpp @@ -104,8 +104,8 @@ inline void ShenandoahMarkingContext::capture_top_at_mark_start(ShenandoahHeapRe "Region %zu, bitmap should be clear while adjusting TAMS: " PTR_FORMAT " -> " PTR_FORMAT, idx, p2i(old_tams), p2i(new_tams)); - log_debug(gc)("Capturing TAMS for %s Region %zu, was: " PTR_FORMAT ", now: " PTR_FORMAT, - r->affiliation_name(), idx, p2i(old_tams), p2i(new_tams)); + log_debug(gc, mark)("Capturing TAMS for %s Region %zu, was: " PTR_FORMAT ", now: " PTR_FORMAT, + r->affiliation_name(), idx, p2i(old_tams), p2i(new_tams)); _top_at_mark_starts_base[idx] = new_tams; _top_bitmaps[idx] = new_tams; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.cpp b/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.cpp index 23c705348c4..4a0215f15f1 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.cpp @@ -683,9 +683,9 @@ void ShenandoahScanRememberedTask::do_work(uint worker_id) { struct ShenandoahRegionChunk assignment; while (_work_list->next(&assignment)) { ShenandoahHeapRegion* region = assignment._r; - log_debug(gc)("ShenandoahScanRememberedTask::do_work(%u), processing slice of region " - "%zu at offset %zu, size: %zu", - worker_id, region->index(), assignment._chunk_offset, assignment._chunk_size); + log_debug(gc, remset)("ShenandoahScanRememberedTask::do_work(%u), processing slice of region " + "%zu at offset %zu, size: %zu", + worker_id, region->index(), assignment._chunk_offset, assignment._chunk_size); if (region->is_old()) { size_t cluster_size = CardTable::card_size_in_words() * ShenandoahCardCluster::CardsPerCluster; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.inline.hpp b/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.inline.hpp index ce7cda98412..919cc4f6fd7 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.inline.hpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahScanRemembered.inline.hpp @@ -343,9 +343,9 @@ ShenandoahScanRemembered::process_region_slice(ShenandoahHeapRegion *region, siz } } - log_debug(gc)("Remembered set scan processing Region %zu, from " PTR_FORMAT " to " PTR_FORMAT ", using %s table", - region->index(), p2i(start_of_range), p2i(end_of_range), - use_write_table? "read/write (updating)": "read (marking)"); + log_debug(gc, remset)("Remembered set scan processing Region %zu, from " PTR_FORMAT " to " PTR_FORMAT ", using %s table", + region->index(), p2i(start_of_range), p2i(end_of_range), + use_write_table? "read/write (updating)": "read (marking)"); // Note that end_of_range may point to the middle of a cluster because we limit scanning to // region->top() or region->get_update_watermark(). We avoid processing past end_of_range. From 9589a29d2515888b437d382204df22d01d4266ff Mon Sep 17 00:00:00 2001 From: Mikael Vidstedt Date: Thu, 16 Oct 2025 19:43:44 +0000 Subject: [PATCH 540/556] 8355752: Bump minimum boot jdk to JDK 25 Reviewed-by: darcy, shade, ihse, iris --- make/conf/github-actions.conf | 20 ++++++++++---------- make/conf/jib-profiles.js | 4 ++-- make/conf/version-numbers.conf | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/make/conf/github-actions.conf b/make/conf/github-actions.conf index 74a830cbcc2..bd73e909062 100644 --- a/make/conf/github-actions.conf +++ b/make/conf/github-actions.conf @@ -29,21 +29,21 @@ GTEST_VERSION=1.14.0 JTREG_VERSION=8.1+1 LINUX_X64_BOOT_JDK_EXT=tar.gz -LINUX_X64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_linux-x64_bin.tar.gz -LINUX_X64_BOOT_JDK_SHA256=88b090fa80c6c1d084ec9a755233967458788e2c0777ae2e172230c5c692d7ef +LINUX_X64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk25/bd75d5f9689641da8e1daabeccb5528b/36/GPL/openjdk-25_linux-x64_bin.tar.gz +LINUX_X64_BOOT_JDK_SHA256=59cdcaf255add4721de38eb411d4ecfe779356b61fb671aee63c7dec78054c2b ALPINE_LINUX_X64_BOOT_JDK_EXT=tar.gz -ALPINE_LINUX_X64_BOOT_JDK_URL=https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24%2B36/OpenJDK24U-jdk_x64_alpine-linux_hotspot_24_36.tar.gz -ALPINE_LINUX_X64_BOOT_JDK_SHA256=a642608f0da78344ee6812fb1490b8bc1d7ad5a18064c70994d6f330568c51cb +ALPINE_LINUX_X64_BOOT_JDK_URL=https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25%2B36/OpenJDK25U-jdk_x64_alpine-linux_hotspot_25_36.tar.gz +ALPINE_LINUX_X64_BOOT_JDK_SHA256=637e47474d411ed86134f413af7d5fef4180ddb0bf556347b7e74a88cf8904c8 MACOS_AARCH64_BOOT_JDK_EXT=tar.gz -MACOS_AARCH64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_macos-aarch64_bin.tar.gz -MACOS_AARCH64_BOOT_JDK_SHA256=f7133238a12714a62c5ad2bd4da6741130be1a82512065da9ca23dee26b2d3d3 +MACOS_AARCH64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk25/bd75d5f9689641da8e1daabeccb5528b/36/GPL/openjdk-25_macos-aarch64_bin.tar.gz +MACOS_AARCH64_BOOT_JDK_SHA256=2006337bf326fdfdf6117081751ba38c1c8706d63419ecac7ff102ff7c776876 MACOS_X64_BOOT_JDK_EXT=tar.gz -MACOS_X64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_macos-x64_bin.tar.gz -MACOS_X64_BOOT_JDK_SHA256=6bbfb1d01741cbe55ab90299cb91464b695de9a3ace85c15131aa2f50292f321 +MACOS_X64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk25/bd75d5f9689641da8e1daabeccb5528b/36/GPL/openjdk-25_macos-x64_bin.tar.gz +MACOS_X64_BOOT_JDK_SHA256=47482ad9888991ecac9b2bcc131e2b53ff78aff275104cef85f66252308e8a09 WINDOWS_X64_BOOT_JDK_EXT=zip -WINDOWS_X64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk24/1f9ff9062db4449d8ca828c504ffae90/36/GPL/openjdk-24_windows-x64_bin.zip -WINDOWS_X64_BOOT_JDK_SHA256=11d1d9f6ac272d5361c8a0bef01894364081c7fb1a6914c2ad2fc312ae83d63b +WINDOWS_X64_BOOT_JDK_URL=https://download.java.net/java/GA/jdk25/bd75d5f9689641da8e1daabeccb5528b/36/GPL/openjdk-25_windows-x64_bin.zip +WINDOWS_X64_BOOT_JDK_SHA256=85bcc178461e2cb3c549ab9ca9dfa73afd54c09a175d6510d0884071867137d3 diff --git a/make/conf/jib-profiles.js b/make/conf/jib-profiles.js index 9706321d7b6..795335d7c3c 100644 --- a/make/conf/jib-profiles.js +++ b/make/conf/jib-profiles.js @@ -387,8 +387,8 @@ var getJibProfilesCommon = function (input, data) { }; }; - common.boot_jdk_version = "24"; - common.boot_jdk_build_number = "36"; + common.boot_jdk_version = "25"; + common.boot_jdk_build_number = "37"; common.boot_jdk_home = input.get("boot_jdk", "install_path") + "/jdk-" + common.boot_jdk_version + (input.build_os == "macosx" ? ".jdk/Contents/Home" : ""); diff --git a/make/conf/version-numbers.conf b/make/conf/version-numbers.conf index 38d6e42dff9..977809535ba 100644 --- a/make/conf/version-numbers.conf +++ b/make/conf/version-numbers.conf @@ -37,6 +37,6 @@ DEFAULT_VERSION_DATE=2026-03-17 DEFAULT_VERSION_CLASSFILE_MAJOR=70 # "`$EXPR $DEFAULT_VERSION_FEATURE + 44`" DEFAULT_VERSION_CLASSFILE_MINOR=0 DEFAULT_VERSION_DOCS_API_SINCE=11 -DEFAULT_ACCEPTABLE_BOOT_VERSIONS="24 25 26" +DEFAULT_ACCEPTABLE_BOOT_VERSIONS="25 26" DEFAULT_JDK_SOURCE_TARGET_VERSION=26 DEFAULT_PROMOTED_VERSION_PRE=ea From 3248aaf3c4f6784d5176e2a2c5bac0fbda47ee6b Mon Sep 17 00:00:00 2001 From: Chen Liang Date: Thu, 16 Oct 2025 19:45:57 +0000 Subject: [PATCH 541/556] 8356548: Use ClassFile API instead of ASM to transform classes in tests Reviewed-by: sspitsyn, lmesnik, coleenp, iklam --- .../calls/common/InvokeDynamicPatcher.java | 215 ++++++++---------- .../CompiledInvokeDynamic2CompiledTest.java | 2 +- ...CompiledInvokeDynamic2InterpretedTest.java | 2 +- .../CompiledInvokeDynamic2NativeTest.java | 2 +- ...InterpretedInvokeDynamic2CompiledTest.java | 2 +- ...erpretedInvokeDynamic2InterpretedTest.java | 2 +- .../InterpretedInvokeDynamic2NativeTest.java | 2 +- ...fineMethodUsedByMultipleMethodHandles.java | 42 ++-- .../compiler/jvmci/common/CTVMUtilities.java | 125 +++++----- .../jtreg/runtime/MirrorFrame/Asmator.java | 47 ++-- .../runtime/MirrorFrame/Test8003720.java | 1 - .../MissedStackMapFrames.java | 45 ++-- .../RedefineClasses/RedefineAnnotations.java | 75 ++---- .../RedefineGenericSignatureTest.java | 30 ++- .../jvmti/RedefineClasses/RedefineObject.java | 46 +--- .../RedefineRetransform.java | 86 +++---- .../gc/g1/unloading/GenClassPoolJar.java | 35 +-- .../TestDescription.java | 1 - .../TestDescription.java | 1 - .../TestDescription.java | 1 - .../TestDescription.java | 1 - .../TestDescription.java | 1 - .../TestDescription.java | 1 - .../nsk/jvmti/GetClassFields/getclfld007.java | 57 ++--- 24 files changed, 306 insertions(+), 516 deletions(-) diff --git a/test/hotspot/jtreg/compiler/calls/common/InvokeDynamicPatcher.java b/test/hotspot/jtreg/compiler/calls/common/InvokeDynamicPatcher.java index b82e06317d2..d5c93ebaf5e 100644 --- a/test/hotspot/jtreg/compiler/calls/common/InvokeDynamicPatcher.java +++ b/test/hotspot/jtreg/compiler/calls/common/InvokeDynamicPatcher.java @@ -23,150 +23,121 @@ package compiler.calls.common; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Handle; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - -import java.io.FileInputStream; import java.io.IOException; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.Label; +import java.lang.constant.ClassDesc; +import java.lang.constant.DirectMethodHandleDesc; +import java.lang.constant.DynamicCallSiteDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; import java.lang.invoke.CallSite; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import static java.lang.constant.ConstantDescs.*; + /** * A class which patch InvokeDynamic class bytecode with invokydynamic instruction, rewriting "caller" method to call "callee" method using invokedynamic */ -public class InvokeDynamicPatcher extends ClassVisitor { +public final class InvokeDynamicPatcher { - private static final String CLASS = InvokeDynamic.class.getName() - .replace('.', '/'); + private static final ClassDesc CLASS = InvokeDynamic.class.describeConstable().orElseThrow(); private static final String CALLER_METHOD_NAME = "caller"; private static final String CALLEE_METHOD_NAME = "callee"; private static final String NATIVE_CALLEE_METHOD_NAME = "calleeNative"; private static final String BOOTSTRAP_METHOD_NAME = "bootstrapMethod"; private static final String CALL_NATIVE_FIELD = "nativeCallee"; - private static final String CALL_NATIVE_FIELD_DESC = "Z"; - private static final String CALLEE_METHOD_DESC - = "(L" + CLASS + ";IJFDLjava/lang/String;)Z"; - private static final String ASSERTTRUE_METHOD_DESC - = "(ZLjava/lang/String;)V"; - private static final String ASSERTS_CLASS = "jdk/test/lib/Asserts"; + private static final ClassDesc CALL_NATIVE_FIELD_DESC = CD_boolean; + private static final MethodTypeDesc CALLEE_METHOD_DESC = MethodTypeDesc.of( + CD_boolean, CLASS, CD_int, CD_long, CD_float, CD_double, CD_String); + private static final MethodTypeDesc ASSERTTRUE_METHOD_DESC = MethodTypeDesc.of( + CD_void, CD_boolean, CD_String); + private static final ClassDesc ASSERTS_CLASS = ClassDesc.ofInternalName("jdk/test/lib/Asserts"); private static final String ASSERTTRUE_METHOD_NAME = "assertTrue"; - public static void main(String args[]) { - ClassReader cr; - Path filePath; - try { - filePath = Paths.get(InvokeDynamic.class.getProtectionDomain().getCodeSource() - .getLocation().toURI()).resolve(CLASS + ".class"); - } catch (URISyntaxException ex) { - throw new Error("TESTBUG: Can't get code source" + ex, ex); - } - try (FileInputStream fis = new FileInputStream(filePath.toFile())) { - cr = new ClassReader(fis); - } catch (IOException e) { - throw new Error("Error reading file", e); - } - ClassWriter cw = new ClassWriter(cr, - ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - cr.accept(new InvokeDynamicPatcher(Opcodes.ASM5, cw), 0); - try { - Files.write(filePath, cw.toByteArray(), - StandardOpenOption.WRITE); - } catch (IOException e) { - throw new Error(e); - } - } + public static void main(String args[]) throws IOException, URISyntaxException { + Path filePath = Path.of(InvokeDynamic.class.getProtectionDomain().getCodeSource() + .getLocation().toURI()).resolve(InvokeDynamic.class.getName().replace('.', '/') +".class"); + var bytes = ClassFile.of().transformClass(ClassFile.of().parse(filePath), + ClassTransform.transformingMethodBodies(m -> m.methodName().equalsString(CALLER_METHOD_NAME), new CodeTransform() { + @Override + public void accept(CodeBuilder builder, CodeElement element) { + // discard + } - public InvokeDynamicPatcher(int api, ClassWriter cw) { - super(api, cw); - } - - @Override - public MethodVisitor visitMethod(final int access, final String name, - final String desc, final String signature, - final String[] exceptions) { - /* a code generate looks like - * 0: aload_0 - * 1: ldc #125 // int 1 - * 3: ldc2_w #126 // long 2l - * 6: ldc #128 // float 3.0f - * 8: ldc2_w #129 // double 4.0d - * 11: ldc #132 // String 5 - * 13: aload_0 - * 14: getfield #135 // Field nativeCallee:Z - * 17: ifeq 28 - * 20: invokedynamic #181, 0 // InvokeDynamic #1:calleeNative:(Lcompiler/calls/common/InvokeDynamic;IJFDLjava/lang/String;)Z - * 25: goto 33 - * 28: invokedynamic #183, 0 // InvokeDynamic #1:callee:(Lcompiler/calls/common/InvokeDynamic;IJFDLjava/lang/String;)Z - * 33: ldc #185 // String Call insuccessfull - * 35: invokestatic #191 // Method jdk/test/lib/Asserts.assertTrue:(ZLjava/lang/String;)V - * 38: return - * - * or, using java-like pseudo-code - * if (this.nativeCallee == false) { - * invokedynamic-call-return-value = invokedynamic-of-callee - * } else { - * invokedynamic-call-return-value = invokedynamic-of-nativeCallee - * } - * Asserts.assertTrue(invokedynamic-call-return-value, error-message); - * return; - */ - if (name.equals(CALLER_METHOD_NAME)) { - MethodVisitor mv = cv.visitMethod(access, name, desc, - signature, exceptions); - Label nonNativeLabel = new Label(); - Label checkLabel = new Label(); - MethodType mtype = MethodType.methodType(CallSite.class, - MethodHandles.Lookup.class, String.class, MethodType.class); - Handle bootstrap = new Handle(Opcodes.H_INVOKESTATIC, CLASS, - BOOTSTRAP_METHOD_NAME, mtype.toMethodDescriptorString()); - mv.visitCode(); - // push callee parameters onto stack - mv.visitVarInsn(Opcodes.ALOAD, 0);//push "this" - mv.visitLdcInsn(1); - mv.visitLdcInsn(2L); - mv.visitLdcInsn(3.0f); - mv.visitLdcInsn(4.0d); - mv.visitLdcInsn("5"); - // params loaded. let's decide what method to call - mv.visitVarInsn(Opcodes.ALOAD, 0); // push "this" - // get nativeCallee field - mv.visitFieldInsn(Opcodes.GETFIELD, CLASS, CALL_NATIVE_FIELD, - CALL_NATIVE_FIELD_DESC); - // if nativeCallee == false goto nonNativeLabel - mv.visitJumpInsn(Opcodes.IFEQ, nonNativeLabel); - // invokedynamic nativeCalleeMethod using bootstrap method - mv.visitInvokeDynamicInsn(NATIVE_CALLEE_METHOD_NAME, - CALLEE_METHOD_DESC, bootstrap); - // goto checkLabel - mv.visitJumpInsn(Opcodes.GOTO, checkLabel); - // label: nonNativeLabel - mv.visitLabel(nonNativeLabel); - // invokedynamic calleeMethod using bootstrap method - mv.visitInvokeDynamicInsn(CALLEE_METHOD_NAME, CALLEE_METHOD_DESC, - bootstrap); - mv.visitLabel(checkLabel); - mv.visitLdcInsn(CallsBase.CALL_ERR_MSG); - mv.visitMethodInsn(Opcodes.INVOKESTATIC, ASSERTS_CLASS, - ASSERTTRUE_METHOD_NAME, ASSERTTRUE_METHOD_DESC, false); - // label: return - mv.visitInsn(Opcodes.RETURN); - mv.visitMaxs(0, 0); - mv.visitEnd(); - return null; - } - return super.visitMethod(access, name, desc, signature, exceptions); + /* the code generated looks like + * 0: aload_0 + * 1: ldc #125 // int 1 + * 3: ldc2_w #126 // long 2l + * 6: ldc #128 // float 3.0f + * 8: ldc2_w #129 // double 4.0d + * 11: ldc #132 // String 5 + * 13: aload_0 + * 14: getfield #135 // Field nativeCallee:Z + * 17: ifeq 28 + * 20: invokedynamic #181, 0 // InvokeDynamic #1:calleeNative:(Lcompiler/calls/common/InvokeDynamic;IJFDLjava/lang/String;)Z + * 25: goto 33 + * 28: invokedynamic #183, 0 // InvokeDynamic #1:callee:(Lcompiler/calls/common/InvokeDynamic;IJFDLjava/lang/String;)Z + * 33: ldc #185 // String Call insuccessfull + * 35: invokestatic #191 // Method jdk/test/lib/Asserts.assertTrue:(ZLjava/lang/String;)V + * 38: return + * + * or, using java-like pseudo-code + * if (this.nativeCallee == false) { + * invokedynamic-call-return-value = invokedynamic-of-callee + * } else { + * invokedynamic-call-return-value = invokedynamic-of-nativeCallee + * } + * Asserts.assertTrue(invokedynamic-call-return-value, error-message); + * return; + */ + @Override + public void atEnd(CodeBuilder builder) { + Label nonNativeLabel = builder.newLabel(); + Label checkLabel = builder.newLabel(); + MethodType mtype = MethodType.methodType(CallSite.class, + MethodHandles.Lookup.class, String.class, MethodType.class); + DirectMethodHandleDesc dmh = MethodHandleDesc.of(DirectMethodHandleDesc.Kind.STATIC, + CLASS, BOOTSTRAP_METHOD_NAME, mtype.descriptorString()); + // push callee parameters onto stack + builder.aload(builder.receiverSlot()) + .ldc(1) + .ldc(2L) + .ldc(3.0f) + .ldc(4.0d) + .ldc("5") + // params loaded. let's decide what method to call + .aload(builder.receiverSlot()) + // get nativeCallee field + .getfield(CLASS, CALL_NATIVE_FIELD, CALL_NATIVE_FIELD_DESC) + // if nativeCallee == false goto nonNativeLabel + .ifeq(nonNativeLabel) + // invokedynamic nativeCalleeMethod using bootstrap method + .invokedynamic(DynamicCallSiteDesc.of(dmh, NATIVE_CALLEE_METHOD_NAME, CALLEE_METHOD_DESC)) + // goto checkLabel + .goto_(checkLabel) + // label: nonNativeLabel + .labelBinding(nonNativeLabel) + // invokedynamic calleeMethod using bootstrap method + .invokedynamic(DynamicCallSiteDesc.of(dmh, CALLEE_METHOD_NAME, CALLEE_METHOD_DESC)) + .labelBinding(checkLabel) + .ldc(CallsBase.CALL_ERR_MSG) + .invokestatic(ASSERTS_CLASS, ASSERTTRUE_METHOD_NAME, ASSERTTRUE_METHOD_DESC) + // label: return + .return_(); + } + })); + Files.write(filePath, bytes, StandardOpenOption.WRITE); } } diff --git a/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2CompiledTest.java b/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2CompiledTest.java index 914500a25d4..c20f8f44822 100644 --- a/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2CompiledTest.java +++ b/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2CompiledTest.java @@ -25,10 +25,10 @@ * @test * @summary check calls from compiled to compiled using InvokeDynamic * @library /test/lib / - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * * @build jdk.test.whitebox.WhiteBox + * @build compiler.calls.common.InvokeDynamic * @run driver compiler.calls.common.InvokeDynamicPatcher * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. diff --git a/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2InterpretedTest.java b/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2InterpretedTest.java index b6f8520a90a..ee497d10707 100644 --- a/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2InterpretedTest.java +++ b/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2InterpretedTest.java @@ -25,10 +25,10 @@ * @test * @summary check calls from compiled to interpreted using InvokeDynamic * @library /test/lib / - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * * @build jdk.test.whitebox.WhiteBox + * @build compiler.calls.common.InvokeDynamic * @run driver compiler.calls.common.InvokeDynamicPatcher * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. diff --git a/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2NativeTest.java b/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2NativeTest.java index e334f6a15f0..7dc2b423587 100644 --- a/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2NativeTest.java +++ b/test/hotspot/jtreg/compiler/calls/fromCompiled/CompiledInvokeDynamic2NativeTest.java @@ -25,10 +25,10 @@ * @test * @summary check calls from compiled to native using InvokeDynamic * @library /test/lib / - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * * @build jdk.test.whitebox.WhiteBox + * @build compiler.calls.common.InvokeDynamic * @run driver compiler.calls.common.InvokeDynamicPatcher * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox * @run main/othervm/native -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. diff --git a/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2CompiledTest.java b/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2CompiledTest.java index ca626fd3b01..0ba1dd1a496 100644 --- a/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2CompiledTest.java +++ b/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2CompiledTest.java @@ -25,10 +25,10 @@ * @test * @summary check calls from interpreted to compiled using InvokeDynamic * @library /test/lib / - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * * @build jdk.test.whitebox.WhiteBox + * @build compiler.calls.common.InvokeDynamic * @run driver compiler.calls.common.InvokeDynamicPatcher * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. diff --git a/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2InterpretedTest.java b/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2InterpretedTest.java index d8d877977d4..7ed8c683629 100644 --- a/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2InterpretedTest.java +++ b/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2InterpretedTest.java @@ -25,10 +25,10 @@ * @test * @summary check calls from interpreted to interpreted using InvokeDynamic * @library /test/lib / - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * * @build jdk.test.whitebox.WhiteBox + * @build compiler.calls.common.InvokeDynamic * @run driver compiler.calls.common.InvokeDynamicPatcher * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. diff --git a/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2NativeTest.java b/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2NativeTest.java index 38bca4939d4..20f94db80b1 100644 --- a/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2NativeTest.java +++ b/test/hotspot/jtreg/compiler/calls/fromInterpreted/InterpretedInvokeDynamic2NativeTest.java @@ -25,10 +25,10 @@ * @test * @summary check calls from interpreted to native using InvokeDynamic * @library /test/lib / - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * * @build jdk.test.whitebox.WhiteBox + * @build compiler.calls.common.InvokeDynamic * @run driver compiler.calls.common.InvokeDynamicPatcher * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox * @run main/othervm/native -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. diff --git a/test/hotspot/jtreg/compiler/jsr292/RedefineMethodUsedByMultipleMethodHandles.java b/test/hotspot/jtreg/compiler/jsr292/RedefineMethodUsedByMultipleMethodHandles.java index 769863880f2..9a74806e621 100644 --- a/test/hotspot/jtreg/compiler/jsr292/RedefineMethodUsedByMultipleMethodHandles.java +++ b/test/hotspot/jtreg/compiler/jsr292/RedefineMethodUsedByMultipleMethodHandles.java @@ -21,12 +21,11 @@ * questions. */ -/** +/* * @test * @bug 8042235 * @summary redefining method used by multiple MethodHandles crashes VM * @library / - * @library /testlibrary/asm * @modules java.compiler * java.instrument * jdk.attach @@ -37,15 +36,13 @@ package compiler.jsr292; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassHierarchyResolver; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.instruction.ConstantInstruction; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; @@ -159,28 +156,15 @@ public class RedefineMethodUsedByMultipleMethodHandles { public byte[] transform(ClassLoader cl, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (Foo.class.equals(classBeingRedefined)) { System.out.println("redefining " + classBeingRedefined); - ClassReader cr = new ClassReader(classfileBuffer); - ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); - ClassVisitor adapter = new ClassVisitor(Opcodes.ASM5, cw) { - @Override - public MethodVisitor visitMethod(int access, String base, String desc, String signature, String[] exceptions) { - MethodVisitor mv = cv.visitMethod(access, base, desc, signature, exceptions); - if (mv != null) { - mv = new MethodVisitor(Opcodes.ASM5, mv) { - @Override - public void visitLdcInsn(Object cst) { - System.out.println("replacing \"" + cst + "\" with \"bar\""); - mv.visitLdcInsn("bar"); - } - }; - } - return mv; + var context = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofResourceParsing(cl))); + return context.transformClass(context.parse(classfileBuffer), ClassTransform.transformingMethodBodies((codeBuilder, codeElement) -> { + if (codeElement instanceof ConstantInstruction.LoadConstantInstruction ldc) { + System.out.println("replacing \"" + ldc.constantEntry().constantValue() + "\" with \"bar\""); + codeBuilder.ldc("bar"); + } else { + codeBuilder.with(codeElement); } - }; - - cr.accept(adapter, ClassReader.SKIP_FRAMES); - cw.visitEnd(); - return cw.toByteArray(); + })); } return classfileBuffer; } diff --git a/test/hotspot/jtreg/compiler/jvmci/common/CTVMUtilities.java b/test/hotspot/jtreg/compiler/jvmci/common/CTVMUtilities.java index 95917f6d943..c98d9944c80 100644 --- a/test/hotspot/jtreg/compiler/jvmci/common/CTVMUtilities.java +++ b/test/hotspot/jtreg/compiler/jvmci/common/CTVMUtilities.java @@ -23,28 +23,26 @@ package compiler.jvmci.common; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import jdk.test.lib.Utils; import jdk.vm.ci.code.InstalledCode; import jdk.vm.ci.meta.ResolvedJavaMethod; import jdk.vm.ci.hotspot.CompilerToVMHelper; -import jdk.vm.ci.hotspot.HotSpotNmethod; import jdk.vm.ci.hotspot.HotSpotResolvedJavaMethod; import java.io.IOException; +import java.lang.classfile.Attributes; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.MethodModel; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDescs; +import java.lang.constant.MethodTypeDesc; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.util.HashMap; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -71,76 +69,55 @@ public class CTVMUtilities { } public static Map getBciToLineNumber(Executable method) { - Map lineNumbers = new TreeMap<>(); - Class aClass = method.getDeclaringClass(); - ClassReader cr; - try { - Module aModule = aClass.getModule(); - String name = aClass.getName(); - cr = new ClassReader(aModule.getResourceAsStream( - name.replace('.', '/') + ".class")); - } catch (IOException e) { - throw new Error("TEST BUG: can read " + aClass.getName() + " : " + e, e); - } - ClassNode cn = new ClassNode(); - cr.accept(cn, ClassReader.EXPAND_FRAMES); + ClassModel classModel = findClassBytes(method.getDeclaringClass()); + MethodModel methodModel = findMethod(classModel, method); + if (methodModel == null) + return Map.of(); - Map labels = new HashMap<>(); - ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); - ClassVisitor cv = new ClassVisitorForLabels(cw, labels, method); - cr.accept(cv, ClassReader.EXPAND_FRAMES); - labels.forEach((k, v) -> lineNumbers.put(k.getOffset(), v)); - boolean isEmptyMethod = Modifier.isAbstract(method.getModifiers()) - || Modifier.isNative(method.getModifiers()); - if (lineNumbers.isEmpty() && !isEmptyMethod) { - throw new Error(method + " doesn't contains the line numbers table " - +"(the method marked neither abstract nor native)"); + var foundLineNumberTable = methodModel.code().flatMap(code -> + code.findAttribute(Attributes.lineNumberTable())); + if (foundLineNumberTable.isEmpty()) { + boolean isEmptyMethod = Modifier.isAbstract(method.getModifiers()) + || Modifier.isNative(method.getModifiers()); + if (!isEmptyMethod) { + throw new Error(method + " doesn't contains the line numbers table " + + "(the method marked neither abstract nor native)"); + } + return Map.of(); } + + Map lineNumbers = new TreeMap<>(); + foundLineNumberTable.get().lineNumbers().forEach(ln -> + lineNumbers.put(ln.startPc(), ln.lineNumber())); return lineNumbers; } - private static class ClassVisitorForLabels extends ClassVisitor { - private final Map lineNumbers; - private final String targetName; - private final String targetDesc; - - public ClassVisitorForLabels(ClassWriter cw, Map lines, - Executable target) { - super(Opcodes.ASM7, cw); - this.lineNumbers = lines; - - StringBuilder builder = new StringBuilder("("); - for (Parameter parameter : target.getParameters()) { - builder.append(Utils.toJVMTypeSignature(parameter.getType())); - } - builder.append(")"); - if (target instanceof Constructor) { - targetName = ""; - builder.append("V"); - } else { - targetName = target.getName(); - builder.append(Utils.toJVMTypeSignature( - ((Method) target).getReturnType())); - } - targetDesc = builder.toString(); + // Finds the ClassFile API model of a given class, or fail with an Error. + public static ClassModel findClassBytes(Class clazz) { + String binaryName = clazz.getName(); + byte[] fileBytes; + try (var inputStream = clazz.getModule().getResourceAsStream( + binaryName.replace('.', '/') + ".class")) { + fileBytes = inputStream.readAllBytes(); + } catch (IOException e) { + throw new Error("TEST BUG: cannot read " + binaryName, e); } + return ClassFile.of().parse(fileBytes); + } - @Override - public final MethodVisitor visitMethod(int access, String name, - String desc, String signature, - String[] exceptions) { - MethodVisitor mv = cv.visitMethod(access, name, desc, signature, - exceptions); - if (targetDesc.equals(desc) && targetName.equals(name)) { - return new MethodVisitor(Opcodes.ASM7, mv) { - @Override - public void visitLineNumber(int i, Label label) { - super.visitLineNumber(i, label); - lineNumbers.put(label, i); - } - }; + // Finds a matching method in a class model, or null if none match. + public static MethodModel findMethod(ClassModel classModel, Executable method) { + MethodTypeDesc methodType = MethodType.methodType( + method instanceof Method m ? m.getReturnType() : void.class, + method.getParameterTypes()).describeConstable().orElseThrow(); + String methodName = method instanceof Method m ? m.getName() : ConstantDescs.INIT_NAME; + + for (var methodModel : classModel.methods()) { + if (methodModel.methodName().equalsString(methodName) + && methodModel.methodType().isMethodType(methodType)) { + return methodModel; } - return mv; } + return null; } } diff --git a/test/hotspot/jtreg/runtime/MirrorFrame/Asmator.java b/test/hotspot/jtreg/runtime/MirrorFrame/Asmator.java index c5d9a4cb595..d8165d80196 100644 --- a/test/hotspot/jtreg/runtime/MirrorFrame/Asmator.java +++ b/test/hotspot/jtreg/runtime/MirrorFrame/Asmator.java @@ -21,35 +21,28 @@ * questions. */ -import org.objectweb.asm.*; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; class Asmator { - static byte[] fixup(byte[] buf) throws java.io.IOException { - ClassReader cr = new ClassReader(buf); - ClassWriter cw = new ClassWriter(0); - ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) { - public MethodVisitor visitMethod( - final int access, - final String name, - final String desc, - final String signature, - final String[] exceptions) - { - MethodVisitor mv = super.visitMethod(access, - name, - desc, - signature, - exceptions); - if (mv == null) return null; - if (name.equals("callme")) { - // make receiver go dead! - mv.visitInsn(Opcodes.ACONST_NULL); - mv.visitVarInsn(Opcodes.ASTORE, 0); + static byte[] fixup(byte[] buf) { + return ClassFile.of().transformClass(ClassFile.of().parse(buf), ClassTransform.transformingMethodBodies( + m -> m.methodName().equalsString("callme"), + new CodeTransform() { + @Override + public void atStart(CodeBuilder builder) { + // make receiver go dead! + builder.aconst_null().astore(0); + } + + @Override + public void accept(CodeBuilder builder, CodeElement element) { + builder.with(element); // pass through + } } - return mv; - } - }; - cr.accept(cv, 0); - return cw.toByteArray(); + )); } } diff --git a/test/hotspot/jtreg/runtime/MirrorFrame/Test8003720.java b/test/hotspot/jtreg/runtime/MirrorFrame/Test8003720.java index 43c8fefacd2..5bcae8e45d1 100644 --- a/test/hotspot/jtreg/runtime/MirrorFrame/Test8003720.java +++ b/test/hotspot/jtreg/runtime/MirrorFrame/Test8003720.java @@ -25,7 +25,6 @@ * @test * @bug 8003720 * @summary Method in interpreter stack frame can be deallocated - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * @compile -XDignore.symbol.file Victim.java * @run main/othervm -Xverify:all -Xint Test8003720 diff --git a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/MissedStackMapFrames/MissedStackMapFrames.java b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/MissedStackMapFrames/MissedStackMapFrames.java index 534f077eaf4..941c6e79bb8 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/MissedStackMapFrames/MissedStackMapFrames.java +++ b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/MissedStackMapFrames/MissedStackMapFrames.java @@ -27,17 +27,15 @@ * @bug 8228604 * * @requires vm.jvmti - * @library /testlibrary/asm * @library /test/lib * * @run main/othervm/native -agentlib:MissedStackMapFrames MissedStackMapFrames */ -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - +import java.lang.classfile.Attributes; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.MethodModel; public class MissedStackMapFrames { static { @@ -58,30 +56,19 @@ public class MissedStackMapFrames { private static native byte[] retransformBytes(int idx); private static int getStackMapFrameCount(byte[] classfileBuffer) { - ClassReader reader = new ClassReader(classfileBuffer); - final int[] frameCount = {0}; - ClassVisitor cv = new ClassVisitor(Opcodes.ASM9) { - @Override - public MethodVisitor visitMethod(int access, String name, - String descriptor, String signature, - String[] exceptions) { - return new MethodVisitor(Opcodes.ASM9) { - private int methodFrames = 0; - @Override - public void visitFrame(int type, int numLocal, Object[] local, - int numStack, Object[] stack) { - methodFrames++; - } - @Override - public void visitEnd() { - log(" method " + name + " - " + methodFrames + " frames"); - frameCount[0] += methodFrames; - } - }; + ClassModel clazz = ClassFile.of().parse(classfileBuffer); + int count = 0; + for (MethodModel method : clazz.methods()) { + var foundStackMapTable = method.code().flatMap(code -> code.findAttribute(Attributes.stackMapTable())); + if (foundStackMapTable.isPresent()) { + int methodFrames = foundStackMapTable.get().entries().size(); + log(" method " + method.methodName() + " - " + methodFrames + " frames"); + count += methodFrames; + } else { + log(" method " + method.methodName() + " - No StackMapTable"); } - }; - reader.accept(cv, 0); - return frameCount[0]; + } + return count; } private static int checkStackMapFrames(String mode, byte[] classfileBuffer) { diff --git a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineAnnotations.java b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineAnnotations.java index aad876d7181..320531148d4 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineAnnotations.java +++ b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineAnnotations.java @@ -24,7 +24,6 @@ /* * @test * @library /test/lib - * @library /testlibrary/asm * @summary Test that type annotations are retained after a retransform * @requires vm.jvmti * @modules java.base/jdk.internal.misc @@ -46,6 +45,11 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassElement; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.FieldModel; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; @@ -55,17 +59,10 @@ import java.lang.reflect.AnnotatedParameterizedType; import java.lang.reflect.AnnotatedType; import java.lang.reflect.AnnotatedWildcardType; import java.lang.reflect.Executable; -import java.lang.reflect.TypeVariable; import java.security.ProtectionDomain; -import java.util.Arrays; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.FieldVisitor; -import static org.objectweb.asm.Opcodes.ASM7; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE_USE) @@ -86,53 +83,27 @@ public class RedefineAnnotations { ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { - ClassWriter cw = new ClassWriter(0); - ClassVisitor cv = new ReAddDummyFieldsClassVisitor(ASM7, cw) { }; - ClassReader cr = new ClassReader(classfileBuffer); - cr.accept(cv, 0); - return cw.toByteArray(); - } + // Shuffle constant pool + ClassFile context = ClassFile.of(ClassFile.ConstantPoolSharingOption.NEW_POOL); + return context.transformClass(context.parse(classfileBuffer), new ClassTransform() { + final List dummyFields = new ArrayList<>(); - public class ReAddDummyFieldsClassVisitor extends ClassVisitor { - - LinkedList fields = new LinkedList<>(); - - public ReAddDummyFieldsClassVisitor(int api, ClassVisitor cv) { - super(api, cv); - } - - @Override public FieldVisitor visitField(int access, String name, - String desc, String signature, Object value) { - if (name.startsWith("dummy")) { - // Remove dummy field - fields.addLast(new F(access, name, desc, signature, value)); - return null; + @Override + public void accept(ClassBuilder builder, ClassElement element) { + if (element instanceof FieldModel field && field.fieldName().stringValue().startsWith("dummy")) { + // Hold on to the associated constant pool entries too + dummyFields.addLast(field); + } else { + builder.with(element); + } } - return cv.visitField(access, name, desc, signature, value); - } - @Override public void visitEnd() { - F f; - while ((f = fields.pollFirst()) != null) { - // Re-add dummy fields - cv.visitField(f.access, f.name, f.desc, f.signature, f.value); + @Override + public void atEnd(ClassBuilder builder) { + // Add the associated constant pool entries to the end of the CP + dummyFields.forEach(builder); } - } - - private class F { - private int access; - private String name; - private String desc; - private String signature; - private Object value; - F(int access, String name, String desc, String signature, Object value) { - this.access = access; - this.name = name; - this.desc = desc; - this.signature = signature; - this.value = value; - } - } + }); } @Override public byte[] transform(ClassLoader loader, String className, diff --git a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineGenericSignatureTest.java b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineGenericSignatureTest.java index f220a93f187..923253e8051 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineGenericSignatureTest.java +++ b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineGenericSignatureTest.java @@ -35,6 +35,11 @@ import java.io.File; import java.io.FileOutputStream; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassElement; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.attribute.SourceFileAttribute; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -141,27 +146,28 @@ public class RedefineGenericSignatureTest { private byte[] getNewClassBytes() { byte[] bytecode = InMemoryJavaCompiler.compile(GenericSignatureTarget.class.getName(), newTargetClassSource); - ClassWriter cw = new ClassWriter(0); - ClassReader cr = new ClassReader(bytecode); - cr.accept(new ClassVisitor(Opcodes.ASM7, cw) { + ClassFile context = ClassFile.of(); + return context.transformClass(context.parse(bytecode), new ClassTransform() { private boolean sourceSet = false; @Override - public void visitSource(String source, String debug) { - sourceSet = true; - log("Changing source: \"" + source + "\" -> \"" + sourceFileNameNew + "\""); - super.visitSource(sourceFileNameNew, debug); + public void accept(ClassBuilder builder, ClassElement element) { + if (element instanceof SourceFileAttribute src) { + sourceSet = true; + log("Changing source: \"" + src.sourceFile() + "\" -> \"" + sourceFileNameNew + "\""); + builder.with(SourceFileAttribute.of(sourceFileNameNew)); + } else { + builder.with(element); + } } @Override - public void visitEnd() { + public void atEnd(ClassBuilder builder) { if (!sourceSet) { log("Set source: \"" + sourceFileNameNew + "\""); - super.visitSource(sourceFileNameNew, null); + builder.with(SourceFileAttribute.of(sourceFileNameNew)); } - super.visitEnd(); } - }, 0); - return cw.toByteArray(); + }); } private void runTest() throws Throwable { diff --git a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineObject.java b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineObject.java index de90c15b5a5..d3349282410 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineObject.java +++ b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineObject.java @@ -27,7 +27,6 @@ * @summary Ensure Object natives stay registered after redefinition * @requires vm.jvmti * @library /test/lib - * @library /testlibrary/asm * @modules java.base/jdk.internal.misc * java.compiler * java.instrument @@ -41,19 +40,15 @@ import jdk.test.lib.helpers.ClassFileInstaller; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.lang.RuntimeException; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassElement; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassFileVersion; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; -import java.util.Arrays; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; - -import static org.objectweb.asm.Opcodes.ASM6; -import static org.objectweb.asm.Opcodes.V1_8; public class RedefineObject { @@ -69,31 +64,14 @@ public class RedefineObject { Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { - ClassWriter cw = new ClassWriter(0); - // Force an older ASM to force a bytecode update - ClassVisitor cv = new DummyClassVisitor(ASM6, cw) { }; - ClassReader cr = new ClassReader(classfileBuffer); - cr.accept(cv, 0); - byte[] bytes = cw.toByteArray(); - return bytes; - } - - public class DummyClassVisitor extends ClassVisitor { - - public DummyClassVisitor(int api, ClassVisitor cv) { - super(api, cv); - } - - public void visit( - final int version, - final int access, - final String name, - final String signature, - final String superName, - final String[] interfaces) { - // Artificially lower to JDK 8 version to force a redefine - cv.visit(V1_8, access, name, signature, superName, interfaces); - } + return ClassFile.of().transformClass(ClassFile.of().parse(classfileBuffer), (classBuilder, classElement) -> { + if (classElement instanceof ClassFileVersion cfv) { + // Force a redefine with different class file versions + classBuilder.with(ClassFileVersion.of(cfv.majorVersion() - 1, 0)); + } else { + classBuilder.with(classElement); + } + }); } @Override public byte[] transform(ClassLoader loader, String className, diff --git a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineRetransform/RedefineRetransform.java b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineRetransform/RedefineRetransform.java index a09d1dc318e..167b546ac9d 100644 --- a/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineRetransform/RedefineRetransform.java +++ b/test/hotspot/jtreg/serviceability/jvmti/RedefineClasses/RedefineRetransform/RedefineRetransform.java @@ -27,7 +27,6 @@ * @bug 7124710 * * @requires vm.jvmti - * @library /testlibrary/asm * @library /test/lib * * @comment main/othervm/native -Xlog:redefine*=trace -agentlib:RedefineRetransform RedefineRetransform @@ -42,13 +41,17 @@ import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; - -import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; +import java.lang.classfile.Annotation; +import java.lang.classfile.AnnotationElement; +import java.lang.classfile.AnnotationValue; +import java.lang.classfile.Attributes; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.constant.ClassDesc; +import java.util.List; +import java.util.NoSuchElementException; /* * The test verifies that after interleaved RedefineClasses/RetransformClasses calls @@ -81,67 +84,32 @@ public class RedefineRetransform { // Class bytes for initial TestClass (ClassVersion == 0). private static byte[] initialClassBytes; - private static class VersionScanner extends ClassVisitor { - private Integer detectedVersion; - private Integer versionToSet; - // to get version - public VersionScanner() { - super(Opcodes.ASM7); - } - // to set version - public VersionScanner(int verToSet, ClassVisitor classVisitor) { - super(Opcodes.ASM7, classVisitor); - versionToSet = verToSet; - } - - public int detectedVersion() { - if (detectedVersion == null) { - throw new RuntimeException("Version not detected"); - } - return detectedVersion; - } - - @Override - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - //log("visitAnnotation: descr = '" + descriptor + "', visible = " + visible); - if (Type.getDescriptor(ClassVersion.class).equals(descriptor)) { - return new AnnotationVisitor(Opcodes.ASM7, super.visitAnnotation(descriptor, visible)) { - @Override - public void visit(String name, Object value) { - //log("visit: name = '" + name + "', value = " + value - // + " (" + (value == null ? "N/A" : value.getClass()) + ")"); - if ("value".equals(name) && value instanceof Integer intValue) { - detectedVersion = intValue; - if (versionToSet != null) { - //log("replace with " + versionToSet); - value = versionToSet; - } - } - super.visit(name, value); - } - }; - } - return super.visitAnnotation(descriptor, visible); - } - } + private static final ClassDesc CD_ClassVersion = ClassVersion.class.describeConstable().orElseThrow(); // Generates TestClass class bytes with the specified ClassVersion value. private static byte[] getClassBytes(int ver) { if (ver < 0) { return null; } - ClassWriter cw = new ClassWriter(0); - ClassReader cr = new ClassReader(initialClassBytes); - cr.accept(new VersionScanner(ver, cw), 0); - return cw.toByteArray(); + return ClassFile.of().transformClass(ClassFile.of().parse(initialClassBytes), + // overwrites previously passed RVAA + ClassTransform.endHandler(classBuilder -> classBuilder.with(RuntimeVisibleAnnotationsAttribute + .of(Annotation.of(CD_ClassVersion, AnnotationElement.ofInt("value", ver)))))); } // Extracts ClassVersion values from the provided class bytes. private static int getClassBytesVersion(byte[] classBytes) { - ClassReader cr = new ClassReader(classBytes); - VersionScanner scanner = new VersionScanner(); - cr.accept(scanner, 0); - return scanner.detectedVersion(); + ClassModel classModel = ClassFile.of().parse(classBytes); + RuntimeVisibleAnnotationsAttribute rvaa = classModel.findAttribute(Attributes.runtimeVisibleAnnotations()).orElseThrow(); + List classVersionElementValuePairs = rvaa.annotations().stream() + .filter(anno -> anno.className().isFieldType(CD_ClassVersion)) + .findFirst().orElseThrow().elements(); + if (classVersionElementValuePairs.size() != 1) + throw new NoSuchElementException(); + AnnotationElement elementValuePair = classVersionElementValuePairs.getFirst(); + if (!elementValuePair.name().equalsString("value") || !(elementValuePair.value() instanceof AnnotationValue.OfInt intVal)) + throw new NoSuchElementException(); + return intVal.intValue(); } static void init() { diff --git a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/GenClassPoolJar.java b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/GenClassPoolJar.java index cea00fc2efc..511db8b8ed1 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/GenClassPoolJar.java +++ b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/GenClassPoolJar.java @@ -26,6 +26,9 @@ package gc.g1.unloading; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassTransform; +import java.lang.constant.ClassDesc; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; @@ -42,11 +45,6 @@ import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; - /** * Class that imitates shell script to produce jar file with many similar * classes inside. @@ -261,28 +259,9 @@ public class GenClassPoolJar { * @return new class file to write into class */ byte[] morphClass(byte[] classToMorph, String newName) { - ClassReader cr = new ClassReader(classToMorph); - ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); - ClassVisitor cv = new ClassRenamer(cw, newName); - cr.accept(cv, 0); - return cw.toByteArray(); + var context = ClassFile.of(); + return context.transformClass(context.parse(classToMorph), + ClassDesc.ofInternalName(newName), + ClassTransform.ACCEPT_ALL); } - - /** - * Visitor to rename class. - */ - static class ClassRenamer extends ClassVisitor implements Opcodes { - private final String newName; - - public ClassRenamer(ClassVisitor cv, String newName) { - super(ASM4, cv); - this.newName = newName; - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - cv.visit(version, access, newName, signature, superName, interfaces); - } - - } } diff --git a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_cl/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_cl/TestDescription.java index 19b2a940788..5337aa73d2e 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_cl/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_cl/TestDescription.java @@ -30,7 +30,6 @@ * VM Testbase keywords: [gc, stress, stressopt, nonconcurrent, javac] * * @modules java.base/jdk.internal.misc - * @library /testlibrary/asm * @library /vmTestbase * /test/lib * diff --git a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_class/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_class/TestDescription.java index b84411234a5..75d45dda2fe 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_class/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_class/TestDescription.java @@ -30,7 +30,6 @@ * VM Testbase keywords: [gc, stress, stressopt, nonconcurrent, javac] * * @modules java.base/jdk.internal.misc - * @library /testlibrary/asm * @library /vmTestbase * /test/lib * diff --git a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_obj/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_obj/TestDescription.java index f0cc45744c4..aceade65a66 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_obj/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_inMemoryCompilation_keep_obj/TestDescription.java @@ -30,7 +30,6 @@ * VM Testbase keywords: [gc, stress, stressopt, nonconcurrent, javac] * * @modules java.base/jdk.internal.misc - * @library /testlibrary/asm * @library /vmTestbase * /test/lib * diff --git a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_cl/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_cl/TestDescription.java index 48f1680897f..a2547a7556f 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_cl/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_cl/TestDescription.java @@ -30,7 +30,6 @@ * VM Testbase keywords: [gc, stress, stressopt, nonconcurrent, javac] * * @modules java.base/jdk.internal.misc - * @library /testlibrary/asm * @library /vmTestbase * /test/lib * diff --git a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_class/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_class/TestDescription.java index 68263db73ac..d822f65034d 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_class/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_class/TestDescription.java @@ -30,7 +30,6 @@ * VM Testbase keywords: [gc, stress, stressopt, nonconcurrent, javac] * * @modules java.base/jdk.internal.misc - * @library /testlibrary/asm * @library /vmTestbase * /test/lib * diff --git a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_obj/TestDescription.java b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_obj/TestDescription.java index 4ee02a649d3..e875b964f0f 100644 --- a/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_obj/TestDescription.java +++ b/test/hotspot/jtreg/vmTestbase/gc/g1/unloading/tests/unloading_keepRef_rootClass_keep_obj/TestDescription.java @@ -30,7 +30,6 @@ * VM Testbase keywords: [gc, stress, stressopt, nonconcurrent, javac] * * @modules java.base/jdk.internal.misc - * @library /testlibrary/asm * @library /vmTestbase * /test/lib * diff --git a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/GetClassFields/getclfld007.java b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/GetClassFields/getclfld007.java index 6d79e479b97..300966c04dd 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/GetClassFields/getclfld007.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/GetClassFields/getclfld007.java @@ -25,14 +25,12 @@ package nsk.jvmti.GetClassFields; import java.io.PrintStream; import java.io.InputStream; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.FieldModel; import java.util.List; import java.util.ArrayList; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.Opcodes; - public class getclfld007 { @@ -79,44 +77,29 @@ public class getclfld007 { static void check(Class cls) throws Exception { - FieldExplorer explorer = new FieldExplorer(cls); - List fields = explorer.get(); + List fields = getFields(cls); check(cls, fields.toArray(new String[0])); } - // helper class to get list of the class fields - // in the order they appear in the class file - static class FieldExplorer extends ClassVisitor { - private final Class cls; - private List fieldNameAndSig = new ArrayList<>(); - private FieldExplorer(Class cls) { - super(Opcodes.ASM7); - this.cls = cls; - } + private static InputStream getClassBytes(Class cls) throws Exception { + String clsName = cls.getName(); + String clsPath = clsName.replace('.', '/') + ".class"; + return cls.getClassLoader().getResourceAsStream(clsPath); + } - @Override - public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - System.out.println(" field '" + name + "', type = " + descriptor); - fieldNameAndSig.add(name); - fieldNameAndSig.add(descriptor); - return super.visitField(access, name, descriptor, signature, value); - } - - private InputStream getClassBytes() throws Exception { - String clsName = cls.getName(); - String clsPath = clsName.replace('.', '/') + ".class"; - return cls.getClassLoader().getResourceAsStream(clsPath); - } - - // each field is represented by 2 Strings in the list: name and type descriptor - public List get() throws Exception { - System.out.println("Class " + cls.getName()); - try (InputStream classBytes = getClassBytes()) { - ClassReader classReader = new ClassReader(classBytes); - classReader.accept(this, 0); + // get list of the class fields in the order they appear in the class file + // each field is represented by 2 Strings in the list: name and type descriptor + public static List getFields(Class cls) throws Exception { + System.out.println("Class " + cls.getName()); + List fieldNameAndSig = new ArrayList<>(); + try (InputStream classBytes = getClassBytes(cls)) { + ClassModel classModel = ClassFile.of().parse(classBytes.readAllBytes()); + for (FieldModel field : classModel.fields()) { + fieldNameAndSig.add(field.fieldName().stringValue()); + fieldNameAndSig.add(field.fieldType().stringValue()); } - return fieldNameAndSig; } + return fieldNameAndSig; } static class InnerClass1 { From 1392a0b4608f6196f207fcebbab75b2d79fdc758 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Thu, 16 Oct 2025 19:55:07 +0000 Subject: [PATCH 542/556] 8368740: Serial: Swap eden and survivor spaces position in young generation Reviewed-by: gli, fandreuzzi --- .../share/gc/serial/defNewGeneration.cpp | 271 +++++++++--------- .../share/gc/serial/defNewGeneration.hpp | 24 +- src/hotspot/share/gc/serial/serialHeap.cpp | 9 +- src/hotspot/share/gc/serial/serialHeap.hpp | 8 +- src/hotspot/share/gc/shared/space.cpp | 5 +- 5 files changed, 165 insertions(+), 152 deletions(-) diff --git a/src/hotspot/share/gc/serial/defNewGeneration.cpp b/src/hotspot/share/gc/serial/defNewGeneration.cpp index 5b7bc744236..aef896182c0 100644 --- a/src/hotspot/share/gc/serial/defNewGeneration.cpp +++ b/src/hotspot/share/gc/serial/defNewGeneration.cpp @@ -225,16 +225,12 @@ DefNewGeneration::DefNewGeneration(ReservedSpace rs, _promo_failure_drain_in_progress(false), _string_dedup_requests() { - MemRegion cmr((HeapWord*)_virtual_space.low(), - (HeapWord*)_virtual_space.high()); - SerialHeap* gch = SerialHeap::heap(); - - gch->rem_set()->resize_covered_region(cmr); - _eden_space = new ContiguousSpace(); _from_space = new ContiguousSpace(); _to_space = new ContiguousSpace(); + init_spaces(); + // Compute the maximum eden and survivor space sizes. These sizes // are computed assuming the entire reserved space is committed. // These values are exported as performance counters. @@ -256,7 +252,6 @@ DefNewGeneration::DefNewGeneration(ReservedSpace rs, _to_counters = new CSpaceCounters("s1", 2, _max_survivor_size, _to_space, _gen_counters); - compute_space_boundaries(0, SpaceDecorator::Clear, SpaceDecorator::Mangle); update_counters(); _old_gen = nullptr; _tenuring_threshold = MaxTenuringThreshold; @@ -268,74 +263,51 @@ DefNewGeneration::DefNewGeneration(ReservedSpace rs, _gc_tracer = new DefNewTracer(); } -void DefNewGeneration::compute_space_boundaries(uintx minimum_eden_size, - bool clear_space, - bool mangle_space) { - // If the spaces are being cleared (only done at heap initialization - // currently), the survivor spaces need not be empty. - // Otherwise, no care is taken for used areas in the survivor spaces - // so check. - assert(clear_space || (to()->is_empty() && from()->is_empty()), - "Initialization of the survivor spaces assumes these are empty"); +void DefNewGeneration::init_spaces() { + // Using layout: from, to, eden, so only from can be non-empty. + assert(eden()->is_empty(), "precondition"); + assert(to()->is_empty(), "precondition"); + + if (!from()->is_empty()) { + assert((char*) from()->bottom() == _virtual_space.low(), "inv"); + } // Compute sizes - uintx size = _virtual_space.committed_size(); - uintx survivor_size = compute_survivor_size(size, SpaceAlignment); - uintx eden_size = size - (2*survivor_size); - if (eden_size > max_eden_size()) { - // Need to reduce eden_size to satisfy the max constraint. The delta needs - // to be 2*SpaceAlignment aligned so that both survivors are properly - // aligned. - uintx eden_delta = align_up(eden_size - max_eden_size(), 2*SpaceAlignment); - eden_size -= eden_delta; - survivor_size += eden_delta/2; - } + size_t size = _virtual_space.committed_size(); + size_t survivor_size = compute_survivor_size(size, SpaceAlignment); + assert(survivor_size >= from()->used(), "inv"); + assert(size > 2 * survivor_size, "inv"); + size_t eden_size = size - (2 * survivor_size); assert(eden_size > 0 && survivor_size <= eden_size, "just checking"); - if (eden_size < minimum_eden_size) { - // May happen due to 64Kb rounding, if so adjust eden size back up - minimum_eden_size = align_up(minimum_eden_size, SpaceAlignment); - uintx maximum_survivor_size = (size - minimum_eden_size) / 2; - uintx unaligned_survivor_size = - align_down(maximum_survivor_size, SpaceAlignment); - survivor_size = MAX2(unaligned_survivor_size, SpaceAlignment); - eden_size = size - (2*survivor_size); - assert(eden_size > 0 && survivor_size <= eden_size, "just checking"); - assert(eden_size >= minimum_eden_size, "just checking"); - } + // layout: from, to, eden + char* from_start = _virtual_space.low(); + char* to_start = from_start + survivor_size; + char* eden_start = to_start + survivor_size; + char* eden_end = eden_start + eden_size; - char *eden_start = _virtual_space.low(); - char *from_start = eden_start + eden_size; - char *to_start = from_start + survivor_size; - char *to_end = to_start + survivor_size; - - assert(to_end == _virtual_space.high(), "just checking"); - assert(is_aligned(eden_start, SpaceAlignment), "checking alignment"); + assert(eden_end == _virtual_space.high(), "just checking"); assert(is_aligned(from_start, SpaceAlignment), "checking alignment"); assert(is_aligned(to_start, SpaceAlignment), "checking alignment"); + assert(is_aligned(eden_start, SpaceAlignment), "checking alignment"); + assert(is_aligned(eden_end, SpaceAlignment), "checking alignment"); - MemRegion edenMR((HeapWord*)eden_start, (HeapWord*)from_start); MemRegion fromMR((HeapWord*)from_start, (HeapWord*)to_start); - MemRegion toMR ((HeapWord*)to_start, (HeapWord*)to_end); - - // A minimum eden size implies that there is a part of eden that - // is being used and that affects the initialization of any - // newly formed eden. - bool live_in_eden = minimum_eden_size > 0; + MemRegion toMR ((HeapWord*)to_start, (HeapWord*)eden_start); + MemRegion edenMR((HeapWord*)eden_start, (HeapWord*)eden_end); // Reset the spaces for their new regions. - eden()->initialize(edenMR, - clear_space && !live_in_eden, - SpaceDecorator::Mangle); - // If clear_space and live_in_eden, we will not have cleared any - // portion of eden above its top. This can cause newly - // expanded space not to be mangled if using ZapUnusedHeapArea. - // We explicitly do such mangling here. - if (ZapUnusedHeapArea && clear_space && live_in_eden && mangle_space) { - eden()->mangle_unused_area(); - } - from()->initialize(fromMR, clear_space, mangle_space); - to()->initialize(toMR, clear_space, mangle_space); + from()->initialize(fromMR, from()->is_empty(), SpaceDecorator::Mangle); + to()->initialize(toMR, true, SpaceDecorator::Mangle); + eden()->initialize(edenMR, true, SpaceDecorator::Mangle); + + post_resize(); +} + +void DefNewGeneration::post_resize() { + MemRegion cmr((HeapWord*)_virtual_space.low(), + (HeapWord*)_virtual_space.high()); + SerialHeap::heap()->rem_set()->resize_covered_region(cmr); } void DefNewGeneration::swap_spaces() { @@ -351,20 +323,28 @@ void DefNewGeneration::swap_spaces() { } bool DefNewGeneration::expand(size_t bytes) { - HeapWord* prev_high = (HeapWord*) _virtual_space.high(); + assert(bytes != 0, "precondition"); + assert(is_aligned(bytes, SpaceAlignment), "precondition"); + bool success = _virtual_space.expand_by(bytes); - if (success && ZapUnusedHeapArea) { - // Mangle newly committed space immediately because it - // can be done here more simply that after the new - // spaces have been computed. - HeapWord* new_high = (HeapWord*) _virtual_space.high(); - MemRegion mangle_region(prev_high, new_high); - SpaceMangler::mangle_region(mangle_region); + if (!success) { + log_info(gc)("Failed to expand young-gen by %zu bytes", bytes); } return success; } +void DefNewGeneration::expand_eden_by(size_t delta_bytes) { + if (!expand(delta_bytes)) { + return; + } + + MemRegion eden_mr{eden()->bottom(), (HeapWord*)_virtual_space.high()}; + eden()->initialize(eden_mr, eden()->is_empty(), SpaceDecorator::Mangle); + + post_resize(); +} + size_t DefNewGeneration::calculate_thread_increase_size(int threads_count) const { size_t thread_increase_size = 0; // Check an overflow at 'threads_count * NewSizeThreadIncrease'. @@ -397,18 +377,8 @@ size_t DefNewGeneration::adjust_for_thread_increase(size_t new_size_candidate, return desired_new_size; } -void DefNewGeneration::compute_new_size() { - // This is called after a GC that includes the old generation, so from-space - // will normally be empty. - // Note that we check both spaces, since if scavenge failed they revert roles. - // If not we bail out (otherwise we would have to relocate the objects). - if (!from()->is_empty() || !to()->is_empty()) { - return; - } - - SerialHeap* gch = SerialHeap::heap(); - - size_t old_size = gch->old_gen()->capacity(); +size_t DefNewGeneration::calculate_desired_young_gen_bytes() const { + size_t old_size = SerialHeap::heap()->old_gen()->capacity(); size_t new_size_before = _virtual_space.committed_size(); size_t min_new_size = NewSize; size_t max_new_size = reserved().byte_size(); @@ -429,46 +399,82 @@ void DefNewGeneration::compute_new_size() { // Adjust new generation size desired_new_size = clamp(desired_new_size, min_new_size, max_new_size); - assert(desired_new_size <= max_new_size, "just checking"); + if (!from()->is_empty()) { + // Mininum constraint to hold all live objs inside from-space. + size_t min_survivor_size = align_up(from()->used(), alignment); - bool changed = false; - if (desired_new_size > new_size_before) { - size_t change = desired_new_size - new_size_before; - assert(change % alignment == 0, "just checking"); - if (expand(change)) { - changed = true; + // SurvivorRatio := eden_size / survivor_size + // young-gen-size = eden_size + 2 * survivor_size + // = SurvivorRatio * survivor_size + 2 * survivor_size + // = (SurvivorRatio + 2) * survivor_size + size_t min_young_gen_size = min_survivor_size * (SurvivorRatio + 2); + + desired_new_size = MAX2(min_young_gen_size, desired_new_size); + } + assert(is_aligned(desired_new_size, alignment), "postcondition"); + + return desired_new_size; +} + +void DefNewGeneration::resize_inner() { + assert(eden()->is_empty(), "precondition"); + assert(to()->is_empty(), "precondition"); + + size_t current_young_gen_size_bytes = _virtual_space.committed_size(); + size_t desired_young_gen_size_bytes = calculate_desired_young_gen_bytes(); + if (current_young_gen_size_bytes == desired_young_gen_size_bytes) { + return; + } + + // Commit/uncommit + if (desired_young_gen_size_bytes > current_young_gen_size_bytes) { + size_t delta_bytes = desired_young_gen_size_bytes - current_young_gen_size_bytes; + if (!expand(delta_bytes)) { + return; } - // If the heap failed to expand to the desired size, - // "changed" will be false. If the expansion failed - // (and at this point it was expected to succeed), - // ignore the failure (leaving "changed" as false). + } else { + size_t delta_bytes = current_young_gen_size_bytes - desired_young_gen_size_bytes; + _virtual_space.shrink_by(delta_bytes); } - if (desired_new_size < new_size_before && eden()->is_empty()) { - // bail out of shrinking if objects in eden - size_t change = new_size_before - desired_new_size; - assert(change % alignment == 0, "just checking"); - _virtual_space.shrink_by(change); - changed = true; - } - if (changed) { - // The spaces have already been mangled at this point but - // may not have been cleared (set top = bottom) and should be. - // Mangling was done when the heap was being expanded. - compute_space_boundaries(eden()->used(), - SpaceDecorator::Clear, - SpaceDecorator::DontMangle); - MemRegion cmr((HeapWord*)_virtual_space.low(), - (HeapWord*)_virtual_space.high()); - gch->rem_set()->resize_covered_region(cmr); - log_debug(gc, ergo, heap)( - "New generation size %zuK->%zuK [eden=%zuK,survivor=%zuK]", - new_size_before/K, _virtual_space.committed_size()/K, - eden()->capacity()/K, from()->capacity()/K); - log_trace(gc, ergo, heap)( - " [allowed %zuK extra for %d threads]", - thread_increase_size/K, threads_count); - } + assert(desired_young_gen_size_bytes == _virtual_space.committed_size(), "inv"); + + init_spaces(); + + log_debug(gc, ergo, heap)("New generation size %zuK->%zuK [eden=%zuK,survivor=%zuK]", + current_young_gen_size_bytes/K, _virtual_space.committed_size()/K, + eden()->capacity()/K, from()->capacity()/K); +} + +void DefNewGeneration::resize_after_young_gc() { + // Called only after successful young-gc. + assert(eden()->is_empty(), "precondition"); + assert(to()->is_empty(), "precondition"); + + if ((char*)to()->bottom() == _virtual_space.low()) { + // layout: to, from, eden; can't resize. + return; + } + + assert((char*)from()->bottom() == _virtual_space.low(), "inv"); + resize_inner(); +} + +void DefNewGeneration::resize_after_full_gc() { + if (eden()->is_empty() && from()->is_empty() && to()->is_empty()) { + resize_inner(); + return; + } + + // Usually the young-gen is empty after full-gc. + // This is the extreme case; expand young-gen to its max size. + if (_virtual_space.uncommitted_size() == 0) { + // Already at its max size. + return; + } + + // Keep from/to and expand eden. + expand_eden_by(_virtual_space.uncommitted_size()); } void DefNewGeneration::ref_processor_init() { @@ -483,13 +489,11 @@ size_t DefNewGeneration::capacity() const { + from()->capacity(); // to() is only used during scavenge } - size_t DefNewGeneration::used() const { return eden()->used() + from()->used(); // to() is only used during scavenge } - size_t DefNewGeneration::free() const { return eden()->free() + from()->free(); // to() is only used during scavenge @@ -497,7 +501,8 @@ size_t DefNewGeneration::free() const { size_t DefNewGeneration::max_capacity() const { const size_t reserved_bytes = reserved().byte_size(); - return reserved_bytes - compute_survivor_size(reserved_bytes, SpaceAlignment); + const size_t min_survivor_bytes = SpaceAlignment; + return reserved_bytes - min_survivor_bytes; } bool DefNewGeneration::is_in(const void* p) const { @@ -589,7 +594,6 @@ bool DefNewGeneration::collect(bool clear_all_soft_refs) { IsAliveClosure is_alive(this); age_table()->clear(); - to()->clear(SpaceDecorator::Mangle); YoungGenScanClosure young_gen_cl(this); OldGenScanClosure old_gen_cl(this); @@ -839,13 +843,18 @@ void DefNewGeneration::print_on(outputStream* st) const { to()->print_on(st, "to "); } -HeapWord* DefNewGeneration::allocate(size_t word_size) { - // This is the slow-path allocation for the DefNewGeneration. - // Most allocations are fast-path in compiled code. - // We try to allocate from the eden. If that works, we are happy. - // Note that since DefNewGeneration supports lock-free allocation, we - // have to use it here, as well. - HeapWord* result = eden()->par_allocate(word_size); +HeapWord* DefNewGeneration::expand_and_allocate(size_t word_size) { + assert(SafepointSynchronize::is_at_safepoint(), "precondition"); + assert(Thread::current()->is_VM_thread(), "precondition"); + + size_t eden_free_bytes = eden()->free(); + size_t requested_bytes = word_size * HeapWordSize; + if (eden_free_bytes < requested_bytes) { + size_t expand_bytes = requested_bytes - eden_free_bytes; + expand_eden_by(align_up(expand_bytes, SpaceAlignment)); + } + + HeapWord* result = eden()->allocate(word_size); return result; } diff --git a/src/hotspot/share/gc/serial/defNewGeneration.hpp b/src/hotspot/share/gc/serial/defNewGeneration.hpp index 32b6b32f42f..40d2116cb58 100644 --- a/src/hotspot/share/gc/serial/defNewGeneration.hpp +++ b/src/hotspot/share/gc/serial/defNewGeneration.hpp @@ -131,6 +131,13 @@ class DefNewGeneration: public Generation { return n > alignment ? align_down(n, alignment) : alignment; } + size_t calculate_desired_young_gen_bytes() const; + + void expand_eden_by(size_t delta_bytes); + + void resize_inner(); + void post_resize(); + public: DefNewGeneration(ReservedSpace rs, size_t initial_byte_size, @@ -183,9 +190,8 @@ class DefNewGeneration: public Generation { HeapWord* block_start(const void* p) const; - // Allocate requested size or return null; single-threaded and lock-free versions. - HeapWord* allocate(size_t word_size); HeapWord* par_allocate(size_t word_size); + HeapWord* expand_and_allocate(size_t word_size); void gc_epilogue(); @@ -196,8 +202,8 @@ class DefNewGeneration: public Generation { // Reset for contribution of "to-space". void reset_scratch(); - // GC support - void compute_new_size(); + void resize_after_young_gc(); + void resize_after_full_gc(); bool collect(bool clear_all_soft_refs); @@ -220,13 +226,9 @@ class DefNewGeneration: public Generation { DefNewTracer* gc_tracer() const { return _gc_tracer; } - protected: - // If clear_space is true, clear the survivor spaces. Eden is - // cleared if the minimum size of eden is 0. If mangle_space - // is true, also mangle the space in debug mode. - void compute_space_boundaries(uintx minimum_eden_size, - bool clear_space, - bool mangle_space); + private: + // Initialize eden/from/to spaces. + void init_spaces(); // Return adjusted new size for NewSizeThreadIncrease. // If any overflow happens, revert to previous new size. diff --git a/src/hotspot/share/gc/serial/serialHeap.cpp b/src/hotspot/share/gc/serial/serialHeap.cpp index 3ab88da4633..3511318e169 100644 --- a/src/hotspot/share/gc/serial/serialHeap.cpp +++ b/src/hotspot/share/gc/serial/serialHeap.cpp @@ -269,9 +269,9 @@ size_t SerialHeap::max_capacity() const { } HeapWord* SerialHeap::expand_heap_and_allocate(size_t size, bool is_tlab) { - HeapWord* result = _young_gen->allocate(size); + HeapWord* result = _young_gen->expand_and_allocate(size); - if (result == nullptr) { + if (result == nullptr && !is_tlab) { result = _old_gen->expand_and_allocate(size); } @@ -388,14 +388,13 @@ bool SerialHeap::do_young_collection(bool clear_soft_refs) { // Only update stats for successful young-gc if (result) { _old_gen->update_promote_stats(); + _young_gen->resize_after_young_gc(); } if (should_verify && VerifyAfterGC) { Universe::verify("After GC"); } - _young_gen->compute_new_size(); - print_heap_change(pre_gc_values); // Track memory usage and detect low memory after GC finishes @@ -581,7 +580,7 @@ void SerialHeap::do_full_collection(bool clear_all_soft_refs) { // Adjust generation sizes. _old_gen->compute_new_size(); - _young_gen->compute_new_size(); + _young_gen->resize_after_full_gc(); _old_gen->update_promote_stats(); diff --git a/src/hotspot/share/gc/serial/serialHeap.hpp b/src/hotspot/share/gc/serial/serialHeap.hpp index 27053b4cc81..3915f8c4af9 100644 --- a/src/hotspot/share/gc/serial/serialHeap.hpp +++ b/src/hotspot/share/gc/serial/serialHeap.hpp @@ -55,10 +55,10 @@ class TenuredGeneration; // +-- generation boundary (fixed after startup) // | // |<- young gen (reserved MaxNewSize) ->|<- old gen (reserved MaxOldSize) ->| -// +-----------------+--------+--------+--------+---------------+-------------------+ -// | eden | from | to | | old | | -// | | (to) | (from) | | | | -// +-----------------+--------+--------+--------+---------------+-------------------+ +// +--------+--------+-----------------+--------+---------------+-------------------+ +// | from | to | eden | | old | | +// | (to) | (from) | | | | | +// +--------+--------+-----------------+--------+---------------+-------------------+ // |<- committed ->| |<- committed ->| // class SerialHeap : public CollectedHeap { diff --git a/src/hotspot/share/gc/shared/space.cpp b/src/hotspot/share/gc/shared/space.cpp index 08476cb2a3a..1d15fbc3fa9 100644 --- a/src/hotspot/share/gc/shared/space.cpp +++ b/src/hotspot/share/gc/shared/space.cpp @@ -53,7 +53,10 @@ void ContiguousSpace::initialize(MemRegion mr, set_bottom(bottom); set_end(end); if (clear_space) { - clear(mangle_space); + clear(SpaceDecorator::DontMangle); + } + if (ZapUnusedHeapArea && mangle_space) { + mangle_unused_area(); } } From 18fd04770294e27011bd576b5ea5fe43fa03e5e3 Mon Sep 17 00:00:00 2001 From: Justin King Date: Thu, 16 Oct 2025 19:59:13 +0000 Subject: [PATCH 543/556] 8369506: Bytecode rewriting causes Java heap corruption on AArch64 Co-authored-by: Man Cao Co-authored-by: Chuck Rasbold Reviewed-by: shade, aph, manc --- src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp | 11 +++++++++++ src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp | 2 ++ src/hotspot/cpu/aarch64/templateTable_aarch64.cpp | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp b/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp index 607912e6e49..6f8795494a2 100644 --- a/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp @@ -1704,3 +1704,14 @@ void InterpreterMacroAssembler::load_method_entry(Register cache, Register index add(cache, cache, Array::base_offset_in_bytes()); lea(cache, Address(cache, index)); } + +#ifdef ASSERT +void InterpreterMacroAssembler::verify_field_offset(Register reg) { + // Verify the field offset is not in the header, implicitly checks for 0 + Label L; + subs(zr, reg, oopDesc::base_offset_in_bytes()); + br(Assembler::GE, L); + stop("bad field offset"); + bind(L); +} +#endif diff --git a/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp b/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp index e896a2a9430..e07e6e49f53 100644 --- a/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp @@ -319,6 +319,8 @@ class InterpreterMacroAssembler: public MacroAssembler { void load_resolved_indy_entry(Register cache, Register index); void load_field_entry(Register cache, Register index, int bcp_offset = 1); void load_method_entry(Register cache, Register index, int bcp_offset = 1); + + void verify_field_offset(Register reg) NOT_DEBUG_RETURN; }; #endif // CPU_AARCH64_INTERP_MASM_AARCH64_HPP diff --git a/src/hotspot/cpu/aarch64/templateTable_aarch64.cpp b/src/hotspot/cpu/aarch64/templateTable_aarch64.cpp index 5195432f54e..f4774f31bbd 100644 --- a/src/hotspot/cpu/aarch64/templateTable_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/templateTable_aarch64.cpp @@ -168,6 +168,7 @@ void TemplateTable::patch_bytecode(Bytecodes::Code bc, Register bc_reg, Register temp_reg, bool load_bc_into_bc_reg/*=true*/, int byte_no) { + assert_different_registers(bc_reg, temp_reg); if (!RewriteBytecodes) return; Label L_patch_done; @@ -231,9 +232,12 @@ void TemplateTable::patch_bytecode(Bytecodes::Code bc, Register bc_reg, __ stop("patching the wrong bytecode"); __ bind(L_okay); #endif - - // patch bytecode - __ strb(bc_reg, at_bcp(0)); + // Patch bytecode with release store to coordinate with ResolvedFieldEntry loads + // in fast bytecode codelets. load_field_entry has a memory barrier that gains + // the needed ordering, together with control dependency on entering the fast codelet + // itself. + __ lea(temp_reg, at_bcp(0)); + __ stlrb(bc_reg, temp_reg); __ bind(L_patch_done); } @@ -3094,6 +3098,7 @@ void TemplateTable::fast_storefield(TosState state) // R1: field offset, R2: field holder, R5: flags load_resolved_field_entry(r2, r2, noreg, r1, r5); + __ verify_field_offset(r1); { Label notVolatile; @@ -3183,6 +3188,8 @@ void TemplateTable::fast_accessfield(TosState state) __ load_field_entry(r2, r1); __ load_sized_value(r1, Address(r2, in_bytes(ResolvedFieldEntry::field_offset_offset())), sizeof(int), true /*is_signed*/); + __ verify_field_offset(r1); + __ load_unsigned_byte(r3, Address(r2, in_bytes(ResolvedFieldEntry::flags_offset()))); // r0: object @@ -3249,7 +3256,9 @@ void TemplateTable::fast_xaccess(TosState state) __ ldr(r0, aaddress(0)); // access constant pool cache __ load_field_entry(r2, r3, 2); + __ load_sized_value(r1, Address(r2, in_bytes(ResolvedFieldEntry::field_offset_offset())), sizeof(int), true /*is_signed*/); + __ verify_field_offset(r1); // 8179954: We need to make sure that the code generated for // volatile accesses forms a sequentially-consistent set of From 0c1c86e68efcc140cefbde89b4d1d8708e931528 Mon Sep 17 00:00:00 2001 From: Patricio Chilano Mateo Date: Thu, 16 Oct 2025 21:20:42 +0000 Subject: [PATCH 544/556] 8370036: TestJhsdbJstackWithVirtualThread.java fails when run with -showversion Reviewed-by: ayang, cjplummer --- .../serviceability/sa/TestJhsdbJstackWithVirtualThread.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java b/test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java index fce9906ca94..acea00a190b 100644 --- a/test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java +++ b/test/hotspot/jtreg/serviceability/sa/TestJhsdbJstackWithVirtualThread.java @@ -45,7 +45,7 @@ public class TestJhsdbJstackWithVirtualThread { private static void runJstack(LingeredApp app) throws Exception { JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jhsdb"); - launcher.addVMArgs(Utils.getTestJavaOpts()); + launcher.addVMArgs(Utils.getFilteredTestJavaOpts("-showversion")); launcher.addToolArg("jstack"); launcher.addToolArg("--pid"); launcher.addToolArg(Long.toString(app.getPid())); From 0bdd6f0640fc25667f911228eed6a0fa118e8ff8 Mon Sep 17 00:00:00 2001 From: Francesco Andreuzzi Date: Thu, 16 Oct 2025 22:04:40 +0000 Subject: [PATCH 545/556] 8369734: JvmtiExport::post_class_file_load_hook return value is never used Reviewed-by: dholmes, sspitsyn --- src/hotspot/share/prims/jvmtiExport.cpp | 12 +++--------- src/hotspot/share/prims/jvmtiExport.hpp | 5 ++--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/hotspot/share/prims/jvmtiExport.cpp b/src/hotspot/share/prims/jvmtiExport.cpp index fa6ede86cd9..0884fce2ff7 100644 --- a/src/hotspot/share/prims/jvmtiExport.cpp +++ b/src/hotspot/share/prims/jvmtiExport.cpp @@ -879,7 +879,6 @@ class JvmtiClassFileLoadHookPoster : public StackObj { JvmtiThreadState * _state; Klass* _class_being_redefined; JvmtiClassLoadKind _load_kind; - bool _has_been_modified; public: inline JvmtiClassFileLoadHookPoster(Symbol* h_name, Handle class_loader, @@ -896,7 +895,6 @@ class JvmtiClassFileLoadHookPoster : public StackObj { _curr_data = *data_ptr; _curr_env = nullptr; _cached_class_file_ptr = cache_ptr; - _has_been_modified = false; _state = JvmtiExport::get_jvmti_thread_state(_thread); if (_state != nullptr) { @@ -935,8 +933,6 @@ class JvmtiClassFileLoadHookPoster : public StackObj { copy_modified_data(); } - bool has_been_modified() { return _has_been_modified; } - private: void post_all_envs() { if (_load_kind != jvmti_class_load_kind_retransform) { @@ -983,7 +979,6 @@ class JvmtiClassFileLoadHookPoster : public StackObj { } if (new_data != nullptr) { // this agent has modified class data. - _has_been_modified = true; if (caching_needed && *_cached_class_file_ptr == nullptr) { // data has been changed by the new retransformable agent // and it hasn't already been cached, cache it @@ -1058,18 +1053,18 @@ bool JvmtiExport::_should_post_class_file_load_hook = false; int JvmtiExport::_should_notify_object_alloc = 0; // this entry is for class file load hook on class load, redefine and retransform -bool JvmtiExport::post_class_file_load_hook(Symbol* h_name, +void JvmtiExport::post_class_file_load_hook(Symbol* h_name, Handle class_loader, Handle h_protection_domain, unsigned char **data_ptr, unsigned char **end_ptr, JvmtiCachedClassFileData **cache_ptr) { if (JvmtiEnv::get_phase() < JVMTI_PHASE_PRIMORDIAL) { - return false; + return; } if (JavaThread::current()->should_hide_jvmti_events()) { - return false; + return; } JvmtiClassFileLoadHookPoster poster(h_name, class_loader, @@ -1077,7 +1072,6 @@ bool JvmtiExport::post_class_file_load_hook(Symbol* h_name, data_ptr, end_ptr, cache_ptr); poster.post(); - return poster.has_been_modified(); } void JvmtiExport::report_unsupported(bool on) { diff --git a/src/hotspot/share/prims/jvmtiExport.hpp b/src/hotspot/share/prims/jvmtiExport.hpp index 062057c70ab..8906d6b81df 100644 --- a/src/hotspot/share/prims/jvmtiExport.hpp +++ b/src/hotspot/share/prims/jvmtiExport.hpp @@ -377,11 +377,10 @@ class JvmtiExport : public AllStatic { static bool is_early_phase() NOT_JVMTI_RETURN_(false); static bool has_early_class_hook_env() NOT_JVMTI_RETURN_(false); static bool has_early_vmstart_env() NOT_JVMTI_RETURN_(false); - // Return true if the class was modified by the hook. - static bool post_class_file_load_hook(Symbol* h_name, Handle class_loader, + static void post_class_file_load_hook(Symbol* h_name, Handle class_loader, Handle h_protection_domain, unsigned char **data_ptr, unsigned char **end_ptr, - JvmtiCachedClassFileData **cache_ptr) NOT_JVMTI_RETURN_(false); + JvmtiCachedClassFileData **cache_ptr) NOT_JVMTI_RETURN; static void post_native_method_bind(Method* method, address* function_ptr) NOT_JVMTI_RETURN; static void post_compiled_method_load(JvmtiEnv* env, nmethod *nm) NOT_JVMTI_RETURN; static void post_compiled_method_load(nmethod *nm) NOT_JVMTI_RETURN; From 4d20f7696c015bc0e59544ff064fe0c640d61edf Mon Sep 17 00:00:00 2001 From: William Kemper Date: Fri, 17 Oct 2025 00:15:37 +0000 Subject: [PATCH 546/556] 8370050: Shenandoah: Obsolete ShenandoahPacing option Reviewed-by: ysr --- src/hotspot/share/runtime/arguments.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hotspot/share/runtime/arguments.cpp b/src/hotspot/share/runtime/arguments.cpp index 8b703cb442a..0d9973a1b09 100644 --- a/src/hotspot/share/runtime/arguments.cpp +++ b/src/hotspot/share/runtime/arguments.cpp @@ -548,6 +548,7 @@ static SpecialFlag const special_jvm_flags[] = { { "ZGenerational", JDK_Version::jdk(23), JDK_Version::jdk(24), JDK_Version::undefined() }, { "ZMarkStackSpaceLimit", JDK_Version::undefined(), JDK_Version::jdk(25), JDK_Version::undefined() }, { "G1UpdateBufferSize", JDK_Version::undefined(), JDK_Version::jdk(26), JDK_Version::jdk(27) }, + { "ShenandoahPacing", JDK_Version::jdk(25), JDK_Version::jdk(26), JDK_Version::jdk(27) }, #if defined(AARCH64) { "NearCpool", JDK_Version::undefined(), JDK_Version::jdk(25), JDK_Version::undefined() }, #endif From bd7315648f2bb18cba9cfbeca00e6132b8eb95ef Mon Sep 17 00:00:00 2001 From: Ioi Lam Date: Fri, 17 Oct 2025 00:36:54 +0000 Subject: [PATCH 547/556] 8369856: AOT map does not include unregistered classes Co-authored-by: Ashutosh Mehra Reviewed-by: kvn, matsaave --- .../classfile/systemDictionaryShared.cpp | 4 + .../{CDSMapReader.java => AOTMapReader.java} | 77 +++++++++++++++++-- .../cds/{CDSMapTest.java => AOTMapTest.java} | 12 +-- .../cds/appcds/aotCache/AOTMapTest.java | 55 +++++++++---- 4 files changed, 120 insertions(+), 28 deletions(-) rename test/hotspot/jtreg/runtime/cds/{CDSMapReader.java => AOTMapReader.java} (81%) rename test/hotspot/jtreg/runtime/cds/{CDSMapTest.java => AOTMapTest.java} (92%) diff --git a/src/hotspot/share/classfile/systemDictionaryShared.cpp b/src/hotspot/share/classfile/systemDictionaryShared.cpp index b092e71f4e7..2d31a7c49f6 100644 --- a/src/hotspot/share/classfile/systemDictionaryShared.cpp +++ b/src/hotspot/share/classfile/systemDictionaryShared.cpp @@ -1420,6 +1420,10 @@ void SystemDictionaryShared::get_all_archived_classes(bool is_static_archive, Gr get_archive(is_static_archive)->_builtin_dictionary.iterate([&] (const RunTimeClassInfo* record) { classes->append(record->klass()); }); + + get_archive(is_static_archive)->_unregistered_dictionary.iterate([&] (const RunTimeClassInfo* record) { + classes->append(record->klass()); + }); } class SharedDictionaryPrinter : StackObj { diff --git a/test/hotspot/jtreg/runtime/cds/CDSMapReader.java b/test/hotspot/jtreg/runtime/cds/AOTMapReader.java similarity index 81% rename from test/hotspot/jtreg/runtime/cds/CDSMapReader.java rename to test/hotspot/jtreg/runtime/cds/AOTMapReader.java index f25455b2f03..e407d4e2ecc 100644 --- a/test/hotspot/jtreg/runtime/cds/CDSMapReader.java +++ b/test/hotspot/jtreg/runtime/cds/AOTMapReader.java @@ -26,6 +26,7 @@ import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -33,7 +34,7 @@ import java.util.regex.Pattern; This is a simple parser for parsing the output of - java -Xshare:dump -Xlog:aot+map=debug,aot+map+oops=trace:file=cds.map:none:filesize=0 + java -Xshare:dump -Xlog:aot+map=debug,aot+map+oops=trace:file=aot.map:none:filesize=0 The map file contains patterns like this for the heap objects: @@ -59,8 +60,9 @@ more analysis on the HeapObjects. */ -public class CDSMapReader { +public class AOTMapReader { public static class MapFile { + HashSet classes = new HashSet<>(); ArrayList heapObjects = new ArrayList<>(); HashMap oopToObject = new HashMap<>(); HashMap narrowOopToObject = new HashMap<>(); @@ -80,6 +82,20 @@ public class CDSMapReader { public int heapObjectCount() { return heapObjects.size(); } + + void addClass(String className) { + classes.add(className); + } + + public boolean hasClass(String className) { + return classes.contains(className); + } + + public void shouldHaveClass(String className) { + if (!hasClass(className)) { + throw new RuntimeException("AOT map file is missing class " + className); + } + } } public static class HeapAddress { @@ -140,13 +156,17 @@ public class CDSMapReader { this.name = name; this.offset = Integer.parseInt(offset); this.referentAddress = new HeapAddress(oopStr, narrowOopStr); - this.lineCount = CDSMapReader.lineCount; + this.lineCount = AOTMapReader.lineCount; } } // 0x00000007ffc00000: 4a5b8701 00000063 00010290 00000000 00010100 fff80003 static Pattern rawDataPattern = Pattern.compile("^0x([0-9a-f]+): *( [0-9a-f]+)+ *$"); + // ------------------------------------------------------------------------------- + // Patterns for heap objects + // ------------------------------------------------------------------------------- + // (one address) // 0x00000007ffc00000: @@ Object java.lang.String static Pattern objPattern1 = Pattern.compile("^0x([0-9a-f]+): @@ Object ([^ ]*)"); @@ -179,6 +199,15 @@ public class CDSMapReader { // - injected 'module_entry' 'J' @16 0 (0x0000000000000000) static Pattern moduleEntryPattern = Pattern.compile("- injected 'module_entry' 'J' @[0-9]+[ ]+([0-9]+)"); + // ------------------------------------------------------------------------------- + // Patterns for metaspace objects + // ------------------------------------------------------------------------------- + + // 0x00000008000d1698: @@ Class 512 [Ljdk.internal.vm.FillerElement; + // 0x00000008000d18a0: @@ Class 520 java.lang.Cloneable + static Pattern classPattern = Pattern.compile("^0x([0-9a-f]+): @@ Class [ ]*([0-9]+) (.*)"); + + private static Matcher match(String line, Pattern pattern) { Matcher m = pattern.matcher(line); if (m.find()) { @@ -253,6 +282,11 @@ public class CDSMapReader { } } + private static void parseClassObject(String className, String addr, String size) throws IOException { + mapFile.addClass(className); + nextLine(); + } + static MapFile mapFile; static BufferedReader reader; static String line = null; // current line being parsed @@ -277,6 +311,8 @@ public class CDSMapReader { parseHeapObject(m.group(3), m.group(1), m.group(2)); } else if ((m = match(line, objPattern1)) != null) { parseHeapObject(m.group(2), m.group(1), null); + } else if ((m = match(line, classPattern)) != null) { + parseClassObject(m.group(3), m.group(1), m.group(2)); // name, addr, size } else { nextLine(); } @@ -303,8 +339,15 @@ public class CDSMapReader { } } + public static void validate(MapFile mapFile, String classLoadLogFile) throws IOException { + validateOops(mapFile); + if (classLoadLogFile != null) { + validateClasses(mapFile, classLoadLogFile); + } + } + // Check that each oop fields in the HeapObjects must point to a valid HeapObject. - public static void validate(MapFile mapFile) { + static void validateOops(MapFile mapFile) { int count1 = 0; int count2 = 0; for (HeapObject heapObject : mapFile.heapObjects) { @@ -333,10 +376,10 @@ public class CDSMapReader { if (mapFile.heapObjectCount() > 0) { // heapObjectCount() may be zero if the selected GC doesn't support heap object archiving. if (mapFile.stringCount <= 0) { - throw new RuntimeException("CDS map file should contain at least one string"); + throw new RuntimeException("AOT map file should contain at least one string"); } if (count1 < mapFile.stringCount) { - throw new RuntimeException("CDS map file seems incorrect: " + mapFile.heapObjectCount() + + throw new RuntimeException("AOT map file seems incorrect: " + mapFile.heapObjectCount() + " objects (" + mapFile.stringCount + " strings). Each string should" + " have one non-null oop field but we found only " + count1 + " non-null oop field references"); @@ -344,8 +387,26 @@ public class CDSMapReader { } } - public static void main(String args[]) { + // classLoadLogFile should be generated with -Xlog:class+load:file=:none:filesize=0 + // Check that every class loaded from "source: shared objects file" have an entry inside the mapFile. + static void validateClasses(MapFile mapFile, String classLoadLogFile) throws IOException { + try (BufferedReader r = new BufferedReader(new FileReader(classLoadLogFile))) { + String line; + String suffix = " source: shared objects file"; + int suffixLen = suffix.length(); + while ((line = r.readLine()) != null) { + if (line.endsWith(suffix)) { + String className = line.substring(0, line.length() - suffixLen); + if (!mapFile.hasClass(className)) { + throw new RuntimeException("AOT map file is missing class " + className); + } + } + } + } + } + + public static void main(String args[]) throws IOException { MapFile mapFile = read(args[0]); - validate(mapFile); + validate(mapFile, null); } } diff --git a/test/hotspot/jtreg/runtime/cds/CDSMapTest.java b/test/hotspot/jtreg/runtime/cds/AOTMapTest.java similarity index 92% rename from test/hotspot/jtreg/runtime/cds/CDSMapTest.java rename to test/hotspot/jtreg/runtime/cds/AOTMapTest.java index 5a9fa82552b..dff98090859 100644 --- a/test/hotspot/jtreg/runtime/cds/CDSMapTest.java +++ b/test/hotspot/jtreg/runtime/cds/AOTMapTest.java @@ -27,7 +27,7 @@ * @summary Test the contents of -Xlog:aot+map * @requires vm.cds * @library /test/lib - * @run driver/timeout=240 CDSMapTest + * @run driver/timeout=240 AOTMapTest */ import jdk.test.lib.cds.CDSOptions; @@ -37,7 +37,7 @@ import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; import java.util.ArrayList; -public class CDSMapTest { +public class AOTMapTest { public static void main(String[] args) throws Exception { doTest(false); @@ -79,8 +79,8 @@ public class CDSMapTest { .addSuffix(args); CDSTestUtils.createArchiveAndCheck(opts); - CDSMapReader.MapFile mapFile = CDSMapReader.read(mapName); - CDSMapReader.validate(mapFile); + AOTMapReader.MapFile mapFile = AOTMapReader.read(mapName); + AOTMapReader.validate(mapFile, null); return archiveName; } @@ -98,7 +98,7 @@ public class CDSMapTest { OutputAnalyzer out = CDSTestUtils.executeAndLog(pb, "exec"); out.shouldHaveExitValue(0); - CDSMapReader.MapFile mapFile = CDSMapReader.read(mapName); - CDSMapReader.validate(mapFile); + AOTMapReader.MapFile mapFile = AOTMapReader.read(mapName); + AOTMapReader.validate(mapFile, null); } } diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/AOTMapTest.java b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/AOTMapTest.java index bcd2c71fea0..6cbfcbbd3c3 100644 --- a/test/hotspot/jtreg/runtime/cds/appcds/aotCache/AOTMapTest.java +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotCache/AOTMapTest.java @@ -26,9 +26,10 @@ * @bug 8362566 * @summary Test the contents of -Xlog:aot+map with AOT workflow * @requires vm.cds.supports.aot.class.linking - * @library /test/lib /test/hotspot/jtreg/runtime/cds - * @build AOTMapTest + * @library /test/lib /test/hotspot/jtreg/runtime/cds /test/hotspot/jtreg/runtime/cds/appcds/test-classes + * @build AOTMapTest Hello * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar AOTMapTestApp + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar cust.jar Hello * @run driver/timeout=240 AOTMapTest AOT --two-step-training */ @@ -37,15 +38,18 @@ * @bug 8362566 * @summary Test the contents of -Xlog:aot+map with dynamic CDS archive * @requires vm.cds.supports.aot.class.linking - * @library /test/lib /test/hotspot/jtreg/runtime/cds + * @library /test/lib /test/hotspot/jtreg/runtime/cds /test/hotspot/jtreg/runtime/cds/appcds/test-classes * @build jdk.test.whitebox.WhiteBox * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox - * @build AOTMapTest + * @build AOTMapTest Hello * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar AOTMapTestApp + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar cust.jar Hello * @run main/othervm/timeout=240 -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. AOTMapTest DYNAMIC */ - +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; import java.util.ArrayList; import jdk.test.lib.cds.CDSAppTester; import jdk.test.lib.helpers.ClassFileInstaller; @@ -54,6 +58,7 @@ import jdk.test.lib.Platform; public class AOTMapTest { static final String appJar = ClassFileInstaller.getJarPath("app.jar"); static final String mainClass = "AOTMapTestApp"; + static final String classLoadLogFile = "production.class.load.log"; public static void main(String[] args) throws Exception { doTest(args); @@ -63,13 +68,25 @@ public class AOTMapTest { Tester tester = new Tester(); tester.run(args); - validate(tester.dumpMapFile); - validate(tester.runMapFile); + if (tester.isDynamicWorkflow()) { + // For dynamic workflow, the AOT map file doesn't include classes in the base archive, so + // AOTMapReader.validateClasses() will fail. + validate(tester.dumpMapFile, false); + } else { + validate(tester.dumpMapFile, true); + } + validate(tester.runMapFile, true); } - static void validate(String mapFileName) { - CDSMapReader.MapFile mapFile = CDSMapReader.read(mapFileName); - CDSMapReader.validate(mapFile); + static void validate(String mapFileName, boolean checkClases) throws Exception { + AOTMapReader.MapFile mapFile = AOTMapReader.read(mapFileName); + if (checkClases) { + AOTMapReader.validate(mapFile, classLoadLogFile); + } else { + AOTMapReader.validate(mapFile, null); + } + mapFile.shouldHaveClass("AOTMapTestApp"); // built-in class + mapFile.shouldHaveClass("Hello"); // unregistered class } static class Tester extends CDSAppTester { @@ -97,12 +114,13 @@ public class AOTMapTest { // filesize=0 ensures that a large map file not broken up in multiple files. String logMapPrefix = "-Xlog:aot+map=debug,aot+map+oops=trace:file="; - String logMapSuffix = ":none:filesize=0"; + String logSuffix = ":none:filesize=0"; if (runMode == RunMode.ASSEMBLY || runMode == RunMode.DUMP_DYNAMIC) { - vmArgs.add(logMapPrefix + dumpMapFile + logMapSuffix); + vmArgs.add(logMapPrefix + dumpMapFile + logSuffix); } else if (runMode == RunMode.PRODUCTION) { - vmArgs.add(logMapPrefix + runMapFile + logMapSuffix); + vmArgs.add(logMapPrefix + runMapFile + logSuffix); + vmArgs.add("-Xlog:class+load:file=" + classLoadLogFile + logSuffix); } return vmArgs.toArray(new String[vmArgs.size()]); @@ -118,7 +136,16 @@ public class AOTMapTest { } class AOTMapTestApp { - public static void main(String[] args) { + public static void main(String[] args) throws Exception { System.out.println("Hello AOTMapTestApp"); + testCustomLoader(); + } + + static void testCustomLoader() throws Exception { + File custJar = new File("cust.jar"); + URL[] urls = new URL[] {custJar.toURI().toURL()}; + URLClassLoader loader = new URLClassLoader(urls, AOTMapTestApp.class.getClassLoader()); + Class c = loader.loadClass("Hello"); + System.out.println(c); } } From 55787fe5f52544ea902cac35f1f552e26d954167 Mon Sep 17 00:00:00 2001 From: Prasanta Sadhukhan Date: Fri, 17 Oct 2025 01:31:39 +0000 Subject: [PATCH 548/556] 8342401: [TESTBUG] javax/swing/JSpinner/8223788/JSpinnerButtonFocusTest.java test fails in ubuntu 22.04 on SBR Hosts Reviewed-by: honkar, serb --- .../8223788/JSpinnerButtonFocusTest.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/jdk/javax/swing/JSpinner/8223788/JSpinnerButtonFocusTest.java b/test/jdk/javax/swing/JSpinner/8223788/JSpinnerButtonFocusTest.java index 4060042ca4f..994a03959b2 100644 --- a/test/jdk/javax/swing/JSpinner/8223788/JSpinnerButtonFocusTest.java +++ b/test/jdk/javax/swing/JSpinner/8223788/JSpinnerButtonFocusTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -63,7 +63,7 @@ public class JSpinnerButtonFocusTest { robot.setAutoDelay(50); SwingUtilities.invokeAndWait(() -> { - frame = new JFrame(); + frame = new JFrame("JSpinnerButtonFocusTest"); spinner1 = new JSpinner(); spinner2 = new JSpinner(); @@ -72,6 +72,15 @@ public class JSpinnerButtonFocusTest { frame.getContentPane().add(spinner2, BorderLayout.SOUTH); editor1 = ((DefaultEditor)spinner1.getEditor()); + editor1.getTextField().addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + super.focusGained(e); + robot.keyPress(KeyEvent.VK_TAB); + robot.keyRelease(KeyEvent.VK_TAB); + latch1.countDown(); + } + }); editor1.setFocusable(false); spinner1.setFocusable(false); @@ -84,26 +93,18 @@ public class JSpinnerButtonFocusTest { frame.setFocusTraversalPolicyProvider(true); frame.setAlwaysOnTop(true); - frame.pack(); + frame.setSize(100, 100); + frame.setLocationRelativeTo(null); frame.setVisible(true); }); robot.waitForIdle(); - - editor1.getTextField().addFocusListener(new FocusAdapter() { - @Override - public void focusGained(FocusEvent e) { - super.focusGained(e); - robot.keyPress(KeyEvent.VK_TAB); - robot.keyRelease(KeyEvent.VK_TAB); - latch1.countDown(); - } - }); + robot.delay(1000); SwingUtilities.invokeAndWait(() -> { editor1.getTextField().requestFocusInWindow(); }); - if (!latch1.await(15, TimeUnit.MINUTES)) { + if (!latch1.await(1, TimeUnit.MINUTES)) { throw new RuntimeException(LF.getClassName() + ": Timeout waiting for editor1 to gain focus."); } From 31beb7d3b34c3516c326c9d29a267f6becb38805 Mon Sep 17 00:00:00 2001 From: Prasanta Sadhukhan Date: Fri, 17 Oct 2025 01:33:30 +0000 Subject: [PATCH 549/556] 8068310: [TEST_BUG] Test javax/swing/JColorChooser/Test4234761.java fails with GTKL&F Reviewed-by: serb --- .../jdk/javax/swing/JColorChooser/Test4234761.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/jdk/javax/swing/JColorChooser/Test4234761.java b/test/jdk/javax/swing/JColorChooser/Test4234761.java index c2b2d9ed7b9..fb55ca37feb 100644 --- a/test/jdk/javax/swing/JColorChooser/Test4234761.java +++ b/test/jdk/javax/swing/JColorChooser/Test4234761.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,10 +23,12 @@ /* * @test - * @key headful * @bug 4234761 + * @key headful * @summary RGB values sholdn't be changed in transition to HSB tab - * @author Oleg Mokhovikov + * @library /test/lib + * @build jtreg.SkippedException + * @run main Test4234761 */ import java.awt.Color; @@ -35,11 +37,17 @@ import java.beans.PropertyChangeListener; import javax.swing.JColorChooser; import javax.swing.JDialog; import javax.swing.JTabbedPane; +import javax.swing.UIManager; + +import jtreg.SkippedException; public class Test4234761 implements PropertyChangeListener { private static final Color COLOR = new Color(51, 51, 51); public static void main(String[] args) { + if (UIManager.getLookAndFeel().getName().contains("GTK")) { + throw new SkippedException("Test skipped for GTK"); + } JColorChooser chooser = new JColorChooser(COLOR); JDialog dialog = Test4177735.show(chooser); From 46c23bb1a252916096876c2ae3a72f4a525dd6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20Bourg=C3=A8s?= Date: Fri, 17 Oct 2025 05:43:10 +0000 Subject: [PATCH 550/556] 8341381: Random lines appear in graphic causing by the fix of JDK-8297230 Reviewed-by: prr --- .../classes/sun/java2d/marlin/Curve.java | 53 +- .../java2d/marlin/DMarlinRenderingEngine.java | 2 +- .../classes/sun/java2d/marlin/Helpers.java | 23 +- .../classes/sun/java2d/marlin/Stroker.java | 14 +- test/jdk/sun/java2d/marlin/Bug8341381.java | 601 ++++++++++++++++++ 5 files changed, 656 insertions(+), 37 deletions(-) create mode 100644 test/jdk/sun/java2d/marlin/Bug8341381.java diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java b/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java index 2ce0cd4672c..9d2c8dc2a72 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Curve.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -144,7 +144,9 @@ final class Curve { // finds points where the first and second derivative are // perpendicular. This happens when g(t) = f'(t)*f''(t) == 0 (where // * is a dot product). Unfortunately, we have to solve a cubic. - private int perpendiculardfddf(final double[] pts, final int off) { + private int perpendiculardfddf(final double[] pts, final int off, + final double A, final double B) + { assert pts.length >= off + 4; // these are the coefficients of some multiple of g(t) (not g(t), @@ -155,7 +157,7 @@ final class Curve { final double c = 2.0d * (dax * cx + day * cy) + dbx * dbx + dby * dby; final double d = dbx * cx + dby * cy; - return Helpers.cubicRootsInAB(a, b, c, d, pts, off, 0.0d, 1.0d); + return Helpers.cubicRootsInAB(a, b, c, d, pts, off, A, B); } // Tries to find the roots of the function ROC(t)-w in [0, 1). It uses @@ -171,35 +173,43 @@ final class Curve { // at most 4 sub-intervals of (0,1). ROC has asymptotes at inflection // points, so roc-w can have at least 6 roots. This shouldn't be a // problem for what we're trying to do (draw a nice looking curve). - int rootsOfROCMinusW(final double[] roots, final int off, final double w2, final double err) { + int rootsOfROCMinusW(final double[] roots, final int off, final double w2, + final double A, final double B) + { // no OOB exception, because by now off<=6, and roots.length >= 10 assert off <= 6 && roots.length >= 10; int ret = off; - final int end = off + perpendiculardfddf(roots, off); + final int end = off + perpendiculardfddf(roots, off, A, B); + Helpers.isort(roots, off, end); roots[end] = 1.0d; // always check interval end points - double t0 = 0.0d, ft0 = ROCsq(t0) - w2; + double t0 = 0.0d; + double ft0 = eliminateInf(ROCsq(t0) - w2); + double t1, ft1; for (int i = off; i <= end; i++) { - double t1 = roots[i], ft1 = ROCsq(t1) - w2; + t1 = roots[i]; + ft1 = eliminateInf(ROCsq(t1) - w2); if (ft0 == 0.0d) { roots[ret++] = t0; } else if (ft1 * ft0 < 0.0d) { // have opposite signs // (ROC(t)^2 == w^2) == (ROC(t) == w) is true because // ROC(t) >= 0 for all t. - roots[ret++] = falsePositionROCsqMinusX(t0, t1, w2, err); + roots[ret++] = falsePositionROCsqMinusX(t0, t1, ft0, ft1, w2, A); // A = err } t0 = t1; ft0 = ft1; } - return ret - off; } - private static double eliminateInf(final double x) { - return (x == Double.POSITIVE_INFINITY ? Double.MAX_VALUE : - (x == Double.NEGATIVE_INFINITY ? Double.MIN_VALUE : x)); + private final static double MAX_ROC_SQ = 1e20; + + private static double eliminateInf(final double x2) { + // limit the value of x to avoid numerical problems (smaller step): + // must handle NaN and +Infinity: + return (x2 <= MAX_ROC_SQ) ? x2 : MAX_ROC_SQ; } // A slight modification of the false position algorithm on wikipedia. @@ -210,17 +220,18 @@ final class Curve { // and turn out. Same goes for the newton's method // algorithm in Helpers.java private double falsePositionROCsqMinusX(final double t0, final double t1, + final double ft0, final double ft1, final double w2, final double err) { final int iterLimit = 100; int side = 0; - double t = t1, ft = eliminateInf(ROCsq(t) - w2); - double s = t0, fs = eliminateInf(ROCsq(s) - w2); + double s = t0, fs = eliminateInf(ft0); + double t = t1, ft = eliminateInf(ft1); double r = s, fr; - for (int i = 0; i < iterLimit && Math.abs(t - s) > err * Math.abs(t + s); i++) { + for (int i = 0; i < iterLimit && Math.abs(t - s) > err; i++) { r = (fs * t - ft * s) / (fs - ft); - fr = ROCsq(r) - w2; + fr = eliminateInf(ROCsq(r) - w2); if (sameSign(fr, ft)) { ft = fr; t = r; if (side < 0) { @@ -241,7 +252,7 @@ final class Curve { break; } } - return r; + return (Math.abs(ft) <= Math.abs(fs)) ? t : s; } private static boolean sameSign(final double x, final double y) { @@ -256,9 +267,9 @@ final class Curve { final double dy = t * (t * day + dby) + cy; final double ddx = 2.0d * dax * t + dbx; final double ddy = 2.0d * day * t + dby; - final double dx2dy2 = dx * dx + dy * dy; - final double ddx2ddy2 = ddx * ddx + ddy * ddy; - final double ddxdxddydy = ddx * dx + ddy * dy; - return dx2dy2 * ((dx2dy2 * dx2dy2) / (dx2dy2 * ddx2ddy2 - ddxdxddydy * ddxdxddydy)); + final double dx2dy2 = dx * dx + dy * dy; // positive + final double dxddyddxdy = dx * ddy - dy * ddx; + // may return +Infinity if dxddyddxdy = 0 or NaN if 0/0: + return (dx2dy2 * dx2dy2 * dx2dy2) / (dxddyddxdy * dxddyddxdy); // both positive } } diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java b/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java index 66eb9334e86..f829872a8a8 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/DMarlinRenderingEngine.java @@ -564,7 +564,7 @@ public final class DMarlinRenderingEngine extends RenderingEngine } private static boolean nearZero(final double num) { - return Math.abs(num) < 2.0d * Math.ulp(num); + return Math.abs(num) < 2.0d * Helpers.ulp(num); } abstract static class NormalizingPathIterator implements PathIterator { diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java b/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java index 0aed05ab506..926533cdb2b 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Helpers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,12 +31,19 @@ import sun.java2d.marlin.stats.StatLong; final class Helpers implements MarlinConst { + private final static double T_ERR = 1e-4; + private final static double T_A = T_ERR; + private final static double T_B = 1.0 - T_ERR; + private static final double EPS = 1e-9d; private Helpers() { throw new Error("This is a non instantiable class"); } + /** use lower precision like former Pisces and Marlin (float-precision) */ + static double ulp(final double value) { return Math.ulp((float)value); } + static boolean within(final double x, final double y) { return within(x, y, EPS); } @@ -322,10 +329,10 @@ final class Helpers implements MarlinConst { // now we must subdivide at points where one of the offset curves will have // a cusp. This happens at ts where the radius of curvature is equal to w. - ret += c.rootsOfROCMinusW(ts, ret, w2, 0.0001d); + ret += c.rootsOfROCMinusW(ts, ret, w2, T_A, T_B); - ret = filterOutNotInAB(ts, 0, ret, 0.0001d, 0.9999d); - isort(ts, ret); + ret = filterOutNotInAB(ts, 0, ret, T_A, T_B); + isort(ts, 0, ret); return ret; } @@ -354,7 +361,7 @@ final class Helpers implements MarlinConst { if ((outCodeOR & OUTCODE_BOTTOM) != 0) { ret += curve.yPoints(ts, ret, clipRect[1]); } - isort(ts, ret); + isort(ts, 0, ret); return ret; } @@ -374,11 +381,11 @@ final class Helpers implements MarlinConst { } } - static void isort(final double[] a, final int len) { - for (int i = 1, j; i < len; i++) { + static void isort(final double[] a, final int off, final int len) { + for (int i = off + 1, j; i < len; i++) { final double ai = a[i]; j = i - 1; - for (; j >= 0 && a[j] > ai; j--) { + for (; j >= off && a[j] > ai; j--) { a[j + 1] = a[j]; } a[j + 1] = ai; diff --git a/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java b/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java index 59f93ed7d6d..1c257bc13d9 100644 --- a/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java +++ b/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -886,8 +886,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { // if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4, // in which case ignore if p1 == p2 - final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Math.ulp(y2)); - final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Math.ulp(y4)); + final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Helpers.ulp(y2)); + final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Helpers.ulp(y4)); if (p1eqp2 && p3eqp4) { return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); @@ -905,7 +905,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { final double l1sq = dx1 * dx1 + dy1 * dy1; final double l4sq = dx4 * dx4 + dy4 * dy4; - if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Math.ulp(dotsq))) { + if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Helpers.ulp(dotsq))) { return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); } @@ -1078,8 +1078,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { // equal if they're very close to each other. // if p1 == p2 or p2 == p3: draw line from p1->p3 - final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Math.ulp(y2)); - final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Math.ulp(y3)); + final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Helpers.ulp(y2)); + final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Helpers.ulp(y3)); if (p1eqp2 || p2eqp3) { return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); @@ -1091,7 +1091,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst { final double l1sq = dx12 * dx12 + dy12 * dy12; final double l3sq = dx23 * dx23 + dy23 * dy23; - if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Math.ulp(dotsq))) { + if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Helpers.ulp(dotsq))) { return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff); } diff --git a/test/jdk/sun/java2d/marlin/Bug8341381.java b/test/jdk/sun/java2d/marlin/Bug8341381.java new file mode 100644 index 00000000000..b469ac49313 --- /dev/null +++ b/test/jdk/sun/java2d/marlin/Bug8341381.java @@ -0,0 +1,601 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.CubicCurve2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import javax.imageio.ImageIO; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.Timer; + +import static java.lang.System.out; + +/** + * @test + * @bug 8341381 + * @summary fix cubic offsetting issue (numerical accuracy) + * @run main/othervm/timeout=20 Bug8341381 + * @modules java.desktop/sun.java2d.marlin + */ +public final class Bug8341381 { + + static final boolean SHOW_GUI = false; + + static final boolean CHECK_PIXELS = true; + static final boolean TRACE_ALL = false; + static final boolean TRACE_CHECK_PIXELS = false; + + static final boolean SAVE_IMAGE = false; + + static final boolean INTENSIVE = false; + + static final double DPI = 96; + static final float STROKE_WIDTH = 15f; + + // delay is 1 frame at 60hz + static final int DELAY = 16; + // off-screen test step (1.0 by default) + static final double STEP = (INTENSIVE) ? 1.0 / 117 : 1.0; + + // stats: + static int N_TEST = 0; + static int N_FAIL = 0; + + static final AtomicBoolean isMarlin = new AtomicBoolean(); + static final CountDownLatch latch = new CountDownLatch(1); + + public static void main(final String[] args) { + Locale.setDefault(Locale.US); + + // FIRST: Get Marlin runtime state from its log: + + // initialize j.u.l Logger: + final Logger log = Logger.getLogger("sun.java2d.marlin"); + log.addHandler(new Handler() { + @Override + public void publish(LogRecord record) { + final String msg = record.getMessage(); + if (msg != null) { + // last space to avoid matching other settings: + if (msg.startsWith("sun.java2d.renderer ")) { + isMarlin.set(msg.contains("DMarlinRenderingEngine")); + } + } + + final Throwable th = record.getThrown(); + // detect any Throwable: + if (th != null) { + out.println("Test failed:\n" + record.getMessage()); + th.printStackTrace(out); + throw new RuntimeException("Test failed: ", th); + } + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + }); + + out.println("Bug8341381: start"); + final long startTime = System.currentTimeMillis(); + + // enable Marlin logging & internal checks: + System.setProperty("sun.java2d.renderer.log", "true"); + System.setProperty("sun.java2d.renderer.useLogger", "true"); + + try { + startTest(); + + out.println("WAITING ..."); + latch.await(15, TimeUnit.SECONDS); // 2s typically + + if (isMarlin.get()) { + out.println("Marlin renderer used at runtime."); + } else { + throw new RuntimeException("Marlin renderer NOT used at runtime !"); + } + + // show test report: + out.println("TESTS: " + N_TEST + " FAILS: " + N_FAIL); + + if (N_FAIL > 0) { + throw new RuntimeException("Bug8341381: " + N_FAIL + " / " + N_TEST + " test(s) failed !"); + } + + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } catch (InvocationTargetException ite) { + throw new RuntimeException(ite); + } finally { + final double elapsed = (System.currentTimeMillis() - startTime) / 1000.0; + out.println("Bug8341381: end (" + elapsed + " s)"); + } + } + + private static void startTest() throws InterruptedException, InvocationTargetException { + if (SHOW_GUI) { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + final JFrame viewer = new JFrame(); + viewer.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + viewer.setContentPane(new CanvasPanel(viewer)); + viewer.pack(); + viewer.setVisible(true); + } + }); + return; + } else { + out.println("STEP: " + STEP); + new Thread(new Runnable() { + @Override + public void run() { + final Context ctx = new Context(); + final Dimension initialDim = ctx.bugDisplay.getSize(DPI); + + double w = initialDim.width; + double h = initialDim.height; + do { + ctx.shouldScale(w, h); + ctx.paintImage(); + + // resize component: + w -= STEP; + h -= STEP; + + } while (ctx.iterate()); + } + }).start(); + } + } + + static final class Context { + + final BugDisplay bugDisplay = new BugDisplay(); + double width = 0.0, height = 0.0; + + BufferedImage bimg = null; + + boolean shouldScale(final double w, final double h) { + if ((w != width) || (h != height) || !bugDisplay.isScaled) { + width = w; + height = h; + bugDisplay.scale(width, height); + N_TEST++; + return true; + } + return false; + } + + void paintImage() { + final int w = bugDisplay.canvasWidth; + final int h = bugDisplay.canvasHeight; + + if ((bimg == null) || (w > bimg.getWidth()) || (h > bimg.getHeight())) { + bimg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); + } + final Graphics gi = bimg.getGraphics(); + try { + bugDisplay.paint(gi); + } finally { + gi.dispose(); + } + if (!bugDisplay.checkImage(bimg)) { + N_FAIL++; + } + } + + boolean iterate() { + if ((bugDisplay.canvasWidth > 10) || (bugDisplay.canvasHeight > 10)) { + // continue: + return true; + } + out.println("Stop"); + latch.countDown(); + return false; + } + } + + static final class CanvasPanel extends JPanel { + private static final long serialVersionUID = 1L; + + private final Context ctx = new Context(); + private boolean resized = false; + private Timer timer = null; + + public CanvasPanel(final JFrame frame) { + timer = new Timer(DELAY, e -> { + if (resized) { + resized = false; + + if (ctx.iterate()) { + // resize component: + setSize((int) Math.round(ctx.width - 1), (int) Math.round(ctx.height - 1)); + } else { + timer.stop(); + if (frame != null) { + frame.setVisible(false); + } + } + } + }); + timer.setCoalesce(true); + timer.setRepeats(true); + timer.start(); + } + + @Override + public void paint(final Graphics g) { + final Dimension dim = getSize(); + if (ctx.shouldScale(dim.width, dim.height)) { + this.resized = true; + } + super.paint(g); + + // paint on buffered image: + if (CHECK_PIXELS) { + final int w = ctx.bugDisplay.canvasWidth; + final int h = ctx.bugDisplay.canvasHeight; + if (this.resized) { + ctx.paintImage(); + } + g.drawImage(ctx.bimg.getSubimage(0, 0, w, h), 0, 0, null); + } else { + ctx.bugDisplay.paint(g); + } + } + + @Override + public Dimension getPreferredSize() { + return ctx.bugDisplay.getSize(DPI); + } + } + + static final class BugDisplay { + + boolean isScaled = false; + int canvasWidth; + int canvasHeight; + + private final static java.util.List curves1 = Arrays.asList( + new CubicCurve2D.Double(2191.0, 7621.0, 2191.0, 7619.0, 2191.0, 7618.0, 2191.0, 7617.0), + new CubicCurve2D.Double(2191.0, 7617.0, 2191.0, 7617.0, 2191.0, 7616.0, 2191.0, 7615.0), + new CubicCurve2D.Double(2198.0, 7602.0, 2200.0, 7599.0, 2203.0, 7595.0, 2205.0, 7590.0), + new CubicCurve2D.Double(2205.0, 7590.0, 2212.0, 7580.0, 2220.0, 7571.0, 2228.0, 7563.0), + new CubicCurve2D.Double(2228.0, 7563.0, 2233.0, 7557.0, 2239.0, 7551.0, 2245.0, 7546.0), + new CubicCurve2D.Double(2245.0, 7546.0, 2252.0, 7540.0, 2260.0, 7534.0, 2267.0, 7528.0), + new CubicCurve2D.Double(2267.0, 7528.0, 2271.0, 7526.0, 2275.0, 7524.0, 2279.0, 7521.0), + new CubicCurve2D.Double(2279.0, 7521.0, 2279.0, 7520.0, 2280.0, 7520.0, 2281.0, 7519.0) + ); + private final static java.util.List curves2 = Arrays.asList( + new CubicCurve2D.Double(2281.0, 7519.0, 2282.0, 7518.0, 2282.0, 7517.0, 2283.0, 7516.0), + new CubicCurve2D.Double(2283.0, 7516.0, 2284.0, 7515.0, 2284.0, 7515.0, 2285.0, 7514.0), + new CubicCurve2D.Double(2291.0, 7496.0, 2292.0, 7495.0, 2292.0, 7494.0, 2291.0, 7493.0), + new CubicCurve2D.Double(2291.0, 7493.0, 2290.0, 7492.0, 2290.0, 7492.0, 2289.0, 7492.0), + new CubicCurve2D.Double(2289.0, 7492.0, 2288.0, 7491.0, 2286.0, 7492.0, 2285.0, 7492.0), + new CubicCurve2D.Double(2262.0, 7496.0, 2260.0, 7497.0, 2259.0, 7497.0, 2257.0, 7498.0), + new CubicCurve2D.Double(2257.0, 7498.0, 2254.0, 7498.0, 2251.0, 7499.0, 2248.0, 7501.0), + new CubicCurve2D.Double(2248.0, 7501.0, 2247.0, 7501.0, 2245.0, 7502.0, 2244.0, 7503.0), + new CubicCurve2D.Double(2207.0, 7523.0, 2203.0, 7525.0, 2199.0, 7528.0, 2195.0, 7530.0), + new CubicCurve2D.Double(2195.0, 7530.0, 2191.0, 7534.0, 2186.0, 7538.0, 2182.0, 7541.0) + ); + private final static java.util.List curves3 = Arrays.asList( + new CubicCurve2D.Double(2182.0, 7541.0, 2178.0, 7544.0, 2174.0, 7547.0, 2170.0, 7551.0), + new CubicCurve2D.Double(2170.0, 7551.0, 2164.0, 7556.0, 2158.0, 7563.0, 2152.0, 7569.0), + new CubicCurve2D.Double(2152.0, 7569.0, 2148.0, 7573.0, 2145.0, 7577.0, 2141.0, 7582.0), + new CubicCurve2D.Double(2141.0, 7582.0, 2138.0, 7588.0, 2134.0, 7595.0, 2132.0, 7602.0), + new CubicCurve2D.Double(2132.0, 7602.0, 2132.0, 7605.0, 2131.0, 7608.0, 2131.0, 7617.0), + new CubicCurve2D.Double(2131.0, 7617.0, 2131.0, 7620.0, 2131.0, 7622.0, 2131.0, 7624.0), + new CubicCurve2D.Double(2131.0, 7624.0, 2131.0, 7630.0, 2132.0, 7636.0, 2135.0, 7641.0), + new CubicCurve2D.Double(2135.0, 7641.0, 2136.0, 7644.0, 2137.0, 7647.0, 2139.0, 7650.0), + new CubicCurve2D.Double(2139.0, 7650.0, 2143.0, 7658.0, 2149.0, 7664.0, 2155.0, 7670.0), + new CubicCurve2D.Double(2155.0, 7670.0, 2160.0, 7676.0, 2165.0, 7681.0, 2171.0, 7686.0) + ); + private final static java.util.List curves4 = Arrays.asList( + new CubicCurve2D.Double(2171.0, 7686.0, 2174.0, 7689.0, 2177.0, 7692.0, 2180.0, 7694.0), + new CubicCurve2D.Double(2180.0, 7694.0, 2185.0, 7698.0, 2191.0, 7702.0, 2196.0, 7706.0), + new CubicCurve2D.Double(2196.0, 7706.0, 2199.0, 7708.0, 2203.0, 7711.0, 2207.0, 7713.0), + new CubicCurve2D.Double(2244.0, 7734.0, 2245.0, 7734.0, 2247.0, 7735.0, 2248.0, 7736.0), + new CubicCurve2D.Double(2248.0, 7736.0, 2251.0, 7738.0, 2254.0, 7739.0, 2257.0, 7739.0), + new CubicCurve2D.Double(2257.0, 7739.0, 2259.0, 7739.0, 2260.0, 7739.0, 2262.0, 7740.0), + new CubicCurve2D.Double(2285.0, 7745.0, 2286.0, 7745.0, 2288.0, 7745.0, 2289.0, 7745.0), + new CubicCurve2D.Double(2289.0, 7745.0, 2290.0, 7745.0, 2290.0, 7744.0, 2291.0, 7743.0), + new CubicCurve2D.Double(2291.0, 7743.0, 2292.0, 7742.0, 2292.0, 7741.0, 2291.0, 7740.0), + new CubicCurve2D.Double(2285.0, 7722.0, 2284.0, 7721.0, 2284.0, 7721.0, 2283.0, 7720.0), + new CubicCurve2D.Double(2283.0, 7720.0, 2282.0, 7719.0, 2282.0, 7719.0, 2281.0, 7718.0), + new CubicCurve2D.Double(2281.0, 7718.0, 2280.0, 7717.0, 2279.0, 7716.0, 2279.0, 7716.0), + new CubicCurve2D.Double(2279.0, 7716.0, 2275.0, 7712.0, 2271.0, 7710.0, 2267.0, 7708.0), + new CubicCurve2D.Double(2267.0, 7708.0, 2260.0, 7702.0, 2252.0, 7697.0, 2245.0, 7691.0), + new CubicCurve2D.Double(2245.0, 7691.0, 2239.0, 7685.0, 2233.0, 7679.0, 2228.0, 7673.0), + new CubicCurve2D.Double(2228.0, 7673.0, 2220.0, 7665.0, 2212.0, 7656.0, 2205.0, 7646.0), + new CubicCurve2D.Double(2205.0, 7646.0, 2203.0, 7641.0, 2200.0, 7637.0, 2198.0, 7634.0) + ); + + private final static Point2D.Double[] extent = {new Point2D.Double(0.0, 0.0), new Point2D.Double(7777.0, 10005.0)}; + + private final static Stroke STROKE = new BasicStroke(STROKE_WIDTH); + private final static Stroke STROKE_DASHED = new BasicStroke(STROKE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, + 10.0f, new float[] {100f, 0f}, 0.0f); + + // members: + private final java.util.List allCurves = new ArrayList<>(); + private final Rectangle2D bboxAllCurves = new Rectangle2D.Double(); + + BugDisplay() { + allCurves.addAll(curves1); + allCurves.addAll(curves2); + allCurves.addAll(curves3); + allCurves.addAll(curves4); + + // initialize bounding box: + double x1 = Double.POSITIVE_INFINITY; + double y1 = Double.POSITIVE_INFINITY; + double x2 = Double.NEGATIVE_INFINITY; + double y2 = Double.NEGATIVE_INFINITY; + + for (final CubicCurve2D c : allCurves) { + final Rectangle2D r = c.getBounds2D(); + if (r.getMinX() < x1) { + x1 = r.getMinX(); + } + if (r.getMinY() < y1) { + y1 = r.getMinY(); + } + if (r.getMaxX() > x2) { + x2 = r.getMaxX(); + } + if (r.getMaxY() > y2) { + y2 = r.getMaxY(); + } + } + // add margin of 10%: + final double m = 1.1 * STROKE_WIDTH; + bboxAllCurves.setFrameFromDiagonal(x1 - m, y1 - m, x2 + m, y2 + m); + } + + public void paint(final Graphics g) { + final Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, this.canvasWidth, this.canvasHeight); + + // ------ scale + final AffineTransform tx_orig = g2d.getTransform(); + final AffineTransform tx = getDrawTransform(); + g2d.transform(tx); + + // draw bbox: + if (!CHECK_PIXELS) { + g2d.setColor(Color.RED); + g2d.setStroke(STROKE); + g2d.draw(bboxAllCurves); + } + // draw curves: + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); + g2d.setColor(Color.BLACK); + + // dasher + stroker: + g2d.setStroke(STROKE_DASHED); + this.allCurves.forEach(g2d::draw); + + // reset + g2d.setTransform(tx_orig); + } + + private AffineTransform getDrawTransform() { + // ------ scale + double minX = extent[0].x, maxX = extent[1].x; + double minY = extent[0].y, maxY = extent[1].y; + + // we're scaling and respecting the proportions, check which scale to use + double sx = this.canvasWidth / Math.abs(maxX - minX); + double sy = this.canvasHeight / Math.abs(maxY - minY); + double s = Math.min(sx, sy); + + double m00, m11, m02, m12; + if (minX < maxX) { + m00 = s; + m02 = -s * minX; + } else { + // inverted X axis + m00 = -s; + m02 = this.canvasWidth + s * maxX; + } + if (minY < maxY) { + m11 = s; + m12 = -s * minY; + } else { + // inverted Y axis + m11 = -s; + m12 = this.canvasHeight + s * maxY; + } + + // scale to the available view port + AffineTransform scaleTransform = new AffineTransform(m00, 0, 0, m11, m02, m12); + + // invert the Y axis since (0, 0) is at top left for AWT + AffineTransform invertY = new AffineTransform(1, 0, 0, -1, 0, this.canvasHeight); + invertY.concatenate(scaleTransform); + + return invertY; + } + + public Dimension getSize(double dpi) { + double metricScalingFactor = 0.02539999969303608; + // 1 inch = 25,4 millimeter + final double factor = dpi * metricScalingFactor / 25.4; + + int width = (int) Math.ceil(Math.abs(extent[1].x - extent[0].x) * factor); + int height = (int) Math.ceil(Math.abs(extent[1].y - extent[0].y) * factor); + + return new Dimension(width, height); + } + + public void scale(double w, double h) { + double extentWidth = Math.abs(extent[1].x - extent[0].x); + double extentHeight = Math.abs(extent[1].y - extent[0].y); + + double fx = w / extentWidth; + if (fx * extentHeight > h) { + fx = h / extentHeight; + } + this.canvasWidth = (int) Math.round(fx * extentWidth); + this.canvasHeight = (int) Math.round(fx * extentHeight); + + // out.println("canvas scaled (" + canvasWidth + " x " + canvasHeight + ")"); + + this.isScaled = true; + } + + protected boolean checkImage(BufferedImage image) { + final AffineTransform tx = getDrawTransform(); + + final Point2D pMin = new Point2D.Double(bboxAllCurves.getMinX(), bboxAllCurves.getMinY()); + final Point2D pMax = new Point2D.Double(bboxAllCurves.getMaxX(), bboxAllCurves.getMaxY()); + + final Point2D tMin = tx.transform(pMin, null); + final Point2D tMax = tx.transform(pMax, null); + + int xMin = (int) tMin.getX(); + int xMax = (int) tMax.getX(); + if (xMin > xMax) { + int t = xMin; + xMin = xMax; + xMax = t; + } + + int yMin = (int) tMin.getY(); + int yMax = (int) tMax.getY(); + if (yMin > yMax) { + int t = yMin; + yMin = yMax; + yMax = t; + } + // add pixel margin (AA): + xMin -= 3; + xMax += 4; + yMin -= 3; + yMax += 4; + + if (xMin < 0 || xMax > image.getWidth() + || yMin < 0 || yMax > image.getHeight()) { + return true; + } + + // out.println("Checking rectangle: " + tMin + " to " + tMax); + // out.println("X min: " + xMin + " - max: " + xMax); + // out.println("Y min: " + yMin + " - max: " + yMax); + + final Raster raster = image.getData(); + final int expected = Color.WHITE.getRGB(); + int nBadPixels = 0; + + // horizontal lines: + for (int x = xMin; x <= xMax; x++) { + if (!checkPixel(raster, x, yMin, expected)) { + nBadPixels++; + } + if (!checkPixel(raster, x, yMax, expected)) { + nBadPixels++; + } + } + + // vertical lines: + for (int y = yMin; y <= yMax; y++) { + if (!checkPixel(raster, xMin, y, expected)) { + nBadPixels++; + } + if (!checkPixel(raster, xMax, y, expected)) { + nBadPixels++; + } + } + + if (nBadPixels != 0) { + out.println("(" + canvasWidth + " x " + canvasHeight + ") BAD pixels = " + nBadPixels); + + if (SAVE_IMAGE) { + try { + final File file = new File("Bug8341381-" + canvasWidth + "-" + canvasHeight + ".png"); + + out.println("Writing file: " + file.getAbsolutePath()); + ImageIO.write(image.getSubimage(0, 0, canvasWidth, canvasHeight), "PNG", file); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + return false; + } else if (TRACE_ALL) { + out.println("(" + canvasWidth + " x " + canvasHeight + ") OK"); + } + return true; + } + + private final static int[] TMP_RGB = new int[1]; + + private static boolean checkPixel(final Raster raster, + final int x, final int y, + final int expected) { + + final int[] rgb = (int[]) raster.getDataElements(x, y, TMP_RGB); + + if (rgb[0] != expected) { + if (TRACE_CHECK_PIXELS) { + out.println("bad pixel at (" + x + ", " + y + ") = " + rgb[0] + + " expected = " + expected); + } + return false; + } + return true; + } + } +} From a22438ddc5949fcfb6f773bd8dc080cd8a1f2710 Mon Sep 17 00:00:00 2001 From: Kevin Walls Date: Fri, 17 Oct 2025 08:16:59 +0000 Subject: [PATCH 551/556] 8369924: Remove test/jdk/javax/management/remote/mandatory/loading/MissingClassTest.java from problemlist Reviewed-by: sspitsyn --- test/jdk/ProblemList-Virtual.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/jdk/ProblemList-Virtual.txt b/test/jdk/ProblemList-Virtual.txt index 37c2c447efa..dcd4dbac310 100644 --- a/test/jdk/ProblemList-Virtual.txt +++ b/test/jdk/ProblemList-Virtual.txt @@ -30,8 +30,6 @@ com/sun/jdi/EATests.java#id0 8264699 generic- com/sun/jdi/ExceptionEvents.java 8278470 generic-all com/sun/jdi/RedefineCrossStart.java 8278470 generic-all -javax/management/remote/mandatory/loading/MissingClassTest.java 8145413 windows-x64 - java/lang/ScopedValue/StressStackOverflow.java#default 8309646 generic-all java/lang/ScopedValue/StressStackOverflow.java#no-TieredCompilation 8309646 generic-all java/lang/ScopedValue/StressStackOverflow.java#TieredStopAtLevel1 8309646 generic-all From 9b9559a2e33827126e1aeab7bf6f4861acaae109 Mon Sep 17 00:00:00 2001 From: David Briemann Date: Fri, 17 Oct 2025 08:59:55 +0000 Subject: [PATCH 552/556] 8369979: Flag UsePopCountInstruction was accidentally disabled on PPC64 Reviewed-by: aph, mdoerr --- src/hotspot/cpu/ppc/vm_version_ppc.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hotspot/cpu/ppc/vm_version_ppc.cpp b/src/hotspot/cpu/ppc/vm_version_ppc.cpp index e2dfd4ecec9..8b1de754650 100644 --- a/src/hotspot/cpu/ppc/vm_version_ppc.cpp +++ b/src/hotspot/cpu/ppc/vm_version_ppc.cpp @@ -99,6 +99,10 @@ void VM_Version::initialize() { FLAG_SET_ERGO(TrapBasedRangeChecks, false); } + if (FLAG_IS_DEFAULT(UsePopCountInstruction)) { + FLAG_SET_ERGO(UsePopCountInstruction, true); + } + if (PowerArchitecturePPC64 >= 9) { // Performance is good since Power9. if (FLAG_IS_DEFAULT(SuperwordUseVSX)) { From e62a7fa3832bbba11e6d630015f85ae945fac824 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Fri, 17 Oct 2025 09:02:09 +0000 Subject: [PATCH 553/556] 8342659: Test vmTestbase/nsk/jdi/ObjectReference/referringObjects/referringObjects002/referringObjects002.java failed: Class nsk.share.jdi.TestClass1 was not unloaded Co-authored-by: Chris Plummer Reviewed-by: sspitsyn, cjplummer --- .../vmTestbase/nsk/share/ClassUnloader.java | 51 ++++++------------- .../nsk/share/jpda/AbstractDebuggeeTest.java | 12 +---- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/test/hotspot/jtreg/vmTestbase/nsk/share/ClassUnloader.java b/test/hotspot/jtreg/vmTestbase/nsk/share/ClassUnloader.java index 5ea01fb3af4..e3b6693657c 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/share/ClassUnloader.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/share/ClassUnloader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,7 @@ package nsk.share; -import java.lang.ref.Cleaner; +import java.lang.ref.PhantomReference; import java.util.*; import nsk.share.gc.gp.*; import nsk.share.test.ExecutionController; @@ -77,19 +77,9 @@ public class ClassUnloader { public static final String INTERNAL_CLASS_LOADER_NAME = "nsk.share.CustomClassLoader"; /** - * Whole amount of time in milliseconds to wait for class loader to be reclaimed. + * Phantom reference to the class loader. */ - private static final int WAIT_TIMEOUT = 15000; - - /** - * Sleep time in milliseconds for the loop waiting for the class loader to be reclaimed. - */ - private static final int WAIT_DELTA = 1000; - - /** - * Has class loader been reclaimed or not. - */ - volatile boolean is_reclaimed = false; + private PhantomReference customClassLoaderPhantomRef = null; /** * Current class loader used for loading classes. @@ -101,6 +91,14 @@ public class ClassUnloader { */ private Vector> classObjects = new Vector>(); + /** + * Has class loader been reclaimed or not. + */ + private boolean isClassLoaderReclaimed() { + return customClassLoaderPhantomRef != null + && customClassLoaderPhantomRef.refersTo(null); + } + /** * Class object of the first class been loaded with current class loader. * To get the rest loaded classes use getLoadedClass(int). @@ -138,8 +136,7 @@ public class ClassUnloader { customClassLoader = new CustomClassLoader(); classObjects.removeAllElements(); - // Register a Cleaner to inform us when the class loader has been reclaimed. - Cleaner.create().register(customClassLoader, () -> { is_reclaimed = true; } ); + customClassLoaderPhantomRef = new PhantomReference<>(customClassLoader, null); return customClassLoader; } @@ -154,8 +151,7 @@ public class ClassUnloader { this.customClassLoader = customClassLoader; classObjects.removeAllElements(); - // Register a Cleaner to inform us when the class loader has been reclaimed. - Cleaner.create().register(customClassLoader, () -> { is_reclaimed = true; } ); + customClassLoaderPhantomRef = new PhantomReference<>(customClassLoader, null); } /** @@ -244,32 +240,15 @@ public class ClassUnloader { */ public boolean unloadClass(ExecutionController stresser) { - is_reclaimed = false; - // free references to class and class loader to be able for collecting by GC - long waitTimeout = (customClassLoader == null) ? 0 : WAIT_TIMEOUT; classObjects.removeAllElements(); customClassLoader = null; // force class unloading by eating memory pool eatMemory(stresser); - // give GC chance to run and wait for receiving reclaim notification - long timeToFinish = System.currentTimeMillis() + waitTimeout; - while (!is_reclaimed && System.currentTimeMillis() < timeToFinish) { - if (!stresser.continueExecution()) { - return false; - } - try { - // suspend thread for a while - Thread.sleep(WAIT_DELTA); - } catch (InterruptedException e) { - throw new Failure("Unexpected InterruptedException while class unloading: " + e); - } - } - // force GC to unload marked class loader and its classes - if (is_reclaimed) { + if (isClassLoaderReclaimed()) { Runtime.getRuntime().gc(); return true; } diff --git a/test/hotspot/jtreg/vmTestbase/nsk/share/jpda/AbstractDebuggeeTest.java b/test/hotspot/jtreg/vmTestbase/nsk/share/jpda/AbstractDebuggeeTest.java index 0668297d211..86d5d7ecd24 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/share/jpda/AbstractDebuggeeTest.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/share/jpda/AbstractDebuggeeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -149,19 +149,11 @@ public class AbstractDebuggeeTest { } } - public static final int MAX_UNLOAD_ATTEMPS = 5; - public void unloadTestClass(String className, boolean expectedUnloadingResult) { ClassUnloader classUnloader = loadedClasses.get(className); - int unloadAttemps = 0; - if (classUnloader != null) { - boolean wasUnloaded = false; - - while (!wasUnloaded && (unloadAttemps++ < MAX_UNLOAD_ATTEMPS)) { - wasUnloaded = classUnloader.unloadClass(); - } + boolean wasUnloaded = classUnloader.unloadClass(); if (wasUnloaded) loadedClasses.remove(className); From 0a97bef840f8799313a1a55a65d9334e09cc1cf4 Mon Sep 17 00:00:00 2001 From: Albert Mingkun Yang Date: Fri, 17 Oct 2025 09:32:40 +0000 Subject: [PATCH 554/556] 8369814: G1: Relax card mark and store ordering Reviewed-by: tschatzl, fandreuzzi --- src/hotspot/share/gc/g1/g1BarrierSet.hpp | 4 - src/hotspot/share/gc/g1/g1CollectedHeap.cpp | 1 - .../gc/parallel/parallelScavengeHeap.cpp | 1 - src/hotspot/share/gc/serial/serialHeap.cpp | 1 - .../share/gc/shared/cardTableBarrierSet.cpp | 73 +------------------ .../share/gc/shared/cardTableBarrierSet.hpp | 28 ------- src/hotspot/share/gc/shared/gc_globals.hpp | 4 - src/hotspot/share/gc/shared/vmStructs_gc.hpp | 1 - .../gc/shenandoah/shenandoahBarrierSet.cpp | 1 - src/hotspot/share/runtime/javaThread.cpp | 4 - src/hotspot/share/runtime/javaThread.hpp | 8 -- 11 files changed, 3 insertions(+), 123 deletions(-) diff --git a/src/hotspot/share/gc/g1/g1BarrierSet.hpp b/src/hotspot/share/gc/g1/g1BarrierSet.hpp index 20642cfc7e6..1e5c111a652 100644 --- a/src/hotspot/share/gc/g1/g1BarrierSet.hpp +++ b/src/hotspot/share/gc/g1/g1BarrierSet.hpp @@ -84,10 +84,6 @@ class G1BarrierSet: public CardTableBarrierSet { // Update the given thread's card table (byte map) base to the current card table's. void update_card_table_base(Thread* thread); - virtual bool card_mark_must_follow_store() const { - return true; - } - // Add "pre_val" to a set of objects that may have been disconnected from the // pre-marking object graph. Prefer the version that takes location, as it // can avoid touching the heap unnecessarily. diff --git a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp index c1b18a71cfb..485caa9f6c0 100644 --- a/src/hotspot/share/gc/g1/g1CollectedHeap.cpp +++ b/src/hotspot/share/gc/g1/g1CollectedHeap.cpp @@ -1391,7 +1391,6 @@ jint G1CollectedHeap::initialize() { G1CardTable* refinement_table = new G1CardTable(_reserved); G1BarrierSet* bs = new G1BarrierSet(card_table, refinement_table); - bs->initialize(); assert(bs->is_a(BarrierSet::G1BarrierSet), "sanity"); // Create space mappers. diff --git a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp index 18cbe2403d8..eb1552e3db6 100644 --- a/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp +++ b/src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp @@ -79,7 +79,6 @@ jint ParallelScavengeHeap::initialize() { card_table->initialize(old_rs.base(), young_rs.base()); CardTableBarrierSet* const barrier_set = new CardTableBarrierSet(card_table); - barrier_set->initialize(); BarrierSet::set_barrier_set(barrier_set); // Set up WorkerThreads diff --git a/src/hotspot/share/gc/serial/serialHeap.cpp b/src/hotspot/share/gc/serial/serialHeap.cpp index 3511318e169..8022b317ca6 100644 --- a/src/hotspot/share/gc/serial/serialHeap.cpp +++ b/src/hotspot/share/gc/serial/serialHeap.cpp @@ -182,7 +182,6 @@ jint SerialHeap::initialize() { _rem_set->initialize(young_rs.base(), old_rs.base()); CardTableBarrierSet *bs = new CardTableBarrierSet(_rem_set); - bs->initialize(); BarrierSet::set_barrier_set(bs); _young_gen = new DefNewGeneration(young_rs, NewSize, MinNewSize, MaxNewSize); diff --git a/src/hotspot/share/gc/shared/cardTableBarrierSet.cpp b/src/hotspot/share/gc/shared/cardTableBarrierSet.cpp index dfa00636dec..de514f64be2 100644 --- a/src/hotspot/share/gc/shared/cardTableBarrierSet.cpp +++ b/src/hotspot/share/gc/shared/cardTableBarrierSet.cpp @@ -57,7 +57,6 @@ CardTableBarrierSet::CardTableBarrierSet(BarrierSetAssembler* barrier_set_assemb barrier_set_c1, barrier_set_c2, fake_rtti.add_tag(BarrierSet::CardTableBarrierSet)), - _defer_initial_card_mark(false), _card_table(card_table) {} @@ -66,14 +65,9 @@ CardTableBarrierSet::CardTableBarrierSet(CardTable* card_table) : make_barrier_set_c1(), make_barrier_set_c2(), BarrierSet::FakeRtti(BarrierSet::CardTableBarrierSet)), - _defer_initial_card_mark(false), _card_table(card_table) {} -void CardTableBarrierSet::initialize() { - initialize_deferred_card_mark_barriers(); -} - CardTableBarrierSet::~CardTableBarrierSet() { delete _card_table; } @@ -108,9 +102,7 @@ void CardTableBarrierSet::print_on(outputStream* st) const { // to the post-barrier, we note that G1 needs a RS update barrier // which simply enqueues a (sequence of) dirty cards which may // optionally be refined by the concurrent update threads. Note -// that this barrier need only be applied to a non-young write, -// but, because of the presence of concurrent refinement, -// must strictly follow the oop-store. +// that this barrier need only be applied to a non-young write. // // For any future collector, this code should be reexamined with // that specific collector in mind, and the documentation above suitably @@ -120,72 +112,13 @@ void CardTableBarrierSet::on_slowpath_allocation_exit(JavaThread* thread, oop ne if (!ReduceInitialCardMarks) { return; } - // If a previous card-mark was deferred, flush it now. - flush_deferred_card_mark_barrier(thread); if (new_obj->is_typeArray() || _card_table->is_in_young(new_obj)) { // Arrays of non-references don't need a post-barrier. - // The deferred_card_mark region should be empty - // following the flush above. - assert(thread->deferred_card_mark().is_empty(), "Error"); } else { MemRegion mr(cast_from_oop(new_obj), new_obj->size()); assert(!mr.is_empty(), "Error"); - if (_defer_initial_card_mark) { - // Defer the card mark - thread->set_deferred_card_mark(mr); - } else { - // Do the card mark - write_region(mr); - } + // Do the card mark + write_region(mr); } #endif // COMPILER2_OR_JVMCI } - -void CardTableBarrierSet::initialize_deferred_card_mark_barriers() { - // Used for ReduceInitialCardMarks (when COMPILER2 or JVMCI is used); - // otherwise remains unused. -#if COMPILER2_OR_JVMCI - _defer_initial_card_mark = CompilerConfig::is_c2_or_jvmci_compiler_enabled() && ReduceInitialCardMarks - && (DeferInitialCardMark || card_mark_must_follow_store()); -#else - assert(_defer_initial_card_mark == false, "Who would set it?"); -#endif -} - -void CardTableBarrierSet::flush_deferred_card_mark_barrier(JavaThread* thread) { -#if COMPILER2_OR_JVMCI - MemRegion deferred = thread->deferred_card_mark(); - if (!deferred.is_empty()) { - assert(_defer_initial_card_mark, "Otherwise should be empty"); - { - // Verify that the storage points to a parsable object in heap - DEBUG_ONLY(oop old_obj = cast_to_oop(deferred.start());) - assert(!_card_table->is_in_young(old_obj), - "Else should have been filtered in on_slowpath_allocation_exit()"); - assert(oopDesc::is_oop(old_obj), "Not an oop"); - assert(deferred.word_size() == old_obj->size(), - "Mismatch: multiple objects?"); - } - write_region(thread, deferred); - // "Clear" the deferred_card_mark field - thread->set_deferred_card_mark(MemRegion()); - } - assert(thread->deferred_card_mark().is_empty(), "invariant"); -#else - assert(!_defer_initial_card_mark, "Should be false"); - assert(thread->deferred_card_mark().is_empty(), "Should be empty"); -#endif -} - -void CardTableBarrierSet::on_thread_detach(Thread* thread) { - // The deferred store barriers must all have been flushed to the - // card-table (or other remembered set structure) before GC starts - // processing the card-table (or other remembered set). - if (thread->is_Java_thread()) { // Only relevant for Java threads. - flush_deferred_card_mark_barrier(JavaThread::cast(thread)); - } -} - -bool CardTableBarrierSet::card_mark_must_follow_store() const { - return false; -} diff --git a/src/hotspot/share/gc/shared/cardTableBarrierSet.hpp b/src/hotspot/share/gc/shared/cardTableBarrierSet.hpp index 13f3e0783a6..e97da234d16 100644 --- a/src/hotspot/share/gc/shared/cardTableBarrierSet.hpp +++ b/src/hotspot/share/gc/shared/cardTableBarrierSet.hpp @@ -47,9 +47,6 @@ class CardTableBarrierSet: public ModRefBarrierSet { protected: typedef CardTable::CardValue CardValue; - // Used in support of ReduceInitialCardMarks; only consulted if COMPILER2 - // or INCLUDE_JVMCI is being used - bool _defer_initial_card_mark; CardTable* _card_table; CardTableBarrierSet(BarrierSetAssembler* barrier_set_assembler, @@ -64,13 +61,10 @@ public: CardTable* card_table() const { return _card_table; } - void initialize(); - void write_region(JavaThread* thread, MemRegion mr) { write_region(mr); } - public: // Record a reference update. Note that these versions are precise! // The scanning code has to handle the fact that the write barrier may be // either precise or imprecise. We make non-virtual inline variants of @@ -80,29 +74,7 @@ public: virtual void write_region(MemRegion mr); - // ReduceInitialCardMarks - void initialize_deferred_card_mark_barriers(); - - // If the CollectedHeap was asked to defer a store barrier above, - // this informs it to flush such a deferred store barrier to the - // remembered set. - void flush_deferred_card_mark_barrier(JavaThread* thread); - - // If a compiler is eliding store barriers for TLAB-allocated objects, - // we will be informed of a slow-path allocation by a call - // to on_slowpath_allocation_exit() below. Such a call precedes the - // initialization of the object itself, and no post-store-barriers will - // be issued. Some heap types require that the barrier strictly follows - // the initializing stores. (This is currently implemented by deferring the - // barrier until the next slow-path allocation or gc-related safepoint.) - // This interface answers whether a particular barrier type needs the card - // mark to be thus strictly sequenced after the stores. - virtual bool card_mark_must_follow_store() const; - virtual void on_slowpath_allocation_exit(JavaThread* thread, oop new_obj); - virtual void on_thread_detach(Thread* thread); - - virtual void make_parsable(JavaThread* thread) { flush_deferred_card_mark_barrier(thread); } virtual void print_on(outputStream* st) const; diff --git a/src/hotspot/share/gc/shared/gc_globals.hpp b/src/hotspot/share/gc/shared/gc_globals.hpp index 0b245026d68..956bffde156 100644 --- a/src/hotspot/share/gc/shared/gc_globals.hpp +++ b/src/hotspot/share/gc/shared/gc_globals.hpp @@ -418,10 +418,6 @@ "dictionary, classloader_data_graph, metaspace, jni_handles, " \ "codecache_oops, resolved_method_table, stringdedup") \ \ - product(bool, DeferInitialCardMark, false, DIAGNOSTIC, \ - "When +ReduceInitialCardMarks, explicitly defer any that " \ - "may arise from new_pre_store_barrier") \ - \ product(bool, UseCondCardMark, false, \ "Check for already marked card before updating card table") \ \ diff --git a/src/hotspot/share/gc/shared/vmStructs_gc.hpp b/src/hotspot/share/gc/shared/vmStructs_gc.hpp index bba9c9e099f..9d84a56fbd7 100644 --- a/src/hotspot/share/gc/shared/vmStructs_gc.hpp +++ b/src/hotspot/share/gc/shared/vmStructs_gc.hpp @@ -88,7 +88,6 @@ nonstatic_field(CardTable, _byte_map_size, const size_t) \ nonstatic_field(CardTable, _byte_map, CardTable::CardValue*) \ nonstatic_field(CardTable, _byte_map_base, CardTable::CardValue*) \ - nonstatic_field(CardTableBarrierSet, _defer_initial_card_mark, bool) \ nonstatic_field(CardTableBarrierSet, _card_table, CardTable*) \ \ static_field(CollectedHeap, _lab_alignment_reserve, size_t) \ diff --git a/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp b/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp index f6733d4a923..2aa37d7c575 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahBarrierSet.cpp @@ -103,7 +103,6 @@ void ShenandoahBarrierSet::on_slowpath_allocation_exit(JavaThread* thread, oop n ); } #endif // COMPILER2_OR_JVMCI - assert(thread->deferred_card_mark().is_empty(), "We don't use this"); } void ShenandoahBarrierSet::on_thread_create(Thread* thread) { diff --git a/src/hotspot/share/runtime/javaThread.cpp b/src/hotspot/share/runtime/javaThread.cpp index 8bb8095878f..36544cf1118 100644 --- a/src/hotspot/share/runtime/javaThread.cpp +++ b/src/hotspot/share/runtime/javaThread.cpp @@ -535,7 +535,6 @@ JavaThread::JavaThread(MemTag mem_tag) : set_requires_cross_modify_fence(false); pd_initialize(); - assert(deferred_card_mark().is_empty(), "Default MemRegion ctor"); } JavaThread* JavaThread::create_attaching_thread() { @@ -1359,9 +1358,6 @@ void JavaThread::pop_jni_handle_block() { } void JavaThread::oops_do_no_frames(OopClosure* f, NMethodClosure* cf) { - // Verify that the deferred card marks have been flushed. - assert(deferred_card_mark().is_empty(), "Should be empty during GC"); - // Traverse the GCHandles Thread::oops_do_no_frames(f, cf); diff --git a/src/hotspot/share/runtime/javaThread.hpp b/src/hotspot/share/runtime/javaThread.hpp index c8be1594a69..a6a00bfbd03 100644 --- a/src/hotspot/share/runtime/javaThread.hpp +++ b/src/hotspot/share/runtime/javaThread.hpp @@ -149,11 +149,6 @@ class JavaThread: public Thread { oop _vm_result_oop; // oop result is GC-preserved Metadata* _vm_result_metadata; // non-oop result - // See ReduceInitialCardMarks: this holds the precise space interval of - // the most recent slow path allocation for which compiled code has - // elided card-marks for performance along the fast-path. - MemRegion _deferred_card_mark; - ObjectMonitor* volatile _current_pending_monitor; // ObjectMonitor this thread is waiting to lock bool _current_pending_monitor_is_from_java; // locking is from Java code ObjectMonitor* volatile _current_waiting_monitor; // ObjectMonitor on which this thread called Object.wait() @@ -776,9 +771,6 @@ public: void set_vm_result_metadata(Metadata* x) { _vm_result_metadata = x; } - MemRegion deferred_card_mark() const { return _deferred_card_mark; } - void set_deferred_card_mark(MemRegion mr) { _deferred_card_mark = mr; } - // Is thread in scope of an InternalOOMEMark? bool is_in_internal_oome_mark() const { return _is_in_internal_oome_mark; } void set_is_in_internal_oome_mark(bool b) { _is_in_internal_oome_mark = b; } From e8e2aadd9ea302b7b448d0fda9d069d3813f31c5 Mon Sep 17 00:00:00 2001 From: Hamlin Li Date: Fri, 17 Oct 2025 11:22:23 +0000 Subject: [PATCH 555/556] 8369685: RISC-V: refactor code related to RVFeatureValue::enabled Reviewed-by: fyang, rehn --- src/hotspot/cpu/riscv/vm_version_riscv.cpp | 13 +----- src/hotspot/cpu/riscv/vm_version_riscv.hpp | 45 +++++++++---------- .../linux_riscv/vm_version_linux_riscv.cpp | 13 ++++-- 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.cpp b/src/hotspot/cpu/riscv/vm_version_riscv.cpp index 9d6146a8389..6f4babc872f 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.cpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.cpp @@ -103,17 +103,6 @@ void VM_Version::common_initialize() { useRVA23U64Profile(); } - // Enable vendor specific features - - if (mvendorid.enabled()) { - // Rivos - if (mvendorid.value() == RIVOS) { - if (FLAG_IS_DEFAULT(UseConservativeFence)) { - FLAG_SET_DEFAULT(UseConservativeFence, false); - } - } - } - if (UseZic64b) { if (CacheLineSize != 64) { assert(!FLAG_IS_DEFAULT(CacheLineSize), "default cache line size should be 64 bytes"); @@ -199,7 +188,7 @@ void VM_Version::common_initialize() { FLAG_SET_DEFAULT(UsePopCountInstruction, false); } - if (UseZicboz && zicboz_block_size.enabled() && zicboz_block_size.value() > 0) { + if (UseZicboz && zicboz_block_size.value() > 0) { assert(is_power_of_2(zicboz_block_size.value()), "Sanity"); if (FLAG_IS_DEFAULT(UseBlockZeroing)) { FLAG_SET_DEFAULT(UseBlockZeroing, true); diff --git a/src/hotspot/cpu/riscv/vm_version_riscv.hpp b/src/hotspot/cpu/riscv/vm_version_riscv.hpp index 3d555d47e9f..f74992cbc37 100644 --- a/src/hotspot/cpu/riscv/vm_version_riscv.hpp +++ b/src/hotspot/cpu/riscv/vm_version_riscv.hpp @@ -52,24 +52,19 @@ class VM_Version : public Abstract_VM_Version { const char* const _pretty; const bool _feature_string; const uint64_t _linux_feature_bit; - int64_t _value; + public: RVFeatureValue(const char* pretty, int linux_bit_num, bool fstring) : - _pretty(pretty), _feature_string(fstring), _linux_feature_bit(nth_bit(linux_bit_num)), - _value(-1) { + _pretty(pretty), _feature_string(fstring), _linux_feature_bit(nth_bit(linux_bit_num)) { } - virtual void enable_feature(int64_t value = 0) { - _value = value; - } - virtual void disable_feature() { - _value = -1; - } - const char* pretty() { return _pretty; } - uint64_t feature_bit() { return _linux_feature_bit; } - bool feature_string() { return _feature_string; } - int64_t value() { return _value; } + virtual void enable_feature(int64_t value = 0) = 0; + virtual void disable_feature() = 0; + const char* pretty() { return _pretty; } + uint64_t feature_bit() { return _linux_feature_bit; } + bool feature_string() { return _feature_string; } virtual bool enabled() = 0; virtual void update_flag() = 0; + virtual void log_enabled() = 0; }; #define UPDATE_DEFAULT(flag) \ @@ -135,13 +130,12 @@ class VM_Version : public Abstract_VM_Version { return RVExtFeatures::current()->support_feature(_cpu_feature_index); } void enable_feature(int64_t value = 0) { - RVFeatureValue::enable_feature(value); RVExtFeatures::current()->set_feature(_cpu_feature_index); } void disable_feature() { - RVFeatureValue::disable_feature(); RVExtFeatures::current()->clear_feature(_cpu_feature_index); } + void log_enabled(); protected: bool deps_all_enabled(RVExtFeatureValue* dep0, ...) { @@ -196,21 +190,22 @@ class VM_Version : public Abstract_VM_Version { }; class RVNonExtFeatureValue : public RVFeatureValue { - bool _enabled; + static const int64_t DEFAULT_VALUE = -1; + int64_t _value; + public: RVNonExtFeatureValue(const char* pretty, int linux_bit_num, bool fstring) : RVFeatureValue(pretty, linux_bit_num, fstring), - _enabled(false) { + _value(DEFAULT_VALUE) { } - bool enabled() { return _enabled; } - void enable_feature(int64_t value = 0) { - RVFeatureValue::enable_feature(value); - _enabled = true; - } - void disable_feature() { - RVFeatureValue::disable_feature(); - _enabled = false; + bool enabled() { return _value != DEFAULT_VALUE; } + void enable_feature(int64_t value) { + assert(value != DEFAULT_VALUE, "Sanity"); + _value = value; } + void disable_feature() { _value = DEFAULT_VALUE; } + int64_t value() { return _value; } + void log_enabled(); }; public: diff --git a/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp b/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp index e414a3889c2..0799de014a9 100644 --- a/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp +++ b/src/hotspot/os_cpu/linux_riscv/vm_version_linux_riscv.cpp @@ -103,6 +103,14 @@ uint32_t VM_Version::cpu_vector_length() { return (uint32_t)read_csr(CSR_VLENB); } +void VM_Version::RVExtFeatureValue::log_enabled() { + log_debug(os, cpu)("Enabled RV64 feature \"%s\"", pretty()); +} + +void VM_Version::RVNonExtFeatureValue::log_enabled() { + log_debug(os, cpu)("Enabled RV64 feature \"%s\" (%ld)", pretty(), value()); +} + void VM_Version::setup_cpu_available_features() { assert(ext_i.feature_bit() == HWCAP_ISA_I, "Bit for I must follow Linux HWCAP"); @@ -144,9 +152,8 @@ void VM_Version::setup_cpu_available_features() { continue; } - log_debug(os, cpu)("Enabled RV64 feature \"%s\" (%ld)", - _feature_list[i]->pretty(), - _feature_list[i]->value()); + _feature_list[i]->log_enabled(); + // The feature string if (_feature_list[i]->feature_string()) { const char* tmp = _feature_list[i]->pretty(); From 998e08ccd6a661a4f0ef8e32543d8d435188c86d Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Fri, 17 Oct 2025 14:13:49 +0200 Subject: [PATCH 556/556] 8367499: Refactor exhaustiveness computation from Flow into a separate class --- .../javac/comp/ExhaustivenessComputer.java | 605 ++++++++++++++++++ .../com/sun/tools/javac/comp/Flow.java | 545 +--------------- 2 files changed, 609 insertions(+), 541 deletions(-) create mode 100644 src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java new file mode 100644 index 00000000000..77adbcbb073 --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/ExhaustivenessComputer.java @@ -0,0 +1,605 @@ +/* + * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.tools.javac.comp; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import com.sun.tools.javac.code.*; +import com.sun.tools.javac.tree.*; +import com.sun.tools.javac.util.*; + +import com.sun.tools.javac.code.Symbol.*; +import com.sun.tools.javac.tree.JCTree.*; + +import com.sun.tools.javac.code.Kinds.Kind; +import com.sun.tools.javac.code.Type.TypeVar; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.groupingBy; + +/** A class to compute exhaustiveness of set of switch cases. + * + *

        This is NOT part of any supported API. + * If you write code that depends on this, you do so at your own risk. + * This code and its internal interfaces are subject to change or + * deletion without notice. + */ +public class ExhaustivenessComputer { + protected static final Context.Key exhaustivenessKey = new Context.Key<>(); + + private final Symtab syms; + private final Types types; + private final Check chk; + private final Infer infer; + + public static ExhaustivenessComputer instance(Context context) { + ExhaustivenessComputer instance = context.get(exhaustivenessKey); + if (instance == null) + instance = new ExhaustivenessComputer(context); + return instance; + } + + @SuppressWarnings("this-escape") + protected ExhaustivenessComputer(Context context) { + context.put(exhaustivenessKey, this); + syms = Symtab.instance(context); + types = Types.instance(context); + chk = Check.instance(context); + infer = Infer.instance(context); + } + + public boolean exhausts(JCExpression selector, List cases) { + Set patternSet = new HashSet<>(); + Map> enum2Constants = new HashMap<>(); + Set booleanLiterals = new HashSet<>(Set.of(0, 1)); + for (JCCase c : cases) { + if (!TreeInfo.unguardedCase(c)) + continue; + + for (var l : c.labels) { + if (l instanceof JCPatternCaseLabel patternLabel) { + for (Type component : components(selector.type)) { + patternSet.add(makePatternDescription(component, patternLabel.pat)); + } + } else if (l instanceof JCConstantCaseLabel constantLabel) { + if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN)) { + Object value = ((JCLiteral) constantLabel.expr).value; + booleanLiterals.remove(value); + } else { + Symbol s = TreeInfo.symbol(constantLabel.expr); + if (s != null && s.isEnum()) { + enum2Constants.computeIfAbsent(s.owner, x -> { + Set result = new HashSet<>(); + s.owner.members() + .getSymbols(sym -> sym.kind == Kind.VAR && sym.isEnum()) + .forEach(result::add); + return result; + }).remove(s); + } + } + } + } + } + + if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { + return true; + } + + for (Entry> e : enum2Constants.entrySet()) { + if (e.getValue().isEmpty()) { + patternSet.add(new BindingPattern(e.getKey().type)); + } + } + Set patterns = patternSet; + boolean useHashes = true; + try { + boolean repeat = true; + while (repeat) { + Set updatedPatterns; + updatedPatterns = reduceBindingPatterns(selector.type, patterns); + updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); + updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + repeat = !updatedPatterns.equals(patterns); + if (checkCovered(selector.type, patterns)) { + return true; + } + if (!repeat) { + //there may be situation like: + //class B permits S1, S2 + //patterns: R(S1, B), R(S2, S2) + //this might be joined to R(B, S2), as B could be rewritten to S2 + //but hashing in reduceNestedPatterns will not allow that + //disable the use of hashing, and use subtyping in + //reduceNestedPatterns to handle situations like this: + repeat = useHashes; + useHashes = false; + } else { + //if a reduction happened, make sure hashing in reduceNestedPatterns + //is enabled, as the hashing speeds up the process significantly: + useHashes = true; + } + patterns = updatedPatterns; + } + return checkCovered(selector.type, patterns); + } catch (CompletionFailure cf) { + chk.completionError(selector.pos(), cf); + return true; //error recovery + } + } + + private boolean checkCovered(Type seltype, Iterable patterns) { + for (Type seltypeComponent : components(seltype)) { + for (PatternDescription pd : patterns) { + if(isBpCovered(seltypeComponent, pd)) { + return true; + } + } + } + return false; + } + + private List components(Type seltype) { + return switch (seltype.getTag()) { + case CLASS -> { + if (seltype.isCompound()) { + if (seltype.isIntersection()) { + yield ((Type.IntersectionClassType) seltype).getComponents() + .stream() + .flatMap(t -> components(t).stream()) + .collect(List.collector()); + } + yield List.nil(); + } + yield List.of(types.erasure(seltype)); + } + case TYPEVAR -> components(((TypeVar) seltype).getUpperBound()); + default -> List.of(types.erasure(seltype)); + }; + } + + /* In a set of patterns, search for a sub-set of binding patterns that + * in combination exhaust their sealed supertype. If such a sub-set + * is found, it is removed, and replaced with a binding pattern + * for the sealed supertype. + */ + private Set reduceBindingPatterns(Type selectorType, Set patterns) { + Set existingBindings = patterns.stream() + .filter(pd -> pd instanceof BindingPattern) + .map(pd -> ((BindingPattern) pd).type.tsym) + .collect(Collectors.toSet()); + + for (PatternDescription pdOne : patterns) { + if (pdOne instanceof BindingPattern bpOne) { + Set toAdd = new HashSet<>(); + + for (Type sup : types.directSupertypes(bpOne.type)) { + ClassSymbol clazz = (ClassSymbol) types.erasure(sup).tsym; + + clazz.complete(); + + if (clazz.isSealed() && clazz.isAbstract() && + //if a binding pattern for clazz already exists, no need to analyze it again: + !existingBindings.contains(clazz)) { + ListBuffer bindings = new ListBuffer<>(); + //do not reduce to types unrelated to the selector type: + Type clazzErasure = types.erasure(clazz.type); + if (components(selectorType).stream() + .map(types::erasure) + .noneMatch(c -> types.isSubtype(clazzErasure, c))) { + continue; + } + + Set permitted = allPermittedSubTypes(clazz, csym -> { + Type instantiated; + if (csym.type.allparams().isEmpty()) { + instantiated = csym.type; + } else { + instantiated = infer.instantiatePatternType(selectorType, csym); + } + + return instantiated != null && types.isCastable(selectorType, instantiated); + }); + + for (PatternDescription pdOther : patterns) { + if (pdOther instanceof BindingPattern bpOther) { + Set currentPermittedSubTypes = + allPermittedSubTypes(bpOther.type.tsym, s -> true); + + PERMITTED: for (Iterator it = permitted.iterator(); it.hasNext();) { + Symbol perm = it.next(); + + for (Symbol currentPermitted : currentPermittedSubTypes) { + if (types.isSubtype(types.erasure(currentPermitted.type), + types.erasure(perm.type))) { + it.remove(); + continue PERMITTED; + } + } + if (types.isSubtype(types.erasure(perm.type), + types.erasure(bpOther.type))) { + it.remove(); + } + } + } + } + + if (permitted.isEmpty()) { + toAdd.add(new BindingPattern(clazz.type)); + } + } + } + + if (!toAdd.isEmpty()) { + Set newPatterns = new HashSet<>(patterns); + newPatterns.addAll(toAdd); + return newPatterns; + } + } + } + return patterns; + } + + private Set allPermittedSubTypes(TypeSymbol root, Predicate accept) { + Set permitted = new HashSet<>(); + List permittedSubtypesClosure = baseClasses(root); + + while (permittedSubtypesClosure.nonEmpty()) { + ClassSymbol current = permittedSubtypesClosure.head; + + permittedSubtypesClosure = permittedSubtypesClosure.tail; + + current.complete(); + + if (current.isSealed() && current.isAbstract()) { + for (Type t : current.getPermittedSubclasses()) { + ClassSymbol csym = (ClassSymbol) t.tsym; + + if (accept.test(csym)) { + permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); + permitted.add(csym); + } + } + } + } + + return permitted; + } + + private List baseClasses(TypeSymbol root) { + if (root instanceof ClassSymbol clazz) { + return List.of(clazz); + } else if (root instanceof TypeVariableSymbol tvar) { + ListBuffer result = new ListBuffer<>(); + for (Type bound : tvar.getBounds()) { + result.appendList(baseClasses(bound.tsym)); + } + return result.toList(); + } else { + return List.nil(); + } + } + + /* Among the set of patterns, find sub-set of patterns such: + * $record($prefix$, $nested, $suffix$) + * Where $record, $prefix$ and $suffix$ is the same for each pattern + * in the set, and the patterns only differ in one "column" in + * the $nested pattern. + * Then, the set of $nested patterns is taken, and passed recursively + * to reduceNestedPatterns and to reduceBindingPatterns, to + * simplify the pattern. If that succeeds, the original found sub-set + * of patterns is replaced with a new set of patterns of the form: + * $record($prefix$, $resultOfReduction, $suffix$) + * + * useHashes: when true, patterns will be subject to exact equivalence; + * when false, two binding patterns will be considered equivalent + * if one of them is more generic than the other one; + * when false, the processing will be significantly slower, + * as pattern hashes cannot be used to speed up the matching process + */ + private Set reduceNestedPatterns(Set patterns, + boolean useHashes) { + /* implementation note: + * finding a sub-set of patterns that only differ in a single + * column is time-consuming task, so this method speeds it up by: + * - group the patterns by their record class + * - for each column (nested pattern) do: + * -- group patterns by their hash + * -- in each such by-hash group, find sub-sets that only differ in + * the chosen column, and then call reduceBindingPatterns and reduceNestedPatterns + * on patterns in the chosen column, as described above + */ + var groupByRecordClass = + patterns.stream() + .filter(pd -> pd instanceof RecordPattern) + .map(pd -> (RecordPattern) pd) + .collect(groupingBy(pd -> (ClassSymbol) pd.recordType.tsym)); + + for (var e : groupByRecordClass.entrySet()) { + int nestedPatternsCount = e.getKey().getRecordComponents().size(); + Set current = new HashSet<>(e.getValue()); + + for (int mismatchingCandidate = 0; + mismatchingCandidate < nestedPatternsCount; + mismatchingCandidate++) { + int mismatchingCandidateFin = mismatchingCandidate; + var groupEquivalenceCandidates = + current + .stream() + //error recovery, ignore patterns with incorrect number of nested patterns: + .filter(pd -> pd.nested.length == nestedPatternsCount) + .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); + for (var candidates : groupEquivalenceCandidates.values()) { + var candidatesArr = candidates.toArray(RecordPattern[]::new); + + for (int firstCandidate = 0; + firstCandidate < candidatesArr.length; + firstCandidate++) { + RecordPattern rpOne = candidatesArr[firstCandidate]; + ListBuffer join = new ListBuffer<>(); + + join.append(rpOne); + + NEXT_PATTERN: for (int nextCandidate = 0; + nextCandidate < candidatesArr.length; + nextCandidate++) { + if (firstCandidate == nextCandidate) { + continue; + } + + RecordPattern rpOther = candidatesArr[nextCandidate]; + if (rpOne.recordType.tsym == rpOther.recordType.tsym) { + for (int i = 0; i < rpOne.nested.length; i++) { + if (i != mismatchingCandidate) { + if (!rpOne.nested[i].equals(rpOther.nested[i])) { + if (useHashes || + //when not using hashes, + //check if rpOne.nested[i] is + //a subtype of rpOther.nested[i]: + !(rpOne.nested[i] instanceof BindingPattern bpOne) || + !(rpOther.nested[i] instanceof BindingPattern bpOther) || + !types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { + continue NEXT_PATTERN; + } + } + } + } + join.append(rpOther); + } + } + + var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); + var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); + + updatedPatterns = reduceRecordPatterns(updatedPatterns); + updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); + updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); + + if (!nestedPatterns.equals(updatedPatterns)) { + if (useHashes) { + current.removeAll(join); + } + + for (PatternDescription nested : updatedPatterns) { + PatternDescription[] newNested = + Arrays.copyOf(rpOne.nested, rpOne.nested.length); + newNested[mismatchingCandidateFin] = nested; + current.add(new RecordPattern(rpOne.recordType(), + rpOne.fullComponentTypes(), + newNested)); + } + } + } + } + } + + if (!current.equals(new HashSet<>(e.getValue()))) { + Set result = new HashSet<>(patterns); + result.removeAll(e.getValue()); + result.addAll(current); + return result; + } + } + return patterns; + } + + /* In the set of patterns, find those for which, given: + * $record($nested1, $nested2, ...) + * all the $nestedX pattern cover the given record component, + * and replace those with a simple binding pattern over $record. + */ + private Set reduceRecordPatterns(Set patterns) { + var newPatterns = new HashSet(); + boolean modified = false; + for (PatternDescription pd : patterns) { + if (pd instanceof RecordPattern rpOne) { + PatternDescription reducedPattern = reduceRecordPattern(rpOne); + if (reducedPattern != rpOne) { + newPatterns.add(reducedPattern); + modified = true; + continue; + } + } + newPatterns.add(pd); + } + return modified ? newPatterns : patterns; + } + + private PatternDescription reduceRecordPattern(PatternDescription pattern) { + if (pattern instanceof RecordPattern rpOne) { + Type[] componentType = rpOne.fullComponentTypes(); + //error recovery, ignore patterns with incorrect number of nested patterns: + if (componentType.length != rpOne.nested.length) { + return pattern; + } + PatternDescription[] reducedNestedPatterns = null; + boolean covered = true; + for (int i = 0; i < componentType.length; i++) { + PatternDescription newNested = reduceRecordPattern(rpOne.nested[i]); + if (newNested != rpOne.nested[i]) { + if (reducedNestedPatterns == null) { + reducedNestedPatterns = Arrays.copyOf(rpOne.nested, rpOne.nested.length); + } + reducedNestedPatterns[i] = newNested; + } + + covered &= checkCovered(componentType[i], List.of(newNested)); + } + if (covered) { + return new BindingPattern(rpOne.recordType); + } else if (reducedNestedPatterns != null) { + return new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); + } + } + return pattern; + } + + private Set removeCoveredRecordPatterns(Set patterns) { + Set existingBindings = patterns.stream() + .filter(pd -> pd instanceof BindingPattern) + .map(pd -> ((BindingPattern) pd).type.tsym) + .collect(Collectors.toSet()); + Set result = new HashSet<>(patterns); + + for (Iterator it = result.iterator(); it.hasNext();) { + PatternDescription pd = it.next(); + if (pd instanceof RecordPattern rp && existingBindings.contains(rp.recordType.tsym)) { + it.remove(); + } + } + + return result; + } + + private boolean isBpCovered(Type componentType, PatternDescription newNested) { + if (newNested instanceof BindingPattern bp) { + Type seltype = types.erasure(componentType); + Type pattype = types.erasure(bp.type); + + return seltype.isPrimitive() ? + types.isUnconditionallyExact(seltype, pattype) : + (bp.type.isPrimitive() && types.isUnconditionallyExact(types.unboxedType(seltype), bp.type)) || types.isSubtype(seltype, pattype); + } + return false; + } + + sealed interface PatternDescription { } + public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { + if (pattern instanceof JCBindingPattern binding) { + Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) + ? selectorType : binding.type; + return new BindingPattern(type); + } else if (pattern instanceof JCRecordPattern record) { + Type[] componentTypes; + + if (!record.type.isErroneous()) { + componentTypes = ((ClassSymbol) record.type.tsym).getRecordComponents() + .map(r -> types.memberType(record.type, r)) + .toArray(s -> new Type[s]); + } + else { + componentTypes = record.nested.map(t -> types.createErrorType(t.type)).toArray(s -> new Type[s]);; + } + + PatternDescription[] nestedDescriptions = + new PatternDescription[record.nested.size()]; + int i = 0; + for (List it = record.nested; + it.nonEmpty(); + it = it.tail, i++) { + Type componentType = i < componentTypes.length ? componentTypes[i] + : syms.errType; + nestedDescriptions[i] = makePatternDescription(types.erasure(componentType), it.head); + } + return new RecordPattern(record.type, componentTypes, nestedDescriptions); + } else if (pattern instanceof JCAnyPattern) { + return new BindingPattern(selectorType); + } else { + throw Assert.error(); + } + } + record BindingPattern(Type type) implements PatternDescription { + @Override + public int hashCode() { + return type.tsym.hashCode(); + } + @Override + public boolean equals(Object o) { + return o instanceof BindingPattern other && + type.tsym == other.type.tsym; + } + @Override + public String toString() { + return type.tsym + " _"; + } + } + record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { + + public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { + this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); + } + + @Override + public int hashCode() { + return _hashCode; + } + + @Override + public boolean equals(Object o) { + return o instanceof RecordPattern other && + recordType.tsym == other.recordType.tsym && + Arrays.equals(nested, other.nested); + } + + public int hashCode(int excludeComponent) { + return hashCode(excludeComponent, recordType, nested); + } + + public static int hashCode(int excludeComponent, Type recordType, PatternDescription... nested) { + int hash = 5; + hash = 41 * hash + recordType.tsym.hashCode(); + for (int i = 0; i < nested.length; i++) { + if (i != excludeComponent) { + hash = 41 * hash + nested[i].hashCode(); + } + } + return hash; + } + @Override + public String toString() { + return recordType.tsym + "(" + Arrays.stream(nested) + .map(pd -> pd.toString()) + .collect(Collectors.joining(", ")) + ")"; + } + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java index 3bbd007c66a..e74aed6a357 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Flow.java @@ -27,11 +27,7 @@ package com.sun.tools.javac.comp; -import java.util.Map; -import java.util.Map.Entry; import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; import java.util.function.Consumer; import com.sun.source.tree.LambdaExpressionTree.BodyKind; @@ -51,20 +47,12 @@ import com.sun.tools.javac.tree.JCTree.*; import static com.sun.tools.javac.code.Flags.*; import static com.sun.tools.javac.code.Flags.BLOCK; -import com.sun.tools.javac.code.Kinds.Kind; import static com.sun.tools.javac.code.Kinds.Kind.*; -import com.sun.tools.javac.code.Type.TypeVar; import static com.sun.tools.javac.code.TypeTag.BOOLEAN; import static com.sun.tools.javac.code.TypeTag.VOID; import com.sun.tools.javac.resources.CompilerProperties.Fragments; import static com.sun.tools.javac.tree.JCTree.Tag.*; import com.sun.tools.javac.util.JCDiagnostic.Fragment; -import java.util.Arrays; -import java.util.Iterator; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import static java.util.stream.Collectors.groupingBy; /** This pass implements dataflow analysis for Java programs though * different AST visitor steps. Liveness analysis (see AliveAnalyzer) checks that @@ -213,8 +201,8 @@ public class Flow { private TreeMaker make; private final Resolve rs; private final JCDiagnostic.Factory diags; + private final ExhaustivenessComputer exhaustiveness; private Env attrEnv; - private final Infer infer; public static Flow instance(Context context) { Flow instance = context.get(flowKey); @@ -336,10 +324,9 @@ public class Flow { syms = Symtab.instance(context); types = Types.instance(context); chk = Check.instance(context); - infer = Infer.instance(context); rs = Resolve.instance(context); diags = JCDiagnostic.Factory.instance(context); - Source source = Source.instance(context); + exhaustiveness = ExhaustivenessComputer.instance(context); } /** @@ -709,7 +696,7 @@ public class Flow { tree.isExhaustive = tree.hasUnconditionalPattern || TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases); if (exhaustiveSwitch) { - tree.isExhaustive |= exhausts(tree.selector, tree.cases); + tree.isExhaustive |= exhaustiveness.exhausts(tree.selector, tree.cases); if (!tree.isExhaustive) { log.error(tree, Errors.NotExhaustiveStatement); } @@ -748,7 +735,7 @@ public class Flow { TreeInfo.isErrorEnumSwitch(tree.selector, tree.cases)) { tree.isExhaustive = true; } else { - tree.isExhaustive = exhausts(tree.selector, tree.cases); + tree.isExhaustive = exhaustiveness.exhausts(tree.selector, tree.cases); } if (!tree.isExhaustive) { @@ -758,429 +745,6 @@ public class Flow { alive = alive.or(resolveYields(tree, prevPendingExits)); } - private boolean exhausts(JCExpression selector, List cases) { - Set patternSet = new HashSet<>(); - Map> enum2Constants = new HashMap<>(); - Set booleanLiterals = new HashSet<>(Set.of(0, 1)); - for (JCCase c : cases) { - if (!TreeInfo.unguardedCase(c)) - continue; - - for (var l : c.labels) { - if (l instanceof JCPatternCaseLabel patternLabel) { - for (Type component : components(selector.type)) { - patternSet.add(makePatternDescription(component, patternLabel.pat)); - } - } else if (l instanceof JCConstantCaseLabel constantLabel) { - if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN)) { - Object value = ((JCLiteral) constantLabel.expr).value; - booleanLiterals.remove(value); - } else { - Symbol s = TreeInfo.symbol(constantLabel.expr); - if (s != null && s.isEnum()) { - enum2Constants.computeIfAbsent(s.owner, x -> { - Set result = new HashSet<>(); - s.owner.members() - .getSymbols(sym -> sym.kind == Kind.VAR && sym.isEnum()) - .forEach(result::add); - return result; - }).remove(s); - } - } - } - } - } - - if (types.unboxedTypeOrType(selector.type).hasTag(TypeTag.BOOLEAN) && booleanLiterals.isEmpty()) { - return true; - } - - for (Entry> e : enum2Constants.entrySet()) { - if (e.getValue().isEmpty()) { - patternSet.add(new BindingPattern(e.getKey().type)); - } - } - Set patterns = patternSet; - boolean useHashes = true; - try { - boolean repeat = true; - while (repeat) { - Set updatedPatterns; - updatedPatterns = reduceBindingPatterns(selector.type, patterns); - updatedPatterns = reduceNestedPatterns(updatedPatterns, useHashes); - updatedPatterns = reduceRecordPatterns(updatedPatterns); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - repeat = !updatedPatterns.equals(patterns); - if (checkCovered(selector.type, patterns)) { - return true; - } - if (!repeat) { - //there may be situation like: - //class B permits S1, S2 - //patterns: R(S1, B), R(S2, S2) - //this might be joined to R(B, S2), as B could be rewritten to S2 - //but hashing in reduceNestedPatterns will not allow that - //disable the use of hashing, and use subtyping in - //reduceNestedPatterns to handle situations like this: - repeat = useHashes; - useHashes = false; - } else { - //if a reduction happened, make sure hashing in reduceNestedPatterns - //is enabled, as the hashing speeds up the process significantly: - useHashes = true; - } - patterns = updatedPatterns; - } - return checkCovered(selector.type, patterns); - } catch (CompletionFailure cf) { - chk.completionError(selector.pos(), cf); - return true; //error recovery - } - } - - private boolean checkCovered(Type seltype, Iterable patterns) { - for (Type seltypeComponent : components(seltype)) { - for (PatternDescription pd : patterns) { - if(isBpCovered(seltypeComponent, pd)) { - return true; - } - } - } - return false; - } - - private List components(Type seltype) { - return switch (seltype.getTag()) { - case CLASS -> { - if (seltype.isCompound()) { - if (seltype.isIntersection()) { - yield ((Type.IntersectionClassType) seltype).getComponents() - .stream() - .flatMap(t -> components(t).stream()) - .collect(List.collector()); - } - yield List.nil(); - } - yield List.of(types.erasure(seltype)); - } - case TYPEVAR -> components(((TypeVar) seltype).getUpperBound()); - default -> List.of(types.erasure(seltype)); - }; - } - - /* In a set of patterns, search for a sub-set of binding patterns that - * in combination exhaust their sealed supertype. If such a sub-set - * is found, it is removed, and replaced with a binding pattern - * for the sealed supertype. - */ - private Set reduceBindingPatterns(Type selectorType, Set patterns) { - Set existingBindings = patterns.stream() - .filter(pd -> pd instanceof BindingPattern) - .map(pd -> ((BindingPattern) pd).type.tsym) - .collect(Collectors.toSet()); - - for (PatternDescription pdOne : patterns) { - if (pdOne instanceof BindingPattern bpOne) { - Set toAdd = new HashSet<>(); - - for (Type sup : types.directSupertypes(bpOne.type)) { - ClassSymbol clazz = (ClassSymbol) types.erasure(sup).tsym; - - clazz.complete(); - - if (clazz.isSealed() && clazz.isAbstract() && - //if a binding pattern for clazz already exists, no need to analyze it again: - !existingBindings.contains(clazz)) { - ListBuffer bindings = new ListBuffer<>(); - //do not reduce to types unrelated to the selector type: - Type clazzErasure = types.erasure(clazz.type); - if (components(selectorType).stream() - .map(types::erasure) - .noneMatch(c -> types.isSubtype(clazzErasure, c))) { - continue; - } - - Set permitted = allPermittedSubTypes(clazz, csym -> { - Type instantiated; - if (csym.type.allparams().isEmpty()) { - instantiated = csym.type; - } else { - instantiated = infer.instantiatePatternType(selectorType, csym); - } - - return instantiated != null && types.isCastable(selectorType, instantiated); - }); - - for (PatternDescription pdOther : patterns) { - if (pdOther instanceof BindingPattern bpOther) { - Set currentPermittedSubTypes = - allPermittedSubTypes(bpOther.type.tsym, s -> true); - - PERMITTED: for (Iterator it = permitted.iterator(); it.hasNext();) { - Symbol perm = it.next(); - - for (Symbol currentPermitted : currentPermittedSubTypes) { - if (types.isSubtype(types.erasure(currentPermitted.type), - types.erasure(perm.type))) { - it.remove(); - continue PERMITTED; - } - } - if (types.isSubtype(types.erasure(perm.type), - types.erasure(bpOther.type))) { - it.remove(); - } - } - } - } - - if (permitted.isEmpty()) { - toAdd.add(new BindingPattern(clazz.type)); - } - } - } - - if (!toAdd.isEmpty()) { - Set newPatterns = new HashSet<>(patterns); - newPatterns.addAll(toAdd); - return newPatterns; - } - } - } - return patterns; - } - - private Set allPermittedSubTypes(TypeSymbol root, Predicate accept) { - Set permitted = new HashSet<>(); - List permittedSubtypesClosure = baseClasses(root); - - while (permittedSubtypesClosure.nonEmpty()) { - ClassSymbol current = permittedSubtypesClosure.head; - - permittedSubtypesClosure = permittedSubtypesClosure.tail; - - current.complete(); - - if (current.isSealed() && current.isAbstract()) { - for (Type t : current.getPermittedSubclasses()) { - ClassSymbol csym = (ClassSymbol) t.tsym; - - if (accept.test(csym)) { - permittedSubtypesClosure = permittedSubtypesClosure.prepend(csym); - permitted.add(csym); - } - } - } - } - - return permitted; - } - - private List baseClasses(TypeSymbol root) { - if (root instanceof ClassSymbol clazz) { - return List.of(clazz); - } else if (root instanceof TypeVariableSymbol tvar) { - ListBuffer result = new ListBuffer<>(); - for (Type bound : tvar.getBounds()) { - result.appendList(baseClasses(bound.tsym)); - } - return result.toList(); - } else { - return List.nil(); - } - } - - /* Among the set of patterns, find sub-set of patterns such: - * $record($prefix$, $nested, $suffix$) - * Where $record, $prefix$ and $suffix$ is the same for each pattern - * in the set, and the patterns only differ in one "column" in - * the $nested pattern. - * Then, the set of $nested patterns is taken, and passed recursively - * to reduceNestedPatterns and to reduceBindingPatterns, to - * simplify the pattern. If that succeeds, the original found sub-set - * of patterns is replaced with a new set of patterns of the form: - * $record($prefix$, $resultOfReduction, $suffix$) - * - * useHashes: when true, patterns will be subject to exact equivalence; - * when false, two binding patterns will be considered equivalent - * if one of them is more generic than the other one; - * when false, the processing will be significantly slower, - * as pattern hashes cannot be used to speed up the matching process - */ - private Set reduceNestedPatterns(Set patterns, - boolean useHashes) { - /* implementation note: - * finding a sub-set of patterns that only differ in a single - * column is time-consuming task, so this method speeds it up by: - * - group the patterns by their record class - * - for each column (nested pattern) do: - * -- group patterns by their hash - * -- in each such by-hash group, find sub-sets that only differ in - * the chosen column, and then call reduceBindingPatterns and reduceNestedPatterns - * on patterns in the chosen column, as described above - */ - var groupByRecordClass = - patterns.stream() - .filter(pd -> pd instanceof RecordPattern) - .map(pd -> (RecordPattern) pd) - .collect(groupingBy(pd -> (ClassSymbol) pd.recordType.tsym)); - - for (var e : groupByRecordClass.entrySet()) { - int nestedPatternsCount = e.getKey().getRecordComponents().size(); - Set current = new HashSet<>(e.getValue()); - - for (int mismatchingCandidate = 0; - mismatchingCandidate < nestedPatternsCount; - mismatchingCandidate++) { - int mismatchingCandidateFin = mismatchingCandidate; - var groupEquivalenceCandidates = - current - .stream() - //error recovery, ignore patterns with incorrect number of nested patterns: - .filter(pd -> pd.nested.length == nestedPatternsCount) - .collect(groupingBy(pd -> useHashes ? pd.hashCode(mismatchingCandidateFin) : 0)); - for (var candidates : groupEquivalenceCandidates.values()) { - var candidatesArr = candidates.toArray(RecordPattern[]::new); - - for (int firstCandidate = 0; - firstCandidate < candidatesArr.length; - firstCandidate++) { - RecordPattern rpOne = candidatesArr[firstCandidate]; - ListBuffer join = new ListBuffer<>(); - - join.append(rpOne); - - NEXT_PATTERN: for (int nextCandidate = 0; - nextCandidate < candidatesArr.length; - nextCandidate++) { - if (firstCandidate == nextCandidate) { - continue; - } - - RecordPattern rpOther = candidatesArr[nextCandidate]; - if (rpOne.recordType.tsym == rpOther.recordType.tsym) { - for (int i = 0; i < rpOne.nested.length; i++) { - if (i != mismatchingCandidate) { - if (!rpOne.nested[i].equals(rpOther.nested[i])) { - if (useHashes || - //when not using hashes, - //check if rpOne.nested[i] is - //a subtype of rpOther.nested[i]: - !(rpOne.nested[i] instanceof BindingPattern bpOne) || - !(rpOther.nested[i] instanceof BindingPattern bpOther) || - !types.isSubtype(types.erasure(bpOne.type), types.erasure(bpOther.type))) { - continue NEXT_PATTERN; - } - } - } - } - join.append(rpOther); - } - } - - var nestedPatterns = join.stream().map(rp -> rp.nested[mismatchingCandidateFin]).collect(Collectors.toSet()); - var updatedPatterns = reduceNestedPatterns(nestedPatterns, useHashes); - - updatedPatterns = reduceRecordPatterns(updatedPatterns); - updatedPatterns = removeCoveredRecordPatterns(updatedPatterns); - updatedPatterns = reduceBindingPatterns(rpOne.fullComponentTypes()[mismatchingCandidateFin], updatedPatterns); - - if (!nestedPatterns.equals(updatedPatterns)) { - if (useHashes) { - current.removeAll(join); - } - - for (PatternDescription nested : updatedPatterns) { - PatternDescription[] newNested = - Arrays.copyOf(rpOne.nested, rpOne.nested.length); - newNested[mismatchingCandidateFin] = nested; - current.add(new RecordPattern(rpOne.recordType(), - rpOne.fullComponentTypes(), - newNested)); - } - } - } - } - } - - if (!current.equals(new HashSet<>(e.getValue()))) { - Set result = new HashSet<>(patterns); - result.removeAll(e.getValue()); - result.addAll(current); - return result; - } - } - return patterns; - } - - /* In the set of patterns, find those for which, given: - * $record($nested1, $nested2, ...) - * all the $nestedX pattern cover the given record component, - * and replace those with a simple binding pattern over $record. - */ - private Set reduceRecordPatterns(Set patterns) { - var newPatterns = new HashSet(); - boolean modified = false; - for (PatternDescription pd : patterns) { - if (pd instanceof RecordPattern rpOne) { - PatternDescription reducedPattern = reduceRecordPattern(rpOne); - if (reducedPattern != rpOne) { - newPatterns.add(reducedPattern); - modified = true; - continue; - } - } - newPatterns.add(pd); - } - return modified ? newPatterns : patterns; - } - - private PatternDescription reduceRecordPattern(PatternDescription pattern) { - if (pattern instanceof RecordPattern rpOne) { - Type[] componentType = rpOne.fullComponentTypes(); - //error recovery, ignore patterns with incorrect number of nested patterns: - if (componentType.length != rpOne.nested.length) { - return pattern; - } - PatternDescription[] reducedNestedPatterns = null; - boolean covered = true; - for (int i = 0; i < componentType.length; i++) { - PatternDescription newNested = reduceRecordPattern(rpOne.nested[i]); - if (newNested != rpOne.nested[i]) { - if (reducedNestedPatterns == null) { - reducedNestedPatterns = Arrays.copyOf(rpOne.nested, rpOne.nested.length); - } - reducedNestedPatterns[i] = newNested; - } - - covered &= checkCovered(componentType[i], List.of(newNested)); - } - if (covered) { - return new BindingPattern(rpOne.recordType); - } else if (reducedNestedPatterns != null) { - return new RecordPattern(rpOne.recordType, rpOne.fullComponentTypes(), reducedNestedPatterns); - } - } - return pattern; - } - - private Set removeCoveredRecordPatterns(Set patterns) { - Set existingBindings = patterns.stream() - .filter(pd -> pd instanceof BindingPattern) - .map(pd -> ((BindingPattern) pd).type.tsym) - .collect(Collectors.toSet()); - Set result = new HashSet<>(patterns); - - for (Iterator it = result.iterator(); it.hasNext();) { - PatternDescription pd = it.next(); - if (pd instanceof RecordPattern rp && existingBindings.contains(rp.recordType.tsym)) { - it.remove(); - } - } - - return result; - } - public void visitTry(JCTry tree) { ListBuffer prevPendingExits = pendingExits; pendingExits = new ListBuffer<>(); @@ -1326,18 +890,6 @@ public class Flow { } } - private boolean isBpCovered(Type componentType, PatternDescription newNested) { - if (newNested instanceof BindingPattern bp) { - Type seltype = types.erasure(componentType); - Type pattype = types.erasure(bp.type); - - return seltype.isPrimitive() ? - types.isUnconditionallyExact(seltype, pattype) : - (bp.type.isPrimitive() && types.isUnconditionallyExact(types.unboxedType(seltype), bp.type)) || types.isSubtype(seltype, pattype); - } - return false; - } - /** * This pass implements the second step of the dataflow analysis, namely * the exception analysis. This is to ensure that every checked exception that is @@ -3473,93 +3025,4 @@ public class Flow { } } - sealed interface PatternDescription { } - public PatternDescription makePatternDescription(Type selectorType, JCPattern pattern) { - if (pattern instanceof JCBindingPattern binding) { - Type type = !selectorType.isPrimitive() && types.isSubtype(selectorType, binding.type) - ? selectorType : binding.type; - return new BindingPattern(type); - } else if (pattern instanceof JCRecordPattern record) { - Type[] componentTypes; - - if (!record.type.isErroneous()) { - componentTypes = ((ClassSymbol) record.type.tsym).getRecordComponents() - .map(r -> types.memberType(record.type, r)) - .toArray(s -> new Type[s]); - } - else { - componentTypes = record.nested.map(t -> types.createErrorType(t.type)).toArray(s -> new Type[s]);; - } - - PatternDescription[] nestedDescriptions = - new PatternDescription[record.nested.size()]; - int i = 0; - for (List it = record.nested; - it.nonEmpty(); - it = it.tail, i++) { - Type componentType = i < componentTypes.length ? componentTypes[i] - : syms.errType; - nestedDescriptions[i] = makePatternDescription(types.erasure(componentType), it.head); - } - return new RecordPattern(record.type, componentTypes, nestedDescriptions); - } else if (pattern instanceof JCAnyPattern) { - return new BindingPattern(selectorType); - } else { - throw Assert.error(); - } - } - record BindingPattern(Type type) implements PatternDescription { - @Override - public int hashCode() { - return type.tsym.hashCode(); - } - @Override - public boolean equals(Object o) { - return o instanceof BindingPattern other && - type.tsym == other.type.tsym; - } - @Override - public String toString() { - return type.tsym + " _"; - } - } - record RecordPattern(Type recordType, int _hashCode, Type[] fullComponentTypes, PatternDescription... nested) implements PatternDescription { - - public RecordPattern(Type recordType, Type[] fullComponentTypes, PatternDescription[] nested) { - this(recordType, hashCode(-1, recordType, nested), fullComponentTypes, nested); - } - - @Override - public int hashCode() { - return _hashCode; - } - - @Override - public boolean equals(Object o) { - return o instanceof RecordPattern other && - recordType.tsym == other.recordType.tsym && - Arrays.equals(nested, other.nested); - } - - public int hashCode(int excludeComponent) { - return hashCode(excludeComponent, recordType, nested); - } - - public static int hashCode(int excludeComponent, Type recordType, PatternDescription... nested) { - int hash = 5; - hash = 41 * hash + recordType.tsym.hashCode(); - for (int i = 0; i < nested.length; i++) { - if (i != excludeComponent) { - hash = 41 * hash + nested[i].hashCode(); - } - } - return hash; - } - @Override - public String toString() { - return recordType.tsym + "(" + Arrays.stream(nested) - .map(pd -> pd.toString()) - .collect(Collectors.joining(", ")) + ")"; - } - } }